#![cfg(target_os = "linux")]
use std::io::Write;
use std::net::{TcpListener, TcpStream};
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
use fresh::services::authority::TerminalWrapper;
use fresh::services::remote::ConnectionParams;
fn is_file(p: &Path) -> bool {
p.is_file()
}
fn resolve(name: &str, fallbacks: &[&str]) -> Option<PathBuf> {
if let Some(path) = std::env::var_os("PATH") {
for dir in std::env::split_paths(&path) {
let cand = dir.join(name);
if is_file(&cand) {
return Some(cand);
}
}
}
fallbacks
.iter()
.map(PathBuf::from)
.find(|cand| is_file(cand))
}
fn keygen(keygen_bin: &Path, path: &Path) {
let status = Command::new(keygen_bin)
.args(["-t", "ed25519", "-q", "-N", ""])
.arg("-f")
.arg(path)
.status()
.expect("run ssh-keygen");
assert!(status.success(), "ssh-keygen failed for {path:?}");
}
fn set_mode(path: &Path, mode: u32) {
std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode)).unwrap();
}
fn free_port() -> u16 {
TcpListener::bind("127.0.0.1:0")
.expect("bind ephemeral port")
.local_addr()
.unwrap()
.port()
}
fn wait_for_listen(port: u16, timeout: Duration) -> bool {
let start = Instant::now();
while start.elapsed() < timeout {
if TcpStream::connect(("127.0.0.1", port)).is_ok() {
return true;
}
std::thread::sleep(Duration::from_millis(50));
}
false
}
struct KillOnDrop(Child);
impl Drop for KillOnDrop {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
fn current_user() -> Option<String> {
std::env::var("USER")
.ok()
.or_else(|| std::env::var("LOGNAME").ok())
.or_else(|| {
let out = Command::new("id").arg("-un").output().ok()?;
String::from_utf8(out.stdout)
.ok()
.map(|s| s.trim().to_string())
})
.filter(|s| !s.is_empty())
}
#[test]
fn ssh_terminal_wrapper_runs_shell_on_remote_host() {
let (Some(ssh), Some(sshd), Some(ssh_keygen)) = (
resolve("ssh", &[]),
resolve(
"sshd",
&["/usr/sbin/sshd", "/sbin/sshd", "/usr/local/sbin/sshd"],
),
resolve("ssh-keygen", &[]),
) else {
eprintln!("skipping: ssh/sshd/ssh-keygen not installed");
return;
};
let _ = ssh;
let Some(user) = current_user() else {
eprintln!("skipping: could not determine current user");
return;
};
let tmp = tempfile::tempdir().expect("tempdir");
let t = tmp.path();
let hostkey = t.join("hostkey");
let id = t.join("id");
let authorized = t.join("authorized_keys");
let config = t.join("sshd_config");
let work = t.join("work");
std::fs::create_dir(&work).unwrap();
let dot_ssh = t.join(".ssh");
std::fs::create_dir(&dot_ssh).unwrap();
set_mode(&dot_ssh, 0o700);
keygen(&ssh_keygen, &hostkey);
keygen(&ssh_keygen, &id);
std::fs::copy(t.join("id.pub"), &authorized).unwrap();
set_mode(&authorized, 0o600);
let port = free_port();
std::fs::write(
&config,
format!(
"Port {port}\n\
ListenAddress 127.0.0.1\n\
HostKey {hostkey}\n\
PidFile {pid}\n\
AuthorizedKeysFile {authorized}\n\
StrictModes no\n\
UsePAM no\n\
PasswordAuthentication no\n\
PubkeyAuthentication yes\n",
hostkey = hostkey.display(),
pid = t.join("sshd.pid").display(),
authorized = authorized.display(),
),
)
.unwrap();
let log = t.join("sshd.log");
let logf = std::fs::File::create(&log).unwrap();
let sshd_child = Command::new(&sshd)
.arg("-D") .arg("-e") .arg("-f")
.arg(&config)
.stdout(Stdio::from(logf.try_clone().unwrap()))
.stderr(Stdio::from(logf))
.spawn()
.expect("spawn sshd");
let _sshd_guard = KillOnDrop(sshd_child);
assert!(
wait_for_listen(port, Duration::from_secs(10)),
"sshd never listened on {port}.\nsshd log:\n{}",
std::fs::read_to_string(&log).unwrap_or_default()
);
let params = ConnectionParams {
user: Some(user.clone()),
host: "127.0.0.1".to_string(),
port: Some(port),
identity_file: Some(id.clone()),
extra_args: Vec::new(),
};
let work_str = work.to_string_lossy().into_owned();
let wrapper = TerminalWrapper::ssh(¶ms, Some(&work_str));
assert_eq!(
wrapper.command, "ssh",
"remote terminal must launch via ssh"
);
assert!(
wrapper.manages_cwd,
"ssh terminal re-parents the shell, so it manages its own cwd"
);
let mut child = Command::new(&wrapper.command)
.args(&wrapper.args)
.env("HOME", t)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn ssh terminal wrapper");
child
.stdin
.take()
.unwrap()
.write_all(b"pwd\nprintf 'CONN=%s\\n' \"$SSH_CONNECTION\"\nexit\n")
.unwrap();
let out = child.wait_with_output().expect("wait for ssh wrapper");
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
let conn = stdout
.lines()
.find(|l| l.starts_with("CONN="))
.unwrap_or_else(|| panic!("no CONN line.\nstdout:\n{stdout}\nstderr:\n{stderr}"));
assert!(
conn.len() > "CONN=".len() && conn.contains("127.0.0.1"),
"SSH_CONNECTION empty — the terminal shell did NOT run through SSH.\n\
line={conn:?}\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(
stdout.contains(&work_str),
"shell did not start in the workspace dir {work_str:?}.\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
}