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}