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(¶ms2);
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}