1use std::fmt;
2use std::str::FromStr;
3
4use crate::{
5 auth::{AuthRetryMode, AuthType, ParseAuthRetryModeError},
6 client_deny::ClientDeny,
7 kill_target::KillTarget,
8 need_ok::NeedOkResponse,
9 proxy_action::ProxyAction,
10 redacted::Redacted,
11 remote_action::RemoteAction,
12 signal::{ParseSignalError, Signal},
13 status_format::StatusFormat,
14 stream_mode::{ParseStreamModeError, StreamMode},
15 transport_protocol::TransportProtocol,
16};
17use tracing::warn;
18
19fn next_token(input: &str) -> Option<(String, &str)> {
37 let input = input.trim_start();
38 if input.is_empty() {
39 return None;
40 }
41
42 if let Some(quoted) = input.strip_prefix('"') {
43 let mut chars = quoted.chars();
45 let mut token = String::new();
46 let mut closed = false;
47 loop {
48 match chars.next() {
49 None => break,
50 Some('"') => {
51 closed = true;
52 break;
53 }
54 Some('\\') => match chars.next() {
55 Some(escaped) => token.push(escaped),
56 None => break,
57 },
58 Some(plain) => token.push(plain),
59 }
60 }
61 if !closed {
62 return None;
63 }
64 let rest = chars.as_str().trim_start();
65 Some((token, rest))
66 } else {
67 match input.split_once(char::is_whitespace) {
69 Some((tok, rest)) => Some((tok.to_string(), rest.trim_start())),
70 None => Some((input.to_string(), "")),
71 }
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
77pub enum CommandParseError {
78 #[error(transparent)]
80 Signal(#[from] ParseSignalError),
81
82 #[error(transparent)]
84 StreamMode(#[from] ParseStreamModeError),
85
86 #[error(transparent)]
88 AuthRetryMode(#[from] ParseAuthRetryModeError),
89
90 #[error("{field} must be a number, got: {input}")]
92 InvalidNumber {
93 field: &'static str,
95 input: String,
97 },
98
99 #[error("invalid {field}: {input} ({hint})")]
101 InvalidChoice {
102 field: &'static str,
104 input: String,
106 hint: &'static str,
108 },
109
110 #[error("{0}")]
112 MissingArgs(&'static str),
113}
114
115#[derive(Debug, Clone, PartialEq, Eq)]
117pub enum RemoteEntryRange {
118 Single(u32),
120
121 Range {
123 from: u32,
125 end: u32,
127 },
128
129 All,
131}
132
133impl fmt::Display for RemoteEntryRange {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 match self {
136 Self::Single(index) => write!(f, "{index}"),
137 Self::Range { from, end } => write!(f, "{from} {end}"),
138 Self::All => f.write_str("all"),
139 }
140 }
141}
142
143#[derive(Debug, Clone, PartialEq, Eq, strum::IntoStaticStr)]
153#[strum(serialize_all = "kebab-case")]
154pub enum OvpnCommand {
155 Status(StatusFormat),
159
160 State,
163
164 StateStream(StreamMode),
167
168 Version,
171
172 SetVersion(u32),
183
184 Pid,
187
188 Help,
191
192 Verb(Option<u8>),
196
197 Mute(Option<u32>),
200
201 Net,
204
205 Log(StreamMode),
217
218 Echo(StreamMode),
221
222 ByteCount(u32),
226
227 Signal(Signal),
231
232 Kill(KillTarget),
235
236 HoldQuery,
240
241 HoldOn,
244
245 HoldOff,
248
249 HoldRelease,
253
254 Username {
258 auth_type: AuthType,
260 value: Redacted,
262 },
263
264 Password {
268 auth_type: AuthType,
270 value: Redacted,
272 },
273
274 AuthRetry(AuthRetryMode),
277
278 ForgetPasswords,
281
282 ChallengeResponse {
286 state_id: String,
288 response: Redacted,
290 },
291
292 StaticChallengeResponse {
298 password_b64: Redacted,
300 response_b64: Redacted,
302 },
303
304 NeedOk {
308 name: String,
310 response: NeedOkResponse,
312 },
313
314 NeedStr {
317 name: String,
319 value: String,
321 },
322
323 Pkcs11IdCount,
327
328 Pkcs11IdGet(u32),
331
332 RsaSig {
337 base64_lines: Vec<String>,
339 },
340
341 ClientAuth {
347 cid: u64,
349 kid: u64,
351 config_lines: Vec<String>,
353 },
354
355 ClientAuthNt {
358 cid: u64,
360 kid: u64,
362 },
363
364 ClientDeny(ClientDeny),
367
368 ClientKill {
372 cid: u64,
374 message: Option<String>,
377 },
378
379 Remote(RemoteAction),
383
384 Proxy(ProxyAction),
387
388 LoadStats,
393
394 ClientPendingAuth {
398 cid: u64,
400 kid: u64,
402 extra: String,
404 timeout: u32,
406 },
407
408 CrResponse {
411 response: Redacted,
413 },
414
415 PkSig {
420 base64_lines: Vec<String>,
422 },
423
424 EnvFilter(u32),
430
431 RemoteEntryCount,
436
437 RemoteEntryGet(RemoteEntryRange),
441
442 PushUpdateBroad {
446 options: String,
448 },
449
450 PushUpdateCid {
453 cid: u64,
455 options: String,
457 },
458
459 Certificate {
464 pem_lines: Vec<String>,
466 },
467
468 ManagementPassword(Redacted),
474
475 Exit,
479
480 Quit,
482
483 Raw(String),
487
488 RawMultiLine(String),
496}
497
498#[derive(Debug, Clone, Copy, PartialEq, Eq)]
502pub(crate) enum ResponseKind {
503 SuccessOrError,
505
506 MultiLine,
508
509 NoResponse,
511}
512
513impl OvpnCommand {
514 pub(crate) fn expected_response(&self) -> ResponseKind {
517 match self {
518 Self::Status(_)
520 | Self::Version
521 | Self::Help
522 | Self::Net
523 | Self::RemoteEntryCount
524 | Self::RemoteEntryGet(_) => ResponseKind::MultiLine,
525
526 Self::StateStream(mode) | Self::Log(mode) | Self::Echo(mode) => match mode {
528 StreamMode::All | StreamMode::OnAll | StreamMode::Recent(_) => {
529 ResponseKind::MultiLine
530 }
531 StreamMode::On | StreamMode::Off => ResponseKind::SuccessOrError,
532 },
533
534 Self::State => ResponseKind::MultiLine,
536
537 Self::RawMultiLine(_) => ResponseKind::MultiLine,
539
540 Self::Exit | Self::Quit => ResponseKind::NoResponse,
542
543 Self::SetVersion(n) if *n < 4 => ResponseKind::NoResponse,
545
546 _ => ResponseKind::SuccessOrError,
548 }
549 }
550}
551
552impl FromStr for OvpnCommand {
553 type Err = CommandParseError;
554
555 fn from_str(line: &str) -> Result<Self, Self::Err> {
578 fn cmd_err<T>(msg: &'static str) -> Result<T, CommandParseError> {
580 Err(CommandParseError::MissingArgs(msg))
581 }
582
583 let line = line.trim();
584 let (cmd, args) = line
585 .split_once(char::is_whitespace)
586 .map(|(c, a)| (c, a.trim()))
587 .unwrap_or((line, ""));
588
589 match cmd {
590 "version" if args.is_empty() => Ok(Self::Version),
592 "version" => Ok(Self::SetVersion(args.parse().map_err(|_| {
593 CommandParseError::InvalidChoice {
594 field: "version number",
595 input: args.to_string(),
596 hint: "expected a positive integer (e.g. version 2)",
597 }
598 })?)),
599 "pid" => Ok(Self::Pid),
600 "help" => Ok(Self::Help),
601 "net" => Ok(Self::Net),
602 "load-stats" => Ok(Self::LoadStats),
603
604 "status" => match args {
605 "" | "1" => Ok(Self::Status(StatusFormat::V1)),
606 "2" => Ok(Self::Status(StatusFormat::V2)),
607 "3" => Ok(Self::Status(StatusFormat::V3)),
608 _ => Err(CommandParseError::InvalidChoice {
609 field: "status format",
610 input: args.to_string(),
611 hint: "use 1, 2, or 3",
612 }),
613 },
614
615 "state" => match args {
616 "" => Ok(Self::State),
617 other => Ok(Self::StateStream(other.parse::<StreamMode>()?)),
618 },
619
620 "log" => Ok(Self::Log(args.parse::<StreamMode>()?)),
621 "echo" => Ok(Self::Echo(args.parse::<StreamMode>()?)),
622
623 "verb" => {
624 if args.is_empty() {
625 Ok(Self::Verb(None))
626 } else {
627 args.parse::<u8>()
628 .map(|level| Self::Verb(Some(level)))
629 .map_err(|_| CommandParseError::InvalidNumber {
630 field: "verbosity",
631 input: args.to_string(),
632 })
633 }
634 }
635
636 "mute" => {
637 if args.is_empty() {
638 Ok(Self::Mute(None))
639 } else {
640 args.parse::<u32>()
641 .map(|threshold| Self::Mute(Some(threshold)))
642 .map_err(|_| CommandParseError::InvalidNumber {
643 field: "mute threshold",
644 input: args.to_string(),
645 })
646 }
647 }
648
649 "bytecount" => args.parse::<u32>().map(Self::ByteCount).map_err(|_| {
650 CommandParseError::InvalidNumber {
651 field: "bytecount interval",
652 input: args.to_string(),
653 }
654 }),
655
656 "signal" => Ok(Self::Signal(args.parse::<Signal>()?)),
658
659 "kill" => {
660 if args.is_empty() {
661 return cmd_err("kill requires a target (common name or proto:ip:port)");
662 }
663 let parts: Vec<&str> = args.splitn(3, ':').collect();
664 if parts.len() == 3
665 && let Ok(port) = parts[2].parse::<u16>()
666 {
667 return Ok(Self::Kill(KillTarget::Address {
668 protocol: parts[0]
669 .parse()
670 .inspect_err(|error| warn!(%error, "unknown transport protocol"))
671 .unwrap_or_else(|_| TransportProtocol::Unknown(parts[0].to_string())),
672 ip: parts[1].to_string(),
673 port,
674 }));
675 }
676 Ok(Self::Kill(KillTarget::CommonName(args.to_string())))
677 }
678
679 "hold" => match args {
680 "" => Ok(Self::HoldQuery),
681 "on" => Ok(Self::HoldOn),
682 "off" => Ok(Self::HoldOff),
683 "release" => Ok(Self::HoldRelease),
684 _ => Err(CommandParseError::InvalidChoice {
685 field: "hold argument",
686 input: args.to_string(),
687 hint: "use on, off, or release",
688 }),
689 },
690
691 "username" => {
702 let (auth_type_str, rest) = next_token(args).ok_or(
703 CommandParseError::MissingArgs("usage: username <auth-type> <value>"),
704 )?;
705 let (value, _) = next_token(rest).ok_or(CommandParseError::MissingArgs(
706 "usage: username <auth-type> <value>",
707 ))?;
708 Ok(Self::Username {
709 auth_type: auth_type_str
710 .parse()
711 .inspect_err(|error| warn!(%error, "unknown auth type"))
712 .unwrap_or(AuthType::Unknown(auth_type_str)),
713 value: value.into(),
714 })
715 }
716
717 "password" => {
718 let (auth_type_str, rest) = next_token(args).ok_or(
719 CommandParseError::MissingArgs("usage: password <auth-type> <value>"),
720 )?;
721 let (value, _) = next_token(rest).ok_or(CommandParseError::MissingArgs(
722 "usage: password <auth-type> <value>",
723 ))?;
724 Ok(Self::Password {
725 auth_type: auth_type_str
726 .parse()
727 .inspect_err(|error| warn!(%error, "unknown auth type"))
728 .unwrap_or(AuthType::Unknown(auth_type_str)),
729 value: value.into(),
730 })
731 }
732
733 "auth-retry" => Ok(Self::AuthRetry(args.parse::<AuthRetryMode>()?)),
734
735 "forget-passwords" => Ok(Self::ForgetPasswords),
736
737 "needok" => {
739 let (name, resp) =
740 args.rsplit_once(char::is_whitespace)
741 .ok_or(CommandParseError::MissingArgs(
742 "usage: needok <name> ok|cancel",
743 ))?;
744 let response = match resp {
745 "ok" => NeedOkResponse::Ok,
746 "cancel" => NeedOkResponse::Cancel,
747 _ => {
748 return Err(CommandParseError::InvalidChoice {
749 field: "needok response",
750 input: resp.to_string(),
751 hint: "use ok or cancel",
752 });
753 }
754 };
755 Ok(Self::NeedOk {
756 name: name.trim().to_string(),
757 response,
758 })
759 }
760
761 "needstr" => {
766 let (name, rest) = next_token(args).ok_or(CommandParseError::MissingArgs(
767 "usage: needstr <name> <value>",
768 ))?;
769 let (value, _) = next_token(rest).ok_or(CommandParseError::MissingArgs(
770 "usage: needstr <name> <value>",
771 ))?;
772 Ok(Self::NeedStr { name, value })
773 }
774
775 "pkcs11-id-count" => Ok(Self::Pkcs11IdCount),
777
778 "pkcs11-id-get" => args.parse::<u32>().map(Self::Pkcs11IdGet).map_err(|_| {
779 CommandParseError::InvalidNumber {
780 field: "pkcs11-id-get index",
781 input: args.to_string(),
782 }
783 }),
784
785 "client-auth" => {
787 let mut parts = args.splitn(3, char::is_whitespace);
788 let cid = parts
789 .next()
790 .ok_or(CommandParseError::MissingArgs(
791 "usage: client-auth <cid> <kid> [config-lines]",
792 ))?
793 .parse::<u64>()
794 .map_err(|_| CommandParseError::MissingArgs("cid must be a number"))?;
795 let kid = parts
796 .next()
797 .ok_or(CommandParseError::MissingArgs(
798 "usage: client-auth <cid> <kid> [config-lines]",
799 ))?
800 .parse::<u64>()
801 .map_err(|_| CommandParseError::MissingArgs("kid must be a number"))?;
802 let config_lines = match parts.next() {
803 Some(rest) => rest
804 .split(',')
805 .map(|line| line.trim().to_string())
806 .collect(),
807 None => vec![],
808 };
809 Ok(Self::ClientAuth {
810 cid,
811 kid,
812 config_lines,
813 })
814 }
815
816 "client-auth-nt" => {
817 let (cid_s, kid_s) =
818 args.split_once(char::is_whitespace)
819 .ok_or(CommandParseError::MissingArgs(
820 "usage: client-auth-nt <cid> <kid>",
821 ))?;
822 Ok(Self::ClientAuthNt {
823 cid: cid_s
824 .parse()
825 .map_err(|_| CommandParseError::MissingArgs("cid must be a number"))?,
826 kid: kid_s
827 .trim()
828 .parse()
829 .map_err(|_| CommandParseError::MissingArgs("kid must be a number"))?,
830 })
831 }
832
833 "client-deny" => {
838 let (cid_str, rest) = next_token(args).ok_or(CommandParseError::MissingArgs(
839 "usage: client-deny <cid> <kid> <reason> [client-reason]",
840 ))?;
841 let cid = cid_str
842 .parse::<u64>()
843 .map_err(|_| CommandParseError::MissingArgs("cid must be a number"))?;
844 let (kid_str, rest) = next_token(rest).ok_or(CommandParseError::MissingArgs(
845 "usage: client-deny <cid> <kid> <reason> [client-reason]",
846 ))?;
847 let kid = kid_str
848 .parse::<u64>()
849 .map_err(|_| CommandParseError::MissingArgs("kid must be a number"))?;
850 let (reason, rest) = next_token(rest).ok_or(CommandParseError::MissingArgs(
851 "usage: client-deny <cid> <kid> <reason> [client-reason]",
852 ))?;
853 let client_reason = next_token(rest).map(|(cr, _)| cr);
854 Ok(Self::ClientDeny(ClientDeny {
855 cid,
856 kid,
857 reason,
858 client_reason,
859 }))
860 }
861
862 "client-kill" => {
863 let (cid_str, message) = match args.split_once(char::is_whitespace) {
864 Some((c, m)) => (c, Some(m.trim().to_string())),
865 None => (args, None),
866 };
867 let cid = cid_str
868 .parse::<u64>()
869 .map_err(|_| CommandParseError::InvalidNumber {
870 field: "client-kill CID",
871 input: cid_str.to_string(),
872 })?;
873 Ok(Self::ClientKill { cid, message })
874 }
875
876 "remote" => match args.split_whitespace().collect::<Vec<_>>().as_slice() {
878 ["accept" | "ACCEPT"] => Ok(Self::Remote(RemoteAction::Accept)),
879 ["skip" | "SKIP"] => Ok(Self::Remote(RemoteAction::Skip)),
880 ["skip" | "SKIP", n] => Ok(Self::Remote(RemoteAction::SkipN(n.parse().map_err(
881 |_| CommandParseError::InvalidChoice {
882 field: "remote skip count",
883 input: n.to_string(),
884 hint: "expected a positive integer (e.g. remote SKIP 3)",
885 },
886 )?))),
887 ["mod" | "MOD", host, port] => Ok(Self::Remote(RemoteAction::Modify {
888 host: host.to_string(),
889 port: port
890 .parse()
891 .map_err(|_| CommandParseError::MissingArgs("port must be a number"))?,
892 })),
893 _ => cmd_err("usage: remote accept|skip [n]|mod <host> <port>"),
894 },
895
896 "proxy" => match args.split_whitespace().collect::<Vec<_>>().as_slice() {
897 ["none" | "NONE"] => Ok(Self::Proxy(ProxyAction::None)),
898 ["http" | "HTTP", host, port] => Ok(Self::Proxy(ProxyAction::Http {
899 host: host.to_string(),
900 port: port
901 .parse()
902 .map_err(|_| CommandParseError::MissingArgs("port must be a number"))?,
903 non_cleartext_only: false,
904 })),
905 ["http" | "HTTP", host, port, "nct"] => Ok(Self::Proxy(ProxyAction::Http {
906 host: host.to_string(),
907 port: port
908 .parse()
909 .map_err(|_| CommandParseError::MissingArgs("port must be a number"))?,
910 non_cleartext_only: true,
911 })),
912 ["socks" | "SOCKS", host, port] => Ok(Self::Proxy(ProxyAction::Socks {
913 host: host.to_string(),
914 port: port
915 .parse()
916 .map_err(|_| CommandParseError::MissingArgs("port must be a number"))?,
917 })),
918 _ => cmd_err("usage: proxy none|http <host> <port> [nct]|socks <host> <port>"),
919 },
920
921 "env-filter" => {
923 let level = if args.is_empty() {
924 0
925 } else {
926 args.parse::<u32>()
927 .map_err(|_| CommandParseError::InvalidNumber {
928 field: "env-filter level",
929 input: args.to_string(),
930 })?
931 };
932 Ok(Self::EnvFilter(level))
933 }
934
935 "remote-entry-count" => Ok(Self::RemoteEntryCount),
937
938 "remote-entry-get" => {
939 if args.is_empty() {
940 return cmd_err("usage: remote-entry-get i|all [j]");
941 }
942 let range = if args == "all" {
943 RemoteEntryRange::All
944 } else {
945 let mut parts = args.splitn(2, char::is_whitespace);
946 let from = parts.next().unwrap().parse::<u32>().map_err(|_| {
947 CommandParseError::InvalidNumber {
948 field: "remote-entry-get index",
949 input: args.to_string(),
950 }
951 })?;
952 match parts.next() {
953 Some(to_str) => {
954 let end = to_str.trim().parse::<u32>().map_err(|_| {
955 CommandParseError::InvalidNumber {
956 field: "remote-entry-get end index",
957 input: to_str.to_string(),
958 }
959 })?;
960 RemoteEntryRange::Range { from, end }
961 }
962 None => RemoteEntryRange::Single(from),
963 }
964 };
965 Ok(Self::RemoteEntryGet(range))
966 }
967
968 "push-update-broad" => {
975 let (options, _) = next_token(args).ok_or(CommandParseError::MissingArgs(
976 "usage: push-update-broad <options>",
977 ))?;
978 Ok(Self::PushUpdateBroad { options })
979 }
980
981 "push-update-cid" => {
982 let (cid_str, rest) = next_token(args).ok_or(CommandParseError::MissingArgs(
983 "usage: push-update-cid <cid> <options>",
984 ))?;
985 let cid = cid_str.parse::<u64>().map_err(|_| {
986 CommandParseError::MissingArgs("push-update-cid: cid must be a number")
987 })?;
988 let (options, _) = next_token(rest).ok_or(CommandParseError::MissingArgs(
989 "usage: push-update-cid <cid> <options>",
990 ))?;
991 Ok(Self::PushUpdateCid { cid, options })
992 }
993
994 "client-pending-auth" => {
999 let (cid_str, rest) = next_token(args).ok_or(CommandParseError::MissingArgs(
1000 "usage: client-pending-auth <cid> <kid> <extra> <timeout>",
1001 ))?;
1002 let cid = cid_str
1003 .parse::<u64>()
1004 .map_err(|_| CommandParseError::MissingArgs("cid must be a number"))?;
1005 let (kid_str, rest) = next_token(rest).ok_or(CommandParseError::MissingArgs(
1006 "usage: client-pending-auth <cid> <kid> <extra> <timeout>",
1007 ))?;
1008 let kid = kid_str
1009 .parse::<u64>()
1010 .map_err(|_| CommandParseError::MissingArgs("kid must be a number"))?;
1011 let (extra, rest) = next_token(rest).ok_or(CommandParseError::MissingArgs(
1012 "usage: client-pending-auth <cid> <kid> <extra> <timeout>",
1013 ))?;
1014 let (timeout_str, _) = next_token(rest).ok_or(CommandParseError::MissingArgs(
1015 "usage: client-pending-auth <cid> <kid> <extra> <timeout>",
1016 ))?;
1017 let timeout =
1018 timeout_str
1019 .parse::<u32>()
1020 .map_err(|_| CommandParseError::InvalidNumber {
1021 field: "client-pending-auth timeout",
1022 input: timeout_str,
1023 })?;
1024 Ok(Self::ClientPendingAuth {
1025 cid,
1026 kid,
1027 extra,
1028 timeout,
1029 })
1030 }
1031
1032 "cr-response" => {
1036 let (response, _) = next_token(args).ok_or(CommandParseError::MissingArgs(
1037 "usage: cr-response <response>",
1038 ))?;
1039 Ok(Self::CrResponse {
1040 response: Redacted::new(response),
1041 })
1042 }
1043
1044 "raw-ml" => {
1046 if args.is_empty() {
1047 return cmd_err("usage: raw-ml <command>");
1048 }
1049 Ok(Self::RawMultiLine(args.to_string()))
1050 }
1051
1052 "exit" => Ok(Self::Exit),
1054 "quit" => Ok(Self::Quit),
1055
1056 _ => Ok(Self::Raw(line.to_string())),
1058 }
1059 }
1060}
1061
1062pub fn connection_sequence(bytecount_interval: u32) -> Vec<OvpnCommand> {
1130 let mut cmds = vec![
1131 OvpnCommand::Log(StreamMode::OnAll),
1132 OvpnCommand::StateStream(StreamMode::OnAll),
1133 OvpnCommand::Pid,
1134 ];
1135 if bytecount_interval > 0 {
1136 cmds.push(OvpnCommand::ByteCount(bytecount_interval));
1137 }
1138 cmds.push(OvpnCommand::HoldRelease);
1139 cmds
1140}
1141
1142pub fn server_connection_sequence(bytecount_interval: u32, env_filter: u32) -> Vec<OvpnCommand> {
1171 let mut cmds = vec![
1172 OvpnCommand::Log(StreamMode::OnAll),
1173 OvpnCommand::StateStream(StreamMode::OnAll),
1174 OvpnCommand::Pid,
1175 OvpnCommand::EnvFilter(env_filter),
1176 ];
1177 if bytecount_interval > 0 {
1178 cmds.push(OvpnCommand::ByteCount(bytecount_interval));
1179 }
1180 cmds.push(OvpnCommand::HoldRelease);
1181 cmds
1182}
1183
1184#[cfg(test)]
1185mod tests {
1186 use super::*;
1187
1188 #[test]
1189 fn into_static_str_labels() {
1190 let label: &str = (&OvpnCommand::State).into();
1191 assert_eq!(label, "state");
1192
1193 let label: &str = (&OvpnCommand::ForgetPasswords).into();
1194 assert_eq!(label, "forget-passwords");
1195
1196 let label: &str = (&OvpnCommand::ByteCount(5)).into();
1197 assert_eq!(label, "byte-count");
1198 }
1199
1200 #[test]
1203 fn connection_sequence_with_bytecount() {
1204 let cmds = connection_sequence(5);
1205 assert_eq!(
1206 cmds,
1207 vec![
1208 OvpnCommand::Log(StreamMode::OnAll),
1209 OvpnCommand::StateStream(StreamMode::OnAll),
1210 OvpnCommand::Pid,
1211 OvpnCommand::ByteCount(5),
1212 OvpnCommand::HoldRelease,
1213 ]
1214 );
1215 }
1216
1217 #[test]
1218 fn connection_sequence_without_bytecount() {
1219 let cmds = connection_sequence(0);
1220 assert_eq!(
1221 cmds,
1222 vec![
1223 OvpnCommand::Log(StreamMode::OnAll),
1224 OvpnCommand::StateStream(StreamMode::OnAll),
1225 OvpnCommand::Pid,
1226 OvpnCommand::HoldRelease,
1227 ]
1228 );
1229 }
1230
1231 #[test]
1234 fn parse_simple_commands() {
1235 assert_eq!("version".parse(), Ok(OvpnCommand::Version));
1236 assert_eq!("pid".parse(), Ok(OvpnCommand::Pid));
1237 assert_eq!("help".parse(), Ok(OvpnCommand::Help));
1238 assert_eq!("net".parse(), Ok(OvpnCommand::Net));
1239 assert_eq!("load-stats".parse(), Ok(OvpnCommand::LoadStats));
1240 assert_eq!("forget-passwords".parse(), Ok(OvpnCommand::ForgetPasswords));
1241 assert_eq!("pkcs11-id-count".parse(), Ok(OvpnCommand::Pkcs11IdCount));
1242 assert_eq!("exit".parse(), Ok(OvpnCommand::Exit));
1243 assert_eq!("quit".parse(), Ok(OvpnCommand::Quit));
1244 }
1245
1246 #[test]
1247 fn parse_status() {
1248 assert_eq!("status".parse(), Ok(OvpnCommand::Status(StatusFormat::V1)));
1249 assert_eq!(
1250 "status 1".parse(),
1251 Ok(OvpnCommand::Status(StatusFormat::V1))
1252 );
1253 assert_eq!(
1254 "status 2".parse(),
1255 Ok(OvpnCommand::Status(StatusFormat::V2))
1256 );
1257 assert_eq!(
1258 "status 3".parse(),
1259 Ok(OvpnCommand::Status(StatusFormat::V3))
1260 );
1261 assert!("status 4".parse::<OvpnCommand>().is_err());
1262 }
1263
1264 #[test]
1267 fn parse_state_bare() {
1268 assert_eq!("state".parse(), Ok(OvpnCommand::State));
1269 }
1270
1271 #[test]
1272 fn parse_state_stream_modes() {
1273 assert_eq!(
1274 "state on".parse(),
1275 Ok(OvpnCommand::StateStream(StreamMode::On))
1276 );
1277 assert_eq!(
1278 "state off".parse(),
1279 Ok(OvpnCommand::StateStream(StreamMode::Off))
1280 );
1281 assert_eq!(
1282 "state all".parse(),
1283 Ok(OvpnCommand::StateStream(StreamMode::All))
1284 );
1285 assert_eq!(
1286 "state on all".parse(),
1287 Ok(OvpnCommand::StateStream(StreamMode::OnAll))
1288 );
1289 assert_eq!(
1290 "state 5".parse(),
1291 Ok(OvpnCommand::StateStream(StreamMode::Recent(5)))
1292 );
1293 }
1294
1295 #[test]
1296 fn parse_log_and_echo() {
1297 assert_eq!("log on".parse(), Ok(OvpnCommand::Log(StreamMode::On)));
1298 assert_eq!(
1299 "log on all".parse(),
1300 Ok(OvpnCommand::Log(StreamMode::OnAll))
1301 );
1302 assert_eq!("echo off".parse(), Ok(OvpnCommand::Echo(StreamMode::Off)));
1303 assert_eq!(
1304 "echo 10".parse(),
1305 Ok(OvpnCommand::Echo(StreamMode::Recent(10)))
1306 );
1307 }
1308
1309 #[test]
1312 fn parse_verb() {
1313 assert_eq!("verb".parse(), Ok(OvpnCommand::Verb(None)));
1314 assert_eq!("verb 4".parse(), Ok(OvpnCommand::Verb(Some(4))));
1315 assert!("verb abc".parse::<OvpnCommand>().is_err());
1316 }
1317
1318 #[test]
1319 fn parse_mute() {
1320 assert_eq!("mute".parse(), Ok(OvpnCommand::Mute(None)));
1321 assert_eq!("mute 40".parse(), Ok(OvpnCommand::Mute(Some(40))));
1322 assert!("mute abc".parse::<OvpnCommand>().is_err());
1323 }
1324
1325 #[test]
1326 fn parse_bytecount() {
1327 assert_eq!("bytecount 5".parse(), Ok(OvpnCommand::ByteCount(5)));
1328 assert_eq!("bytecount 0".parse(), Ok(OvpnCommand::ByteCount(0)));
1329 assert!("bytecount".parse::<OvpnCommand>().is_err());
1330 }
1331
1332 #[test]
1335 fn parse_signal() {
1336 assert_eq!(
1337 "signal SIGHUP".parse(),
1338 Ok(OvpnCommand::Signal(Signal::SigHup))
1339 );
1340 assert_eq!(
1341 "signal SIGTERM".parse(),
1342 Ok(OvpnCommand::Signal(Signal::SigTerm))
1343 );
1344 assert_eq!(
1345 "signal SIGUSR1".parse(),
1346 Ok(OvpnCommand::Signal(Signal::SigUsr1))
1347 );
1348 assert_eq!(
1349 "signal SIGUSR2".parse(),
1350 Ok(OvpnCommand::Signal(Signal::SigUsr2))
1351 );
1352 assert!("signal SIGKILL".parse::<OvpnCommand>().is_err());
1353 }
1354
1355 #[test]
1358 fn parse_kill_common_name() {
1359 assert_eq!(
1360 "kill TestClient".parse(),
1361 Ok(OvpnCommand::Kill(KillTarget::CommonName(
1362 "TestClient".to_string()
1363 )))
1364 );
1365 }
1366
1367 #[test]
1368 fn parse_kill_address() {
1369 assert_eq!(
1370 "kill tcp:1.2.3.4:4000".parse(),
1371 Ok(OvpnCommand::Kill(KillTarget::Address {
1372 protocol: TransportProtocol::Tcp,
1373 ip: "1.2.3.4".to_string(),
1374 port: 4000,
1375 }))
1376 );
1377 }
1378
1379 #[test]
1380 fn parse_kill_empty_is_err() {
1381 assert!("kill".parse::<OvpnCommand>().is_err());
1382 }
1383
1384 #[test]
1387 fn parse_hold() {
1388 assert_eq!("hold".parse(), Ok(OvpnCommand::HoldQuery));
1389 assert_eq!("hold on".parse(), Ok(OvpnCommand::HoldOn));
1390 assert_eq!("hold off".parse(), Ok(OvpnCommand::HoldOff));
1391 assert_eq!("hold release".parse(), Ok(OvpnCommand::HoldRelease));
1392 assert!("hold bogus".parse::<OvpnCommand>().is_err());
1393 }
1394
1395 #[test]
1398 fn next_token_unquoted() {
1399 let (tok, rest) = next_token("Auth s3cret").unwrap();
1400 assert_eq!(tok, "Auth");
1401 assert_eq!(rest, "s3cret");
1402 }
1403
1404 #[test]
1405 fn next_token_quoted_simple() {
1406 let (tok, rest) = next_token(r#""Private Key" "s3cret""#).unwrap();
1407 assert_eq!(tok, "Private Key");
1408 assert_eq!(rest, r#""s3cret""#);
1409 }
1410
1411 #[test]
1412 fn next_token_quoted_with_escapes() {
1413 let (tok, _) = next_token(r#""foo\\\"bar""#).unwrap();
1415 assert_eq!(tok, r#"foo\"bar"#);
1416 }
1417
1418 #[test]
1419 fn next_token_empty() {
1420 assert!(next_token("").is_none());
1421 assert!(next_token(" ").is_none());
1422 }
1423
1424 #[test]
1425 fn next_token_last_token_no_trailing() {
1426 let (tok, rest) = next_token("onlyone").unwrap();
1427 assert_eq!(tok, "onlyone");
1428 assert_eq!(rest, "");
1429 }
1430
1431 #[test]
1432 fn next_token_unclosed_quote_returns_none() {
1433 assert!(next_token(r#""unclosed string"#).is_none());
1434 assert!(next_token(r#""trailing backslash\"#).is_none());
1435 }
1436
1437 #[test]
1440 fn parse_username() {
1441 let cmd: OvpnCommand = "username Auth alice".parse().unwrap();
1442 assert_eq!(
1443 cmd,
1444 OvpnCommand::Username {
1445 auth_type: AuthType::Auth,
1446 value: "alice".into(),
1447 }
1448 );
1449 }
1450
1451 #[test]
1452 fn parse_password() {
1453 let cmd: OvpnCommand = "password Auth s3cret".parse().unwrap();
1454 assert_eq!(
1455 cmd,
1456 OvpnCommand::Password {
1457 auth_type: AuthType::Auth,
1458 value: "s3cret".into(),
1459 }
1460 );
1461 }
1462
1463 #[test]
1464 fn parse_username_missing_value_is_err() {
1465 assert!("username".parse::<OvpnCommand>().is_err());
1466 assert!("username Auth".parse::<OvpnCommand>().is_err());
1467 }
1468
1469 #[test]
1470 fn parse_auth_retry() {
1471 assert_eq!(
1472 "auth-retry none".parse(),
1473 Ok(OvpnCommand::AuthRetry(AuthRetryMode::None))
1474 );
1475 assert_eq!(
1476 "auth-retry interact".parse(),
1477 Ok(OvpnCommand::AuthRetry(AuthRetryMode::Interact))
1478 );
1479 assert_eq!(
1480 "auth-retry nointeract".parse(),
1481 Ok(OvpnCommand::AuthRetry(AuthRetryMode::NoInteract))
1482 );
1483 assert!("auth-retry bogus".parse::<OvpnCommand>().is_err());
1484 }
1485
1486 #[test]
1489 fn parse_needok() {
1490 assert_eq!(
1491 "needok token-insertion ok".parse(),
1492 Ok(OvpnCommand::NeedOk {
1493 name: "token-insertion".to_string(),
1494 response: NeedOkResponse::Ok,
1495 })
1496 );
1497 assert_eq!(
1498 "needok token-insertion cancel".parse(),
1499 Ok(OvpnCommand::NeedOk {
1500 name: "token-insertion".to_string(),
1501 response: NeedOkResponse::Cancel,
1502 })
1503 );
1504 assert!("needok".parse::<OvpnCommand>().is_err());
1505 assert!("needok name bogus".parse::<OvpnCommand>().is_err());
1506 }
1507
1508 #[test]
1509 fn parse_needstr() {
1510 assert_eq!(
1511 "needstr prompt-name John".parse(),
1512 Ok(OvpnCommand::NeedStr {
1513 name: "prompt-name".to_string(),
1514 value: "John".to_string(),
1515 })
1516 );
1517 assert!("needstr".parse::<OvpnCommand>().is_err());
1518 }
1519
1520 #[test]
1523 fn parse_pkcs11_id_get() {
1524 assert_eq!("pkcs11-id-get 1".parse(), Ok(OvpnCommand::Pkcs11IdGet(1)));
1525 assert!("pkcs11-id-get abc".parse::<OvpnCommand>().is_err());
1526 }
1527
1528 #[test]
1531 fn parse_client_auth() {
1532 assert_eq!(
1533 "client-auth 42 7".parse(),
1534 Ok(OvpnCommand::ClientAuth {
1535 cid: 42,
1536 kid: 7,
1537 config_lines: vec![],
1538 })
1539 );
1540 }
1541
1542 #[test]
1543 fn parse_client_auth_with_config() {
1544 let cmd: OvpnCommand = "client-auth 1 2 push route 10.0.0.0,ifconfig-push 10.0.1.1"
1545 .parse()
1546 .unwrap();
1547 assert_eq!(
1548 cmd,
1549 OvpnCommand::ClientAuth {
1550 cid: 1,
1551 kid: 2,
1552 config_lines: vec![
1553 "push route 10.0.0.0".to_string(),
1554 "ifconfig-push 10.0.1.1".to_string(),
1555 ],
1556 }
1557 );
1558 }
1559
1560 #[test]
1561 fn parse_client_auth_nt() {
1562 assert_eq!(
1563 "client-auth-nt 5 3".parse(),
1564 Ok(OvpnCommand::ClientAuthNt { cid: 5, kid: 3 })
1565 );
1566 assert!("client-auth-nt abc 3".parse::<OvpnCommand>().is_err());
1567 }
1568
1569 #[test]
1570 fn parse_client_deny() {
1571 assert_eq!(
1572 "client-deny 1 2 rejected".parse(),
1573 Ok(OvpnCommand::ClientDeny(ClientDeny {
1574 cid: 1,
1575 kid: 2,
1576 reason: "rejected".to_string(),
1577 client_reason: None,
1578 }))
1579 );
1580 assert_eq!(
1581 "client-deny 1 2 rejected sorry".parse(),
1582 Ok(OvpnCommand::ClientDeny(ClientDeny {
1583 cid: 1,
1584 kid: 2,
1585 reason: "rejected".to_string(),
1586 client_reason: Some("sorry".to_string()),
1587 }))
1588 );
1589 }
1590
1591 #[test]
1592 fn parse_client_kill() {
1593 assert_eq!(
1594 "client-kill 99".parse(),
1595 Ok(OvpnCommand::ClientKill {
1596 cid: 99,
1597 message: None,
1598 })
1599 );
1600 assert_eq!(
1601 "client-kill 99 HALT".parse(),
1602 Ok(OvpnCommand::ClientKill {
1603 cid: 99,
1604 message: Some("HALT".to_string()),
1605 })
1606 );
1607 assert!("client-kill abc".parse::<OvpnCommand>().is_err());
1608 }
1609
1610 #[test]
1613 fn parse_remote() {
1614 assert_eq!(
1615 "remote accept".parse(),
1616 Ok(OvpnCommand::Remote(RemoteAction::Accept))
1617 );
1618 assert_eq!(
1619 "remote SKIP".parse(),
1620 Ok(OvpnCommand::Remote(RemoteAction::Skip))
1621 );
1622 assert_eq!(
1623 "remote MOD example.com 443".parse(),
1624 Ok(OvpnCommand::Remote(RemoteAction::Modify {
1625 host: "example.com".to_string(),
1626 port: 443,
1627 }))
1628 );
1629 assert!("remote".parse::<OvpnCommand>().is_err());
1630 }
1631
1632 #[test]
1633 fn parse_proxy() {
1634 assert_eq!(
1635 "proxy none".parse(),
1636 Ok(OvpnCommand::Proxy(ProxyAction::None))
1637 );
1638 assert_eq!(
1639 "proxy HTTP proxy.local 8080".parse(),
1640 Ok(OvpnCommand::Proxy(ProxyAction::Http {
1641 host: "proxy.local".to_string(),
1642 port: 8080,
1643 non_cleartext_only: false,
1644 }))
1645 );
1646 assert_eq!(
1647 "proxy http proxy.local 8080 nct".parse(),
1648 Ok(OvpnCommand::Proxy(ProxyAction::Http {
1649 host: "proxy.local".to_string(),
1650 port: 8080,
1651 non_cleartext_only: true,
1652 }))
1653 );
1654 assert_eq!(
1655 "proxy socks socks.local 1080".parse(),
1656 Ok(OvpnCommand::Proxy(ProxyAction::Socks {
1657 host: "socks.local".to_string(),
1658 port: 1080,
1659 }))
1660 );
1661 assert!("proxy".parse::<OvpnCommand>().is_err());
1662 }
1663
1664 #[test]
1667 fn parse_raw_ml() {
1668 assert_eq!(
1669 "raw-ml some-cmd".parse(),
1670 Ok(OvpnCommand::RawMultiLine("some-cmd".to_string()))
1671 );
1672 assert!("raw-ml".parse::<OvpnCommand>().is_err());
1673 }
1674
1675 #[test]
1676 fn parse_unrecognized_falls_through_to_raw() {
1677 assert_eq!(
1678 "unknown-cmd foo bar".parse(),
1679 Ok(OvpnCommand::Raw("unknown-cmd foo bar".to_string()))
1680 );
1681 }
1682
1683 #[test]
1684 fn parse_trims_whitespace() {
1685 assert_eq!(" version ".parse(), Ok(OvpnCommand::Version));
1686 assert_eq!(
1687 " state on ".parse(),
1688 Ok(OvpnCommand::StateStream(StreamMode::On))
1689 );
1690 }
1691
1692 #[test]
1695 fn parse_state_invalid_stream_mode() {
1696 assert!("state bogus".parse::<OvpnCommand>().is_err());
1697 }
1698
1699 #[test]
1700 fn parse_log_invalid_stream_mode() {
1701 assert!("log bogus".parse::<OvpnCommand>().is_err());
1702 }
1703
1704 #[test]
1705 fn parse_echo_invalid_stream_mode() {
1706 assert!("echo bogus".parse::<OvpnCommand>().is_err());
1707 }
1708
1709 #[test]
1710 fn parse_kill_unknown_protocol_falls_back() {
1711 let cmd: OvpnCommand = "kill sctp:1.2.3.4:4000".parse().unwrap();
1712 assert_eq!(
1713 cmd,
1714 OvpnCommand::Kill(KillTarget::Address {
1715 protocol: TransportProtocol::Unknown("sctp".to_string()),
1716 ip: "1.2.3.4".to_string(),
1717 port: 4000,
1718 })
1719 );
1720 }
1721
1722 #[test]
1723 fn parse_username_unknown_auth_type_falls_back() {
1724 let cmd: OvpnCommand = "username MyPlugin alice".parse().unwrap();
1725 assert_eq!(
1726 cmd,
1727 OvpnCommand::Username {
1728 auth_type: AuthType::Unknown("MyPlugin".to_string()),
1729 value: "alice".into(),
1730 }
1731 );
1732 }
1733
1734 #[test]
1735 fn parse_password_unknown_auth_type_falls_back() {
1736 let cmd: OvpnCommand = "password MyPlugin s3cret".parse().unwrap();
1737 assert_eq!(
1738 cmd,
1739 OvpnCommand::Password {
1740 auth_type: AuthType::Unknown("MyPlugin".to_string()),
1741 value: "s3cret".into(),
1742 }
1743 );
1744 }
1745
1746 #[test]
1747 fn parse_password_missing_value_is_err() {
1748 assert!("password".parse::<OvpnCommand>().is_err());
1749 assert!("password Auth".parse::<OvpnCommand>().is_err());
1750 }
1751
1752 #[test]
1763 fn parse_password_quoted_spaced_auth_type() {
1764 let cmd: OvpnCommand = r#"password "Private Key" "s3cret""#.parse().unwrap();
1765 assert_eq!(
1766 cmd,
1767 OvpnCommand::Password {
1768 auth_type: AuthType::PrivateKey,
1769 value: "s3cret".into(),
1770 }
1771 );
1772 }
1773
1774 #[test]
1775 fn parse_password_quoted_http_proxy() {
1776 let cmd: OvpnCommand = r#"password "HTTP Proxy" "proxypass""#.parse().unwrap();
1777 assert_eq!(
1778 cmd,
1779 OvpnCommand::Password {
1780 auth_type: AuthType::HttpProxy,
1781 value: "proxypass".into(),
1782 }
1783 );
1784 }
1785
1786 #[test]
1787 fn parse_password_quoted_socks_proxy() {
1788 let cmd: OvpnCommand = r#"password "SOCKS Proxy" "sockspass""#.parse().unwrap();
1789 assert_eq!(
1790 cmd,
1791 OvpnCommand::Password {
1792 auth_type: AuthType::SocksProxy,
1793 value: "sockspass".into(),
1794 }
1795 );
1796 }
1797
1798 #[test]
1799 fn parse_username_quoted_spaced_auth_type() {
1800 let cmd: OvpnCommand = r#"username "HTTP Proxy" "proxyuser""#.parse().unwrap();
1801 assert_eq!(
1802 cmd,
1803 OvpnCommand::Username {
1804 auth_type: AuthType::HttpProxy,
1805 value: "proxyuser".into(),
1806 }
1807 );
1808 }
1809
1810 #[test]
1811 fn parse_password_roundtrip_spaced_auth_types() {
1812 use crate::OvpnCodec;
1815 use bytes::BytesMut;
1816 use tokio_util::codec::Encoder;
1817
1818 for auth_type in [
1819 AuthType::PrivateKey,
1820 AuthType::HttpProxy,
1821 AuthType::SocksProxy,
1822 ] {
1823 let original = OvpnCommand::Password {
1824 auth_type: auth_type.clone(),
1825 value: "test".into(),
1826 };
1827 let mut codec = OvpnCodec::new();
1828 let mut buf = BytesMut::new();
1829 codec.encode(original.clone(), &mut buf).unwrap();
1830 let wire = String::from_utf8(buf.to_vec()).unwrap();
1831 let parsed: OvpnCommand = wire.trim().parse().unwrap();
1832 assert_eq!(parsed, original);
1833 }
1834 }
1835
1836 #[test]
1837 fn parse_client_auth_non_numeric_cid() {
1838 assert!("client-auth abc 1".parse::<OvpnCommand>().is_err());
1839 }
1840
1841 #[test]
1842 fn parse_client_auth_non_numeric_kid() {
1843 assert!("client-auth 1 abc".parse::<OvpnCommand>().is_err());
1844 }
1845
1846 #[test]
1847 fn parse_client_auth_nt_non_numeric_kid() {
1848 assert!("client-auth-nt 1 abc".parse::<OvpnCommand>().is_err());
1849 }
1850
1851 #[test]
1852 fn parse_client_deny_missing_args() {
1853 assert!("client-deny".parse::<OvpnCommand>().is_err());
1854 assert!("client-deny 1".parse::<OvpnCommand>().is_err());
1855 assert!("client-deny 1 2".parse::<OvpnCommand>().is_err());
1856 }
1857
1858 #[test]
1859 fn parse_client_deny_non_numeric_ids() {
1860 assert!("client-deny abc 1 reason".parse::<OvpnCommand>().is_err());
1861 assert!("client-deny 1 abc reason".parse::<OvpnCommand>().is_err());
1862 }
1863
1864 #[test]
1865 fn parse_remote_non_numeric_port() {
1866 assert!("remote mod host abc".parse::<OvpnCommand>().is_err());
1867 }
1868
1869 #[test]
1870 fn parse_proxy_non_numeric_port() {
1871 assert!("proxy http host abc".parse::<OvpnCommand>().is_err());
1872 assert!("proxy http host abc nct".parse::<OvpnCommand>().is_err());
1873 assert!("proxy socks host abc".parse::<OvpnCommand>().is_err());
1874 }
1875
1876 #[test]
1877 fn parse_pkcs11_id_get_missing_arg() {
1878 assert!("pkcs11-id-get".parse::<OvpnCommand>().is_err());
1879 }
1880
1881 #[test]
1882 fn parse_bytecount_non_numeric() {
1883 assert!("bytecount abc".parse::<OvpnCommand>().is_err());
1884 }
1885
1886 #[test]
1887 fn parse_needstr_missing_value() {
1888 assert!("needstr".parse::<OvpnCommand>().is_err());
1889 }
1890
1891 #[test]
1894 fn parse_env_filter() {
1895 assert_eq!("env-filter 2".parse(), Ok(OvpnCommand::EnvFilter(2)));
1896 assert_eq!("env-filter 0".parse(), Ok(OvpnCommand::EnvFilter(0)));
1897 assert_eq!("env-filter".parse(), Ok(OvpnCommand::EnvFilter(0)));
1898 assert!("env-filter abc".parse::<OvpnCommand>().is_err());
1899 }
1900
1901 #[test]
1902 fn parse_remote_entry_count() {
1903 assert_eq!(
1904 "remote-entry-count".parse(),
1905 Ok(OvpnCommand::RemoteEntryCount)
1906 );
1907 }
1908
1909 #[test]
1910 fn parse_remote_entry_get() {
1911 assert_eq!(
1912 "remote-entry-get 0".parse(),
1913 Ok(OvpnCommand::RemoteEntryGet(RemoteEntryRange::Single(0)))
1914 );
1915 assert_eq!(
1916 "remote-entry-get 0 3".parse(),
1917 Ok(OvpnCommand::RemoteEntryGet(RemoteEntryRange::Range {
1918 from: 0,
1919 end: 3
1920 }))
1921 );
1922 assert_eq!(
1923 "remote-entry-get all".parse(),
1924 Ok(OvpnCommand::RemoteEntryGet(RemoteEntryRange::All))
1925 );
1926 assert!("remote-entry-get".parse::<OvpnCommand>().is_err());
1927 assert!("remote-entry-get abc".parse::<OvpnCommand>().is_err());
1928 assert!("remote-entry-get 0 abc".parse::<OvpnCommand>().is_err());
1929 }
1930
1931 #[test]
1932 fn parse_push_update_broad() {
1933 let cmd: OvpnCommand = r#"push-update-broad "route 10.0.0.0""#.parse().unwrap();
1936 assert_eq!(
1937 cmd,
1938 OvpnCommand::PushUpdateBroad {
1939 options: "route 10.0.0.0".to_string()
1940 }
1941 );
1942 assert!("push-update-broad".parse::<OvpnCommand>().is_err());
1943 }
1944
1945 #[test]
1948 fn exit_quit_expect_no_response() {
1949 assert_eq!(
1950 OvpnCommand::Exit.expected_response(),
1951 ResponseKind::NoResponse,
1952 );
1953 assert_eq!(
1954 OvpnCommand::Quit.expected_response(),
1955 ResponseKind::NoResponse,
1956 );
1957 }
1958
1959 #[test]
1960 fn parse_push_update_cid() {
1961 let cmd: OvpnCommand = r#"push-update-cid 42 "route 10.0.0.0""#.parse().unwrap();
1964 assert_eq!(
1965 cmd,
1966 OvpnCommand::PushUpdateCid {
1967 cid: 42,
1968 options: "route 10.0.0.0".to_string()
1969 }
1970 );
1971 assert!("push-update-cid".parse::<OvpnCommand>().is_err());
1972 assert!("push-update-cid abc opts".parse::<OvpnCommand>().is_err());
1973 }
1974
1975 #[test]
1978 fn parse_client_pending_auth() {
1979 let cmd: OvpnCommand = "client-pending-auth 42 1 WEB_AUTH::https://example.com 120"
1982 .parse()
1983 .unwrap();
1984 assert_eq!(
1985 cmd,
1986 OvpnCommand::ClientPendingAuth {
1987 cid: 42,
1988 kid: 1,
1989 extra: "WEB_AUTH::https://example.com".to_string(),
1990 timeout: 120,
1991 }
1992 );
1993 }
1994
1995 #[test]
1996 fn parse_client_pending_auth_missing_args() {
1997 assert!("client-pending-auth".parse::<OvpnCommand>().is_err());
1998 assert!("client-pending-auth 1".parse::<OvpnCommand>().is_err());
1999 assert!("client-pending-auth 1 2".parse::<OvpnCommand>().is_err());
2000 assert!(
2001 "client-pending-auth 1 2 extra"
2002 .parse::<OvpnCommand>()
2003 .is_err()
2004 );
2005 }
2006
2007 #[test]
2008 fn parse_client_pending_auth_non_numeric() {
2009 assert!(
2010 "client-pending-auth abc 1 extra 120"
2011 .parse::<OvpnCommand>()
2012 .is_err()
2013 );
2014 assert!(
2015 "client-pending-auth 1 abc extra 120"
2016 .parse::<OvpnCommand>()
2017 .is_err()
2018 );
2019 assert!(
2020 "client-pending-auth 1 2 extra abc"
2021 .parse::<OvpnCommand>()
2022 .is_err()
2023 );
2024 }
2025
2026 #[test]
2029 fn parse_cr_response() {
2030 let cmd: OvpnCommand = "cr-response dGVzdA==".parse().unwrap();
2033 assert_eq!(
2034 cmd,
2035 OvpnCommand::CrResponse {
2036 response: Redacted::new("dGVzdA=="),
2037 }
2038 );
2039 }
2040
2041 #[test]
2042 fn parse_cr_response_missing_arg() {
2043 assert!("cr-response".parse::<OvpnCommand>().is_err());
2044 }
2045}