Skip to main content

ssh2_config/
params.rs

1//! # params
2//!
3//! Ssh config params for host rule
4
5mod algos;
6
7use std::collections::HashMap;
8
9pub use self::algos::Algorithms;
10pub(crate) use self::algos::AlgorithmsRule;
11use super::{Duration, PathBuf};
12use crate::DefaultAlgorithms;
13
14/// Describes the ssh configuration.
15/// Configuration is described in this document: <http://man.openbsd.org/OpenBSD-current/man5/ssh_config.5>
16/// Only arguments supported by libssh2 are implemented
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct HostParams {
19    /// Specifies whether keys should be automatically added to a running ssh-agent(1)
20    pub add_keys_to_agent: Option<bool>,
21    /// Specifies to use the specified address on the local machine as the source address of the connection
22    pub bind_address: Option<String>,
23    /// Use the specified address on the local machine as the source address of the connection
24    pub bind_interface: Option<String>,
25    /// Specifies which algorithms are allowed for signing of certificates by certificate authorities
26    pub ca_signature_algorithms: Algorithms,
27    /// Specifies a file from which the user's certificate is read
28    pub certificate_file: Option<PathBuf>,
29    /// Specifies the ciphers allowed for protocol version 2 in order of preference
30    pub ciphers: Algorithms,
31    /// Specifies whether to use compression
32    pub compression: Option<bool>,
33    /// Specifies the number of attempts to make before exiting
34    pub connection_attempts: Option<usize>,
35    /// Specifies the timeout used when connecting to the SSH server
36    pub connect_timeout: Option<Duration>,
37    /// Specifies whether the connection to the authentication agent (if any) will be forwarded to the remote machine
38    pub forward_agent: Option<bool>,
39    /// Specifies the host key signature algorithms that the client wants to use in order of preference
40    pub host_key_algorithms: Algorithms,
41    /// Specifies the real host name to log into
42    pub host_name: Option<String>,
43    /// Specifies the path of the identity file to be used when authenticating.
44    /// More than one file can be specified.
45    /// If more than one file is specified, they will be read in order.
46    /// There may be multiple lines of this directive; in case subsequent lines will be appended to the previous ones.
47    pub identity_file: Option<Vec<PathBuf>>,
48    /// Specifies a pattern-list of unknown options to be ignored if they are encountered in configuration parsing
49    pub ignore_unknown: Option<Vec<String>>,
50    /// Specifies the available KEX (Key Exchange) algorithms
51    pub kex_algorithms: Algorithms,
52    /// Specifies the MAC (message authentication code) algorithms in order of preference
53    pub mac: Algorithms,
54    /// Specifies the port number to connect on the remote host.
55    pub port: Option<u16>,
56    /// Specifies one or more jump proxies as either \[user@\]host\[:port\] or an ssh URI
57    pub proxy_jump: Option<Vec<String>>,
58    /// Specifies the signature algorithms that will be used for public key authentication
59    pub pubkey_accepted_algorithms: Algorithms,
60    /// Specifies whether to try public key authentication using SSH keys
61    pub pubkey_authentication: Option<bool>,
62    /// Specifies that a TCP port on the remote machine be forwarded over the secure channel
63    pub remote_forward: Option<u16>,
64    /// Sets a timeout interval in seconds after which if no data has been received from the server, keep alive will be sent
65    pub server_alive_interval: Option<Duration>,
66    /// Specifies whether to send TCP keepalives to the other side
67    pub tcp_keep_alive: Option<bool>,
68    #[cfg(target_os = "macos")]
69    /// specifies whether the system should search for passphrases in the user's keychain when attempting to use a particular key
70    pub use_keychain: Option<bool>,
71    /// Specifies the user to log in as.
72    pub user: Option<String>,
73    /// fields that the parser wasn't able to parse
74    pub ignored_fields: HashMap<String, Vec<String>>,
75    /// fields that the parser was able to parse but ignored
76    pub unsupported_fields: HashMap<String, Vec<String>>,
77}
78
79impl HostParams {
80    /// Create a new [`HostParams`] object with the [`DefaultAlgorithms`]
81    pub fn new(default_algorithms: &DefaultAlgorithms) -> Self {
82        Self {
83            add_keys_to_agent: None,
84            bind_address: None,
85            bind_interface: None,
86            ca_signature_algorithms: Algorithms::new(&default_algorithms.ca_signature_algorithms),
87            certificate_file: None,
88            ciphers: Algorithms::new(&default_algorithms.ciphers),
89            compression: None,
90            connection_attempts: None,
91            connect_timeout: None,
92            forward_agent: None,
93            host_key_algorithms: Algorithms::new(&default_algorithms.host_key_algorithms),
94            host_name: None,
95            identity_file: None,
96            ignore_unknown: None,
97            kex_algorithms: Algorithms::new(&default_algorithms.kex_algorithms),
98            mac: Algorithms::new(&default_algorithms.mac),
99            port: None,
100            proxy_jump: None,
101            pubkey_accepted_algorithms: Algorithms::new(
102                &default_algorithms.pubkey_accepted_algorithms,
103            ),
104            pubkey_authentication: None,
105            remote_forward: None,
106            server_alive_interval: None,
107            tcp_keep_alive: None,
108            #[cfg(target_os = "macos")]
109            use_keychain: None,
110            user: None,
111            ignored_fields: HashMap::new(),
112            unsupported_fields: HashMap::new(),
113        }
114    }
115
116    /// Return whether a certain `param` is in the ignored list
117    pub(crate) fn ignored(&self, param: &str) -> bool {
118        self.ignore_unknown
119            .as_ref()
120            .map(|x| x.iter().any(|x| x.as_str() == param))
121            .unwrap_or(false)
122    }
123
124    /// Given a [`HostParams`] object `b`, it will overwrite all the params from `self` only if they are [`None`]
125    pub fn overwrite_if_none(&mut self, b: &Self) {
126        self.add_keys_to_agent = self.add_keys_to_agent.or(b.add_keys_to_agent);
127        self.bind_address = self.bind_address.clone().or_else(|| b.bind_address.clone());
128        self.bind_interface = self
129            .bind_interface
130            .clone()
131            .or_else(|| b.bind_interface.clone());
132        self.certificate_file = self
133            .certificate_file
134            .clone()
135            .or_else(|| b.certificate_file.clone());
136        self.compression = self.compression.or(b.compression);
137        self.connection_attempts = self.connection_attempts.or(b.connection_attempts);
138        self.connect_timeout = self.connect_timeout.or(b.connect_timeout);
139        self.forward_agent = self.forward_agent.or(b.forward_agent);
140        self.host_name = self.host_name.clone().or_else(|| b.host_name.clone());
141        // IdentityFile accumulates across Host blocks (unlike other directives)
142        match (&mut self.identity_file, &b.identity_file) {
143            (Some(existing), Some(other)) => existing.extend(other.clone()),
144            (None, Some(other)) => self.identity_file = Some(other.clone()),
145            _ => {}
146        }
147        self.ignore_unknown = self
148            .ignore_unknown
149            .clone()
150            .or_else(|| b.ignore_unknown.clone());
151        self.port = self.port.or(b.port);
152        self.proxy_jump = self.proxy_jump.clone().or_else(|| b.proxy_jump.clone());
153        self.pubkey_authentication = self.pubkey_authentication.or(b.pubkey_authentication);
154        self.remote_forward = self.remote_forward.or(b.remote_forward);
155        self.server_alive_interval = self.server_alive_interval.or(b.server_alive_interval);
156        #[cfg(target_os = "macos")]
157        {
158            self.use_keychain = self.use_keychain.or(b.use_keychain);
159        }
160        self.tcp_keep_alive = self.tcp_keep_alive.or(b.tcp_keep_alive);
161        self.user = self.user.clone().or_else(|| b.user.clone());
162        for (ignored_field, args) in &b.ignored_fields {
163            if !self.ignored_fields.contains_key(ignored_field) {
164                self.ignored_fields
165                    .insert(ignored_field.to_owned(), args.to_owned());
166            }
167        }
168        for (unsupported_field, args) in &b.unsupported_fields {
169            if !self.unsupported_fields.contains_key(unsupported_field) {
170                self.unsupported_fields
171                    .insert(unsupported_field.to_owned(), args.to_owned());
172            }
173        }
174
175        // merge algos if default and b is not default
176        if self.ca_signature_algorithms.is_default() && !b.ca_signature_algorithms.is_default() {
177            self.ca_signature_algorithms = b.ca_signature_algorithms.clone();
178        }
179        if self.ciphers.is_default() && !b.ciphers.is_default() {
180            self.ciphers = b.ciphers.clone();
181        }
182        if self.host_key_algorithms.is_default() && !b.host_key_algorithms.is_default() {
183            self.host_key_algorithms = b.host_key_algorithms.clone();
184        }
185        if self.kex_algorithms.is_default() && !b.kex_algorithms.is_default() {
186            self.kex_algorithms = b.kex_algorithms.clone();
187        }
188        if self.mac.is_default() && !b.mac.is_default() {
189            self.mac = b.mac.clone();
190        }
191        if self.pubkey_accepted_algorithms.is_default()
192            && !b.pubkey_accepted_algorithms.is_default()
193        {
194            self.pubkey_accepted_algorithms = b.pubkey_accepted_algorithms.clone();
195        }
196    }
197}
198
199#[cfg(test)]
200mod tests {
201
202    use std::str::FromStr;
203
204    use pretty_assertions::assert_eq;
205
206    use super::*;
207    use crate::params::algos::AlgorithmsRule;
208
209    #[test]
210    fn should_initialize_params() {
211        let params = HostParams::new(&DefaultAlgorithms::default());
212        assert!(params.add_keys_to_agent.is_none());
213        assert!(params.bind_address.is_none());
214        assert!(params.bind_interface.is_none());
215        assert_eq!(
216            params.ca_signature_algorithms.algorithms(),
217            DefaultAlgorithms::default().ca_signature_algorithms
218        );
219        assert!(params.certificate_file.is_none());
220        assert_eq!(
221            params.ciphers.algorithms(),
222            DefaultAlgorithms::default().ciphers
223        );
224        assert!(params.compression.is_none());
225        assert!(params.connection_attempts.is_none());
226        assert!(params.connect_timeout.is_none());
227        assert!(params.forward_agent.is_none());
228        assert_eq!(
229            params.host_key_algorithms.algorithms(),
230            DefaultAlgorithms::default().host_key_algorithms
231        );
232        assert!(params.host_name.is_none());
233        assert!(params.identity_file.is_none());
234        assert!(params.ignore_unknown.is_none());
235        assert_eq!(
236            params.kex_algorithms.algorithms(),
237            DefaultAlgorithms::default().kex_algorithms
238        );
239        assert_eq!(params.mac.algorithms(), DefaultAlgorithms::default().mac);
240        assert!(params.port.is_none());
241        assert!(params.proxy_jump.is_none());
242        assert_eq!(
243            params.pubkey_accepted_algorithms.algorithms(),
244            DefaultAlgorithms::default().pubkey_accepted_algorithms
245        );
246        assert!(params.pubkey_authentication.is_none());
247        assert!(params.remote_forward.is_none());
248        assert!(params.server_alive_interval.is_none());
249        #[cfg(target_os = "macos")]
250        assert!(params.use_keychain.is_none());
251        assert!(params.tcp_keep_alive.is_none());
252    }
253
254    #[test]
255    fn test_should_overwrite_if_none() {
256        let mut params = HostParams::new(&DefaultAlgorithms::default());
257        params.bind_address = Some(String::from("pippo"));
258
259        let mut b = HostParams::new(&DefaultAlgorithms::default());
260        b.bind_address = Some(String::from("pluto"));
261        b.bind_interface = Some(String::from("tun0"));
262        b.ciphers
263            .apply(AlgorithmsRule::from_str("c,d").expect("parse error"));
264
265        params.overwrite_if_none(&b);
266        assert_eq!(params.bind_address.unwrap(), "pippo");
267        assert_eq!(params.bind_interface.unwrap(), "tun0");
268
269        // algos
270        assert_eq!(
271            params.ciphers.algorithms(),
272            vec!["c".to_string(), "d".to_string()]
273        );
274    }
275
276    #[test]
277    fn test_ignored_returns_false_when_none() {
278        let params = HostParams::new(&DefaultAlgorithms::default());
279        assert!(!params.ignored("SomeParam"));
280    }
281
282    #[test]
283    fn test_ignored_returns_false_when_not_in_list() {
284        let mut params = HostParams::new(&DefaultAlgorithms::default());
285        params.ignore_unknown = Some(vec!["Param1".to_string(), "Param2".to_string()]);
286        assert!(!params.ignored("OtherParam"));
287    }
288
289    #[test]
290    fn test_ignored_returns_true_when_in_list() {
291        let mut params = HostParams::new(&DefaultAlgorithms::default());
292        params.ignore_unknown = Some(vec!["Param1".to_string(), "Param2".to_string()]);
293        assert!(params.ignored("Param1"));
294        assert!(params.ignored("Param2"));
295    }
296
297    #[test]
298    fn test_overwrite_if_none_all_fields() {
299        let mut params = HostParams::new(&DefaultAlgorithms::empty());
300
301        let mut b = HostParams::new(&DefaultAlgorithms::empty());
302        b.add_keys_to_agent = Some(true);
303        b.bind_address = Some(String::from("addr"));
304        b.bind_interface = Some(String::from("iface"));
305        b.certificate_file = Some(std::path::PathBuf::from("/cert"));
306        b.compression = Some(true);
307        b.connection_attempts = Some(5);
308        b.connect_timeout = Some(Duration::from_secs(30));
309        b.forward_agent = Some(true);
310        b.host_name = Some(String::from("host"));
311        b.identity_file = Some(vec![std::path::PathBuf::from("/id")]);
312        b.ignore_unknown = Some(vec!["field".to_string()]);
313        b.port = Some(22);
314        b.proxy_jump = Some(vec!["proxy".to_string()]);
315        b.pubkey_authentication = Some(true);
316        b.remote_forward = Some(8080);
317        b.server_alive_interval = Some(Duration::from_secs(60));
318        b.tcp_keep_alive = Some(true);
319        #[cfg(target_os = "macos")]
320        {
321            b.use_keychain = Some(true);
322        }
323        b.user = Some(String::from("user"));
324        b.ignored_fields
325            .insert("custom".to_string(), vec!["value".to_string()]);
326        b.unsupported_fields
327            .insert("unsupported".to_string(), vec!["val".to_string()]);
328
329        params.overwrite_if_none(&b);
330
331        assert_eq!(params.add_keys_to_agent, Some(true));
332        assert_eq!(params.bind_address, Some(String::from("addr")));
333        assert_eq!(params.bind_interface, Some(String::from("iface")));
334        assert_eq!(
335            params.certificate_file,
336            Some(std::path::PathBuf::from("/cert"))
337        );
338        assert_eq!(params.compression, Some(true));
339        assert_eq!(params.connection_attempts, Some(5));
340        assert_eq!(params.connect_timeout, Some(Duration::from_secs(30)));
341        assert_eq!(params.forward_agent, Some(true));
342        assert_eq!(params.host_name, Some(String::from("host")));
343        assert_eq!(
344            params.identity_file,
345            Some(vec![std::path::PathBuf::from("/id")])
346        );
347        assert_eq!(params.ignore_unknown, Some(vec!["field".to_string()]));
348        assert_eq!(params.port, Some(22));
349        assert_eq!(params.proxy_jump, Some(vec!["proxy".to_string()]));
350        assert_eq!(params.pubkey_authentication, Some(true));
351        assert_eq!(params.remote_forward, Some(8080));
352        assert_eq!(params.server_alive_interval, Some(Duration::from_secs(60)));
353        assert_eq!(params.tcp_keep_alive, Some(true));
354        #[cfg(target_os = "macos")]
355        assert_eq!(params.use_keychain, Some(true));
356        assert_eq!(params.user, Some(String::from("user")));
357        assert!(params.ignored_fields.contains_key("custom"));
358        assert!(params.unsupported_fields.contains_key("unsupported"));
359    }
360
361    #[test]
362    fn test_overwrite_if_none_does_not_overwrite_existing() {
363        let mut params = HostParams::new(&DefaultAlgorithms::empty());
364        params.add_keys_to_agent = Some(false);
365        params.bind_address = Some(String::from("original"));
366        params.compression = Some(false);
367        params.port = Some(2222);
368        params.user = Some(String::from("original_user"));
369        params
370            .ignored_fields
371            .insert("existing".to_string(), vec!["val1".to_string()]);
372        params
373            .unsupported_fields
374            .insert("existing_unsup".to_string(), vec!["val1".to_string()]);
375
376        let mut b = HostParams::new(&DefaultAlgorithms::empty());
377        b.add_keys_to_agent = Some(true);
378        b.bind_address = Some(String::from("new"));
379        b.compression = Some(true);
380        b.port = Some(22);
381        b.user = Some(String::from("new_user"));
382        b.ignored_fields
383            .insert("existing".to_string(), vec!["val2".to_string()]);
384        b.unsupported_fields
385            .insert("existing_unsup".to_string(), vec!["val2".to_string()]);
386
387        params.overwrite_if_none(&b);
388
389        // Should keep original values
390        assert_eq!(params.add_keys_to_agent, Some(false));
391        assert_eq!(params.bind_address, Some(String::from("original")));
392        assert_eq!(params.compression, Some(false));
393        assert_eq!(params.port, Some(2222));
394        assert_eq!(params.user, Some(String::from("original_user")));
395        assert_eq!(
396            params.ignored_fields.get("existing"),
397            Some(&vec!["val1".to_string()])
398        );
399        assert_eq!(
400            params.unsupported_fields.get("existing_unsup"),
401            Some(&vec!["val1".to_string()])
402        );
403    }
404
405    #[test]
406    fn test_overwrite_if_none_algorithms_when_self_is_default() {
407        let mut params = HostParams::new(&DefaultAlgorithms::empty());
408
409        let mut b = HostParams::new(&DefaultAlgorithms::empty());
410        b.ca_signature_algorithms
411            .apply(AlgorithmsRule::from_str("ca-algo").expect("parse error"));
412        b.host_key_algorithms
413            .apply(AlgorithmsRule::from_str("hk-algo").expect("parse error"));
414        b.kex_algorithms
415            .apply(AlgorithmsRule::from_str("kex-algo").expect("parse error"));
416        b.mac
417            .apply(AlgorithmsRule::from_str("mac-algo").expect("parse error"));
418        b.pubkey_accepted_algorithms
419            .apply(AlgorithmsRule::from_str("pk-algo").expect("parse error"));
420
421        params.overwrite_if_none(&b);
422
423        assert_eq!(
424            params.ca_signature_algorithms.algorithms(),
425            &["ca-algo".to_string()]
426        );
427        assert_eq!(
428            params.host_key_algorithms.algorithms(),
429            &["hk-algo".to_string()]
430        );
431        assert_eq!(
432            params.kex_algorithms.algorithms(),
433            &["kex-algo".to_string()]
434        );
435        assert_eq!(params.mac.algorithms(), &["mac-algo".to_string()]);
436        assert_eq!(
437            params.pubkey_accepted_algorithms.algorithms(),
438            &["pk-algo".to_string()]
439        );
440    }
441
442    #[test]
443    fn test_overwrite_if_none_algorithms_when_self_is_not_default() {
444        let mut params = HostParams::new(&DefaultAlgorithms::empty());
445        params
446            .ciphers
447            .apply(AlgorithmsRule::from_str("self-cipher").expect("parse error"));
448
449        let mut b = HostParams::new(&DefaultAlgorithms::empty());
450        b.ciphers
451            .apply(AlgorithmsRule::from_str("other-cipher").expect("parse error"));
452
453        params.overwrite_if_none(&b);
454
455        // Self's cipher should remain since it was already overridden
456        assert_eq!(params.ciphers.algorithms(), &["self-cipher".to_string()]);
457    }
458
459    #[test]
460    fn test_overwrite_if_none_accumulates_identity_files() {
461        let mut params = HostParams::new(&DefaultAlgorithms::empty());
462        params.identity_file = Some(vec![std::path::PathBuf::from("/path/to/key1")]);
463
464        let mut b = HostParams::new(&DefaultAlgorithms::empty());
465        b.identity_file = Some(vec![
466            std::path::PathBuf::from("/path/to/key2"),
467            std::path::PathBuf::from("/path/to/key3"),
468        ]);
469
470        params.overwrite_if_none(&b);
471
472        // Identity files should be accumulated, not replaced
473        assert_eq!(
474            params.identity_file,
475            Some(vec![
476                std::path::PathBuf::from("/path/to/key1"),
477                std::path::PathBuf::from("/path/to/key2"),
478                std::path::PathBuf::from("/path/to/key3"),
479            ])
480        );
481    }
482
483    #[test]
484    fn test_overwrite_if_none_identity_files_when_self_is_none() {
485        let mut params = HostParams::new(&DefaultAlgorithms::empty());
486
487        let mut b = HostParams::new(&DefaultAlgorithms::empty());
488        b.identity_file = Some(vec![std::path::PathBuf::from("/path/to/key1")]);
489
490        params.overwrite_if_none(&b);
491
492        assert_eq!(
493            params.identity_file,
494            Some(vec![std::path::PathBuf::from("/path/to/key1")])
495        );
496    }
497}