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