mirror of
https://github.com/wez/wezterm.git
synced 2024-12-23 13:21:38 +03:00
Add some initial tests to wezterm-ssh, ignoring read/write of file as those are hanging
This commit is contained in:
parent
23fd145ceb
commit
fca3bd6904
207
Cargo.lock
generated
207
Cargo.lock
generated
@ -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",
|
||||
|
@ -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" }
|
||||
|
1
wezterm-ssh/tests/e2e/mod.rs
Normal file
1
wezterm-ssh/tests/e2e/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
mod sftp;
|
67
wezterm-ssh/tests/e2e/sftp.rs
Normal file
67
wezterm-ssh/tests/e2e/sftp.rs
Normal 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"),
|
||||
]
|
||||
);
|
||||
}
|
52
wezterm-ssh/tests/e2e/sftp/file.rs
Normal file
52
wezterm-ssh/tests/e2e/sftp/file.rs
Normal 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
2
wezterm-ssh/tests/lib.rs
Normal file
@ -0,0 +1,2 @@
|
||||
mod e2e;
|
||||
mod sshd;
|
479
wezterm-ssh/tests/sshd.rs
Normal file
479
wezterm-ssh/tests/sshd.rs
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user