Skip to main content

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;
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
34// -- Ssh key storage
35
36/// This trait must be implemented in order to use ssh keys for authentication for sftp/scp.
37pub trait SshKeyStorage: Send + Sync {
38    /// Return RSA key path from host and username
39    fn resolve(&self, host: &str, username: &str) -> Option<PathBuf>;
40}
41
42// -- ssh options
43
44/// Ssh agent identity
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub enum SshAgentIdentity {
47    /// Try all identities
48    All,
49    /// Use a specific identity
50    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    /// Check if the provided public key matches the identity
67    ///
68    /// If [`SshAgentIdentity::All`] is provided, this method will always return `true`
69    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
77/// Ssh options;
78/// used to build and configure SCP/SFTP client.
79///
80/// ### Conflict resolution
81///
82/// You may specify some options that can be in conflict (e.g. `port` and `Port` parameter in ssh configuration).
83/// In these cases, the resolution is performed in this order (from highest, to lower priority):
84///
85/// 1. [`SshOpts`] attribute (e.g. `port` or `username`)
86/// 2. Ssh configuration
87///
88/// This applies also to ciphers and key exchange methods.
89///
90pub struct SshOpts {
91    /// hostname of the remote ssh server
92    host: String,
93    /// Port of the remote ssh server
94    port: Option<u16>,
95    /// Username to authenticate with
96    username: Option<String>,
97    /// Password to authenticate or to decrypt RSA key
98    password: Option<String>,
99    /// Connection timeout (default 30 seconds)
100    connection_timeout: Option<Duration>,
101    /// SSH configuration file. If provided will be parsed on connect.
102    config_file: Option<PathBuf>,
103    /// Key storage
104    key_storage: Option<Box<dyn SshKeyStorage>>,
105    /// Preferred key exchange methods.
106    methods: Vec<KeyMethod>,
107    /// Ssh config parser ruleset
108    parse_rules: ParseRule,
109    /// Ssh agent configuration for authentication
110    ssh_agent_identity: Option<SshAgentIdentity>,
111    /// tokio runtime
112    #[cfg(feature = "russh")]
113    runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
114}
115
116impl SshOpts {
117    /// Initialize [`SshOpts`].
118    /// You must define the host you want to connect to.
119    /// Host may be resolved by ssh configuration, if specified.
120    ///
121    /// Other options can be specified with other constructors.
122    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    /// Specify the port the remote server is listening to.
140    /// This option will override an eventual port specified for the current host in the ssh configuration
141    pub fn port(mut self, port: u16) -> Self {
142        self.port = Some(port);
143        self
144    }
145
146    /// Set username to log in as
147    /// This option will override an eventual username specified for the current host in the ssh configuration
148    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    /// Set password to authenticate with
154    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    /// Set connection timeout
160    /// This option will override an eventual connection timeout specified for the current host in the ssh configuration
161    pub fn connection_timeout(mut self, timeout: Duration) -> Self {
162        self.connection_timeout = Some(timeout);
163        self
164    }
165
166    /// Set configuration for ssh agent
167    ///
168    /// If `None` the ssh agent will be disabled
169    ///
170    /// If `Some(SshAgentIdentity::All)` all identities will be tried
171    /// Otherwise the provided public key will be used
172    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    /// Set SSH configuration file to read
178    ///
179    /// The supported options are:
180    ///
181    /// - Host block
182    /// - HostName
183    /// - Port
184    /// - User
185    /// - Ciphers
186    /// - MACs
187    /// - KexAlgorithms
188    /// - HostKeyAlgorithms
189    /// - ConnectionAttempts
190    /// - ConnectTimeout
191    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    /// Set key storage to read RSA keys from
198    pub fn key_storage(mut self, storage: Box<dyn SshKeyStorage>) -> Self {
199        self.key_storage = Some(storage);
200        self
201    }
202
203    /// Add key method to ssh options
204    pub fn method(mut self, method: KeyMethod) -> Self {
205        self.methods.push(method);
206        self
207    }
208
209    /// Pass tokio runtime for backends which require it
210    #[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}