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));
421 if let Some(stripped) = path_match.strip_prefix("~") {
423 format!("{dir}{PATH_SEPARATOR}{stripped}", dir = home_dir.display())
424 } else {
425 format!(
427 "{dir}{PATH_SEPARATOR}{path_match}",
428 dir = home_dir.join(".ssh").display()
429 )
430 }
431 }
432 }
433
434 fn include_files(
436 args: Vec<String>,
437 host: &mut Host,
438 rules: ParseRule,
439 default_algos: &DefaultAlgorithms,
440 ignore_unknown: Option<Vec<String>>,
441 ) -> SshParserResult<Vec<Host>> {
442 let path_match = Self::resolve_include_path(&Self::parse_string(args)?);
443
444 trace!("include files: {path_match}",);
445 let files = glob(&path_match)?;
446
447 let mut new_hosts = vec![];
448
449 for file in files {
450 let file = file?;
451 trace!("including file: {}", file.display());
452 let mut reader = BufReader::new(File::open(file)?);
453 let mut sub_config = SshConfig::default().default_algorithms(default_algos.clone());
454 Self::parse(&mut sub_config, &mut reader, rules, ignore_unknown.clone())?;
455
456 for pattern in &host.pattern {
458 if pattern.negated {
459 trace!("excluding sub-config for pattern: {pattern:?}",);
460 continue;
461 }
462 trace!("merging sub-config for pattern: {pattern:?}",);
463 let params = sub_config.query(&pattern.pattern);
464 host.params.overwrite_if_none(¶ms);
465 }
466
467 for sub_host in sub_config.hosts.into_iter().skip(1) {
469 trace!("adding sub-host: {sub_host:?}",);
470 new_hosts.push(sub_host);
471 }
472 }
473
474 Ok(new_hosts)
475 }
476
477 fn tokenize_line(line: &str) -> SshParserResult<(Field, Vec<String>)> {
490 let trimmed_line = line.trim();
492 let (field, other_tokens) = if trimmed_line.find('=').unwrap_or(usize::MAX)
494 < trimmed_line.find(char::is_whitespace).unwrap_or(usize::MAX)
495 {
496 trimmed_line
497 .split_once('=')
498 .ok_or(SshParserError::MissingArgument)?
499 } else {
500 trimmed_line
501 .split_once(char::is_whitespace)
502 .ok_or(SshParserError::MissingArgument)?
503 };
504
505 trace!("tokenized line '{line}' - field '{field}' with args '{other_tokens}'",);
506
507 let other_tokens = other_tokens.trim().trim_start_matches('=').trim();
509 trace!("other tokens trimmed: '{other_tokens}'",);
510
511 let args = if other_tokens.starts_with('"') && other_tokens.ends_with('"') {
513 trace!("quoted args: '{other_tokens}'",);
514 vec![other_tokens[1..other_tokens.len() - 1].to_string()]
515 } else {
516 trace!("splitting args (non-quoted): '{other_tokens}'",);
517 let tokens = other_tokens.split_whitespace();
519
520 tokens
521 .map(|x| x.trim().to_string())
522 .filter(|x| !x.is_empty())
523 .collect()
524 };
525
526 match Field::from_str(field) {
527 Ok(field) => Ok((field, args)),
528 Err(_) => Err(SshParserError::UnknownField(field.to_string(), args)),
529 }
530 }
531
532 fn parse_boolean(args: Vec<String>) -> SshParserResult<bool> {
536 match args.first().map(|x| x.as_str()) {
537 Some("yes") => Ok(true),
538 Some("no") => Ok(false),
539 Some(_) => Err(SshParserError::ExpectedBoolean),
540 None => Err(SshParserError::MissingArgument),
541 }
542 }
543
544 fn parse_algos(args: Vec<String>) -> SshParserResult<AlgorithmsRule> {
546 let first = args.first().ok_or(SshParserError::MissingArgument)?;
547
548 AlgorithmsRule::from_str(first)
549 }
550
551 fn parse_comma_separated_list(args: Vec<String>) -> SshParserResult<Vec<String>> {
553 match args
554 .first()
555 .map(|x| x.split(',').map(|x| x.to_string()).collect())
556 {
557 Some(args) => Ok(args),
558 _ => Err(SshParserError::MissingArgument),
559 }
560 }
561
562 fn parse_duration(args: Vec<String>) -> SshParserResult<Duration> {
564 let value = Self::parse_unsigned(args)?;
565 Ok(Duration::from_secs(value as u64))
566 }
567
568 fn parse_host(args: Vec<String>) -> SshParserResult<Vec<HostClause>> {
570 if args.is_empty() {
571 return Err(SshParserError::MissingArgument);
572 }
573 Ok(args
575 .into_iter()
576 .map(|x| {
577 let tokens: Vec<&str> = x.split('!').collect();
578 if tokens.len() == 2 {
579 HostClause::new(tokens[1].to_string(), true)
580 } else {
581 HostClause::new(tokens[0].to_string(), false)
582 }
583 })
584 .collect())
585 }
586
587 fn parse_path_list(args: Vec<String>) -> SshParserResult<Vec<PathBuf>> {
589 if args.is_empty() {
590 return Err(SshParserError::MissingArgument);
591 }
592 args.iter()
593 .map(|x| Self::parse_path_arg(x.as_str()))
594 .collect()
595 }
596
597 fn parse_path(args: Vec<String>) -> SshParserResult<PathBuf> {
599 if let Some(s) = args.first() {
600 Self::parse_path_arg(s)
601 } else {
602 Err(SshParserError::MissingArgument)
603 }
604 }
605
606 fn parse_path_arg(s: &str) -> SshParserResult<PathBuf> {
608 let s = if s.starts_with('~') {
610 let home_dir = dirs::home_dir()
611 .unwrap_or_else(|| PathBuf::from("~"))
612 .to_string_lossy()
613 .to_string();
614 s.replacen('~', &home_dir, 1)
615 } else {
616 s.to_string()
617 };
618 Ok(PathBuf::from(s))
619 }
620
621 fn parse_port(args: Vec<String>) -> SshParserResult<u16> {
623 match args.first().map(|x| u16::from_str(x)) {
624 Some(Ok(val)) => Ok(val),
625 Some(Err(_)) => Err(SshParserError::ExpectedPort),
626 None => Err(SshParserError::MissingArgument),
627 }
628 }
629
630 fn parse_string(args: Vec<String>) -> SshParserResult<String> {
632 if let Some(s) = args.into_iter().next() {
633 Ok(s)
634 } else {
635 Err(SshParserError::MissingArgument)
636 }
637 }
638
639 fn parse_unsigned(args: Vec<String>) -> SshParserResult<usize> {
641 match args.first().map(|x| usize::from_str(x)) {
642 Some(Ok(val)) => Ok(val),
643 Some(Err(_)) => Err(SshParserError::ExpectedUnsigned),
644 None => Err(SshParserError::MissingArgument),
645 }
646 }
647}
648
649#[cfg(test)]
650mod tests {
651
652 use std::fs::File;
653 use std::io::{BufReader, Write};
654 use std::path::Path;
655
656 use pretty_assertions::assert_eq;
657 use tempfile::NamedTempFile;
658
659 use super::*;
660 use crate::DefaultAlgorithms;
661
662 #[test]
663 fn should_parse_configuration() -> Result<(), SshParserError> {
664 crate::test_log();
665 let temp = create_ssh_config();
666 let file = File::open(temp.path()).expect("Failed to open tempfile");
667 let mut reader = BufReader::new(file);
668 let config = SshConfig::default()
669 .default_algorithms(DefaultAlgorithms {
670 ca_signature_algorithms: vec![],
671 ciphers: vec![],
672 host_key_algorithms: vec![],
673 kex_algorithms: vec![],
674 mac: vec![],
675 pubkey_accepted_algorithms: vec!["omar-crypt".to_string()],
676 })
677 .parse(&mut reader, ParseRule::STRICT)?;
678
679 let params = config.query("*");
682 assert_eq!(
683 params.ignore_unknown.as_deref().unwrap(),
684 &["Pippo", "Pluto"]
685 );
686 assert_eq!(params.compression.unwrap(), true);
687 assert_eq!(params.connection_attempts.unwrap(), 10);
688 assert_eq!(params.connect_timeout.unwrap(), Duration::from_secs(60));
689 assert_eq!(
690 params.server_alive_interval.unwrap(),
691 Duration::from_secs(40)
692 );
693 assert_eq!(params.tcp_keep_alive.unwrap(), true);
694 assert_eq!(params.ciphers.algorithms(), &["a-manella", "blowfish"]);
695 assert_eq!(
696 params.pubkey_accepted_algorithms.algorithms(),
697 &["desu", "omar-crypt", "fast-omar-crypt"]
698 );
699
700 assert_eq!(params.ca_signature_algorithms.algorithms(), &["random"]);
702 assert_eq!(
703 params.host_key_algorithms.algorithms(),
704 &["luigi", "mario",]
705 );
706 assert_eq!(params.kex_algorithms.algorithms(), &["desu", "gigi",]);
707 assert_eq!(params.mac.algorithms(), &["concorde"]);
708 assert!(params.bind_address.is_none());
709
710 let params_172_26_104_4 = config.query("172.26.104.4");
714
715 assert_eq!(params_172_26_104_4.add_keys_to_agent.unwrap(), true);
717 assert_eq!(params_172_26_104_4.compression.unwrap(), true);
718 assert_eq!(params_172_26_104_4.connection_attempts.unwrap(), 10);
719 assert_eq!(
720 params_172_26_104_4.connect_timeout.unwrap(),
721 Duration::from_secs(60)
722 );
723 assert_eq!(params_172_26_104_4.tcp_keep_alive.unwrap(), true);
724
725 assert_eq!(
727 params_172_26_104_4.ca_signature_algorithms.algorithms(),
728 &["random"]
729 );
730 assert_eq!(
731 params_172_26_104_4.ciphers.algorithms(),
732 &["a-manella", "blowfish",]
733 );
734 assert_eq!(params_172_26_104_4.mac.algorithms(), &["spyro", "deoxys"]); assert_eq!(
736 params_172_26_104_4.proxy_jump.unwrap(),
737 &["jump.example.com"]
738 ); assert_eq!(
740 params_172_26_104_4
741 .pubkey_accepted_algorithms
742 .algorithms()
743 .is_empty(), true
745 );
746 assert_eq!(
747 params_172_26_104_4.bind_address.as_deref().unwrap(),
748 "10.8.0.10"
749 );
750 assert_eq!(
751 params_172_26_104_4.bind_interface.as_deref().unwrap(),
752 "tun0"
753 );
754 assert_eq!(params_172_26_104_4.port.unwrap(), 2222);
755 assert_eq!(
756 params_172_26_104_4.identity_file.as_deref().unwrap(),
757 vec![
758 Path::new("/home/root/.ssh/pippo.key"),
759 Path::new("/home/root/.ssh/pluto.key")
760 ]
761 );
762 assert_eq!(params_172_26_104_4.user.as_deref().unwrap(), "omar");
763
764 let params_tostapane = config.query("tostapane");
766 assert_eq!(params_tostapane.compression.unwrap(), true); assert_eq!(params_tostapane.connection_attempts.unwrap(), 10);
768 assert_eq!(
769 params_tostapane.connect_timeout.unwrap(),
770 Duration::from_secs(60)
771 );
772 assert_eq!(params_tostapane.tcp_keep_alive.unwrap(), true);
773 assert_eq!(params_tostapane.remote_forward.unwrap(), 88);
774 assert_eq!(params_tostapane.user.as_deref().unwrap(), "ciro-esposito");
775
776 assert_eq!(
778 params_tostapane.ca_signature_algorithms.algorithms(),
779 &["random"]
780 );
781 assert_eq!(
782 params_tostapane.ciphers.algorithms(),
783 &["a-manella", "blowfish",]
784 );
785 assert_eq!(
786 params_tostapane.mac.algorithms(),
787 vec!["spyro".to_string(), "deoxys".to_string(),]
788 );
789 assert_eq!(
790 params_tostapane.proxy_jump.unwrap(),
791 vec![
792 "jump1.example.com".to_string(),
793 "jump2.example.com".to_string(),
794 ]
795 );
796 assert_eq!(
797 params_tostapane.pubkey_accepted_algorithms.algorithms(),
798 &["desu", "omar-crypt", "fast-omar-crypt"]
799 );
800
801 let params_192_168_1_30 = config.query("192.168.1.30");
803
804 assert_eq!(params_192_168_1_30.user.as_deref().unwrap(), "nutellaro");
806 assert_eq!(params_192_168_1_30.remote_forward.unwrap(), 123);
807
808 assert_eq!(params_192_168_1_30.compression.unwrap(), true);
810 assert_eq!(params_192_168_1_30.connection_attempts.unwrap(), 10);
811 assert_eq!(
812 params_192_168_1_30.connect_timeout.unwrap(),
813 Duration::from_secs(60)
814 );
815 assert_eq!(params_192_168_1_30.tcp_keep_alive.unwrap(), true);
816
817 assert_eq!(
819 params_192_168_1_30.ca_signature_algorithms.algorithms(),
820 &["random"]
821 );
822 assert_eq!(
823 params_192_168_1_30.ciphers.algorithms(),
824 &["a-manella", "blowfish"]
825 );
826 assert_eq!(params_192_168_1_30.mac.algorithms(), &["concorde"]);
827 assert_eq!(
828 params_192_168_1_30.pubkey_accepted_algorithms.algorithms(),
829 &["desu", "omar-crypt", "fast-omar-crypt"]
830 );
831
832 Ok(())
833 }
834
835 #[test]
836 fn should_allow_unknown_field() -> Result<(), SshParserError> {
837 crate::test_log();
838 let temp = create_ssh_config_with_unknown_fields();
839 let file = File::open(temp.path()).expect("Failed to open tempfile");
840 let mut reader = BufReader::new(file);
841 let _config = SshConfig::default()
842 .default_algorithms(DefaultAlgorithms::empty())
843 .parse(&mut reader, ParseRule::ALLOW_UNKNOWN_FIELDS)?;
844
845 Ok(())
846 }
847
848 #[test]
849 fn should_not_allow_unknown_field() {
850 crate::test_log();
851 let temp = create_ssh_config_with_unknown_fields();
852 let file = File::open(temp.path()).expect("Failed to open tempfile");
853 let mut reader = BufReader::new(file);
854 assert!(matches!(
855 SshConfig::default()
856 .default_algorithms(DefaultAlgorithms::empty())
857 .parse(&mut reader, ParseRule::STRICT)
858 .unwrap_err(),
859 SshParserError::UnknownField(..)
860 ));
861 }
862
863 #[test]
864 fn should_store_unknown_fields() {
865 crate::test_log();
866 let temp = create_ssh_config_with_unknown_fields();
867 let file = File::open(temp.path()).expect("Failed to open tempfile");
868 let mut reader = BufReader::new(file);
869 let config = SshConfig::default()
870 .default_algorithms(DefaultAlgorithms::empty())
871 .parse(&mut reader, ParseRule::ALLOW_UNKNOWN_FIELDS)
872 .unwrap();
873
874 let host = config.query("cross-platform");
875 assert_eq!(
876 host.ignored_fields.get("Piropero").unwrap(),
877 &vec![String::from("yes")]
878 );
879 }
880
881 #[test]
882 fn should_parse_inversed_ssh_config() {
883 crate::test_log();
884 let temp = create_inverted_ssh_config();
885 let file = File::open(temp.path()).expect("Failed to open tempfile");
886 let mut reader = BufReader::new(file);
887 let config = SshConfig::default()
888 .default_algorithms(DefaultAlgorithms::empty())
889 .parse(&mut reader, ParseRule::STRICT)
890 .unwrap();
891
892 let home_dir = dirs::home_dir()
893 .unwrap_or_else(|| PathBuf::from("~"))
894 .to_string_lossy()
895 .to_string();
896
897 let remote_host = config.query("remote-host");
898
899 assert_eq!(
901 remote_host.identity_file.unwrap()[0].as_path(),
902 Path::new(format!("{home_dir}/.ssh/id_rsa_good").as_str()) );
904
905 assert_eq!(remote_host.host_name.unwrap(), "hostname.com");
907 assert_eq!(remote_host.user.unwrap(), "user");
908
909 assert_eq!(
911 remote_host.connect_timeout.unwrap(),
912 Duration::from_secs(15)
913 );
914 }
915
916 #[test]
917 fn should_parse_configuration_with_hosts() {
918 crate::test_log();
919 let temp = create_ssh_config_with_comments();
920
921 let file = File::open(temp.path()).expect("Failed to open tempfile");
922 let mut reader = BufReader::new(file);
923 let config = SshConfig::default()
924 .default_algorithms(DefaultAlgorithms::empty())
925 .parse(&mut reader, ParseRule::STRICT)
926 .unwrap();
927
928 let hostname = config.query("cross-platform").host_name.unwrap();
929 assert_eq!(&hostname, "hostname.com");
930
931 assert!(config.query("this").host_name.is_none());
932 }
933
934 #[test]
935 fn should_update_host_bind_address() -> Result<(), SshParserError> {
936 crate::test_log();
937 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
938 SshConfigParser::update_host(
939 Field::BindAddress,
940 vec![String::from("127.0.0.1")],
941 &mut host,
942 ParseRule::ALLOW_UNKNOWN_FIELDS,
943 &DefaultAlgorithms::empty(),
944 )?;
945 assert_eq!(host.params.bind_address.as_deref().unwrap(), "127.0.0.1");
946 Ok(())
947 }
948
949 #[test]
950 fn should_update_host_bind_interface() -> Result<(), SshParserError> {
951 crate::test_log();
952 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
953 SshConfigParser::update_host(
954 Field::BindInterface,
955 vec![String::from("aaa")],
956 &mut host,
957 ParseRule::ALLOW_UNKNOWN_FIELDS,
958 &DefaultAlgorithms::empty(),
959 )?;
960 assert_eq!(host.params.bind_interface.as_deref().unwrap(), "aaa");
961 Ok(())
962 }
963
964 #[test]
965 fn should_update_host_ca_signature_algos() -> Result<(), SshParserError> {
966 crate::test_log();
967 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
968 SshConfigParser::update_host(
969 Field::CaSignatureAlgorithms,
970 vec![String::from("a,b,c")],
971 &mut host,
972 ParseRule::ALLOW_UNKNOWN_FIELDS,
973 &DefaultAlgorithms::empty(),
974 )?;
975 assert_eq!(
976 host.params.ca_signature_algorithms.algorithms(),
977 &["a", "b", "c"]
978 );
979 Ok(())
980 }
981
982 #[test]
983 fn should_update_host_certificate_file() -> Result<(), SshParserError> {
984 crate::test_log();
985 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
986 SshConfigParser::update_host(
987 Field::CertificateFile,
988 vec![String::from("/tmp/a.crt")],
989 &mut host,
990 ParseRule::ALLOW_UNKNOWN_FIELDS,
991 &DefaultAlgorithms::empty(),
992 )?;
993 assert_eq!(
994 host.params.certificate_file.as_deref().unwrap(),
995 Path::new("/tmp/a.crt")
996 );
997 Ok(())
998 }
999
1000 #[test]
1001 fn should_update_host_ciphers() -> Result<(), SshParserError> {
1002 crate::test_log();
1003 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1004 SshConfigParser::update_host(
1005 Field::Ciphers,
1006 vec![String::from("a,b,c")],
1007 &mut host,
1008 ParseRule::ALLOW_UNKNOWN_FIELDS,
1009 &DefaultAlgorithms::empty(),
1010 )?;
1011 assert_eq!(host.params.ciphers.algorithms(), &["a", "b", "c"]);
1012 Ok(())
1013 }
1014
1015 #[test]
1016 fn should_update_host_compression() -> Result<(), SshParserError> {
1017 crate::test_log();
1018 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1019 SshConfigParser::update_host(
1020 Field::Compression,
1021 vec![String::from("yes")],
1022 &mut host,
1023 ParseRule::ALLOW_UNKNOWN_FIELDS,
1024 &DefaultAlgorithms::empty(),
1025 )?;
1026 assert_eq!(host.params.compression.unwrap(), true);
1027 Ok(())
1028 }
1029
1030 #[test]
1031 fn should_update_host_connection_attempts() -> Result<(), SshParserError> {
1032 crate::test_log();
1033 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1034 SshConfigParser::update_host(
1035 Field::ConnectionAttempts,
1036 vec![String::from("4")],
1037 &mut host,
1038 ParseRule::ALLOW_UNKNOWN_FIELDS,
1039 &DefaultAlgorithms::empty(),
1040 )?;
1041 assert_eq!(host.params.connection_attempts.unwrap(), 4);
1042 Ok(())
1043 }
1044
1045 #[test]
1046 fn should_update_host_connection_timeout() -> Result<(), SshParserError> {
1047 crate::test_log();
1048 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1049 SshConfigParser::update_host(
1050 Field::ConnectTimeout,
1051 vec![String::from("10")],
1052 &mut host,
1053 ParseRule::ALLOW_UNKNOWN_FIELDS,
1054 &DefaultAlgorithms::empty(),
1055 )?;
1056 assert_eq!(
1057 host.params.connect_timeout.unwrap(),
1058 Duration::from_secs(10)
1059 );
1060 Ok(())
1061 }
1062
1063 #[test]
1064 fn should_update_host_key_algorithms() -> Result<(), SshParserError> {
1065 crate::test_log();
1066 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1067 SshConfigParser::update_host(
1068 Field::HostKeyAlgorithms,
1069 vec![String::from("a,b,c")],
1070 &mut host,
1071 ParseRule::ALLOW_UNKNOWN_FIELDS,
1072 &DefaultAlgorithms::empty(),
1073 )?;
1074 assert_eq!(
1075 host.params.host_key_algorithms.algorithms(),
1076 &["a", "b", "c"]
1077 );
1078 Ok(())
1079 }
1080
1081 #[test]
1082 fn should_update_host_host_name() -> Result<(), SshParserError> {
1083 crate::test_log();
1084 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1085 SshConfigParser::update_host(
1086 Field::HostName,
1087 vec![String::from("192.168.1.1")],
1088 &mut host,
1089 ParseRule::ALLOW_UNKNOWN_FIELDS,
1090 &DefaultAlgorithms::empty(),
1091 )?;
1092 assert_eq!(host.params.host_name.as_deref().unwrap(), "192.168.1.1");
1093 Ok(())
1094 }
1095
1096 #[test]
1097 fn should_update_host_ignore_unknown() -> Result<(), SshParserError> {
1098 crate::test_log();
1099 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1100 SshConfigParser::update_host(
1101 Field::IgnoreUnknown,
1102 vec![String::from("a,b,c")],
1103 &mut host,
1104 ParseRule::ALLOW_UNKNOWN_FIELDS,
1105 &DefaultAlgorithms::empty(),
1106 )?;
1107 assert_eq!(
1108 host.params.ignore_unknown.as_deref().unwrap(),
1109 &["a", "b", "c"]
1110 );
1111 Ok(())
1112 }
1113
1114 #[test]
1115 fn should_update_kex_algorithms() -> Result<(), SshParserError> {
1116 crate::test_log();
1117 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1118 SshConfigParser::update_host(
1119 Field::KexAlgorithms,
1120 vec![String::from("a,b,c")],
1121 &mut host,
1122 ParseRule::ALLOW_UNKNOWN_FIELDS,
1123 &DefaultAlgorithms::empty(),
1124 )?;
1125 assert_eq!(host.params.kex_algorithms.algorithms(), &["a", "b", "c"]);
1126 Ok(())
1127 }
1128
1129 #[test]
1130 fn should_update_host_mac() -> Result<(), SshParserError> {
1131 crate::test_log();
1132 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1133 SshConfigParser::update_host(
1134 Field::Mac,
1135 vec![String::from("a,b,c")],
1136 &mut host,
1137 ParseRule::ALLOW_UNKNOWN_FIELDS,
1138 &DefaultAlgorithms::empty(),
1139 )?;
1140 assert_eq!(host.params.mac.algorithms(), &["a", "b", "c"]);
1141 Ok(())
1142 }
1143
1144 #[test]
1145 fn should_update_host_port() -> Result<(), SshParserError> {
1146 crate::test_log();
1147 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1148 SshConfigParser::update_host(
1149 Field::Port,
1150 vec![String::from("2222")],
1151 &mut host,
1152 ParseRule::ALLOW_UNKNOWN_FIELDS,
1153 &DefaultAlgorithms::empty(),
1154 )?;
1155 assert_eq!(host.params.port.unwrap(), 2222);
1156 Ok(())
1157 }
1158
1159 #[test]
1160 fn should_update_host_pubkey_accepted_algos() -> Result<(), SshParserError> {
1161 crate::test_log();
1162 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1163 SshConfigParser::update_host(
1164 Field::PubkeyAcceptedAlgorithms,
1165 vec![String::from("a,b,c")],
1166 &mut host,
1167 ParseRule::ALLOW_UNKNOWN_FIELDS,
1168 &DefaultAlgorithms::empty(),
1169 )?;
1170 assert_eq!(
1171 host.params.pubkey_accepted_algorithms.algorithms(),
1172 &["a", "b", "c"]
1173 );
1174 Ok(())
1175 }
1176
1177 #[test]
1178 fn should_update_host_pubkey_authentication() -> Result<(), SshParserError> {
1179 crate::test_log();
1180 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1181 SshConfigParser::update_host(
1182 Field::PubkeyAuthentication,
1183 vec![String::from("yes")],
1184 &mut host,
1185 ParseRule::ALLOW_UNKNOWN_FIELDS,
1186 &DefaultAlgorithms::empty(),
1187 )?;
1188 assert_eq!(host.params.pubkey_authentication.unwrap(), true);
1189 Ok(())
1190 }
1191
1192 #[test]
1193 fn should_update_host_remote_forward() -> Result<(), SshParserError> {
1194 crate::test_log();
1195 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1196 SshConfigParser::update_host(
1197 Field::RemoteForward,
1198 vec![String::from("3005")],
1199 &mut host,
1200 ParseRule::ALLOW_UNKNOWN_FIELDS,
1201 &DefaultAlgorithms::empty(),
1202 )?;
1203 assert_eq!(host.params.remote_forward.unwrap(), 3005);
1204 Ok(())
1205 }
1206
1207 #[test]
1208 fn should_update_host_server_alive_interval() -> Result<(), SshParserError> {
1209 crate::test_log();
1210 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1211 SshConfigParser::update_host(
1212 Field::ServerAliveInterval,
1213 vec![String::from("40")],
1214 &mut host,
1215 ParseRule::ALLOW_UNKNOWN_FIELDS,
1216 &DefaultAlgorithms::empty(),
1217 )?;
1218 assert_eq!(
1219 host.params.server_alive_interval.unwrap(),
1220 Duration::from_secs(40)
1221 );
1222 Ok(())
1223 }
1224
1225 #[test]
1226 fn should_update_host_tcp_keep_alive() -> Result<(), SshParserError> {
1227 crate::test_log();
1228 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1229 SshConfigParser::update_host(
1230 Field::TcpKeepAlive,
1231 vec![String::from("no")],
1232 &mut host,
1233 ParseRule::ALLOW_UNKNOWN_FIELDS,
1234 &DefaultAlgorithms::empty(),
1235 )?;
1236 assert_eq!(host.params.tcp_keep_alive.unwrap(), false);
1237 Ok(())
1238 }
1239
1240 #[test]
1241 fn should_update_host_user() -> Result<(), SshParserError> {
1242 crate::test_log();
1243 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1244 SshConfigParser::update_host(
1245 Field::User,
1246 vec![String::from("pippo")],
1247 &mut host,
1248 ParseRule::ALLOW_UNKNOWN_FIELDS,
1249 &DefaultAlgorithms::empty(),
1250 )?;
1251 assert_eq!(host.params.user.as_deref().unwrap(), "pippo");
1252 Ok(())
1253 }
1254
1255 #[test]
1256 fn should_not_update_host_if_unknown() -> Result<(), SshParserError> {
1257 crate::test_log();
1258 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1259 let result = SshConfigParser::update_host(
1260 Field::PasswordAuthentication,
1261 vec![String::from("yes")],
1262 &mut host,
1263 ParseRule::ALLOW_UNKNOWN_FIELDS,
1264 &DefaultAlgorithms::empty(),
1265 );
1266
1267 match result {
1268 Ok(_) | Err(SshParserError::UnsupportedField(_, _)) => Ok(()),
1269 Err(e) => Err(e),
1270 }?;
1271
1272 assert_eq!(host.params, HostParams::new(&DefaultAlgorithms::empty()));
1273 Ok(())
1274 }
1275
1276 #[test]
1277 fn should_update_host_if_unsupported() -> Result<(), SshParserError> {
1278 crate::test_log();
1279 let mut host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::empty()));
1280 let result = SshConfigParser::update_host(
1281 Field::PasswordAuthentication,
1282 vec![String::from("yes")],
1283 &mut host,
1284 ParseRule::ALLOW_UNKNOWN_FIELDS,
1285 &DefaultAlgorithms::empty(),
1286 );
1287
1288 match result {
1289 Err(SshParserError::UnsupportedField(field, _)) => {
1290 assert_eq!(field, "passwordauthentication");
1291 Ok(())
1292 }
1293 Ok(_) => Ok(()),
1294 Err(e) => Err(e),
1295 }?;
1296
1297 assert_eq!(host.params, HostParams::new(&DefaultAlgorithms::empty()));
1298 Ok(())
1299 }
1300
1301 #[test]
1302 fn should_tokenize_line() -> Result<(), SshParserError> {
1303 crate::test_log();
1304 assert_eq!(
1305 SshConfigParser::tokenize_line("HostName 192.168.*.* 172.26.*.*")?,
1306 (
1307 Field::HostName,
1308 vec![String::from("192.168.*.*"), String::from("172.26.*.*")]
1309 )
1310 );
1311 assert_eq!(
1313 SshConfigParser::tokenize_line(
1314 " HostName 192.168.*.* 172.26.*.* "
1315 )?,
1316 (
1317 Field::HostName,
1318 vec![String::from("192.168.*.*"), String::from("172.26.*.*")]
1319 )
1320 );
1321 Ok(())
1322 }
1323
1324 #[test]
1325 fn should_not_tokenize_line() {
1326 crate::test_log();
1327 assert!(matches!(
1328 SshConfigParser::tokenize_line("Omar yes").unwrap_err(),
1329 SshParserError::UnknownField(..)
1330 ));
1331 }
1332
1333 #[test]
1334 fn should_fail_parsing_field() {
1335 crate::test_log();
1336
1337 assert!(matches!(
1338 SshConfigParser::tokenize_line(" ").unwrap_err(),
1339 SshParserError::MissingArgument
1340 ));
1341 }
1342
1343 #[test]
1344 fn should_parse_boolean() -> Result<(), SshParserError> {
1345 crate::test_log();
1346 assert_eq!(
1347 SshConfigParser::parse_boolean(vec![String::from("yes")])?,
1348 true
1349 );
1350 assert_eq!(
1351 SshConfigParser::parse_boolean(vec![String::from("no")])?,
1352 false
1353 );
1354 Ok(())
1355 }
1356
1357 #[test]
1358 fn should_fail_parsing_boolean() {
1359 crate::test_log();
1360 assert!(matches!(
1361 SshConfigParser::parse_boolean(vec!["boh".to_string()]).unwrap_err(),
1362 SshParserError::ExpectedBoolean
1363 ));
1364 assert!(matches!(
1365 SshConfigParser::parse_boolean(vec![]).unwrap_err(),
1366 SshParserError::MissingArgument
1367 ));
1368 }
1369
1370 #[test]
1371 fn should_parse_algos() -> Result<(), SshParserError> {
1372 crate::test_log();
1373 assert_eq!(
1374 SshConfigParser::parse_algos(vec![String::from("a,b,c,d")])?,
1375 AlgorithmsRule::Set(vec![
1376 "a".to_string(),
1377 "b".to_string(),
1378 "c".to_string(),
1379 "d".to_string(),
1380 ])
1381 );
1382
1383 assert_eq!(
1384 SshConfigParser::parse_algos(vec![String::from("a")])?,
1385 AlgorithmsRule::Set(vec!["a".to_string()])
1386 );
1387
1388 assert_eq!(
1389 SshConfigParser::parse_algos(vec![String::from("+a,b")])?,
1390 AlgorithmsRule::Append(vec!["a".to_string(), "b".to_string()])
1391 );
1392
1393 Ok(())
1394 }
1395
1396 #[test]
1397 fn should_parse_comma_separated_list() -> Result<(), SshParserError> {
1398 crate::test_log();
1399 assert_eq!(
1400 SshConfigParser::parse_comma_separated_list(vec![String::from("a,b,c,d")])?,
1401 vec![
1402 "a".to_string(),
1403 "b".to_string(),
1404 "c".to_string(),
1405 "d".to_string(),
1406 ]
1407 );
1408 assert_eq!(
1409 SshConfigParser::parse_comma_separated_list(vec![String::from("a")])?,
1410 vec!["a".to_string()]
1411 );
1412 Ok(())
1413 }
1414
1415 #[test]
1416 fn should_fail_parsing_comma_separated_list() {
1417 crate::test_log();
1418 assert!(matches!(
1419 SshConfigParser::parse_comma_separated_list(vec![]).unwrap_err(),
1420 SshParserError::MissingArgument
1421 ));
1422 }
1423
1424 #[test]
1425 fn should_parse_duration() -> Result<(), SshParserError> {
1426 crate::test_log();
1427 assert_eq!(
1428 SshConfigParser::parse_duration(vec![String::from("60")])?,
1429 Duration::from_secs(60)
1430 );
1431 Ok(())
1432 }
1433
1434 #[test]
1435 fn should_fail_parsing_duration() {
1436 crate::test_log();
1437 assert!(matches!(
1438 SshConfigParser::parse_duration(vec![String::from("AAA")]).unwrap_err(),
1439 SshParserError::ExpectedUnsigned
1440 ));
1441 assert!(matches!(
1442 SshConfigParser::parse_duration(vec![]).unwrap_err(),
1443 SshParserError::MissingArgument
1444 ));
1445 }
1446
1447 #[test]
1448 fn should_parse_host() -> Result<(), SshParserError> {
1449 crate::test_log();
1450 assert_eq!(
1451 SshConfigParser::parse_host(vec![
1452 String::from("192.168.*.*"),
1453 String::from("!192.168.1.1"),
1454 String::from("172.26.104.*"),
1455 String::from("!172.26.104.10"),
1456 ])?,
1457 vec![
1458 HostClause::new(String::from("192.168.*.*"), false),
1459 HostClause::new(String::from("192.168.1.1"), true),
1460 HostClause::new(String::from("172.26.104.*"), false),
1461 HostClause::new(String::from("172.26.104.10"), true),
1462 ]
1463 );
1464 Ok(())
1465 }
1466
1467 #[test]
1468 fn should_fail_parsing_host() {
1469 crate::test_log();
1470 assert!(matches!(
1471 SshConfigParser::parse_host(vec![]).unwrap_err(),
1472 SshParserError::MissingArgument
1473 ));
1474 }
1475
1476 #[test]
1477 fn should_parse_path() -> Result<(), SshParserError> {
1478 crate::test_log();
1479 assert_eq!(
1480 SshConfigParser::parse_path(vec![String::from("/tmp/a.txt")])?,
1481 PathBuf::from("/tmp/a.txt")
1482 );
1483 Ok(())
1484 }
1485
1486 #[test]
1487 fn should_parse_path_and_resolve_tilde() -> Result<(), SshParserError> {
1488 crate::test_log();
1489 let mut expected = dirs::home_dir().unwrap();
1490 expected.push(".ssh/id_dsa");
1491 assert_eq!(
1492 SshConfigParser::parse_path(vec![String::from("~/.ssh/id_dsa")])?,
1493 expected
1494 );
1495 Ok(())
1496 }
1497
1498 #[test]
1499 fn should_parse_path_list() -> Result<(), SshParserError> {
1500 crate::test_log();
1501 assert_eq!(
1502 SshConfigParser::parse_path_list(vec![
1503 String::from("/tmp/a.txt"),
1504 String::from("/tmp/b.txt")
1505 ])?,
1506 vec![PathBuf::from("/tmp/a.txt"), PathBuf::from("/tmp/b.txt")]
1507 );
1508 Ok(())
1509 }
1510
1511 #[test]
1512 fn should_fail_parse_path_list() {
1513 crate::test_log();
1514 assert!(matches!(
1515 SshConfigParser::parse_path_list(vec![]).unwrap_err(),
1516 SshParserError::MissingArgument
1517 ));
1518 }
1519
1520 #[test]
1521 fn should_fail_parsing_path() {
1522 crate::test_log();
1523 assert!(matches!(
1524 SshConfigParser::parse_path(vec![]).unwrap_err(),
1525 SshParserError::MissingArgument
1526 ));
1527 }
1528
1529 #[test]
1530 fn should_parse_port() -> Result<(), SshParserError> {
1531 crate::test_log();
1532 assert_eq!(SshConfigParser::parse_port(vec![String::from("22")])?, 22);
1533 Ok(())
1534 }
1535
1536 #[test]
1537 fn should_fail_parsing_port() {
1538 crate::test_log();
1539 assert!(matches!(
1540 SshConfigParser::parse_port(vec![String::from("1234567")]).unwrap_err(),
1541 SshParserError::ExpectedPort
1542 ));
1543 assert!(matches!(
1544 SshConfigParser::parse_port(vec![]).unwrap_err(),
1545 SshParserError::MissingArgument
1546 ));
1547 }
1548
1549 #[test]
1550 fn should_parse_string() -> Result<(), SshParserError> {
1551 crate::test_log();
1552 assert_eq!(
1553 SshConfigParser::parse_string(vec![String::from("foobar")])?,
1554 String::from("foobar")
1555 );
1556 Ok(())
1557 }
1558
1559 #[test]
1560 fn should_fail_parsing_string() {
1561 crate::test_log();
1562 assert!(matches!(
1563 SshConfigParser::parse_string(vec![]).unwrap_err(),
1564 SshParserError::MissingArgument
1565 ));
1566 }
1567
1568 #[test]
1569 fn should_parse_unsigned() -> Result<(), SshParserError> {
1570 crate::test_log();
1571 assert_eq!(
1572 SshConfigParser::parse_unsigned(vec![String::from("43")])?,
1573 43
1574 );
1575 Ok(())
1576 }
1577
1578 #[test]
1579 fn should_fail_parsing_unsigned() {
1580 crate::test_log();
1581 assert!(matches!(
1582 SshConfigParser::parse_unsigned(vec![String::from("abc")]).unwrap_err(),
1583 SshParserError::ExpectedUnsigned
1584 ));
1585 assert!(matches!(
1586 SshConfigParser::parse_unsigned(vec![]).unwrap_err(),
1587 SshParserError::MissingArgument
1588 ));
1589 }
1590
1591 #[test]
1592 fn should_strip_comments() {
1593 crate::test_log();
1594
1595 assert_eq!(
1596 SshConfigParser::strip_comments("host my_host # this is my fav host").as_str(),
1597 "host my_host "
1598 );
1599 assert_eq!(
1600 SshConfigParser::strip_comments("# this is a comment").as_str(),
1601 ""
1602 );
1603 }
1604
1605 #[test]
1606 fn test_should_parse_config_with_quotes_and_eq() {
1607 crate::test_log();
1608
1609 let config = create_ssh_config_with_quotes_and_eq();
1610 let file = File::open(config.path()).expect("Failed to open tempfile");
1611 let mut reader = BufReader::new(file);
1612
1613 let config = SshConfig::default()
1614 .default_algorithms(DefaultAlgorithms::empty())
1615 .parse(&mut reader, ParseRule::STRICT)
1616 .expect("Failed to parse config");
1617
1618 let params = config.query("foo");
1619
1620 assert_eq!(
1622 params.connect_timeout.expect("unspec connect timeout"),
1623 Duration::from_secs(15)
1624 );
1625 assert_eq!(
1626 params
1627 .ignore_unknown
1628 .as_deref()
1629 .expect("unspec ignore unknown"),
1630 &["Pippo", "Pluto"]
1631 );
1632 assert_eq!(
1633 params
1634 .ciphers
1635 .algorithms()
1636 .iter()
1637 .map(|x| x.as_str())
1638 .collect::<Vec<&str>>(),
1639 &["Pepperoni Pizza", "Margherita Pizza", "Hawaiian Pizza"]
1640 );
1641 assert_eq!(
1642 params
1643 .mac
1644 .algorithms()
1645 .iter()
1646 .map(|x| x.as_str())
1647 .collect::<Vec<&str>>(),
1648 &["Pasta Carbonara", "Pasta con tonno"]
1649 );
1650 }
1651
1652 #[test]
1653 fn test_should_resolve_absolute_include_path() {
1654 crate::test_log();
1655
1656 let expected = PathBuf::from("/tmp/config.local");
1657
1658 let s = "/tmp/config.local";
1659 let resolved = PathBuf::from(SshConfigParser::resolve_include_path(s));
1660 assert_eq!(resolved, expected);
1661 }
1662
1663 #[test]
1664 fn test_should_resolve_relative_include_path() {
1665 crate::test_log();
1666
1667 let expected = dirs::home_dir()
1668 .unwrap_or_else(|| PathBuf::from("~"))
1669 .join(".ssh")
1670 .join("config.local");
1671
1672 let s = "config.local";
1673 let resolved = PathBuf::from(SshConfigParser::resolve_include_path(s));
1674 assert_eq!(resolved, expected);
1675 }
1676
1677 #[test]
1678 fn test_should_resolve_include_path_with_tilde() {
1679 let p = "~/.ssh/config.local";
1680 let resolved = SshConfigParser::resolve_include_path(p);
1681 let mut expected = dirs::home_dir().unwrap_or_else(|| PathBuf::from("~"));
1682 expected.push(".ssh");
1683 expected.push("config.local");
1684 assert_eq!(PathBuf::from(resolved), expected);
1685 }
1686
1687 fn create_ssh_config_with_quotes_and_eq() -> NamedTempFile {
1688 let mut tmpfile: tempfile::NamedTempFile =
1689 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1690 let config = r##"
1691# ssh config
1692# written by veeso
1693
1694
1695# I put a comment here just to annoy
1696
1697IgnoreUnknown=Pippo,Pluto
1698ConnectTimeout = 15
1699Ciphers "Pepperoni Pizza,Margherita Pizza,Hawaiian Pizza"
1700Macs="Pasta Carbonara,Pasta con tonno"
1701"##;
1702 tmpfile.write_all(config.as_bytes()).unwrap();
1703 tmpfile
1704 }
1705
1706 fn create_ssh_config() -> NamedTempFile {
1707 let mut tmpfile: tempfile::NamedTempFile =
1708 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1709 let config = r##"
1710# ssh config
1711# written by veeso
1712
1713
1714 # I put a comment here just to annoy
1715
1716IgnoreUnknown Pippo,Pluto
1717
1718Compression yes
1719ConnectionAttempts 10
1720ConnectTimeout 60
1721ServerAliveInterval 40
1722TcpKeepAlive yes
1723Ciphers +a-manella,blowfish
1724
1725# Let's start defining some hosts
1726
1727Host 192.168.*.* 172.26.*.* !192.168.1.30
1728 User omar
1729 # ForwardX11 is actually not supported; I just want to see that it wont' fail parsing
1730 ForwardX11 yes
1731 BindAddress 10.8.0.10
1732 BindInterface tun0
1733 AddKeysToAgent yes
1734 Ciphers +coi-piedi,cazdecan,triestin-stretto
1735 IdentityFile /home/root/.ssh/pippo.key /home/root/.ssh/pluto.key
1736 Macs spyro,deoxys
1737 Port 2222
1738 PubkeyAcceptedAlgorithms -omar-crypt
1739 ProxyJump jump.example.com
1740
1741Host tostapane
1742 User ciro-esposito
1743 HostName 192.168.24.32
1744 RemoteForward 88
1745 Compression no
1746 Pippo yes
1747 Pluto 56
1748 ProxyJump jump1.example.com,jump2.example.com
1749 Macs +spyro,deoxys
1750
1751Host 192.168.1.30
1752 User nutellaro
1753 RemoteForward 123
1754
1755Host *
1756 CaSignatureAlgorithms random
1757 HostKeyAlgorithms luigi,mario
1758 KexAlgorithms desu,gigi
1759 Macs concorde
1760 PubkeyAcceptedAlgorithms desu,omar-crypt,fast-omar-crypt
1761"##;
1762 tmpfile.write_all(config.as_bytes()).unwrap();
1763 tmpfile
1764 }
1765
1766 fn create_inverted_ssh_config() -> NamedTempFile {
1767 let mut tmpfile: tempfile::NamedTempFile =
1768 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1769 let config = r##"
1770Host *-host
1771 IdentityFile ~/.ssh/id_rsa_good
1772
1773Host remote-*
1774 HostName hostname.com
1775 User user
1776 IdentityFile ~/.ssh/id_rsa_bad
1777
1778Host *
1779 ConnectTimeout 15
1780 IdentityFile ~/.ssh/id_rsa_ugly
1781 "##;
1782 tmpfile.write_all(config.as_bytes()).unwrap();
1783 tmpfile
1784 }
1785
1786 fn create_ssh_config_with_comments() -> NamedTempFile {
1787 let mut tmpfile: tempfile::NamedTempFile =
1788 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1789 let config = r##"
1790Host cross-platform # this is my fav host
1791 HostName hostname.com
1792 User user
1793 IdentityFile ~/.ssh/id_rsa_good
1794
1795Host *
1796 AddKeysToAgent yes
1797 IdentityFile ~/.ssh/id_rsa_bad
1798 "##;
1799 tmpfile.write_all(config.as_bytes()).unwrap();
1800 tmpfile
1801 }
1802
1803 fn create_ssh_config_with_unknown_fields() -> NamedTempFile {
1804 let mut tmpfile: tempfile::NamedTempFile =
1805 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1806 let config = r##"
1807Host cross-platform # this is my fav host
1808 HostName hostname.com
1809 User user
1810 IdentityFile ~/.ssh/id_rsa_good
1811 Piropero yes
1812
1813Host *
1814 AddKeysToAgent yes
1815 IdentityFile ~/.ssh/id_rsa_bad
1816 "##;
1817 tmpfile.write_all(config.as_bytes()).unwrap();
1818 tmpfile
1819 }
1820
1821 #[test]
1822 fn test_should_parse_config_with_include() {
1823 crate::test_log();
1824
1825 let config = create_include_config();
1826 let file = File::open(config.config.path()).expect("Failed to open tempfile");
1827 let mut reader = BufReader::new(file);
1828
1829 let config = SshConfig::default()
1830 .default_algorithms(DefaultAlgorithms::empty())
1831 .parse(&mut reader, ParseRule::STRICT)
1832 .expect("Failed to parse config");
1833
1834 let default_params = config.query("unknown-host");
1835 assert_eq!(
1837 default_params.connect_timeout.unwrap(),
1838 Duration::from_secs(60) );
1840 assert_eq!(
1841 default_params.server_alive_interval.unwrap(),
1842 Duration::from_secs(40) );
1844 assert_eq!(default_params.tcp_keep_alive.unwrap(), true);
1845 assert_eq!(default_params.ciphers.algorithms().is_empty(), true);
1846 assert_eq!(
1847 default_params.ignore_unknown.as_deref().unwrap(),
1848 &["Pippo", "Pluto"]
1849 );
1850 assert_eq!(default_params.compression.unwrap(), true);
1851 assert_eq!(default_params.connection_attempts.unwrap(), 10);
1852
1853 let glob_params = config.query("192.168.1.1");
1855 assert_eq!(
1856 glob_params.connect_timeout.unwrap(),
1857 Duration::from_secs(60)
1858 );
1859 assert_eq!(
1860 glob_params.server_alive_interval.unwrap(),
1861 Duration::from_secs(40) );
1863 assert_eq!(glob_params.tcp_keep_alive.unwrap(), true);
1864 assert_eq!(glob_params.ciphers.algorithms().is_empty(), true);
1865
1866 let tostapane_params = config.query("tostapane");
1868 assert_eq!(
1869 tostapane_params.connect_timeout.unwrap(),
1870 Duration::from_secs(60) );
1872 assert_eq!(
1873 tostapane_params.server_alive_interval.unwrap(),
1874 Duration::from_secs(40) );
1876 assert_eq!(tostapane_params.tcp_keep_alive.unwrap(), true);
1877 assert_eq!(
1879 tostapane_params.ciphers.algorithms(),
1880 &[
1881 "a-manella",
1882 "blowfish",
1883 "coi-piedi",
1884 "cazdecan",
1885 "triestin-stretto"
1886 ]
1887 );
1888
1889 let microwave_params = config.query("microwave");
1891 assert_eq!(
1892 microwave_params.connect_timeout.unwrap(),
1893 Duration::from_secs(60) );
1895 assert_eq!(
1896 microwave_params.server_alive_interval.unwrap(),
1897 Duration::from_secs(40) );
1899 assert_eq!(
1900 microwave_params.port.unwrap(),
1901 345 );
1903 assert_eq!(microwave_params.tcp_keep_alive.unwrap(), true);
1904 assert_eq!(microwave_params.ciphers.algorithms().is_empty(), true);
1905 assert_eq!(microwave_params.user.as_deref().unwrap(), "mario-rossi");
1906 assert_eq!(
1907 microwave_params.host_name.as_deref().unwrap(),
1908 "192.168.24.33"
1909 );
1910 assert_eq!(microwave_params.remote_forward.unwrap(), 88);
1911 assert_eq!(microwave_params.compression.unwrap(), true);
1912
1913 let fridge_params = config.query("fridge");
1915 assert_eq!(
1916 fridge_params.connect_timeout.unwrap(),
1917 Duration::from_secs(60)
1918 ); assert_eq!(
1920 fridge_params.server_alive_interval.unwrap(),
1921 Duration::from_secs(40)
1922 ); assert_eq!(fridge_params.tcp_keep_alive.unwrap(), true);
1924 assert_eq!(fridge_params.ciphers.algorithms().is_empty(), true);
1925 assert_eq!(fridge_params.user.as_deref().unwrap(), "luigi-verdi");
1926 assert_eq!(fridge_params.host_name.as_deref().unwrap(), "192.168.24.34");
1927 }
1928
1929 #[allow(dead_code)]
1930 struct ConfigWithInclude {
1931 config: NamedTempFile,
1932 inc1: NamedTempFile,
1933 inc2: NamedTempFile,
1934 inc3: NamedTempFile,
1935 inc4: NamedTempFile,
1936 }
1937
1938 fn create_include_config() -> ConfigWithInclude {
1939 let mut config_file: tempfile::NamedTempFile =
1940 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1941 let mut inc1_file: tempfile::NamedTempFile =
1942 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1943 let mut inc2_file: tempfile::NamedTempFile =
1944 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1945 let mut inc3_file: tempfile::NamedTempFile =
1946 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1947 let mut inc4_file: tempfile::NamedTempFile =
1948 tempfile::NamedTempFile::new().expect("Failed to create tempfile");
1949
1950 let config = format!(
1951 r##"
1952# ssh config
1953# written by veeso
1954
1955
1956 # I put a comment here just to annoy
1957
1958IgnoreUnknown Pippo,Pluto
1959
1960Compression yes
1961ConnectionAttempts 10
1962ConnectTimeout 60
1963ServerAliveInterval 40
1964Include {inc1}
1965
1966# Let's start defining some hosts
1967
1968Host tostapane
1969 User ciro-esposito
1970 HostName 192.168.24.32
1971 RemoteForward 88
1972 Compression no
1973 # Ignore unknown fields should be inherited from the global section
1974 Pippo yes
1975 Pluto 56
1976 Include {inc2}
1977
1978Include {inc3}
1979Include {inc4}
1980"##,
1981 inc1 = inc1_file.path().display(),
1982 inc2 = inc2_file.path().display(),
1983 inc3 = inc3_file.path().display(),
1984 inc4 = inc4_file.path().display(),
1985 );
1986 config_file.write_all(config.as_bytes()).unwrap();
1987
1988 let inc1 = r##"
1990 ConnectTimeout 60
1991 ServerAliveInterval 60
1992 TcpKeepAlive yes
1993 "##;
1994 inc1_file.write_all(inc1.as_bytes()).unwrap();
1995
1996 let inc2 = r##"
1998 ConnectTimeout 180
1999 ServerAliveInterval 180
2000 Ciphers +a-manella,blowfish,coi-piedi,cazdecan,triestin-stretto
2001 "##;
2002 inc2_file.write_all(inc2.as_bytes()).unwrap();
2003
2004 let inc3 = r##"
2006Host microwave
2007 User mario-rossi
2008 HostName 192.168.24.33
2009 RemoteForward 88
2010 Compression no
2011 # Ignore unknown fields should be inherited from the global section
2012 Pippo yes
2013 Pluto 56
2014"##;
2015 inc3_file.write_all(inc3.as_bytes()).unwrap();
2016
2017 let inc4 = r##"
2019 # Update microwave
2020 ServerAliveInterval 30
2021 Port 345
2022
2023# Force microwave update (it won't work)
2024Host microwave
2025 ConnectTimeout 30
2026
2027Host fridge
2028 User luigi-verdi
2029 HostName 192.168.24.34
2030 RemoteForward 88
2031 Compression no
2032"##;
2033 inc4_file.write_all(inc4.as_bytes()).unwrap();
2034
2035 ConfigWithInclude {
2036 config: config_file,
2037 inc1: inc1_file,
2038 inc2: inc2_file,
2039 inc3: inc3_file,
2040 inc4: inc4_file,
2041 }
2042 }
2043}