ssh_test_server/
lib.rs

1//! This crate allow running mockup ssh server in integration tests.
2//!
3//! # Examples
4//!
5//! ```
6//! use ssh_test_server::{SshServerBuilder, SshExecuteContext, SshExecuteResult, User};
7//!
8//! fn cmd_check_password(
9//!     context: &SshExecuteContext,
10//!     _program: &str,
11//!     args: &[&str],
12//! ) -> SshExecuteResult {
13//!     let mut args = args.iter();
14//!     let (Some(login), Some(password)) = (args.next(), args.next()) else {
15//!         return SshExecuteResult::stderr(2, "Usage: check_password <login> <password>");
16//!     };
17//!
18//!     if !context.current_admin() {
19//!         return SshExecuteResult::stderr(1, "Permission denied.");
20//!     }
21//!
22//!     let users = context.users.lock().unwrap();
23//!     let Some(user) = users.get(*login) else {
24//!         return SshExecuteResult::stderr(1, format!("Unknown user {login}."));
25//!     };
26//!
27//!     if user.password() == *password {
28//!         SshExecuteResult::stdout(0, "Password correct.")
29//!     } else {
30//!         SshExecuteResult::stderr(1, "Password does not match.")
31//!     }
32//! }
33//!
34//! # #[tokio::main(flavor = "current_thread")]
35//! # async fn main() {
36//! let ssh = SshServerBuilder::default()
37//!     .add_user(User::new("user", "123"))
38//!     .add_user(User::new_admin("root", "abc123"))
39//!     .add_program("check_password", Box::new(cmd_check_password))
40//!     .run()
41//!     .await
42//!     .unwrap();
43//!
44//! println!("ssh -p {} root@{} check_password user 123", ssh.port(), ssh.host());
45//! # }
46//! ```
47//!
48#![warn(missing_docs)]
49use russh_keys::key::PublicKey;
50use russh_keys::PublicKeyBase64;
51use std::collections::HashMap;
52use std::sync::{Arc, Mutex};
53use tokio::task::JoinHandle;
54
55mod builder;
56mod command;
57mod session;
58mod user;
59
60pub use builder::SshServerBuilder;
61pub use user::User;
62
63/// Users required in ssh server context.
64/// Key of the hash map is a user login.
65pub type UsersMap = Arc<Mutex<HashMap<String, User>>>;
66
67/// Function signature for custom commands.
68pub type SshExecuteHandler =
69    dyn Fn(&SshExecuteContext, &str, &[&str]) -> SshExecuteResult + Sync + Send;
70
71/// Context of ssh server passed to every custom function.
72///
73/// For example, it's allows to implement program that modifies
74/// user password.
75pub struct SshExecuteContext<'a> {
76    /// Users registered in server.
77    pub users: &'a UsersMap,
78    /// Current user's login.
79    pub current_user: &'a str,
80}
81
82impl<'a> SshExecuteContext<'a> {
83    /// Return true if current user has admin flag.
84    pub fn current_admin(&self) -> bool {
85        self.users
86            .lock()
87            .unwrap()
88            .get(self.current_user)
89            .map(|u| u.admin())
90            .unwrap_or(false)
91    }
92}
93
94/// Response that have to be returned by custom command handler.
95pub struct SshExecuteResult {
96    /// Standard output.
97    pub stdout: String,
98    /// Standard error.
99    pub stderr: String,
100    /// Program exit code. Usually 0 means success.
101    pub status_code: u32,
102}
103
104impl SshExecuteResult {
105    /// Create a stdout result.
106    ///
107    /// # Example
108    /// ```
109    /// use ssh_test_server::SshExecuteResult;
110    /// let result = SshExecuteResult::stdout(0, "Password chained.");
111    ///
112    /// assert_eq!(result.status_code, 0);
113    /// assert_eq!(result.stdout, "Password chained.");
114    /// ```
115    pub fn stdout(status_code: u32, stdout: impl Into<String>) -> Self {
116        Self {
117            stdout: stdout.into(),
118            stderr: "".to_string(),
119            status_code,
120        }
121    }
122
123    /// Create a stderr result.
124    ///
125    /// # Example
126    /// ```
127    /// use ssh_test_server::SshExecuteResult;
128    /// let result = SshExecuteResult::stderr(1, "Permission denied.");
129    ///
130    /// assert_eq!(result.status_code, 1);
131    /// assert_eq!(result.stderr, "Permission denied.");
132    /// ```
133    pub fn stderr(status_code: u32, stderr: impl Into<String>) -> Self {
134        Self {
135            stdout: "".to_string(),
136            stderr: stderr.into(),
137            status_code,
138        }
139    }
140}
141
142/// Running SSH server.
143///
144/// When is dropped then ssh server stops.
145#[derive(Debug)]
146pub struct SshServer {
147    listener: JoinHandle<()>,
148    users: UsersMap,
149    port: u16,
150    host: String,
151    server_public_key: PublicKey,
152}
153
154impl SshServer {
155    /// IP or hostname of the ssh server.
156    pub fn host(&self) -> &str {
157        &self.host
158    }
159
160    /// Port number of the ssh server.
161    pub fn port(&self) -> u16 {
162        self.port
163    }
164
165    /// Host and port pair.
166    ///
167    /// Format:
168    /// ```text
169    /// <host>:<port>
170    /// ```
171    pub fn addr(&self) -> String {
172        format!("{}:{}", self.host(), self.port())
173    }
174
175    /// Ssh public key of the ssh server.
176    pub fn server_public_key(&self) -> String {
177        format!(
178            "{} {}",
179            self.server_public_key.name(),
180            self.server_public_key.public_key_base64()
181        )
182    }
183
184    /// Registered users in the ssh server.
185    pub fn users(&self) -> UsersMap {
186        self.users.clone()
187    }
188}
189
190impl Drop for SshServer {
191    fn drop(&mut self) {
192        self.listener.abort();
193    }
194}