Skip to main content

fluidattacks_core_git/
remote.rs

1use anyhow::{Context, Result};
2use base64::Engine;
3use std::time::Duration;
4use tokio::process::Command;
5
6use super::types::{CredentialKind, Credentials, LsRemoteResult};
7
8use super::aws;
9use crate::clone::https::embed_credentials_in_url;
10use crate::clone::ssh::{normalize_ssh_url, setup_ssh_env};
11
12pub async fn ls_remote(
13    url: &str,
14    branch: &str,
15    credentials: Option<&Credentials>,
16    follow_redirects: bool,
17) -> Result<LsRemoteResult> {
18    match credentials.map(|c| &c.kind) {
19        Some(CredentialKind::Ssh { key }) => {
20            ls_remote_ssh(url, branch, key, follow_redirects).await
21        }
22        Some(CredentialKind::AwsRole {
23            arn,
24            external_id,
25            via,
26        }) => ls_remote_codecommit(url, branch, arn, external_id, via.as_deref()).await,
27        Some(CredentialKind::Https { user, password }) => {
28            let repo_url = embed_credentials_in_url(url, user, password)?;
29            ls_remote_https(&repo_url, branch, follow_redirects, &[]).await
30        }
31        Some(CredentialKind::Token {
32            token,
33            is_pat,
34            oauth_type: _,
35        }) => {
36            if *is_pat {
37                let encoded = base64::engine::general_purpose::STANDARD.encode(format!(":{token}"));
38                let extra_config = vec![
39                    "-c".to_owned(),
40                    format!("http.extraHeader=Authorization: Basic {encoded}"),
41                ];
42                ls_remote_https(url, branch, follow_redirects, &extra_config).await
43            } else {
44                let user = if url.to_lowercase().contains("bitbucket") {
45                    "x-token-auth"
46                } else {
47                    "oauth2"
48                };
49                let repo_url = embed_credentials_in_url(url, user, token)?;
50                ls_remote_https(&repo_url, branch, follow_redirects, &[]).await
51            }
52        }
53        None => ls_remote_https(url, branch, follow_redirects, &[]).await,
54    }
55}
56
57async fn ls_remote_ssh(
58    url: &str,
59    branch: &str,
60    key_b64: &str,
61    follow_redirects: bool,
62) -> Result<LsRemoteResult> {
63    let key_data = base64::engine::general_purpose::STANDARD
64        .decode(key_b64)
65        .context("decoding SSH key")?;
66
67    let key_file = tempfile::Builder::new()
68        .prefix("melts-ssh-key-")
69        .tempfile()
70        .context("creating SSH key file")?;
71
72    std::fs::write(key_file.path(), &key_data).context("writing SSH key")?;
73
74    #[cfg(unix)]
75    {
76        use std::os::unix::fs::PermissionsExt;
77        std::fs::set_permissions(key_file.path(), std::fs::Permissions::from_mode(0o400))
78            .context("setting SSH key permissions")?;
79    }
80
81    let git_ssh_command = setup_ssh_env(key_file.path());
82    let normalized_url = normalize_ssh_url(url);
83
84    let mut args: Vec<String> = Vec::new();
85    if follow_redirects {
86        args.extend(["-c".to_owned(), "http.followRedirects=true".to_owned()]);
87    }
88    args.extend([
89        "ls-remote".to_owned(),
90        "--".to_owned(),
91        normalized_url,
92        branch.to_owned(),
93    ]);
94
95    let output = tokio::time::timeout(
96        Duration::from_secs(20),
97        Command::new("git")
98            .args(&args)
99            .env("GIT_SSH_COMMAND", &git_ssh_command)
100            .kill_on_drop(true)
101            .output(),
102    )
103    .await
104    .context("git ls-remote timed out")?
105    .context("running git ls-remote (SSH)")?;
106
107    Ok(parse_ls_remote_output(&output))
108}
109
110async fn ls_remote_https(
111    url: &str,
112    branch: &str,
113    follow_redirects: bool,
114    extra_config: &[String],
115) -> Result<LsRemoteResult> {
116    let mut args = vec!["-c".to_owned(), "http.sslVerify=false".to_owned()];
117
118    if follow_redirects {
119        args.extend(["-c".to_owned(), "http.followRedirects=true".to_owned()]);
120    }
121
122    args.extend(extra_config.iter().cloned());
123    args.extend([
124        "ls-remote".to_owned(),
125        "--".to_owned(),
126        url.to_owned(),
127        branch.to_owned(),
128    ]);
129
130    let output = tokio::time::timeout(
131        Duration::from_secs(20),
132        Command::new("git").args(&args).kill_on_drop(true).output(),
133    )
134    .await
135    .context("git ls-remote timed out")?
136    .context("running git ls-remote (HTTPS)")?;
137
138    Ok(parse_ls_remote_output(&output))
139}
140
141async fn ls_remote_codecommit(
142    url: &str,
143    branch: &str,
144    arn: &str,
145    external_id: &str,
146    via: Option<&str>,
147) -> Result<LsRemoteResult> {
148    let creds = aws::assume_role(arn, external_id, url, via).await?;
149
150    let args = vec![
151        "-c".to_owned(),
152        "http.sslVerify=false".to_owned(),
153        "ls-remote".to_owned(),
154        "--".to_owned(),
155        url.to_owned(),
156        branch.to_owned(),
157    ];
158
159    let output = tokio::time::timeout(
160        Duration::from_secs(20),
161        Command::new("git")
162            .args(&args)
163            .env("AWS_ACCESS_KEY_ID", &creds.access_key)
164            .env("AWS_SECRET_ACCESS_KEY", &creds.secret_key)
165            .env("AWS_SESSION_TOKEN", &creds.session_token)
166            .env("AWS_DEFAULT_REGION", &creds.region)
167            .kill_on_drop(true)
168            .output(),
169    )
170    .await
171    .context("git ls-remote timed out")?
172    .context("running git ls-remote (CodeCommit)")?;
173
174    Ok(parse_ls_remote_output(&output))
175}
176
177fn parse_ls_remote_output(output: &std::process::Output) -> LsRemoteResult {
178    if !output.status.success() {
179        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
180        return LsRemoteResult {
181            commit: None,
182            error: Some(stderr),
183        };
184    }
185
186    let stdout = String::from_utf8_lossy(&output.stdout);
187    let commit = stdout
188        .lines()
189        .next()
190        .and_then(|line| line.split('\t').next())
191        .map(|s| s.trim().to_owned())
192        .filter(|s| !s.is_empty());
193
194    LsRemoteResult {
195        commit,
196        error: None,
197    }
198}