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