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