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