ssh2_config/
parser.rs

1//! # parser
2//!
3//! Ssh config parser
4
5use std::fs::File;
6use std::io::{BufRead, BufReader, Error as IoError};
7use std::path::PathBuf;
8use std::str::FromStr;
9use std::time::Duration;
10
11use bitflags::bitflags;
12use glob::glob;
13use thiserror::Error;
14
15use super::{Host, HostClause, HostParams, SshConfig};
16use crate::DefaultAlgorithms;
17use crate::params::AlgorithmsRule;
18
19// modules
20mod field;
21use field::Field;
22
23pub type SshParserResult<T> = Result<T, SshParserError>;
24
25/// [`SshConfigParser::update_host`] result
26#[derive(Debug, PartialEq, Eq)]
27enum UpdateHost {
28    /// Update current host
29    UpdateHost,
30    /// Add new hosts
31    NewHosts(Vec<Host>),
32}
33
34/// Ssh config parser error
35#[derive(Debug, Error)]
36pub enum SshParserError {
37    #[error("expected boolean value ('yes', 'no')")]
38    ExpectedBoolean,
39    #[error("expected port number")]
40    ExpectedPort,
41    #[error("expected unsigned value")]
42    ExpectedUnsigned,
43    #[error("expected algorithms")]
44    ExpectedAlgorithms,
45    #[error("expected path")]
46    ExpectedPath,
47    #[error("IO error: {0}")]
48    Io(#[from] IoError),
49    #[error("glob error: {0}")]
50    Glob(#[from] glob::GlobError),
51    #[error("missing argument")]
52    MissingArgument,
53    #[error("pattern error: {0}")]
54    PatternError(#[from] glob::PatternError),
55    #[error("unknown field: {0}")]
56    UnknownField(String, Vec<String>),
57    #[error("unknown field: {0}")]
58    UnsupportedField(String, Vec<String>),
59}
60
61bitflags! {
62    /// The parsing mode
63    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
64    pub struct ParseRule: u8 {
65        /// Don't allow any invalid field or value
66        const STRICT = 0b00000000;
67        /// Allow unknown field
68        const ALLOW_UNKNOWN_FIELDS = 0b00000001;
69        /// Allow unsupported fields
70        const ALLOW_UNSUPPORTED_FIELDS = 0b00000010;
71    }
72}
73
74// -- parser
75
76/// Ssh config parser
77pub(crate) struct SshConfigParser;
78
79impl SshConfigParser {
80    /// Parse reader lines and apply parameters to configuration
81    pub(crate) fn parse(
82        config: &mut SshConfig,
83        reader: &mut impl BufRead,
84        rules: ParseRule,
85        ignore_unknown: Option<Vec<String>>,
86    ) -> SshParserResult<()> {
87        // Options preceding the first `Host` section
88        // are parsed as command line options;
89        // overriding all following host-specific options.
90        //
91        // See https://github.com/openssh/openssh-portable/blob/master/readconf.c#L1173-L1176
92        let mut default_params = HostParams::new(&config.default_algorithms);
93        default_params.ignore_unknown = ignore_unknown;
94        config.hosts.push(Host::new(
95            vec![HostClause::new(String::from("*"), false)],
96            default_params,
97        ));
98
99        // Current host pointer
100        let mut current_host = config.hosts.last_mut().unwrap();
101
102        let mut lines = reader.lines();
103        // iter lines
104        loop {
105            let line = match lines.next() {
106                None => break,
107                Some(Err(err)) => return Err(SshParserError::Io(err)),
108                Some(Ok(line)) => Self::strip_comments(line.trim()),
109            };
110            if line.is_empty() {
111                continue;
112            }
113            // tokenize
114            let (field, args) = match Self::tokenize_line(&line) {
115                Ok((field, args)) => (field, args),
116                Err(SshParserError::UnknownField(field, args))
117                    if rules.intersects(ParseRule::ALLOW_UNKNOWN_FIELDS)
118                        || current_host.params.ignored(&field) =>
119                {
120                    current_host.params.ignored_fields.insert(field, args);
121                    continue;
122                }
123                Err(SshParserError::UnknownField(field, args)) => {
124                    return Err(SshParserError::UnknownField(field, args));
125                }
126                Err(err) => return Err(err),
127            };
128            // If field is block, init a new block
129            if field == Field::Host {
130                // Pass `ignore_unknown` from global overrides down into the tokenizer.
131                let mut params = HostParams::new(&config.default_algorithms);
132                params.ignore_unknown = config.hosts[0].params.ignore_unknown.clone();
133                let pattern = Self::parse_host(args)?;
134                trace!("Adding new host: {pattern:?}",);
135
136                // Add a new host
137                config.hosts.push(Host::new(pattern, params));
138                // Update current host pointer
139                current_host = config.hosts.last_mut().expect("Just added hosts");
140            } else {
141                // Update field
142                match Self::update_host(
143                    field,
144                    args,
145                    current_host,
146                    rules,
147                    &config.default_algorithms,
148                ) {
149                    Ok(UpdateHost::UpdateHost) => Ok(()),
150                    Ok(UpdateHost::NewHosts(new_hosts)) => {
151                        trace!("Adding new hosts from 'UpdateHost::NewHosts': {new_hosts:?}",);
152                        config.hosts.extend(new_hosts);
153                        current_host = config.hosts.last_mut().expect("Just added hosts");
154                        Ok(())
155                    }
156                    // If we're allowing unsupported fields to be parsed, add them to the map
157                    Err(SshParserError::UnsupportedField(field, args))
158                        if rules.intersects(ParseRule::ALLOW_UNSUPPORTED_FIELDS) =>
159                    {
160                        current_host.params.unsupported_fields.insert(field, args);
161                        Ok(())
162                    }
163                    // Eat the error here to not break the API with this change
164                    // Also it'd be weird to error on correct ssh_config's just because they're
165                    // not supported by this library
166                    Err(SshParserError::UnsupportedField(_, _)) => Ok(()),
167                    Err(e) => Err(e),
168                }?;
169            }
170        }
171
172        Ok(())
173    }
174
175    /// Strip comments from line
176    fn strip_comments(s: &str) -> String {
177        if let Some(pos) = s.find('#') {
178            s[..pos].to_string()
179        } else {
180            s.to_string()
181        }
182    }
183
184    /// Update current given host with field argument
185    fn update_host(
186        field: Field,
187        args: Vec<String>,
188        host: &mut Host,
189        rules: ParseRule,
190        default_algos: &DefaultAlgorithms,
191    ) -> SshParserResult<UpdateHost> {
192        trace!("parsing field {field:?} with args {args:?}",);
193        let params = &mut host.params;
194        match field {
195            Field::AddKeysToAgent => {
196                let value = Self::parse_boolean(args)?;
197                trace!("add_keys_to_agent: {value}",);
198                params.add_keys_to_agent = Some(value);
199            }
200            Field::BindAddress => {
201                let value = Self::parse_string(args)?;
202                trace!("bind_address: {value}",);
203                params.bind_address = Some(value);
204            }
205            Field::BindInterface => {
206                let value = Self::parse_string(args)?;
207                trace!("bind_interface: {value}",);
208                params.bind_interface = Some(value);
209            }
210            Field::CaSignatureAlgorithms => {
211                let rule = Self::parse_algos(args)?;
212                trace!("ca_signature_algorithms: {rule:?}",);
213                params.ca_signature_algorithms.apply(rule);
214            }
215            Field::CertificateFile => {
216                let value = Self::parse_path(args)?;
217                trace!("certificate_file: {value:?}",);
218                params.certificate_file = Some(value);
219            }
220            Field::Ciphers => {
221                let rule = Self::parse_algos(args)?;
222                trace!("ciphers: {rule:?}",);
223                params.ciphers.apply(rule);
224            }
225            Field::Compression => {
226                let value = Self::parse_boolean(args)?;
227                trace!("compression: {value}",);
228                params.compression = Some(value);
229            }
230            Field::ConnectTimeout => {
231                let value = Self::parse_duration(args)?;
232                trace!("connect_timeout: {value:?}",);
233                params.connect_timeout = Some(value);
234            }
235            Field::ConnectionAttempts => {
236                let value = Self::parse_unsigned(args)?;
237                trace!("connection_attempts: {value}",);
238                params.connection_attempts = Some(value);
239            }
240            Field::ForwardAgent => {
241                let value = Self::parse_boolean(args)?;
242                trace!("forward_agent: {value}",);
243                params.forward_agent = Some(value);
244            }
245            Field::Host => { /* already handled before */ }
246            Field::HostKeyAlgorithms => {
247                let rule = Self::parse_algos(args)?;
248                trace!("host_key_algorithm: {rule:?}",);
249                params.host_key_algorithms.apply(rule);
250            }
251            Field::HostName => {
252                let value = Self::parse_string(args)?;
253                trace!("host_name: {value}",);
254                params.host_name = Some(value);
255            }
256            Field::Include => {
257                return Self::include_files(
258                    args,
259                    host,
260                    rules,
261                    default_algos,
262                    host.params.ignore_unknown.clone(),
263                )
264                .map(UpdateHost::NewHosts);
265            }
266            Field::IdentityFile => {
267                let value = Self::parse_path_list(args)?;
268                trace!("identity_file: {value:?}",);
269                params.identity_file = Some(value);
270            }
271            Field::IgnoreUnknown => {
272                let value = Self::parse_comma_separated_list(args)?;
273                trace!("ignore_unknown: {value:?}",);
274                params.ignore_unknown = Some(value);
275            }
276            Field::KexAlgorithms => {
277                let rule = Self::parse_algos(args)?;
278                trace!("kex_algorithms: {rule:?}",);
279                params.kex_algorithms.apply(rule);
280            }
281            Field::Mac => {
282                let rule = Self::parse_algos(args)?;
283                trace!("mac: {rule:?}",);
284                params.mac.apply(rule);
285            }
286            Field::Port => {
287                let value = Self::parse_port(args)?;
288                trace!("port: {value}",);
289                params.port = Some(value);
290            }
291            Field::ProxyJump => {
292                let rule = Self::parse_comma_separated_list(args)?;
293                trace!("proxy_jump: {rule:?}",);
294                params.proxy_jump = Some(rule);
295            }
296            Field::PubkeyAcceptedAlgorithms => {
297                let rule = Self::parse_algos(args)?;
298                trace!("pubkey_accepted_algorithms: {rule:?}",);
299                params.pubkey_accepted_algorithms.apply(rule);
300            }
301            Field::PubkeyAuthentication => {
302                let value = Self::parse_boolean(args)?;
303                trace!("pubkey_authentication: {value}",);
304                params.pubkey_authentication = Some(value);
305            }
306            Field::RemoteForward => {
307                let value = Self::parse_port(args)?;
308                trace!("remote_forward: {value}",);
309                params.remote_forward = Some(value);
310            }
311            Field::ServerAliveInterval => {
312                let value = Self::parse_duration(args)?;
313                trace!("server_alive_interval: {value:?}",);
314                params.server_alive_interval = Some(value);
315            }
316            Field::TcpKeepAlive => {
317                let value = Self::parse_boolean(args)?;
318                trace!("tcp_keep_alive: {value}",);
319                params.tcp_keep_alive = Some(value);
320            }
321            #[cfg(target_os = "macos")]
322            Field::UseKeychain => {
323                let value = Self::parse_boolean(args)?;
324                trace!("use_keychain: {value}",);
325                params.use_keychain = Some(value);
326            }
327            Field::User => {
328                let value = Self::parse_string(args)?;
329                trace!("user: {value}",);
330                params.user = Some(value);
331            }
332            // -- unimplemented fields
333            Field::AddressFamily
334            | Field::BatchMode
335            | Field::CanonicalDomains
336            | Field::CanonicalizeFallbackLock
337            | Field::CanonicalizeHostname
338            | Field::CanonicalizeMaxDots
339            | Field::CanonicalizePermittedCNAMEs
340            | Field::CheckHostIP
341            | Field::ClearAllForwardings
342            | Field::ControlMaster
343            | Field::ControlPath
344            | Field::ControlPersist
345            | Field::DynamicForward
346            | Field::EnableSSHKeysign
347            | Field::EscapeChar
348            | Field::ExitOnForwardFailure
349            | Field::FingerprintHash
350            | Field::ForkAfterAuthentication
351            | Field::ForwardX11
352            | Field::ForwardX11Timeout
353            | Field::ForwardX11Trusted
354            | Field::GatewayPorts
355            | Field::GlobalKnownHostsFile
356            | Field::GSSAPIAuthentication
357            | Field::GSSAPIDelegateCredentials
358            | Field::HashKnownHosts
359            | Field::HostbasedAcceptedAlgorithms
360            | Field::HostbasedAuthentication
361            | Field::HostKeyAlias
362            | Field::HostbasedKeyTypes
363            | Field::IdentitiesOnly
364            | Field::IdentityAgent
365            | Field::IPQoS
366            | Field::KbdInteractiveAuthentication
367            | Field::KbdInteractiveDevices
368            | Field::KnownHostsCommand
369            | Field::LocalCommand
370            | Field::LocalForward
371            | Field::LogLevel
372            | Field::LogVerbose
373            | Field::NoHostAuthenticationForLocalhost
374            | Field::NumberOfPasswordPrompts
375            | Field::PasswordAuthentication
376            | Field::PermitLocalCommand
377            | Field::PermitRemoteOpen
378            | Field::PKCS11Provider
379            | Field::PreferredAuthentications
380            | Field::ProxyCommand
381            | Field::ProxyUseFdpass
382            | Field::PubkeyAcceptedKeyTypes
383            | Field::RekeyLimit
384            | Field::RequestTTY
385            | Field::RevokedHostKeys
386            | Field::SecruityKeyProvider
387            | Field::SendEnv
388            | Field::ServerAliveCountMax
389            | Field::SessionType
390            | Field::SetEnv
391            | Field::StdinNull
392            | Field::StreamLocalBindMask
393            | Field::StrictHostKeyChecking
394            | Field::SyslogFacility
395            | Field::UpdateHostKeys
396            | Field::UserKnownHostsFile
397            | Field::VerifyHostKeyDNS
398            | Field::VisualHostKey
399            | Field::XAuthLocation => {
400                return Err(SshParserError::UnsupportedField(field.to_string(), args));
401            }
402        }
403        Ok(UpdateHost::UpdateHost)
404    }
405
406    /// Resolve the include path for a given path match.
407    ///
408    /// If the path match is absolute, it just returns the path as-is;
409    /// if it is relative, it prepends $HOME/.ssh to it
410    fn resolve_include_path(path_match: &str) -> String {
411        #[cfg(windows)]
412        const PATH_SEPARATOR: &str = "\\";
413        #[cfg(unix)]
414        const PATH_SEPARATOR: &str = "/";
415
416        // if path match doesn't start with the path separator, prepend it
417        if path_match.starts_with(PATH_SEPARATOR) {
418            path_match.to_string()
419        } else {
420            // prepend $HOME/.ssh
421            let home_dir = dirs::home_dir().unwrap_or(PathBuf::from(PATH_SEPARATOR));
422            format!(
423                "{dir}{PATH_SEPARATOR}{path_match}",
424                dir = home_dir.join(".ssh").display()
425            )
426        }
427    }
428
429    /// include a file by parsing it and updating host rules by merging the read config to the current one for the host
430    fn include_files(
431        args: Vec<String>,
432        host: &mut Host,
433        rules: ParseRule,
434        default_algos: &DefaultAlgorithms,
435        ignore_unknown: Option<Vec<String>>,
436    ) -> SshParserResult<Vec<Host>> {
437        let path_match = Self::resolve_include_path(&Self::parse_string(args)?);
438
439        trace!("include files: {path_match}",);
440        let files = glob(&path_match)?;
441
442        let mut new_hosts = vec![];
443
444        for file in files {
445            let file = file?;
446            trace!("including file: {}", file.display());
447            let mut reader = BufReader::new(File::open(file)?);
448            let mut sub_config = SshConfig::default().default_algorithms(default_algos.clone());
449            Self::parse(&mut sub_config, &mut reader, rules, ignore_unknown.clone())?;
450
451            // merge sub-config into host
452            for pattern in &host.pattern {
453                if pattern.negated {
454                    trace!("excluding sub-config for pattern: {pattern:?}",);
455                    continue;
456                }
457                trace!("merging sub-config for pattern: {pattern:?}",);
458                let params = sub_config.query(&pattern.pattern);
459                host.params.overwrite_if_none(&params);
460            }
461
462            // merge additional hosts
463            for sub_host in sub_config.hosts.into_iter().skip(1) {
464                trace!("adding sub-host: {sub_host:?}",);
465                new_hosts.push(sub_host);
466            }
467        }
468
469        Ok(new_hosts)
470    }
471
472    /// Tokenize line if possible. Returns [`Field`] name and args as a [`Vec`] of [`String`].
473    ///
474    /// All of these lines are valid for tokenization
475    ///
476    /// ```txt
477    /// IgnoreUnknown=Pippo,Pluto
478    /// ConnectTimeout = 15
479    /// Ciphers "Pepperoni Pizza,Margherita Pizza,Hawaiian Pizza"
480    /// Macs="Pasta Carbonara,Pasta con tonno"
481    /// ```
482    ///
483    /// So lines have syntax `field args...`, `field=args...`, `field "args"`, `field="args"`
484    fn tokenize_line(line: &str) -> SshParserResult<(Field, Vec<String>)> {
485        // check what comes first, space or =?
486        let trimmed_line = line.trim();
487        // first token is the field, and it may be separated either by a space or by '='
488        let (field, other_tokens) = if trimmed_line.find('=').unwrap_or(usize::MAX)
489            < trimmed_line.find(char::is_whitespace).unwrap_or(usize::MAX)
490        {
491            trimmed_line
492                .split_once('=')
493                .ok_or(SshParserError::MissingArgument)?
494        } else {
495            trimmed_line
496                .split_once(char::is_whitespace)
497                .ok_or(SshParserError::MissingArgument)?
498        };
499
500        trace!("tokenized line '{line}' - field '{field}' with args '{other_tokens}'",);
501
502        // other tokens should trim = and whitespace
503        let other_tokens = other_tokens.trim().trim_start_matches('=').trim();
504        trace!("other tokens trimmed: '{other_tokens}'",);
505
506        // if args is quoted, don't split it
507        let args = if other_tokens.starts_with('"') && other_tokens.ends_with('"') {
508            trace!("quoted args: '{other_tokens}'",);
509            vec![other_tokens[1..other_tokens.len() - 1].to_string()]
510        } else {
511            trace!("splitting args (non-quoted): '{other_tokens}'",);
512            // split by whitespace
513            let tokens = other_tokens.split_whitespace();
514
515            tokens
516                .map(|x| x.trim().to_string())
517                .filter(|x| !x.is_empty())
518                .collect()
519        };
520
521        match Field::from_str(field) {
522            Ok(field) => Ok((field, args)),
523            Err(_) => Err(SshParserError::UnknownField(field.to_string(), args)),
524        }
525    }
526
527    // -- value parsers
528
529    /// parse boolean value
530    fn parse_boolean(args: Vec<String>) -> SshParserResult<bool> {
531        match args.first().map(|x| x.as_str()) {
532            Some("yes") => Ok(true),
533            Some("no") => Ok(false),
534            Some(_) => Err(SshParserError::ExpectedBoolean),
535            None => Err(SshParserError::MissingArgument),
536        }
537    }
538
539    /// Parse algorithms argument
540    fn parse_algos(args: Vec<String>) -> SshParserResult<AlgorithmsRule> {
541        let first = args.first().ok_or(SshParserError::MissingArgument)?;
542
543        AlgorithmsRule::from_str(first)
544    }
545
546    /// Parse comma separated list arguments
547    fn parse_comma_separated_list(args: Vec<String>) -> SshParserResult<Vec<String>> {
548        match args
549            .first()
550            .map(|x| x.split(',').map(|x| x.to_string()).collect())
551        {
552            Some(args) => Ok(args),
553            _ => Err(SshParserError::MissingArgument),
554        }
555    }
556
557    /// Parse duration argument
558    fn parse_duration(args: Vec<String>) -> SshParserResult<Duration> {
559        let value = Self::parse_unsigned(args)?;
560        Ok(Duration::from_secs(value as u64))
561    }
562
563    /// Parse host argument
564    fn parse_host(args: Vec<String>) -> SshParserResult<Vec<HostClause>> {
565        if args.is_empty() {
566            return Err(SshParserError::MissingArgument);
567        }
568        // Collect hosts
569        Ok(args
570            .into_iter()
571            .map(|x| {
572                let tokens: Vec<&str> = x.split('!').collect();
573                if tokens.len() == 2 {
574                    HostClause::new(tokens[1].to_string(), true)
575                } else {
576                    HostClause::new(tokens[0].to_string(), false)
577                }
578            })
579            .collect())
580    }
581
582    /// Parse a list of paths
583    fn parse_path_list(args: Vec<String>) -> SshParserResult<Vec<PathBuf>> {
584        if args.is_empty() {
585            return Err(SshParserError::MissingArgument);
586        }
587        args.iter()
588            .map(|x| Self::parse_path_arg(x.as_str()))
589            .collect()
590    }
591
592    /// Parse path argument
593    fn parse_path(args: Vec<String>) -> SshParserResult<PathBuf> {
594        if let Some(s) = args.first() {
595            Self::parse_path_arg(s)
596        } else {
597            Err(SshParserError::MissingArgument)
598        }
599    }
600
601    /// Parse path argument
602    fn parse_path_arg(s: &str) -> SshParserResult<PathBuf> {
603        // Remove tilde
604        let s = if s.starts_with('~') {
605            let home_dir = dirs::home_dir()
606                .unwrap_or_else(|| PathBuf::from("~"))
607                .to_string_lossy()
608                .to_string();
609            s.replacen('~', &home_dir, 1)
610        } else {
611            s.to_string()
612        };
613        Ok(PathBuf::from(s))
614    }
615
616    /// Parse port number argument
617    fn parse_port(args: Vec<String>) -> SshParserResult<u16> {
618        match args.first().map(|x| u16::from_str(x)) {
619            Some(Ok(val)) => Ok(val),
620            Some(Err(_)) => Err(SshParserError::ExpectedPort),
621            None => Err(SshParserError::MissingArgument),
622        }
623    }
624
625    /// Parse string argument
626    fn parse_string(args: Vec<String>) -> SshParserResult<String> {
627        if let Some(s) = args.into_iter().next() {
628            Ok(s)
629        } else {
630            Err(SshParserError::MissingArgument)
631        }
632    }
633
634    /// Parse unsigned argument
635    fn parse_unsigned(args: Vec<String>) -> SshParserResult<usize> {
636        match args.first().map(|x| usize::from_str(x)) {
637            Some(Ok(val)) => Ok(val),
638            Some(Err(_)) => Err(SshParserError::ExpectedUnsigned),
639            None => Err(SshParserError::MissingArgument),
640        }
641    }
642}
643
644#[cfg(test)]
645mod tests {
646
647    use std::fs::File;
648    use std::io::{BufReader, Write};
649    use std::path::Path;
650
651    use pretty_assertions::assert_eq;
652    use tempfile::NamedTempFile;
653
654    use super::*;
655    use crate::DefaultAlgorithms;
656
657    #[test]
658    fn should_parse_configuration() -> Result<(), SshParserError> {
659        crate::test_log();
660        let temp = create_ssh_config();
661        let file = File::open(temp.path()).expect("Failed to open tempfile");
662        let mut reader = BufReader::new(file);
663        let config = SshConfig::default()
664            .default_algorithms(DefaultAlgorithms {
665                ca_signature_algorithms: vec![],
666                ciphers: vec![],
667                host_key_algorithms: vec![],
668                kex_algorithms: vec![],
669                mac: vec![],
670                pubkey_accepted_algorithms: vec!["omar-crypt".to_string()],
671            })
672            .parse(&mut reader, ParseRule::STRICT)?;
673
674        // Query openssh cmdline overrides (options preceding the first `Host` section,
675        // overriding all following options)
676        let params = config.query("*");
677        assert_eq!(
678            params.ignore_unknown.as_deref().unwrap(),
679            &["Pippo", "Pluto"]
680        );
681        assert_eq!(params.compression.unwrap(), true);
682        assert_eq!(params.connection_attempts.unwrap(), 10);
683        assert_eq!(params.connect_timeout.unwrap(), Duration::from_secs(60));
684        assert_eq!(
685            params.server_alive_interval.unwrap(),
686            Duration::from_secs(40)
687        );
688        assert_eq!(params.tcp_keep_alive.unwrap(), true);
689        assert_eq!(params.ciphers.algorithms(), &["a-manella", "blowfish"]);
690        assert_eq!(
691            params.pubkey_accepted_algorithms.algorithms(),
692            &["desu", "omar-crypt", "fast-omar-crypt"]
693        );
694
695        // Query explicit all-hosts fallback options (`Host *`)
696        assert_eq!(params.ca_signature_algorithms.algorithms(), &["random"]);
697        assert_eq!(
698            params.host_key_algorithms.algorithms(),
699            &["luigi", "mario",]
700        );
701        assert_eq!(params.kex_algorithms.algorithms(), &["desu", "gigi",]);
702        assert_eq!(params.mac.algorithms(), &["concorde"]);
703        assert!(params.bind_address.is_none());
704
705        // Query 172.26.104.4, yielding cmdline overrides,
706        // explicit `Host 192.168.*.* 172.26.*.* !192.168.1.30` options,
707        // and all-hosts fallback options.
708        let params_172_26_104_4 = config.query("172.26.104.4");
709
710        // cmdline overrides
711        assert_eq!(params_172_26_104_4.add_keys_to_agent.unwrap(), true);
712        assert_eq!(params_172_26_104_4.compression.unwrap(), true);
713        assert_eq!(params_172_26_104_4.connection_attempts.unwrap(), 10);
714        assert_eq!(
715            params_172_26_104_4.connect_timeout.unwrap(),
716            Duration::from_secs(60)
717        );
718        assert_eq!(params_172_26_104_4.tcp_keep_alive.unwrap(), true);
719
720        // all-hosts fallback options, merged with host-specific options
721        assert_eq!(
722            params_172_26_104_4.ca_signature_algorithms.algorithms(),
723            &["random"]
724        );
725        assert_eq!(
726            params_172_26_104_4.ciphers.algorithms(),
727            &["a-manella", "blowfish",]
728        );
729        assert_eq!(params_172_26_104_4.mac.algorithms(), &["spyro", "deoxys"]); // use subconfig; defined before * macs
730        assert_eq!(
731            params_172_26_104_4.proxy_jump.unwrap(),
732            &["jump.example.com"]
733        ); // use subconfig; defined before * macs
734        assert_eq!(
735            params_172_26_104_4
736                .pubkey_accepted_algorithms
737                .algorithms()
738                .is_empty(), // should have removed omar-crypt
739            true
740        );
741        assert_eq!(
742            params_172_26_104_4.bind_address.as_deref().unwrap(),
743            "10.8.0.10"
744        );
745        assert_eq!(
746            params_172_26_104_4.bind_interface.as_deref().unwrap(),
747            "tun0"
748        );
749        assert_eq!(params_172_26_104_4.port.unwrap(), 2222);
750        assert_eq!(
751            params_172_26_104_4.identity_file.as_deref().unwrap(),
752            vec![
753                Path::new("/home/root/.ssh/pippo.key"),
754                Path::new("/home/root/.ssh/pluto.key")
755            ]
756        );
757        assert_eq!(params_172_26_104_4.user.as_deref().unwrap(), "omar");
758
759        // Query tostapane
760        let params_tostapane = config.query("tostapane");
761        assert_eq!(params_tostapane.compression.unwrap(), true); // it takes the first value defined, which is `yes`
762        assert_eq!(params_tostapane.connection_attempts.unwrap(), 10);
763        assert_eq!(
764            params_tostapane.connect_timeout.unwrap(),
765            Duration::from_secs(60)
766        );
767        assert_eq!(params_tostapane.tcp_keep_alive.unwrap(), true);
768        assert_eq!(params_tostapane.remote_forward.unwrap(), 88);
769        assert_eq!(params_tostapane.user.as_deref().unwrap(), "ciro-esposito");
770
771        // all-hosts fallback options
772        assert_eq!(
773            params_tostapane.ca_signature_algorithms.algorithms(),
774            &["random"]
775        );
776        assert_eq!(
777            params_tostapane.ciphers.algorithms(),
778            &["a-manella", "blowfish",]
779        );
780        assert_eq!(
781            params_tostapane.mac.algorithms(),
782            vec!["spyro".to_string(), "deoxys".to_string(),]
783        );
784        assert_eq!(
785            params_tostapane.proxy_jump.unwrap(),
786            vec![
787                "jump1.example.com".to_string(),
788                "jump2.example.com".to_string(),
789            ]
790        );
791        assert_eq!(
792            params_tostapane.pubkey_accepted_algorithms.algorithms(),
793            &["desu", "omar-crypt", "fast-omar-crypt"]
794        );
795
796        // query 192.168.1.30
797        let params_192_168_1_30 = config.query("192.168.1.30");
798
799        // host-specific options
800        assert_eq!(params_192_168_1_30.user.as_deref().unwrap(), "nutellaro");
801        assert_eq!(params_192_168_1_30.remote_forward.unwrap(), 123);
802
803        // cmdline overrides
804        assert_eq!(params_192_168_1_30.compression.unwrap(), true);
805        assert_eq!(params_192_168_1_30.connection_attempts.unwrap(), 10);
806        assert_eq!(
807            params_192_168_1_30.connect_timeout.unwrap(),
808            Duration::from_secs(60)
809        );
810        assert_eq!(params_192_168_1_30.tcp_keep_alive.unwrap(), true);
811
812        // all-hosts fallback options
813        assert_eq!(
814            params_192_168_1_30.ca_signature_algorithms.algorithms(),
815            &["random"]
816        );
817        assert_eq!(
818            params_192_168_1_30.ciphers.algorithms(),
819            &["a-manella", "blowfish"]
820        );
821        assert_eq!(params_192_168_1_30.mac.algorithms(), &["concorde"]);
822        assert_eq!(
823            params_192_168_1_30.pubkey_accepted_algorithms.algorithms(),
824            &["desu", "omar-crypt", "fast-omar-crypt"]
825        );
826
827        Ok(())
828    }
829
830    #[test]
831    fn should_allow_unknown_field() -> Result<(), SshParserError> {
832        crate::test_log();
833        let temp = create_ssh_config_with_unknown_fields();
834        let file = File::open(temp.path()).expect("Failed to open tempfile");
835        let mut reader = BufReader::new(file);
836        let _config = SshConfig::default()
837            .default_algorithms(DefaultAlgorithms::empty())
838            .parse(&mut reader, ParseRule::ALLOW_UNKNOWN_FIELDS)?;
839
840        Ok(())
841    }
842
843    #[test]
844    fn should_not_allow_unknown_field() {
845        crate::test_log();
846        let temp = create_ssh_config_with_unknown_fields();
847        let file = File::open(temp.path()).expect("Failed to open tempfile");
848        let mut reader = BufReader::new(file);
849        assert!(matches!(
850            SshConfig::default()
851                .default_algorithms(DefaultAlgorithms::empty())
852                .parse(&mut reader, ParseRule::STRICT)
853                .unwrap_err(),
854            SshParserError::UnknownField(..)
855        ));
856    }
857
858    #[test]
859    fn should_store_unknown_fields() {
860        crate::test_log();
861        let temp = create_ssh_config_with_unknown_fields();
862        let file = File::open(temp.path()).expect("Failed to open tempfile");
863        let mut reader = BufReader::new(file);
864        let config = SshConfig::default()
865            .default_algorithms(DefaultAlgorithms::empty())
866            .parse(&mut reader, ParseRule::ALLOW_UNKNOWN_FIELDS)
867            .unwrap();
868
869        let host = config.query("cross-platform");
870        assert_eq!(
871            host.ignored_fields.get("Piropero").unwrap(),
872            &vec![String::from("yes")]
873        );
874    }
875
876    #[test]
877    fn should_parse_inversed_ssh_config() {
878        crate::test_log();
879        let temp = create_inverted_ssh_config();
880        let file = File::open(temp.path()).expect("Failed to open tempfile");
881        let mut reader = BufReader::new(file);
882        let config = SshConfig::default()
883            .default_algorithms(DefaultAlgorithms::empty())
884            .parse(&mut reader, ParseRule::STRICT)
885            .unwrap();
886
887        let home_dir = dirs::home_dir()
888            .unwrap_or_else(|| PathBuf::from("~"))
889            .to_string_lossy()
890            .to_string();
891
892        let remote_host = config.query("remote-host");
893
894        // From `*-host`
895        assert_eq!(
896            remote_host.identity_file.unwrap()[0].as_path(),
897            Path::new(format!("{home_dir}/.ssh/id_rsa_good").as_str()) // because it's the first in the file
898        );
899
900        // From `remote-*`
901        assert_eq!(remote_host.host_name.unwrap(), "hostname.com");
902        assert_eq!(remote_host.user.unwrap(), "user");
903
904        // From `*`
905        assert_eq!(
906            remote_host.connect_timeout.unwrap(),
907            Duration::from_secs(15)
908        );
909    }
910
911    #[test]
912    fn should_parse_configuration_with_hosts() {
913        crate::test_log();
914        let temp = create_ssh_config_with_comments();
915
916        let file = File::open(temp.path()).expect("Failed to open tempfile");
917        let mut reader = BufReader::new(file);
918        let config = SshConfig::default()
919            .default_algorithms(DefaultAlgorithms::empty())
920            .parse(&mut reader, ParseRule::STRICT)
921            .unwrap();
922
923        let hostname = config.query("cross-platform").host_name.unwrap();
924        assert_eq!(&hostname, "hostname.com");
925
926        assert!(config.query("this").host_name.is_none());
927    }
928
929    #[test]
930    fn should_update_host_bind_address() -> Result<(), SshParserError> {
931        crate::test_log();
932        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
933        SshConfigParser::update_host(
934            Field::BindAddress,
935            vec![String::from("127.0.0.1")],
936            &mut host,
937            ParseRule::ALLOW_UNKNOWN_FIELDS,
938            &DefaultAlgorithms::empty(),
939        )?;
940        assert_eq!(host.params.bind_address.as_deref().unwrap(), "127.0.0.1");
941        Ok(())
942    }
943
944    #[test]
945    fn should_update_host_bind_interface() -> Result<(), SshParserError> {
946        crate::test_log();
947        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
948        SshConfigParser::update_host(
949            Field::BindInterface,
950            vec![String::from("aaa")],
951            &mut host,
952            ParseRule::ALLOW_UNKNOWN_FIELDS,
953            &DefaultAlgorithms::empty(),
954        )?;
955        assert_eq!(host.params.bind_interface.as_deref().unwrap(), "aaa");
956        Ok(())
957    }
958
959    #[test]
960    fn should_update_host_ca_signature_algos() -> Result<(), SshParserError> {
961        crate::test_log();
962        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
963        SshConfigParser::update_host(
964            Field::CaSignatureAlgorithms,
965            vec![String::from("a,b,c")],
966            &mut host,
967            ParseRule::ALLOW_UNKNOWN_FIELDS,
968            &DefaultAlgorithms::empty(),
969        )?;
970        assert_eq!(
971            host.params.ca_signature_algorithms.algorithms(),
972            &["a", "b", "c"]
973        );
974        Ok(())
975    }
976
977    #[test]
978    fn should_update_host_certificate_file() -> Result<(), SshParserError> {
979        crate::test_log();
980        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
981        SshConfigParser::update_host(
982            Field::CertificateFile,
983            vec![String::from("/tmp/a.crt")],
984            &mut host,
985            ParseRule::ALLOW_UNKNOWN_FIELDS,
986            &DefaultAlgorithms::empty(),
987        )?;
988        assert_eq!(
989            host.params.certificate_file.as_deref().unwrap(),
990            Path::new("/tmp/a.crt")
991        );
992        Ok(())
993    }
994
995    #[test]
996    fn should_update_host_ciphers() -> Result<(), SshParserError> {
997        crate::test_log();
998        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
999        SshConfigParser::update_host(
1000            Field::Ciphers,
1001            vec![String::from("a,b,c")],
1002            &mut host,
1003            ParseRule::ALLOW_UNKNOWN_FIELDS,
1004            &DefaultAlgorithms::empty(),
1005        )?;
1006        assert_eq!(host.params.ciphers.algorithms(), &["a", "b", "c"]);
1007        Ok(())
1008    }
1009
1010    #[test]
1011    fn should_update_host_compression() -> Result<(), SshParserError> {
1012        crate::test_log();
1013        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1014        SshConfigParser::update_host(
1015            Field::Compression,
1016            vec![String::from("yes")],
1017            &mut host,
1018            ParseRule::ALLOW_UNKNOWN_FIELDS,
1019            &DefaultAlgorithms::empty(),
1020        )?;
1021        assert_eq!(host.params.compression.unwrap(), true);
1022        Ok(())
1023    }
1024
1025    #[test]
1026    fn should_update_host_connection_attempts() -> Result<(), SshParserError> {
1027        crate::test_log();
1028        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1029        SshConfigParser::update_host(
1030            Field::ConnectionAttempts,
1031            vec![String::from("4")],
1032            &mut host,
1033            ParseRule::ALLOW_UNKNOWN_FIELDS,
1034            &DefaultAlgorithms::empty(),
1035        )?;
1036        assert_eq!(host.params.connection_attempts.unwrap(), 4);
1037        Ok(())
1038    }
1039
1040    #[test]
1041    fn should_update_host_connection_timeout() -> Result<(), SshParserError> {
1042        crate::test_log();
1043        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1044        SshConfigParser::update_host(
1045            Field::ConnectTimeout,
1046            vec![String::from("10")],
1047            &mut host,
1048            ParseRule::ALLOW_UNKNOWN_FIELDS,
1049            &DefaultAlgorithms::empty(),
1050        )?;
1051        assert_eq!(
1052            host.params.connect_timeout.unwrap(),
1053            Duration::from_secs(10)
1054        );
1055        Ok(())
1056    }
1057
1058    #[test]
1059    fn should_update_host_key_algorithms() -> Result<(), SshParserError> {
1060        crate::test_log();
1061        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1062        SshConfigParser::update_host(
1063            Field::HostKeyAlgorithms,
1064            vec![String::from("a,b,c")],
1065            &mut host,
1066            ParseRule::ALLOW_UNKNOWN_FIELDS,
1067            &DefaultAlgorithms::empty(),
1068        )?;
1069        assert_eq!(
1070            host.params.host_key_algorithms.algorithms(),
1071            &["a", "b", "c"]
1072        );
1073        Ok(())
1074    }
1075
1076    #[test]
1077    fn should_update_host_host_name() -> Result<(), SshParserError> {
1078        crate::test_log();
1079        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1080        SshConfigParser::update_host(
1081            Field::HostName,
1082            vec![String::from("192.168.1.1")],
1083            &mut host,
1084            ParseRule::ALLOW_UNKNOWN_FIELDS,
1085            &DefaultAlgorithms::empty(),
1086        )?;
1087        assert_eq!(host.params.host_name.as_deref().unwrap(), "192.168.1.1");
1088        Ok(())
1089    }
1090
1091    #[test]
1092    fn should_update_host_ignore_unknown() -> Result<(), SshParserError> {
1093        crate::test_log();
1094        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1095        SshConfigParser::update_host(
1096            Field::IgnoreUnknown,
1097            vec![String::from("a,b,c")],
1098            &mut host,
1099            ParseRule::ALLOW_UNKNOWN_FIELDS,
1100            &DefaultAlgorithms::empty(),
1101        )?;
1102        assert_eq!(
1103            host.params.ignore_unknown.as_deref().unwrap(),
1104            &["a", "b", "c"]
1105        );
1106        Ok(())
1107    }
1108
1109    #[test]
1110    fn should_update_kex_algorithms() -> Result<(), SshParserError> {
1111        crate::test_log();
1112        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1113        SshConfigParser::update_host(
1114            Field::KexAlgorithms,
1115            vec![String::from("a,b,c")],
1116            &mut host,
1117            ParseRule::ALLOW_UNKNOWN_FIELDS,
1118            &DefaultAlgorithms::empty(),
1119        )?;
1120        assert_eq!(host.params.kex_algorithms.algorithms(), &["a", "b", "c"]);
1121        Ok(())
1122    }
1123
1124    #[test]
1125    fn should_update_host_mac() -> Result<(), SshParserError> {
1126        crate::test_log();
1127        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1128        SshConfigParser::update_host(
1129            Field::Mac,
1130            vec![String::from("a,b,c")],
1131            &mut host,
1132            ParseRule::ALLOW_UNKNOWN_FIELDS,
1133            &DefaultAlgorithms::empty(),
1134        )?;
1135        assert_eq!(host.params.mac.algorithms(), &["a", "b", "c"]);
1136        Ok(())
1137    }
1138
1139    #[test]
1140    fn should_update_host_port() -> Result<(), SshParserError> {
1141        crate::test_log();
1142        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1143        SshConfigParser::update_host(
1144            Field::Port,
1145            vec![String::from("2222")],
1146            &mut host,
1147            ParseRule::ALLOW_UNKNOWN_FIELDS,
1148            &DefaultAlgorithms::empty(),
1149        )?;
1150        assert_eq!(host.params.port.unwrap(), 2222);
1151        Ok(())
1152    }
1153
1154    #[test]
1155    fn should_update_host_pubkey_accepted_algos() -> Result<(), SshParserError> {
1156        crate::test_log();
1157        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1158        SshConfigParser::update_host(
1159            Field::PubkeyAcceptedAlgorithms,
1160            vec![String::from("a,b,c")],
1161            &mut host,
1162            ParseRule::ALLOW_UNKNOWN_FIELDS,
1163            &DefaultAlgorithms::empty(),
1164        )?;
1165        assert_eq!(
1166            host.params.pubkey_accepted_algorithms.algorithms(),
1167            &["a", "b", "c"]
1168        );
1169        Ok(())
1170    }
1171
1172    #[test]
1173    fn should_update_host_pubkey_authentication() -> Result<(), SshParserError> {
1174        crate::test_log();
1175        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1176        SshConfigParser::update_host(
1177            Field::PubkeyAuthentication,
1178            vec![String::from("yes")],
1179            &mut host,
1180            ParseRule::ALLOW_UNKNOWN_FIELDS,
1181            &DefaultAlgorithms::empty(),
1182        )?;
1183        assert_eq!(host.params.pubkey_authentication.unwrap(), true);
1184        Ok(())
1185    }
1186
1187    #[test]
1188    fn should_update_host_remote_forward() -> Result<(), SshParserError> {
1189        crate::test_log();
1190        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1191        SshConfigParser::update_host(
1192            Field::RemoteForward,
1193            vec![String::from("3005")],
1194            &mut host,
1195            ParseRule::ALLOW_UNKNOWN_FIELDS,
1196            &DefaultAlgorithms::empty(),
1197        )?;
1198        assert_eq!(host.params.remote_forward.unwrap(), 3005);
1199        Ok(())
1200    }
1201
1202    #[test]
1203    fn should_update_host_server_alive_interval() -> Result<(), SshParserError> {
1204        crate::test_log();
1205        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1206        SshConfigParser::update_host(
1207            Field::ServerAliveInterval,
1208            vec![String::from("40")],
1209            &mut host,
1210            ParseRule::ALLOW_UNKNOWN_FIELDS,
1211            &DefaultAlgorithms::empty(),
1212        )?;
1213        assert_eq!(
1214            host.params.server_alive_interval.unwrap(),
1215            Duration::from_secs(40)
1216        );
1217        Ok(())
1218    }
1219
1220    #[test]
1221    fn should_update_host_tcp_keep_alive() -> Result<(), SshParserError> {
1222        crate::test_log();
1223        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1224        SshConfigParser::update_host(
1225            Field::TcpKeepAlive,
1226            vec![String::from("no")],
1227            &mut host,
1228            ParseRule::ALLOW_UNKNOWN_FIELDS,
1229            &DefaultAlgorithms::empty(),
1230        )?;
1231        assert_eq!(host.params.tcp_keep_alive.unwrap(), false);
1232        Ok(())
1233    }
1234
1235    #[test]
1236    fn should_update_host_user() -> Result<(), SshParserError> {
1237        crate::test_log();
1238        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1239        SshConfigParser::update_host(
1240            Field::User,
1241            vec![String::from("pippo")],
1242            &mut host,
1243            ParseRule::ALLOW_UNKNOWN_FIELDS,
1244            &DefaultAlgorithms::empty(),
1245        )?;
1246        assert_eq!(host.params.user.as_deref().unwrap(), "pippo");
1247        Ok(())
1248    }
1249
1250    #[test]
1251    fn should_not_update_host_if_unknown() -> Result<(), SshParserError> {
1252        crate::test_log();
1253        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1254        let result = SshConfigParser::update_host(
1255            Field::PasswordAuthentication,
1256            vec![String::from("yes")],
1257            &mut host,
1258            ParseRule::ALLOW_UNKNOWN_FIELDS,
1259            &DefaultAlgorithms::empty(),
1260        );
1261
1262        match result {
1263            Ok(_) | Err(SshParserError::UnsupportedField(_, _)) => Ok(()),
1264            Err(e) => Err(e),
1265        }?;
1266
1267        assert_eq!(host.params, HostParams::new(&DefaultAlgorithms::empty()));
1268        Ok(())
1269    }
1270
1271    #[test]
1272    fn should_update_host_if_unsupported() -> Result<(), SshParserError> {
1273        crate::test_log();
1274        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1275        let result = SshConfigParser::update_host(
1276            Field::PasswordAuthentication,
1277            vec![String::from("yes")],
1278            &mut host,
1279            ParseRule::ALLOW_UNKNOWN_FIELDS,
1280            &DefaultAlgorithms::empty(),
1281        );
1282
1283        match result {
1284            Err(SshParserError::UnsupportedField(field, _)) => {
1285                assert_eq!(field, "passwordauthentication");
1286                Ok(())
1287            }
1288            Ok(_) => Ok(()),
1289            Err(e) => Err(e),
1290        }?;
1291
1292        assert_eq!(host.params, HostParams::new(&DefaultAlgorithms::empty()));
1293        Ok(())
1294    }
1295
1296    #[test]
1297    fn should_tokenize_line() -> Result<(), SshParserError> {
1298        crate::test_log();
1299        assert_eq!(
1300            SshConfigParser::tokenize_line("HostName 192.168.*.* 172.26.*.*")?,
1301            (
1302                Field::HostName,
1303                vec![String::from("192.168.*.*"), String::from("172.26.*.*")]
1304            )
1305        );
1306        // Tokenize line with spaces
1307        assert_eq!(
1308            SshConfigParser::tokenize_line(
1309                "      HostName        192.168.*.*        172.26.*.*        "
1310            )?,
1311            (
1312                Field::HostName,
1313                vec![String::from("192.168.*.*"), String::from("172.26.*.*")]
1314            )
1315        );
1316        Ok(())
1317    }
1318
1319    #[test]
1320    fn should_not_tokenize_line() {
1321        crate::test_log();
1322        assert!(matches!(
1323            SshConfigParser::tokenize_line("Omar     yes").unwrap_err(),
1324            SshParserError::UnknownField(..)
1325        ));
1326    }
1327
1328    #[test]
1329    fn should_fail_parsing_field() {
1330        crate::test_log();
1331
1332        assert!(matches!(
1333            SshConfigParser::tokenize_line("                  ").unwrap_err(),
1334            SshParserError::MissingArgument
1335        ));
1336    }
1337
1338    #[test]
1339    fn should_parse_boolean() -> Result<(), SshParserError> {
1340        crate::test_log();
1341        assert_eq!(
1342            SshConfigParser::parse_boolean(vec![String::from("yes")])?,
1343            true
1344        );
1345        assert_eq!(
1346            SshConfigParser::parse_boolean(vec![String::from("no")])?,
1347            false
1348        );
1349        Ok(())
1350    }
1351
1352    #[test]
1353    fn should_fail_parsing_boolean() {
1354        crate::test_log();
1355        assert!(matches!(
1356            SshConfigParser::parse_boolean(vec!["boh".to_string()]).unwrap_err(),
1357            SshParserError::ExpectedBoolean
1358        ));
1359        assert!(matches!(
1360            SshConfigParser::parse_boolean(vec![]).unwrap_err(),
1361            SshParserError::MissingArgument
1362        ));
1363    }
1364
1365    #[test]
1366    fn should_parse_algos() -> Result<(), SshParserError> {
1367        crate::test_log();
1368        assert_eq!(
1369            SshConfigParser::parse_algos(vec![String::from("a,b,c,d")])?,
1370            AlgorithmsRule::Set(vec![
1371                "a".to_string(),
1372                "b".to_string(),
1373                "c".to_string(),
1374                "d".to_string(),
1375            ])
1376        );
1377
1378        assert_eq!(
1379            SshConfigParser::parse_algos(vec![String::from("a")])?,
1380            AlgorithmsRule::Set(vec!["a".to_string()])
1381        );
1382
1383        assert_eq!(
1384            SshConfigParser::parse_algos(vec![String::from("+a,b")])?,
1385            AlgorithmsRule::Append(vec!["a".to_string(), "b".to_string()])
1386        );
1387
1388        Ok(())
1389    }
1390
1391    #[test]
1392    fn should_parse_comma_separated_list() -> Result<(), SshParserError> {
1393        crate::test_log();
1394        assert_eq!(
1395            SshConfigParser::parse_comma_separated_list(vec![String::from("a,b,c,d")])?,
1396            vec![
1397                "a".to_string(),
1398                "b".to_string(),
1399                "c".to_string(),
1400                "d".to_string(),
1401            ]
1402        );
1403        assert_eq!(
1404            SshConfigParser::parse_comma_separated_list(vec![String::from("a")])?,
1405            vec!["a".to_string()]
1406        );
1407        Ok(())
1408    }
1409
1410    #[test]
1411    fn should_fail_parsing_comma_separated_list() {
1412        crate::test_log();
1413        assert!(matches!(
1414            SshConfigParser::parse_comma_separated_list(vec![]).unwrap_err(),
1415            SshParserError::MissingArgument
1416        ));
1417    }
1418
1419    #[test]
1420    fn should_parse_duration() -> Result<(), SshParserError> {
1421        crate::test_log();
1422        assert_eq!(
1423            SshConfigParser::parse_duration(vec![String::from("60")])?,
1424            Duration::from_secs(60)
1425        );
1426        Ok(())
1427    }
1428
1429    #[test]
1430    fn should_fail_parsing_duration() {
1431        crate::test_log();
1432        assert!(matches!(
1433            SshConfigParser::parse_duration(vec![String::from("AAA")]).unwrap_err(),
1434            SshParserError::ExpectedUnsigned
1435        ));
1436        assert!(matches!(
1437            SshConfigParser::parse_duration(vec![]).unwrap_err(),
1438            SshParserError::MissingArgument
1439        ));
1440    }
1441
1442    #[test]
1443    fn should_parse_host() -> Result<(), SshParserError> {
1444        crate::test_log();
1445        assert_eq!(
1446            SshConfigParser::parse_host(vec![
1447                String::from("192.168.*.*"),
1448                String::from("!192.168.1.1"),
1449                String::from("172.26.104.*"),
1450                String::from("!172.26.104.10"),
1451            ])?,
1452            vec![
1453                HostClause::new(String::from("192.168.*.*"), false),
1454                HostClause::new(String::from("192.168.1.1"), true),
1455                HostClause::new(String::from("172.26.104.*"), false),
1456                HostClause::new(String::from("172.26.104.10"), true),
1457            ]
1458        );
1459        Ok(())
1460    }
1461
1462    #[test]
1463    fn should_fail_parsing_host() {
1464        crate::test_log();
1465        assert!(matches!(
1466            SshConfigParser::parse_host(vec![]).unwrap_err(),
1467            SshParserError::MissingArgument
1468        ));
1469    }
1470
1471    #[test]
1472    fn should_parse_path() -> Result<(), SshParserError> {
1473        crate::test_log();
1474        assert_eq!(
1475            SshConfigParser::parse_path(vec![String::from("/tmp/a.txt")])?,
1476            PathBuf::from("/tmp/a.txt")
1477        );
1478        Ok(())
1479    }
1480
1481    #[test]
1482    fn should_parse_path_and_resolve_tilde() -> Result<(), SshParserError> {
1483        crate::test_log();
1484        let mut expected = dirs::home_dir().unwrap();
1485        expected.push(".ssh/id_dsa");
1486        assert_eq!(
1487            SshConfigParser::parse_path(vec![String::from("~/.ssh/id_dsa")])?,
1488            expected
1489        );
1490        Ok(())
1491    }
1492
1493    #[test]
1494    fn should_parse_path_list() -> Result<(), SshParserError> {
1495        crate::test_log();
1496        assert_eq!(
1497            SshConfigParser::parse_path_list(vec![
1498                String::from("/tmp/a.txt"),
1499                String::from("/tmp/b.txt")
1500            ])?,
1501            vec![PathBuf::from("/tmp/a.txt"), PathBuf::from("/tmp/b.txt")]
1502        );
1503        Ok(())
1504    }
1505
1506    #[test]
1507    fn should_fail_parse_path_list() {
1508        crate::test_log();
1509        assert!(matches!(
1510            SshConfigParser::parse_path_list(vec![]).unwrap_err(),
1511            SshParserError::MissingArgument
1512        ));
1513    }
1514
1515    #[test]
1516    fn should_fail_parsing_path() {
1517        crate::test_log();
1518        assert!(matches!(
1519            SshConfigParser::parse_path(vec![]).unwrap_err(),
1520            SshParserError::MissingArgument
1521        ));
1522    }
1523
1524    #[test]
1525    fn should_parse_port() -> Result<(), SshParserError> {
1526        crate::test_log();
1527        assert_eq!(SshConfigParser::parse_port(vec![String::from("22")])?, 22);
1528        Ok(())
1529    }
1530
1531    #[test]
1532    fn should_fail_parsing_port() {
1533        crate::test_log();
1534        assert!(matches!(
1535            SshConfigParser::parse_port(vec![String::from("1234567")]).unwrap_err(),
1536            SshParserError::ExpectedPort
1537        ));
1538        assert!(matches!(
1539            SshConfigParser::parse_port(vec![]).unwrap_err(),
1540            SshParserError::MissingArgument
1541        ));
1542    }
1543
1544    #[test]
1545    fn should_parse_string() -> Result<(), SshParserError> {
1546        crate::test_log();
1547        assert_eq!(
1548            SshConfigParser::parse_string(vec![String::from("foobar")])?,
1549            String::from("foobar")
1550        );
1551        Ok(())
1552    }
1553
1554    #[test]
1555    fn should_fail_parsing_string() {
1556        crate::test_log();
1557        assert!(matches!(
1558            SshConfigParser::parse_string(vec![]).unwrap_err(),
1559            SshParserError::MissingArgument
1560        ));
1561    }
1562
1563    #[test]
1564    fn should_parse_unsigned() -> Result<(), SshParserError> {
1565        crate::test_log();
1566        assert_eq!(
1567            SshConfigParser::parse_unsigned(vec![String::from("43")])?,
1568            43
1569        );
1570        Ok(())
1571    }
1572
1573    #[test]
1574    fn should_fail_parsing_unsigned() {
1575        crate::test_log();
1576        assert!(matches!(
1577            SshConfigParser::parse_unsigned(vec![String::from("abc")]).unwrap_err(),
1578            SshParserError::ExpectedUnsigned
1579        ));
1580        assert!(matches!(
1581            SshConfigParser::parse_unsigned(vec![]).unwrap_err(),
1582            SshParserError::MissingArgument
1583        ));
1584    }
1585
1586    #[test]
1587    fn should_strip_comments() {
1588        crate::test_log();
1589
1590        assert_eq!(
1591            SshConfigParser::strip_comments("host my_host # this is my fav host").as_str(),
1592            "host my_host "
1593        );
1594        assert_eq!(
1595            SshConfigParser::strip_comments("# this is a comment").as_str(),
1596            ""
1597        );
1598    }
1599
1600    #[test]
1601    fn test_should_parse_config_with_quotes_and_eq() {
1602        crate::test_log();
1603
1604        let config = create_ssh_config_with_quotes_and_eq();
1605        let file = File::open(config.path()).expect("Failed to open tempfile");
1606        let mut reader = BufReader::new(file);
1607
1608        let config = SshConfig::default()
1609            .default_algorithms(DefaultAlgorithms::empty())
1610            .parse(&mut reader, ParseRule::STRICT)
1611            .expect("Failed to parse config");
1612
1613        let params = config.query("foo");
1614
1615        // connect timeout is 15
1616        assert_eq!(
1617            params.connect_timeout.expect("unspec connect timeout"),
1618            Duration::from_secs(15)
1619        );
1620        assert_eq!(
1621            params
1622                .ignore_unknown
1623                .as_deref()
1624                .expect("unspec ignore unknown"),
1625            &["Pippo", "Pluto"]
1626        );
1627        assert_eq!(
1628            params
1629                .ciphers
1630                .algorithms()
1631                .iter()
1632                .map(|x| x.as_str())
1633                .collect::<Vec<&str>>(),
1634            &["Pepperoni Pizza", "Margherita Pizza", "Hawaiian Pizza"]
1635        );
1636        assert_eq!(
1637            params
1638                .mac
1639                .algorithms()
1640                .iter()
1641                .map(|x| x.as_str())
1642                .collect::<Vec<&str>>(),
1643            &["Pasta Carbonara", "Pasta con tonno"]
1644        );
1645    }
1646
1647    #[test]
1648    fn test_should_resolve_absolute_include_path() {
1649        crate::test_log();
1650
1651        let expected = PathBuf::from("/tmp/config.local");
1652
1653        let s = "/tmp/config.local";
1654        let resolved = PathBuf::from(SshConfigParser::resolve_include_path(s));
1655        assert_eq!(resolved, expected);
1656    }
1657
1658    #[test]
1659    fn test_should_resolve_relative_include_path() {
1660        crate::test_log();
1661
1662        let expected = dirs::home_dir()
1663            .unwrap_or_else(|| PathBuf::from("~"))
1664            .join(".ssh")
1665            .join("config.local");
1666
1667        let s = "config.local";
1668        let resolved = PathBuf::from(SshConfigParser::resolve_include_path(s));
1669        assert_eq!(resolved, expected);
1670    }
1671
1672    fn create_ssh_config_with_quotes_and_eq() -> NamedTempFile {
1673        let mut tmpfile: tempfile::NamedTempFile =
1674            tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1675        let config = r##"
1676# ssh config
1677# written by veeso
1678
1679
1680# I put a comment here just to annoy
1681
1682IgnoreUnknown=Pippo,Pluto
1683ConnectTimeout = 15
1684Ciphers "Pepperoni Pizza,Margherita Pizza,Hawaiian Pizza"
1685Macs="Pasta Carbonara,Pasta con tonno"
1686"##;
1687        tmpfile.write_all(config.as_bytes()).unwrap();
1688        tmpfile
1689    }
1690
1691    fn create_ssh_config() -> NamedTempFile {
1692        let mut tmpfile: tempfile::NamedTempFile =
1693            tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1694        let config = r##"
1695# ssh config
1696# written by veeso
1697
1698
1699        # I put a comment here just to annoy
1700
1701IgnoreUnknown Pippo,Pluto
1702
1703Compression yes
1704ConnectionAttempts          10
1705ConnectTimeout 60
1706ServerAliveInterval 40
1707TcpKeepAlive    yes
1708Ciphers     +a-manella,blowfish
1709
1710# Let's start defining some hosts
1711
1712Host 192.168.*.*    172.26.*.*      !192.168.1.30
1713    User    omar
1714    # ForwardX11 is actually not supported; I just want to see that it wont' fail parsing
1715    ForwardX11    yes
1716    BindAddress     10.8.0.10
1717    BindInterface   tun0
1718    AddKeysToAgent yes
1719    Ciphers     +coi-piedi,cazdecan,triestin-stretto
1720    IdentityFile    /home/root/.ssh/pippo.key /home/root/.ssh/pluto.key
1721    Macs     spyro,deoxys
1722    Port 2222
1723    PubkeyAcceptedAlgorithms    -omar-crypt
1724    ProxyJump jump.example.com
1725
1726Host tostapane
1727    User    ciro-esposito
1728    HostName    192.168.24.32
1729    RemoteForward   88
1730    Compression no
1731    Pippo yes
1732    Pluto 56
1733    ProxyJump jump1.example.com,jump2.example.com
1734    Macs +spyro,deoxys
1735
1736Host    192.168.1.30
1737    User    nutellaro
1738    RemoteForward   123
1739
1740Host *
1741    CaSignatureAlgorithms   random
1742    HostKeyAlgorithms   luigi,mario
1743    KexAlgorithms   desu,gigi
1744    Macs     concorde
1745    PubkeyAcceptedAlgorithms    desu,omar-crypt,fast-omar-crypt
1746"##;
1747        tmpfile.write_all(config.as_bytes()).unwrap();
1748        tmpfile
1749    }
1750
1751    fn create_inverted_ssh_config() -> NamedTempFile {
1752        let mut tmpfile: tempfile::NamedTempFile =
1753            tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1754        let config = r##"
1755Host *-host
1756    IdentityFile ~/.ssh/id_rsa_good
1757
1758Host remote-*
1759    HostName hostname.com
1760    User user
1761    IdentityFile ~/.ssh/id_rsa_bad
1762
1763Host *
1764    ConnectTimeout 15
1765    IdentityFile ~/.ssh/id_rsa_ugly
1766    "##;
1767        tmpfile.write_all(config.as_bytes()).unwrap();
1768        tmpfile
1769    }
1770
1771    fn create_ssh_config_with_comments() -> NamedTempFile {
1772        let mut tmpfile: tempfile::NamedTempFile =
1773            tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1774        let config = r##"
1775Host cross-platform # this is my fav host
1776    HostName hostname.com
1777    User user
1778    IdentityFile ~/.ssh/id_rsa_good
1779
1780Host *
1781    AddKeysToAgent yes
1782    IdentityFile ~/.ssh/id_rsa_bad
1783    "##;
1784        tmpfile.write_all(config.as_bytes()).unwrap();
1785        tmpfile
1786    }
1787
1788    fn create_ssh_config_with_unknown_fields() -> NamedTempFile {
1789        let mut tmpfile: tempfile::NamedTempFile =
1790            tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1791        let config = r##"
1792Host cross-platform # this is my fav host
1793    HostName hostname.com
1794    User user
1795    IdentityFile ~/.ssh/id_rsa_good
1796    Piropero yes
1797
1798Host *
1799    AddKeysToAgent yes
1800    IdentityFile ~/.ssh/id_rsa_bad
1801    "##;
1802        tmpfile.write_all(config.as_bytes()).unwrap();
1803        tmpfile
1804    }
1805
1806    #[test]
1807    fn test_should_parse_config_with_include() {
1808        crate::test_log();
1809
1810        let config = create_include_config();
1811        let file = File::open(config.config.path()).expect("Failed to open tempfile");
1812        let mut reader = BufReader::new(file);
1813
1814        let config = SshConfig::default()
1815            .default_algorithms(DefaultAlgorithms::empty())
1816            .parse(&mut reader, ParseRule::STRICT)
1817            .expect("Failed to parse config");
1818
1819        let default_params = config.query("unknown-host");
1820        // verify default params
1821        assert_eq!(
1822            default_params.connect_timeout.unwrap(),
1823            Duration::from_secs(60) // first read
1824        );
1825        assert_eq!(
1826            default_params.server_alive_interval.unwrap(),
1827            Duration::from_secs(40) // first read
1828        );
1829        assert_eq!(default_params.tcp_keep_alive.unwrap(), true);
1830        assert_eq!(default_params.ciphers.algorithms().is_empty(), true);
1831        assert_eq!(
1832            default_params.ignore_unknown.as_deref().unwrap(),
1833            &["Pippo", "Pluto"]
1834        );
1835        assert_eq!(default_params.compression.unwrap(), true);
1836        assert_eq!(default_params.connection_attempts.unwrap(), 10);
1837
1838        // verify include 1 overwrites the default value
1839        let glob_params = config.query("192.168.1.1");
1840        assert_eq!(
1841            glob_params.connect_timeout.unwrap(),
1842            Duration::from_secs(60)
1843        );
1844        assert_eq!(
1845            glob_params.server_alive_interval.unwrap(),
1846            Duration::from_secs(40) // first read
1847        );
1848        assert_eq!(glob_params.tcp_keep_alive.unwrap(), true);
1849        assert_eq!(glob_params.ciphers.algorithms().is_empty(), true);
1850
1851        // verify tostapane
1852        let tostapane_params = config.query("tostapane");
1853        assert_eq!(
1854            tostapane_params.connect_timeout.unwrap(),
1855            Duration::from_secs(60) // first read
1856        );
1857        assert_eq!(
1858            tostapane_params.server_alive_interval.unwrap(),
1859            Duration::from_secs(40) // first read
1860        );
1861        assert_eq!(tostapane_params.tcp_keep_alive.unwrap(), true);
1862        // verify ciphers
1863        assert_eq!(
1864            tostapane_params.ciphers.algorithms(),
1865            &[
1866                "a-manella",
1867                "blowfish",
1868                "coi-piedi",
1869                "cazdecan",
1870                "triestin-stretto"
1871            ]
1872        );
1873
1874        // verify included host (microwave)
1875        let microwave_params = config.query("microwave");
1876        assert_eq!(
1877            microwave_params.connect_timeout.unwrap(),
1878            Duration::from_secs(60) // (not) updated in inc4
1879        );
1880        assert_eq!(
1881            microwave_params.server_alive_interval.unwrap(),
1882            Duration::from_secs(40) // (not) updated in inc4
1883        );
1884        assert_eq!(
1885            microwave_params.port.unwrap(),
1886            345 // updated in inc4
1887        );
1888        assert_eq!(microwave_params.tcp_keep_alive.unwrap(), true);
1889        assert_eq!(microwave_params.ciphers.algorithms().is_empty(), true);
1890        assert_eq!(microwave_params.user.as_deref().unwrap(), "mario-rossi");
1891        assert_eq!(
1892            microwave_params.host_name.as_deref().unwrap(),
1893            "192.168.24.33"
1894        );
1895        assert_eq!(microwave_params.remote_forward.unwrap(), 88);
1896        assert_eq!(microwave_params.compression.unwrap(), true);
1897
1898        // verify included host (fridge)
1899        let fridge_params = config.query("fridge");
1900        assert_eq!(
1901            fridge_params.connect_timeout.unwrap(),
1902            Duration::from_secs(60)
1903        ); // default
1904        assert_eq!(
1905            fridge_params.server_alive_interval.unwrap(),
1906            Duration::from_secs(40)
1907        ); // default
1908        assert_eq!(fridge_params.tcp_keep_alive.unwrap(), true);
1909        assert_eq!(fridge_params.ciphers.algorithms().is_empty(), true);
1910        assert_eq!(fridge_params.user.as_deref().unwrap(), "luigi-verdi");
1911        assert_eq!(fridge_params.host_name.as_deref().unwrap(), "192.168.24.34");
1912    }
1913
1914    #[allow(dead_code)]
1915    struct ConfigWithInclude {
1916        config: NamedTempFile,
1917        inc1: NamedTempFile,
1918        inc2: NamedTempFile,
1919        inc3: NamedTempFile,
1920        inc4: NamedTempFile,
1921    }
1922
1923    fn create_include_config() -> ConfigWithInclude {
1924        let mut config_file: tempfile::NamedTempFile =
1925            tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1926        let mut inc1_file: tempfile::NamedTempFile =
1927            tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1928        let mut inc2_file: tempfile::NamedTempFile =
1929            tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1930        let mut inc3_file: tempfile::NamedTempFile =
1931            tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1932        let mut inc4_file: tempfile::NamedTempFile =
1933            tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1934
1935        let config = format!(
1936            r##"
1937# ssh config
1938# written by veeso
1939
1940
1941        # I put a comment here just to annoy
1942
1943IgnoreUnknown Pippo,Pluto
1944
1945Compression yes
1946ConnectionAttempts          10
1947ConnectTimeout 60
1948ServerAliveInterval 40
1949Include {inc1}
1950
1951# Let's start defining some hosts
1952
1953Host tostapane
1954    User    ciro-esposito
1955    HostName    192.168.24.32
1956    RemoteForward   88
1957    Compression no
1958    # Ignore unknown fields should be inherited from the global section
1959    Pippo yes
1960    Pluto 56
1961    Include {inc2}
1962
1963Include {inc3}
1964Include {inc4}
1965"##,
1966            inc1 = inc1_file.path().display(),
1967            inc2 = inc2_file.path().display(),
1968            inc3 = inc3_file.path().display(),
1969            inc4 = inc4_file.path().display(),
1970        );
1971        config_file.write_all(config.as_bytes()).unwrap();
1972
1973        // write include 1
1974        let inc1 = r##"
1975        ConnectTimeout 60
1976        ServerAliveInterval 60
1977        TcpKeepAlive    yes
1978        "##;
1979        inc1_file.write_all(inc1.as_bytes()).unwrap();
1980
1981        // write include 2
1982        let inc2 = r##"
1983        ConnectTimeout 180
1984        ServerAliveInterval 180
1985        Ciphers     +a-manella,blowfish,coi-piedi,cazdecan,triestin-stretto
1986        "##;
1987        inc2_file.write_all(inc2.as_bytes()).unwrap();
1988
1989        // write include 3 with host directive
1990        let inc3 = r##"
1991Host microwave
1992    User    mario-rossi
1993    HostName    192.168.24.33
1994    RemoteForward   88
1995    Compression no
1996    # Ignore unknown fields should be inherited from the global section
1997    Pippo yes
1998    Pluto 56
1999"##;
2000        inc3_file.write_all(inc3.as_bytes()).unwrap();
2001
2002        // write include 4 which updates a param from microwave and then create a new host
2003        let inc4 = r##"
2004    # Update microwave
2005    ServerAliveInterval 30
2006    Port 345
2007
2008# Force microwave update (it won't work)
2009Host microwave
2010    ConnectTimeout 30
2011
2012Host fridge
2013    User    luigi-verdi
2014    HostName    192.168.24.34
2015    RemoteForward   88
2016    Compression no
2017"##;
2018        inc4_file.write_all(inc4.as_bytes()).unwrap();
2019
2020        ConfigWithInclude {
2021            config: config_file,
2022            inc1: inc1_file,
2023            inc2: inc2_file,
2024            inc3: inc3_file,
2025            inc4: inc4_file,
2026        }
2027    }
2028}