Skip to main content

fluidattacks_core_git/clone/
https.rs

1use anyhow::{Context, Result};
2use base64::Engine;
3use std::path::Path;
4use tokio::process::Command;
5use url::Url;
6
7use super::{build_clone_args, reason, CloneOpts};
8use crate::types::CredentialKind;
9
10pub async fn clone_https(opts: &CloneOpts, dest: &Path) -> Result<()> {
11    let CredentialKind::Https { user, password } = &opts
12        .credentials
13        .as_ref()
14        .context("missing credentials")?
15        .kind
16    else {
17        anyhow::bail!("expected HTTPS credentials");
18    };
19    let repo_url = embed_credentials_in_url(&opts.repo_url, user, password)?;
20
21    let extra_config = vec!["-c".to_owned(), "http.sslVerify=false".to_owned()];
22    let args = build_clone_args(opts, &repo_url, dest, &extra_config);
23    run_git_clone(&args, &[password.as_str()]).await
24}
25
26pub async fn clone_https_token(opts: &CloneOpts, dest: &Path) -> Result<()> {
27    let CredentialKind::Token {
28        token,
29        is_pat,
30        oauth_type: _,
31    } = &opts
32        .credentials
33        .as_ref()
34        .context("missing credentials")?
35        .kind
36    else {
37        anyhow::bail!("expected Token credentials");
38    };
39
40    if *is_pat {
41        let encoded = base64::engine::general_purpose::STANDARD.encode(format!(":{token}"));
42        let extra_config = vec![
43            "-c".to_owned(),
44            "http.sslVerify=false".to_owned(),
45            "-c".to_owned(),
46            format!("http.extraHeader=Authorization: Basic {encoded}"),
47        ];
48        let args = build_clone_args(opts, &opts.repo_url, dest, &extra_config);
49        return run_git_clone(&args, &[token.as_str(), &encoded]).await;
50    }
51
52    let user = if opts.repo_url.to_lowercase().contains("bitbucket") {
53        "x-token-auth"
54    } else {
55        "oauth2"
56    };
57
58    let repo_url = embed_credentials_in_url(&opts.repo_url, user, token)?;
59    let extra_config = vec!["-c".to_owned(), "http.sslVerify=false".to_owned()];
60    let args = build_clone_args(opts, &repo_url, dest, &extra_config);
61    run_git_clone(&args, &[token.as_str()]).await
62}
63
64pub async fn clone_https_public(opts: &CloneOpts, dest: &Path) -> Result<()> {
65    let extra_config = vec!["-c".to_owned(), "http.sslVerify=false".to_owned()];
66    let args = build_clone_args(opts, &opts.repo_url, dest, &extra_config);
67    run_git_clone(&args, &[]).await
68}
69
70pub fn embed_credentials_in_url(raw_url: &str, user: &str, password: &str) -> Result<String> {
71    let mut url = Url::parse(raw_url).context("parsing URL")?;
72    url.set_username(user)
73        .map_err(|()| anyhow::anyhow!("failed to set username"))?;
74    url.set_password(Some(password))
75        .map_err(|()| anyhow::anyhow!("failed to set password"))?;
76    Ok(url.to_string())
77}
78
79/// Build git config args for HTTPS operations (equivalent to Python's `get_https_git_config_args`).
80#[must_use]
81pub fn build_https_config_args(
82    follow_redirects: bool,
83    is_pat: bool,
84    token: Option<&str>,
85    disable_credential_prompt: bool,
86) -> Vec<String> {
87    let mut args = vec![
88        "-c".to_owned(),
89        "http.sslVerify=false".to_owned(),
90        "-c".to_owned(),
91        format!("http.followRedirects={follow_redirects}"),
92    ];
93    if is_pat {
94        if let Some(t) = token {
95            let encoded = base64::engine::general_purpose::STANDARD.encode(format!(":{t}"));
96            args.extend([
97                "-c".to_owned(),
98                format!("http.extraHeader=Authorization: Basic {encoded}"),
99            ]);
100        }
101    }
102    if disable_credential_prompt {
103        args.extend(["-c".to_owned(), "credential.helper=".to_owned()]);
104    }
105    args
106}
107
108/// Format URL with credentials embedded (equivalent to Python's `format_url`).
109#[allow(clippy::too_many_arguments)]
110pub fn format_https_url(
111    repo_url: &str,
112    user: Option<&str>,
113    password: Option<&str>,
114    token: Option<&str>,
115    provider: Option<&str>,
116    is_pat: bool,
117) -> Result<String> {
118    let mut parsed = Url::parse(repo_url).context("parsing URL")?;
119    // Re-quote the path
120    let path = parsed.path().to_owned();
121    parsed.set_path(&path);
122
123    if is_pat {
124        return Ok(parsed.to_string());
125    }
126
127    if let Some(t) = token {
128        let user_part = match provider {
129            Some(p) if p.eq_ignore_ascii_case("BITBUCKET") => "x-token-auth",
130            Some(_) => "oauth2",
131            None => "",
132        };
133        if user_part.is_empty() {
134            // No provider: use token as username, no password
135            parsed
136                .set_username(t)
137                .map_err(|()| anyhow::anyhow!("set username"))?;
138            parsed
139                .set_password(None)
140                .map_err(|()| anyhow::anyhow!("set password"))?;
141        } else {
142            // Use username/password separately to avoid percent-encoding the colon
143            parsed
144                .set_username(user_part)
145                .map_err(|()| anyhow::anyhow!("set username"))?;
146            parsed
147                .set_password(Some(t))
148                .map_err(|()| anyhow::anyhow!("set password"))?;
149        }
150        return Ok(parsed.to_string());
151    }
152
153    if let (Some(u), Some(p)) = (user, password) {
154        parsed
155            .set_username(u)
156            .map_err(|()| anyhow::anyhow!("set username"))?;
157        parsed
158            .set_password(Some(p))
159            .map_err(|()| anyhow::anyhow!("set password"))?;
160        return Ok(parsed.to_string());
161    }
162
163    Ok(parsed.to_string())
164}
165
166async fn run_git_clone(args: &[String], secrets: &[&str]) -> Result<()> {
167    reason::run(Command::new("git").args(args), "HTTPS", secrets).await
168}