Skip to main content

openvpn_mgmt_codec/
codec.rs

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/// Controls how the encoder handles characters that are unsafe for the
17/// line-oriented management protocol (`\n`, `\r`, `\0`).
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub enum EncoderMode {
20    /// Silently strip unsafe characters (default, defensive).
21    ///
22    /// `\n`, `\r`, and `\0` are removed from all user-supplied strings.
23    /// Block body lines equaling `"END"` are escaped to `" END"`.
24    #[default]
25    Sanitize,
26
27    /// Reject inputs containing unsafe characters with an error.
28    ///
29    /// [`Encoder::encode`] returns `Err(io::Error)` if any field contains
30    /// `\n`, `\r`, or `\0`, or if a block body line equals `"END"`.
31    /// The inner error can be downcast to [`EncodeError`] for structured
32    /// matching.
33    Strict,
34}
35
36/// Structured error for encoder-side validation failures.
37///
38/// Returned as the inner error of [`std::io::Error`] when [`EncoderMode::Strict`]
39/// is active and the input contains characters that would corrupt the wire protocol.
40/// Callers can recover it via [`std::io::Error::get_ref`] and
41/// [`downcast_ref::<EncodeError>()`](std::error::Error::downcast_ref).
42#[derive(Debug, thiserror::Error)]
43pub enum EncodeError {
44    /// A field contains `\n`, `\r`, or `\0`.
45    #[error("{0} contains characters unsafe for the management protocol (\\n, \\r, or \\0)")]
46    UnsafeCharacters(&'static str),
47
48    /// A multi-line block body line equals `"END"`.
49    #[error("block body line equals \"END\", which would terminate the block early")]
50    EndInBlockBody,
51}
52
53/// Characters that are unsafe in the line-oriented management protocol:
54/// `\n` and `\r` split commands; `\0` truncates at the C layer.
55const WIRE_UNSAFE: &[char] = &['\n', '\r', '\0'];
56
57/// Ensure a string is safe for the wire protocol.
58///
59/// In [`EncoderMode::Sanitize`]: strips `\n`, `\r`, and `\0`, returning
60/// the cleaned string (or borrowing the original if already clean).
61///
62/// In [`EncoderMode::Strict`]: returns `Err` if any unsafe characters
63/// are present.
64fn 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
80/// Escape a string value per the OpenVPN config-file lexer rules and
81/// wrap it in double quotes. This is required for any user-supplied
82/// string that might contain whitespace, backslashes, or quotes —
83/// passwords, reason strings, needstr values, etc.
84///
85/// The escaping rules from the "Command Parsing" section:
86///   `\` → `\\`
87///   `"` → `\"`
88///
89/// This function performs *only* lexer escaping. Wire-safety validation
90/// or sanitization must happen upstream via [`wire_safe`].
91fn 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/// Internal state for accumulating multi-line `>CLIENT:` notifications.
111#[derive(Debug)]
112struct ClientNotifAccum {
113    event: ClientEvent,
114    cid: u64,
115    kid: Option<u64>,
116    env: Vec<(String, String)>,
117}
118
119/// Controls how many items the decoder will accumulate in a multi-line
120/// response or `>CLIENT:` ENV block before returning an error.
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum AccumulationLimit {
123    /// No limit on accumulated items (the default).
124    Unlimited,
125    /// At most this many items before the decoder returns an error.
126    Max(usize),
127}
128
129/// Tokio codec for the OpenVPN management interface.
130///
131/// The encoder serializes typed `OvpnCommand` values into correct wire-format
132/// bytes, including proper escaping and multi-line block framing. The decoder
133/// uses command-tracking state to correctly distinguish single-line from
134/// multi-line responses, and accumulates multi-line `>CLIENT:` notifications
135/// into a single `OvpnMessage` before emitting them.
136///
137/// # Sequential usage
138///
139/// The OpenVPN management protocol is strictly sequential: each command
140/// produces exactly one response, and the server processes commands one
141/// at a time. This codec tracks which response type to expect from the
142/// last encoded command. **You must fully drain [`decode()`] (until it
143/// returns `Ok(None)` or the expected response is received) before
144/// calling [`encode()`] again.** Encoding a new command while a
145/// multi-line response or CLIENT notification is still being accumulated
146/// will overwrite the tracking state and corrupt decoding.
147///
148/// In debug builds, `encode()` asserts that no accumulation is in
149/// progress.
150pub struct OvpnCodec {
151    /// What kind of response we expect from the last command we encoded.
152    /// This resolves the protocol's ambiguity: when we see a line that is
153    /// not `SUCCESS:`, `ERROR:`, or a `>` notification, this field tells
154    /// us whether to treat it as the start of a multi-line block or as a
155    /// standalone value.
156    expected: ResponseKind,
157
158    /// Accumulator for multi-line (END-terminated) command responses.
159    multi_line_buf: Option<Vec<String>>,
160
161    /// Accumulator for multi-line `>CLIENT:` notifications. When this is
162    /// `Some(...)`, the decoder is waiting for `>CLIENT:ENV,END`.
163    client_notif: Option<ClientNotifAccum>,
164
165    /// Maximum lines to accumulate in a multi-line response.
166    max_multi_line_lines: AccumulationLimit,
167
168    /// Maximum ENV entries to accumulate for a `>CLIENT:` notification.
169    max_client_env_entries: AccumulationLimit,
170
171    /// How the encoder handles unsafe characters in user-supplied strings.
172    encoder_mode: EncoderMode,
173}
174
175impl OvpnCodec {
176    /// Create a new codec with default state, ready to encode commands and
177    /// decode responses.
178    pub fn new() -> Self {
179        Self {
180            // Before any command is sent, OpenVPN sends a greeting
181            // (`>INFO:...` notification). SuccessOrError is a safe default
182            // because SUCCESS/ERROR/notifications are all self-describing —
183            // this field only matters for ambiguous (non-prefixed) lines.
184            expected: ResponseKind::SuccessOrError,
185            multi_line_buf: None,
186            client_notif: None,
187            max_multi_line_lines: AccumulationLimit::Unlimited,
188            max_client_env_entries: AccumulationLimit::Unlimited,
189            encoder_mode: EncoderMode::default(),
190        }
191    }
192
193    /// Set the maximum number of lines accumulated in a multi-line
194    /// response before the decoder returns an error.
195    pub fn with_max_multi_line_lines(mut self, limit: AccumulationLimit) -> Self {
196        self.max_multi_line_lines = limit;
197        self
198    }
199
200    /// Set the maximum number of ENV entries accumulated for
201    /// `>CLIENT:` notifications before the decoder returns an error.
202    pub fn with_max_client_env_entries(mut self, limit: AccumulationLimit) -> Self {
203        self.max_client_env_entries = limit;
204        self
205    }
206
207    /// Set the encoder mode for handling unsafe characters in user-supplied
208    /// strings.
209    ///
210    /// The default is [`EncoderMode::Sanitize`], which silently strips
211    /// `\n`, `\r`, and `\0`. Use [`EncoderMode::Strict`] to reject inputs
212    /// containing those characters with an error instead.
213    pub fn with_encoder_mode(mut self, mode: EncoderMode) -> Self {
214        self.encoder_mode = mode;
215        self
216    }
217}
218
219fn check_accumulation_limit(
220    current_len: usize,
221    limit: AccumulationLimit,
222    what: &str,
223) -> Result<(), io::Error> {
224    if let AccumulationLimit::Max(max) = limit
225        && current_len >= max
226    {
227        return Err(io::Error::other(format!(
228            "{what} accumulation limit exceeded ({max})"
229        )));
230    }
231    Ok(())
232}
233
234impl Default for OvpnCodec {
235    fn default() -> Self {
236        Self::new()
237    }
238}
239
240// ── Encoder ───────────────────────────────────────────────────────
241
242impl Encoder<OvpnCommand> for OvpnCodec {
243    type Error = io::Error;
244
245    fn encode(&mut self, item: OvpnCommand, dst: &mut BytesMut) -> Result<(), Self::Error> {
246        debug_assert!(
247            self.multi_line_buf.is_none() && self.client_notif.is_none(),
248            "encode() called while the decoder is mid-accumulation \
249             (multi_line_buf or client_notif is active). \
250             Drain decode() before sending a new command."
251        );
252
253        // Record the expected response kind BEFORE writing, so the decoder
254        // is ready when data starts arriving.
255        self.expected = item.expected_response();
256        let label: &'static str = (&item).into();
257        debug!(cmd = %label, expected = ?self.expected, "encoding command");
258
259        let mode = self.encoder_mode;
260
261        match item {
262            // ── Informational ────────────────────────────────────
263            OvpnCommand::Status(StatusFormat::V1) => write_line(dst, "status"),
264            OvpnCommand::Status(ref fmt) => write_line(dst, &format!("status {fmt}")),
265            OvpnCommand::State => write_line(dst, "state"),
266            OvpnCommand::StateStream(ref m) => write_line(dst, &format!("state {m}")),
267            OvpnCommand::Version => write_line(dst, "version"),
268            OvpnCommand::Pid => write_line(dst, "pid"),
269            OvpnCommand::Help => write_line(dst, "help"),
270            OvpnCommand::Net => write_line(dst, "net"),
271            OvpnCommand::Verb(Some(n)) => write_line(dst, &format!("verb {n}")),
272            OvpnCommand::Verb(None) => write_line(dst, "verb"),
273            OvpnCommand::Mute(Some(n)) => write_line(dst, &format!("mute {n}")),
274            OvpnCommand::Mute(None) => write_line(dst, "mute"),
275
276            // ── Real-time notification control ───────────────────
277            OvpnCommand::Log(ref m) => write_line(dst, &format!("log {m}")),
278            OvpnCommand::Echo(ref m) => write_line(dst, &format!("echo {m}")),
279            OvpnCommand::ByteCount(n) => write_line(dst, &format!("bytecount {n}")),
280
281            // ── Connection control ───────────────────────────────
282            OvpnCommand::Signal(sig) => write_line(dst, &format!("signal {sig}")),
283            OvpnCommand::Kill(KillTarget::CommonName(ref cn)) => {
284                write_line(dst, &format!("kill {}", wire_safe(cn, "kill CN", mode)?));
285            }
286            OvpnCommand::Kill(KillTarget::Address {
287                ref protocol,
288                ref ip,
289                port,
290            }) => {
291                write_line(
292                    dst,
293                    &format!(
294                        "kill {}:{}:{port}",
295                        wire_safe(protocol, "kill address protocol", mode)?,
296                        wire_safe(ip, "kill address ip", mode)?
297                    ),
298                );
299            }
300            OvpnCommand::HoldQuery => write_line(dst, "hold"),
301            OvpnCommand::HoldOn => write_line(dst, "hold on"),
302            OvpnCommand::HoldOff => write_line(dst, "hold off"),
303            OvpnCommand::HoldRelease => write_line(dst, "hold release"),
304
305            // ── Authentication ───────────────────────────────────
306            //
307            // Both username and password values MUST be properly escaped.
308            // The auth type is always double-quoted on the wire.
309            OvpnCommand::Username {
310                ref auth_type,
311                ref value,
312            } => {
313                // Per the doc: username "Auth" foo
314                // Values containing special chars must be quoted+escaped:
315                //   username "Auth" "foo\"bar"
316                let at = quote_and_escape(&wire_safe(
317                    &auth_type.to_string(),
318                    "username auth_type",
319                    mode,
320                )?);
321                let val = quote_and_escape(&wire_safe(value.expose(), "username value", mode)?);
322                write_line(dst, &format!("username {at} {val}"));
323            }
324            OvpnCommand::Password {
325                ref auth_type,
326                ref value,
327            } => {
328                let at = quote_and_escape(&wire_safe(
329                    &auth_type.to_string(),
330                    "password auth_type",
331                    mode,
332                )?);
333                let val = quote_and_escape(&wire_safe(value.expose(), "password value", mode)?);
334                write_line(dst, &format!("password {at} {val}"));
335            }
336            OvpnCommand::AuthRetry(auth_retry_mode) => {
337                write_line(dst, &format!("auth-retry {auth_retry_mode}"));
338            }
339            OvpnCommand::ForgetPasswords => write_line(dst, "forget-passwords"),
340
341            // ── Challenge-response ──────────────────────────────
342            OvpnCommand::ChallengeResponse {
343                ref state_id,
344                ref response,
345            } => {
346                let sid = wire_safe(state_id, "challenge-response state_id", mode)?;
347                let resp = wire_safe(response.expose(), "challenge-response response", mode)?;
348                let value = format!("CRV1::{sid}::{resp}");
349                let escaped = quote_and_escape(&value);
350                write_line(dst, &format!("password \"Auth\" {escaped}"));
351            }
352            OvpnCommand::StaticChallengeResponse {
353                ref password_b64,
354                ref response_b64,
355            } => {
356                let pw = wire_safe(password_b64.expose(), "static-challenge password_b64", mode)?;
357                let resp = wire_safe(response_b64.expose(), "static-challenge response_b64", mode)?;
358                let value = format!("SCRV1:{pw}:{resp}");
359                let escaped = quote_and_escape(&value);
360                write_line(dst, &format!("password \"Auth\" {escaped}"));
361            }
362
363            // ── Interactive prompts ──────────────────────────────
364            OvpnCommand::NeedOk { ref name, response } => {
365                write_line(
366                    dst,
367                    &format!(
368                        "needok {} {response}",
369                        wire_safe(name, "needok name", mode)?
370                    ),
371                );
372            }
373            OvpnCommand::NeedStr {
374                ref name,
375                ref value,
376            } => {
377                let escaped = quote_and_escape(&wire_safe(value, "needstr value", mode)?);
378                write_line(
379                    dst,
380                    &format!(
381                        "needstr {} {escaped}",
382                        wire_safe(name, "needstr name", mode)?
383                    ),
384                );
385            }
386
387            // ── PKCS#11 ─────────────────────────────────────────
388            OvpnCommand::Pkcs11IdCount => write_line(dst, "pkcs11-id-count"),
389            OvpnCommand::Pkcs11IdGet(idx) => write_line(dst, &format!("pkcs11-id-get {idx}")),
390
391            // ── External key (multi-line command) ────────────────
392            //
393            // Wire format:
394            //   rsa-sig
395            //   BASE64_LINE_1
396            //   BASE64_LINE_2
397            //   END
398            OvpnCommand::RsaSig { ref base64_lines } => {
399                write_block(dst, "rsa-sig", base64_lines, mode)?;
400            }
401
402            // ── Client management ────────────────────────────────
403            //
404            // client-auth is a multi-line command:
405            //   client-auth {CID} {KID}
406            //   push "route 10.0.0.0 255.255.0.0"
407            //   END
408            // An empty config_lines produces header + immediate END.
409            OvpnCommand::ClientAuth {
410                cid,
411                kid,
412                ref config_lines,
413            } => {
414                write_block(dst, &format!("client-auth {cid} {kid}"), config_lines, mode)?;
415            }
416
417            OvpnCommand::ClientAuthNt { cid, kid } => {
418                write_line(dst, &format!("client-auth-nt {cid} {kid}"));
419            }
420
421            OvpnCommand::ClientDeny {
422                cid,
423                kid,
424                ref reason,
425                ref client_reason,
426            } => {
427                let r = quote_and_escape(&wire_safe(reason, "client-deny reason", mode)?);
428                match client_reason {
429                    Some(cr) => {
430                        let cr_esc =
431                            quote_and_escape(&wire_safe(cr, "client-deny client_reason", mode)?);
432                        write_line(dst, &format!("client-deny {cid} {kid} {r} {cr_esc}"));
433                    }
434                    None => write_line(dst, &format!("client-deny {cid} {kid} {r}")),
435                }
436            }
437
438            OvpnCommand::ClientKill { cid, ref message } => match message {
439                Some(msg) => write_line(
440                    dst,
441                    &format!(
442                        "client-kill {cid} {}",
443                        wire_safe(msg, "client-kill message", mode)?
444                    ),
445                ),
446                None => write_line(dst, &format!("client-kill {cid}")),
447            },
448
449            // ── Server statistics ─────────────────────────────────
450            OvpnCommand::LoadStats => write_line(dst, "load-stats"),
451
452            // ── Extended client management ───────────────────────
453            //
454            // TODO: warn when `extra` exceeds 245 characters — real-world
455            // limit discovered by jkroepke/openvpn-auth-oauth2 (used for
456            // WEB_AUTH URLs). Not documented in management-notes.txt.
457            // Will address when adding tracing support.
458            OvpnCommand::ClientPendingAuth {
459                cid,
460                kid,
461                ref extra,
462                timeout,
463            } => write_line(
464                dst,
465                &format!(
466                    "client-pending-auth {cid} {kid} {} {timeout}",
467                    wire_safe(extra, "client-pending-auth extra", mode)?
468                ),
469            ),
470
471            OvpnCommand::CrResponse { ref response } => {
472                write_line(
473                    dst,
474                    &format!(
475                        "cr-response {}",
476                        wire_safe(response.expose(), "cr-response", mode)?
477                    ),
478                );
479            }
480
481            // ── External certificate ─────────────────────────────
482            OvpnCommand::Certificate { ref pem_lines } => {
483                write_block(dst, "certificate", pem_lines, mode)?;
484            }
485
486            // ── Remote/Proxy ─────────────────────────────────────
487            OvpnCommand::Remote(RemoteAction::Accept) => write_line(dst, "remote ACCEPT"),
488            OvpnCommand::Remote(RemoteAction::Skip) => write_line(dst, "remote SKIP"),
489            OvpnCommand::Remote(RemoteAction::Modify { ref host, port }) => {
490                write_line(
491                    dst,
492                    &format!(
493                        "remote MOD {} {port}",
494                        wire_safe(host, "remote MOD host", mode)?
495                    ),
496                );
497            }
498            OvpnCommand::Proxy(ProxyAction::None) => write_line(dst, "proxy NONE"),
499            OvpnCommand::Proxy(ProxyAction::Http {
500                ref host,
501                port,
502                non_cleartext_only,
503            }) => {
504                let nct = if non_cleartext_only { " nct" } else { "" };
505                write_line(
506                    dst,
507                    &format!(
508                        "proxy HTTP {} {port}{nct}",
509                        wire_safe(host, "proxy HTTP host", mode)?
510                    ),
511                );
512            }
513            OvpnCommand::Proxy(ProxyAction::Socks { ref host, port }) => {
514                write_line(
515                    dst,
516                    &format!(
517                        "proxy SOCKS {} {port}",
518                        wire_safe(host, "proxy SOCKS host", mode)?
519                    ),
520                );
521            }
522
523            // ── Management interface auth ─────────────────────────
524            // Bare line, no quoting — the management password protocol
525            // does not use the config-file lexer.
526            OvpnCommand::ManagementPassword(ref pw) => {
527                write_line(dst, &wire_safe(pw.expose(), "management password", mode)?);
528            }
529
530            // ── Lifecycle ────────────────────────────────────────
531            OvpnCommand::Exit => write_line(dst, "exit"),
532            OvpnCommand::Quit => write_line(dst, "quit"),
533
534            // ── Escape hatch ─────────────────────────────────────
535            OvpnCommand::Raw(ref cmd) | OvpnCommand::RawMultiLine(ref cmd) => {
536                write_line(dst, &wire_safe(cmd, "raw command", mode)?);
537            }
538        }
539
540        Ok(())
541    }
542}
543
544/// Write a single line followed by `\n`.
545fn write_line(dst: &mut BytesMut, s: &str) {
546    dst.reserve(s.len() + 1);
547    dst.put_slice(s.as_bytes());
548    dst.put_u8(b'\n');
549}
550
551/// Write a multi-line block: header line, body lines, and a terminating `END`.
552///
553/// In [`EncoderMode::Sanitize`] mode, body lines have `\n`, `\r`, and `\0`
554/// stripped, and any line that would be exactly `"END"` is escaped to
555/// `" END"` so the server does not treat it as the block terminator.
556///
557/// In [`EncoderMode::Strict`] mode, body lines containing unsafe characters
558/// or equaling `"END"` cause an error.
559fn write_block(
560    dst: &mut BytesMut,
561    header: &str,
562    lines: &[String],
563    mode: EncoderMode,
564) -> Result<(), io::Error> {
565    let total: usize = header.len() + 1 + lines.iter().map(|l| l.len() + 2).sum::<usize>() + 4;
566    dst.reserve(total);
567    dst.put_slice(header.as_bytes());
568    dst.put_u8(b'\n');
569    for line in lines {
570        let clean = wire_safe(line, "block body line", mode)?;
571        if *clean == *"END" {
572            match mode {
573                EncoderMode::Sanitize => {
574                    dst.put_slice(b" END");
575                    dst.put_u8(b'\n');
576                    continue;
577                }
578                EncoderMode::Strict => {
579                    return Err(io::Error::other(EncodeError::EndInBlockBody));
580                }
581            }
582        }
583        dst.put_slice(clean.as_bytes());
584        dst.put_u8(b'\n');
585    }
586    dst.put_slice(b"END\n");
587    Ok(())
588}
589
590// ── Decoder ───────────────────────────────────────────────────────
591
592impl Decoder for OvpnCodec {
593    type Item = OvpnMessage;
594    type Error = io::Error;
595
596    fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
597        loop {
598            // The password prompt may arrive without a trailing newline
599            // (OpenVPN ≥ 2.6 sends it as an interactive prompt, expecting
600            // the password on the same line). Handle this only when no
601            // complete line is available — if `\n` is in the buffer, the
602            // normal line-based path below handles it correctly.
603            const PW_PROMPT: &[u8] = b"ENTER PASSWORD:";
604
605            // Find the next complete line.
606            let Some(newline_pos) = src.iter().position(|&b| b == b'\n') else {
607                // No complete line yet. Check for a password prompt
608                // without a trailing newline (OpenVPN ≥ 2.6 sends it as
609                // an interactive prompt with no line terminator).
610                // We accept any buffer that starts with the prompt text
611                // since no `\n` is present (checked above). Consume the
612                // prompt and any trailing `\r`.
613                if src.starts_with(PW_PROMPT) {
614                    let mut consume = PW_PROMPT.len();
615                    if src.get(consume) == Some(&b'\r') {
616                        consume += 1;
617                    }
618                    src.advance(consume);
619                    return Ok(Some(OvpnMessage::PasswordPrompt));
620                }
621                return Ok(None); // Need more data.
622            };
623
624            // Extract the line and advance the buffer past the newline.
625            let line_bytes = src.split_to(newline_pos + 1);
626            let line = match std::str::from_utf8(&line_bytes) {
627                Ok(s) => s,
628                Err(e) => {
629                    // Reset all accumulation state so the decoder doesn't
630                    // remain stuck in a half-finished multi-line block.
631                    self.multi_line_buf = None;
632                    self.client_notif = None;
633                    self.expected = ResponseKind::SuccessOrError;
634                    return Err(io::Error::new(io::ErrorKind::InvalidData, e));
635                }
636            }
637            .trim_end_matches(['\r', '\n'])
638            .to_string();
639
640            // Bare newlines (empty lines) carry no information when the
641            // decoder is not inside an accumulation context AND is not
642            // expecting a multi-line response. Skip them silently rather
643            // than emitting Unrecognized. This also absorbs the trailing
644            // `\n` when the password prompt was already consumed without
645            // a line terminator (OpenVPN ≥ 2.6).
646            if line.is_empty()
647                && self.multi_line_buf.is_none()
648                && self.client_notif.is_none()
649                && !matches!(self.expected, ResponseKind::MultiLine)
650            {
651                continue;
652            }
653
654            // ── Phase 1: Multi-line >CLIENT: accumulation ────────
655            //
656            // When we're accumulating a CLIENT notification, >CLIENT:ENV
657            // lines belong to it. The block terminates with >CLIENT:ENV,END.
658            // The spec guarantees atomicity for CLIENT notifications, so
659            // interleaving here should not occur. Any other line (SUCCESS,
660            // ERROR, other notifications) falls through to normal processing
661            // as a defensive measure.
662            if let Some(ref mut accum) = self.client_notif
663                && let Some(rest) = line.strip_prefix(">CLIENT:ENV,")
664            {
665                if rest == "END" {
666                    let finished = self.client_notif.take().expect("guarded by if-let");
667                    debug!(event = ?finished.event, cid = finished.cid, env_count = finished.env.len(), "decoded CLIENT notification");
668                    return Ok(Some(OvpnMessage::Notification(Notification::Client {
669                        event: finished.event,
670                        cid: finished.cid,
671                        kid: finished.kid,
672                        env: finished.env,
673                    })));
674                } else {
675                    // Parse "key=value" (value may contain '=').
676                    let (k, v) = rest
677                        .split_once('=')
678                        .map(|(k, v)| (k.to_string(), v.to_string()))
679                        .unwrap_or_else(|| (rest.to_string(), String::new()));
680                    check_accumulation_limit(
681                        accum.env.len(),
682                        self.max_client_env_entries,
683                        "client ENV",
684                    )?;
685                    accum.env.push((k, v));
686                    continue; // Next line.
687                }
688            }
689            // Not a >CLIENT:ENV line — fall through to normal processing.
690            // This handles interleaved notifications or unexpected output.
691
692            // ── Phase 2: Multi-line command response accumulation ─
693            if let Some(ref mut buf) = self.multi_line_buf {
694                if line == "END" {
695                    let lines = self.multi_line_buf.take().expect("guarded by if-let");
696                    debug!(line_count = lines.len(), "decoded multi-line response");
697                    return Ok(Some(OvpnMessage::MultiLine(lines)));
698                }
699                // The spec only guarantees atomicity for CLIENT notifications,
700                // not for command responses — real-time notifications (>STATE:,
701                // >LOG:, etc.) can arrive mid-response. Emit them immediately
702                // without breaking the accumulation.
703                if line.starts_with('>') {
704                    if let Some(msg) = self.parse_notification(&line) {
705                        return Ok(Some(msg));
706                    }
707                    // parse_notification returns None when it starts a CLIENT
708                    // accumulation. Loop to read the next line.
709                    continue;
710                }
711                check_accumulation_limit(
712                    buf.len(),
713                    self.max_multi_line_lines,
714                    "multi-line response",
715                )?;
716                buf.push(line);
717                continue; // Next line.
718            }
719
720            // ── Phase 3: Self-describing lines ───────────────────
721            //
722            // SUCCESS: and ERROR: are unambiguous. We match on "SUCCESS:"
723            // without requiring a trailing space — the doc shows
724            // "SUCCESS: [text]" but text could be empty.
725            if let Some(rest) = line.strip_prefix("SUCCESS:") {
726                return Ok(Some(OvpnMessage::Success(
727                    rest.strip_prefix(' ').unwrap_or(rest).to_string(),
728                )));
729            }
730            if let Some(rest) = line.strip_prefix("ERROR:") {
731                return Ok(Some(OvpnMessage::Error(
732                    rest.strip_prefix(' ').unwrap_or(rest).to_string(),
733                )));
734            }
735
736            // Management interface password prompt (no `>` prefix).
737            if line == "ENTER PASSWORD:" {
738                return Ok(Some(OvpnMessage::PasswordPrompt));
739            }
740
741            // Real-time notifications.
742            if line.starts_with('>') {
743                if let Some(msg) = self.parse_notification(&line) {
744                    return Ok(Some(msg));
745                }
746                // Started CLIENT notification accumulation — loop for ENV lines.
747                continue;
748            }
749
750            // ── Phase 4: Ambiguous lines — use command tracking ──
751            //
752            // The line is not self-describing (no SUCCESS/ERROR/> prefix).
753            // Use the expected-response state from the last encoded command
754            // to decide how to frame it.
755            match self.expected {
756                ResponseKind::MultiLine => {
757                    if line == "END" {
758                        // Edge case: empty multi-line block (header-less).
759                        return Ok(Some(OvpnMessage::MultiLine(Vec::new())));
760                    }
761                    self.multi_line_buf = Some(vec![line]);
762                    continue; // Accumulate until END.
763                }
764                ResponseKind::SuccessOrError | ResponseKind::NoResponse => {
765                    warn!(line = %line, "unrecognized line from server");
766                    return Ok(Some(OvpnMessage::Unrecognized {
767                        line,
768                        kind: UnrecognizedKind::UnexpectedLine,
769                    }));
770                }
771            }
772        }
773    }
774}
775
776impl OvpnCodec {
777    /// Parse a `>` notification line. Returns `Some(msg)` for single-line
778    /// notifications and `None` when a multi-line CLIENT accumulation has
779    /// been started (the caller should continue reading lines).
780    fn parse_notification(&mut self, line: &str) -> Option<OvpnMessage> {
781        let inner = &line[1..]; // Strip leading `>`
782
783        let Some((kind, payload)) = inner.split_once(':') else {
784            // Malformed notification — no colon.
785            warn!(line = %line, "malformed notification (no colon)");
786            return Some(OvpnMessage::Unrecognized {
787                line: line.to_string(),
788                kind: UnrecognizedKind::MalformedNotification,
789            });
790        };
791
792        // >INFO: gets its own message variant for convenience (it's always
793        // the first thing you see on connect).
794        if kind == "INFO" {
795            return Some(OvpnMessage::Info(payload.to_string()));
796        }
797
798        // >CLIENT: may be multi-line. Inspect the sub-type to decide.
799        if kind == "CLIENT" {
800            let (event, args) = payload
801                .split_once(',')
802                .map(|(e, a)| (e.to_string(), a.to_string()))
803                .unwrap_or_else(|| (payload.to_string(), String::new()));
804
805            // ADDRESS notifications are always single-line (no ENV block).
806            if event == "ADDRESS" {
807                let mut parts = args.splitn(3, ',');
808                let cid = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0);
809                let addr = parts.next().unwrap_or("").to_string();
810                let primary = parts.next() == Some("1");
811                return Some(OvpnMessage::Notification(Notification::ClientAddress {
812                    cid,
813                    addr,
814                    primary,
815                }));
816            }
817
818            // CONNECT, REAUTH, ESTABLISHED, DISCONNECT, and CR_RESPONSE all
819            // have ENV blocks. Parse CID, optional KID, and (for CR_RESPONSE)
820            // the trailing base64 response from the args.
821            let mut id_parts = args.splitn(3, ',');
822            let cid = id_parts.next().and_then(|s| s.parse().ok()).unwrap_or(0);
823            let kid = id_parts.next().and_then(|s| s.parse().ok());
824
825            let parsed_event = if event == "CR_RESPONSE" {
826                let response = id_parts.next().unwrap_or("").to_string();
827                ClientEvent::CrResponse(response)
828            } else {
829                ClientEvent::parse(&event)
830            };
831
832            // Start accumulation — don't emit anything yet.
833            self.client_notif = Some(ClientNotifAccum {
834                event: parsed_event,
835                cid,
836                kid,
837                env: Vec::new(),
838            });
839            return None; // Signal to the caller to keep reading.
840        }
841
842        // Dispatch to typed parsers. On parse failure, fall back to Simple.
843        let notification = match kind {
844            "STATE" => parse_state(payload),
845            "BYTECOUNT" => parse_bytecount(payload),
846            "BYTECOUNT_CLI" => parse_bytecount_cli(payload),
847            "LOG" => parse_log(payload),
848            "ECHO" => parse_echo(payload),
849            "HOLD" => Some(Notification::Hold {
850                text: payload.to_string(),
851            }),
852            "FATAL" => Some(Notification::Fatal {
853                message: payload.to_string(),
854            }),
855            "PKCS11ID-COUNT" => parse_pkcs11id_count(payload),
856            "NEED-OK" => parse_need_ok(payload),
857            "NEED-STR" => parse_need_str(payload),
858            "RSA_SIGN" => Some(Notification::RsaSign {
859                data: payload.to_string(),
860            }),
861            "REMOTE" => parse_remote(payload),
862            "PROXY" => parse_proxy(payload),
863            "PASSWORD" => parse_password(payload),
864            "PKCS11ID-ENTRY" => {
865                return parse_pkcs11id_entry_notif(payload).or_else(|| {
866                    Some(OvpnMessage::Notification(Notification::Simple {
867                        kind: kind.to_string(),
868                        payload: payload.to_string(),
869                    }))
870                });
871            }
872            _ => None,
873        };
874
875        Some(OvpnMessage::Notification(notification.unwrap_or(
876            Notification::Simple {
877                kind: kind.to_string(),
878                payload: payload.to_string(),
879            },
880        )))
881    }
882}
883
884// ── Notification parsers ──────────────────────────────────────────
885//
886// Each returns `Option<Notification>`. `None` means "could not parse,
887// fall back to Simple". This is intentional — the protocol varies
888// across OpenVPN versions and we never want a parse failure to
889// produce an error.
890
891fn parse_state(payload: &str) -> Option<Notification> {
892    // Wire format per management-notes.txt:
893    //   (a) timestamp, (b) state, (c) desc, (d) local_ip, (e) remote_ip,
894    //   (f) remote_port, (g) local_addr, (h) local_port, (i) local_ipv6
895    let mut parts = payload.splitn(9, ',');
896    let timestamp = parts.next()?.parse().ok()?;
897    let name = OpenVpnState::parse(parts.next()?);
898    let description = parts.next()?.to_string();
899    let local_ip = parts.next()?.to_string();
900    let remote_ip = parts.next()?.to_string();
901    let remote_port = parts.next().unwrap_or("").to_string();
902    let local_addr = parts.next().unwrap_or("").to_string();
903    let local_port = parts.next().unwrap_or("").to_string();
904    let local_ipv6 = parts.next().unwrap_or("").to_string();
905    Some(Notification::State {
906        timestamp,
907        name,
908        description,
909        local_ip,
910        remote_ip,
911        remote_port,
912        local_addr,
913        local_port,
914        local_ipv6,
915    })
916}
917
918fn parse_bytecount(payload: &str) -> Option<Notification> {
919    let (a, b) = payload.split_once(',')?;
920    Some(Notification::ByteCount {
921        bytes_in: a.parse().ok()?,
922        bytes_out: b.parse().ok()?,
923    })
924}
925
926fn parse_bytecount_cli(payload: &str) -> Option<Notification> {
927    let mut parts = payload.splitn(3, ',');
928    let cid = parts.next()?.parse().ok()?;
929    let bytes_in = parts.next()?.parse().ok()?;
930    let bytes_out = parts.next()?.parse().ok()?;
931    Some(Notification::ByteCountCli {
932        cid,
933        bytes_in,
934        bytes_out,
935    })
936}
937
938fn parse_log(payload: &str) -> Option<Notification> {
939    let (ts_str, rest) = payload.split_once(',')?;
940    let timestamp = ts_str.parse().ok()?;
941    let (level_str, message) = rest.split_once(',')?;
942    Some(Notification::Log {
943        timestamp,
944        level: LogLevel::parse(level_str),
945        message: message.to_string(),
946    })
947}
948
949fn parse_echo(payload: &str) -> Option<Notification> {
950    let (ts_str, param) = payload.split_once(',')?;
951    let timestamp = ts_str.parse().ok()?;
952    Some(Notification::Echo {
953        timestamp,
954        param: param.to_string(),
955    })
956}
957
958fn parse_pkcs11id_count(payload: &str) -> Option<Notification> {
959    let count = payload.trim().parse().ok()?;
960    Some(Notification::Pkcs11IdCount { count })
961}
962
963/// Parse `>PKCS11ID-ENTRY:'idx', ID:'id', BLOB:'blob'` from the notification
964/// payload (after the kind and colon have been stripped).
965fn parse_pkcs11id_entry_notif(payload: &str) -> Option<OvpnMessage> {
966    let rest = payload.strip_prefix('\'')?;
967    let (index, rest) = rest.split_once("', ID:'")?;
968    let (id, rest) = rest.split_once("', BLOB:'")?;
969    let blob = rest.strip_suffix('\'')?;
970    Some(OvpnMessage::Pkcs11IdEntry {
971        index: index.to_string(),
972        id: id.to_string(),
973        blob: blob.to_string(),
974    })
975}
976
977/// Parse `Need 'name' ... MSG:message` from NEED-OK payload.
978fn parse_need_ok(payload: &str) -> Option<Notification> {
979    // Format: Need 'name' confirmation MSG:message
980    let rest = payload.strip_prefix("Need '")?;
981    let (name, rest) = rest.split_once('\'')?;
982    let msg = rest.split_once("MSG:")?.1;
983    Some(Notification::NeedOk {
984        name: name.to_string(),
985        message: msg.to_string(),
986    })
987}
988
989/// Parse `Need 'name' input MSG:message` from NEED-STR payload.
990fn parse_need_str(payload: &str) -> Option<Notification> {
991    let rest = payload.strip_prefix("Need '")?;
992    let (name, rest) = rest.split_once('\'')?;
993    let msg = rest.split_once("MSG:")?.1;
994    Some(Notification::NeedStr {
995        name: name.to_string(),
996        message: msg.to_string(),
997    })
998}
999
1000fn parse_remote(payload: &str) -> Option<Notification> {
1001    let mut parts = payload.splitn(3, ',');
1002    let host = parts.next()?.to_string();
1003    let port = parts.next()?.parse().ok()?;
1004    let protocol = TransportProtocol::parse(parts.next()?);
1005    Some(Notification::Remote {
1006        host,
1007        port,
1008        protocol,
1009    })
1010}
1011
1012fn parse_proxy(payload: &str) -> Option<Notification> {
1013    // Wire: >PROXY:{index},{type},{host}  (3 fields per init.c)
1014    let mut parts = payload.splitn(3, ',');
1015    let index = parts.next()?.parse().ok()?;
1016    let proxy_type = parts.next()?.to_string();
1017    let host = parts.next()?.to_string();
1018    Some(Notification::Proxy {
1019        index,
1020        proxy_type,
1021        host,
1022    })
1023}
1024
1025use crate::message::PasswordNotification;
1026
1027use crate::auth::AuthType;
1028
1029/// Map a wire auth-type string to the typed enum.
1030fn parse_auth_type(s: &str) -> AuthType {
1031    match s {
1032        "Auth" => AuthType::Auth,
1033        "Private Key" => AuthType::PrivateKey,
1034        "HTTP Proxy" => AuthType::HttpProxy,
1035        "SOCKS Proxy" => AuthType::SocksProxy,
1036        other => AuthType::Custom(other.to_string()),
1037    }
1038}
1039
1040fn parse_password(payload: &str) -> Option<Notification> {
1041    // Auth-Token:{token}
1042    // Source: manage.c management_auth_token()
1043    if let Some(token) = payload.strip_prefix("Auth-Token:") {
1044        return Some(Notification::Password(PasswordNotification::AuthToken {
1045            token: Redacted::new(token),
1046        }));
1047    }
1048
1049    // Verification Failed: 'Auth' ['CRV1:flags:state_id:user_b64:challenge']
1050    // Verification Failed: 'Auth'
1051    if let Some(rest) = payload.strip_prefix("Verification Failed: '") {
1052        // Check for CRV1 dynamic challenge data
1053        if let Some((auth_part, crv1_part)) = rest.split_once("' ['CRV1:") {
1054            let _ = auth_part; // auth type is always "Auth" for CRV1
1055            let crv1_data = crv1_part.strip_suffix("']")?;
1056            let mut parts = crv1_data.splitn(4, ':');
1057            let flags = parts.next()?.to_string();
1058            let state_id = parts.next()?.to_string();
1059            let username_b64 = parts.next()?.to_string();
1060            let challenge = parts.next()?.to_string();
1061            return Some(Notification::Password(
1062                PasswordNotification::DynamicChallenge {
1063                    flags,
1064                    state_id,
1065                    username_b64,
1066                    challenge,
1067                },
1068            ));
1069        }
1070        // Bare verification failure
1071        let auth_type = rest.strip_suffix('\'')?;
1072        return Some(Notification::Password(
1073            PasswordNotification::VerificationFailed {
1074                auth_type: parse_auth_type(auth_type),
1075            },
1076        ));
1077    }
1078
1079    // Need 'type' username/password [SC:...]
1080    // Need 'type' password
1081    let rest = payload.strip_prefix("Need '")?;
1082    let (auth_type_str, rest) = rest.split_once('\'')?;
1083    let rest = rest.trim_start();
1084
1085    if let Some(after_up) = rest.strip_prefix("username/password") {
1086        let after_up = after_up.trim_start();
1087
1088        // Static challenge: SC:flag,challenge_text
1089        // flag is a multi-bit integer: bit 0 = ECHO, bit 1 = FORMAT/CONCAT
1090        if let Some(sc) = after_up.strip_prefix("SC:") {
1091            let (flag_str, challenge) = sc.split_once(',')?;
1092            let flags: u32 = flag_str.parse().ok()?;
1093            return Some(Notification::Password(
1094                PasswordNotification::StaticChallenge {
1095                    echo: flags & 1 != 0,
1096                    response_concat: flags & 2 != 0,
1097                    challenge: challenge.to_string(),
1098                },
1099            ));
1100        }
1101
1102        // Plain username/password request
1103        return Some(Notification::Password(PasswordNotification::NeedAuth {
1104            auth_type: parse_auth_type(auth_type_str),
1105        }));
1106    }
1107
1108    // Need 'type' password
1109    if rest.starts_with("password") {
1110        return Some(Notification::Password(PasswordNotification::NeedPassword {
1111            auth_type: parse_auth_type(auth_type_str),
1112        }));
1113    }
1114
1115    None // Unrecognized PASSWORD sub-format — fall back to Simple
1116}
1117
1118#[cfg(test)]
1119mod tests {
1120    use super::*;
1121    use crate::auth::AuthType;
1122    use crate::client_event::ClientEvent;
1123    use crate::message::PasswordNotification;
1124    use crate::signal::Signal;
1125    use crate::status_format::StatusFormat;
1126    use crate::stream_mode::StreamMode;
1127    use bytes::BytesMut;
1128    use tokio_util::codec::{Decoder, Encoder};
1129
1130    /// Helper: encode a command and return the wire bytes as a string.
1131    fn encode_to_string(cmd: OvpnCommand) -> String {
1132        let mut codec = OvpnCodec::new();
1133        let mut buf = BytesMut::new();
1134        codec.encode(cmd, &mut buf).unwrap();
1135        String::from_utf8(buf.to_vec()).unwrap()
1136    }
1137
1138    /// Helper: feed raw bytes into a fresh codec and collect all decoded messages.
1139    fn decode_all(input: &str) -> Vec<OvpnMessage> {
1140        let mut codec = OvpnCodec::new();
1141        let mut buf = BytesMut::from(input);
1142        let mut msgs = Vec::new();
1143        while let Some(msg) = codec.decode(&mut buf).unwrap() {
1144            msgs.push(msg);
1145        }
1146        msgs
1147    }
1148
1149    /// Helper: encode a command, then feed raw response bytes, collecting messages.
1150    fn encode_then_decode(cmd: OvpnCommand, response: &str) -> Vec<OvpnMessage> {
1151        let mut codec = OvpnCodec::new();
1152        let mut enc_buf = BytesMut::new();
1153        codec.encode(cmd, &mut enc_buf).unwrap();
1154        let mut dec_buf = BytesMut::from(response);
1155        let mut msgs = Vec::new();
1156        while let Some(msg) = codec.decode(&mut dec_buf).unwrap() {
1157            msgs.push(msg);
1158        }
1159        msgs
1160    }
1161
1162    // ── Encoder tests ────────────────────────────────────────────
1163
1164    #[test]
1165    fn encode_status_v1() {
1166        assert_eq!(
1167            encode_to_string(OvpnCommand::Status(StatusFormat::V1)),
1168            "status\n"
1169        );
1170    }
1171
1172    #[test]
1173    fn encode_status_v3() {
1174        assert_eq!(
1175            encode_to_string(OvpnCommand::Status(StatusFormat::V3)),
1176            "status 3\n"
1177        );
1178    }
1179
1180    #[test]
1181    fn encode_signal() {
1182        assert_eq!(
1183            encode_to_string(OvpnCommand::Signal(Signal::SigUsr1)),
1184            "signal SIGUSR1\n"
1185        );
1186    }
1187
1188    #[test]
1189    fn encode_state_on_all() {
1190        assert_eq!(
1191            encode_to_string(OvpnCommand::StateStream(StreamMode::OnAll)),
1192            "state on all\n"
1193        );
1194    }
1195
1196    #[test]
1197    fn encode_state_recent() {
1198        assert_eq!(
1199            encode_to_string(OvpnCommand::StateStream(StreamMode::Recent(5))),
1200            "state 5\n"
1201        );
1202    }
1203
1204    #[test]
1205    fn encode_password_escaping() {
1206        // A password containing a backslash and a double quote must be
1207        // properly escaped on the wire.
1208        let wire = encode_to_string(OvpnCommand::Password {
1209            auth_type: AuthType::PrivateKey,
1210            value: r#"foo\"bar"#.into(),
1211        });
1212        assert_eq!(wire, "password \"Private Key\" \"foo\\\\\\\"bar\"\n");
1213    }
1214
1215    #[test]
1216    fn encode_password_simple() {
1217        let wire = encode_to_string(OvpnCommand::Password {
1218            auth_type: AuthType::Auth,
1219            value: "hunter2".into(),
1220        });
1221        assert_eq!(wire, "password \"Auth\" \"hunter2\"\n");
1222    }
1223
1224    #[test]
1225    fn encode_client_auth_with_config() {
1226        let wire = encode_to_string(OvpnCommand::ClientAuth {
1227            cid: 42,
1228            kid: 0,
1229            config_lines: vec![
1230                "push \"route 10.0.0.0 255.255.0.0\"".to_string(),
1231                "push \"dhcp-option DNS 10.0.0.1\"".to_string(),
1232            ],
1233        });
1234        assert_eq!(
1235            wire,
1236            "client-auth 42 0\n\
1237             push \"route 10.0.0.0 255.255.0.0\"\n\
1238             push \"dhcp-option DNS 10.0.0.1\"\n\
1239             END\n"
1240        );
1241    }
1242
1243    #[test]
1244    fn encode_client_auth_empty_config() {
1245        let wire = encode_to_string(OvpnCommand::ClientAuth {
1246            cid: 1,
1247            kid: 0,
1248            config_lines: vec![],
1249        });
1250        assert_eq!(wire, "client-auth 1 0\nEND\n");
1251    }
1252
1253    #[test]
1254    fn encode_client_deny_with_client_reason() {
1255        let wire = encode_to_string(OvpnCommand::ClientDeny {
1256            cid: 5,
1257            kid: 0,
1258            reason: "cert revoked".to_string(),
1259            client_reason: Some("Your access has been revoked.".to_string()),
1260        });
1261        assert_eq!(
1262            wire,
1263            "client-deny 5 0 \"cert revoked\" \"Your access has been revoked.\"\n"
1264        );
1265    }
1266
1267    #[test]
1268    fn encode_rsa_sig() {
1269        let wire = encode_to_string(OvpnCommand::RsaSig {
1270            base64_lines: vec!["AAAA".to_string(), "BBBB".to_string()],
1271        });
1272        assert_eq!(wire, "rsa-sig\nAAAA\nBBBB\nEND\n");
1273    }
1274
1275    #[test]
1276    fn encode_remote_modify() {
1277        let wire = encode_to_string(OvpnCommand::Remote(RemoteAction::Modify {
1278            host: "vpn.example.com".to_string(),
1279            port: 1234,
1280        }));
1281        assert_eq!(wire, "remote MOD vpn.example.com 1234\n");
1282    }
1283
1284    #[test]
1285    fn encode_proxy_http_nct() {
1286        let wire = encode_to_string(OvpnCommand::Proxy(ProxyAction::Http {
1287            host: "proxy.local".to_string(),
1288            port: 8080,
1289            non_cleartext_only: true,
1290        }));
1291        assert_eq!(wire, "proxy HTTP proxy.local 8080 nct\n");
1292    }
1293
1294    #[test]
1295    fn encode_needok() {
1296        use crate::need_ok::NeedOkResponse;
1297        let wire = encode_to_string(OvpnCommand::NeedOk {
1298            name: "token-insertion-request".to_string(),
1299            response: NeedOkResponse::Ok,
1300        });
1301        assert_eq!(wire, "needok token-insertion-request ok\n");
1302    }
1303
1304    #[test]
1305    fn encode_needstr() {
1306        let wire = encode_to_string(OvpnCommand::NeedStr {
1307            name: "name".to_string(),
1308            value: "John".to_string(),
1309        });
1310        assert_eq!(wire, "needstr name \"John\"\n");
1311    }
1312
1313    #[test]
1314    fn encode_forget_passwords() {
1315        assert_eq!(
1316            encode_to_string(OvpnCommand::ForgetPasswords),
1317            "forget-passwords\n"
1318        );
1319    }
1320
1321    #[test]
1322    fn encode_hold_query() {
1323        assert_eq!(encode_to_string(OvpnCommand::HoldQuery), "hold\n");
1324    }
1325
1326    #[test]
1327    fn encode_echo_on_all() {
1328        assert_eq!(
1329            encode_to_string(OvpnCommand::Echo(StreamMode::OnAll)),
1330            "echo on all\n"
1331        );
1332    }
1333
1334    // ── Decoder tests ────────────────────────────────────────────
1335
1336    #[test]
1337    fn decode_success() {
1338        let msgs = decode_all("SUCCESS: pid=12345\n");
1339        assert_eq!(msgs.len(), 1);
1340        assert!(matches!(&msgs[0], OvpnMessage::Success(s) if s == "pid=12345"));
1341    }
1342
1343    #[test]
1344    fn decode_success_bare() {
1345        // Edge case: SUCCESS: with no trailing text.
1346        let msgs = decode_all("SUCCESS:\n");
1347        assert_eq!(msgs.len(), 1);
1348        assert!(matches!(&msgs[0], OvpnMessage::Success(s) if s.is_empty()));
1349    }
1350
1351    #[test]
1352    fn decode_error() {
1353        let msgs = decode_all("ERROR: unknown command\n");
1354        assert_eq!(msgs.len(), 1);
1355        assert!(matches!(&msgs[0], OvpnMessage::Error(s) if s == "unknown command"));
1356    }
1357
1358    #[test]
1359    fn decode_info_notification() {
1360        let msgs = decode_all(">INFO:OpenVPN Management Interface Version 5\n");
1361        assert_eq!(msgs.len(), 1);
1362        assert!(matches!(
1363            &msgs[0],
1364            OvpnMessage::Info(s) if s == "OpenVPN Management Interface Version 5"
1365        ));
1366    }
1367
1368    #[test]
1369    fn decode_state_notification() {
1370        let msgs = decode_all(">STATE:1234567890,CONNECTED,SUCCESS,,10.0.0.1\n");
1371        assert_eq!(msgs.len(), 1);
1372        match &msgs[0] {
1373            OvpnMessage::Notification(Notification::State {
1374                timestamp,
1375                name,
1376                description,
1377                local_ip,
1378                remote_ip,
1379                ..
1380            }) => {
1381                assert_eq!(*timestamp, 1234567890);
1382                assert_eq!(*name, OpenVpnState::Connected);
1383                assert_eq!(description, "SUCCESS");
1384                assert_eq!(local_ip, "");
1385                assert_eq!(remote_ip, "10.0.0.1");
1386            }
1387            other => panic!("unexpected: {other:?}"),
1388        }
1389    }
1390
1391    #[test]
1392    fn decode_multiline_with_command_tracking() {
1393        // After encoding a `status` command, the codec expects a multi-line
1394        // response. Lines that would otherwise be ambiguous are correctly
1395        // accumulated until END.
1396        let msgs = encode_then_decode(
1397            OvpnCommand::Status(StatusFormat::V1),
1398            "OpenVPN CLIENT LIST\nCommon Name,Real Address\ntest,1.2.3.4:1234\nEND\n",
1399        );
1400        assert_eq!(msgs.len(), 1);
1401        match &msgs[0] {
1402            OvpnMessage::MultiLine(lines) => {
1403                assert_eq!(lines.len(), 3);
1404                assert_eq!(lines[0], "OpenVPN CLIENT LIST");
1405                assert_eq!(lines[2], "test,1.2.3.4:1234");
1406            }
1407            other => panic!("unexpected: {other:?}"),
1408        }
1409    }
1410
1411    #[test]
1412    fn decode_hold_query_success() {
1413        // Bare `hold` returns SUCCESS: hold=0 or SUCCESS: hold=1
1414        let msgs = encode_then_decode(OvpnCommand::HoldQuery, "SUCCESS: hold=0\n");
1415        assert_eq!(msgs.len(), 1);
1416        assert!(matches!(&msgs[0], OvpnMessage::Success(s) if s == "hold=0"));
1417    }
1418
1419    #[test]
1420    fn decode_bare_state_multiline() {
1421        // Bare `state` returns state history lines + END
1422        let msgs = encode_then_decode(
1423            OvpnCommand::State,
1424            "1234567890,CONNECTED,SUCCESS,,10.0.0.1,,,,\nEND\n",
1425        );
1426        assert_eq!(msgs.len(), 1);
1427        match &msgs[0] {
1428            OvpnMessage::MultiLine(lines) => {
1429                assert_eq!(lines.len(), 1);
1430                assert!(lines[0].starts_with("1234567890"));
1431            }
1432            other => panic!("unexpected: {other:?}"),
1433        }
1434    }
1435
1436    #[test]
1437    fn decode_notification_during_multiline() {
1438        // A notification can arrive in the middle of a multi-line response.
1439        // It should be emitted immediately without breaking the accumulation.
1440        let msgs = encode_then_decode(
1441            OvpnCommand::Status(StatusFormat::V1),
1442            "header line\n>BYTECOUNT:1000,2000\ndata line\nEND\n",
1443        );
1444        assert_eq!(msgs.len(), 2);
1445        // First emitted message: the interleaved notification.
1446        assert!(matches!(
1447            &msgs[0],
1448            OvpnMessage::Notification(Notification::ByteCount {
1449                bytes_in: 1000,
1450                bytes_out: 2000
1451            })
1452        ));
1453        // Second: the completed multi-line block (notification is not included).
1454        match &msgs[1] {
1455            OvpnMessage::MultiLine(lines) => {
1456                assert_eq!(lines, &["header line", "data line"]);
1457            }
1458            other => panic!("unexpected: {other:?}"),
1459        }
1460    }
1461
1462    #[test]
1463    fn decode_client_connect_multiline_notification() {
1464        let input = "\
1465            >CLIENT:CONNECT,0,1\n\
1466            >CLIENT:ENV,untrusted_ip=1.2.3.4\n\
1467            >CLIENT:ENV,common_name=TestClient\n\
1468            >CLIENT:ENV,END\n";
1469        let msgs = decode_all(input);
1470        assert_eq!(msgs.len(), 1);
1471        match &msgs[0] {
1472            OvpnMessage::Notification(Notification::Client {
1473                event,
1474                cid,
1475                kid,
1476                env,
1477            }) => {
1478                assert_eq!(*event, ClientEvent::Connect);
1479                assert_eq!(*cid, 0);
1480                assert_eq!(*kid, Some(1));
1481                assert_eq!(env.len(), 2);
1482                assert_eq!(env[0], ("untrusted_ip".to_string(), "1.2.3.4".to_string()));
1483                assert_eq!(
1484                    env[1],
1485                    ("common_name".to_string(), "TestClient".to_string())
1486                );
1487            }
1488            other => panic!("unexpected: {other:?}"),
1489        }
1490    }
1491
1492    #[test]
1493    fn decode_client_address_single_line() {
1494        let msgs = decode_all(">CLIENT:ADDRESS,3,10.0.0.5,1\n");
1495        assert_eq!(msgs.len(), 1);
1496        match &msgs[0] {
1497            OvpnMessage::Notification(Notification::ClientAddress { cid, addr, primary }) => {
1498                assert_eq!(*cid, 3);
1499                assert_eq!(addr, "10.0.0.5");
1500                assert!(*primary);
1501            }
1502            other => panic!("unexpected: {other:?}"),
1503        }
1504    }
1505
1506    #[test]
1507    fn decode_client_disconnect() {
1508        let input = "\
1509            >CLIENT:DISCONNECT,5\n\
1510            >CLIENT:ENV,bytes_received=12345\n\
1511            >CLIENT:ENV,bytes_sent=67890\n\
1512            >CLIENT:ENV,END\n";
1513        let msgs = decode_all(input);
1514        assert_eq!(msgs.len(), 1);
1515        match &msgs[0] {
1516            OvpnMessage::Notification(Notification::Client {
1517                event,
1518                cid,
1519                kid,
1520                env,
1521            }) => {
1522                assert_eq!(*event, ClientEvent::Disconnect);
1523                assert_eq!(*cid, 5);
1524                assert_eq!(*kid, None);
1525                assert_eq!(env.len(), 2);
1526            }
1527            other => panic!("unexpected: {other:?}"),
1528        }
1529    }
1530
1531    #[test]
1532    fn decode_password_notification() {
1533        let msgs = decode_all(">PASSWORD:Need 'Auth' username/password\n");
1534        assert_eq!(msgs.len(), 1);
1535        match &msgs[0] {
1536            OvpnMessage::Notification(Notification::Password(PasswordNotification::NeedAuth {
1537                auth_type,
1538            })) => {
1539                assert_eq!(*auth_type, AuthType::Auth);
1540            }
1541            other => panic!("unexpected: {other:?}"),
1542        }
1543    }
1544
1545    #[test]
1546    fn quote_and_escape_special_chars() {
1547        assert_eq!(quote_and_escape(r#"foo"bar"#), r#""foo\"bar""#);
1548        assert_eq!(quote_and_escape(r"a\b"), r#""a\\b""#);
1549        assert_eq!(quote_and_escape("simple"), r#""simple""#);
1550    }
1551
1552    #[test]
1553    fn decode_empty_multiline() {
1554        // Some commands can return an empty multi-line block (just "END").
1555        let msgs = encode_then_decode(OvpnCommand::Status(StatusFormat::V1), "END\n");
1556        assert_eq!(msgs.len(), 1);
1557        assert!(matches!(&msgs[0], OvpnMessage::MultiLine(lines) if lines.is_empty()));
1558    }
1559
1560    #[test]
1561    fn decode_need_ok_notification() {
1562        let msgs = decode_all(
1563            ">NEED-OK:Need 'token-insertion-request' confirmation MSG:Please insert your token\n",
1564        );
1565        assert_eq!(msgs.len(), 1);
1566        match &msgs[0] {
1567            OvpnMessage::Notification(Notification::NeedOk { name, message }) => {
1568                assert_eq!(name, "token-insertion-request");
1569                assert_eq!(message, "Please insert your token");
1570            }
1571            other => panic!("unexpected: {other:?}"),
1572        }
1573    }
1574
1575    #[test]
1576    fn decode_hold_notification() {
1577        let msgs = decode_all(">HOLD:Waiting for hold release\n");
1578        assert_eq!(msgs.len(), 1);
1579        match &msgs[0] {
1580            OvpnMessage::Notification(Notification::Hold { text }) => {
1581                assert_eq!(text, "Waiting for hold release");
1582            }
1583            other => panic!("unexpected: {other:?}"),
1584        }
1585    }
1586
1587    // ── RawMultiLine tests ──────────────────────────────────────
1588
1589    #[test]
1590    fn encode_raw_multiline() {
1591        assert_eq!(
1592            encode_to_string(OvpnCommand::RawMultiLine("custom-cmd arg".to_string())),
1593            "custom-cmd arg\n"
1594        );
1595    }
1596
1597    #[test]
1598    fn raw_multiline_expects_multiline_response() {
1599        let msgs = encode_then_decode(
1600            OvpnCommand::RawMultiLine("custom".to_string()),
1601            "line1\nline2\nEND\n",
1602        );
1603        assert_eq!(msgs.len(), 1);
1604        match &msgs[0] {
1605            OvpnMessage::MultiLine(lines) => {
1606                assert_eq!(lines, &["line1", "line2"]);
1607            }
1608            other => panic!("expected MultiLine, got: {other:?}"),
1609        }
1610    }
1611
1612    #[test]
1613    fn raw_multiline_sanitizes_newlines() {
1614        // Default mode is Sanitize — newlines are stripped.
1615        let wire = encode_to_string(OvpnCommand::RawMultiLine("cmd\ninjected".to_string()));
1616        assert_eq!(wire, "cmdinjected\n");
1617    }
1618
1619    #[test]
1620    fn raw_multiline_strict_rejects_newlines() {
1621        let mut codec = OvpnCodec::new().with_encoder_mode(EncoderMode::Strict);
1622        let mut buf = BytesMut::new();
1623        let result = codec.encode(
1624            OvpnCommand::RawMultiLine("cmd\ninjected".to_string()),
1625            &mut buf,
1626        );
1627        assert!(result.is_err());
1628    }
1629
1630    // ── Sequential encode/decode assertion tests ────────────────
1631
1632    #[test]
1633    #[should_panic(expected = "mid-accumulation")]
1634    fn encode_during_multiline_accumulation_panics() {
1635        let mut codec = OvpnCodec::new();
1636        let mut buf = BytesMut::new();
1637        // Encode a command that expects multi-line response.
1638        codec
1639            .encode(OvpnCommand::Status(StatusFormat::V1), &mut buf)
1640            .unwrap();
1641        // Feed partial multi-line response (no END yet).
1642        let mut dec = BytesMut::from("header line\n");
1643        let _ = codec.decode(&mut dec); // starts multi_line_buf accumulation
1644        // Encoding again while accumulating should panic in debug.
1645        codec.encode(OvpnCommand::Pid, &mut buf).unwrap();
1646    }
1647
1648    #[test]
1649    #[should_panic(expected = "mid-accumulation")]
1650    fn encode_during_client_notif_accumulation_panics() {
1651        let mut codec = OvpnCodec::new();
1652        let mut buf = BytesMut::new();
1653        // Feed a CLIENT header — starts client_notif accumulation.
1654        let mut dec = BytesMut::from(">CLIENT:CONNECT,0,1\n");
1655        let _ = codec.decode(&mut dec);
1656        // Encoding while client_notif is active should panic.
1657        codec.encode(OvpnCommand::Pid, &mut buf).unwrap();
1658    }
1659
1660    // ── Accumulation limit tests ────────────────────────────────
1661
1662    #[test]
1663    fn unlimited_accumulation_default() {
1664        let mut codec = OvpnCodec::new();
1665        let mut enc = BytesMut::new();
1666        codec
1667            .encode(OvpnCommand::Status(StatusFormat::V1), &mut enc)
1668            .unwrap();
1669        // Feed 500 lines + END — should succeed with Unlimited default.
1670        let mut data = String::new();
1671        for i in 0..500 {
1672            data.push_str(&format!("line {i}\n"));
1673        }
1674        data.push_str("END\n");
1675        let mut dec = BytesMut::from(data.as_str());
1676        let mut msgs = Vec::new();
1677        while let Some(msg) = codec.decode(&mut dec).unwrap() {
1678            msgs.push(msg);
1679        }
1680        assert_eq!(msgs.len(), 1);
1681        match &msgs[0] {
1682            OvpnMessage::MultiLine(lines) => assert_eq!(lines.len(), 500),
1683            other => panic!("expected MultiLine, got: {other:?}"),
1684        }
1685    }
1686
1687    #[test]
1688    fn multi_line_limit_exceeded() {
1689        let mut codec = OvpnCodec::new().with_max_multi_line_lines(AccumulationLimit::Max(3));
1690        let mut enc = BytesMut::new();
1691        codec
1692            .encode(OvpnCommand::Status(StatusFormat::V1), &mut enc)
1693            .unwrap();
1694        let mut dec = BytesMut::from("a\nb\nc\nd\nEND\n");
1695        let result = loop {
1696            match codec.decode(&mut dec) {
1697                Ok(Some(msg)) => break Ok(msg),
1698                Ok(None) => continue,
1699                Err(e) => break Err(e),
1700            }
1701        };
1702        assert!(result.is_err(), "expected error when limit exceeded");
1703        let err = result.unwrap_err();
1704        assert!(
1705            err.to_string().contains("multi-line response"),
1706            "error should mention multi-line: {err}"
1707        );
1708    }
1709
1710    #[test]
1711    fn multi_line_limit_exact_boundary_passes() {
1712        let mut codec = OvpnCodec::new().with_max_multi_line_lines(AccumulationLimit::Max(3));
1713        let mut enc = BytesMut::new();
1714        codec
1715            .encode(OvpnCommand::Status(StatusFormat::V1), &mut enc)
1716            .unwrap();
1717        // Exactly 3 lines should succeed.
1718        let mut dec = BytesMut::from("a\nb\nc\nEND\n");
1719        let mut msgs = Vec::new();
1720        while let Some(msg) = codec.decode(&mut dec).unwrap() {
1721            msgs.push(msg);
1722        }
1723        assert_eq!(msgs.len(), 1);
1724        match &msgs[0] {
1725            OvpnMessage::MultiLine(lines) => assert_eq!(lines.len(), 3),
1726            other => panic!("expected MultiLine, got: {other:?}"),
1727        }
1728    }
1729
1730    #[test]
1731    fn client_env_limit_exceeded() {
1732        let mut codec = OvpnCodec::new().with_max_client_env_entries(AccumulationLimit::Max(2));
1733        let mut dec = BytesMut::from(
1734            ">CLIENT:CONNECT,0,1\n\
1735             >CLIENT:ENV,a=1\n\
1736             >CLIENT:ENV,b=2\n\
1737             >CLIENT:ENV,c=3\n\
1738             >CLIENT:ENV,END\n",
1739        );
1740        let result = loop {
1741            match codec.decode(&mut dec) {
1742                Ok(Some(msg)) => break Ok(msg),
1743                Ok(None) => continue,
1744                Err(e) => break Err(e),
1745            }
1746        };
1747        assert!(
1748            result.is_err(),
1749            "expected error when client ENV limit exceeded"
1750        );
1751        let err = result.unwrap_err();
1752        assert!(
1753            err.to_string().contains("client ENV"),
1754            "error should mention client ENV: {err}"
1755        );
1756    }
1757
1758    // ── UTF-8 error state reset tests ───────────────────────────
1759
1760    #[test]
1761    fn utf8_error_resets_multiline_state() {
1762        let mut codec = OvpnCodec::new();
1763        let mut enc = BytesMut::new();
1764        codec
1765            .encode(OvpnCommand::Status(StatusFormat::V1), &mut enc)
1766            .unwrap();
1767        // Feed a valid first line to start multi-line accumulation.
1768        let mut dec = BytesMut::from("header\n");
1769        assert!(codec.decode(&mut dec).unwrap().is_none());
1770        // Feed invalid UTF-8.
1771        dec.extend_from_slice(b"bad \xff line\n");
1772        assert!(codec.decode(&mut dec).is_err());
1773        // State should be reset — next valid line should decode cleanly
1774        // as an Unrecognized (since expected was reset to SuccessOrError).
1775        dec.extend_from_slice(b"SUCCESS: recovered\n");
1776        let msg = codec.decode(&mut dec).unwrap();
1777        match msg {
1778            Some(OvpnMessage::Success(ref s)) if s.contains("recovered") => {}
1779            other => panic!("expected Success after UTF-8 reset, got: {other:?}"),
1780        }
1781    }
1782
1783    #[test]
1784    fn utf8_error_resets_client_notif_state() {
1785        let mut codec = OvpnCodec::new();
1786        // Start CLIENT accumulation.
1787        let mut dec = BytesMut::from(">CLIENT:CONNECT,0,1\n");
1788        assert!(codec.decode(&mut dec).unwrap().is_none());
1789        // Feed invalid UTF-8 within the ENV block.
1790        dec.extend_from_slice(b">CLIENT:ENV,\xff\n");
1791        assert!(codec.decode(&mut dec).is_err());
1792        // State should be reset.
1793        dec.extend_from_slice(b"SUCCESS: ok\n");
1794        let msg = codec.decode(&mut dec).unwrap();
1795        match msg {
1796            Some(OvpnMessage::Success(_)) => {}
1797            other => panic!("expected Success after UTF-8 reset, got: {other:?}"),
1798        }
1799    }
1800}