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;
27pub use self::key_method::{KeyMethod, MethodType};
28pub use self::scp::ScpFs;
29pub use self::sftp::SftpFs;
30
31pub trait SshKeyStorage: Send + Sync {
35 fn resolve(&self, host: &str, username: &str) -> Option<PathBuf>;
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum SshAgentIdentity {
44 All,
46 Pubkey(Vec<u8>),
48}
49
50impl From<Vec<u8>> for SshAgentIdentity {
51 fn from(v: Vec<u8>) -> Self {
52 SshAgentIdentity::Pubkey(v)
53 }
54}
55
56impl From<&[u8]> for SshAgentIdentity {
57 fn from(v: &[u8]) -> Self {
58 SshAgentIdentity::Pubkey(v.to_vec())
59 }
60}
61
62impl SshAgentIdentity {
63 pub(crate) fn pubkey_matches(&self, blob: &[u8]) -> bool {
67 match self {
68 SshAgentIdentity::All => true,
69 SshAgentIdentity::Pubkey(v) => v == blob,
70 }
71 }
72}
73
74pub struct SshOpts {
88 host: String,
90 port: Option<u16>,
92 username: Option<String>,
94 password: Option<String>,
96 connection_timeout: Option<Duration>,
98 config_file: Option<PathBuf>,
100 key_storage: Option<Box<dyn SshKeyStorage>>,
102 methods: Vec<KeyMethod>,
104 parse_rules: ParseRule,
106 ssh_agent_identity: Option<SshAgentIdentity>,
108}
109
110impl SshOpts {
111 pub fn new<S: AsRef<str>>(host: S) -> Self {
117 Self {
118 host: host.as_ref().to_string(),
119 port: None,
120 username: None,
121 password: None,
122 connection_timeout: None,
123 config_file: None,
124 key_storage: None,
125 methods: Vec::default(),
126 parse_rules: ParseRule::STRICT,
127 ssh_agent_identity: None,
128 }
129 }
130
131 pub fn port(mut self, port: u16) -> Self {
134 self.port = Some(port);
135 self
136 }
137
138 pub fn username<S: AsRef<str>>(mut self, username: S) -> Self {
141 self.username = Some(username.as_ref().to_string());
142 self
143 }
144
145 pub fn password<S: AsRef<str>>(mut self, password: S) -> Self {
147 self.password = Some(password.as_ref().to_string());
148 self
149 }
150
151 pub fn connection_timeout(mut self, timeout: Duration) -> Self {
154 self.connection_timeout = Some(timeout);
155 self
156 }
157
158 pub fn ssh_agent_identity(mut self, ssh_agent_identity: Option<SshAgentIdentity>) -> Self {
165 self.ssh_agent_identity = ssh_agent_identity;
166 self
167 }
168
169 pub fn config_file<P: AsRef<Path>>(mut self, p: P, rules: ParseRule) -> Self {
184 self.config_file = Some(p.as_ref().to_path_buf());
185 self.parse_rules = rules;
186 self
187 }
188
189 pub fn key_storage(mut self, storage: Box<dyn SshKeyStorage>) -> Self {
191 self.key_storage = Some(storage);
192 self
193 }
194
195 pub fn method(mut self, method: KeyMethod) -> Self {
197 self.methods.push(method);
198 self
199 }
200}
201
202#[cfg(feature = "libssh")]
203impl From<SshOpts> for SftpFs<LibSshSession> {
204 fn from(opts: SshOpts) -> Self {
205 Self::libssh(opts)
206 }
207}
208
209#[cfg(feature = "libssh")]
210impl From<SshOpts> for ScpFs<LibSshSession> {
211 fn from(opts: SshOpts) -> Self {
212 Self::libssh(opts)
213 }
214}
215
216#[cfg(feature = "libssh2")]
217impl From<SshOpts> for SftpFs<LibSsh2Session> {
218 fn from(opts: SshOpts) -> Self {
219 Self::libssh2(opts)
220 }
221}
222
223#[cfg(feature = "libssh2")]
224impl From<SshOpts> for ScpFs<LibSsh2Session> {
225 fn from(opts: SshOpts) -> Self {
226 Self::libssh2(opts)
227 }
228}
229
230#[cfg(test)]
231mod test {
232
233 use pretty_assertions::assert_eq;
234
235 use super::*;
236 use crate::mock::ssh::MockSshKeyStorage;
237
238 #[test]
239 fn should_create_key_method() {
240 let key_method = KeyMethod::new(
241 MethodType::CryptClientServer,
242 &[
243 "aes128-ctr".to_string(),
244 "aes192-ctr".to_string(),
245 "aes256-ctr".to_string(),
246 "aes128-cbc".to_string(),
247 "3des-cbc".to_string(),
248 ],
249 );
250 assert_eq!(
251 key_method.prefs().as_str(),
252 "aes128-ctr,aes192-ctr,aes256-ctr,aes128-cbc,3des-cbc"
253 );
254 }
255
256 #[test]
257 fn test_should_tell_whether_pubkey_matches() {
258 let identity = SshAgentIdentity::Pubkey(b"hello".to_vec());
259 assert!(identity.pubkey_matches(b"hello"));
260 assert!(!identity.pubkey_matches(b"world"));
261
262 let identity = SshAgentIdentity::All;
263 assert!(identity.pubkey_matches(b"hello"));
264 }
265
266 #[test]
267 fn should_initialize_ssh_opts() {
268 let opts = SshOpts::new("localhost");
269 assert_eq!(opts.host.as_str(), "localhost");
270 assert!(opts.port.is_none());
271 assert!(opts.username.is_none());
272 assert!(opts.password.is_none());
273 assert!(opts.connection_timeout.is_none());
274 assert!(opts.config_file.is_none());
275 assert!(opts.key_storage.is_none());
276 assert!(opts.methods.is_empty());
277 }
278
279 #[test]
280 fn should_build_ssh_opts() {
281 let opts = SshOpts::new("localhost")
282 .port(22)
283 .username("foobar")
284 .password("qwerty123")
285 .connection_timeout(Duration::from_secs(10))
286 .config_file(Path::new("/home/pippo/.ssh/config"), ParseRule::STRICT)
287 .key_storage(Box::new(MockSshKeyStorage::default()))
288 .method(KeyMethod::new(
289 MethodType::CryptClientServer,
290 &[
291 "aes128-ctr".to_string(),
292 "aes192-ctr".to_string(),
293 "aes256-ctr".to_string(),
294 "aes128-cbc".to_string(),
295 "3des-cbc".to_string(),
296 ],
297 ));
298 assert_eq!(opts.host.as_str(), "localhost");
299 assert_eq!(opts.port.unwrap(), 22);
300 assert_eq!(opts.username.as_deref().unwrap(), "foobar");
301 assert_eq!(opts.password.as_deref().unwrap(), "qwerty123");
302 assert_eq!(opts.connection_timeout.unwrap(), Duration::from_secs(10));
303 assert_eq!(
304 opts.config_file.as_deref().unwrap(),
305 Path::new("/home/pippo/.ssh/config")
306 );
307 assert!(opts.key_storage.is_some());
308 assert_eq!(opts.methods.len(), 1);
309 }
310}