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