remotefs_ssh/
ssh.rs

1//! ## SSH
2//!
3//! implements the file transfer for SSH based protocols: SFTP and SCP
4
5// -- ext
6use std::path::{Path, PathBuf};
7use std::time::Duration;
8
9// -- modules
10mod 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
31// -- Ssh key storage
32
33/// This trait must be implemented in order to use ssh keys for authentication for sftp/scp.
34pub trait SshKeyStorage: Send + Sync {
35    /// Return RSA key path from host and username
36    fn resolve(&self, host: &str, username: &str) -> Option<PathBuf>;
37}
38
39// -- ssh options
40
41/// Ssh agent identity
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum SshAgentIdentity {
44    /// Try all identities
45    All,
46    /// Use a specific identity
47    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    /// Check if the provided public key matches the identity
64    ///
65    /// If [`SshAgentIdentity::All`] is provided, this method will always return `true`
66    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
74/// Ssh options;
75/// used to build and configure SCP/SFTP client.
76///
77/// ### Conflict resolution
78///
79/// You may specify some options that can be in conflict (e.g. `port` and `Port` parameter in ssh configuration).
80/// In these cases, the resolution is performed in this order (from highest, to lower priority):
81///
82/// 1. [`SshOpts`] attribute (e.g. `port` or `username`)
83/// 2. Ssh configuration
84///
85/// This applies also to ciphers and key exchange methods.
86///
87pub struct SshOpts {
88    /// hostname of the remote ssh server
89    host: String,
90    /// Port of the remote ssh server
91    port: Option<u16>,
92    /// Username to authenticate with
93    username: Option<String>,
94    /// Password to authenticate or to decrypt RSA key
95    password: Option<String>,
96    /// Connection timeout (default 30 seconds)
97    connection_timeout: Option<Duration>,
98    /// SSH configuration file. If provided will be parsed on connect.
99    config_file: Option<PathBuf>,
100    /// Key storage
101    key_storage: Option<Box<dyn SshKeyStorage>>,
102    /// Preferred key exchange methods.
103    methods: Vec<KeyMethod>,
104    /// Ssh config parser ruleset
105    parse_rules: ParseRule,
106    /// Ssh agent configuration for authentication
107    ssh_agent_identity: Option<SshAgentIdentity>,
108}
109
110impl SshOpts {
111    /// Initialize [`SshOpts`].
112    /// You must define the host you want to connect to.
113    /// Host may be resolved by ssh configuration, if specified.
114    ///
115    /// Other options can be specified with other constructors.
116    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    /// Specify the port the remote server is listening to.
132    /// This option will override an eventual port specified for the current host in the ssh configuration
133    pub fn port(mut self, port: u16) -> Self {
134        self.port = Some(port);
135        self
136    }
137
138    /// Set username to log in as
139    /// This option will override an eventual username specified for the current host in the ssh configuration
140    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    /// Set password to authenticate with
146    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    /// Set connection timeout
152    /// This option will override an eventual connection timeout specified for the current host in the ssh configuration
153    pub fn connection_timeout(mut self, timeout: Duration) -> Self {
154        self.connection_timeout = Some(timeout);
155        self
156    }
157
158    /// Set configuration for ssh agent
159    ///
160    /// If `None` the ssh agent will be disabled
161    ///
162    /// If `Some(SshAgentIdentity::All)` all identities will be tried
163    /// Otherwise the provided public key will be used
164    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    /// Set SSH configuration file to read
170    ///
171    /// The supported options are:
172    ///
173    /// - Host block
174    /// - HostName
175    /// - Port
176    /// - User
177    /// - Ciphers
178    /// - MACs
179    /// - KexAlgorithms
180    /// - HostKeyAlgorithms
181    /// - ConnectionAttempts
182    /// - ConnectTimeout
183    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    /// Set key storage to read RSA keys from
190    pub fn key_storage(mut self, storage: Box<dyn SshKeyStorage>) -> Self {
191        self.key_storage = Some(storage);
192        self
193    }
194
195    /// Add key method to ssh options
196    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}