use std::fmt;
use std::str::FromStr;
use crate::{
auth::{AuthRetryMode, AuthType, ParseAuthRetryModeError},
client_deny::ClientDeny,
kill_target::KillTarget,
need_ok::NeedOkResponse,
proxy_action::ProxyAction,
redacted::Redacted,
remote_action::RemoteAction,
signal::{ParseSignalError, Signal},
status_format::StatusFormat,
stream_mode::{ParseStreamModeError, StreamMode},
transport_protocol::TransportProtocol,
};
use tracing::warn;
fn next_token(input: &str) -> Option<(String, &str)> {
let input = input.trim_start();
if input.is_empty() {
return None;
}
if let Some(quoted) = input.strip_prefix('"') {
let mut chars = quoted.chars();
let mut token = String::new();
let mut closed = false;
loop {
match chars.next() {
None => break,
Some('"') => {
closed = true;
break;
}
Some('\\') => match chars.next() {
Some(escaped) => token.push(escaped),
None => break,
},
Some(plain) => token.push(plain),
}
}
if !closed {
return None;
}
let rest = chars.as_str().trim_start();
Some((token, rest))
} else {
match input.split_once(char::is_whitespace) {
Some((tok, rest)) => Some((tok.to_string(), rest.trim_start())),
None => Some((input.to_string(), "")),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum CommandParseError {
#[error(transparent)]
Signal(#[from] ParseSignalError),
#[error(transparent)]
StreamMode(#[from] ParseStreamModeError),
#[error(transparent)]
AuthRetryMode(#[from] ParseAuthRetryModeError),
#[error("{field} must be a number, got: {input}")]
InvalidNumber {
field: &'static str,
input: String,
},
#[error("invalid {field}: {input} ({hint})")]
InvalidChoice {
field: &'static str,
input: String,
hint: &'static str,
},
#[error("{0}")]
MissingArgs(&'static str),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RemoteEntryRange {
Single(u32),
Range {
from: u32,
end: u32,
},
All,
}
impl fmt::Display for RemoteEntryRange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Single(index) => write!(f, "{index}"),
Self::Range { from, end } => write!(f, "{from} {end}"),
Self::All => f.write_str("all"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, strum::IntoStaticStr)]
#[strum(serialize_all = "kebab-case")]
pub enum OvpnCommand {
Status(StatusFormat),
State,
StateStream(StreamMode),
Version,
SetVersion(u32),
Pid,
Help,
Verb(Option<u8>),
Mute(Option<u32>),
Net,
Log(StreamMode),
Echo(StreamMode),
ByteCount(u32),
Signal(Signal),
Kill(KillTarget),
HoldQuery,
HoldOn,
HoldOff,
HoldRelease,
Username {
auth_type: AuthType,
value: Redacted,
},
Password {
auth_type: AuthType,
value: Redacted,
},
AuthRetry(AuthRetryMode),
ForgetPasswords,
ChallengeResponse {
state_id: String,
response: Redacted,
},
StaticChallengeResponse {
password_b64: Redacted,
response_b64: Redacted,
},
NeedOk {
name: String,
response: NeedOkResponse,
},
NeedStr {
name: String,
value: String,
},
Pkcs11IdCount,
Pkcs11IdGet(u32),
RsaSig {
base64_lines: Vec<String>,
},
ClientAuth {
cid: u64,
kid: u64,
config_lines: Vec<String>,
},
ClientAuthNt {
cid: u64,
kid: u64,
},
ClientDeny(ClientDeny),
ClientKill {
cid: u64,
message: Option<String>,
},
Remote(RemoteAction),
Proxy(ProxyAction),
LoadStats,
ClientPendingAuth {
cid: u64,
kid: u64,
extra: String,
timeout: u32,
},
CrResponse {
response: Redacted,
},
PkSig {
base64_lines: Vec<String>,
},
EnvFilter(u32),
RemoteEntryCount,
RemoteEntryGet(RemoteEntryRange),
PushUpdateBroad {
options: String,
},
PushUpdateCid {
cid: u64,
options: String,
},
Certificate {
pem_lines: Vec<String>,
},
ManagementPassword(Redacted),
Exit,
Quit,
Raw(String),
RawMultiLine(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ResponseKind {
SuccessOrError,
MultiLine,
NoResponse,
}
impl OvpnCommand {
pub(crate) fn expected_response(&self) -> ResponseKind {
match self {
Self::Status(_)
| Self::Version
| Self::Help
| Self::Net
| Self::RemoteEntryCount
| Self::RemoteEntryGet(_) => ResponseKind::MultiLine,
Self::StateStream(mode) | Self::Log(mode) | Self::Echo(mode) => match mode {
StreamMode::All | StreamMode::OnAll | StreamMode::Recent(_) => {
ResponseKind::MultiLine
}
StreamMode::On | StreamMode::Off => ResponseKind::SuccessOrError,
},
Self::State => ResponseKind::MultiLine,
Self::RawMultiLine(_) => ResponseKind::MultiLine,
Self::Exit | Self::Quit => ResponseKind::NoResponse,
Self::SetVersion(n) if *n < 4 => ResponseKind::NoResponse,
_ => ResponseKind::SuccessOrError,
}
}
}
impl FromStr for OvpnCommand {
type Err = CommandParseError;
fn from_str(line: &str) -> Result<Self, Self::Err> {
fn cmd_err<T>(msg: &'static str) -> Result<T, CommandParseError> {
Err(CommandParseError::MissingArgs(msg))
}
let line = line.trim();
let (cmd, args) = line
.split_once(char::is_whitespace)
.map(|(c, a)| (c, a.trim()))
.unwrap_or((line, ""));
match cmd {
"version" if args.is_empty() => Ok(Self::Version),
"version" => Ok(Self::SetVersion(args.parse().map_err(|_| {
CommandParseError::InvalidChoice {
field: "version number",
input: args.to_string(),
hint: "expected a positive integer (e.g. version 2)",
}
})?)),
"pid" => Ok(Self::Pid),
"help" => Ok(Self::Help),
"net" => Ok(Self::Net),
"load-stats" => Ok(Self::LoadStats),
"status" => match args {
"" | "1" => Ok(Self::Status(StatusFormat::V1)),
"2" => Ok(Self::Status(StatusFormat::V2)),
"3" => Ok(Self::Status(StatusFormat::V3)),
_ => Err(CommandParseError::InvalidChoice {
field: "status format",
input: args.to_string(),
hint: "use 1, 2, or 3",
}),
},
"state" => match args {
"" => Ok(Self::State),
other => Ok(Self::StateStream(other.parse::<StreamMode>()?)),
},
"log" => Ok(Self::Log(args.parse::<StreamMode>()?)),
"echo" => Ok(Self::Echo(args.parse::<StreamMode>()?)),
"verb" => {
if args.is_empty() {
Ok(Self::Verb(None))
} else {
args.parse::<u8>()
.map(|level| Self::Verb(Some(level)))
.map_err(|_| CommandParseError::InvalidNumber {
field: "verbosity",
input: args.to_string(),
})
}
}
"mute" => {
if args.is_empty() {
Ok(Self::Mute(None))
} else {
args.parse::<u32>()
.map(|threshold| Self::Mute(Some(threshold)))
.map_err(|_| CommandParseError::InvalidNumber {
field: "mute threshold",
input: args.to_string(),
})
}
}
"bytecount" => args.parse::<u32>().map(Self::ByteCount).map_err(|_| {
CommandParseError::InvalidNumber {
field: "bytecount interval",
input: args.to_string(),
}
}),
"signal" => Ok(Self::Signal(args.parse::<Signal>()?)),
"kill" => {
if args.is_empty() {
return cmd_err("kill requires a target (common name or proto:ip:port)");
}
let parts: Vec<&str> = args.splitn(3, ':').collect();
if parts.len() == 3
&& let Ok(port) = parts[2].parse::<u16>()
{
return Ok(Self::Kill(KillTarget::Address {
protocol: parts[0]
.parse()
.inspect_err(|error| warn!(%error, "unknown transport protocol"))
.unwrap_or_else(|_| TransportProtocol::Unknown(parts[0].to_string())),
ip: parts[1].to_string(),
port,
}));
}
Ok(Self::Kill(KillTarget::CommonName(args.to_string())))
}
"hold" => match args {
"" => Ok(Self::HoldQuery),
"on" => Ok(Self::HoldOn),
"off" => Ok(Self::HoldOff),
"release" => Ok(Self::HoldRelease),
_ => Err(CommandParseError::InvalidChoice {
field: "hold argument",
input: args.to_string(),
hint: "use on, off, or release",
}),
},
"username" => {
let (auth_type_str, rest) = next_token(args).ok_or(
CommandParseError::MissingArgs("usage: username <auth-type> <value>"),
)?;
let (value, _) = next_token(rest).ok_or(CommandParseError::MissingArgs(
"usage: username <auth-type> <value>",
))?;
Ok(Self::Username {
auth_type: auth_type_str
.parse()
.inspect_err(|error| warn!(%error, "unknown auth type"))
.unwrap_or(AuthType::Unknown(auth_type_str)),
value: value.into(),
})
}
"password" => {
let (auth_type_str, rest) = next_token(args).ok_or(
CommandParseError::MissingArgs("usage: password <auth-type> <value>"),
)?;
let (value, _) = next_token(rest).ok_or(CommandParseError::MissingArgs(
"usage: password <auth-type> <value>",
))?;
Ok(Self::Password {
auth_type: auth_type_str
.parse()
.inspect_err(|error| warn!(%error, "unknown auth type"))
.unwrap_or(AuthType::Unknown(auth_type_str)),
value: value.into(),
})
}
"auth-retry" => Ok(Self::AuthRetry(args.parse::<AuthRetryMode>()?)),
"forget-passwords" => Ok(Self::ForgetPasswords),
"needok" => {
let (name, resp) =
args.rsplit_once(char::is_whitespace)
.ok_or(CommandParseError::MissingArgs(
"usage: needok <name> ok|cancel",
))?;
let response = match resp {
"ok" => NeedOkResponse::Ok,
"cancel" => NeedOkResponse::Cancel,
_ => {
return Err(CommandParseError::InvalidChoice {
field: "needok response",
input: resp.to_string(),
hint: "use ok or cancel",
});
}
};
Ok(Self::NeedOk {
name: name.trim().to_string(),
response,
})
}
"needstr" => {
let (name, rest) = next_token(args).ok_or(CommandParseError::MissingArgs(
"usage: needstr <name> <value>",
))?;
let (value, _) = next_token(rest).ok_or(CommandParseError::MissingArgs(
"usage: needstr <name> <value>",
))?;
Ok(Self::NeedStr { name, value })
}
"pkcs11-id-count" => Ok(Self::Pkcs11IdCount),
"pkcs11-id-get" => args.parse::<u32>().map(Self::Pkcs11IdGet).map_err(|_| {
CommandParseError::InvalidNumber {
field: "pkcs11-id-get index",
input: args.to_string(),
}
}),
"client-auth" => {
let mut parts = args.splitn(3, char::is_whitespace);
let cid = parts
.next()
.ok_or(CommandParseError::MissingArgs(
"usage: client-auth <cid> <kid> [config-lines]",
))?
.parse::<u64>()
.map_err(|_| CommandParseError::MissingArgs("cid must be a number"))?;
let kid = parts
.next()
.ok_or(CommandParseError::MissingArgs(
"usage: client-auth <cid> <kid> [config-lines]",
))?
.parse::<u64>()
.map_err(|_| CommandParseError::MissingArgs("kid must be a number"))?;
let config_lines = match parts.next() {
Some(rest) => rest
.split(',')
.map(|line| line.trim().to_string())
.collect(),
None => vec![],
};
Ok(Self::ClientAuth {
cid,
kid,
config_lines,
})
}
"client-auth-nt" => {
let (cid_s, kid_s) =
args.split_once(char::is_whitespace)
.ok_or(CommandParseError::MissingArgs(
"usage: client-auth-nt <cid> <kid>",
))?;
Ok(Self::ClientAuthNt {
cid: cid_s
.parse()
.map_err(|_| CommandParseError::MissingArgs("cid must be a number"))?,
kid: kid_s
.trim()
.parse()
.map_err(|_| CommandParseError::MissingArgs("kid must be a number"))?,
})
}
"client-deny" => {
let (cid_str, rest) = next_token(args).ok_or(CommandParseError::MissingArgs(
"usage: client-deny <cid> <kid> <reason> [client-reason]",
))?;
let cid = cid_str
.parse::<u64>()
.map_err(|_| CommandParseError::MissingArgs("cid must be a number"))?;
let (kid_str, rest) = next_token(rest).ok_or(CommandParseError::MissingArgs(
"usage: client-deny <cid> <kid> <reason> [client-reason]",
))?;
let kid = kid_str
.parse::<u64>()
.map_err(|_| CommandParseError::MissingArgs("kid must be a number"))?;
let (reason, rest) = next_token(rest).ok_or(CommandParseError::MissingArgs(
"usage: client-deny <cid> <kid> <reason> [client-reason]",
))?;
let client_reason = next_token(rest).map(|(cr, _)| cr);
Ok(Self::ClientDeny(ClientDeny {
cid,
kid,
reason,
client_reason,
}))
}
"client-kill" => {
let (cid_str, message) = match args.split_once(char::is_whitespace) {
Some((c, m)) => (c, Some(m.trim().to_string())),
None => (args, None),
};
let cid = cid_str
.parse::<u64>()
.map_err(|_| CommandParseError::InvalidNumber {
field: "client-kill CID",
input: cid_str.to_string(),
})?;
Ok(Self::ClientKill { cid, message })
}
"remote" => match args.split_whitespace().collect::<Vec<_>>().as_slice() {
["accept" | "ACCEPT"] => Ok(Self::Remote(RemoteAction::Accept)),
["skip" | "SKIP"] => Ok(Self::Remote(RemoteAction::Skip)),
["skip" | "SKIP", n] => Ok(Self::Remote(RemoteAction::SkipN(n.parse().map_err(
|_| CommandParseError::InvalidChoice {
field: "remote skip count",
input: n.to_string(),
hint: "expected a positive integer (e.g. remote SKIP 3)",
},
)?))),
["mod" | "MOD", host, port] => Ok(Self::Remote(RemoteAction::Modify {
host: host.to_string(),
port: port
.parse()
.map_err(|_| CommandParseError::MissingArgs("port must be a number"))?,
})),
_ => cmd_err("usage: remote accept|skip [n]|mod <host> <port>"),
},
"proxy" => match args.split_whitespace().collect::<Vec<_>>().as_slice() {
["none" | "NONE"] => Ok(Self::Proxy(ProxyAction::None)),
["http" | "HTTP", host, port] => Ok(Self::Proxy(ProxyAction::Http {
host: host.to_string(),
port: port
.parse()
.map_err(|_| CommandParseError::MissingArgs("port must be a number"))?,
non_cleartext_only: false,
})),
["http" | "HTTP", host, port, "nct"] => Ok(Self::Proxy(ProxyAction::Http {
host: host.to_string(),
port: port
.parse()
.map_err(|_| CommandParseError::MissingArgs("port must be a number"))?,
non_cleartext_only: true,
})),
["socks" | "SOCKS", host, port] => Ok(Self::Proxy(ProxyAction::Socks {
host: host.to_string(),
port: port
.parse()
.map_err(|_| CommandParseError::MissingArgs("port must be a number"))?,
})),
_ => cmd_err("usage: proxy none|http <host> <port> [nct]|socks <host> <port>"),
},
"env-filter" => {
let level = if args.is_empty() {
0
} else {
args.parse::<u32>()
.map_err(|_| CommandParseError::InvalidNumber {
field: "env-filter level",
input: args.to_string(),
})?
};
Ok(Self::EnvFilter(level))
}
"remote-entry-count" => Ok(Self::RemoteEntryCount),
"remote-entry-get" => {
if args.is_empty() {
return cmd_err("usage: remote-entry-get i|all [j]");
}
let range = if args == "all" {
RemoteEntryRange::All
} else {
let mut parts = args.splitn(2, char::is_whitespace);
let from = parts.next().unwrap().parse::<u32>().map_err(|_| {
CommandParseError::InvalidNumber {
field: "remote-entry-get index",
input: args.to_string(),
}
})?;
match parts.next() {
Some(to_str) => {
let end = to_str.trim().parse::<u32>().map_err(|_| {
CommandParseError::InvalidNumber {
field: "remote-entry-get end index",
input: to_str.to_string(),
}
})?;
RemoteEntryRange::Range { from, end }
}
None => RemoteEntryRange::Single(from),
}
};
Ok(Self::RemoteEntryGet(range))
}
"push-update-broad" => {
let (options, _) = next_token(args).ok_or(CommandParseError::MissingArgs(
"usage: push-update-broad <options>",
))?;
Ok(Self::PushUpdateBroad { options })
}
"push-update-cid" => {
let (cid_str, rest) = next_token(args).ok_or(CommandParseError::MissingArgs(
"usage: push-update-cid <cid> <options>",
))?;
let cid = cid_str.parse::<u64>().map_err(|_| {
CommandParseError::MissingArgs("push-update-cid: cid must be a number")
})?;
let (options, _) = next_token(rest).ok_or(CommandParseError::MissingArgs(
"usage: push-update-cid <cid> <options>",
))?;
Ok(Self::PushUpdateCid { cid, options })
}
"client-pending-auth" => {
let (cid_str, rest) = next_token(args).ok_or(CommandParseError::MissingArgs(
"usage: client-pending-auth <cid> <kid> <extra> <timeout>",
))?;
let cid = cid_str
.parse::<u64>()
.map_err(|_| CommandParseError::MissingArgs("cid must be a number"))?;
let (kid_str, rest) = next_token(rest).ok_or(CommandParseError::MissingArgs(
"usage: client-pending-auth <cid> <kid> <extra> <timeout>",
))?;
let kid = kid_str
.parse::<u64>()
.map_err(|_| CommandParseError::MissingArgs("kid must be a number"))?;
let (extra, rest) = next_token(rest).ok_or(CommandParseError::MissingArgs(
"usage: client-pending-auth <cid> <kid> <extra> <timeout>",
))?;
let (timeout_str, _) = next_token(rest).ok_or(CommandParseError::MissingArgs(
"usage: client-pending-auth <cid> <kid> <extra> <timeout>",
))?;
let timeout =
timeout_str
.parse::<u32>()
.map_err(|_| CommandParseError::InvalidNumber {
field: "client-pending-auth timeout",
input: timeout_str,
})?;
Ok(Self::ClientPendingAuth {
cid,
kid,
extra,
timeout,
})
}
"cr-response" => {
let (response, _) = next_token(args).ok_or(CommandParseError::MissingArgs(
"usage: cr-response <response>",
))?;
Ok(Self::CrResponse {
response: Redacted::new(response),
})
}
"raw-ml" => {
if args.is_empty() {
return cmd_err("usage: raw-ml <command>");
}
Ok(Self::RawMultiLine(args.to_string()))
}
"exit" => Ok(Self::Exit),
"quit" => Ok(Self::Quit),
_ => Ok(Self::Raw(line.to_string())),
}
}
}
pub fn connection_sequence(bytecount_interval: u32) -> Vec<OvpnCommand> {
let mut cmds = vec![
OvpnCommand::Log(StreamMode::OnAll),
OvpnCommand::StateStream(StreamMode::OnAll),
OvpnCommand::Pid,
];
if bytecount_interval > 0 {
cmds.push(OvpnCommand::ByteCount(bytecount_interval));
}
cmds.push(OvpnCommand::HoldRelease);
cmds
}
pub fn server_connection_sequence(bytecount_interval: u32, env_filter: u32) -> Vec<OvpnCommand> {
let mut cmds = vec![
OvpnCommand::Log(StreamMode::OnAll),
OvpnCommand::StateStream(StreamMode::OnAll),
OvpnCommand::Pid,
OvpnCommand::EnvFilter(env_filter),
];
if bytecount_interval > 0 {
cmds.push(OvpnCommand::ByteCount(bytecount_interval));
}
cmds.push(OvpnCommand::HoldRelease);
cmds
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn into_static_str_labels() {
let label: &str = (&OvpnCommand::State).into();
assert_eq!(label, "state");
let label: &str = (&OvpnCommand::ForgetPasswords).into();
assert_eq!(label, "forget-passwords");
let label: &str = (&OvpnCommand::ByteCount(5)).into();
assert_eq!(label, "byte-count");
}
#[test]
fn connection_sequence_with_bytecount() {
let cmds = connection_sequence(5);
assert_eq!(
cmds,
vec![
OvpnCommand::Log(StreamMode::OnAll),
OvpnCommand::StateStream(StreamMode::OnAll),
OvpnCommand::Pid,
OvpnCommand::ByteCount(5),
OvpnCommand::HoldRelease,
]
);
}
#[test]
fn connection_sequence_without_bytecount() {
let cmds = connection_sequence(0);
assert_eq!(
cmds,
vec![
OvpnCommand::Log(StreamMode::OnAll),
OvpnCommand::StateStream(StreamMode::OnAll),
OvpnCommand::Pid,
OvpnCommand::HoldRelease,
]
);
}
#[test]
fn parse_simple_commands() {
assert_eq!("version".parse(), Ok(OvpnCommand::Version));
assert_eq!("pid".parse(), Ok(OvpnCommand::Pid));
assert_eq!("help".parse(), Ok(OvpnCommand::Help));
assert_eq!("net".parse(), Ok(OvpnCommand::Net));
assert_eq!("load-stats".parse(), Ok(OvpnCommand::LoadStats));
assert_eq!("forget-passwords".parse(), Ok(OvpnCommand::ForgetPasswords));
assert_eq!("pkcs11-id-count".parse(), Ok(OvpnCommand::Pkcs11IdCount));
assert_eq!("exit".parse(), Ok(OvpnCommand::Exit));
assert_eq!("quit".parse(), Ok(OvpnCommand::Quit));
}
#[test]
fn parse_status() {
assert_eq!("status".parse(), Ok(OvpnCommand::Status(StatusFormat::V1)));
assert_eq!(
"status 1".parse(),
Ok(OvpnCommand::Status(StatusFormat::V1))
);
assert_eq!(
"status 2".parse(),
Ok(OvpnCommand::Status(StatusFormat::V2))
);
assert_eq!(
"status 3".parse(),
Ok(OvpnCommand::Status(StatusFormat::V3))
);
assert!("status 4".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_state_bare() {
assert_eq!("state".parse(), Ok(OvpnCommand::State));
}
#[test]
fn parse_state_stream_modes() {
assert_eq!(
"state on".parse(),
Ok(OvpnCommand::StateStream(StreamMode::On))
);
assert_eq!(
"state off".parse(),
Ok(OvpnCommand::StateStream(StreamMode::Off))
);
assert_eq!(
"state all".parse(),
Ok(OvpnCommand::StateStream(StreamMode::All))
);
assert_eq!(
"state on all".parse(),
Ok(OvpnCommand::StateStream(StreamMode::OnAll))
);
assert_eq!(
"state 5".parse(),
Ok(OvpnCommand::StateStream(StreamMode::Recent(5)))
);
}
#[test]
fn parse_log_and_echo() {
assert_eq!("log on".parse(), Ok(OvpnCommand::Log(StreamMode::On)));
assert_eq!(
"log on all".parse(),
Ok(OvpnCommand::Log(StreamMode::OnAll))
);
assert_eq!("echo off".parse(), Ok(OvpnCommand::Echo(StreamMode::Off)));
assert_eq!(
"echo 10".parse(),
Ok(OvpnCommand::Echo(StreamMode::Recent(10)))
);
}
#[test]
fn parse_verb() {
assert_eq!("verb".parse(), Ok(OvpnCommand::Verb(None)));
assert_eq!("verb 4".parse(), Ok(OvpnCommand::Verb(Some(4))));
assert!("verb abc".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_mute() {
assert_eq!("mute".parse(), Ok(OvpnCommand::Mute(None)));
assert_eq!("mute 40".parse(), Ok(OvpnCommand::Mute(Some(40))));
assert!("mute abc".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_bytecount() {
assert_eq!("bytecount 5".parse(), Ok(OvpnCommand::ByteCount(5)));
assert_eq!("bytecount 0".parse(), Ok(OvpnCommand::ByteCount(0)));
assert!("bytecount".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_signal() {
assert_eq!(
"signal SIGHUP".parse(),
Ok(OvpnCommand::Signal(Signal::SigHup))
);
assert_eq!(
"signal SIGTERM".parse(),
Ok(OvpnCommand::Signal(Signal::SigTerm))
);
assert_eq!(
"signal SIGUSR1".parse(),
Ok(OvpnCommand::Signal(Signal::SigUsr1))
);
assert_eq!(
"signal SIGUSR2".parse(),
Ok(OvpnCommand::Signal(Signal::SigUsr2))
);
assert!("signal SIGKILL".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_kill_common_name() {
assert_eq!(
"kill TestClient".parse(),
Ok(OvpnCommand::Kill(KillTarget::CommonName(
"TestClient".to_string()
)))
);
}
#[test]
fn parse_kill_address() {
assert_eq!(
"kill tcp:1.2.3.4:4000".parse(),
Ok(OvpnCommand::Kill(KillTarget::Address {
protocol: TransportProtocol::Tcp,
ip: "1.2.3.4".to_string(),
port: 4000,
}))
);
}
#[test]
fn parse_kill_empty_is_err() {
assert!("kill".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_hold() {
assert_eq!("hold".parse(), Ok(OvpnCommand::HoldQuery));
assert_eq!("hold on".parse(), Ok(OvpnCommand::HoldOn));
assert_eq!("hold off".parse(), Ok(OvpnCommand::HoldOff));
assert_eq!("hold release".parse(), Ok(OvpnCommand::HoldRelease));
assert!("hold bogus".parse::<OvpnCommand>().is_err());
}
#[test]
fn next_token_unquoted() {
let (tok, rest) = next_token("Auth s3cret").unwrap();
assert_eq!(tok, "Auth");
assert_eq!(rest, "s3cret");
}
#[test]
fn next_token_quoted_simple() {
let (tok, rest) = next_token(r#""Private Key" "s3cret""#).unwrap();
assert_eq!(tok, "Private Key");
assert_eq!(rest, r#""s3cret""#);
}
#[test]
fn next_token_quoted_with_escapes() {
let (tok, _) = next_token(r#""foo\\\"bar""#).unwrap();
assert_eq!(tok, r#"foo\"bar"#);
}
#[test]
fn next_token_empty() {
assert!(next_token("").is_none());
assert!(next_token(" ").is_none());
}
#[test]
fn next_token_last_token_no_trailing() {
let (tok, rest) = next_token("onlyone").unwrap();
assert_eq!(tok, "onlyone");
assert_eq!(rest, "");
}
#[test]
fn next_token_unclosed_quote_returns_none() {
assert!(next_token(r#""unclosed string"#).is_none());
assert!(next_token(r#""trailing backslash\"#).is_none());
}
#[test]
fn parse_username() {
let cmd: OvpnCommand = "username Auth alice".parse().unwrap();
assert_eq!(
cmd,
OvpnCommand::Username {
auth_type: AuthType::Auth,
value: "alice".into(),
}
);
}
#[test]
fn parse_password() {
let cmd: OvpnCommand = "password Auth s3cret".parse().unwrap();
assert_eq!(
cmd,
OvpnCommand::Password {
auth_type: AuthType::Auth,
value: "s3cret".into(),
}
);
}
#[test]
fn parse_username_missing_value_is_err() {
assert!("username".parse::<OvpnCommand>().is_err());
assert!("username Auth".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_auth_retry() {
assert_eq!(
"auth-retry none".parse(),
Ok(OvpnCommand::AuthRetry(AuthRetryMode::None))
);
assert_eq!(
"auth-retry interact".parse(),
Ok(OvpnCommand::AuthRetry(AuthRetryMode::Interact))
);
assert_eq!(
"auth-retry nointeract".parse(),
Ok(OvpnCommand::AuthRetry(AuthRetryMode::NoInteract))
);
assert!("auth-retry bogus".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_needok() {
assert_eq!(
"needok token-insertion ok".parse(),
Ok(OvpnCommand::NeedOk {
name: "token-insertion".to_string(),
response: NeedOkResponse::Ok,
})
);
assert_eq!(
"needok token-insertion cancel".parse(),
Ok(OvpnCommand::NeedOk {
name: "token-insertion".to_string(),
response: NeedOkResponse::Cancel,
})
);
assert!("needok".parse::<OvpnCommand>().is_err());
assert!("needok name bogus".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_needstr() {
assert_eq!(
"needstr prompt-name John".parse(),
Ok(OvpnCommand::NeedStr {
name: "prompt-name".to_string(),
value: "John".to_string(),
})
);
assert!("needstr".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_pkcs11_id_get() {
assert_eq!("pkcs11-id-get 1".parse(), Ok(OvpnCommand::Pkcs11IdGet(1)));
assert!("pkcs11-id-get abc".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_client_auth() {
assert_eq!(
"client-auth 42 7".parse(),
Ok(OvpnCommand::ClientAuth {
cid: 42,
kid: 7,
config_lines: vec![],
})
);
}
#[test]
fn parse_client_auth_with_config() {
let cmd: OvpnCommand = "client-auth 1 2 push route 10.0.0.0,ifconfig-push 10.0.1.1"
.parse()
.unwrap();
assert_eq!(
cmd,
OvpnCommand::ClientAuth {
cid: 1,
kid: 2,
config_lines: vec![
"push route 10.0.0.0".to_string(),
"ifconfig-push 10.0.1.1".to_string(),
],
}
);
}
#[test]
fn parse_client_auth_nt() {
assert_eq!(
"client-auth-nt 5 3".parse(),
Ok(OvpnCommand::ClientAuthNt { cid: 5, kid: 3 })
);
assert!("client-auth-nt abc 3".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_client_deny() {
assert_eq!(
"client-deny 1 2 rejected".parse(),
Ok(OvpnCommand::ClientDeny(ClientDeny {
cid: 1,
kid: 2,
reason: "rejected".to_string(),
client_reason: None,
}))
);
assert_eq!(
"client-deny 1 2 rejected sorry".parse(),
Ok(OvpnCommand::ClientDeny(ClientDeny {
cid: 1,
kid: 2,
reason: "rejected".to_string(),
client_reason: Some("sorry".to_string()),
}))
);
}
#[test]
fn parse_client_kill() {
assert_eq!(
"client-kill 99".parse(),
Ok(OvpnCommand::ClientKill {
cid: 99,
message: None,
})
);
assert_eq!(
"client-kill 99 HALT".parse(),
Ok(OvpnCommand::ClientKill {
cid: 99,
message: Some("HALT".to_string()),
})
);
assert!("client-kill abc".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_remote() {
assert_eq!(
"remote accept".parse(),
Ok(OvpnCommand::Remote(RemoteAction::Accept))
);
assert_eq!(
"remote SKIP".parse(),
Ok(OvpnCommand::Remote(RemoteAction::Skip))
);
assert_eq!(
"remote MOD example.com 443".parse(),
Ok(OvpnCommand::Remote(RemoteAction::Modify {
host: "example.com".to_string(),
port: 443,
}))
);
assert!("remote".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_proxy() {
assert_eq!(
"proxy none".parse(),
Ok(OvpnCommand::Proxy(ProxyAction::None))
);
assert_eq!(
"proxy HTTP proxy.local 8080".parse(),
Ok(OvpnCommand::Proxy(ProxyAction::Http {
host: "proxy.local".to_string(),
port: 8080,
non_cleartext_only: false,
}))
);
assert_eq!(
"proxy http proxy.local 8080 nct".parse(),
Ok(OvpnCommand::Proxy(ProxyAction::Http {
host: "proxy.local".to_string(),
port: 8080,
non_cleartext_only: true,
}))
);
assert_eq!(
"proxy socks socks.local 1080".parse(),
Ok(OvpnCommand::Proxy(ProxyAction::Socks {
host: "socks.local".to_string(),
port: 1080,
}))
);
assert!("proxy".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_raw_ml() {
assert_eq!(
"raw-ml some-cmd".parse(),
Ok(OvpnCommand::RawMultiLine("some-cmd".to_string()))
);
assert!("raw-ml".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_unrecognized_falls_through_to_raw() {
assert_eq!(
"unknown-cmd foo bar".parse(),
Ok(OvpnCommand::Raw("unknown-cmd foo bar".to_string()))
);
}
#[test]
fn parse_trims_whitespace() {
assert_eq!(" version ".parse(), Ok(OvpnCommand::Version));
assert_eq!(
" state on ".parse(),
Ok(OvpnCommand::StateStream(StreamMode::On))
);
}
#[test]
fn parse_state_invalid_stream_mode() {
assert!("state bogus".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_log_invalid_stream_mode() {
assert!("log bogus".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_echo_invalid_stream_mode() {
assert!("echo bogus".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_kill_unknown_protocol_falls_back() {
let cmd: OvpnCommand = "kill sctp:1.2.3.4:4000".parse().unwrap();
assert_eq!(
cmd,
OvpnCommand::Kill(KillTarget::Address {
protocol: TransportProtocol::Unknown("sctp".to_string()),
ip: "1.2.3.4".to_string(),
port: 4000,
})
);
}
#[test]
fn parse_username_unknown_auth_type_falls_back() {
let cmd: OvpnCommand = "username MyPlugin alice".parse().unwrap();
assert_eq!(
cmd,
OvpnCommand::Username {
auth_type: AuthType::Unknown("MyPlugin".to_string()),
value: "alice".into(),
}
);
}
#[test]
fn parse_password_unknown_auth_type_falls_back() {
let cmd: OvpnCommand = "password MyPlugin s3cret".parse().unwrap();
assert_eq!(
cmd,
OvpnCommand::Password {
auth_type: AuthType::Unknown("MyPlugin".to_string()),
value: "s3cret".into(),
}
);
}
#[test]
fn parse_password_missing_value_is_err() {
assert!("password".parse::<OvpnCommand>().is_err());
assert!("password Auth".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_password_quoted_spaced_auth_type() {
let cmd: OvpnCommand = r#"password "Private Key" "s3cret""#.parse().unwrap();
assert_eq!(
cmd,
OvpnCommand::Password {
auth_type: AuthType::PrivateKey,
value: "s3cret".into(),
}
);
}
#[test]
fn parse_password_quoted_http_proxy() {
let cmd: OvpnCommand = r#"password "HTTP Proxy" "proxypass""#.parse().unwrap();
assert_eq!(
cmd,
OvpnCommand::Password {
auth_type: AuthType::HttpProxy,
value: "proxypass".into(),
}
);
}
#[test]
fn parse_password_quoted_socks_proxy() {
let cmd: OvpnCommand = r#"password "SOCKS Proxy" "sockspass""#.parse().unwrap();
assert_eq!(
cmd,
OvpnCommand::Password {
auth_type: AuthType::SocksProxy,
value: "sockspass".into(),
}
);
}
#[test]
fn parse_username_quoted_spaced_auth_type() {
let cmd: OvpnCommand = r#"username "HTTP Proxy" "proxyuser""#.parse().unwrap();
assert_eq!(
cmd,
OvpnCommand::Username {
auth_type: AuthType::HttpProxy,
value: "proxyuser".into(),
}
);
}
#[test]
fn parse_password_roundtrip_spaced_auth_types() {
use crate::OvpnCodec;
use bytes::BytesMut;
use tokio_util::codec::Encoder;
for auth_type in [
AuthType::PrivateKey,
AuthType::HttpProxy,
AuthType::SocksProxy,
] {
let original = OvpnCommand::Password {
auth_type: auth_type.clone(),
value: "test".into(),
};
let mut codec = OvpnCodec::new();
let mut buf = BytesMut::new();
codec.encode(original.clone(), &mut buf).unwrap();
let wire = String::from_utf8(buf.to_vec()).unwrap();
let parsed: OvpnCommand = wire.trim().parse().unwrap();
assert_eq!(parsed, original);
}
}
#[test]
fn parse_client_auth_non_numeric_cid() {
assert!("client-auth abc 1".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_client_auth_non_numeric_kid() {
assert!("client-auth 1 abc".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_client_auth_nt_non_numeric_kid() {
assert!("client-auth-nt 1 abc".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_client_deny_missing_args() {
assert!("client-deny".parse::<OvpnCommand>().is_err());
assert!("client-deny 1".parse::<OvpnCommand>().is_err());
assert!("client-deny 1 2".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_client_deny_non_numeric_ids() {
assert!("client-deny abc 1 reason".parse::<OvpnCommand>().is_err());
assert!("client-deny 1 abc reason".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_remote_non_numeric_port() {
assert!("remote mod host abc".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_proxy_non_numeric_port() {
assert!("proxy http host abc".parse::<OvpnCommand>().is_err());
assert!("proxy http host abc nct".parse::<OvpnCommand>().is_err());
assert!("proxy socks host abc".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_pkcs11_id_get_missing_arg() {
assert!("pkcs11-id-get".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_bytecount_non_numeric() {
assert!("bytecount abc".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_needstr_missing_value() {
assert!("needstr".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_env_filter() {
assert_eq!("env-filter 2".parse(), Ok(OvpnCommand::EnvFilter(2)));
assert_eq!("env-filter 0".parse(), Ok(OvpnCommand::EnvFilter(0)));
assert_eq!("env-filter".parse(), Ok(OvpnCommand::EnvFilter(0)));
assert!("env-filter abc".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_remote_entry_count() {
assert_eq!(
"remote-entry-count".parse(),
Ok(OvpnCommand::RemoteEntryCount)
);
}
#[test]
fn parse_remote_entry_get() {
assert_eq!(
"remote-entry-get 0".parse(),
Ok(OvpnCommand::RemoteEntryGet(RemoteEntryRange::Single(0)))
);
assert_eq!(
"remote-entry-get 0 3".parse(),
Ok(OvpnCommand::RemoteEntryGet(RemoteEntryRange::Range {
from: 0,
end: 3
}))
);
assert_eq!(
"remote-entry-get all".parse(),
Ok(OvpnCommand::RemoteEntryGet(RemoteEntryRange::All))
);
assert!("remote-entry-get".parse::<OvpnCommand>().is_err());
assert!("remote-entry-get abc".parse::<OvpnCommand>().is_err());
assert!("remote-entry-get 0 abc".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_push_update_broad() {
let cmd: OvpnCommand = r#"push-update-broad "route 10.0.0.0""#.parse().unwrap();
assert_eq!(
cmd,
OvpnCommand::PushUpdateBroad {
options: "route 10.0.0.0".to_string()
}
);
assert!("push-update-broad".parse::<OvpnCommand>().is_err());
}
#[test]
fn exit_quit_expect_no_response() {
assert_eq!(
OvpnCommand::Exit.expected_response(),
ResponseKind::NoResponse,
);
assert_eq!(
OvpnCommand::Quit.expected_response(),
ResponseKind::NoResponse,
);
}
#[test]
fn parse_push_update_cid() {
let cmd: OvpnCommand = r#"push-update-cid 42 "route 10.0.0.0""#.parse().unwrap();
assert_eq!(
cmd,
OvpnCommand::PushUpdateCid {
cid: 42,
options: "route 10.0.0.0".to_string()
}
);
assert!("push-update-cid".parse::<OvpnCommand>().is_err());
assert!("push-update-cid abc opts".parse::<OvpnCommand>().is_err());
}
#[test]
fn parse_client_pending_auth() {
let cmd: OvpnCommand = "client-pending-auth 42 1 WEB_AUTH::https://example.com 120"
.parse()
.unwrap();
assert_eq!(
cmd,
OvpnCommand::ClientPendingAuth {
cid: 42,
kid: 1,
extra: "WEB_AUTH::https://example.com".to_string(),
timeout: 120,
}
);
}
#[test]
fn parse_client_pending_auth_missing_args() {
assert!("client-pending-auth".parse::<OvpnCommand>().is_err());
assert!("client-pending-auth 1".parse::<OvpnCommand>().is_err());
assert!("client-pending-auth 1 2".parse::<OvpnCommand>().is_err());
assert!(
"client-pending-auth 1 2 extra"
.parse::<OvpnCommand>()
.is_err()
);
}
#[test]
fn parse_client_pending_auth_non_numeric() {
assert!(
"client-pending-auth abc 1 extra 120"
.parse::<OvpnCommand>()
.is_err()
);
assert!(
"client-pending-auth 1 abc extra 120"
.parse::<OvpnCommand>()
.is_err()
);
assert!(
"client-pending-auth 1 2 extra abc"
.parse::<OvpnCommand>()
.is_err()
);
}
#[test]
fn parse_cr_response() {
let cmd: OvpnCommand = "cr-response dGVzdA==".parse().unwrap();
assert_eq!(
cmd,
OvpnCommand::CrResponse {
response: Redacted::new("dGVzdA=="),
}
);
}
#[test]
fn parse_cr_response_missing_arg() {
assert!("cr-response".parse::<OvpnCommand>().is_err());
}
}