use std::net::SocketAddr;
use std::path::Path;
use super::helper::COMMAND;
use super::socks::USERNAME;
use super::{PROXY_ENV, TOKEN_ENV};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GitEnv {
pub set: Vec<(String, String)>,
pub remove: Vec<&'static str>,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct SshSettings<'a> {
pub git_ssh_command: Option<&'a str>,
pub core_ssh_command: Option<&'a str>,
pub git_ssh: Option<&'a str>,
}
pub fn build(
proxy: SocketAddr,
token: &str,
exe: &Path,
ssh: SshSettings<'_>,
existing_count: Option<&str>,
) -> Result<GitEnv, String> {
let exe = exe
.to_str()
.ok_or_else(|| format!("gitaway's path isn't valid UTF-8: {}", exe.display()))?;
let helper = format!("{} {COMMAND}", shell_quote(exe));
let proxy_url = format!("socks5h://{USERNAME}:{token}@{proxy}");
let config = [
("http.proxy".to_owned(), proxy_url),
(
format!("credential.socks5h://{proxy}.helper"),
String::new(),
),
];
let first = match existing_count.map(str::trim) {
None | Some("") => 0,
Some(count) => count
.parse::<usize>()
.map_err(|_| format!("GIT_CONFIG_COUNT is `{count}`, not a number"))?,
};
let mut set = Vec::new();
for (i, (key, value)) in config.iter().cloned().enumerate() {
set.push((format!("GIT_CONFIG_KEY_{}", first + i), key));
set.push((format!("GIT_CONFIG_VALUE_{}", first + i), value));
}
set.push((
"GIT_CONFIG_COUNT".to_owned(),
(first + config.len()).to_string(),
));
let proxy_command = format!("ProxyCommand={helper} %h %p");
set.push((
"GIT_SSH_COMMAND".to_owned(),
format!("{} -o {}", ssh_base(ssh), shell_quote(&proxy_command)),
));
set.push((PROXY_ENV.to_owned(), proxy.to_string()));
set.push((TOKEN_ENV.to_owned(), token.to_owned()));
Ok(GitEnv {
set,
remove: vec!["NO_PROXY", "no_proxy"],
})
}
fn ssh_base(ssh: SshSettings<'_>) -> String {
fn non_empty(value: Option<&str>) -> Option<&str> {
value.filter(|v| !v.trim().is_empty())
}
if let Some(command) = non_empty(ssh.git_ssh_command) {
return command.to_owned();
}
if let Some(command) = non_empty(ssh.core_ssh_command) {
return command.to_owned();
}
if let Some(program) = non_empty(ssh.git_ssh) {
return shell_quote(program);
}
"ssh".to_owned()
}
pub fn shell_quote(s: &str) -> String {
let plain = !s.is_empty()
&& s.bytes()
.all(|b| b.is_ascii_alphanumeric() || b"/-_.,:=@%+".contains(&b));
if plain {
s.to_owned()
} else {
format!("'{}'", s.replace('\'', r"'\''"))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn proxy() -> SocketAddr {
"127.0.0.1:41234".parse().unwrap()
}
fn value<'a>(env: &'a GitEnv, name: &str) -> &'a str {
env.set
.iter()
.find(|(k, _)| k == name)
.map(|(_, v)| v.as_str())
.unwrap_or_else(|| panic!("{name} not set"))
}
fn config(env: &GitEnv) -> Vec<(String, String)> {
let count: usize = value(env, "GIT_CONFIG_COUNT").parse().unwrap();
(0..count)
.filter(|i| {
env.set
.iter()
.any(|(k, _)| k == &format!("GIT_CONFIG_KEY_{i}"))
})
.map(|i| {
(
value(env, &format!("GIT_CONFIG_KEY_{i}")).to_owned(),
value(env, &format!("GIT_CONFIG_VALUE_{i}")).to_owned(),
)
})
.collect()
}
#[test]
fn git_gets_the_proxy_for_http_and_ssh() {
let env = build(
proxy(),
"tok",
Path::new("/usr/local/bin/gitaway"),
SshSettings::default(),
None,
)
.unwrap();
assert_eq!(
config(&env),
[
(
"http.proxy".into(),
"socks5h://gitaway:tok@127.0.0.1:41234".into()
),
(
"credential.socks5h://127.0.0.1:41234.helper".into(),
String::new()
),
]
);
assert_eq!(
value(&env, "GIT_SSH_COMMAND"),
"ssh -o 'ProxyCommand=/usr/local/bin/gitaway __proxy-connect %h %p'"
);
assert_eq!(value(&env, PROXY_ENV), "127.0.0.1:41234");
assert_eq!(value(&env, TOKEN_ENV), "tok");
assert_eq!(env.remove, ["NO_PROXY", "no_proxy"]);
}
#[test]
fn existing_config_from_the_environment_is_kept() {
let env = build(
proxy(),
"tok",
Path::new("/g"),
SshSettings::default(),
Some("2"),
)
.unwrap();
assert_eq!(value(&env, "GIT_CONFIG_COUNT"), "4");
assert_eq!(value(&env, "GIT_CONFIG_KEY_2"), "http.proxy");
assert!(
!env.set.iter().any(|(k, _)| k == "GIT_CONFIG_KEY_0"),
"the user's own are untouched"
);
assert!(
build(
proxy(),
"tok",
Path::new("/g"),
SshSettings::default(),
Some("x")
)
.is_err()
);
}
#[test]
fn the_users_ssh_command_is_kept_in_gits_order() {
let ssh = |git_ssh_command, core_ssh_command, git_ssh| {
let settings = SshSettings {
git_ssh_command,
core_ssh_command,
git_ssh,
};
let env = build(proxy(), "t", Path::new("/g"), settings, None).unwrap();
value(&env, "GIT_SSH_COMMAND")
.split(" -o 'ProxyCommand")
.next()
.unwrap()
.to_owned()
};
assert_eq!(
ssh(Some("ssh -i ~/.ssh/id_work"), Some("ssh -v"), Some("/x")),
"ssh -i ~/.ssh/id_work"
);
assert_eq!(ssh(None, Some("ssh -v"), Some("/x")), "ssh -v");
assert_eq!(ssh(None, None, Some("/opt/my ssh")), "'/opt/my ssh'");
assert_eq!(ssh(Some(""), None, None), "ssh");
}
#[cfg(unix)]
#[test]
fn paths_with_spaces_and_quotes_survive_two_shells() {
let env = build(
proxy(),
"t",
Path::new("/Users/alice/My Tools/it's/gitaway"),
SshSettings::default(),
None,
)
.unwrap();
let command = value(&env, "GIT_SSH_COMMAND");
let after_git_shell = run_sh(&format!(
"printf %s {}",
command.split_once(" -o ").unwrap().1
));
assert_eq!(
after_git_shell,
r"ProxyCommand='/Users/alice/My Tools/it'\''s/gitaway' __proxy-connect %h %p"
);
let program = after_git_shell
.strip_prefix("ProxyCommand=")
.unwrap()
.strip_suffix(" __proxy-connect %h %p")
.unwrap();
assert_eq!(
run_sh(&format!("printf %s {program}")),
"/Users/alice/My Tools/it's/gitaway"
);
}
#[cfg(unix)]
fn run_sh(script: &str) -> String {
let out = std::process::Command::new("sh")
.args(["-c", script])
.output()
.unwrap();
String::from_utf8(out.stdout).unwrap()
}
#[test]
fn shell_quoting() {
assert_eq!(shell_quote("ssh"), "ssh");
assert_eq!(shell_quote("/usr/bin/ssh"), "/usr/bin/ssh");
assert_eq!(shell_quote("a b"), "'a b'");
assert_eq!(shell_quote("it's"), r"'it'\''s'");
assert_eq!(shell_quote(""), "''");
assert_eq!(shell_quote("$(rm -rf /)"), "'$(rm -rf /)'");
}
}