use std::process::Command;
use crate::permalink::is_safe_host;
use strop_core::worker::{CancelToken, FailureKind};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EffectiveHostError {
InvalidHost,
Spawn(String),
Failed(String),
NoHostname,
Process(strop_core::worker::Failure),
Unresolved,
}
impl std::fmt::Display for EffectiveHostError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EffectiveHostError::InvalidHost => {
write!(f, "not a valid hostname or alias")
}
EffectiveHostError::Spawn(message) => write!(f, "cannot run ssh: {message}"),
EffectiveHostError::Failed(message) => write!(f, "ssh -G failed: {message}"),
EffectiveHostError::NoHostname => write!(f, "ssh -G reported no hostname"),
EffectiveHostError::Process(failure) => write!(f, "ssh -G: {}", failure.message),
EffectiveHostError::Unresolved => {
write!(f, "SSH alias has no configured web hostname; check ssh -G")
}
}
}
}
impl std::error::Error for EffectiveHostError {}
pub fn effective_host(
remote: &crate::permalink::AliasRemote,
token: &CancelToken,
) -> Result<String, EffectiveHostError> {
let mut command = Command::new("ssh");
if let Some(user) = &remote.user {
command.arg("-l").arg(user);
}
if let Some(port) = remote.port {
command.arg("-p").arg(port.to_string());
}
effective_host_via(&mut command, remote.host(), token)
}
fn effective_host_via(
command: &mut Command,
host: &str,
token: &CancelToken,
) -> Result<String, EffectiveHostError> {
let host = is_safe_host(host)
.then_some(host)
.ok_or(EffectiveHostError::InvalidHost)?;
command.arg("-G").arg(host);
let output = strop_core::process::capture(command, token).map_err(|failure| {
if failure.kind == FailureKind::Spawn {
EffectiveHostError::Spawn(failure.message)
} else {
EffectiveHostError::Process(failure)
}
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(EffectiveHostError::Failed(stderr.trim().to_string()));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let hostname = parse_effective_hostname(&stdout).ok_or(EffectiveHostError::NoHostname)?;
if hostname == host && !hostname.contains('.') {
return Err(EffectiveHostError::Unresolved);
}
Ok(hostname)
}
pub fn parse_effective_hostname(output: &str) -> Option<String> {
output.lines().find_map(|line| {
let mut parts = line.split_whitespace();
match (parts.next(), parts.next()) {
(Some("hostname"), Some(host)) if is_safe_host(host) => Some(host.to_string()),
_ => None,
}
})
}
#[cfg(test)]
mod tests {
use super::*;
fn resolve(program: &str, host: &str) -> Result<String, EffectiveHostError> {
let (tx, rx) = std::sync::mpsc::channel();
let program = program.to_owned();
let host = host.to_owned();
let handle = strop_core::worker::spawn(
"ssh-test",
move |outcome| {
tx.send(outcome).unwrap();
},
move |token| {
strop_core::worker::Outcome::Success(effective_host_via(
&mut Command::new(program),
&host,
&token,
))
},
);
let strop_core::worker::Outcome::Success(result) = rx.recv().unwrap() else {
panic!("worker failed")
};
drop(handle);
result
}
#[cfg(unix)]
fn fake_ssh(dir: &std::path::Path, hostname: &str) -> std::path::PathBuf {
use std::os::unix::fs::PermissionsExt;
let path = dir.join("fake-ssh");
let script = format!(
"#!/bin/sh\nprintf '%s\\n' \"$@\" > \"$0.argv\"\nprintf 'user git\\nhostname {hostname}\\nport 22\\n'\n",
);
std::fs::write(&path, script).unwrap();
let mut permissions = std::fs::metadata(&path).unwrap().permissions();
permissions.set_mode(0o755);
std::fs::set_permissions(&path, permissions).unwrap();
path
}
#[test]
fn parses_effective_hostname_from_g_output() {
let output = "user git\nhostname bbgithub.dev.bloomberg.com\nport 22\n";
assert_eq!(
parse_effective_hostname(output).as_deref(),
Some("bbgithub.dev.bloomberg.com")
);
assert_eq!(
parse_effective_hostname("hostname\thost.example.com").as_deref(),
Some("host.example.com")
);
assert_eq!(parse_effective_hostname("user git\nport 22"), None);
assert_eq!(parse_effective_hostname(""), None);
}
#[test]
fn refuses_non_hostname_shaped_output() {
assert_eq!(parse_effective_hostname("hostname -oProxy"), None);
assert_eq!(parse_effective_hostname("hostname "), None);
}
#[cfg(unix)]
#[test]
fn effective_host_spawns_one_safe_argv_element() {
let dir = tempfile::tempdir().unwrap();
let ssh = fake_ssh(dir.path(), "bbgithub.dev.bloomberg.com");
let argv_file = dir.path().join("fake-ssh.argv");
let host = resolve(ssh.to_str().unwrap(), "bbgithub").unwrap();
assert_eq!(host, "bbgithub.dev.bloomberg.com");
assert_eq!(
std::fs::read_to_string(&argv_file).unwrap(),
"-G\nbbgithub\n",
"argv must be exactly [-G, bbgithub]"
);
}
#[cfg(unix)]
#[test]
fn option_shaped_hosts_never_spawn() {
let dir = tempfile::tempdir().unwrap();
let ssh = fake_ssh(dir.path(), "should-not-run");
let argv_file = dir.path().join("fake-ssh.argv");
for host in ["-oProxyCommand=evil", "", "git@bb", "bb github"] {
assert_eq!(
resolve(ssh.to_str().unwrap(), host),
Err(EffectiveHostError::InvalidHost),
"should refuse: {host:?}"
);
}
assert!(!argv_file.exists(), "no process may run for invalid hosts");
}
#[cfg(unix)]
#[test]
fn failing_ssh_reports_stderr() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
let path = root.join("failing-ssh");
std::fs::write(
&path,
"#!/bin/sh\necho 'Bad configuration option.' >&2\nexit 255\n",
)
.unwrap();
let mut permissions = std::fs::metadata(&path).unwrap().permissions();
permissions.set_mode(0o755);
std::fs::set_permissions(&path, permissions).unwrap();
match resolve(path.to_str().unwrap(), "bbgithub") {
Err(EffectiveHostError::Failed(message)) => {
assert!(message.contains("Bad configuration option."), "{message}")
}
other => panic!("expected Failed, got {other:?}"),
}
}
#[test]
fn missing_program_is_spawn_failure() {
let missing = "/nonexistent/strop-test-ssh";
match resolve(missing, "bbgithub") {
Err(EffectiveHostError::Spawn(_)) => {}
other => panic!("expected Spawn, got {other:?}"),
}
}
}