ssh2_config/
lib.rs

1#![crate_name = "ssh2_config"]
2#![crate_type = "lib"]
3
4//! # ssh2-config
5//!
6//! ssh2-config a library which provides a parser for the SSH configuration file,
7//! to be used in pair with the [ssh2](https://github.com/alexcrichton/ssh2-rs) crate.
8//!
9//! This library provides a method to parse the configuration file and returns the
10//! configuration parsed into a structure.
11//! The `SshConfig` structure provides all the attributes which **can** be used to configure the **ssh2 Session**
12//! and to resolve the host, port and username.
13//!
14//! Once the configuration has been parsed you can use the `query(&str)`
15//! method to query configuration for a certain host, based on the configured patterns.
16//! Even if many attributes are not exposed, since not supported, there is anyway a validation of the configuration,
17//! so invalid configuration will result in a parsing error.
18//!
19//! ## Get started
20//!
21//! First of you need to add **ssh2-config** to your project dependencies:
22//!
23//! ```toml
24//! ssh2-config = "^0.5"
25//! ```
26//!
27//! ## Example
28//!
29//! Here is a basic example:
30//!
31//! ```rust
32//!
33//! use ssh2::Session;
34//! use ssh2_config::{HostParams, ParseRule, SshConfig};
35//! use std::fs::File;
36//! use std::io::BufReader;
37//! use std::path::Path;
38//!
39//! let mut reader = BufReader::new(
40//!     File::open(Path::new("./assets/ssh.config"))
41//!         .expect("Could not open configuration file")
42//! );
43//!
44//! let config = SshConfig::default().parse(&mut reader, ParseRule::STRICT).expect("Failed to parse configuration");
45//!
46//! // Query parameters for your host
47//! // If there's no rule for your host, default params are returned
48//! let params = config.query("192.168.1.2");
49//!
50//! // ...
51//!
52//! // serialize configuration to string
53//! let s = config.to_string();
54//!
55//! ```
56//!
57//! ---
58//!
59//! ## How host parameters are resolved
60//!
61//! This topic has been debated a lot over the years, so finally since 0.5 this has been fixed to follow the official ssh configuration file rules, as described in the MAN <https://man.openbsd.org/OpenBSD-current/man5/ssh_config.5#DESCRIPTION>.
62//!
63//! > Unless noted otherwise, for each parameter, the first obtained value will be used. The configuration files contain sections separated by Host specifications, and that section is only applied for hosts that match one of the patterns given in the specification. The matched host name is usually the one given on the command line (see the CanonicalizeHostname option for exceptions).
64//! >
65//! > Since the first obtained value for each parameter is used, more host-specific declarations should be given near the beginning of the file, and general defaults at the end.
66//!
67//! This means that:
68//!
69//! 1. The first obtained value parsing the configuration top-down will be used
70//! 2. Host specific rules ARE not overriding default ones if they are not the first obtained value
71//! 3. If you want to achieve default values to be less specific than host specific ones, you should put the default values at the end of the configuration file using `Host *`.
72//! 4. Algorithms, so `KexAlgorithms`, `Ciphers`, `MACs` and `HostKeyAlgorithms` use a different resolvers which supports appending, excluding and heading insertions, as described in the man page at ciphers: <https://man.openbsd.org/OpenBSD-current/man5/ssh_config.5#Ciphers>.
73//!
74//! ### Resolvers examples
75//!
76//! ```ssh
77//! Compression yes
78//!
79//! Host 192.168.1.1
80//!     Compression no
81//! ```
82//!
83//! If we get rules for `192.168.1.1`, compression will be `yes`, because it's the first obtained value.
84//!
85//! ```ssh
86//! Host 192.168.1.1
87//!     Compression no
88//!
89//! Host *
90//!     Compression yes
91//! ```
92//!
93//! If we get rules for `192.168.1.1`, compression will be `no`, because it's the first obtained value.
94//!
95//! If we get rules for `172.168.1.1`, compression will be `yes`, because it's the first obtained value MATCHING the host rule.
96//!
97//! ```ssh
98//!
99//! Host 192.168.1.1
100//!     Ciphers +c
101//! ```
102//!
103//! If we get rules for `192.168.1.1`, ciphers will be `c` appended to default algorithms, which can be specified in the [`SshConfig`] constructor.
104//!
105//! ## Configuring default algorithms
106//!
107//! When you invoke [`SshConfig::default`], the default algorithms are set from openssh source code, which are the following:
108//!
109//! ```txt
110//! ca_signature_algorithms:
111//!     "ssh-ed25519",
112//!     "ecdsa-sha2-nistp256",
113//!     "ecdsa-sha2-nistp384",
114//!     "ecdsa-sha2-nistp521",
115//!     "sk-ssh-ed25519@openssh.com",
116//!     "sk-ecdsa-sha2-nistp256@openssh.com",
117//!     "rsa-sha2-512",
118//!     "rsa-sha2-256",
119//!
120//! ciphers:
121//!     "chacha20-poly1305@openssh.com",
122//!     "aes128-ctr,aes192-ctr,aes256-ctr",
123//!     "aes128-gcm@openssh.com,aes256-gcm@openssh.com",
124//!
125//! host_key_algorithms:
126//!     "ssh-ed25519-cert-v01@openssh.com",
127//!     "ecdsa-sha2-nistp256-cert-v01@openssh.com",
128//!     "ecdsa-sha2-nistp384-cert-v01@openssh.com",
129//!     "ecdsa-sha2-nistp521-cert-v01@openssh.com",
130//!     "sk-ssh-ed25519-cert-v01@openssh.com",
131//!     "sk-ecdsa-sha2-nistp256-cert-v01@openssh.com",
132//!     "rsa-sha2-512-cert-v01@openssh.com",
133//!     "rsa-sha2-256-cert-v01@openssh.com",
134//!     "ssh-ed25519",
135//!     "ecdsa-sha2-nistp256",
136//!     "ecdsa-sha2-nistp384",
137//!     "ecdsa-sha2-nistp521",
138//!     "sk-ssh-ed25519@openssh.com",
139//!     "sk-ecdsa-sha2-nistp256@openssh.com",
140//!     "rsa-sha2-512",
141//!     "rsa-sha2-256",
142//!
143//! kex_algorithms:
144//!     "sntrup761x25519-sha512",
145//!     "sntrup761x25519-sha512@openssh.com",
146//!     "mlkem768x25519-sha256",
147//!     "curve25519-sha256",
148//!     "curve25519-sha256@libssh.org",
149//!     "ecdh-sha2-nistp256",
150//!     "ecdh-sha2-nistp384",
151//!     "ecdh-sha2-nistp521",
152//!     "diffie-hellman-group-exchange-sha256",
153//!     "diffie-hellman-group16-sha512",
154//!     "diffie-hellman-group18-sha512",
155//!     "diffie-hellman-group14-sha256",
156//!     "ssh-ed25519-cert-v01@openssh.com",
157//!     "ecdsa-sha2-nistp256-cert-v01@openssh.com",
158//!     "ecdsa-sha2-nistp384-cert-v01@openssh.com",
159//!     "ecdsa-sha2-nistp521-cert-v01@openssh.com",
160//!     "sk-ssh-ed25519-cert-v01@openssh.com",
161//!     "sk-ecdsa-sha2-nistp256-cert-v01@openssh.com",
162//!     "rsa-sha2-512-cert-v01@openssh.com",
163//!     "rsa-sha2-256-cert-v01@openssh.com",
164//!     "ssh-ed25519",
165//!     "ecdsa-sha2-nistp256",
166//!     "ecdsa-sha2-nistp384",
167//!     "ecdsa-sha2-nistp521",
168//!     "sk-ssh-ed25519@openssh.com",
169//!     "sk-ecdsa-sha2-nistp256@openssh.com",
170//!     "rsa-sha2-512",
171//!     "rsa-sha2-256",
172//!     "chacha20-poly1305@openssh.com",
173//!     "aes128-ctr,aes192-ctr,aes256-ctr",
174//!     "aes128-gcm@openssh.com,aes256-gcm@openssh.com",
175//!     "chacha20-poly1305@openssh.com",
176//!     "aes128-ctr,aes192-ctr,aes256-ctr",
177//!     "aes128-gcm@openssh.com,aes256-gcm@openssh.com",
178//!     "umac-64-etm@openssh.com",
179//!     "umac-128-etm@openssh.com",
180//!     "hmac-sha2-256-etm@openssh.com",
181//!     "hmac-sha2-512-etm@openssh.com",
182//!     "hmac-sha1-etm@openssh.com",
183//!     "umac-64@openssh.com",
184//!     "umac-128@openssh.com",
185//!     "hmac-sha2-256",
186//!     "hmac-sha2-512",
187//!     "hmac-sha1",
188//!     "umac-64-etm@openssh.com",
189//!     "umac-128-etm@openssh.com",
190//!     "hmac-sha2-256-etm@openssh.com",
191//!     "hmac-sha2-512-etm@openssh.com",
192//!     "hmac-sha1-etm@openssh.com",
193//!     "umac-64@openssh.com",
194//!     "umac-128@openssh.com",
195//!     "hmac-sha2-256",
196//!     "hmac-sha2-512",
197//!     "hmac-sha1",
198//!     "none,zlib@openssh.com",
199//!     "none,zlib@openssh.com",
200//!
201//! mac:
202//!     "umac-64-etm@openssh.com",
203//!     "umac-128-etm@openssh.com",
204//!     "hmac-sha2-256-etm@openssh.com",
205//!     "hmac-sha2-512-etm@openssh.com",
206//!     "hmac-sha1-etm@openssh.com",
207//!     "umac-64@openssh.com",
208//!     "umac-128@openssh.com",
209//!     "hmac-sha2-256",
210//!     "hmac-sha2-512",
211//!     "hmac-sha1",
212//!
213//! pubkey_accepted_algorithms:
214//!     "ssh-ed25519-cert-v01@openssh.com",
215//!     "ecdsa-sha2-nistp256-cert-v01@openssh.com",
216//!     "ecdsa-sha2-nistp384-cert-v01@openssh.com",
217//!     "ecdsa-sha2-nistp521-cert-v01@openssh.com",
218//!     "sk-ssh-ed25519-cert-v01@openssh.com",
219//!     "sk-ecdsa-sha2-nistp256-cert-v01@openssh.com",
220//!     "rsa-sha2-512-cert-v01@openssh.com",
221//!     "rsa-sha2-256-cert-v01@openssh.com",
222//!     "ssh-ed25519",
223//!     "ecdsa-sha2-nistp256",
224//!     "ecdsa-sha2-nistp384",
225//!     "ecdsa-sha2-nistp521",
226//!     "sk-ssh-ed25519@openssh.com",
227//!     "sk-ecdsa-sha2-nistp256@openssh.com",
228//!     "rsa-sha2-512",
229//!     "rsa-sha2-256",
230//! ```
231//!
232//! If you want you can use a custom constructor [`SshConfig::default_algorithms`] to set your own default algorithms.
233
234#![doc(html_playground_url = "https://play.rust-lang.org")]
235
236#[macro_use]
237extern crate log;
238
239use std::fmt;
240use std::fs::File;
241use std::io::{self, BufRead, BufReader};
242use std::path::PathBuf;
243use std::time::Duration;
244// -- modules
245mod default_algorithms;
246mod host;
247mod params;
248mod parser;
249mod serializer;
250
251// -- export
252pub use self::default_algorithms::DefaultAlgorithms;
253pub use self::host::{Host, HostClause};
254pub use self::params::{Algorithms, HostParams};
255pub use self::parser::{ParseRule, SshParserError, SshParserResult};
256
257/// Describes the ssh configuration.
258/// Configuration is described in this document: <http://man.openbsd.org/OpenBSD-current/man5/ssh_config.5>
259#[derive(Debug, Clone, PartialEq, Eq, Default)]
260pub struct SshConfig {
261    /// Default algorithms for ssh.
262    default_algorithms: DefaultAlgorithms,
263    /// Rulesets for hosts.
264    /// Default config will be stored with key `*`
265    hosts: Vec<Host>,
266}
267
268impl fmt::Display for SshConfig {
269    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270        serializer::SshConfigSerializer::from(self).serialize(f)
271    }
272}
273
274impl SshConfig {
275    /// Query params for a certain host. Returns [`HostParams`] for the host.
276    pub fn query<S: AsRef<str>>(&self, pattern: S) -> HostParams {
277        let mut params = HostParams::new(&self.default_algorithms);
278        // iter keys, overwrite if None top-down
279        for host in self.hosts.iter() {
280            if host.intersects(pattern.as_ref()) {
281                debug!(
282                    "Merging params for host: {:?} into params {params:?}",
283                    host.pattern
284                );
285                params.overwrite_if_none(&host.params);
286                trace!("Params after merge: {params:?}");
287            }
288        }
289        // return calculated params
290        params
291    }
292
293    /// Get an iterator over the [`Host`]s which intersect with the given host pattern
294    pub fn intersecting_hosts(&self, pattern: &str) -> impl Iterator<Item = &'_ Host> {
295        self.hosts.iter().filter(|host| host.intersects(pattern))
296    }
297
298    /// Set default algorithms for ssh.
299    ///
300    /// If you want to use the default algorithms from the system, you can use the `Default::default()` method.
301    pub fn default_algorithms(mut self, algos: DefaultAlgorithms) -> Self {
302        self.default_algorithms = algos;
303
304        self
305    }
306
307    /// Parse [`SshConfig`] from stream which implements [`BufRead`] and return parsed configuration or parser error
308    ///
309    /// ## Example
310    ///
311    /// ```rust,ignore
312    /// let mut reader = BufReader::new(
313    ///    File::open(Path::new("./assets/ssh.config"))
314    ///       .expect("Could not open configuration file")
315    /// );
316    ///
317    /// let config = SshConfig::default().parse(&mut reader, ParseRule::STRICT).expect("Failed to parse configuration");
318    /// ```
319    pub fn parse(mut self, reader: &mut impl BufRead, rules: ParseRule) -> SshParserResult<Self> {
320        parser::SshConfigParser::parse(&mut self, reader, rules).map(|_| self)
321    }
322
323    /// Parse `~/.ssh/config`` file and return parsed configuration [`SshConfig`] or parser error
324    pub fn parse_default_file(rules: ParseRule) -> SshParserResult<Self> {
325        let ssh_folder = dirs::home_dir()
326            .ok_or_else(|| {
327                SshParserError::Io(io::Error::new(
328                    io::ErrorKind::NotFound,
329                    "Home folder not found",
330                ))
331            })?
332            .join(".ssh");
333
334        let mut reader =
335            BufReader::new(File::open(ssh_folder.join("config")).map_err(SshParserError::Io)?);
336
337        Self::default().parse(&mut reader, rules)
338    }
339
340    /// Get list of [`Host`]s in the configuration
341    pub fn get_hosts(&self) -> &Vec<Host> {
342        &self.hosts
343    }
344}
345
346#[cfg(test)]
347fn test_log() {
348    use std::sync::Once;
349
350    static INIT: Once = Once::new();
351
352    INIT.call_once(|| {
353        let _ = env_logger::builder()
354            .filter_level(log::LevelFilter::Trace)
355            .is_test(true)
356            .try_init();
357    });
358}
359
360#[cfg(test)]
361mod test {
362
363    use pretty_assertions::assert_eq;
364
365    use super::*;
366
367    #[test]
368    fn should_init_ssh_config() {
369        test_log();
370
371        let config = SshConfig::default();
372        assert_eq!(config.hosts.len(), 0);
373        assert_eq!(
374            config.query("192.168.1.2"),
375            HostParams::new(&DefaultAlgorithms::default())
376        );
377    }
378
379    #[test]
380    fn should_parse_default_config() -> Result<(), parser::SshParserError> {
381        test_log();
382
383        let _config = SshConfig::parse_default_file(ParseRule::ALLOW_UNKNOWN_FIELDS)?;
384        Ok(())
385    }
386
387    #[test]
388    fn should_parse_config() -> Result<(), parser::SshParserError> {
389        test_log();
390
391        use std::fs::File;
392        use std::io::BufReader;
393        use std::path::Path;
394
395        let mut reader = BufReader::new(
396            File::open(Path::new("./assets/ssh.config"))
397                .expect("Could not open configuration file"),
398        );
399
400        SshConfig::default().parse(&mut reader, ParseRule::STRICT)?;
401
402        Ok(())
403    }
404
405    #[test]
406    fn should_query_ssh_config() {
407        test_log();
408
409        let mut config = SshConfig::default();
410        // add config
411        let mut params1 = HostParams::new(&DefaultAlgorithms::default());
412        params1.bind_address = Some("0.0.0.0".to_string());
413        config.hosts.push(Host::new(
414            vec![HostClause::new(String::from("192.168.*.*"), false)],
415            params1.clone(),
416        ));
417        let mut params2 = HostParams::new(&DefaultAlgorithms::default());
418        params2.bind_interface = Some(String::from("tun0"));
419        config.hosts.push(Host::new(
420            vec![HostClause::new(String::from("192.168.10.*"), false)],
421            params2.clone(),
422        ));
423
424        let mut params3 = HostParams::new(&DefaultAlgorithms::default());
425        params3.host_name = Some("172.26.104.4".to_string());
426        config.hosts.push(Host::new(
427            vec![
428                HostClause::new(String::from("172.26.*.*"), false),
429                HostClause::new(String::from("172.26.104.4"), true),
430            ],
431            params3.clone(),
432        ));
433        // Query
434        assert_eq!(config.query("192.168.1.32"), params1);
435        // merged case
436        params1.overwrite_if_none(&params2);
437        assert_eq!(config.query("192.168.10.1"), params1);
438        // Negated case
439        assert_eq!(config.query("172.26.254.1"), params3);
440        assert_eq!(
441            config.query("172.26.104.4"),
442            HostParams::new(&DefaultAlgorithms::default())
443        );
444    }
445}