fluidattacks_core_git/clone/
https.rs1use 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#[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#[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 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 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 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}