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