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,
108//! which can be seen in the [`default_openssh_algorithms`] function documentation.
109//!
110//! If you want you can use a custom constructor [`SshConfig::default_algorithms`] to set your own default algorithms.
111
112#![doc(html_playground_url = "https://play.rust-lang.org")]
113
114#[macro_use]
115extern crate log;
116
117use std::fmt;
118use std::fs::File;
119use std::io::{self, BufRead, BufReader};
120use std::path::PathBuf;
121use std::time::Duration;
122// -- modules
123mod default_algorithms;
124mod host;
125mod params;
126mod parser;
127mod serializer;
128
129// -- export
130pub use self::default_algorithms::{
131    DefaultAlgorithms, default_algorithms as default_openssh_algorithms,
132};
133pub use self::host::{Host, HostClause};
134pub use self::params::{Algorithms, HostParams};
135pub use self::parser::{ParseRule, SshParserError, SshParserResult};
136
137/// Describes the ssh configuration.
138/// Configuration is described in this document: <http://man.openbsd.org/OpenBSD-current/man5/ssh_config.5>
139#[derive(Debug, Clone, PartialEq, Eq, Default)]
140pub struct SshConfig {
141    /// Default algorithms for ssh.
142    default_algorithms: DefaultAlgorithms,
143    /// Rulesets for hosts.
144    /// Default config will be stored with key `*`
145    hosts: Vec<Host>,
146}
147
148impl fmt::Display for SshConfig {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        serializer::SshConfigSerializer::from(self).serialize(f)
151    }
152}
153
154impl SshConfig {
155    /// Constructs a new [`SshConfig`] from a list of [`Host`]s.
156    ///
157    /// You can later also set the [`DefaultAlgorithms`] using [`SshConfig::default_algorithms`].
158    ///
159    /// ```rust
160    /// use ssh2_config::{DefaultAlgorithms, Host, SshConfig};
161    ///
162    /// let config = SshConfig::from_hosts(vec![/* put your hosts here */]).default_algorithms(DefaultAlgorithms::default());
163    /// ```
164    pub fn from_hosts(hosts: Vec<Host>) -> Self {
165        Self {
166            default_algorithms: DefaultAlgorithms::default(),
167            hosts,
168        }
169    }
170
171    /// Query params for a certain host. Returns [`HostParams`] for the host.
172    pub fn query<S: AsRef<str>>(&self, pattern: S) -> HostParams {
173        let mut params = HostParams::new(&self.default_algorithms);
174        // iter keys, overwrite if None top-down
175        for host in self.hosts.iter() {
176            if host.intersects(pattern.as_ref()) {
177                debug!(
178                    "Merging params for host: {:?} into params {params:?}",
179                    host.pattern
180                );
181                params.overwrite_if_none(&host.params);
182                trace!("Params after merge: {params:?}");
183            }
184        }
185        // return calculated params
186        params
187    }
188
189    /// Get an iterator over the [`Host`]s which intersect with the given host pattern
190    pub fn intersecting_hosts(&self, pattern: &str) -> impl Iterator<Item = &'_ Host> {
191        self.hosts.iter().filter(|host| host.intersects(pattern))
192    }
193
194    /// Set default algorithms for ssh.
195    ///
196    /// If you want to use the default algorithms from the system, you can use the `Default::default()` method.
197    pub fn default_algorithms(mut self, algos: DefaultAlgorithms) -> Self {
198        self.default_algorithms = algos;
199
200        self
201    }
202
203    /// Parse [`SshConfig`] from stream which implements [`BufRead`] and return parsed configuration or parser error
204    ///
205    /// ## Example
206    ///
207    /// ```rust,ignore
208    /// let mut reader = BufReader::new(
209    ///    File::open(Path::new("./assets/ssh.config"))
210    ///       .expect("Could not open configuration file")
211    /// );
212    ///
213    /// let config = SshConfig::default().parse(&mut reader, ParseRule::STRICT).expect("Failed to parse configuration");
214    /// ```
215    pub fn parse(mut self, reader: &mut impl BufRead, rules: ParseRule) -> SshParserResult<Self> {
216        parser::SshConfigParser::parse(&mut self, reader, rules, None).map(|_| self)
217    }
218
219    /// Parse `~/.ssh/config`` file and return parsed configuration [`SshConfig`] or parser error
220    pub fn parse_default_file(rules: ParseRule) -> SshParserResult<Self> {
221        let ssh_folder = dirs::home_dir()
222            .ok_or_else(|| {
223                SshParserError::Io(io::Error::new(
224                    io::ErrorKind::NotFound,
225                    "Home folder not found",
226                ))
227            })?
228            .join(".ssh");
229
230        let mut reader =
231            BufReader::new(File::open(ssh_folder.join("config")).map_err(SshParserError::Io)?);
232
233        Self::default().parse(&mut reader, rules)
234    }
235
236    /// Get list of [`Host`]s in the configuration
237    pub fn get_hosts(&self) -> &Vec<Host> {
238        &self.hosts
239    }
240}
241
242#[cfg(test)]
243fn test_log() {
244    use std::sync::Once;
245
246    static INIT: Once = Once::new();
247
248    INIT.call_once(|| {
249        let _ = env_logger::builder()
250            .filter_level(log::LevelFilter::Trace)
251            .is_test(true)
252            .try_init();
253    });
254}
255
256#[cfg(test)]
257mod tests {
258
259    use pretty_assertions::assert_eq;
260
261    use super::*;
262
263    #[test]
264    fn should_init_ssh_config() {
265        test_log();
266
267        let config = SshConfig::default();
268        assert_eq!(config.hosts.len(), 0);
269        assert_eq!(
270            config.query("192.168.1.2"),
271            HostParams::new(&DefaultAlgorithms::default())
272        );
273    }
274
275    #[test]
276    fn should_parse_default_config() -> Result<(), parser::SshParserError> {
277        test_log();
278
279        let _config = SshConfig::parse_default_file(ParseRule::ALLOW_UNKNOWN_FIELDS)?;
280        Ok(())
281    }
282
283    #[test]
284    fn should_parse_config() -> Result<(), parser::SshParserError> {
285        test_log();
286
287        use std::fs::File;
288        use std::io::BufReader;
289        use std::path::Path;
290
291        let mut reader = BufReader::new(
292            File::open(Path::new("./assets/ssh.config"))
293                .expect("Could not open configuration file"),
294        );
295
296        SshConfig::default().parse(&mut reader, ParseRule::STRICT)?;
297
298        Ok(())
299    }
300
301    #[test]
302    fn should_query_ssh_config() {
303        test_log();
304
305        let mut config = SshConfig::default();
306        // add config
307        let mut params1 = HostParams::new(&DefaultAlgorithms::default());
308        params1.bind_address = Some("0.0.0.0".to_string());
309        config.hosts.push(Host::new(
310            vec![HostClause::new(String::from("192.168.*.*"), false)],
311            params1.clone(),
312        ));
313        let mut params2 = HostParams::new(&DefaultAlgorithms::default());
314        params2.bind_interface = Some(String::from("tun0"));
315        config.hosts.push(Host::new(
316            vec![HostClause::new(String::from("192.168.10.*"), false)],
317            params2.clone(),
318        ));
319
320        let mut params3 = HostParams::new(&DefaultAlgorithms::default());
321        params3.host_name = Some("172.26.104.4".to_string());
322        config.hosts.push(Host::new(
323            vec![
324                HostClause::new(String::from("172.26.*.*"), false),
325                HostClause::new(String::from("172.26.104.4"), true),
326            ],
327            params3.clone(),
328        ));
329        // Query
330        assert_eq!(config.query("192.168.1.32"), params1);
331        // merged case
332        params1.overwrite_if_none(&params2);
333        assert_eq!(config.query("192.168.10.1"), params1);
334        // Negated case
335        assert_eq!(config.query("172.26.254.1"), params3);
336        assert_eq!(
337            config.query("172.26.104.4"),
338            HostParams::new(&DefaultAlgorithms::default())
339        );
340    }
341
342    #[test]
343    fn roundtrip() {
344        test_log();
345
346        // Root host
347        let mut default_host_params = HostParams::new(&DefaultAlgorithms::default());
348        default_host_params.add_keys_to_agent = Some(true);
349        let root_host_config = Host::new(
350            vec![HostClause::new(String::from("*"), false)],
351            default_host_params,
352        );
353
354        // A host using proxy jumps
355        let mut host_params = HostParams::new(&DefaultAlgorithms::default());
356        host_params.host_name = Some(String::from("192.168.10.1"));
357        host_params.proxy_jump = Some(vec![String::from("jump.example.com")]);
358        let host_config = Host::new(
359            vec![HostClause::new(String::from("server"), false)],
360            host_params,
361        );
362
363        // Create the overall config and serialise it
364        let config = SshConfig::from_hosts(vec![root_host_config, host_config]);
365        let config_string = config.to_string();
366
367        // Parse the serialised string
368        let mut reader = std::io::BufReader::new(config_string.as_bytes());
369        let config_parsed = SshConfig::default()
370            .parse(&mut reader, ParseRule::STRICT)
371            .expect("Could not parse config.");
372
373        assert_eq!(config, config_parsed);
374    }
375}