1use std::path::{Path, PathBuf};
7use std::time::Duration;
8
9mod backend;
11mod config;
12#[cfg(test)]
13mod container;
14mod key_method;
15mod scp;
16mod sftp;
17
18pub use ssh2_config::ParseRule;
19
20#[cfg(feature = "libssh2")]
21#[cfg_attr(docsrs, doc(cfg(feature = "libssh2")))]
22pub use self::backend::LibSsh2Session;
23#[cfg(feature = "libssh")]
24#[cfg_attr(docsrs, doc(cfg(feature = "libssh")))]
25pub use self::backend::LibSshSession;
26pub use self::backend::SshSession;
27#[cfg(feature = "russh")]
28#[cfg_attr(docsrs, doc(cfg(feature = "russh")))]
29pub use self::backend::{NoCheckServerKey, RusshSession};
30pub use self::key_method::{KeyMethod, MethodType};
31pub use self::scp::ScpFs;
32pub use self::sftp::SftpFs;
33
34pub trait SshKeyStorage: Send + Sync {
38 fn resolve(&self, host: &str, username: &str) -> Option<PathBuf>;
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
46pub enum SshAgentIdentity {
47 All,
49 Pubkey(Vec<u8>),
51}
52
53impl From<Vec<u8>> for SshAgentIdentity {
54 fn from(v: Vec<u8>) -> Self {
55 SshAgentIdentity::Pubkey(v)
56 }
57}
58
59impl From<&[u8]> for SshAgentIdentity {
60 fn from(v: &[u8]) -> Self {
61 SshAgentIdentity::Pubkey(v.to_vec())
62 }
63}
64
65impl SshAgentIdentity {
66 pub(crate) fn pubkey_matches(&self, blob: &[u8]) -> bool {
70 match self {
71 SshAgentIdentity::All => true,
72 SshAgentIdentity::Pubkey(v) => v == blob,
73 }
74 }
75}
76
77pub struct SshOpts {
91 host: String,
93 port: Option<u16>,
95 username: Option<String>,
97 password: Option<String>,
99 connection_timeout: Option<Duration>,
101 config_file: Option<PathBuf>,
103 key_storage: Option<Box<dyn SshKeyStorage>>,
105 methods: Vec<KeyMethod>,
107 parse_rules: ParseRule,
109 ssh_agent_identity: Option<SshAgentIdentity>,
111 #[cfg(feature = "russh")]
113 runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
114}
115
116impl SshOpts {
117 pub fn new<S: AsRef<str>>(host: S) -> Self {
123 Self {
124 host: host.as_ref().to_string(),
125 port: None,
126 username: None,
127 password: None,
128 connection_timeout: None,
129 config_file: None,
130 key_storage: None,
131 methods: Vec::default(),
132 parse_rules: ParseRule::STRICT,
133 ssh_agent_identity: None,
134 #[cfg(feature = "russh")]
135 runtime: None,
136 }
137 }
138
139 pub fn port(mut self, port: u16) -> Self {
142 self.port = Some(port);
143 self
144 }
145
146 pub fn username<S: AsRef<str>>(mut self, username: S) -> Self {
149 self.username = Some(username.as_ref().to_string());
150 self
151 }
152
153 pub fn password<S: AsRef<str>>(mut self, password: S) -> Self {
155 self.password = Some(password.as_ref().to_string());
156 self
157 }
158
159 pub fn connection_timeout(mut self, timeout: Duration) -> Self {
162 self.connection_timeout = Some(timeout);
163 self
164 }
165
166 pub fn ssh_agent_identity(mut self, ssh_agent_identity: Option<SshAgentIdentity>) -> Self {
173 self.ssh_agent_identity = ssh_agent_identity;
174 self
175 }
176
177 pub fn config_file<P: AsRef<Path>>(mut self, p: P, rules: ParseRule) -> Self {
192 self.config_file = Some(p.as_ref().to_path_buf());
193 self.parse_rules = rules;
194 self
195 }
196
197 pub fn key_storage(mut self, storage: Box<dyn SshKeyStorage>) -> Self {
199 self.key_storage = Some(storage);
200 self
201 }
202
203 pub fn method(mut self, method: KeyMethod) -> Self {
205 self.methods.push(method);
206 self
207 }
208
209 #[cfg(feature = "russh")]
211 #[cfg_attr(docsrs, doc(cfg(feature = "russh")))]
212 pub(crate) fn runtime(mut self, runtime: std::sync::Arc<tokio::runtime::Runtime>) -> Self {
213 self.runtime = Some(runtime);
214 self
215 }
216}
217
218#[cfg(feature = "libssh")]
219impl From<SshOpts> for SftpFs<LibSshSession> {
220 fn from(opts: SshOpts) -> Self {
221 Self::libssh(opts)
222 }
223}
224
225#[cfg(feature = "libssh")]
226impl From<SshOpts> for ScpFs<LibSshSession> {
227 fn from(opts: SshOpts) -> Self {
228 Self::libssh(opts)
229 }
230}
231
232#[cfg(feature = "libssh2")]
233impl From<SshOpts> for SftpFs<LibSsh2Session> {
234 fn from(opts: SshOpts) -> Self {
235 Self::libssh2(opts)
236 }
237}
238
239#[cfg(feature = "libssh2")]
240impl From<SshOpts> for ScpFs<LibSsh2Session> {
241 fn from(opts: SshOpts) -> Self {
242 Self::libssh2(opts)
243 }
244}
245
246#[cfg(test)]
247mod test {
248
249 use pretty_assertions::assert_eq;
250
251 use super::*;
252 use crate::mock::ssh::MockSshKeyStorage;
253
254 #[test]
255 fn should_create_key_method() {
256 let key_method = KeyMethod::new(
257 MethodType::CryptClientServer,
258 &[
259 "aes128-ctr".to_string(),
260 "aes192-ctr".to_string(),
261 "aes256-ctr".to_string(),
262 "aes128-cbc".to_string(),
263 "3des-cbc".to_string(),
264 ],
265 );
266 assert_eq!(
267 key_method.prefs().as_str(),
268 "aes128-ctr,aes192-ctr,aes256-ctr,aes128-cbc,3des-cbc"
269 );
270 }
271
272 #[test]
273 fn test_should_tell_whether_pubkey_matches() {
274 let identity = SshAgentIdentity::Pubkey(b"hello".to_vec());
275 assert!(identity.pubkey_matches(b"hello"));
276 assert!(!identity.pubkey_matches(b"world"));
277
278 let identity = SshAgentIdentity::All;
279 assert!(identity.pubkey_matches(b"hello"));
280 }
281
282 #[test]
283 fn should_initialize_ssh_opts() {
284 let opts = SshOpts::new("localhost");
285 assert_eq!(opts.host.as_str(), "localhost");
286 assert!(opts.port.is_none());
287 assert!(opts.username.is_none());
288 assert!(opts.password.is_none());
289 assert!(opts.connection_timeout.is_none());
290 assert!(opts.config_file.is_none());
291 assert!(opts.key_storage.is_none());
292 assert!(opts.methods.is_empty());
293 }
294
295 #[test]
296 fn should_build_ssh_opts() {
297 let opts = SshOpts::new("localhost")
298 .port(22)
299 .username("foobar")
300 .password("qwerty123")
301 .connection_timeout(Duration::from_secs(10))
302 .config_file(Path::new("/home/pippo/.ssh/config"), ParseRule::STRICT)
303 .key_storage(Box::new(MockSshKeyStorage::default()))
304 .method(KeyMethod::new(
305 MethodType::CryptClientServer,
306 &[
307 "aes128-ctr".to_string(),
308 "aes192-ctr".to_string(),
309 "aes256-ctr".to_string(),
310 "aes128-cbc".to_string(),
311 "3des-cbc".to_string(),
312 ],
313 ));
314 assert_eq!(opts.host.as_str(), "localhost");
315 assert_eq!(opts.port.unwrap(), 22);
316 assert_eq!(opts.username.as_deref().unwrap(), "foobar");
317 assert_eq!(opts.password.as_deref().unwrap(), "qwerty123");
318 assert_eq!(opts.connection_timeout.unwrap(), Duration::from_secs(10));
319 assert_eq!(
320 opts.config_file.as_deref().unwrap(),
321 Path::new("/home/pippo/.ssh/config")
322 );
323 assert!(opts.key_storage.is_some());
324 assert_eq!(opts.methods.len(), 1);
325 }
326}