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.6"
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    /// Constructs a new [`SshConfig`] from a list of [`Host`]s.
276    ///
277    /// You can later also set the [`DefaultAlgorithms`] using [`SshConfig::default_algorithms`].
278    ///
279    /// ```rust
280    /// use ssh2_config::{DefaultAlgorithms, Host, SshConfig};
281    ///
282    /// let config = SshConfig::from_hosts(vec![/* put your hosts here */]).default_algorithms(DefaultAlgorithms::default());
283    /// ```
284    pub fn from_hosts(hosts: Vec<Host>) -> Self {
285        Self {
286            default_algorithms: DefaultAlgorithms::default(),
287            hosts,
288        }
289    }
290
291    /// Query params for a certain host. Returns [`HostParams`] for the host.
292    pub fn query<S: AsRef<str>>(&self, pattern: S) -> HostParams {
293        let mut params = HostParams::new(&self.default_algorithms);
294        // iter keys, overwrite if None top-down
295        for host in self.hosts.iter() {
296            if host.intersects(pattern.as_ref()) {
297                debug!(
298                    "Merging params for host: {:?} into params {params:?}",
299                    host.pattern
300                );
301                params.overwrite_if_none(&host.params);
302                trace!("Params after merge: {params:?}");
303            }
304        }
305        // return calculated params
306        params
307    }
308
309    /// Get an iterator over the [`Host`]s which intersect with the given host pattern
310    pub fn intersecting_hosts(&self, pattern: &str) -> impl Iterator<Item = &'_ Host> {
311        self.hosts.iter().filter(|host| host.intersects(pattern))
312    }
313
314    /// Set default algorithms for ssh.
315    ///
316    /// If you want to use the default algorithms from the system, you can use the `Default::default()` method.
317    pub fn default_algorithms(mut self, algos: DefaultAlgorithms) -> Self {
318        self.default_algorithms = algos;
319
320        self
321    }
322
323    /// Parse [`SshConfig`] from stream which implements [`BufRead`] and return parsed configuration or parser error
324    ///
325    /// ## Example
326    ///
327    /// ```rust,ignore
328    /// let mut reader = BufReader::new(
329    ///    File::open(Path::new("./assets/ssh.config"))
330    ///       .expect("Could not open configuration file")
331    /// );
332    ///
333    /// let config = SshConfig::default().parse(&mut reader, ParseRule::STRICT).expect("Failed to parse configuration");
334    /// ```
335    pub fn parse(mut self, reader: &mut impl BufRead, rules: ParseRule) -> SshParserResult<Self> {
336        parser::SshConfigParser::parse(&mut self, reader, rules, None).map(|_| self)
337    }
338
339    /// Parse `~/.ssh/config`` file and return parsed configuration [`SshConfig`] or parser error
340    pub fn parse_default_file(rules: ParseRule) -> SshParserResult<Self> {
341        let ssh_folder = dirs::home_dir()
342            .ok_or_else(|| {
343                SshParserError::Io(io::Error::new(
344                    io::ErrorKind::NotFound,
345                    "Home folder not found",
346                ))
347            })?
348            .join(".ssh");
349
350        let mut reader =
351            BufReader::new(File::open(ssh_folder.join("config")).map_err(SshParserError::Io)?);
352
353        Self::default().parse(&mut reader, rules)
354    }
355
356    /// Get list of [`Host`]s in the configuration
357    pub fn get_hosts(&self) -> &Vec<Host> {
358        &self.hosts
359    }
360}
361
362#[cfg(test)]
363fn test_log() {
364    use std::sync::Once;
365
366    static INIT: Once = Once::new();
367
368    INIT.call_once(|| {
369        let _ = env_logger::builder()
370            .filter_level(log::LevelFilter::Trace)
371            .is_test(true)
372            .try_init();
373    });
374}
375
376#[cfg(test)]
377mod tests {
378
379    use pretty_assertions::assert_eq;
380
381    use super::*;
382
383    #[test]
384    fn should_init_ssh_config() {
385        test_log();
386
387        let config = SshConfig::default();
388        assert_eq!(config.hosts.len(), 0);
389        assert_eq!(
390            config.query("192.168.1.2"),
391            HostParams::new(&DefaultAlgorithms::default())
392        );
393    }
394
395    #[test]
396    fn should_parse_default_config() -> Result<(), parser::SshParserError> {
397        test_log();
398
399        let _config = SshConfig::parse_default_file(ParseRule::ALLOW_UNKNOWN_FIELDS)?;
400        Ok(())
401    }
402
403    #[test]
404    fn should_parse_config() -> Result<(), parser::SshParserError> {
405        test_log();
406
407        use std::fs::File;
408        use std::io::BufReader;
409        use std::path::Path;
410
411        let mut reader = BufReader::new(
412            File::open(Path::new("./assets/ssh.config"))
413                .expect("Could not open configuration file"),
414        );
415
416        SshConfig::default().parse(&mut reader, ParseRule::STRICT)?;
417
418        Ok(())
419    }
420
421    #[test]
422    fn should_query_ssh_config() {
423        test_log();
424
425        let mut config = SshConfig::default();
426        // add config
427        let mut params1 = HostParams::new(&DefaultAlgorithms::default());
428        params1.bind_address = Some("0.0.0.0".to_string());
429        config.hosts.push(Host::new(
430            vec![HostClause::new(String::from("192.168.*.*"), false)],
431            params1.clone(),
432        ));
433        let mut params2 = HostParams::new(&DefaultAlgorithms::default());
434        params2.bind_interface = Some(String::from("tun0"));
435        config.hosts.push(Host::new(
436            vec![HostClause::new(String::from("192.168.10.*"), false)],
437            params2.clone(),
438        ));
439
440        let mut params3 = HostParams::new(&DefaultAlgorithms::default());
441        params3.host_name = Some("172.26.104.4".to_string());
442        config.hosts.push(Host::new(
443            vec![
444                HostClause::new(String::from("172.26.*.*"), false),
445                HostClause::new(String::from("172.26.104.4"), true),
446            ],
447            params3.clone(),
448        ));
449        // Query
450        assert_eq!(config.query("192.168.1.32"), params1);
451        // merged case
452        params1.overwrite_if_none(&params2);
453        assert_eq!(config.query("192.168.10.1"), params1);
454        // Negated case
455        assert_eq!(config.query("172.26.254.1"), params3);
456        assert_eq!(
457            config.query("172.26.104.4"),
458            HostParams::new(&DefaultAlgorithms::default())
459        );
460    }
461}