1
1
mirror of https://github.com/wez/wezterm.git synced 2024-12-23 05:12:40 +03:00

Add some initial tests to wezterm-ssh, ignoring read/write of file as those are hanging

This commit is contained in:
Chip Senkbeil 2021-09-24 13:57:05 -05:00 committed by Wez Furlong
parent 23fd145ceb
commit fca3bd6904
7 changed files with 811 additions and 3 deletions

207
Cargo.lock generated
View File

@ -76,7 +76,7 @@ dependencies = [
"encoding_rs",
"flate2",
"glyph-names",
"itertools",
"itertools 0.8.2",
"lazy_static",
"libc",
"log",
@ -133,6 +133,20 @@ dependencies = [
"libloading 0.7.0",
]
[[package]]
name = "assert_fs"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d7b349de9bde9383966c8d3be1103623620ca34d2c43a41b82360e552661007"
dependencies = [
"doc-comment",
"globwalk",
"predicates",
"predicates-core",
"predicates-tree",
"tempfile",
]
[[package]]
name = "async-channel"
version = "1.6.1"
@ -1028,6 +1042,12 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
[[package]]
name = "difflib"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]]
name = "digest"
version = "0.8.1"
@ -1320,6 +1340,15 @@ dependencies = [
"miniz_oxide 0.4.4",
]
[[package]]
name = "float-cmp"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
dependencies = [
"num-traits",
]
[[package]]
name = "flume"
version = "0.10.9"
@ -1799,6 +1828,30 @@ dependencies = [
"takeable-option",
]
[[package]]
name = "globset"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd"
dependencies = [
"aho-corasick",
"bstr 0.2.16",
"fnv",
"log",
"regex",
]
[[package]]
name = "globwalk"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
dependencies = [
"bitflags",
"ignore",
"walkdir",
]
[[package]]
name = "gloo-timers"
version = "0.2.1"
@ -1990,6 +2043,24 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "ignore"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d"
dependencies = [
"crossbeam-utils",
"globset",
"lazy_static",
"log",
"memchr",
"regex",
"same-file",
"thread_local",
"walkdir",
"winapi-util",
]
[[package]]
name = "image"
version = "0.23.14"
@ -2019,6 +2090,15 @@ dependencies = [
"hashbrown 0.11.2",
]
[[package]]
name = "indoc"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5a75aeaaef0ce18b58056d306c27b07436fbb34b8816c53094b76dd81803136"
dependencies = [
"unindent",
]
[[package]]
name = "inotify"
version = "0.7.1"
@ -2081,6 +2161,15 @@ dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "0.4.8"
@ -2714,6 +2803,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db1b4163932b207be6e3a06412aed4d84cca40dc087419f231b3a38cba2ca8e9"
[[package]]
name = "normalize-line-endings"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
[[package]]
name = "notify"
version = "4.0.17"
@ -3013,7 +3108,7 @@ checksum = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252"
dependencies = [
"lock_api 0.3.4",
"parking_lot_core 0.6.2",
"rustc_version",
"rustc_version 0.2.3",
]
[[package]]
@ -3037,7 +3132,7 @@ dependencies = [
"cloudabi",
"libc",
"redox_syscall 0.1.57",
"rustc_version",
"rustc_version 0.2.3",
"smallvec 0.6.14",
"winapi 0.3.9",
]
@ -3288,6 +3383,36 @@ version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
[[package]]
name = "predicates"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c143348f141cc87aab5b950021bac6145d0e5ae754b0591de23244cee42c9308"
dependencies = [
"difflib",
"float-cmp",
"itertools 0.10.1",
"normalize-line-endings",
"predicates-core",
"regex",
]
[[package]]
name = "predicates-core"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57e35a3326b75e49aa85f5dc6ec15b41108cf5aee58eabb1f274dd18b73c2451"
[[package]]
name = "predicates-tree"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7dd0fd014130206c9352efbdc92be592751b2b9274dff685348341082c6ea3d"
dependencies = [
"predicates-core",
"treeline",
]
[[package]]
name = "pretty_assertions"
version = "0.6.1"
@ -3763,6 +3888,19 @@ dependencies = [
"petgraph",
]
[[package]]
name = "rstest"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2288c66aeafe3b2ed227c981f364f9968fa952ef0b30e84ada4486e7ee24d00a"
dependencies = [
"cfg-if 1.0.0",
"proc-macro2",
"quote",
"rustc_version 0.4.0",
"syn",
]
[[package]]
name = "rustc-demangle"
version = "0.1.21"
@ -3784,6 +3922,15 @@ dependencies = [
"semver 0.9.0",
]
[[package]]
name = "rustc_version"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [
"semver 1.0.4",
]
[[package]]
name = "ryu"
version = "1.0.5"
@ -3877,6 +4024,12 @@ dependencies = [
"semver-parser 0.10.2",
]
[[package]]
name = "semver"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012"
[[package]]
name = "semver-parser"
version = "0.7.0"
@ -4120,6 +4273,27 @@ dependencies = [
"once_cell",
]
[[package]]
name = "smol-potat"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "894ffa61af5c0fab697c8c29b1ab10cb6ec4978a1ccac4a81b5b312df1ffd88e"
dependencies = [
"async-io",
"smol-potat-macro",
]
[[package]]
name = "smol-potat-macro"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b7cd8129a18069385b4eadaa81182b1451fab312ad6f58d1d99253082bf3932"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "socket2"
version = "0.4.2"
@ -4447,6 +4621,15 @@ dependencies = [
"syn",
]
[[package]]
name = "thread_local"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd"
dependencies = [
"once_cell",
]
[[package]]
name = "thunderdome"
version = "0.4.1"
@ -4524,6 +4707,12 @@ dependencies = [
"serde",
]
[[package]]
name = "treeline"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41"
[[package]]
name = "typenum"
version = "1.14.0"
@ -4623,6 +4812,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "unindent"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7"
[[package]]
name = "untrusted"
version = "0.7.1"
@ -5165,18 +5360,24 @@ name = "wezterm-ssh"
version = "0.2.0"
dependencies = [
"anyhow",
"assert_fs",
"async_ossl",
"base64",
"dirs-next",
"filedescriptor",
"filenamegen",
"indoc",
"k9",
"log",
"once_cell",
"portable-pty",
"predicates",
"pretty_env_logger",
"regex",
"rstest",
"shell-words",
"smol",
"smol-potat",
"ssh2",
"structopt",
"termwiz",

View File

@ -29,8 +29,14 @@ ssh2 = {version="0.9.3", features=["openssl-on-win32"]}
async_ossl = { path = "../async_ossl" }
[dev-dependencies]
assert_fs = "1.0.4"
indoc = "1.0.3"
k9 = "0.11.0"
once_cell = "1.8.0"
predicates = "2.0.2"
pretty_env_logger = "0.4"
rstest = "0.11.0"
shell-words = "1.0"
smol-potat = "1.1.2"
structopt = "0.3"
termwiz = { path = "../termwiz" }

View File

@ -0,0 +1 @@
mod sftp;

View File

@ -0,0 +1,67 @@
use crate::sshd::session;
use assert_fs::{prelude::*, TempDir};
use rstest::*;
use ssh2::FileType;
use std::path::PathBuf;
use wezterm_ssh::Session;
// Sftp file tests
mod file;
#[inline]
fn file_type_to_str(file_type: FileType) -> &'static str {
if file_type.is_dir() {
"dir"
} else if file_type.is_file() {
"file"
} else {
"symlink"
}
}
#[rstest]
#[smol_potat::test]
async fn should_support_listing_directory_contents(#[future] session: Session) {
let session = session.await;
// $TEMP/dir1/
// $TEMP/dir2/
// $TEMP/file1
// $TEMP/file2
// $TEMP/dir-link -> $TEMP/dir1/
// $TEMP/file-link -> $TEMP/file1
let temp = TempDir::new().unwrap();
let dir1 = temp.child("dir1");
dir1.create_dir_all().unwrap();
let dir2 = temp.child("dir2");
dir2.create_dir_all().unwrap();
let file1 = temp.child("file1");
file1.touch().unwrap();
let file2 = temp.child("file2");
file2.touch().unwrap();
let link_dir = temp.child("link-dir");
link_dir.symlink_to_dir(dir1.path()).unwrap();
let link_file = temp.child("link-file");
link_file.symlink_to_file(file1.path()).unwrap();
let mut contents = session
.readdir(temp.path().to_path_buf())
.await
.expect("Failed to read directory")
.into_iter()
.map(|(p, s)| (p, file_type_to_str(s.file_type())))
.collect::<Vec<(PathBuf, &'static str)>>();
contents.sort_unstable_by_key(|(p, _)| p.to_path_buf());
assert_eq!(
contents,
vec![
(dir1.path().to_path_buf(), "dir"),
(dir2.path().to_path_buf(), "dir"),
(file1.path().to_path_buf(), "file"),
(file2.path().to_path_buf(), "file"),
(link_dir.path().to_path_buf(), "symlink"),
(link_file.path().to_path_buf(), "symlink"),
]
);
}

View File

@ -0,0 +1,52 @@
use crate::sshd::session;
use assert_fs::{prelude::*, TempDir};
use rstest::*;
use smol::io::{AsyncReadExt, AsyncWriteExt};
use wezterm_ssh::Session;
#[rstest]
#[smol_potat::test]
#[ignore]
async fn should_support_async_reading(#[future] session: Session) {
let session = session.await;
let temp = TempDir::new().unwrap();
let file = temp.child("test-file");
file.write_str("some file contents").unwrap();
let mut remote_file = session
.open(file.path().to_path_buf())
.await
.expect("Failed to open remote file");
let mut contents = String::new();
remote_file
.read_to_string(&mut contents)
.await
.expect("Failed to read file to string");
assert_eq!(contents, "some file contents");
}
#[rstest]
#[smol_potat::test]
#[ignore]
async fn should_support_async_writing(#[future] session: Session) {
let session = session.await;
let temp = TempDir::new().unwrap();
let file = temp.child("test-file");
file.write_str("some file contents").unwrap();
let mut remote_file = session
.create(file.path().to_path_buf())
.await
.expect("Failed to open remote file");
remote_file
.write_all(b"new contents for file")
.await
.expect("Failed to write to file");
file.assert("new contents for file");
}

2
wezterm-ssh/tests/lib.rs Normal file
View File

@ -0,0 +1,2 @@
mod e2e;
mod sshd;

479
wezterm-ssh/tests/sshd.rs Normal file
View File

@ -0,0 +1,479 @@
use assert_fs::{prelude::*, TempDir};
use once_cell::sync::OnceCell;
use rstest::*;
use std::{
collections::HashMap,
fmt, io,
path::Path,
process::{Child, Command},
sync::atomic::{AtomicU16, Ordering},
thread,
time::Duration,
};
use wezterm_ssh::{Config, Session, SessionEvent};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
/// NOTE: OpenSSH's sshd requires absolute path
const BIN_PATH_STR: &str = "/usr/sbin/sshd";
/// Port range to use when finding a port to bind to (using IANA guidance)
const PORT_RANGE: (u16, u16) = (49152, 65535);
pub struct SshKeygen;
impl SshKeygen {
// ssh-keygen -t rsa -f $ROOT/id_rsa -N "" -q
pub fn generate_rsa(path: impl AsRef<Path>, passphrase: impl AsRef<str>) -> io::Result<bool> {
let res = Command::new("ssh-keygen")
.args(&["-t", "rsa"])
.arg("-f")
.arg(path.as_ref())
.arg("-N")
.arg(passphrase.as_ref())
.arg("-q")
.status()
.map(|status| status.success())?;
#[cfg(unix)]
if res {
// chmod 600 id_rsa* -> ida_rsa + ida_rsa.pub
std::fs::metadata(path.as_ref().with_extension("pub"))?
.permissions()
.set_mode(0o600);
std::fs::metadata(path)?.permissions().set_mode(0o600);
}
Ok(res)
}
}
pub struct SshAgent;
impl SshAgent {
pub fn generate_shell_env() -> io::Result<HashMap<String, String>> {
let output = Command::new("ssh-agent").arg("-s").output()?;
let stdout = String::from_utf8(output.stdout)
.map_err(|x| io::Error::new(io::ErrorKind::InvalidData, x))?;
Ok(stdout
.split(";")
.map(str::trim)
.filter(|s| s.contains("="))
.map(|s| {
let mut tokens = s.split("=");
let key = tokens.next().unwrap().trim().to_string();
let rest = tokens
.map(str::trim)
.map(ToString::to_string)
.collect::<Vec<String>>()
.join("=");
(key, rest)
})
.collect::<HashMap<String, String>>())
}
pub fn update_tests_with_shell_env() -> io::Result<()> {
let env_map = Self::generate_shell_env()?;
for (key, value) in env_map {
std::env::set_var(key, value);
}
Ok(())
}
}
pub struct SshAdd;
impl SshAdd {
pub fn exec(path: impl AsRef<Path>) -> io::Result<bool> {
let env_map = SshAgent::generate_shell_env()?;
Command::new("ssh-add")
.arg(path.as_ref())
.envs(env_map)
.status()
.map(|status| status.success())
}
}
#[derive(Debug)]
pub struct SshdConfig(HashMap<String, Vec<String>>);
impl Default for SshdConfig {
fn default() -> Self {
let mut config = Self::new();
config.set_authentication_methods(vec!["publickey".to_string()]);
config.set_subsystem(true, true);
config.set_use_pam(false);
config.set_x11_forwarding(true);
config.set_use_privilege_separation(false);
config.set_print_motd(true);
config.set_permit_tunnel(true);
config.set_kbd_interactive_authentication(true);
config.set_allow_tcp_forwarding(true);
config.set_max_startups(500, None);
config.set_strict_modes(false);
config
}
}
impl SshdConfig {
pub fn new() -> Self {
Self(HashMap::new())
}
pub fn set_authentication_methods(&mut self, methods: Vec<String>) {
self.0.insert("AuthenticationMethods".to_string(), methods);
}
pub fn set_authorized_keys_file(&mut self, path: impl AsRef<Path>) {
self.0.insert(
"AuthorizedKeysFile".to_string(),
vec![path.as_ref().to_string_lossy().to_string()],
);
}
pub fn set_host_key(&mut self, path: impl AsRef<Path>) {
self.0.insert(
"HostKey".to_string(),
vec![path.as_ref().to_string_lossy().to_string()],
);
}
pub fn set_pid_file(&mut self, path: impl AsRef<Path>) {
self.0.insert(
"PidFile".to_string(),
vec![path.as_ref().to_string_lossy().to_string()],
);
}
pub fn set_subsystem(&mut self, sftp: bool, internal_sftp: bool) {
let mut values = Vec::new();
if sftp {
values.push("sftp".to_string());
}
if internal_sftp {
values.push("internal-sftp".to_string());
}
self.0.insert("Subsystem".to_string(), values);
}
pub fn set_use_pam(&mut self, yes: bool) {
self.0.insert("UsePAM".to_string(), Self::yes_value(yes));
}
pub fn set_x11_forwarding(&mut self, yes: bool) {
self.0
.insert("X11Forwarding".to_string(), Self::yes_value(yes));
}
pub fn set_use_privilege_separation(&mut self, yes: bool) {
self.0
.insert("UsePrivilegeSeparation".to_string(), Self::yes_value(yes));
}
pub fn set_print_motd(&mut self, yes: bool) {
self.0.insert("PrintMotd".to_string(), Self::yes_value(yes));
}
pub fn set_permit_tunnel(&mut self, yes: bool) {
self.0
.insert("PermitTunnel".to_string(), Self::yes_value(yes));
}
pub fn set_kbd_interactive_authentication(&mut self, yes: bool) {
self.0.insert(
"KbdInteractiveAuthentication".to_string(),
Self::yes_value(yes),
);
}
pub fn set_allow_tcp_forwarding(&mut self, yes: bool) {
self.0
.insert("AllowTcpForwarding".to_string(), Self::yes_value(yes));
}
pub fn set_max_startups(&mut self, start: u16, rate_full: Option<(u16, u16)>) {
let value = format!(
"{}{}",
start,
rate_full
.map(|(r, f)| format!(":{}:{}", r, f))
.unwrap_or_default(),
);
self.0.insert("MaxStartups".to_string(), vec![value]);
}
pub fn set_strict_modes(&mut self, yes: bool) {
self.0
.insert("StrictModes".to_string(), Self::yes_value(yes));
}
fn yes_value(yes: bool) -> Vec<String> {
vec![Self::yes_string(yes)]
}
fn yes_string(yes: bool) -> String {
Self::yes_str(yes).to_string()
}
const fn yes_str(yes: bool) -> &'static str {
if yes {
"yes"
} else {
"no"
}
}
}
impl fmt::Display for SshdConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (keyword, values) in self.0.iter() {
writeln!(
f,
"{} {}",
keyword,
values
.iter()
.map(|v| {
let v = v.trim();
if v.contains(|c: char| c.is_whitespace()) {
format!("\"{}\"", v)
} else {
v.to_string()
}
})
.collect::<Vec<String>>()
.join(" ")
)?;
}
Ok(())
}
}
/// Context for some sshd instance
pub struct Sshd {
child: Child,
/// Port that sshd is listening on
pub port: u16,
/// Temporary directory used to hold resources for sshd such as its config, keys, and log
pub tmp: TempDir,
}
impl Sshd {
pub fn spawn(mut config: SshdConfig) -> Result<Self, Box<dyn std::error::Error>> {
let tmp = TempDir::new()?;
// Ensure that everything needed for interacting with ssh-agent is set
SshAgent::update_tests_with_shell_env()?;
// ssh-keygen -t rsa -f $ROOT/id_rsa -N "" -q
let id_rsa_file = tmp.child("id_rsa");
assert!(
SshKeygen::generate_rsa(id_rsa_file.path(), "")?,
"Failed to ssh-keygen id_rsa"
);
assert!(
SshAdd::exec(id_rsa_file.path())?,
"Failed to ssh-add id_rsa"
);
// cp $ROOT/id_rsa.pub $ROOT/authorized_keys
let authorized_keys_file = tmp.child("authorized_keys");
std::fs::copy(
id_rsa_file.path().with_extension("pub"),
authorized_keys_file.path(),
)?;
// ssh-keygen -t rsa -f $ROOT/ssh_host_rsa_key -N "" -q
let ssh_host_rsa_key_file = tmp.child("ssh_host_rsa_key");
assert!(
SshKeygen::generate_rsa(ssh_host_rsa_key_file.path(), "")?,
"Failed to ssh-keygen ssh_host_rsa_key"
);
config.set_authorized_keys_file(id_rsa_file.path().with_extension("pub"));
config.set_host_key(ssh_host_rsa_key_file.path());
let sshd_pid_file = tmp.child("sshd.pid");
config.set_pid_file(sshd_pid_file.path());
// Generate $ROOT/sshd_config based on config
let sshd_config_file = tmp.child("sshd_config");
sshd_config_file.write_str(&config.to_string())?;
let sshd_log_file = tmp.child("sshd.log");
let (child, port) = Self::try_spawn_next(sshd_config_file.path(), sshd_log_file.path())
.expect("No open port available for sshd");
Ok(Self { child, port, tmp })
}
fn try_spawn_next(
config_path: impl AsRef<Path>,
log_path: impl AsRef<Path>,
) -> io::Result<(Child, u16)> {
static PORT: AtomicU16 = AtomicU16::new(PORT_RANGE.0);
loop {
let port = PORT.fetch_add(1, Ordering::Relaxed);
match Self::try_spawn(port, config_path.as_ref(), log_path.as_ref()) {
// If successful, return our spawned server child process
Ok(Ok(child)) => break Ok((child, port)),
// If the server died when spawned and we reached the final port, we want to exit
Ok(Err((code, msg))) if port == PORT_RANGE.1 => {
break Err(io::Error::new(
io::ErrorKind::Other,
format!(
"{} failed [{}]: {}",
BIN_PATH_STR,
code.map(|x| x.to_string())
.unwrap_or_else(|| String::from("???")),
msg
),
))
}
// If we've reached the final port in our range to try, we want to exit
Err(x) if port == PORT_RANGE.1 => break Err(x),
// Otherwise, try next port
Err(_) | Ok(Err(_)) => continue,
}
}
}
fn try_spawn(
port: u16,
config_path: impl AsRef<Path>,
log_path: impl AsRef<Path>,
) -> io::Result<Result<Child, (Option<i32>, String)>> {
let mut child = Command::new(BIN_PATH_STR)
.arg("-D")
.arg("-p")
.arg(port.to_string())
.arg("-f")
.arg(config_path.as_ref())
.arg("-E")
.arg(log_path.as_ref())
.spawn()?;
// Pause for couple of seconds to make sure that the server didn't die due to an error
thread::sleep(Duration::from_secs(2));
if let Some(exit_status) = child.try_wait()? {
let output = child.wait_with_output()?;
Ok(Err((
exit_status.code(),
format!(
"{}\n{}",
String::from_utf8(output.stdout).unwrap(),
String::from_utf8(output.stderr).unwrap(),
),
)))
} else {
Ok(Ok(child))
}
}
}
impl Drop for Sshd {
/// Kills server upon drop
fn drop(&mut self) {
let _ = self.child.kill();
}
}
#[fixture]
/// Stand up a singular sshd session and hold onto it for the lifetime
/// of our tests, returning a reference to it with each fixture ref
pub fn sshd() -> &'static Sshd {
static SSHD: OnceCell<Sshd> = OnceCell::new();
SSHD.get_or_init(|| Sshd::spawn(Default::default()).unwrap())
}
#[fixture]
/// Stand up an sshd instance and then connect to it and perform authentication
pub async fn session(sshd: &'_ Sshd) -> Session {
let port = sshd.port;
let mut config = Config::new();
config.add_default_config_files();
// Load our config to point to ourselves, using current sshd instance's port,
// generated identity file, and host file
let mut config = config.for_host("localhost");
config.insert("port".to_string(), port.to_string());
config.insert(
"identityfile".to_string(),
sshd.tmp
.child("id_rsa")
.path()
.to_str()
.expect("Failed to get string path for id_rsa")
.to_string(),
);
config.insert(
"userknownhostsfile".to_string(),
sshd.tmp
.child("known_hosts")
.path()
.to_str()
.expect("Failed to get string path for known_hosts")
.to_string(),
);
// Perform our actual connection
let (session, events) = Session::connect(config.clone()).expect("Failed to connect to sshd");
// Perform automated authentication, assuming that we have a publickey with empty password
while let Ok(event) = events.recv().await {
match event {
SessionEvent::Banner(banner) => {
if let Some(banner) = banner {
log::trace!("{}", banner);
}
}
SessionEvent::HostVerify(verify) => {
eprintln!("{}", verify.message);
// Automatically verify any host
verify
.answer(true)
.await
.expect("Failed to send host verification");
}
SessionEvent::Authenticate(auth) => {
if !auth.username.is_empty() {
eprintln!("Authentication for {}", auth.username);
}
if !auth.instructions.is_empty() {
eprintln!("{}", auth.instructions);
}
// Reply with empty string to all authentication requests
let answers = vec![String::new(); auth.prompts.len()];
auth.answer(answers)
.await
.expect("Failed to send authenticate response");
}
SessionEvent::Error(err) => {
panic!("{}", err);
}
SessionEvent::Authenticated => break,
}
}
session
}