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