//! An in-process SSH server for tests (feature `testing`): password `dev` / //! `secret`, commands answered by a closure, and SFTP served from a local //! folder (remote `/x` is `/x`). use russh::server::{self, Auth, ChannelOpenHandle, Msg, Server as _, Session}; use russh::{Channel, ChannelId}; use russh_sftp::protocol::{Attrs, Data, File, FileAttributes, Handle, Name, OpenFlags, Status, StatusCode, Version}; use std::collections::HashMap; use std::io::{Read, Seek, SeekFrom, Write}; use std::path::{Path, PathBuf}; use std::sync::Arc; use tokio::net::TcpListener; use tokio::sync::Mutex; /// Reply of a command: (stdout, exit status). pub type ExecFn = Arc (String, u32) + Send + Sync>; /// Starts the server; returns its port. `root` backs SFTP. pub async fn sshd(root: PathBuf, exec: ExecFn) -> u16 { let config = Arc::new(server::Config { keys: vec![russh::keys::decode_secret_key(include_str!("../tests/fixtures/ssh/host"), None).expect("host key")], auth_rejection_time: std::time::Duration::from_millis(10), auth_rejection_time_initial: Some(std::time::Duration::ZERO), ..Default::default() }); let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind"); let port = listener.local_addr().expect("addr").port(); let mut server = Sshd { root, exec }; tokio::spawn(async move { let _ = server.run_on_socket(config, &listener).await; }); port } #[derive(Clone)] struct Sshd { root: PathBuf, exec: ExecFn, } impl server::Server for Sshd { type Handler = Conn; fn new_client(&mut self, _: Option) -> Conn { Conn { root: self.root.clone(), exec: self.exec.clone(), channels: Arc::default() } } } struct Conn { root: PathBuf, exec: ExecFn, channels: Arc>>>, } impl server::Handler for Conn { type Error = russh::Error; async fn auth_password(&mut self, user: &str, password: &str) -> Result { Ok(if user == "dev" && password == "secret" { Auth::Accept } else { Auth::reject() }) } async fn channel_open_session( &mut self, channel: Channel, reply: ChannelOpenHandle, _session: &mut Session, ) -> Result<(), Self::Error> { self.channels.lock().await.insert(channel.id(), channel); reply.accept().await; Ok(()) } async fn exec_request( &mut self, channel: ChannelId, data: &[u8], session: &mut Session, ) -> Result<(), Self::Error> { let (out, code) = (self.exec)(&String::from_utf8_lossy(data)); session.channel_success(channel)?; session.data(channel, out.into_bytes())?; session.exit_status_request(channel, code)?; session.eof(channel)?; session.close(channel)?; Ok(()) } async fn subsystem_request( &mut self, channel: ChannelId, name: &str, session: &mut Session, ) -> Result<(), Self::Error> { let Some(ch) = self.channels.lock().await.remove(&channel).filter(|_| name == "sftp") else { session.channel_failure(channel)?; return Ok(()); }; session.channel_success(channel)?; let sftp = Sftp { root: self.root.clone(), files: HashMap::new(), dirs: HashMap::new(), next: 0 }; tokio::spawn(russh_sftp::server::run(ch.into_stream(), sftp)); Ok(()) } } struct Sftp { root: PathBuf, files: HashMap, /// Open directories: entries not yet sent. dirs: HashMap>>, next: u64, } fn ok(id: u32) -> Status { Status { id, status_code: StatusCode::Ok, error_message: "Ok".into(), language_tag: "en-US".into() } } fn code(e: std::io::Error) -> StatusCode { match e.kind() { std::io::ErrorKind::NotFound => StatusCode::NoSuchFile, std::io::ErrorKind::PermissionDenied => StatusCode::PermissionDenied, _ => StatusCode::Failure, } } impl Sftp { fn local(&self, path: &str) -> PathBuf { path.split('/').filter(|s| !s.is_empty() && *s != "." && *s != "..").fold(self.root.clone(), |p, s| p.join(s)) } fn handle(&mut self) -> String { self.next += 1; format!("h{}", self.next) } fn attrs(path: &Path) -> Result { std::fs::symlink_metadata(path).map(|m| FileAttributes::from(&m)).map_err(code) } } impl russh_sftp::server::Handler for Sftp { type Error = StatusCode; fn unimplemented(&self) -> Self::Error { StatusCode::OpUnsupported } async fn init(&mut self, _version: u32, _ext: HashMap) -> Result { Ok(Version::new()) } async fn realpath(&mut self, id: u32, path: String) -> Result { let p = if path == "." || path.is_empty() { "/home/dev".to_owned() } else { path }; Ok(Name { id, files: vec![File::dummy(p)] }) } async fn open( &mut self, id: u32, filename: String, pflags: OpenFlags, _attrs: FileAttributes, ) -> Result { let path = self.local(&filename); let file = std::fs::OpenOptions::new() .read(pflags.contains(OpenFlags::READ)) .write(pflags.contains(OpenFlags::WRITE)) .create(pflags.contains(OpenFlags::CREATE)) .create_new(pflags.contains(OpenFlags::EXCLUDE)) .truncate(pflags.contains(OpenFlags::TRUNCATE)) .open(path) .map_err(code)?; let handle = self.handle(); self.files.insert(handle.clone(), file); Ok(Handle { id, handle }) } async fn close(&mut self, id: u32, handle: String) -> Result { self.files.remove(&handle); self.dirs.remove(&handle); Ok(ok(id)) } async fn read(&mut self, id: u32, handle: String, offset: u64, len: u32) -> Result { let file = self.files.get_mut(&handle).ok_or(StatusCode::Failure)?; file.seek(SeekFrom::Start(offset)).map_err(code)?; let mut buf = vec![0; len as usize]; let n = file.read(&mut buf).map_err(code)?; if n == 0 { return Err(StatusCode::Eof); } buf.truncate(n); Ok(Data { id, data: buf }) } async fn write(&mut self, id: u32, handle: String, offset: u64, data: Vec) -> Result { let file = self.files.get_mut(&handle).ok_or(StatusCode::Failure)?; file.seek(SeekFrom::Start(offset)).map_err(code)?; file.write_all(&data).map_err(code)?; Ok(ok(id)) } async fn stat(&mut self, id: u32, path: String) -> Result { let p = self.local(&path); let attrs = std::fs::metadata(&p).map(|m| FileAttributes::from(&m)).map_err(code)?; Ok(Attrs { id, attrs }) } async fn lstat(&mut self, id: u32, path: String) -> Result { Ok(Attrs { id, attrs: Self::attrs(&self.local(&path))? }) } async fn fstat(&mut self, id: u32, handle: String) -> Result { let file = self.files.get(&handle).ok_or(StatusCode::Failure)?; Ok(Attrs { id, attrs: FileAttributes::from(&file.metadata().map_err(code)?) }) } async fn setstat(&mut self, id: u32, path: String, attrs: FileAttributes) -> Result { #[cfg(unix)] if let Some(mode) = attrs.permissions { use std::os::unix::fs::PermissionsExt; std::fs::set_permissions(self.local(&path), std::fs::Permissions::from_mode(mode)).map_err(code)?; } if let Some(t) = attrs.mtime { let time = std::time::UNIX_EPOCH + std::time::Duration::from_secs(t.into()); let f = std::fs::File::options().write(true).open(self.local(&path)).map_err(code)?; f.set_modified(time).map_err(code)?; } Ok(ok(id)) } async fn opendir(&mut self, id: u32, path: String) -> Result { let dir = self.local(&path); let mut files = Vec::new(); for e in std::fs::read_dir(&dir).map_err(code)?.flatten() { let name = e.file_name().to_string_lossy().into_owned(); files.push(File::new(name, Self::attrs(&e.path())?)); } let handle = self.handle(); self.dirs.insert(handle.clone(), Some(files)); Ok(Handle { id, handle }) } async fn readdir(&mut self, id: u32, handle: String) -> Result { match self.dirs.get_mut(&handle).and_then(Option::take) { Some(files) if !files.is_empty() => Ok(Name { id, files }), _ => Err(StatusCode::Eof), } } async fn remove(&mut self, id: u32, filename: String) -> Result { std::fs::remove_file(self.local(&filename)).map_err(code)?; Ok(ok(id)) } async fn mkdir(&mut self, id: u32, path: String, _attrs: FileAttributes) -> Result { std::fs::create_dir(self.local(&path)).map_err(code)?; Ok(ok(id)) } async fn rmdir(&mut self, id: u32, path: String) -> Result { std::fs::remove_dir(self.local(&path)).map_err(code)?; Ok(ok(id)) } #[cfg(unix)] async fn symlink(&mut self, id: u32, target: String, link: String) -> Result { std::os::unix::fs::symlink(target, self.local(&link)).map_err(code)?; Ok(ok(id)) } async fn rename(&mut self, id: u32, oldpath: String, newpath: String) -> Result { std::fs::rename(self.local(&oldpath), self.local(&newpath)).map_err(code)?; Ok(ok(id)) } }