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 repo_url = embed_token_in_url(&opts.repo_url, token, oauth_type)?;
53    let extra_config = vec!["-c".to_owned(), "http.sslVerify=false".to_owned()];
54    let args = build_clone_args(opts, &repo_url, dest, &extra_config);
55    run_git_clone(&args, &[token.as_str()]).await
56}
57
58pub async fn clone_https_public(opts: &CloneOpts, dest: &Path) -> Result<()> {
59    let extra_config = vec!["-c".to_owned(), "http.sslVerify=false".to_owned()];
60    let args = build_clone_args(opts, &opts.repo_url, dest, &extra_config);
61    run_git_clone(&args, &[]).await
62}
63
64pub fn embed_token_in_url(url: &str, token: &str, provider: &str) -> Result<String> {
65    match provider {
66        "BITBUCKET" => embed_credentials_in_url(url, "x-token-auth", token),
67        "" => {
68            let mut parsed = Url::parse(url).context("parsing URL")?;
69            parsed
70                .set_username(token)
71                .map_err(|()| anyhow::anyhow!("failed to set the token as the user"))?;
72            parsed
73                .set_password(None)
74                .map_err(|()| anyhow::anyhow!("failed to drop the password"))?;
75            Ok(parsed.to_string())
76        }
77        _ => embed_credentials_in_url(url, "oauth2", token),
78    }
79}
80
81pub fn embed_credentials_in_url(raw_url: &str, user: &str, password: &str) -> Result<String> {
82    let mut url = Url::parse(raw_url).context("parsing URL")?;
83    url.set_username(user)
84        .map_err(|()| anyhow::anyhow!("failed to set username"))?;
85    url.set_password(Some(password))
86        .map_err(|()| anyhow::anyhow!("failed to set password"))?;
87    Ok(url.to_string())
88}
89
90/// Build git config args for HTTPS operations (equivalent to Python's `get_https_git_config_args`).
91#[must_use]
92pub fn build_https_config_args(
93    follow_redirects: bool,
94    is_pat: bool,
95    token: Option<&str>,
96    disable_credential_prompt: bool,
97) -> Vec<String> {
98    let mut args = vec![
99        "-c".to_owned(),
100        "http.sslVerify=false".to_owned(),
101        "-c".to_owned(),
102        format!("http.followRedirects={follow_redirects}"),
103    ];
104    if is_pat {
105        if let Some(t) = token {
106            let encoded = base64::engine::general_purpose::STANDARD.encode(format!(":{t}"));
107            args.extend([
108                "-c".to_owned(),
109                format!("http.extraHeader=Authorization: Basic {encoded}"),
110            ]);
111        }
112    }
113    if disable_credential_prompt {
114        args.extend(["-c".to_owned(), "credential.helper=".to_owned()]);
115    }
116    args
117}
118
119/// Format URL with credentials embedded (equivalent to Python's `format_url`).
120#[allow(clippy::too_many_arguments)]
121pub fn format_https_url(
122    repo_url: &str,
123    user: Option<&str>,
124    password: Option<&str>,
125    token: Option<&str>,
126    provider: Option<&str>,
127    is_pat: bool,
128) -> Result<String> {
129    let mut parsed = Url::parse(repo_url).context("parsing URL")?;
130    // Re-quote the path
131    let path = parsed.path().to_owned();
132    parsed.set_path(&path);
133
134    if is_pat {
135        return Ok(parsed.to_string());
136    }
137
138    if let Some(t) = token {
139        let user_part = match provider {
140            Some("BITBUCKET") => "x-token-auth",
141            Some("") | None => "",
142            Some(_) => "oauth2",
143        };
144        if user_part.is_empty() {
145            // No provider: use token as username, no password
146            parsed
147                .set_username(t)
148                .map_err(|()| anyhow::anyhow!("set username"))?;
149            parsed
150                .set_password(None)
151                .map_err(|()| anyhow::anyhow!("set password"))?;
152        } else {
153            // Use username/password separately to avoid percent-encoding the colon
154            parsed
155                .set_username(user_part)
156                .map_err(|()| anyhow::anyhow!("set username"))?;
157            parsed
158                .set_password(Some(t))
159                .map_err(|()| anyhow::anyhow!("set password"))?;
160        }
161        return Ok(parsed.to_string());
162    }
163
164    if let (Some(u), Some(p)) = (user, password) {
165        parsed
166            .set_username(u)
167            .map_err(|()| anyhow::anyhow!("set username"))?;
168        parsed
169            .set_password(Some(p))
170            .map_err(|()| anyhow::anyhow!("set password"))?;
171        return Ok(parsed.to_string());
172    }
173
174    Ok(parsed.to_string())
175}
176
177async fn run_git_clone(args: &[String], secrets: &[&str]) -> Result<()> {
178    reason::run(Command::new("git").args(args), "HTTPS", secrets).await
179}
180
181#[cfg(test)]
182mod tests {
183    use super::{embed_token_in_url, format_https_url};
184
185    #[test]
186    fn a_credential_with_no_provider_carries_the_token_with_no_user() {
187        assert_eq!(
188            embed_token_in_url("https://gitlab.com/a/b.git", "tok", "").unwrap(),
189            "https://tok@gitlab.com/a/b.git"
190        );
191    }
192
193    #[test]
194    fn the_provider_names_the_user_even_off_its_own_domain() {
195        assert_eq!(
196            embed_token_in_url("https://git.acme.com/a/b.git", "tok", "BITBUCKET").unwrap(),
197            "https://x-token-auth:tok@git.acme.com/a/b.git"
198        );
199        assert_eq!(
200            embed_token_in_url("https://gitlab.com/a/b.git", "tok", "GITLAB").unwrap(),
201            "https://oauth2:tok@gitlab.com/a/b.git"
202        );
203    }
204
205    #[test]
206    fn the_token_replaces_whatever_the_url_already_carried() {
207        assert_eq!(
208            embed_token_in_url("https://olduser:oldpass@host/a/b.git", "tok", "").unwrap(),
209            "https://tok@host/a/b.git"
210        );
211    }
212
213    #[test]
214    fn the_provider_is_matched_exactly() {
215        assert_eq!(
216            format_https_url(
217                "https://host/a/b.git",
218                None,
219                None,
220                Some("tok"),
221                Some("Bitbucket"),
222                false
223            )
224            .unwrap(),
225            "https://oauth2:tok@host/a/b.git"
226        );
227    }
228
229    #[test]
230    fn an_empty_provider_formats_the_url_like_no_provider_at_all() {
231        let url = "https://gitlab.com/a/b.git";
232        let no_provider = format_https_url(url, None, None, Some("tok"), None, false).unwrap();
233        assert_eq!(no_provider, "https://tok@gitlab.com/a/b.git");
234        assert_eq!(
235            format_https_url(url, None, None, Some("tok"), Some(""), false).unwrap(),
236            no_provider
237        );
238    }
239}