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            let home_dir = dirs::home_dir().unwrap_or(PathBuf::from(PATH_SEPARATOR));
421            // if path_match starts with `~`, strip it and prepend $HOME
422            if let Some(stripped) = path_match.strip_prefix("~") {
423                format!("{dir}{PATH_SEPARATOR}{stripped}", dir = home_dir.display())
424            } else {
425                // prepend $HOME/.ssh
426                format!(
427                    "{dir}{PATH_SEPARATOR}{path_match}",
428                    dir = home_dir.join(".ssh").display()
429                )
430            }
431        }
432    }
433
434    /// include a file by parsing it and updating host rules by merging the read config to the current one for the host
435    fn include_files(
436        args: Vec<String>,
437        host: &mut Host,
438        rules: ParseRule,
439        default_algos: &DefaultAlgorithms,
440        ignore_unknown: Option<Vec<String>>,
441    ) -> SshParserResult<Vec<Host>> {
442        let path_match = Self::resolve_include_path(&Self::parse_string(args)?);
443
444        trace!("include files: {path_match}",);
445        let files = glob(&path_match)?;
446
447        let mut new_hosts = vec![];
448
449        for file in files {
450            let file = file?;
451            trace!("including file: {}", file.display());
452            let mut reader = BufReader::new(File::open(file)?);
453            let mut sub_config = SshConfig::default().default_algorithms(default_algos.clone());
454            Self::parse(&mut sub_config, &mut reader, rules, ignore_unknown.clone())?;
455
456            // merge sub-config into host
457            for pattern in &host.pattern {
458                if pattern.negated {
459                    trace!("excluding sub-config for pattern: {pattern:?}",);
460                    continue;
461                }
462                trace!("merging sub-config for pattern: {pattern:?}",);
463                let params = sub_config.query(&pattern.pattern);
464                host.params.overwrite_if_none(&params);
465            }
466
467            // merge additional hosts
468            for sub_host in sub_config.hosts.into_iter().skip(1) {
469                trace!("adding sub-host: {sub_host:?}",);
470                new_hosts.push(sub_host);
471            }
472        }
473
474        Ok(new_hosts)
475    }
476
477    /// Tokenize line if possible. Returns [`Field`] name and args as a [`Vec`] of [`String`].
478    ///
479    /// All of these lines are valid for tokenization
480    ///
481    /// ```txt
482    /// IgnoreUnknown=Pippo,Pluto
483    /// ConnectTimeout = 15
484    /// Ciphers "Pepperoni Pizza,Margherita Pizza,Hawaiian Pizza"
485    /// Macs="Pasta Carbonara,Pasta con tonno"
486    /// ```
487    ///
488    /// So lines have syntax `field args...`, `field=args...`, `field "args"`, `field="args"`
489    fn tokenize_line(line: &str) -> SshParserResult<(Field, Vec<String>)> {
490        // check what comes first, space or =?
491        let trimmed_line = line.trim();
492        // first token is the field, and it may be separated either by a space or by '='
493        let (field, other_tokens) = if trimmed_line.find('=').unwrap_or(usize::MAX)
494            < trimmed_line.find(char::is_whitespace).unwrap_or(usize::MAX)
495        {
496            trimmed_line
497                .split_once('=')
498                .ok_or(SshParserError::MissingArgument)?
499        } else {
500            trimmed_line
501                .split_once(char::is_whitespace)
502                .ok_or(SshParserError::MissingArgument)?
503        };
504
505        trace!("tokenized line '{line}' - field '{field}' with args '{other_tokens}'",);
506
507        // other tokens should trim = and whitespace
508        let other_tokens = other_tokens.trim().trim_start_matches('=').trim();
509        trace!("other tokens trimmed: '{other_tokens}'",);
510
511        // if args is quoted, don't split it
512        let args = if other_tokens.starts_with('"') && other_tokens.ends_with('"') {
513            trace!("quoted args: '{other_tokens}'",);
514            vec![other_tokens[1..other_tokens.len() - 1].to_string()]
515        } else {
516            trace!("splitting args (non-quoted): '{other_tokens}'",);
517            // split by whitespace
518            let tokens = other_tokens.split_whitespace();
519
520            tokens
521                .map(|x| x.trim().to_string())
522                .filter(|x| !x.is_empty())
523                .collect()
524        };
525
526        match Field::from_str(field) {
527            Ok(field) => Ok((field, args)),
528            Err(_) => Err(SshParserError::UnknownField(field.to_string(), args)),
529        }
530    }
531
532    // -- value parsers
533
534    /// parse boolean value
535    fn parse_boolean(args: Vec<String>) -> SshParserResult<bool> {
536        match args.first().map(|x| x.as_str()) {
537            Some("yes") => Ok(true),
538            Some("no") => Ok(false),
539            Some(_) => Err(SshParserError::ExpectedBoolean),
540            None => Err(SshParserError::MissingArgument),
541        }
542    }
543
544    /// Parse algorithms argument
545    fn parse_algos(args: Vec<String>) -> SshParserResult<AlgorithmsRule> {
546        let first = args.first().ok_or(SshParserError::MissingArgument)?;
547
548        AlgorithmsRule::from_str(first)
549    }
550
551    /// Parse comma separated list arguments
552    fn parse_comma_separated_list(args: Vec<String>) -> SshParserResult<Vec<String>> {
553        match args
554            .first()
555            .map(|x| x.split(',').map(|x| x.to_string()).collect())
556        {
557            Some(args) => Ok(args),
558            _ => Err(SshParserError::MissingArgument),
559        }
560    }
561
562    /// Parse duration argument
563    fn parse_duration(args: Vec<String>) -> SshParserResult<Duration> {
564        let value = Self::parse_unsigned(args)?;
565        Ok(Duration::from_secs(value as u64))
566    }
567
568    /// Parse host argument
569    fn parse_host(args: Vec<String>) -> SshParserResult<Vec<HostClause>> {
570        if args.is_empty() {
571            return Err(SshParserError::MissingArgument);
572        }
573        // Collect hosts
574        Ok(args
575            .into_iter()
576            .map(|x| {
577                let tokens: Vec<&str> = x.split('!').collect();
578                if tokens.len() == 2 {
579                    HostClause::new(tokens[1].to_string(), true)
580                } else {
581                    HostClause::new(tokens[0].to_string(), false)
582                }
583            })
584            .collect())
585    }
586
587    /// Parse a list of paths
588    fn parse_path_list(args: Vec<String>) -> SshParserResult<Vec<PathBuf>> {
589        if args.is_empty() {
590            return Err(SshParserError::MissingArgument);
591        }
592        args.iter()
593            .map(|x| Self::parse_path_arg(x.as_str()))
594            .collect()
595    }
596
597    /// Parse path argument
598    fn parse_path(args: Vec<String>) -> SshParserResult<PathBuf> {
599        if let Some(s) = args.first() {
600            Self::parse_path_arg(s)
601        } else {
602            Err(SshParserError::MissingArgument)
603        }
604    }
605
606    /// Parse path argument
607    fn parse_path_arg(s: &str) -> SshParserResult<PathBuf> {
608        // Remove tilde
609        let s = if s.starts_with('~') {
610            let home_dir = dirs::home_dir()
611                .unwrap_or_else(|| PathBuf::from("~"))
612                .to_string_lossy()
613                .to_string();
614            s.replacen('~', &home_dir, 1)
615        } else {
616            s.to_string()
617        };
618        Ok(PathBuf::from(s))
619    }
620
621    /// Parse port number argument
622    fn parse_port(args: Vec<String>) -> SshParserResult<u16> {
623        match args.first().map(|x| u16::from_str(x)) {
624            Some(Ok(val)) => Ok(val),
625            Some(Err(_)) => Err(SshParserError::ExpectedPort),
626            None => Err(SshParserError::MissingArgument),
627        }
628    }
629
630    /// Parse string argument
631    fn parse_string(args: Vec<String>) -> SshParserResult<String> {
632        if let Some(s) = args.into_iter().next() {
633            Ok(s)
634        } else {
635            Err(SshParserError::MissingArgument)
636        }
637    }
638
639    /// Parse unsigned argument
640    fn parse_unsigned(args: Vec<String>) -> SshParserResult<usize> {
641        match args.first().map(|x| usize::from_str(x)) {
642            Some(Ok(val)) => Ok(val),
643            Some(Err(_)) => Err(SshParserError::ExpectedUnsigned),
644            None => Err(SshParserError::MissingArgument),
645        }
646    }
647}
648
649#[cfg(test)]
650mod tests {
651
652    use std::fs::File;
653    use std::io::{BufReader, Write};
654    use std::path::Path;
655
656    use pretty_assertions::assert_eq;
657    use tempfile::NamedTempFile;
658
659    use super::*;
660    use crate::DefaultAlgorithms;
661
662    #[test]
663    fn should_parse_configuration() -> Result<(), SshParserError> {
664        crate::test_log();
665        let temp = create_ssh_config();
666        let file = File::open(temp.path()).expect("Failed to open tempfile");
667        let mut reader = BufReader::new(file);
668        let config = SshConfig::default()
669            .default_algorithms(DefaultAlgorithms {
670                ca_signature_algorithms: vec![],
671                ciphers: vec![],
672                host_key_algorithms: vec![],
673                kex_algorithms: vec![],
674                mac: vec![],
675                pubkey_accepted_algorithms: vec!["omar-crypt".to_string()],
676            })
677            .parse(&mut reader, ParseRule::STRICT)?;
678
679        // Query openssh cmdline overrides (options preceding the first `Host` section,
680        // overriding all following options)
681        let params = config.query("*");
682        assert_eq!(
683            params.ignore_unknown.as_deref().unwrap(),
684            &["Pippo", "Pluto"]
685        );
686        assert_eq!(params.compression.unwrap(), true);
687        assert_eq!(params.connection_attempts.unwrap(), 10);
688        assert_eq!(params.connect_timeout.unwrap(), Duration::from_secs(60));
689        assert_eq!(
690            params.server_alive_interval.unwrap(),
691            Duration::from_secs(40)
692        );
693        assert_eq!(params.tcp_keep_alive.unwrap(), true);
694        assert_eq!(params.ciphers.algorithms(), &["a-manella", "blowfish"]);
695        assert_eq!(
696            params.pubkey_accepted_algorithms.algorithms(),
697            &["desu", "omar-crypt", "fast-omar-crypt"]
698        );
699
700        // Query explicit all-hosts fallback options (`Host *`)
701        assert_eq!(params.ca_signature_algorithms.algorithms(), &["random"]);
702        assert_eq!(
703            params.host_key_algorithms.algorithms(),
704            &["luigi", "mario",]
705        );
706        assert_eq!(params.kex_algorithms.algorithms(), &["desu", "gigi",]);
707        assert_eq!(params.mac.algorithms(), &["concorde"]);
708        assert!(params.bind_address.is_none());
709
710        // Query 172.26.104.4, yielding cmdline overrides,
711        // explicit `Host 192.168.*.* 172.26.*.* !192.168.1.30` options,
712        // and all-hosts fallback options.
713        let params_172_26_104_4 = config.query("172.26.104.4");
714
715        // cmdline overrides
716        assert_eq!(params_172_26_104_4.add_keys_to_agent.unwrap(), true);
717        assert_eq!(params_172_26_104_4.compression.unwrap(), true);
718        assert_eq!(params_172_26_104_4.connection_attempts.unwrap(), 10);
719        assert_eq!(
720            params_172_26_104_4.connect_timeout.unwrap(),
721            Duration::from_secs(60)
722        );
723        assert_eq!(params_172_26_104_4.tcp_keep_alive.unwrap(), true);
724
725        // all-hosts fallback options, merged with host-specific options
726        assert_eq!(
727            params_172_26_104_4.ca_signature_algorithms.algorithms(),
728            &["random"]
729        );
730        assert_eq!(
731            params_172_26_104_4.ciphers.algorithms(),
732            &["a-manella", "blowfish",]
733        );
734        assert_eq!(params_172_26_104_4.mac.algorithms(), &["spyro", "deoxys"]); // use subconfig; defined before * macs
735        assert_eq!(
736            params_172_26_104_4.proxy_jump.unwrap(),
737            &["jump.example.com"]
738        ); // use subconfig; defined before * macs
739        assert_eq!(
740            params_172_26_104_4
741                .pubkey_accepted_algorithms
742                .algorithms()
743                .is_empty(), // should have removed omar-crypt
744            true
745        );
746        assert_eq!(
747            params_172_26_104_4.bind_address.as_deref().unwrap(),
748            "10.8.0.10"
749        );
750        assert_eq!(
751            params_172_26_104_4.bind_interface.as_deref().unwrap(),
752            "tun0"
753        );
754        assert_eq!(params_172_26_104_4.port.unwrap(), 2222);
755        assert_eq!(
756            params_172_26_104_4.identity_file.as_deref().unwrap(),
757            vec![
758                Path::new("/home/root/.ssh/pippo.key"),
759                Path::new("/home/root/.ssh/pluto.key")
760            ]
761        );
762        assert_eq!(params_172_26_104_4.user.as_deref().unwrap(), "omar");
763
764        // Query tostapane
765        let params_tostapane = config.query("tostapane");
766        assert_eq!(params_tostapane.compression.unwrap(), true); // it takes the first value defined, which is `yes`
767        assert_eq!(params_tostapane.connection_attempts.unwrap(), 10);
768        assert_eq!(
769            params_tostapane.connect_timeout.unwrap(),
770            Duration::from_secs(60)
771        );
772        assert_eq!(params_tostapane.tcp_keep_alive.unwrap(), true);
773        assert_eq!(params_tostapane.remote_forward.unwrap(), 88);
774        assert_eq!(params_tostapane.user.as_deref().unwrap(), "ciro-esposito");
775
776        // all-hosts fallback options
777        assert_eq!(
778            params_tostapane.ca_signature_algorithms.algorithms(),
779            &["random"]
780        );
781        assert_eq!(
782            params_tostapane.ciphers.algorithms(),
783            &["a-manella", "blowfish",]
784        );
785        assert_eq!(
786            params_tostapane.mac.algorithms(),
787            vec!["spyro".to_string(), "deoxys".to_string(),]
788        );
789        assert_eq!(
790            params_tostapane.proxy_jump.unwrap(),
791            vec![
792                "jump1.example.com".to_string(),
793                "jump2.example.com".to_string(),
794            ]
795        );
796        assert_eq!(
797            params_tostapane.pubkey_accepted_algorithms.algorithms(),
798            &["desu", "omar-crypt", "fast-omar-crypt"]
799        );
800
801        // query 192.168.1.30
802        let params_192_168_1_30 = config.query("192.168.1.30");
803
804        // host-specific options
805        assert_eq!(params_192_168_1_30.user.as_deref().unwrap(), "nutellaro");
806        assert_eq!(params_192_168_1_30.remote_forward.unwrap(), 123);
807
808        // cmdline overrides
809        assert_eq!(params_192_168_1_30.compression.unwrap(), true);
810        assert_eq!(params_192_168_1_30.connection_attempts.unwrap(), 10);
811        assert_eq!(
812            params_192_168_1_30.connect_timeout.unwrap(),
813            Duration::from_secs(60)
814        );
815        assert_eq!(params_192_168_1_30.tcp_keep_alive.unwrap(), true);
816
817        // all-hosts fallback options
818        assert_eq!(
819            params_192_168_1_30.ca_signature_algorithms.algorithms(),
820            &["random"]
821        );
822        assert_eq!(
823            params_192_168_1_30.ciphers.algorithms(),
824            &["a-manella", "blowfish"]
825        );
826        assert_eq!(params_192_168_1_30.mac.algorithms(), &["concorde"]);
827        assert_eq!(
828            params_192_168_1_30.pubkey_accepted_algorithms.algorithms(),
829            &["desu", "omar-crypt", "fast-omar-crypt"]
830        );
831
832        Ok(())
833    }
834
835    #[test]
836    fn should_allow_unknown_field() -> Result<(), SshParserError> {
837        crate::test_log();
838        let temp = create_ssh_config_with_unknown_fields();
839        let file = File::open(temp.path()).expect("Failed to open tempfile");
840        let mut reader = BufReader::new(file);
841        let _config = SshConfig::default()
842            .default_algorithms(DefaultAlgorithms::empty())
843            .parse(&mut reader, ParseRule::ALLOW_UNKNOWN_FIELDS)?;
844
845        Ok(())
846    }
847
848    #[test]
849    fn should_not_allow_unknown_field() {
850        crate::test_log();
851        let temp = create_ssh_config_with_unknown_fields();
852        let file = File::open(temp.path()).expect("Failed to open tempfile");
853        let mut reader = BufReader::new(file);
854        assert!(matches!(
855            SshConfig::default()
856                .default_algorithms(DefaultAlgorithms::empty())
857                .parse(&mut reader, ParseRule::STRICT)
858                .unwrap_err(),
859            SshParserError::UnknownField(..)
860        ));
861    }
862
863    #[test]
864    fn should_store_unknown_fields() {
865        crate::test_log();
866        let temp = create_ssh_config_with_unknown_fields();
867        let file = File::open(temp.path()).expect("Failed to open tempfile");
868        let mut reader = BufReader::new(file);
869        let config = SshConfig::default()
870            .default_algorithms(DefaultAlgorithms::empty())
871            .parse(&mut reader, ParseRule::ALLOW_UNKNOWN_FIELDS)
872            .unwrap();
873
874        let host = config.query("cross-platform");
875        assert_eq!(
876            host.ignored_fields.get("Piropero").unwrap(),
877            &vec![String::from("yes")]
878        );
879    }
880
881    #[test]
882    fn should_parse_inversed_ssh_config() {
883        crate::test_log();
884        let temp = create_inverted_ssh_config();
885        let file = File::open(temp.path()).expect("Failed to open tempfile");
886        let mut reader = BufReader::new(file);
887        let config = SshConfig::default()
888            .default_algorithms(DefaultAlgorithms::empty())
889            .parse(&mut reader, ParseRule::STRICT)
890            .unwrap();
891
892        let home_dir = dirs::home_dir()
893            .unwrap_or_else(|| PathBuf::from("~"))
894            .to_string_lossy()
895            .to_string();
896
897        let remote_host = config.query("remote-host");
898
899        // From `*-host`
900        assert_eq!(
901            remote_host.identity_file.unwrap()[0].as_path(),
902            Path::new(format!("{home_dir}/.ssh/id_rsa_good").as_str()) // because it's the first in the file
903        );
904
905        // From `remote-*`
906        assert_eq!(remote_host.host_name.unwrap(), "hostname.com");
907        assert_eq!(remote_host.user.unwrap(), "user");
908
909        // From `*`
910        assert_eq!(
911            remote_host.connect_timeout.unwrap(),
912            Duration::from_secs(15)
913        );
914    }
915
916    #[test]
917    fn should_parse_configuration_with_hosts() {
918        crate::test_log();
919        let temp = create_ssh_config_with_comments();
920
921        let file = File::open(temp.path()).expect("Failed to open tempfile");
922        let mut reader = BufReader::new(file);
923        let config = SshConfig::default()
924            .default_algorithms(DefaultAlgorithms::empty())
925            .parse(&mut reader, ParseRule::STRICT)
926            .unwrap();
927
928        let hostname = config.query("cross-platform").host_name.unwrap();
929        assert_eq!(&hostname, "hostname.com");
930
931        assert!(config.query("this").host_name.is_none());
932    }
933
934    #[test]
935    fn should_update_host_bind_address() -> Result<(), SshParserError> {
936        crate::test_log();
937        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
938        SshConfigParser::update_host(
939            Field::BindAddress,
940            vec![String::from("127.0.0.1")],
941            &mut host,
942            ParseRule::ALLOW_UNKNOWN_FIELDS,
943            &DefaultAlgorithms::empty(),
944        )?;
945        assert_eq!(host.params.bind_address.as_deref().unwrap(), "127.0.0.1");
946        Ok(())
947    }
948
949    #[test]
950    fn should_update_host_bind_interface() -> Result<(), SshParserError> {
951        crate::test_log();
952        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
953        SshConfigParser::update_host(
954            Field::BindInterface,
955            vec![String::from("aaa")],
956            &mut host,
957            ParseRule::ALLOW_UNKNOWN_FIELDS,
958            &DefaultAlgorithms::empty(),
959        )?;
960        assert_eq!(host.params.bind_interface.as_deref().unwrap(), "aaa");
961        Ok(())
962    }
963
964    #[test]
965    fn should_update_host_ca_signature_algos() -> Result<(), SshParserError> {
966        crate::test_log();
967        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
968        SshConfigParser::update_host(
969            Field::CaSignatureAlgorithms,
970            vec![String::from("a,b,c")],
971            &mut host,
972            ParseRule::ALLOW_UNKNOWN_FIELDS,
973            &DefaultAlgorithms::empty(),
974        )?;
975        assert_eq!(
976            host.params.ca_signature_algorithms.algorithms(),
977            &["a", "b", "c"]
978        );
979        Ok(())
980    }
981
982    #[test]
983    fn should_update_host_certificate_file() -> Result<(), SshParserError> {
984        crate::test_log();
985        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
986        SshConfigParser::update_host(
987            Field::CertificateFile,
988            vec![String::from("/tmp/a.crt")],
989            &mut host,
990            ParseRule::ALLOW_UNKNOWN_FIELDS,
991            &DefaultAlgorithms::empty(),
992        )?;
993        assert_eq!(
994            host.params.certificate_file.as_deref().unwrap(),
995            Path::new("/tmp/a.crt")
996        );
997        Ok(())
998    }
999
1000    #[test]
1001    fn should_update_host_ciphers() -> Result<(), SshParserError> {
1002        crate::test_log();
1003        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1004        SshConfigParser::update_host(
1005            Field::Ciphers,
1006            vec![String::from("a,b,c")],
1007            &mut host,
1008            ParseRule::ALLOW_UNKNOWN_FIELDS,
1009            &DefaultAlgorithms::empty(),
1010        )?;
1011        assert_eq!(host.params.ciphers.algorithms(), &["a", "b", "c"]);
1012        Ok(())
1013    }
1014
1015    #[test]
1016    fn should_update_host_compression() -> Result<(), SshParserError> {
1017        crate::test_log();
1018        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1019        SshConfigParser::update_host(
1020            Field::Compression,
1021            vec![String::from("yes")],
1022            &mut host,
1023            ParseRule::ALLOW_UNKNOWN_FIELDS,
1024            &DefaultAlgorithms::empty(),
1025        )?;
1026        assert_eq!(host.params.compression.unwrap(), true);
1027        Ok(())
1028    }
1029
1030    #[test]
1031    fn should_update_host_connection_attempts() -> Result<(), SshParserError> {
1032        crate::test_log();
1033        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1034        SshConfigParser::update_host(
1035            Field::ConnectionAttempts,
1036            vec![String::from("4")],
1037            &mut host,
1038            ParseRule::ALLOW_UNKNOWN_FIELDS,
1039            &DefaultAlgorithms::empty(),
1040        )?;
1041        assert_eq!(host.params.connection_attempts.unwrap(), 4);
1042        Ok(())
1043    }
1044
1045    #[test]
1046    fn should_update_host_connection_timeout() -> Result<(), SshParserError> {
1047        crate::test_log();
1048        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1049        SshConfigParser::update_host(
1050            Field::ConnectTimeout,
1051            vec![String::from("10")],
1052            &mut host,
1053            ParseRule::ALLOW_UNKNOWN_FIELDS,
1054            &DefaultAlgorithms::empty(),
1055        )?;
1056        assert_eq!(
1057            host.params.connect_timeout.unwrap(),
1058            Duration::from_secs(10)
1059        );
1060        Ok(())
1061    }
1062
1063    #[test]
1064    fn should_update_host_key_algorithms() -> Result<(), SshParserError> {
1065        crate::test_log();
1066        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1067        SshConfigParser::update_host(
1068            Field::HostKeyAlgorithms,
1069            vec![String::from("a,b,c")],
1070            &mut host,
1071            ParseRule::ALLOW_UNKNOWN_FIELDS,
1072            &DefaultAlgorithms::empty(),
1073        )?;
1074        assert_eq!(
1075            host.params.host_key_algorithms.algorithms(),
1076            &["a", "b", "c"]
1077        );
1078        Ok(())
1079    }
1080
1081    #[test]
1082    fn should_update_host_host_name() -> Result<(), SshParserError> {
1083        crate::test_log();
1084        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1085        SshConfigParser::update_host(
1086            Field::HostName,
1087            vec![String::from("192.168.1.1")],
1088            &mut host,
1089            ParseRule::ALLOW_UNKNOWN_FIELDS,
1090            &DefaultAlgorithms::empty(),
1091        )?;
1092        assert_eq!(host.params.host_name.as_deref().unwrap(), "192.168.1.1");
1093        Ok(())
1094    }
1095
1096    #[test]
1097    fn should_update_host_ignore_unknown() -> Result<(), SshParserError> {
1098        crate::test_log();
1099        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1100        SshConfigParser::update_host(
1101            Field::IgnoreUnknown,
1102            vec![String::from("a,b,c")],
1103            &mut host,
1104            ParseRule::ALLOW_UNKNOWN_FIELDS,
1105            &DefaultAlgorithms::empty(),
1106        )?;
1107        assert_eq!(
1108            host.params.ignore_unknown.as_deref().unwrap(),
1109            &["a", "b", "c"]
1110        );
1111        Ok(())
1112    }
1113
1114    #[test]
1115    fn should_update_kex_algorithms() -> Result<(), SshParserError> {
1116        crate::test_log();
1117        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1118        SshConfigParser::update_host(
1119            Field::KexAlgorithms,
1120            vec![String::from("a,b,c")],
1121            &mut host,
1122            ParseRule::ALLOW_UNKNOWN_FIELDS,
1123            &DefaultAlgorithms::empty(),
1124        )?;
1125        assert_eq!(host.params.kex_algorithms.algorithms(), &["a", "b", "c"]);
1126        Ok(())
1127    }
1128
1129    #[test]
1130    fn should_update_host_mac() -> Result<(), SshParserError> {
1131        crate::test_log();
1132        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1133        SshConfigParser::update_host(
1134            Field::Mac,
1135            vec![String::from("a,b,c")],
1136            &mut host,
1137            ParseRule::ALLOW_UNKNOWN_FIELDS,
1138            &DefaultAlgorithms::empty(),
1139        )?;
1140        assert_eq!(host.params.mac.algorithms(), &["a", "b", "c"]);
1141        Ok(())
1142    }
1143
1144    #[test]
1145    fn should_update_host_port() -> Result<(), SshParserError> {
1146        crate::test_log();
1147        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1148        SshConfigParser::update_host(
1149            Field::Port,
1150            vec![String::from("2222")],
1151            &mut host,
1152            ParseRule::ALLOW_UNKNOWN_FIELDS,
1153            &DefaultAlgorithms::empty(),
1154        )?;
1155        assert_eq!(host.params.port.unwrap(), 2222);
1156        Ok(())
1157    }
1158
1159    #[test]
1160    fn should_update_host_pubkey_accepted_algos() -> Result<(), SshParserError> {
1161        crate::test_log();
1162        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1163        SshConfigParser::update_host(
1164            Field::PubkeyAcceptedAlgorithms,
1165            vec![String::from("a,b,c")],
1166            &mut host,
1167            ParseRule::ALLOW_UNKNOWN_FIELDS,
1168            &DefaultAlgorithms::empty(),
1169        )?;
1170        assert_eq!(
1171            host.params.pubkey_accepted_algorithms.algorithms(),
1172            &["a", "b", "c"]
1173        );
1174        Ok(())
1175    }
1176
1177    #[test]
1178    fn should_update_host_pubkey_authentication() -> Result<(), SshParserError> {
1179        crate::test_log();
1180        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1181        SshConfigParser::update_host(
1182            Field::PubkeyAuthentication,
1183            vec![String::from("yes")],
1184            &mut host,
1185            ParseRule::ALLOW_UNKNOWN_FIELDS,
1186            &DefaultAlgorithms::empty(),
1187        )?;
1188        assert_eq!(host.params.pubkey_authentication.unwrap(), true);
1189        Ok(())
1190    }
1191
1192    #[test]
1193    fn should_update_host_remote_forward() -> Result<(), SshParserError> {
1194        crate::test_log();
1195        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1196        SshConfigParser::update_host(
1197            Field::RemoteForward,
1198            vec![String::from("3005")],
1199            &mut host,
1200            ParseRule::ALLOW_UNKNOWN_FIELDS,
1201            &DefaultAlgorithms::empty(),
1202        )?;
1203        assert_eq!(host.params.remote_forward.unwrap(), 3005);
1204        Ok(())
1205    }
1206
1207    #[test]
1208    fn should_update_host_server_alive_interval() -> Result<(), SshParserError> {
1209        crate::test_log();
1210        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1211        SshConfigParser::update_host(
1212            Field::ServerAliveInterval,
1213            vec![String::from("40")],
1214            &mut host,
1215            ParseRule::ALLOW_UNKNOWN_FIELDS,
1216            &DefaultAlgorithms::empty(),
1217        )?;
1218        assert_eq!(
1219            host.params.server_alive_interval.unwrap(),
1220            Duration::from_secs(40)
1221        );
1222        Ok(())
1223    }
1224
1225    #[test]
1226    fn should_update_host_tcp_keep_alive() -> Result<(), SshParserError> {
1227        crate::test_log();
1228        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1229        SshConfigParser::update_host(
1230            Field::TcpKeepAlive,
1231            vec![String::from("no")],
1232            &mut host,
1233            ParseRule::ALLOW_UNKNOWN_FIELDS,
1234            &DefaultAlgorithms::empty(),
1235        )?;
1236        assert_eq!(host.params.tcp_keep_alive.unwrap(), false);
1237        Ok(())
1238    }
1239
1240    #[test]
1241    fn should_update_host_user() -> Result<(), SshParserError> {
1242        crate::test_log();
1243        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1244        SshConfigParser::update_host(
1245            Field::User,
1246            vec![String::from("pippo")],
1247            &mut host,
1248            ParseRule::ALLOW_UNKNOWN_FIELDS,
1249            &DefaultAlgorithms::empty(),
1250        )?;
1251        assert_eq!(host.params.user.as_deref().unwrap(), "pippo");
1252        Ok(())
1253    }
1254
1255    #[test]
1256    fn should_not_update_host_if_unknown() -> Result<(), SshParserError> {
1257        crate::test_log();
1258        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1259        let result = SshConfigParser::update_host(
1260            Field::PasswordAuthentication,
1261            vec![String::from("yes")],
1262            &mut host,
1263            ParseRule::ALLOW_UNKNOWN_FIELDS,
1264            &DefaultAlgorithms::empty(),
1265        );
1266
1267        match result {
1268            Ok(_) | Err(SshParserError::UnsupportedField(_, _)) => Ok(()),
1269            Err(e) => Err(e),
1270        }?;
1271
1272        assert_eq!(host.params, HostParams::new(&DefaultAlgorithms::empty()));
1273        Ok(())
1274    }
1275
1276    #[test]
1277    fn should_update_host_if_unsupported() -> Result<(), SshParserError> {
1278        crate::test_log();
1279        let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1280        let result = SshConfigParser::update_host(
1281            Field::PasswordAuthentication,
1282            vec![String::from("yes")],
1283            &mut host,
1284            ParseRule::ALLOW_UNKNOWN_FIELDS,
1285            &DefaultAlgorithms::empty(),
1286        );
1287
1288        match result {
1289            Err(SshParserError::UnsupportedField(field, _)) => {
1290                assert_eq!(field, "passwordauthentication");
1291                Ok(())
1292            }
1293            Ok(_) => Ok(()),
1294            Err(e) => Err(e),
1295        }?;
1296
1297        assert_eq!(host.params, HostParams::new(&DefaultAlgorithms::empty()));
1298        Ok(())
1299    }
1300
1301    #[test]
1302    fn should_tokenize_line() -> Result<(), SshParserError> {
1303        crate::test_log();
1304        assert_eq!(
1305            SshConfigParser::tokenize_line("HostName 192.168.*.* 172.26.*.*")?,
1306            (
1307                Field::HostName,
1308                vec![String::from("192.168.*.*"), String::from("172.26.*.*")]
1309            )
1310        );
1311        // Tokenize line with spaces
1312        assert_eq!(
1313            SshConfigParser::tokenize_line(
1314                "      HostName        192.168.*.*        172.26.*.*        "
1315            )?,
1316            (
1317                Field::HostName,
1318                vec![String::from("192.168.*.*"), String::from("172.26.*.*")]
1319            )
1320        );
1321        Ok(())
1322    }
1323
1324    #[test]
1325    fn should_not_tokenize_line() {
1326        crate::test_log();
1327        assert!(matches!(
1328            SshConfigParser::tokenize_line("Omar     yes").unwrap_err(),
1329            SshParserError::UnknownField(..)
1330        ));
1331    }
1332
1333    #[test]
1334    fn should_fail_parsing_field() {
1335        crate::test_log();
1336
1337        assert!(matches!(
1338            SshConfigParser::tokenize_line("                  ").unwrap_err(),
1339            SshParserError::MissingArgument
1340        ));
1341    }
1342
1343    #[test]
1344    fn should_parse_boolean() -> Result<(), SshParserError> {
1345        crate::test_log();
1346        assert_eq!(
1347            SshConfigParser::parse_boolean(vec![String::from("yes")])?,
1348            true
1349        );
1350        assert_eq!(
1351            SshConfigParser::parse_boolean(vec![String::from("no")])?,
1352            false
1353        );
1354        Ok(())
1355    }
1356
1357    #[test]
1358    fn should_fail_parsing_boolean() {
1359        crate::test_log();
1360        assert!(matches!(
1361            SshConfigParser::parse_boolean(vec!["boh".to_string()]).unwrap_err(),
1362            SshParserError::ExpectedBoolean
1363        ));
1364        assert!(matches!(
1365            SshConfigParser::parse_boolean(vec![]).unwrap_err(),
1366            SshParserError::MissingArgument
1367        ));
1368    }
1369
1370    #[test]
1371    fn should_parse_algos() -> Result<(), SshParserError> {
1372        crate::test_log();
1373        assert_eq!(
1374            SshConfigParser::parse_algos(vec![String::from("a,b,c,d")])?,
1375            AlgorithmsRule::Set(vec![
1376                "a".to_string(),
1377                "b".to_string(),
1378                "c".to_string(),
1379                "d".to_string(),
1380            ])
1381        );
1382
1383        assert_eq!(
1384            SshConfigParser::parse_algos(vec![String::from("a")])?,
1385            AlgorithmsRule::Set(vec!["a".to_string()])
1386        );
1387
1388        assert_eq!(
1389            SshConfigParser::parse_algos(vec![String::from("+a,b")])?,
1390            AlgorithmsRule::Append(vec!["a".to_string(), "b".to_string()])
1391        );
1392
1393        Ok(())
1394    }
1395
1396    #[test]
1397    fn should_parse_comma_separated_list() -> Result<(), SshParserError> {
1398        crate::test_log();
1399        assert_eq!(
1400            SshConfigParser::parse_comma_separated_list(vec![String::from("a,b,c,d")])?,
1401            vec![
1402                "a".to_string(),
1403                "b".to_string(),
1404                "c".to_string(),
1405                "d".to_string(),
1406            ]
1407        );
1408        assert_eq!(
1409            SshConfigParser::parse_comma_separated_list(vec![String::from("a")])?,
1410            vec!["a".to_string()]
1411        );
1412        Ok(())
1413    }
1414
1415    #[test]
1416    fn should_fail_parsing_comma_separated_list() {
1417        crate::test_log();
1418        assert!(matches!(
1419            SshConfigParser::parse_comma_separated_list(vec![]).unwrap_err(),
1420            SshParserError::MissingArgument
1421        ));
1422    }
1423
1424    #[test]
1425    fn should_parse_duration() -> Result<(), SshParserError> {
1426        crate::test_log();
1427        assert_eq!(
1428            SshConfigParser::parse_duration(vec![String::from("60")])?,
1429            Duration::from_secs(60)
1430        );
1431        Ok(())
1432    }
1433
1434    #[test]
1435    fn should_fail_parsing_duration() {
1436        crate::test_log();
1437        assert!(matches!(
1438            SshConfigParser::parse_duration(vec![String::from("AAA")]).unwrap_err(),
1439            SshParserError::ExpectedUnsigned
1440        ));
1441        assert!(matches!(
1442            SshConfigParser::parse_duration(vec![]).unwrap_err(),
1443            SshParserError::MissingArgument
1444        ));
1445    }
1446
1447    #[test]
1448    fn should_parse_host() -> Result<(), SshParserError> {
1449        crate::test_log();
1450        assert_eq!(
1451            SshConfigParser::parse_host(vec![
1452                String::from("192.168.*.*"),
1453                String::from("!192.168.1.1"),
1454                String::from("172.26.104.*"),
1455                String::from("!172.26.104.10"),
1456            ])?,
1457            vec![
1458                HostClause::new(String::from("192.168.*.*"), false),
1459                HostClause::new(String::from("192.168.1.1"), true),
1460                HostClause::new(String::from("172.26.104.*"), false),
1461                HostClause::new(String::from("172.26.104.10"), true),
1462            ]
1463        );
1464        Ok(())
1465    }
1466
1467    #[test]
1468    fn should_fail_parsing_host() {
1469        crate::test_log();
1470        assert!(matches!(
1471            SshConfigParser::parse_host(vec![]).unwrap_err(),
1472            SshParserError::MissingArgument
1473        ));
1474    }
1475
1476    #[test]
1477    fn should_parse_path() -> Result<(), SshParserError> {
1478        crate::test_log();
1479        assert_eq!(
1480            SshConfigParser::parse_path(vec![String::from("/tmp/a.txt")])?,
1481            PathBuf::from("/tmp/a.txt")
1482        );
1483        Ok(())
1484    }
1485
1486    #[test]
1487    fn should_parse_path_and_resolve_tilde() -> Result<(), SshParserError> {
1488        crate::test_log();
1489        let mut expected = dirs::home_dir().unwrap();
1490        expected.push(".ssh/id_dsa");
1491        assert_eq!(
1492            SshConfigParser::parse_path(vec![String::from("~/.ssh/id_dsa")])?,
1493            expected
1494        );
1495        Ok(())
1496    }
1497
1498    #[test]
1499    fn should_parse_path_list() -> Result<(), SshParserError> {
1500        crate::test_log();
1501        assert_eq!(
1502            SshConfigParser::parse_path_list(vec![
1503                String::from("/tmp/a.txt"),
1504                String::from("/tmp/b.txt")
1505            ])?,
1506            vec![PathBuf::from("/tmp/a.txt"), PathBuf::from("/tmp/b.txt")]
1507        );
1508        Ok(())
1509    }
1510
1511    #[test]
1512    fn should_fail_parse_path_list() {
1513        crate::test_log();
1514        assert!(matches!(
1515            SshConfigParser::parse_path_list(vec![]).unwrap_err(),
1516            SshParserError::MissingArgument
1517        ));
1518    }
1519
1520    #[test]
1521    fn should_fail_parsing_path() {
1522        crate::test_log();
1523        assert!(matches!(
1524            SshConfigParser::parse_path(vec![]).unwrap_err(),
1525            SshParserError::MissingArgument
1526        ));
1527    }
1528
1529    #[test]
1530    fn should_parse_port() -> Result<(), SshParserError> {
1531        crate::test_log();
1532        assert_eq!(SshConfigParser::parse_port(vec![String::from("22")])?, 22);
1533        Ok(())
1534    }
1535
1536    #[test]
1537    fn should_fail_parsing_port() {
1538        crate::test_log();
1539        assert!(matches!(
1540            SshConfigParser::parse_port(vec![String::from("1234567")]).unwrap_err(),
1541            SshParserError::ExpectedPort
1542        ));
1543        assert!(matches!(
1544            SshConfigParser::parse_port(vec![]).unwrap_err(),
1545            SshParserError::MissingArgument
1546        ));
1547    }
1548
1549    #[test]
1550    fn should_parse_string() -> Result<(), SshParserError> {
1551        crate::test_log();
1552        assert_eq!(
1553            SshConfigParser::parse_string(vec![String::from("foobar")])?,
1554            String::from("foobar")
1555        );
1556        Ok(())
1557    }
1558
1559    #[test]
1560    fn should_fail_parsing_string() {
1561        crate::test_log();
1562        assert!(matches!(
1563            SshConfigParser::parse_string(vec![]).unwrap_err(),
1564            SshParserError::MissingArgument
1565        ));
1566    }
1567
1568    #[test]
1569    fn should_parse_unsigned() -> Result<(), SshParserError> {
1570        crate::test_log();
1571        assert_eq!(
1572            SshConfigParser::parse_unsigned(vec![String::from("43")])?,
1573            43
1574        );
1575        Ok(())
1576    }
1577
1578    #[test]
1579    fn should_fail_parsing_unsigned() {
1580        crate::test_log();
1581        assert!(matches!(
1582            SshConfigParser::parse_unsigned(vec![String::from("abc")]).unwrap_err(),
1583            SshParserError::ExpectedUnsigned
1584        ));
1585        assert!(matches!(
1586            SshConfigParser::parse_unsigned(vec![]).unwrap_err(),
1587            SshParserError::MissingArgument
1588        ));
1589    }
1590
1591    #[test]
1592    fn should_strip_comments() {
1593        crate::test_log();
1594
1595        assert_eq!(
1596            SshConfigParser::strip_comments("host my_host # this is my fav host").as_str(),
1597            "host my_host "
1598        );
1599        assert_eq!(
1600            SshConfigParser::strip_comments("# this is a comment").as_str(),
1601            ""
1602        );
1603    }
1604
1605    #[test]
1606    fn test_should_parse_config_with_quotes_and_eq() {
1607        crate::test_log();
1608
1609        let config = create_ssh_config_with_quotes_and_eq();
1610        let file = File::open(config.path()).expect("Failed to open tempfile");
1611        let mut reader = BufReader::new(file);
1612
1613        let config = SshConfig::default()
1614            .default_algorithms(DefaultAlgorithms::empty())
1615            .parse(&mut reader, ParseRule::STRICT)
1616            .expect("Failed to parse config");
1617
1618        let params = config.query("foo");
1619
1620        // connect timeout is 15
1621        assert_eq!(
1622            params.connect_timeout.expect("unspec connect timeout"),
1623            Duration::from_secs(15)
1624        );
1625        assert_eq!(
1626            params
1627                .ignore_unknown
1628                .as_deref()
1629                .expect("unspec ignore unknown"),
1630            &["Pippo", "Pluto"]
1631        );
1632        assert_eq!(
1633            params
1634                .ciphers
1635                .algorithms()
1636                .iter()
1637                .map(|x| x.as_str())
1638                .collect::<Vec<&str>>(),
1639            &["Pepperoni Pizza", "Margherita Pizza", "Hawaiian Pizza"]
1640        );
1641        assert_eq!(
1642            params
1643                .mac
1644                .algorithms()
1645                .iter()
1646                .map(|x| x.as_str())
1647                .collect::<Vec<&str>>(),
1648            &["Pasta Carbonara", "Pasta con tonno"]
1649        );
1650    }
1651
1652    #[test]
1653    fn test_should_resolve_absolute_include_path() {
1654        crate::test_log();
1655
1656        let expected = PathBuf::from("/tmp/config.local");
1657
1658        let s = "/tmp/config.local";
1659        let resolved = PathBuf::from(SshConfigParser::resolve_include_path(s));
1660        assert_eq!(resolved, expected);
1661    }
1662
1663    #[test]
1664    fn test_should_resolve_relative_include_path() {
1665        crate::test_log();
1666
1667        let expected = dirs::home_dir()
1668            .unwrap_or_else(|| PathBuf::from("~"))
1669            .join(".ssh")
1670            .join("config.local");
1671
1672        let s = "config.local";
1673        let resolved = PathBuf::from(SshConfigParser::resolve_include_path(s));
1674        assert_eq!(resolved, expected);
1675    }
1676
1677    #[test]
1678    fn test_should_resolve_include_path_with_tilde() {
1679        let p = "~/.ssh/config.local";
1680        let resolved = SshConfigParser::resolve_include_path(p);
1681        let mut expected = dirs::home_dir().unwrap_or_else(|| PathBuf::from("~"));
1682        expected.push(".ssh");
1683        expected.push("config.local");
1684        assert_eq!(PathBuf::from(resolved), expected);
1685    }
1686
1687    fn create_ssh_config_with_quotes_and_eq() -> NamedTempFile {
1688        let mut tmpfile: tempfile::NamedTempFile =
1689            tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1690        let config = r##"
1691# ssh config
1692# written by veeso
1693
1694
1695# I put a comment here just to annoy
1696
1697IgnoreUnknown=Pippo,Pluto
1698ConnectTimeout = 15
1699Ciphers "Pepperoni Pizza,Margherita Pizza,Hawaiian Pizza"
1700Macs="Pasta Carbonara,Pasta con tonno"
1701"##;
1702        tmpfile.write_all(config.as_bytes()).unwrap();
1703        tmpfile
1704    }
1705
1706    fn create_ssh_config() -> NamedTempFile {
1707        let mut tmpfile: tempfile::NamedTempFile =
1708            tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1709        let config = r##"
1710# ssh config
1711# written by veeso
1712
1713
1714        # I put a comment here just to annoy
1715
1716IgnoreUnknown Pippo,Pluto
1717
1718Compression yes
1719ConnectionAttempts          10
1720ConnectTimeout 60
1721ServerAliveInterval 40
1722TcpKeepAlive    yes
1723Ciphers     +a-manella,blowfish
1724
1725# Let's start defining some hosts
1726
1727Host 192.168.*.*    172.26.*.*      !192.168.1.30
1728    User    omar
1729    # ForwardX11 is actually not supported; I just want to see that it wont' fail parsing
1730    ForwardX11    yes
1731    BindAddress     10.8.0.10
1732    BindInterface   tun0
1733    AddKeysToAgent yes
1734    Ciphers     +coi-piedi,cazdecan,triestin-stretto
1735    IdentityFile    /home/root/.ssh/pippo.key /home/root/.ssh/pluto.key
1736    Macs     spyro,deoxys
1737    Port 2222
1738    PubkeyAcceptedAlgorithms    -omar-crypt
1739    ProxyJump jump.example.com
1740
1741Host tostapane
1742    User    ciro-esposito
1743    HostName    192.168.24.32
1744    RemoteForward   88
1745    Compression no
1746    Pippo yes
1747    Pluto 56
1748    ProxyJump jump1.example.com,jump2.example.com
1749    Macs +spyro,deoxys
1750
1751Host    192.168.1.30
1752    User    nutellaro
1753    RemoteForward   123
1754
1755Host *
1756    CaSignatureAlgorithms   random
1757    HostKeyAlgorithms   luigi,mario
1758    KexAlgorithms   desu,gigi
1759    Macs     concorde
1760    PubkeyAcceptedAlgorithms    desu,omar-crypt,fast-omar-crypt
1761"##;
1762        tmpfile.write_all(config.as_bytes()).unwrap();
1763        tmpfile
1764    }
1765
1766    fn create_inverted_ssh_config() -> NamedTempFile {
1767        let mut tmpfile: tempfile::NamedTempFile =
1768            tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1769        let config = r##"
1770Host *-host
1771    IdentityFile ~/.ssh/id_rsa_good
1772
1773Host remote-*
1774    HostName hostname.com
1775    User user
1776    IdentityFile ~/.ssh/id_rsa_bad
1777
1778Host *
1779    ConnectTimeout 15
1780    IdentityFile ~/.ssh/id_rsa_ugly
1781    "##;
1782        tmpfile.write_all(config.as_bytes()).unwrap();
1783        tmpfile
1784    }
1785
1786    fn create_ssh_config_with_comments() -> NamedTempFile {
1787        let mut tmpfile: tempfile::NamedTempFile =
1788            tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1789        let config = r##"
1790Host cross-platform # this is my fav host
1791    HostName hostname.com
1792    User user
1793    IdentityFile ~/.ssh/id_rsa_good
1794
1795Host *
1796    AddKeysToAgent yes
1797    IdentityFile ~/.ssh/id_rsa_bad
1798    "##;
1799        tmpfile.write_all(config.as_bytes()).unwrap();
1800        tmpfile
1801    }
1802
1803    fn create_ssh_config_with_unknown_fields() -> NamedTempFile {
1804        let mut tmpfile: tempfile::NamedTempFile =
1805            tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1806        let config = r##"
1807Host cross-platform # this is my fav host
1808    HostName hostname.com
1809    User user
1810    IdentityFile ~/.ssh/id_rsa_good
1811    Piropero yes
1812
1813Host *
1814    AddKeysToAgent yes
1815    IdentityFile ~/.ssh/id_rsa_bad
1816    "##;
1817        tmpfile.write_all(config.as_bytes()).unwrap();
1818        tmpfile
1819    }
1820
1821    #[test]
1822    fn test_should_parse_config_with_include() {
1823        crate::test_log();
1824
1825        let config = create_include_config();
1826        let file = File::open(config.config.path()).expect("Failed to open tempfile");
1827        let mut reader = BufReader::new(file);
1828
1829        let config = SshConfig::default()
1830            .default_algorithms(DefaultAlgorithms::empty())
1831            .parse(&mut reader, ParseRule::STRICT)
1832            .expect("Failed to parse config");
1833
1834        let default_params = config.query("unknown-host");
1835        // verify default params
1836        assert_eq!(
1837            default_params.connect_timeout.unwrap(),
1838            Duration::from_secs(60) // first read
1839        );
1840        assert_eq!(
1841            default_params.server_alive_interval.unwrap(),
1842            Duration::from_secs(40) // first read
1843        );
1844        assert_eq!(default_params.tcp_keep_alive.unwrap(), true);
1845        assert_eq!(default_params.ciphers.algorithms().is_empty(), true);
1846        assert_eq!(
1847            default_params.ignore_unknown.as_deref().unwrap(),
1848            &["Pippo", "Pluto"]
1849        );
1850        assert_eq!(default_params.compression.unwrap(), true);
1851        assert_eq!(default_params.connection_attempts.unwrap(), 10);
1852
1853        // verify include 1 overwrites the default value
1854        let glob_params = config.query("192.168.1.1");
1855        assert_eq!(
1856            glob_params.connect_timeout.unwrap(),
1857            Duration::from_secs(60)
1858        );
1859        assert_eq!(
1860            glob_params.server_alive_interval.unwrap(),
1861            Duration::from_secs(40) // first read
1862        );
1863        assert_eq!(glob_params.tcp_keep_alive.unwrap(), true);
1864        assert_eq!(glob_params.ciphers.algorithms().is_empty(), true);
1865
1866        // verify tostapane
1867        let tostapane_params = config.query("tostapane");
1868        assert_eq!(
1869            tostapane_params.connect_timeout.unwrap(),
1870            Duration::from_secs(60) // first read
1871        );
1872        assert_eq!(
1873            tostapane_params.server_alive_interval.unwrap(),
1874            Duration::from_secs(40) // first read
1875        );
1876        assert_eq!(tostapane_params.tcp_keep_alive.unwrap(), true);
1877        // verify ciphers
1878        assert_eq!(
1879            tostapane_params.ciphers.algorithms(),
1880            &[
1881                "a-manella",
1882                "blowfish",
1883                "coi-piedi",
1884                "cazdecan",
1885                "triestin-stretto"
1886            ]
1887        );
1888
1889        // verify included host (microwave)
1890        let microwave_params = config.query("microwave");
1891        assert_eq!(
1892            microwave_params.connect_timeout.unwrap(),
1893            Duration::from_secs(60) // (not) updated in inc4
1894        );
1895        assert_eq!(
1896            microwave_params.server_alive_interval.unwrap(),
1897            Duration::from_secs(40) // (not) updated in inc4
1898        );
1899        assert_eq!(
1900            microwave_params.port.unwrap(),
1901            345 // updated in inc4
1902        );
1903        assert_eq!(microwave_params.tcp_keep_alive.unwrap(), true);
1904        assert_eq!(microwave_params.ciphers.algorithms().is_empty(), true);
1905        assert_eq!(microwave_params.user.as_deref().unwrap(), "mario-rossi");
1906        assert_eq!(
1907            microwave_params.host_name.as_deref().unwrap(),
1908            "192.168.24.33"
1909        );
1910        assert_eq!(microwave_params.remote_forward.unwrap(), 88);
1911        assert_eq!(microwave_params.compression.unwrap(), true);
1912
1913        // verify included host (fridge)
1914        let fridge_params = config.query("fridge");
1915        assert_eq!(
1916            fridge_params.connect_timeout.unwrap(),
1917            Duration::from_secs(60)
1918        ); // default
1919        assert_eq!(
1920            fridge_params.server_alive_interval.unwrap(),
1921            Duration::from_secs(40)
1922        ); // default
1923        assert_eq!(fridge_params.tcp_keep_alive.unwrap(), true);
1924        assert_eq!(fridge_params.ciphers.algorithms().is_empty(), true);
1925        assert_eq!(fridge_params.user.as_deref().unwrap(), "luigi-verdi");
1926        assert_eq!(fridge_params.host_name.as_deref().unwrap(), "192.168.24.34");
1927    }
1928
1929    #[allow(dead_code)]
1930    struct ConfigWithInclude {
1931        config: NamedTempFile,
1932        inc1: NamedTempFile,
1933        inc2: NamedTempFile,
1934        inc3: NamedTempFile,
1935        inc4: NamedTempFile,
1936    }
1937
1938    fn create_include_config() -> ConfigWithInclude {
1939        let mut config_file: tempfile::NamedTempFile =
1940            tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1941        let mut inc1_file: tempfile::NamedTempFile =
1942            tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1943        let mut inc2_file: tempfile::NamedTempFile =
1944            tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1945        let mut inc3_file: tempfile::NamedTempFile =
1946            tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1947        let mut inc4_file: tempfile::NamedTempFile =
1948            tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1949
1950        let config = format!(
1951            r##"
1952# ssh config
1953# written by veeso
1954
1955
1956        # I put a comment here just to annoy
1957
1958IgnoreUnknown Pippo,Pluto
1959
1960Compression yes
1961ConnectionAttempts          10
1962ConnectTimeout 60
1963ServerAliveInterval 40
1964Include {inc1}
1965
1966# Let's start defining some hosts
1967
1968Host tostapane
1969    User    ciro-esposito
1970    HostName    192.168.24.32
1971    RemoteForward   88
1972    Compression no
1973    # Ignore unknown fields should be inherited from the global section
1974    Pippo yes
1975    Pluto 56
1976    Include {inc2}
1977
1978Include {inc3}
1979Include {inc4}
1980"##,
1981            inc1 = inc1_file.path().display(),
1982            inc2 = inc2_file.path().display(),
1983            inc3 = inc3_file.path().display(),
1984            inc4 = inc4_file.path().display(),
1985        );
1986        config_file.write_all(config.as_bytes()).unwrap();
1987
1988        // write include 1
1989        let inc1 = r##"
1990        ConnectTimeout 60
1991        ServerAliveInterval 60
1992        TcpKeepAlive    yes
1993        "##;
1994        inc1_file.write_all(inc1.as_bytes()).unwrap();
1995
1996        // write include 2
1997        let inc2 = r##"
1998        ConnectTimeout 180
1999        ServerAliveInterval 180
2000        Ciphers     +a-manella,blowfish,coi-piedi,cazdecan,triestin-stretto
2001        "##;
2002        inc2_file.write_all(inc2.as_bytes()).unwrap();
2003
2004        // write include 3 with host directive
2005        let inc3 = r##"
2006Host microwave
2007    User    mario-rossi
2008    HostName    192.168.24.33
2009    RemoteForward   88
2010    Compression no
2011    # Ignore unknown fields should be inherited from the global section
2012    Pippo yes
2013    Pluto 56
2014"##;
2015        inc3_file.write_all(inc3.as_bytes()).unwrap();
2016
2017        // write include 4 which updates a param from microwave and then create a new host
2018        let inc4 = r##"
2019    # Update microwave
2020    ServerAliveInterval 30
2021    Port 345
2022
2023# Force microwave update (it won't work)
2024Host microwave
2025    ConnectTimeout 30
2026
2027Host fridge
2028    User    luigi-verdi
2029    HostName    192.168.24.34
2030    RemoteForward   88
2031    Compression no
2032"##;
2033        inc4_file.write_all(inc4.as_bytes()).unwrap();
2034
2035        ConfigWithInclude {
2036            config: config_file,
2037            inc1: inc1_file,
2038            inc2: inc2_file,
2039            inc3: inc3_file,
2040            inc4: inc4_file,
2041        }
2042    }
2043}