ssh2_config/parser/
mod.rs

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