1use bytes::{Buf, BufMut, BytesMut};
2use std::borrow::Cow;
3use std::io;
4use tokio_util::codec::{Decoder, Encoder};
5use tracing::{debug, warn};
6
7use crate::command::{OvpnCommand, ResponseKind};
8use crate::kill_target::KillTarget;
9use crate::message::{Notification, OvpnMessage};
10use crate::proxy_action::ProxyAction;
11use crate::redacted::Redacted;
12use crate::remote_action::RemoteAction;
13use crate::status_format::StatusFormat;
14use crate::unrecognized::UnrecognizedKind;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub enum EncoderMode {
20 #[default]
25 Sanitize,
26
27 Strict,
34}
35
36#[derive(Debug, thiserror::Error)]
43pub enum EncodeError {
44 #[error("{0} contains characters unsafe for the management protocol (\\n, \\r, or \\0)")]
46 UnsafeCharacters(&'static str),
47
48 #[error("block body line equals \"END\", which would terminate the block early")]
50 EndInBlockBody,
51}
52
53const WIRE_UNSAFE: &[char] = &['\n', '\r', '\0'];
56
57fn wire_safe<'a>(
65 s: &'a str,
66 field: &'static str,
67 mode: EncoderMode,
68) -> Result<Cow<'a, str>, io::Error> {
69 if !s.contains(WIRE_UNSAFE) {
70 return Ok(Cow::Borrowed(s));
71 }
72 match mode {
73 EncoderMode::Sanitize => Ok(Cow::Owned(
74 s.chars().filter(|c| !WIRE_UNSAFE.contains(c)).collect(),
75 )),
76 EncoderMode::Strict => Err(io::Error::other(EncodeError::UnsafeCharacters(field))),
77 }
78}
79
80fn quote_and_escape(s: &str) -> String {
92 let mut out = String::with_capacity(s.len() + 2);
93 out.push('"');
94 for c in s.chars() {
95 match c {
96 '\\' => out.push_str("\\\\"),
97 '"' => out.push_str("\\\""),
98 _ => out.push(c),
99 }
100 }
101 out.push('"');
102 out
103}
104
105use crate::client_event::ClientEvent;
106use crate::log_level::LogLevel;
107use crate::openvpn_state::OpenVpnState;
108use crate::transport_protocol::TransportProtocol;
109
110#[derive(Debug)]
112struct ClientNotifAccum {
113 event: ClientEvent,
114 cid: u64,
115 kid: Option<u64>,
116 env: Vec<(String, String)>,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum AccumulationLimit {
123 Unlimited,
125 Max(usize),
127}
128
129#[derive(better_default::Default)]
151pub struct OvpnCodec {
152 #[default(ResponseKind::SuccessOrError)]
158 expected: ResponseKind,
159
160 multi_line_buf: Option<Vec<String>>,
162
163 client_notif: Option<ClientNotifAccum>,
166
167 #[default(AccumulationLimit::Unlimited)]
169 max_multi_line_lines: AccumulationLimit,
170
171 #[default(AccumulationLimit::Unlimited)]
173 max_client_env_entries: AccumulationLimit,
174
175 encoder_mode: EncoderMode,
177}
178
179impl OvpnCodec {
180 pub fn new() -> Self {
183 Self::default()
184 }
185
186 pub fn with_max_multi_line_lines(mut self, limit: AccumulationLimit) -> Self {
189 self.max_multi_line_lines = limit;
190 self
191 }
192
193 pub fn with_max_client_env_entries(mut self, limit: AccumulationLimit) -> Self {
196 self.max_client_env_entries = limit;
197 self
198 }
199
200 pub fn with_encoder_mode(mut self, mode: EncoderMode) -> Self {
207 self.encoder_mode = mode;
208 self
209 }
210}
211
212fn check_accumulation_limit(
213 current_len: usize,
214 limit: AccumulationLimit,
215 what: &str,
216) -> Result<(), io::Error> {
217 if let AccumulationLimit::Max(max) = limit
218 && current_len >= max
219 {
220 return Err(io::Error::other(format!(
221 "{what} accumulation limit exceeded ({max})"
222 )));
223 }
224 Ok(())
225}
226
227impl Encoder<OvpnCommand> for OvpnCodec {
230 type Error = io::Error;
231
232 fn encode(&mut self, item: OvpnCommand, dst: &mut BytesMut) -> Result<(), Self::Error> {
233 debug_assert!(
234 self.multi_line_buf.is_none() && self.client_notif.is_none(),
235 "encode() called while the decoder is mid-accumulation \
236 (multi_line_buf or client_notif is active). \
237 Drain decode() before sending a new command."
238 );
239
240 self.expected = item.expected_response();
243 let label: &'static str = (&item).into();
244 debug!(cmd = %label, expected = ?self.expected, "encoding command");
245
246 let mode = self.encoder_mode;
247
248 match item {
249 OvpnCommand::Status(StatusFormat::V1) => write_line(dst, "status"),
251 OvpnCommand::Status(ref fmt) => write_line(dst, &format!("status {fmt}")),
252 OvpnCommand::State => write_line(dst, "state"),
253 OvpnCommand::StateStream(ref m) => write_line(dst, &format!("state {m}")),
254 OvpnCommand::Version => write_line(dst, "version"),
255 OvpnCommand::Pid => write_line(dst, "pid"),
256 OvpnCommand::Help => write_line(dst, "help"),
257 OvpnCommand::Net => write_line(dst, "net"),
258 OvpnCommand::Verb(Some(n)) => write_line(dst, &format!("verb {n}")),
259 OvpnCommand::Verb(None) => write_line(dst, "verb"),
260 OvpnCommand::Mute(Some(n)) => write_line(dst, &format!("mute {n}")),
261 OvpnCommand::Mute(None) => write_line(dst, "mute"),
262
263 OvpnCommand::Log(ref m) => write_line(dst, &format!("log {m}")),
265 OvpnCommand::Echo(ref m) => write_line(dst, &format!("echo {m}")),
266 OvpnCommand::ByteCount(n) => write_line(dst, &format!("bytecount {n}")),
267
268 OvpnCommand::Signal(sig) => write_line(dst, &format!("signal {sig}")),
270 OvpnCommand::Kill(KillTarget::CommonName(ref cn)) => {
271 write_line(dst, &format!("kill {}", wire_safe(cn, "kill CN", mode)?));
272 }
273 OvpnCommand::Kill(KillTarget::Address {
274 ref protocol,
275 ref ip,
276 port,
277 }) => {
278 write_line(
279 dst,
280 &format!(
281 "kill {protocol}:{}:{port}",
282 wire_safe(ip, "kill address ip", mode)?
283 ),
284 );
285 }
286 OvpnCommand::HoldQuery => write_line(dst, "hold"),
287 OvpnCommand::HoldOn => write_line(dst, "hold on"),
288 OvpnCommand::HoldOff => write_line(dst, "hold off"),
289 OvpnCommand::HoldRelease => write_line(dst, "hold release"),
290
291 OvpnCommand::Username {
296 ref auth_type,
297 ref value,
298 } => {
299 let at = quote_and_escape(&wire_safe(
303 &auth_type.to_string(),
304 "username auth_type",
305 mode,
306 )?);
307 let val = quote_and_escape(&wire_safe(value.expose(), "username value", mode)?);
308 write_line(dst, &format!("username {at} {val}"));
309 }
310 OvpnCommand::Password {
311 ref auth_type,
312 ref value,
313 } => {
314 let at = quote_and_escape(&wire_safe(
315 &auth_type.to_string(),
316 "password auth_type",
317 mode,
318 )?);
319 let val = quote_and_escape(&wire_safe(value.expose(), "password value", mode)?);
320 write_line(dst, &format!("password {at} {val}"));
321 }
322 OvpnCommand::AuthRetry(auth_retry_mode) => {
323 write_line(dst, &format!("auth-retry {auth_retry_mode}"));
324 }
325 OvpnCommand::ForgetPasswords => write_line(dst, "forget-passwords"),
326
327 OvpnCommand::ChallengeResponse {
329 ref state_id,
330 ref response,
331 } => {
332 let sid = wire_safe(state_id, "challenge-response state_id", mode)?;
333 let resp = wire_safe(response.expose(), "challenge-response response", mode)?;
334 let value = format!("CRV1::{sid}::{resp}");
335 let escaped = quote_and_escape(&value);
336 write_line(dst, &format!("password \"Auth\" {escaped}"));
337 }
338 OvpnCommand::StaticChallengeResponse {
339 ref password_b64,
340 ref response_b64,
341 } => {
342 let pw = wire_safe(password_b64.expose(), "static-challenge password_b64", mode)?;
343 let resp = wire_safe(response_b64.expose(), "static-challenge response_b64", mode)?;
344 let value = format!("SCRV1:{pw}:{resp}");
345 let escaped = quote_and_escape(&value);
346 write_line(dst, &format!("password \"Auth\" {escaped}"));
347 }
348
349 OvpnCommand::NeedOk { ref name, response } => {
351 write_line(
352 dst,
353 &format!(
354 "needok {} {response}",
355 wire_safe(name, "needok name", mode)?
356 ),
357 );
358 }
359 OvpnCommand::NeedStr {
360 ref name,
361 ref value,
362 } => {
363 let escaped = quote_and_escape(&wire_safe(value, "needstr value", mode)?);
364 write_line(
365 dst,
366 &format!(
367 "needstr {} {escaped}",
368 wire_safe(name, "needstr name", mode)?
369 ),
370 );
371 }
372
373 OvpnCommand::Pkcs11IdCount => write_line(dst, "pkcs11-id-count"),
375 OvpnCommand::Pkcs11IdGet(idx) => write_line(dst, &format!("pkcs11-id-get {idx}")),
376
377 OvpnCommand::RsaSig { ref base64_lines } => {
385 write_block(dst, "rsa-sig", base64_lines, mode)?;
386 }
387
388 OvpnCommand::PkSig { ref base64_lines } => {
390 write_block(dst, "pk-sig", base64_lines, mode)?;
391 }
392
393 OvpnCommand::EnvFilter(level) => write_line(dst, &format!("env-filter {level}")),
395
396 OvpnCommand::RemoteEntryCount => write_line(dst, "remote-entry-count"),
398 OvpnCommand::RemoteEntryGet(ref range) => {
399 write_line(dst, &format!("remote-entry-get {range}"));
400 }
401
402 OvpnCommand::PushUpdateBroad { ref options } => {
404 let opts =
405 quote_and_escape(&wire_safe(options, "push-update-broad options", mode)?);
406 write_line(dst, &format!("push-update-broad {opts}"));
407 }
408 OvpnCommand::PushUpdateCid { cid, ref options } => {
409 let opts = quote_and_escape(&wire_safe(options, "push-update-cid options", mode)?);
410 write_line(dst, &format!("push-update-cid {cid} {opts}"));
411 }
412
413 OvpnCommand::ClientAuth {
421 cid,
422 kid,
423 ref config_lines,
424 } => {
425 write_block(dst, &format!("client-auth {cid} {kid}"), config_lines, mode)?;
426 }
427
428 OvpnCommand::ClientAuthNt { cid, kid } => {
429 write_line(dst, &format!("client-auth-nt {cid} {kid}"));
430 }
431
432 OvpnCommand::ClientDeny {
433 cid,
434 kid,
435 ref reason,
436 ref client_reason,
437 } => {
438 let r = quote_and_escape(&wire_safe(reason, "client-deny reason", mode)?);
439 match client_reason {
440 Some(cr) => {
441 let cr_esc =
442 quote_and_escape(&wire_safe(cr, "client-deny client_reason", mode)?);
443 write_line(dst, &format!("client-deny {cid} {kid} {r} {cr_esc}"));
444 }
445 None => write_line(dst, &format!("client-deny {cid} {kid} {r}")),
446 }
447 }
448
449 OvpnCommand::ClientKill { cid, ref message } => match message {
450 Some(msg) => write_line(
451 dst,
452 &format!(
453 "client-kill {cid} {}",
454 wire_safe(msg, "client-kill message", mode)?
455 ),
456 ),
457 None => write_line(dst, &format!("client-kill {cid}")),
458 },
459
460 OvpnCommand::LoadStats => write_line(dst, "load-stats"),
462
463 OvpnCommand::ClientPendingAuth {
470 cid,
471 kid,
472 ref extra,
473 timeout,
474 } => write_line(
475 dst,
476 &format!(
477 "client-pending-auth {cid} {kid} {} {timeout}",
478 wire_safe(extra, "client-pending-auth extra", mode)?
479 ),
480 ),
481
482 OvpnCommand::CrResponse { ref response } => {
483 write_line(
484 dst,
485 &format!(
486 "cr-response {}",
487 wire_safe(response.expose(), "cr-response", mode)?
488 ),
489 );
490 }
491
492 OvpnCommand::Certificate { ref pem_lines } => {
494 write_block(dst, "certificate", pem_lines, mode)?;
495 }
496
497 OvpnCommand::Remote(RemoteAction::Accept) => write_line(dst, "remote ACCEPT"),
499 OvpnCommand::Remote(RemoteAction::Skip) => write_line(dst, "remote SKIP"),
500 OvpnCommand::Remote(RemoteAction::Modify { ref host, port }) => {
501 write_line(
502 dst,
503 &format!(
504 "remote MOD {} {port}",
505 wire_safe(host, "remote MOD host", mode)?
506 ),
507 );
508 }
509 OvpnCommand::Proxy(ProxyAction::None) => write_line(dst, "proxy NONE"),
510 OvpnCommand::Proxy(ProxyAction::Http {
511 ref host,
512 port,
513 non_cleartext_only,
514 }) => {
515 let nct = if non_cleartext_only { " nct" } else { "" };
516 write_line(
517 dst,
518 &format!(
519 "proxy HTTP {} {port}{nct}",
520 wire_safe(host, "proxy HTTP host", mode)?
521 ),
522 );
523 }
524 OvpnCommand::Proxy(ProxyAction::Socks { ref host, port }) => {
525 write_line(
526 dst,
527 &format!(
528 "proxy SOCKS {} {port}",
529 wire_safe(host, "proxy SOCKS host", mode)?
530 ),
531 );
532 }
533
534 OvpnCommand::ManagementPassword(ref pw) => {
538 write_line(dst, &wire_safe(pw.expose(), "management password", mode)?);
539 }
540
541 OvpnCommand::Exit => write_line(dst, "exit"),
543 OvpnCommand::Quit => write_line(dst, "quit"),
544
545 OvpnCommand::Raw(ref cmd) | OvpnCommand::RawMultiLine(ref cmd) => {
547 write_line(dst, &wire_safe(cmd, "raw command", mode)?);
548 }
549 }
550
551 Ok(())
552 }
553}
554
555fn write_line(dst: &mut BytesMut, s: &str) {
557 dst.reserve(s.len() + 1);
558 dst.put_slice(s.as_bytes());
559 dst.put_u8(b'\n');
560}
561
562fn write_block(
571 dst: &mut BytesMut,
572 header: &str,
573 lines: &[String],
574 mode: EncoderMode,
575) -> Result<(), io::Error> {
576 let total: usize = header.len() + 1 + lines.iter().map(|l| l.len() + 2).sum::<usize>() + 4;
577 dst.reserve(total);
578 dst.put_slice(header.as_bytes());
579 dst.put_u8(b'\n');
580 for line in lines {
581 let clean = wire_safe(line, "block body line", mode)?;
582 if *clean == *"END" {
583 match mode {
584 EncoderMode::Sanitize => {
585 dst.put_slice(b" END");
586 dst.put_u8(b'\n');
587 continue;
588 }
589 EncoderMode::Strict => {
590 return Err(io::Error::other(EncodeError::EndInBlockBody));
591 }
592 }
593 }
594 dst.put_slice(clean.as_bytes());
595 dst.put_u8(b'\n');
596 }
597 dst.put_slice(b"END\n");
598 Ok(())
599}
600
601impl Decoder for OvpnCodec {
604 type Item = OvpnMessage;
605 type Error = io::Error;
606
607 fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
608 loop {
609 const PW_PROMPT: &[u8] = b"ENTER PASSWORD:";
615
616 let Some(newline_pos) = src.iter().position(|&b| b == b'\n') else {
618 if src.starts_with(PW_PROMPT) {
625 let mut consume = PW_PROMPT.len();
626 if src.get(consume) == Some(&b'\r') {
627 consume += 1;
628 }
629 src.advance(consume);
630 return Ok(Some(OvpnMessage::PasswordPrompt));
631 }
632 return Ok(None); };
634
635 let line_bytes = src.split_to(newline_pos + 1);
637 let line = match std::str::from_utf8(&line_bytes) {
638 Ok(s) => s,
639 Err(e) => {
640 self.multi_line_buf = None;
643 self.client_notif = None;
644 self.expected = ResponseKind::SuccessOrError;
645 return Err(io::Error::new(io::ErrorKind::InvalidData, e));
646 }
647 }
648 .trim_end_matches(['\r', '\n'])
649 .to_string();
650
651 if line.is_empty()
658 && self.multi_line_buf.is_none()
659 && self.client_notif.is_none()
660 && !matches!(self.expected, ResponseKind::MultiLine)
661 {
662 continue;
663 }
664
665 if let Some(ref mut accum) = self.client_notif
674 && let Some(rest) = line.strip_prefix(">CLIENT:ENV,")
675 {
676 if rest == "END" {
677 let finished = self.client_notif.take().expect("guarded by if-let");
678 debug!(event = ?finished.event, cid = finished.cid, env_count = finished.env.len(), "decoded CLIENT notification");
679 return Ok(Some(OvpnMessage::Notification(Notification::Client {
680 event: finished.event,
681 cid: finished.cid,
682 kid: finished.kid,
683 env: finished.env,
684 })));
685 } else {
686 let (k, v) = rest
688 .split_once('=')
689 .map(|(k, v)| (k.to_string(), v.to_string()))
690 .unwrap_or_else(|| (rest.to_string(), String::new()));
691 check_accumulation_limit(
692 accum.env.len(),
693 self.max_client_env_entries,
694 "client ENV",
695 )?;
696 accum.env.push((k, v));
697 continue; }
699 }
700 if let Some(ref mut buf) = self.multi_line_buf {
705 if line == "END" {
706 let lines = self.multi_line_buf.take().expect("guarded by if-let");
707 debug!(line_count = lines.len(), "decoded multi-line response");
708 return Ok(Some(OvpnMessage::MultiLine(lines)));
709 }
710 if line.starts_with('>') {
715 if let Some(msg) = self.parse_notification(&line) {
716 return Ok(Some(msg));
717 }
718 continue;
721 }
722 check_accumulation_limit(
723 buf.len(),
724 self.max_multi_line_lines,
725 "multi-line response",
726 )?;
727 buf.push(line);
728 continue; }
730
731 if let Some(rest) = line.strip_prefix("SUCCESS:") {
737 return Ok(Some(OvpnMessage::Success(
738 rest.strip_prefix(' ').unwrap_or(rest).to_string(),
739 )));
740 }
741 if let Some(rest) = line.strip_prefix("ERROR:") {
742 return Ok(Some(OvpnMessage::Error(
743 rest.strip_prefix(' ').unwrap_or(rest).to_string(),
744 )));
745 }
746
747 if line == "ENTER PASSWORD:" {
749 return Ok(Some(OvpnMessage::PasswordPrompt));
750 }
751
752 if line.starts_with('>') {
754 if let Some(msg) = self.parse_notification(&line) {
755 return Ok(Some(msg));
756 }
757 continue;
759 }
760
761 match self.expected {
767 ResponseKind::MultiLine => {
768 if line == "END" {
769 return Ok(Some(OvpnMessage::MultiLine(Vec::new())));
771 }
772 self.multi_line_buf = Some(vec![line]);
773 continue; }
775 ResponseKind::SuccessOrError | ResponseKind::NoResponse => {
776 warn!(line = %line, "unrecognized line from server");
777 return Ok(Some(OvpnMessage::Unrecognized {
778 line,
779 kind: UnrecognizedKind::UnexpectedLine,
780 }));
781 }
782 }
783 }
784 }
785}
786
787impl OvpnCodec {
788 fn parse_notification(&mut self, line: &str) -> Option<OvpnMessage> {
792 let inner = &line[1..]; let Some((kind, payload)) = inner.split_once(':') else {
795 warn!(line = %line, "malformed notification (no colon)");
797 return Some(OvpnMessage::Unrecognized {
798 line: line.to_string(),
799 kind: UnrecognizedKind::MalformedNotification,
800 });
801 };
802
803 if kind == "INFO" {
806 return Some(OvpnMessage::Info(payload.to_string()));
807 }
808
809 if kind == "CLIENT" {
811 let (event, args) = payload
812 .split_once(',')
813 .map(|(e, a)| (e.to_string(), a.to_string()))
814 .unwrap_or_else(|| (payload.to_string(), String::new()));
815
816 if event == "ADDRESS" {
818 let mut parts = args.splitn(3, ',');
819 let cid = parts
820 .next()
821 .and_then(|s| parse_field(s, "client address cid"))
822 .unwrap_or(0);
823 let addr = parts.next().unwrap_or("").to_string();
824 let primary = parts.next() == Some("1");
825 return Some(OvpnMessage::Notification(Notification::ClientAddress {
826 cid,
827 addr,
828 primary,
829 }));
830 }
831
832 let mut id_parts = args.splitn(3, ',');
836 let cid = id_parts
837 .next()
838 .and_then(|s| parse_field(s, "client cid"))
839 .unwrap_or(0);
840 let kid = id_parts.next().and_then(|s| parse_field(s, "client kid"));
841
842 let parsed_event = if event == "CR_RESPONSE" {
843 let response = id_parts.next().unwrap_or("").to_string();
844 ClientEvent::CrResponse(response)
845 } else {
846 event
847 .parse()
848 .inspect_err(|error| warn!(%error, "unknown client event"))
849 .unwrap_or_else(|_| ClientEvent::Unknown(event.clone()))
850 };
851
852 self.client_notif = Some(ClientNotifAccum {
854 event: parsed_event,
855 cid,
856 kid,
857 env: Vec::new(),
858 });
859 return None; }
861
862 let notification = match kind {
864 "STATE" => parse_state(payload),
865 "BYTECOUNT" => parse_bytecount(payload),
866 "BYTECOUNT_CLI" => parse_bytecount_cli(payload),
867 "LOG" => parse_log(payload),
868 "ECHO" => parse_echo(payload),
869 "HOLD" => Some(Notification::Hold {
870 text: payload.to_string(),
871 }),
872 "FATAL" => Some(Notification::Fatal {
873 message: payload.to_string(),
874 }),
875 "PKCS11ID-COUNT" => parse_pkcs11id_count(payload),
876 "NEED-OK" => parse_need_ok(payload),
877 "NEED-STR" => parse_need_str(payload),
878 "RSA_SIGN" => Some(Notification::RsaSign {
879 data: payload.to_string(),
880 }),
881 "REMOTE" => parse_remote(payload),
882 "PROXY" => parse_proxy(payload),
883 "PASSWORD" => parse_password(payload),
884 "PKCS11ID-ENTRY" => {
885 return parse_pkcs11id_entry_notif(payload).or_else(|| {
886 Some(OvpnMessage::Notification(Notification::Simple {
887 kind: kind.to_string(),
888 payload: payload.to_string(),
889 }))
890 });
891 }
892 _ => None,
893 };
894
895 Some(OvpnMessage::Notification(notification.unwrap_or(
896 Notification::Simple {
897 kind: kind.to_string(),
898 payload: payload.to_string(),
899 },
900 )))
901 }
902}
903
904fn parse_optional_port(s: &str) -> Option<u16> {
915 let s = s.trim();
916 if s.is_empty() {
917 return None;
918 }
919 s.parse()
920 .inspect_err(|error| warn!(%error, port = s, "non-numeric port in STATE notification"))
921 .ok()
922}
923
924fn parse_field<T: std::str::FromStr>(s: &str, field: &str) -> Option<T>
929where
930 T::Err: std::fmt::Display,
931{
932 s.parse()
933 .inspect_err(|error| warn!(%error, value = s, field, "failed to parse notification field"))
934 .ok()
935}
936
937fn parse_state(payload: &str) -> Option<Notification> {
938 let mut parts = payload.splitn(9, ',');
942 let timestamp = parse_field(parts.next()?, "state timestamp")?;
943 let state_str = parts.next()?;
944 let name = state_str
945 .parse()
946 .inspect_err(|error| warn!(%error, "unknown OpenVPN state"))
947 .unwrap_or_else(|_| OpenVpnState::Unknown(state_str.to_string()));
948 let description = parts.next()?.to_string();
949 let local_ip = parts.next()?.to_string();
950 let remote_ip = parts.next()?.to_string();
951 let remote_port = parse_optional_port(parts.next().unwrap_or(""));
952 let local_addr = parts.next().unwrap_or("").to_string();
953 let local_port = parse_optional_port(parts.next().unwrap_or(""));
954 let local_ipv6 = parts.next().unwrap_or("").to_string();
955 Some(Notification::State {
956 timestamp,
957 name,
958 description,
959 local_ip,
960 remote_ip,
961 remote_port,
962 local_addr,
963 local_port,
964 local_ipv6,
965 })
966}
967
968fn parse_bytecount(payload: &str) -> Option<Notification> {
969 let (a, b) = payload.split_once(',')?;
970 Some(Notification::ByteCount {
971 bytes_in: parse_field(a, "bytecount bytes_in")?,
972 bytes_out: parse_field(b, "bytecount bytes_out")?,
973 })
974}
975
976fn parse_bytecount_cli(payload: &str) -> Option<Notification> {
977 let mut parts = payload.splitn(3, ',');
978 let cid = parse_field(parts.next()?, "bytecount_cli cid")?;
979 let bytes_in = parse_field(parts.next()?, "bytecount_cli bytes_in")?;
980 let bytes_out = parse_field(parts.next()?, "bytecount_cli bytes_out")?;
981 Some(Notification::ByteCountCli {
982 cid,
983 bytes_in,
984 bytes_out,
985 })
986}
987
988fn parse_log(payload: &str) -> Option<Notification> {
989 let (ts_str, rest) = payload.split_once(',')?;
990 let timestamp = parse_field(ts_str, "log timestamp")?;
991 let (level_str, message) = rest.split_once(',')?;
992 Some(Notification::Log {
993 timestamp,
994 level: level_str
995 .parse()
996 .inspect_err(|error| warn!(%error, "unknown log level"))
997 .unwrap_or_else(|_| LogLevel::Unknown(level_str.to_string())),
998 message: message.to_string(),
999 })
1000}
1001
1002fn parse_echo(payload: &str) -> Option<Notification> {
1003 let (ts_str, param) = payload.split_once(',')?;
1004 let timestamp = parse_field(ts_str, "echo timestamp")?;
1005 Some(Notification::Echo {
1006 timestamp,
1007 param: param.to_string(),
1008 })
1009}
1010
1011fn parse_pkcs11id_count(payload: &str) -> Option<Notification> {
1012 let count = parse_field(payload.trim(), "pkcs11id_count")?;
1013 Some(Notification::Pkcs11IdCount { count })
1014}
1015
1016fn parse_pkcs11id_entry_notif(payload: &str) -> Option<OvpnMessage> {
1019 let rest = payload.strip_prefix('\'')?;
1020 let (index, rest) = rest.split_once("', ID:'")?;
1021 let (id, rest) = rest.split_once("', BLOB:'")?;
1022 let blob = rest.strip_suffix('\'')?;
1023 Some(OvpnMessage::Pkcs11IdEntry {
1024 index: index.to_string(),
1025 id: id.to_string(),
1026 blob: blob.to_string(),
1027 })
1028}
1029
1030fn parse_need_ok(payload: &str) -> Option<Notification> {
1032 let rest = payload.strip_prefix("Need '")?;
1034 let (name, rest) = rest.split_once('\'')?;
1035 let msg = rest.split_once("MSG:")?.1;
1036 Some(Notification::NeedOk {
1037 name: name.to_string(),
1038 message: msg.to_string(),
1039 })
1040}
1041
1042fn parse_need_str(payload: &str) -> Option<Notification> {
1044 let rest = payload.strip_prefix("Need '")?;
1045 let (name, rest) = rest.split_once('\'')?;
1046 let msg = rest.split_once("MSG:")?.1;
1047 Some(Notification::NeedStr {
1048 name: name.to_string(),
1049 message: msg.to_string(),
1050 })
1051}
1052
1053fn parse_remote(payload: &str) -> Option<Notification> {
1054 let mut parts = payload.splitn(3, ',');
1055 let host = parts.next()?.to_string();
1056 let port = parse_field(parts.next()?, "remote port")?;
1057 let proto_str = parts.next()?;
1058 let protocol = proto_str
1059 .parse()
1060 .inspect_err(|error| warn!(%error, "unknown transport protocol"))
1061 .unwrap_or_else(|_| TransportProtocol::Unknown(proto_str.to_string()));
1062 Some(Notification::Remote {
1063 host,
1064 port,
1065 protocol,
1066 })
1067}
1068
1069fn parse_proxy(payload: &str) -> Option<Notification> {
1070 let mut parts = payload.splitn(3, ',');
1072 let index = parse_field(parts.next()?, "proxy index")?;
1073 let pt_str = parts.next()?;
1074 let proxy_type = pt_str
1075 .parse()
1076 .inspect_err(|error| warn!(%error, "unknown proxy type"))
1077 .unwrap_or_else(|_| TransportProtocol::Unknown(pt_str.to_string()));
1078 let host = parts.next()?.to_string();
1079 Some(Notification::Proxy {
1080 index,
1081 proxy_type,
1082 host,
1083 })
1084}
1085
1086use crate::message::PasswordNotification;
1087
1088use crate::auth::AuthType;
1089
1090fn parse_auth_type(s: &str) -> AuthType {
1092 s.parse()
1093 .inspect_err(|error| warn!(%error, "unknown auth type"))
1094 .unwrap_or_else(|_| AuthType::Unknown(s.to_string()))
1095}
1096
1097fn parse_password(payload: &str) -> Option<Notification> {
1098 if let Some(token) = payload.strip_prefix("Auth-Token:") {
1101 return Some(Notification::Password(PasswordNotification::AuthToken {
1102 token: Redacted::new(token),
1103 }));
1104 }
1105
1106 if let Some(rest) = payload.strip_prefix("Verification Failed: '") {
1109 if let Some((auth_part, crv1_part)) = rest.split_once("' ['CRV1:") {
1111 debug_assert_eq!(auth_part, "Auth", "CRV1 auth type should always be 'Auth'");
1112 let crv1_data = crv1_part.strip_suffix("']")?;
1113 let mut parts = crv1_data.splitn(4, ':');
1114 let flags = parts.next()?.to_string();
1115 let state_id = parts.next()?.to_string();
1116 let username_b64 = parts.next()?.to_string();
1117 let challenge = parts.next()?.to_string();
1118 return Some(Notification::Password(
1119 PasswordNotification::DynamicChallenge {
1120 flags,
1121 state_id,
1122 username_b64,
1123 challenge,
1124 },
1125 ));
1126 }
1127 let auth_type = rest.strip_suffix('\'')?;
1129 return Some(Notification::Password(
1130 PasswordNotification::VerificationFailed {
1131 auth_type: parse_auth_type(auth_type),
1132 },
1133 ));
1134 }
1135
1136 let rest = payload.strip_prefix("Need '")?;
1139 let (auth_type_str, rest) = rest.split_once('\'')?;
1140 let rest = rest.trim_start();
1141
1142 if let Some(after_up) = rest.strip_prefix("username/password") {
1143 let after_up = after_up.trim_start();
1144
1145 if let Some(sc) = after_up.strip_prefix("SC:") {
1148 let (flag_str, challenge) = sc.split_once(',')?;
1149 let flags: u32 = parse_field(flag_str, "static challenge flags")?;
1150 return Some(Notification::Password(
1151 PasswordNotification::StaticChallenge {
1152 echo: flags & 1 != 0,
1153 response_concat: flags & 2 != 0,
1154 challenge: challenge.to_string(),
1155 },
1156 ));
1157 }
1158
1159 return Some(Notification::Password(PasswordNotification::NeedAuth {
1161 auth_type: parse_auth_type(auth_type_str),
1162 }));
1163 }
1164
1165 if rest.starts_with("password") {
1167 return Some(Notification::Password(PasswordNotification::NeedPassword {
1168 auth_type: parse_auth_type(auth_type_str),
1169 }));
1170 }
1171
1172 None }
1174
1175#[cfg(test)]
1176mod tests {
1177 use super::*;
1178 use crate::auth::AuthType;
1179 use crate::client_event::ClientEvent;
1180 use crate::message::PasswordNotification;
1181 use crate::signal::Signal;
1182 use crate::status_format::StatusFormat;
1183 use crate::stream_mode::StreamMode;
1184 use bytes::BytesMut;
1185 use tokio_util::codec::{Decoder, Encoder};
1186
1187 fn encode_to_string(cmd: OvpnCommand) -> String {
1189 let mut codec = OvpnCodec::new();
1190 let mut buf = BytesMut::new();
1191 codec.encode(cmd, &mut buf).unwrap();
1192 String::from_utf8(buf.to_vec()).unwrap()
1193 }
1194
1195 fn decode_all(input: &str) -> Vec<OvpnMessage> {
1197 let mut codec = OvpnCodec::new();
1198 let mut buf = BytesMut::from(input);
1199 let mut msgs = Vec::new();
1200 while let Some(msg) = codec.decode(&mut buf).unwrap() {
1201 msgs.push(msg);
1202 }
1203 msgs
1204 }
1205
1206 fn encode_then_decode(cmd: OvpnCommand, response: &str) -> Vec<OvpnMessage> {
1208 let mut codec = OvpnCodec::new();
1209 let mut enc_buf = BytesMut::new();
1210 codec.encode(cmd, &mut enc_buf).unwrap();
1211 let mut dec_buf = BytesMut::from(response);
1212 let mut msgs = Vec::new();
1213 while let Some(msg) = codec.decode(&mut dec_buf).unwrap() {
1214 msgs.push(msg);
1215 }
1216 msgs
1217 }
1218
1219 #[test]
1222 fn encode_status_v1() {
1223 assert_eq!(
1224 encode_to_string(OvpnCommand::Status(StatusFormat::V1)),
1225 "status\n"
1226 );
1227 }
1228
1229 #[test]
1230 fn encode_status_v3() {
1231 assert_eq!(
1232 encode_to_string(OvpnCommand::Status(StatusFormat::V3)),
1233 "status 3\n"
1234 );
1235 }
1236
1237 #[test]
1238 fn encode_signal() {
1239 assert_eq!(
1240 encode_to_string(OvpnCommand::Signal(Signal::SigUsr1)),
1241 "signal SIGUSR1\n"
1242 );
1243 }
1244
1245 #[test]
1246 fn encode_state_on_all() {
1247 assert_eq!(
1248 encode_to_string(OvpnCommand::StateStream(StreamMode::OnAll)),
1249 "state on all\n"
1250 );
1251 }
1252
1253 #[test]
1254 fn encode_state_recent() {
1255 assert_eq!(
1256 encode_to_string(OvpnCommand::StateStream(StreamMode::Recent(5))),
1257 "state 5\n"
1258 );
1259 }
1260
1261 #[test]
1262 fn encode_password_escaping() {
1263 let wire = encode_to_string(OvpnCommand::Password {
1266 auth_type: AuthType::PrivateKey,
1267 value: r#"foo\"bar"#.into(),
1268 });
1269 assert_eq!(wire, "password \"Private Key\" \"foo\\\\\\\"bar\"\n");
1270 }
1271
1272 #[test]
1273 fn encode_password_simple() {
1274 let wire = encode_to_string(OvpnCommand::Password {
1275 auth_type: AuthType::Auth,
1276 value: "hunter2".into(),
1277 });
1278 assert_eq!(wire, "password \"Auth\" \"hunter2\"\n");
1279 }
1280
1281 #[test]
1282 fn encode_client_auth_with_config() {
1283 let wire = encode_to_string(OvpnCommand::ClientAuth {
1284 cid: 42,
1285 kid: 0,
1286 config_lines: vec![
1287 "push \"route 10.0.0.0 255.255.0.0\"".to_string(),
1288 "push \"dhcp-option DNS 10.0.0.1\"".to_string(),
1289 ],
1290 });
1291 assert_eq!(
1292 wire,
1293 "client-auth 42 0\n\
1294 push \"route 10.0.0.0 255.255.0.0\"\n\
1295 push \"dhcp-option DNS 10.0.0.1\"\n\
1296 END\n"
1297 );
1298 }
1299
1300 #[test]
1301 fn encode_client_auth_empty_config() {
1302 let wire = encode_to_string(OvpnCommand::ClientAuth {
1303 cid: 1,
1304 kid: 0,
1305 config_lines: vec![],
1306 });
1307 assert_eq!(wire, "client-auth 1 0\nEND\n");
1308 }
1309
1310 #[test]
1311 fn encode_client_deny_with_client_reason() {
1312 let wire = encode_to_string(OvpnCommand::ClientDeny {
1313 cid: 5,
1314 kid: 0,
1315 reason: "cert revoked".to_string(),
1316 client_reason: Some("Your access has been revoked.".to_string()),
1317 });
1318 assert_eq!(
1319 wire,
1320 "client-deny 5 0 \"cert revoked\" \"Your access has been revoked.\"\n"
1321 );
1322 }
1323
1324 #[test]
1325 fn encode_rsa_sig() {
1326 let wire = encode_to_string(OvpnCommand::RsaSig {
1327 base64_lines: vec!["AAAA".to_string(), "BBBB".to_string()],
1328 });
1329 assert_eq!(wire, "rsa-sig\nAAAA\nBBBB\nEND\n");
1330 }
1331
1332 #[test]
1333 fn encode_remote_modify() {
1334 let wire = encode_to_string(OvpnCommand::Remote(RemoteAction::Modify {
1335 host: "vpn.example.com".to_string(),
1336 port: 1234,
1337 }));
1338 assert_eq!(wire, "remote MOD vpn.example.com 1234\n");
1339 }
1340
1341 #[test]
1342 fn encode_pk_sig() {
1343 let wire = encode_to_string(OvpnCommand::PkSig {
1344 base64_lines: vec!["AAAA".to_string(), "BBBB".to_string()],
1345 });
1346 assert_eq!(wire, "pk-sig\nAAAA\nBBBB\nEND\n");
1347 }
1348
1349 #[test]
1350 fn encode_env_filter() {
1351 assert_eq!(
1352 encode_to_string(OvpnCommand::EnvFilter(2)),
1353 "env-filter 2\n"
1354 );
1355 }
1356
1357 #[test]
1358 fn encode_remote_entry_count() {
1359 assert_eq!(
1360 encode_to_string(OvpnCommand::RemoteEntryCount),
1361 "remote-entry-count\n"
1362 );
1363 }
1364
1365 #[test]
1366 fn encode_remote_entry_get() {
1367 use crate::command::RemoteEntryRange;
1368 assert_eq!(
1369 encode_to_string(OvpnCommand::RemoteEntryGet(RemoteEntryRange::Single(0))),
1370 "remote-entry-get 0\n"
1371 );
1372 assert_eq!(
1373 encode_to_string(OvpnCommand::RemoteEntryGet(RemoteEntryRange::Range {
1374 from: 0,
1375 to: 3
1376 })),
1377 "remote-entry-get 0 3\n"
1378 );
1379 assert_eq!(
1380 encode_to_string(OvpnCommand::RemoteEntryGet(RemoteEntryRange::All)),
1381 "remote-entry-get all\n"
1382 );
1383 }
1384
1385 #[test]
1386 fn encode_push_update_broad() {
1387 let wire = encode_to_string(OvpnCommand::PushUpdateBroad {
1388 options: "route 10.0.0.0".to_string(),
1389 });
1390 assert_eq!(wire, "push-update-broad \"route 10.0.0.0\"\n");
1391 }
1392
1393 #[test]
1394 fn encode_push_update_cid() {
1395 let wire = encode_to_string(OvpnCommand::PushUpdateCid {
1396 cid: 42,
1397 options: "route 10.0.0.0".to_string(),
1398 });
1399 assert_eq!(wire, "push-update-cid 42 \"route 10.0.0.0\"\n");
1400 }
1401
1402 #[test]
1403 fn encode_proxy_http_nct() {
1404 let wire = encode_to_string(OvpnCommand::Proxy(ProxyAction::Http {
1405 host: "proxy.local".to_string(),
1406 port: 8080,
1407 non_cleartext_only: true,
1408 }));
1409 assert_eq!(wire, "proxy HTTP proxy.local 8080 nct\n");
1410 }
1411
1412 #[test]
1413 fn encode_needok() {
1414 use crate::need_ok::NeedOkResponse;
1415 let wire = encode_to_string(OvpnCommand::NeedOk {
1416 name: "token-insertion-request".to_string(),
1417 response: NeedOkResponse::Ok,
1418 });
1419 assert_eq!(wire, "needok token-insertion-request ok\n");
1420 }
1421
1422 #[test]
1423 fn encode_needstr() {
1424 let wire = encode_to_string(OvpnCommand::NeedStr {
1425 name: "name".to_string(),
1426 value: "John".to_string(),
1427 });
1428 assert_eq!(wire, "needstr name \"John\"\n");
1429 }
1430
1431 #[test]
1432 fn encode_forget_passwords() {
1433 assert_eq!(
1434 encode_to_string(OvpnCommand::ForgetPasswords),
1435 "forget-passwords\n"
1436 );
1437 }
1438
1439 #[test]
1440 fn encode_hold_query() {
1441 assert_eq!(encode_to_string(OvpnCommand::HoldQuery), "hold\n");
1442 }
1443
1444 #[test]
1445 fn encode_echo_on_all() {
1446 assert_eq!(
1447 encode_to_string(OvpnCommand::Echo(StreamMode::OnAll)),
1448 "echo on all\n"
1449 );
1450 }
1451
1452 #[test]
1455 fn decode_success() {
1456 let msgs = decode_all("SUCCESS: pid=12345\n");
1457 assert_eq!(msgs.len(), 1);
1458 assert!(matches!(&msgs[0], OvpnMessage::Success(s) if s == "pid=12345"));
1459 }
1460
1461 #[test]
1462 fn decode_success_bare() {
1463 let msgs = decode_all("SUCCESS:\n");
1465 assert_eq!(msgs.len(), 1);
1466 assert!(matches!(&msgs[0], OvpnMessage::Success(s) if s.is_empty()));
1467 }
1468
1469 #[test]
1470 fn decode_error() {
1471 let msgs = decode_all("ERROR: unknown command\n");
1472 assert_eq!(msgs.len(), 1);
1473 assert!(matches!(&msgs[0], OvpnMessage::Error(s) if s == "unknown command"));
1474 }
1475
1476 #[test]
1477 fn decode_info_notification() {
1478 let msgs = decode_all(">INFO:OpenVPN Management Interface Version 5\n");
1479 assert_eq!(msgs.len(), 1);
1480 assert!(matches!(
1481 &msgs[0],
1482 OvpnMessage::Info(s) if s == "OpenVPN Management Interface Version 5"
1483 ));
1484 }
1485
1486 #[test]
1487 fn decode_state_notification() {
1488 let msgs = decode_all(">STATE:1234567890,CONNECTED,SUCCESS,,10.0.0.1\n");
1489 assert_eq!(msgs.len(), 1);
1490 assert!(matches!(
1491 &msgs[0],
1492 OvpnMessage::Notification(Notification::State {
1493 timestamp: 1234567890,
1494 name: OpenVpnState::Connected,
1495 description,
1496 local_ip,
1497 remote_ip,
1498 ..
1499 }) if description == "SUCCESS" && local_ip.is_empty() && remote_ip == "10.0.0.1"
1500 ));
1501 }
1502
1503 #[test]
1504 fn decode_multiline_with_command_tracking() {
1505 let msgs = encode_then_decode(
1509 OvpnCommand::Status(StatusFormat::V1),
1510 "OpenVPN CLIENT LIST\nCommon Name,Real Address\ntest,1.2.3.4:1234\nEND\n",
1511 );
1512 assert_eq!(msgs.len(), 1);
1513 assert!(matches!(
1514 &msgs[0],
1515 OvpnMessage::MultiLine(lines)
1516 if lines.len() == 3
1517 && lines[0] == "OpenVPN CLIENT LIST"
1518 && lines[2] == "test,1.2.3.4:1234"
1519 ));
1520 }
1521
1522 #[test]
1523 fn decode_hold_query_success() {
1524 let msgs = encode_then_decode(OvpnCommand::HoldQuery, "SUCCESS: hold=0\n");
1526 assert_eq!(msgs.len(), 1);
1527 assert!(matches!(&msgs[0], OvpnMessage::Success(s) if s == "hold=0"));
1528 }
1529
1530 #[test]
1531 fn decode_bare_state_multiline() {
1532 let msgs = encode_then_decode(
1534 OvpnCommand::State,
1535 "1234567890,CONNECTED,SUCCESS,,10.0.0.1,,,,\nEND\n",
1536 );
1537 assert_eq!(msgs.len(), 1);
1538 assert!(matches!(
1539 &msgs[0],
1540 OvpnMessage::MultiLine(lines)
1541 if lines.len() == 1 && lines[0].starts_with("1234567890")
1542 ));
1543 }
1544
1545 #[test]
1546 fn decode_notification_during_multiline() {
1547 let msgs = encode_then_decode(
1550 OvpnCommand::Status(StatusFormat::V1),
1551 "header line\n>BYTECOUNT:1000,2000\ndata line\nEND\n",
1552 );
1553 assert_eq!(msgs.len(), 2);
1554 assert!(matches!(
1556 &msgs[0],
1557 OvpnMessage::Notification(Notification::ByteCount {
1558 bytes_in: 1000,
1559 bytes_out: 2000
1560 })
1561 ));
1562 assert!(matches!(
1564 &msgs[1],
1565 OvpnMessage::MultiLine(lines) if lines == &["header line", "data line"]
1566 ));
1567 }
1568
1569 #[test]
1570 fn decode_client_connect_multiline_notification() {
1571 let input = "\
1572 >CLIENT:CONNECT,0,1\n\
1573 >CLIENT:ENV,untrusted_ip=1.2.3.4\n\
1574 >CLIENT:ENV,common_name=TestClient\n\
1575 >CLIENT:ENV,END\n";
1576 let msgs = decode_all(input);
1577 assert_eq!(msgs.len(), 1);
1578 assert!(matches!(
1579 &msgs[0],
1580 OvpnMessage::Notification(Notification::Client {
1581 event: ClientEvent::Connect,
1582 cid: 0,
1583 kid: Some(1),
1584 env,
1585 }) if env.len() == 2
1586 && env[0] == ("untrusted_ip".to_string(), "1.2.3.4".to_string())
1587 && env[1] == ("common_name".to_string(), "TestClient".to_string())
1588 ));
1589 }
1590
1591 #[test]
1592 fn decode_client_address_single_line() {
1593 let msgs = decode_all(">CLIENT:ADDRESS,3,10.0.0.5,1\n");
1594 assert_eq!(msgs.len(), 1);
1595 assert!(matches!(
1596 &msgs[0],
1597 OvpnMessage::Notification(Notification::ClientAddress {
1598 cid: 3,
1599 addr,
1600 primary: true,
1601 }) if addr == "10.0.0.5"
1602 ));
1603 }
1604
1605 #[test]
1606 fn decode_client_disconnect() {
1607 let input = "\
1608 >CLIENT:DISCONNECT,5\n\
1609 >CLIENT:ENV,bytes_received=12345\n\
1610 >CLIENT:ENV,bytes_sent=67890\n\
1611 >CLIENT:ENV,END\n";
1612 let msgs = decode_all(input);
1613 assert_eq!(msgs.len(), 1);
1614 assert!(matches!(
1615 &msgs[0],
1616 OvpnMessage::Notification(Notification::Client {
1617 event: ClientEvent::Disconnect,
1618 cid: 5,
1619 kid: None,
1620 env,
1621 }) if env.len() == 2
1622 ));
1623 }
1624
1625 #[test]
1626 fn decode_password_prompt_no_newline_with_cr() {
1627 let msgs = decode_all("ENTER PASSWORD:\r");
1631 assert_eq!(msgs.len(), 1);
1632 assert_eq!(msgs[0], OvpnMessage::PasswordPrompt);
1633 }
1634
1635 #[test]
1636 fn decode_password_prompt_no_newline_without_cr() {
1637 let msgs = decode_all("ENTER PASSWORD:");
1638 assert_eq!(msgs.len(), 1);
1639 assert_eq!(msgs[0], OvpnMessage::PasswordPrompt);
1640 }
1641
1642 #[test]
1643 fn decode_password_notification() {
1644 let msgs = decode_all(">PASSWORD:Need 'Auth' username/password\n");
1645 assert_eq!(msgs.len(), 1);
1646 assert!(matches!(
1647 &msgs[0],
1648 OvpnMessage::Notification(Notification::Password(PasswordNotification::NeedAuth {
1649 auth_type: AuthType::Auth,
1650 }))
1651 ));
1652 }
1653
1654 #[test]
1655 fn quote_and_escape_special_chars() {
1656 assert_eq!(quote_and_escape(r#"foo"bar"#), r#""foo\"bar""#);
1657 assert_eq!(quote_and_escape(r"a\b"), r#""a\\b""#);
1658 assert_eq!(quote_and_escape("simple"), r#""simple""#);
1659 }
1660
1661 #[test]
1662 fn decode_empty_multiline() {
1663 let msgs = encode_then_decode(OvpnCommand::Status(StatusFormat::V1), "END\n");
1665 assert_eq!(msgs.len(), 1);
1666 assert!(matches!(&msgs[0], OvpnMessage::MultiLine(lines) if lines.is_empty()));
1667 }
1668
1669 #[test]
1670 fn decode_need_ok_notification() {
1671 let msgs = decode_all(
1672 ">NEED-OK:Need 'token-insertion-request' confirmation MSG:Please insert your token\n",
1673 );
1674 assert_eq!(msgs.len(), 1);
1675 assert!(matches!(
1676 &msgs[0],
1677 OvpnMessage::Notification(Notification::NeedOk { name, message })
1678 if name == "token-insertion-request" && message == "Please insert your token"
1679 ));
1680 }
1681
1682 #[test]
1683 fn decode_hold_notification() {
1684 let msgs = decode_all(">HOLD:Waiting for hold release\n");
1685 assert_eq!(msgs.len(), 1);
1686 assert!(matches!(
1687 &msgs[0],
1688 OvpnMessage::Notification(Notification::Hold { text })
1689 if text == "Waiting for hold release"
1690 ));
1691 }
1692
1693 #[test]
1696 fn encode_raw_multiline() {
1697 assert_eq!(
1698 encode_to_string(OvpnCommand::RawMultiLine("custom-cmd arg".to_string())),
1699 "custom-cmd arg\n"
1700 );
1701 }
1702
1703 #[test]
1704 fn raw_multiline_expects_multiline_response() {
1705 let msgs = encode_then_decode(
1706 OvpnCommand::RawMultiLine("custom".to_string()),
1707 "line1\nline2\nEND\n",
1708 );
1709 assert_eq!(msgs.len(), 1);
1710 assert!(matches!(
1711 &msgs[0],
1712 OvpnMessage::MultiLine(lines) if lines == &["line1", "line2"]
1713 ));
1714 }
1715
1716 #[test]
1717 fn raw_multiline_sanitizes_newlines() {
1718 let wire = encode_to_string(OvpnCommand::RawMultiLine("cmd\ninjected".to_string()));
1720 assert_eq!(wire, "cmdinjected\n");
1721 }
1722
1723 #[test]
1724 fn raw_multiline_strict_rejects_newlines() {
1725 let mut codec = OvpnCodec::new().with_encoder_mode(EncoderMode::Strict);
1726 let mut buf = BytesMut::new();
1727 let result = codec.encode(
1728 OvpnCommand::RawMultiLine("cmd\ninjected".to_string()),
1729 &mut buf,
1730 );
1731 assert!(result.is_err());
1732 }
1733
1734 #[test]
1737 #[should_panic(expected = "mid-accumulation")]
1738 fn encode_during_multiline_accumulation_panics() {
1739 let mut codec = OvpnCodec::new();
1740 let mut buf = BytesMut::new();
1741 codec
1743 .encode(OvpnCommand::Status(StatusFormat::V1), &mut buf)
1744 .unwrap();
1745 let mut dec = BytesMut::from("header line\n");
1747 let _ = codec.decode(&mut dec); codec.encode(OvpnCommand::Pid, &mut buf).unwrap();
1750 }
1751
1752 #[test]
1753 #[should_panic(expected = "mid-accumulation")]
1754 fn encode_during_client_notif_accumulation_panics() {
1755 let mut codec = OvpnCodec::new();
1756 let mut buf = BytesMut::new();
1757 let mut dec = BytesMut::from(">CLIENT:CONNECT,0,1\n");
1759 let _ = codec.decode(&mut dec);
1760 codec.encode(OvpnCommand::Pid, &mut buf).unwrap();
1762 }
1763
1764 #[test]
1767 fn unlimited_accumulation_default() {
1768 let mut codec = OvpnCodec::new();
1769 let mut enc = BytesMut::new();
1770 codec
1771 .encode(OvpnCommand::Status(StatusFormat::V1), &mut enc)
1772 .unwrap();
1773 let mut data = String::new();
1775 for i in 0..500 {
1776 data.push_str(&format!("line {i}\n"));
1777 }
1778 data.push_str("END\n");
1779 let mut dec = BytesMut::from(data.as_str());
1780 let mut msgs = Vec::new();
1781 while let Some(msg) = codec.decode(&mut dec).unwrap() {
1782 msgs.push(msg);
1783 }
1784 assert_eq!(msgs.len(), 1);
1785 assert!(matches!(
1786 &msgs[0],
1787 OvpnMessage::MultiLine(lines) if lines.len() == 500
1788 ));
1789 }
1790
1791 #[test]
1792 fn multi_line_limit_exceeded() {
1793 let mut codec = OvpnCodec::new().with_max_multi_line_lines(AccumulationLimit::Max(3));
1794 let mut enc = BytesMut::new();
1795 codec
1796 .encode(OvpnCommand::Status(StatusFormat::V1), &mut enc)
1797 .unwrap();
1798 let mut dec = BytesMut::from("a\nb\nc\nd\nEND\n");
1799 let result = loop {
1800 match codec.decode(&mut dec) {
1801 Ok(Some(msg)) => break Ok(msg),
1802 Ok(None) => continue,
1803 Err(e) => break Err(e),
1804 }
1805 };
1806 assert!(result.is_err(), "expected error when limit exceeded");
1807 let err = result.unwrap_err();
1808 assert!(
1809 err.to_string().contains("multi-line response"),
1810 "error should mention multi-line: {err}"
1811 );
1812 }
1813
1814 #[test]
1815 fn multi_line_limit_exact_boundary_passes() {
1816 let mut codec = OvpnCodec::new().with_max_multi_line_lines(AccumulationLimit::Max(3));
1817 let mut enc = BytesMut::new();
1818 codec
1819 .encode(OvpnCommand::Status(StatusFormat::V1), &mut enc)
1820 .unwrap();
1821 let mut dec = BytesMut::from("a\nb\nc\nEND\n");
1823 let mut msgs = Vec::new();
1824 while let Some(msg) = codec.decode(&mut dec).unwrap() {
1825 msgs.push(msg);
1826 }
1827 assert_eq!(msgs.len(), 1);
1828 assert!(matches!(
1829 &msgs[0],
1830 OvpnMessage::MultiLine(lines) if lines.len() == 3
1831 ));
1832 }
1833
1834 #[test]
1835 fn client_env_limit_exceeded() {
1836 let mut codec = OvpnCodec::new().with_max_client_env_entries(AccumulationLimit::Max(2));
1837 let mut dec = BytesMut::from(
1838 ">CLIENT:CONNECT,0,1\n\
1839 >CLIENT:ENV,a=1\n\
1840 >CLIENT:ENV,b=2\n\
1841 >CLIENT:ENV,c=3\n\
1842 >CLIENT:ENV,END\n",
1843 );
1844 let result = loop {
1845 match codec.decode(&mut dec) {
1846 Ok(Some(msg)) => break Ok(msg),
1847 Ok(None) => continue,
1848 Err(e) => break Err(e),
1849 }
1850 };
1851 assert!(
1852 result.is_err(),
1853 "expected error when client ENV limit exceeded"
1854 );
1855 let err = result.unwrap_err();
1856 assert!(
1857 err.to_string().contains("client ENV"),
1858 "error should mention client ENV: {err}"
1859 );
1860 }
1861
1862 #[test]
1865 fn utf8_error_resets_multiline_state() {
1866 let mut codec = OvpnCodec::new();
1867 let mut enc = BytesMut::new();
1868 codec
1869 .encode(OvpnCommand::Status(StatusFormat::V1), &mut enc)
1870 .unwrap();
1871 let mut dec = BytesMut::from("header\n");
1873 assert!(codec.decode(&mut dec).unwrap().is_none());
1874 dec.extend_from_slice(b"bad \xff line\n");
1876 assert!(codec.decode(&mut dec).is_err());
1877 dec.extend_from_slice(b"SUCCESS: recovered\n");
1880 let msg = codec
1881 .decode(&mut dec)
1882 .unwrap()
1883 .expect("should produce a message");
1884 assert!(
1885 matches!(&msg, OvpnMessage::Success(s) if s.contains("recovered")),
1886 "expected Success containing 'recovered', got {msg:?}"
1887 );
1888 }
1889
1890 #[test]
1891 fn utf8_error_resets_client_notif_state() {
1892 let mut codec = OvpnCodec::new();
1893 let mut dec = BytesMut::from(">CLIENT:CONNECT,0,1\n");
1895 assert!(codec.decode(&mut dec).unwrap().is_none());
1896 dec.extend_from_slice(b">CLIENT:ENV,\xff\n");
1898 assert!(codec.decode(&mut dec).is_err());
1899 dec.extend_from_slice(b"SUCCESS: ok\n");
1901 let msg = codec
1902 .decode(&mut dec)
1903 .unwrap()
1904 .expect("should produce a message");
1905 assert!(
1906 matches!(&msg, OvpnMessage::Success(_)),
1907 "expected Success after UTF-8 reset, got {msg:?}"
1908 );
1909 }
1910}