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