Skip to main content

openvpn_mgmt_codec/
codec.rs

1use bytes::{BufMut, BytesMut};
2use std::io;
3use tokio_util::codec::{Decoder, Encoder};
4
5use crate::command::{OvpnCommand, ResponseKind};
6use crate::kill_target::KillTarget;
7use crate::message::{Notification, OvpnMessage};
8use crate::proxy_action::ProxyAction;
9use crate::remote_action::RemoteAction;
10use crate::status_format::StatusFormat;
11use crate::unrecognized::UnrecognizedKind;
12
13/// Escape a string value per the OpenVPN config-file lexer rules and
14/// wrap it in double quotes. This is required for any user-supplied
15/// string that might contain whitespace, backslashes, or quotes —
16/// passwords, reason strings, needstr values, etc.
17///
18/// The escaping rules from the "Command Parsing" section:
19///   `\` → `\\`
20///   `"` → `\"`
21fn quote_and_escape(s: &str) -> String {
22    let mut out = String::with_capacity(s.len() + 2);
23    out.push('"');
24    for c in s.chars() {
25        match c {
26            '\\' => out.push_str("\\\\"),
27            '"' => out.push_str("\\\""),
28            _ => out.push(c),
29        }
30    }
31    out.push('"');
32    out
33}
34
35use crate::client_event::ClientEvent;
36use crate::log_level::LogLevel;
37use crate::openvpn_state::OpenVpnState;
38use crate::transport_protocol::TransportProtocol;
39
40/// Internal state for accumulating multi-line `>CLIENT:` notifications.
41#[derive(Debug)]
42struct ClientNotifAccum {
43    event: ClientEvent,
44    cid: u64,
45    kid: Option<u64>,
46    env: Vec<(String, String)>,
47}
48
49/// Tokio codec for the OpenVPN management interface.
50///
51/// The encoder serializes typed `OvpnCommand` values into correct wire-format
52/// bytes, including proper escaping and multi-line block framing. The decoder
53/// uses command-tracking state to correctly distinguish single-line from
54/// multi-line responses, and accumulates multi-line `>CLIENT:` notifications
55/// into a single `OvpnMessage` before emitting them.
56pub struct OvpnCodec {
57    /// What kind of response we expect from the last command we encoded.
58    /// This resolves the protocol's ambiguity: when we see a line that is
59    /// not `SUCCESS:`, `ERROR:`, or a `>` notification, this field tells
60    /// us whether to treat it as the start of a multi-line block or as a
61    /// standalone value.
62    expected: ResponseKind,
63
64    /// Accumulator for multi-line (END-terminated) command responses.
65    multi_line_buf: Option<Vec<String>>,
66
67    /// Accumulator for multi-line `>CLIENT:` notifications. When this is
68    /// `Some(...)`, the decoder is waiting for `>CLIENT:ENV,END`.
69    client_notif: Option<ClientNotifAccum>,
70}
71
72impl OvpnCodec {
73    /// Create a new codec with default state, ready to encode commands and
74    /// decode responses.
75    pub fn new() -> Self {
76        Self {
77            // Before any command is sent, OpenVPN sends a greeting
78            // (`>INFO:...` notification). SuccessOrError is a safe default
79            // because SUCCESS/ERROR/notifications are all self-describing —
80            // this field only matters for ambiguous (non-prefixed) lines.
81            expected: ResponseKind::SuccessOrError,
82            multi_line_buf: None,
83            client_notif: None,
84        }
85    }
86}
87
88impl Default for OvpnCodec {
89    fn default() -> Self {
90        Self::new()
91    }
92}
93
94// ── Encoder ───────────────────────────────────────────────────────
95
96impl Encoder<OvpnCommand> for OvpnCodec {
97    type Error = io::Error;
98
99    fn encode(&mut self, item: OvpnCommand, dst: &mut BytesMut) -> Result<(), Self::Error> {
100        // Record the expected response kind BEFORE writing, so the decoder
101        // is ready when data starts arriving.
102        self.expected = item.expected_response();
103
104        match item {
105            // ── Informational ────────────────────────────────────
106            OvpnCommand::Status(StatusFormat::V1) => write_line(dst, "status"),
107            OvpnCommand::Status(ref fmt) => write_line(dst, &format!("status {fmt}")),
108            OvpnCommand::State => write_line(dst, "state"),
109            OvpnCommand::StateStream(ref m) => write_line(dst, &format!("state {}", m)),
110            OvpnCommand::Version => write_line(dst, "version"),
111            OvpnCommand::Pid => write_line(dst, "pid"),
112            OvpnCommand::Help => write_line(dst, "help"),
113            OvpnCommand::Net => write_line(dst, "net"),
114            OvpnCommand::Verb(Some(n)) => write_line(dst, &format!("verb {n}")),
115            OvpnCommand::Verb(None) => write_line(dst, "verb"),
116            OvpnCommand::Mute(Some(n)) => write_line(dst, &format!("mute {n}")),
117            OvpnCommand::Mute(None) => write_line(dst, "mute"),
118
119            // ── Real-time notification control ───────────────────
120            OvpnCommand::Log(ref m) => write_line(dst, &format!("log {}", m)),
121            OvpnCommand::Echo(ref m) => write_line(dst, &format!("echo {}", m)),
122            OvpnCommand::ByteCount(n) => write_line(dst, &format!("bytecount {n}")),
123
124            // ── Connection control ───────────────────────────────
125            OvpnCommand::Signal(sig) => write_line(dst, &format!("signal {sig}")),
126            OvpnCommand::Kill(KillTarget::CommonName(ref cn)) => {
127                write_line(dst, &format!("kill {cn}"))
128            }
129            OvpnCommand::Kill(KillTarget::Address { ref ip, port }) => {
130                write_line(dst, &format!("kill {ip}:{port}"))
131            }
132            OvpnCommand::HoldQuery => write_line(dst, "hold"),
133            OvpnCommand::HoldOn => write_line(dst, "hold on"),
134            OvpnCommand::HoldOff => write_line(dst, "hold off"),
135            OvpnCommand::HoldRelease => write_line(dst, "hold release"),
136
137            // ── Authentication ───────────────────────────────────
138            //
139            // Both username and password values MUST be properly escaped.
140            // The auth type is always double-quoted on the wire.
141            OvpnCommand::Username {
142                ref auth_type,
143                ref value,
144            } => {
145                // Per the doc: username "Auth" foo
146                // Values containing special chars must be quoted+escaped:
147                //   username "Auth" "foo\"bar"
148                let escaped = quote_and_escape(value);
149                write_line(dst, &format!("username \"{auth_type}\" {escaped}"))
150            }
151            OvpnCommand::Password {
152                ref auth_type,
153                ref value,
154            } => {
155                let escaped = quote_and_escape(value);
156                write_line(dst, &format!("password \"{auth_type}\" {escaped}"))
157            }
158            OvpnCommand::AuthRetry(mode) => write_line(dst, &format!("auth-retry {mode}")),
159            OvpnCommand::ForgetPasswords => write_line(dst, "forget-passwords"),
160
161            // ── Challenge-response ──────────────────────────────
162            OvpnCommand::ChallengeResponse {
163                ref state_id,
164                ref response,
165            } => {
166                let value = format!("CRV1::{state_id}::{response}");
167                let escaped = quote_and_escape(&value);
168                write_line(dst, &format!("password \"Auth\" {escaped}"))
169            }
170            OvpnCommand::StaticChallengeResponse {
171                ref password_b64,
172                ref response_b64,
173            } => {
174                let value = format!("SCRV1:{password_b64}:{response_b64}");
175                let escaped = quote_and_escape(&value);
176                write_line(dst, &format!("password \"Auth\" {escaped}"))
177            }
178
179            // ── Interactive prompts ──────────────────────────────
180            OvpnCommand::NeedOk { ref name, response } => {
181                write_line(dst, &format!("needok {name} {response}"))
182            }
183            OvpnCommand::NeedStr {
184                ref name,
185                ref value,
186            } => {
187                let escaped = quote_and_escape(value);
188                write_line(dst, &format!("needstr {name} {escaped}"))
189            }
190
191            // ── PKCS#11 ─────────────────────────────────────────
192            OvpnCommand::Pkcs11IdCount => write_line(dst, "pkcs11-id-count"),
193            OvpnCommand::Pkcs11IdGet(idx) => write_line(dst, &format!("pkcs11-id-get {idx}")),
194
195            // ── External key (multi-line command) ────────────────
196            //
197            // Wire format:
198            //   rsa-sig
199            //   BASE64_LINE_1
200            //   BASE64_LINE_2
201            //   END
202            OvpnCommand::RsaSig { ref base64_lines } => write_block(dst, "rsa-sig", base64_lines),
203
204            // ── Client management ────────────────────────────────
205            //
206            // client-auth is a multi-line command:
207            //   client-auth {CID} {KID}
208            //   push "route 10.0.0.0 255.255.0.0"
209            //   END
210            // An empty config_lines produces header + immediate END.
211            OvpnCommand::ClientAuth {
212                cid,
213                kid,
214                ref config_lines,
215            } => write_block(dst, &format!("client-auth {cid} {kid}"), config_lines),
216
217            OvpnCommand::ClientAuthNt { cid, kid } => {
218                write_line(dst, &format!("client-auth-nt {cid} {kid}"))
219            }
220
221            OvpnCommand::ClientDeny {
222                cid,
223                kid,
224                ref reason,
225                ref client_reason,
226            } => {
227                let r = quote_and_escape(reason);
228                match client_reason {
229                    Some(cr) => {
230                        let cr_esc = quote_and_escape(cr);
231                        write_line(dst, &format!("client-deny {cid} {kid} {r} {cr_esc}"))
232                    }
233                    None => write_line(dst, &format!("client-deny {cid} {kid} {r}")),
234                }
235            }
236
237            OvpnCommand::ClientKill { cid } => write_line(dst, &format!("client-kill {cid}")),
238
239            // client-pf is also a multi-line command:
240            //   client-pf {CID}
241            //   [CLIENTS ACCEPT]
242            //   ...
243            //   [END]
244            //   END
245            OvpnCommand::ClientPf {
246                cid,
247                ref filter_lines,
248            } => write_block(dst, &format!("client-pf {cid}"), filter_lines),
249
250            // ── Server statistics ─────────────────────────────────
251            OvpnCommand::LoadStats => write_line(dst, "load-stats"),
252
253            // ── Extended client management ───────────────────────
254            OvpnCommand::ClientPendingAuth {
255                cid,
256                kid,
257                timeout,
258                ref extra,
259            } => write_line(
260                dst,
261                &format!("client-pending-auth {cid} {kid} {timeout} {extra}"),
262            ),
263
264            OvpnCommand::ClientDenyV2 {
265                cid,
266                kid,
267                ref reason,
268                ref client_reason,
269                ref redirect_url,
270            } => {
271                let r = quote_and_escape(reason);
272                let mut cmd = format!("client-deny-v2 {cid} {kid} {r}");
273                if let Some(cr) = client_reason {
274                    cmd.push(' ');
275                    cmd.push_str(&quote_and_escape(cr));
276                    if let Some(url) = redirect_url {
277                        cmd.push(' ');
278                        cmd.push_str(&quote_and_escape(url));
279                    }
280                }
281                write_line(dst, &cmd)
282            }
283
284            OvpnCommand::CrResponse {
285                cid,
286                kid,
287                ref response,
288            } => write_line(dst, &format!("cr-response {cid} {kid} {response}")),
289
290            // ── External certificate ─────────────────────────────
291            OvpnCommand::Certificate { ref pem_lines } => {
292                write_block(dst, "certificate", pem_lines)
293            }
294
295            // ── Windows service bypass ───────────────────────────
296            OvpnCommand::BypassMessage(ref msg) => {
297                let escaped = quote_and_escape(msg);
298                write_line(dst, &format!("bypass-message {escaped}"))
299            }
300
301            // ── Remote/Proxy ─────────────────────────────────────
302            OvpnCommand::Remote(RemoteAction::Accept) => write_line(dst, "remote ACCEPT"),
303            OvpnCommand::Remote(RemoteAction::Skip) => write_line(dst, "remote SKIP"),
304            OvpnCommand::Remote(RemoteAction::Modify { ref host, port }) => {
305                write_line(dst, &format!("remote MOD {host} {port}"))
306            }
307            OvpnCommand::Proxy(ProxyAction::None) => write_line(dst, "proxy NONE"),
308            OvpnCommand::Proxy(ProxyAction::Http {
309                ref host,
310                port,
311                non_cleartext_only,
312            }) => {
313                let nct = if non_cleartext_only { " nct" } else { "" };
314                write_line(dst, &format!("proxy HTTP {host} {port}{nct}"))
315            }
316            OvpnCommand::Proxy(ProxyAction::Socks { ref host, port }) => {
317                write_line(dst, &format!("proxy SOCKS {host} {port}"))
318            }
319
320            // ── Management interface auth ─────────────────────────
321            // Bare line, no quoting — the management password protocol
322            // does not use the config-file lexer.
323            OvpnCommand::ManagementPassword(ref pw) => write_line(dst, pw),
324
325            // ── Lifecycle ────────────────────────────────────────
326            OvpnCommand::Exit => write_line(dst, "exit"),
327            OvpnCommand::Quit => write_line(dst, "quit"),
328
329            // ── Escape hatch ─────────────────────────────────────
330            OvpnCommand::Raw(ref cmd) => write_line(dst, cmd),
331        }
332
333        Ok(())
334    }
335}
336
337/// Write a single line followed by `\n`.
338fn write_line(dst: &mut BytesMut, s: &str) {
339    dst.reserve(s.len() + 1);
340    dst.put_slice(s.as_bytes());
341    dst.put_u8(b'\n');
342}
343
344/// Write a multi-line block: header line, body lines, and a terminating `END`.
345fn write_block(dst: &mut BytesMut, header: &str, lines: &[String]) {
346    let total: usize = header.len() + 1 + lines.iter().map(|l| l.len() + 1).sum::<usize>() + 4;
347    dst.reserve(total);
348    dst.put_slice(header.as_bytes());
349    dst.put_u8(b'\n');
350    for line in lines {
351        dst.put_slice(line.as_bytes());
352        dst.put_u8(b'\n');
353    }
354    dst.put_slice(b"END\n");
355}
356
357// ── Decoder ───────────────────────────────────────────────────────
358
359impl Decoder for OvpnCodec {
360    type Item = OvpnMessage;
361    type Error = io::Error;
362
363    fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
364        loop {
365            // Find the next complete line.
366            let newline_pos = match src.iter().position(|&b| b == b'\n') {
367                Some(pos) => pos,
368                None => return Ok(None), // Need more data.
369            };
370
371            // Extract the line and advance the buffer past the newline.
372            let line_bytes = src.split_to(newline_pos + 1);
373            let line = std::str::from_utf8(&line_bytes)
374                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
375                .trim_end_matches(['\r', '\n'])
376                .to_owned();
377
378            // ── Phase 1: Multi-line >CLIENT: accumulation ────────
379            //
380            // When we're accumulating a CLIENT notification, >CLIENT:ENV
381            // lines belong to it. The block terminates with >CLIENT:ENV,END.
382            // The spec guarantees atomicity for CLIENT notifications, so
383            // interleaving here should not occur. Any other line (SUCCESS,
384            // ERROR, other notifications) falls through to normal processing
385            // as a defensive measure.
386            if let Some(ref mut accum) = self.client_notif
387                && let Some(rest) = line.strip_prefix(">CLIENT:ENV,") {
388                    if rest == "END" {
389                        let finished = self.client_notif.take().unwrap();
390                        return Ok(Some(OvpnMessage::Notification(Notification::Client {
391                            event: finished.event,
392                            cid: finished.cid,
393                            kid: finished.kid,
394                            env: finished.env,
395                        })));
396                    } else {
397                        // Parse "key=value" (value may contain '=').
398                        let (k, v) = match rest.split_once('=') {
399                            Some((k, v)) => (k.to_owned(), v.to_owned()),
400                            None => (rest.to_owned(), String::new()),
401                        };
402                        accum.env.push((k, v));
403                        continue; // Next line.
404                    }
405                }
406                // Not a >CLIENT:ENV line — fall through to normal processing.
407                // This handles interleaved notifications or unexpected output.
408
409            // ── Phase 2: Multi-line command response accumulation ─
410            if let Some(ref mut buf) = self.multi_line_buf {
411                if line == "END" {
412                    let lines = self.multi_line_buf.take().unwrap();
413                    return Ok(Some(OvpnMessage::MultiLine(lines)));
414                }
415                // The spec only guarantees atomicity for CLIENT notifications,
416                // not for command responses — real-time notifications (>STATE:,
417                // >LOG:, etc.) can arrive mid-response. Emit them immediately
418                // without breaking the accumulation.
419                if line.starts_with('>') {
420                    if let Some(msg) = self.parse_notification(&line) {
421                        return Ok(Some(msg));
422                    }
423                    // parse_notification returns None when it starts a CLIENT
424                    // accumulation. Loop to read the next line.
425                    continue;
426                }
427                buf.push(line);
428                continue; // Next line.
429            }
430
431            // ── Phase 3: Self-describing lines ───────────────────
432            //
433            // SUCCESS: and ERROR: are unambiguous. We match on "SUCCESS:"
434            // without requiring a trailing space — the doc shows
435            // "SUCCESS: [text]" but text could be empty.
436            if let Some(rest) = line.strip_prefix("SUCCESS:") {
437                return Ok(Some(OvpnMessage::Success(
438                    rest.strip_prefix(' ').unwrap_or(rest).to_owned(),
439                )));
440            }
441            if let Some(rest) = line.strip_prefix("ERROR:") {
442                return Ok(Some(OvpnMessage::Error(
443                    rest.strip_prefix(' ').unwrap_or(rest).to_owned(),
444                )));
445            }
446
447            // Management interface password prompt (no `>` prefix).
448            if line == "ENTER PASSWORD:" {
449                return Ok(Some(OvpnMessage::PasswordPrompt));
450            }
451
452            // Real-time notifications.
453            if line.starts_with('>') {
454                if let Some(msg) = self.parse_notification(&line) {
455                    return Ok(Some(msg));
456                }
457                // Started CLIENT notification accumulation — loop for ENV lines.
458                continue;
459            }
460
461            // ── Phase 4: Ambiguous lines — use command tracking ──
462            //
463            // The line is not self-describing (no SUCCESS/ERROR/> prefix).
464            // Use the expected-response state from the last encoded command
465            // to decide how to frame it.
466            match self.expected {
467                ResponseKind::MultiLine => {
468                    if line == "END" {
469                        // Edge case: empty multi-line block (header-less).
470                        return Ok(Some(OvpnMessage::MultiLine(Vec::new())));
471                    }
472                    self.multi_line_buf = Some(vec![line]);
473                    continue; // Accumulate until END.
474                }
475                ResponseKind::SingleValue => {
476                    if let Some(parsed) = parse_pkcs11id_entry(&line) {
477                        return Ok(Some(parsed));
478                    }
479                    return Ok(Some(OvpnMessage::SingleValue(line)));
480                }
481                ResponseKind::SuccessOrError | ResponseKind::NoResponse => {
482                    return Ok(Some(OvpnMessage::Unrecognized {
483                        line,
484                        kind: UnrecognizedKind::UnexpectedLine,
485                    }));
486                }
487            }
488        }
489    }
490}
491
492impl OvpnCodec {
493    /// Parse a `>` notification line. Returns `Some(msg)` for single-line
494    /// notifications and `None` when a multi-line CLIENT accumulation has
495    /// been started (the caller should continue reading lines).
496    fn parse_notification(&mut self, line: &str) -> Option<OvpnMessage> {
497        let inner = &line[1..]; // Strip leading `>`
498
499        let (kind, payload) = match inner.split_once(':') {
500            Some((k, p)) => (k, p),
501            // Malformed notification — no colon.
502            None => {
503                return Some(OvpnMessage::Unrecognized {
504                    line: line.to_owned(),
505                    kind: UnrecognizedKind::MalformedNotification,
506                });
507            }
508        };
509
510        // >INFO: gets its own message variant for convenience (it's always
511        // the first thing you see on connect).
512        if kind == "INFO" {
513            return Some(OvpnMessage::Info(payload.to_owned()));
514        }
515
516        // >CLIENT: may be multi-line. Inspect the sub-type to decide.
517        if kind == "CLIENT" {
518            let (event, args) = match payload.split_once(',') {
519                Some((e, a)) => (e.to_owned(), a.to_owned()),
520                None => (payload.to_owned(), String::new()),
521            };
522
523            // ADDRESS notifications are always single-line (no ENV block).
524            if event == "ADDRESS" {
525                let mut parts = args.splitn(3, ',');
526                let cid = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0);
527                let addr = parts.next().unwrap_or("").to_owned();
528                let primary = parts.next() == Some("1");
529                return Some(OvpnMessage::Notification(Notification::ClientAddress {
530                    cid,
531                    addr,
532                    primary,
533                }));
534            }
535
536            // CONNECT, REAUTH, ESTABLISHED, DISCONNECT all have ENV blocks.
537            // Parse CID and optional KID from the args (e.g. "0,1" or "5").
538            // Some events (e.g. CR_RESPONSE) have extra trailing data after
539            // CID,KID — we use splitn(3) and only parse the first two.
540            let mut id_parts = args.splitn(3, ',');
541            let cid = id_parts.next().and_then(|s| s.parse().ok()).unwrap_or(0);
542            let kid = id_parts.next().and_then(|s| s.parse().ok());
543
544            // Start accumulation — don't emit anything yet.
545            self.client_notif = Some(ClientNotifAccum {
546                event: ClientEvent::parse(&event),
547                cid,
548                kid,
549                env: Vec::new(),
550            });
551            return None; // Signal to the caller to keep reading.
552        }
553
554        // Dispatch to typed parsers. On parse failure, fall back to Simple.
555        let notification = match kind {
556            "STATE" => parse_state(payload),
557            "BYTECOUNT" => parse_bytecount(payload),
558            "BYTECOUNT_CLI" => parse_bytecount_cli(payload),
559            "LOG" => parse_log(payload),
560            "ECHO" => parse_echo(payload),
561            "HOLD" => Some(Notification::Hold {
562                text: payload.to_owned(),
563            }),
564            "FATAL" => Some(Notification::Fatal {
565                message: payload.to_owned(),
566            }),
567            "PKCS11ID-COUNT" => parse_pkcs11id_count(payload),
568            "NEED-OK" => parse_need_ok(payload),
569            "NEED-STR" => parse_need_str(payload),
570            "RSA_SIGN" => Some(Notification::RsaSign {
571                data: payload.to_owned(),
572            }),
573            "REMOTE" => parse_remote(payload),
574            "PROXY" => parse_proxy(payload),
575            "PASSWORD" => parse_password(payload),
576            _ => None,
577        };
578
579        Some(OvpnMessage::Notification(notification.unwrap_or(
580            Notification::Simple {
581                kind: kind.to_owned(),
582                payload: payload.to_owned(),
583            },
584        )))
585    }
586}
587
588// ── Notification parsers ──────────────────────────────────────────
589//
590// Each returns `Option<Notification>`. `None` means "could not parse,
591// fall back to Simple". This is intentional — the protocol varies
592// across OpenVPN versions and we never want a parse failure to
593// produce an error.
594
595fn parse_state(payload: &str) -> Option<Notification> {
596    // Wire format (OpenVPN 2.1+):
597    //   timestamp,name,desc,local_ip,remote_ip[,local_port[,local_addr[,remote_port]]]
598    // Field 7 (local_addr) was added later; we skip it since the
599    // Notification::State struct doesn't model it.
600    let mut parts = payload.splitn(9, ',');
601    let timestamp = parts.next()?.parse().ok()?;
602    let name = OpenVpnState::parse(parts.next()?);
603    let description = parts.next()?.to_owned();
604    let local_ip = parts.next()?.to_owned();
605    let remote_ip = parts.next()?.to_owned();
606    let local_port = parts.next().unwrap_or("").to_owned();
607    let _local_addr = parts.next(); // skip local_addr
608    let remote_port = parts.next().unwrap_or("").to_owned();
609    Some(Notification::State {
610        timestamp,
611        name,
612        description,
613        local_ip,
614        remote_ip,
615        local_port,
616        remote_port,
617    })
618}
619
620fn parse_bytecount(payload: &str) -> Option<Notification> {
621    let (a, b) = payload.split_once(',')?;
622    Some(Notification::ByteCount {
623        bytes_in: a.parse().ok()?,
624        bytes_out: b.parse().ok()?,
625    })
626}
627
628fn parse_bytecount_cli(payload: &str) -> Option<Notification> {
629    let mut parts = payload.splitn(3, ',');
630    let cid = parts.next()?.parse().ok()?;
631    let bytes_in = parts.next()?.parse().ok()?;
632    let bytes_out = parts.next()?.parse().ok()?;
633    Some(Notification::ByteCountCli {
634        cid,
635        bytes_in,
636        bytes_out,
637    })
638}
639
640fn parse_log(payload: &str) -> Option<Notification> {
641    let (ts_str, rest) = payload.split_once(',')?;
642    let timestamp = ts_str.parse().ok()?;
643    let (level_str, message) = rest.split_once(',')?;
644    Some(Notification::Log {
645        timestamp,
646        level: LogLevel::parse(level_str),
647        message: message.to_owned(),
648    })
649}
650
651fn parse_echo(payload: &str) -> Option<Notification> {
652    let (ts_str, param) = payload.split_once(',')?;
653    let timestamp = ts_str.parse().ok()?;
654    Some(Notification::Echo {
655        timestamp,
656        param: param.to_owned(),
657    })
658}
659
660fn parse_pkcs11id_count(payload: &str) -> Option<Notification> {
661    let count = payload.trim().parse().ok()?;
662    Some(Notification::Pkcs11IdCount { count })
663}
664
665/// Parse `PKCS11ID-ENTRY:'idx', ID:'id', BLOB:'blob'` response line.
666fn parse_pkcs11id_entry(line: &str) -> Option<OvpnMessage> {
667    let rest = line.strip_prefix("PKCS11ID-ENTRY:'")?;
668    let (index, rest) = rest.split_once("', ID:'")?;
669    let (id, rest) = rest.split_once("', BLOB:'")?;
670    let blob = rest.strip_suffix('\'')?;
671    Some(OvpnMessage::Pkcs11IdEntry {
672        index: index.to_owned(),
673        id: id.to_owned(),
674        blob: blob.to_owned(),
675    })
676}
677
678/// Parse `Need 'name' ... MSG:message` from NEED-OK payload.
679fn parse_need_ok(payload: &str) -> Option<Notification> {
680    // Format: Need 'name' confirmation MSG:message
681    let rest = payload.strip_prefix("Need '")?;
682    let (name, rest) = rest.split_once('\'')?;
683    let msg = rest.split_once("MSG:")?.1;
684    Some(Notification::NeedOk {
685        name: name.to_owned(),
686        message: msg.to_owned(),
687    })
688}
689
690/// Parse `Need 'name' input MSG:message` from NEED-STR payload.
691fn parse_need_str(payload: &str) -> Option<Notification> {
692    let rest = payload.strip_prefix("Need '")?;
693    let (name, rest) = rest.split_once('\'')?;
694    let msg = rest.split_once("MSG:")?.1;
695    Some(Notification::NeedStr {
696        name: name.to_owned(),
697        message: msg.to_owned(),
698    })
699}
700
701fn parse_remote(payload: &str) -> Option<Notification> {
702    let mut parts = payload.splitn(3, ',');
703    let host = parts.next()?.to_owned();
704    let port = parts.next()?.parse().ok()?;
705    let protocol = TransportProtocol::parse(parts.next()?);
706    Some(Notification::Remote {
707        host,
708        port,
709        protocol,
710    })
711}
712
713fn parse_proxy(payload: &str) -> Option<Notification> {
714    let mut parts = payload.splitn(4, ',');
715    let proto_num = parts.next()?.parse().ok()?;
716    let proto_type = TransportProtocol::parse(parts.next()?);
717    let host = parts.next()?.to_owned();
718    let port = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0);
719    Some(Notification::Proxy {
720        proto_num,
721        proto_type,
722        host,
723        port,
724    })
725}
726
727use crate::message::PasswordNotification;
728
729use crate::auth::AuthType;
730
731/// Map a wire auth-type string to the typed enum.
732fn parse_auth_type(s: &str) -> AuthType {
733    match s {
734        "Auth" => AuthType::Auth,
735        "Private Key" => AuthType::PrivateKey,
736        "HTTP Proxy" => AuthType::HttpProxy,
737        "SOCKS Proxy" => AuthType::SocksProxy,
738        other => AuthType::Custom(other.to_owned()),
739    }
740}
741
742fn parse_password(payload: &str) -> Option<Notification> {
743    // Verification Failed: 'type'
744    if let Some(rest) = payload.strip_prefix("Verification Failed: '") {
745        let auth_type = rest.strip_suffix('\'')?;
746        return Some(Notification::Password(
747            PasswordNotification::VerificationFailed {
748                auth_type: parse_auth_type(auth_type),
749            },
750        ));
751    }
752
753    // Need 'type' username/password [SC:...|CRV1:...]
754    // Need 'type' password
755    let rest = payload.strip_prefix("Need '")?;
756    let (auth_type_str, rest) = rest.split_once('\'')?;
757    let rest = rest.trim_start();
758
759    // Check for challenge-response suffixes
760    if let Some(after_up) = rest.strip_prefix("username/password") {
761        let after_up = after_up.trim_start();
762
763        // Static challenge: SC:echo_flag,challenge_text
764        if let Some(sc) = after_up.strip_prefix("SC:") {
765            let (echo_str, challenge) = sc.split_once(',')?;
766            return Some(Notification::Password(
767                PasswordNotification::StaticChallenge {
768                    echo: echo_str == "1",
769                    challenge: challenge.to_owned(),
770                },
771            ));
772        }
773
774        // Dynamic challenge: CRV1:flags:state_id:username_b64:challenge
775        if let Some(crv1) = after_up.strip_prefix("CRV1:") {
776            let mut parts = crv1.splitn(4, ':');
777            let flags = parts.next()?.to_owned();
778            let state_id = parts.next()?.to_owned();
779            let username_b64 = parts.next()?.to_owned();
780            let challenge = parts.next()?.to_owned();
781            return Some(Notification::Password(
782                PasswordNotification::DynamicChallenge {
783                    flags,
784                    state_id,
785                    username_b64,
786                    challenge,
787                },
788            ));
789        }
790
791        // Plain username/password request
792        return Some(Notification::Password(PasswordNotification::NeedAuth {
793            auth_type: parse_auth_type(auth_type_str),
794        }));
795    }
796
797    // Need 'type' password
798    if rest.starts_with("password") {
799        return Some(Notification::Password(PasswordNotification::NeedPassword {
800            auth_type: parse_auth_type(auth_type_str),
801        }));
802    }
803
804    None // Unrecognized PASSWORD sub-format — fall back to Simple
805}
806
807#[cfg(test)]
808mod tests {
809    use super::*;
810    use crate::auth::AuthType;
811    use crate::client_event::ClientEvent;
812    use crate::message::PasswordNotification;
813    use crate::signal::Signal;
814    use crate::status_format::StatusFormat;
815    use crate::stream_mode::StreamMode;
816    use bytes::BytesMut;
817    use tokio_util::codec::{Decoder, Encoder};
818
819    /// Helper: encode a command and return the wire bytes as a string.
820    fn encode_to_string(cmd: OvpnCommand) -> String {
821        let mut codec = OvpnCodec::new();
822        let mut buf = BytesMut::new();
823        codec.encode(cmd, &mut buf).unwrap();
824        String::from_utf8(buf.to_vec()).unwrap()
825    }
826
827    /// Helper: feed raw bytes into a fresh codec and collect all decoded messages.
828    fn decode_all(input: &str) -> Vec<OvpnMessage> {
829        let mut codec = OvpnCodec::new();
830        let mut buf = BytesMut::from(input);
831        let mut msgs = Vec::new();
832        while let Some(msg) = codec.decode(&mut buf).unwrap() {
833            msgs.push(msg);
834        }
835        msgs
836    }
837
838    /// Helper: encode a command, then feed raw response bytes, collecting messages.
839    fn encode_then_decode(cmd: OvpnCommand, response: &str) -> Vec<OvpnMessage> {
840        let mut codec = OvpnCodec::new();
841        let mut enc_buf = BytesMut::new();
842        codec.encode(cmd, &mut enc_buf).unwrap();
843        let mut dec_buf = BytesMut::from(response);
844        let mut msgs = Vec::new();
845        while let Some(msg) = codec.decode(&mut dec_buf).unwrap() {
846            msgs.push(msg);
847        }
848        msgs
849    }
850
851    // ── Encoder tests ────────────────────────────────────────────
852
853    #[test]
854    fn encode_status_v1() {
855        assert_eq!(
856            encode_to_string(OvpnCommand::Status(StatusFormat::V1)),
857            "status\n"
858        );
859    }
860
861    #[test]
862    fn encode_status_v3() {
863        assert_eq!(
864            encode_to_string(OvpnCommand::Status(StatusFormat::V3)),
865            "status 3\n"
866        );
867    }
868
869    #[test]
870    fn encode_signal() {
871        assert_eq!(
872            encode_to_string(OvpnCommand::Signal(Signal::SigUsr1)),
873            "signal SIGUSR1\n"
874        );
875    }
876
877    #[test]
878    fn encode_state_on_all() {
879        assert_eq!(
880            encode_to_string(OvpnCommand::StateStream(StreamMode::OnAll)),
881            "state on all\n"
882        );
883    }
884
885    #[test]
886    fn encode_state_recent() {
887        assert_eq!(
888            encode_to_string(OvpnCommand::StateStream(StreamMode::Recent(5))),
889            "state 5\n"
890        );
891    }
892
893    #[test]
894    fn encode_password_escaping() {
895        // A password containing a backslash and a double quote must be
896        // properly escaped on the wire.
897        let wire = encode_to_string(OvpnCommand::Password {
898            auth_type: AuthType::PrivateKey,
899            value: r#"foo\"bar"#.to_owned(),
900        });
901        assert_eq!(wire, "password \"Private Key\" \"foo\\\\\\\"bar\"\n");
902    }
903
904    #[test]
905    fn encode_password_simple() {
906        let wire = encode_to_string(OvpnCommand::Password {
907            auth_type: AuthType::Auth,
908            value: "hunter2".to_owned(),
909        });
910        assert_eq!(wire, "password \"Auth\" \"hunter2\"\n");
911    }
912
913    #[test]
914    fn encode_client_auth_with_config() {
915        let wire = encode_to_string(OvpnCommand::ClientAuth {
916            cid: 42,
917            kid: 0,
918            config_lines: vec![
919                "push \"route 10.0.0.0 255.255.0.0\"".to_owned(),
920                "push \"dhcp-option DNS 10.0.0.1\"".to_owned(),
921            ],
922        });
923        assert_eq!(
924            wire,
925            "client-auth 42 0\n\
926             push \"route 10.0.0.0 255.255.0.0\"\n\
927             push \"dhcp-option DNS 10.0.0.1\"\n\
928             END\n"
929        );
930    }
931
932    #[test]
933    fn encode_client_auth_empty_config() {
934        let wire = encode_to_string(OvpnCommand::ClientAuth {
935            cid: 1,
936            kid: 0,
937            config_lines: vec![],
938        });
939        assert_eq!(wire, "client-auth 1 0\nEND\n");
940    }
941
942    #[test]
943    fn encode_client_deny_with_client_reason() {
944        let wire = encode_to_string(OvpnCommand::ClientDeny {
945            cid: 5,
946            kid: 0,
947            reason: "cert revoked".to_owned(),
948            client_reason: Some("Your access has been revoked.".to_owned()),
949        });
950        assert_eq!(
951            wire,
952            "client-deny 5 0 \"cert revoked\" \"Your access has been revoked.\"\n"
953        );
954    }
955
956    #[test]
957    fn encode_rsa_sig() {
958        let wire = encode_to_string(OvpnCommand::RsaSig {
959            base64_lines: vec!["AAAA".to_owned(), "BBBB".to_owned()],
960        });
961        assert_eq!(wire, "rsa-sig\nAAAA\nBBBB\nEND\n");
962    }
963
964    #[test]
965    fn encode_remote_modify() {
966        let wire = encode_to_string(OvpnCommand::Remote(RemoteAction::Modify {
967            host: "vpn.example.com".to_owned(),
968            port: 1234,
969        }));
970        assert_eq!(wire, "remote MOD vpn.example.com 1234\n");
971    }
972
973    #[test]
974    fn encode_proxy_http_nct() {
975        let wire = encode_to_string(OvpnCommand::Proxy(ProxyAction::Http {
976            host: "proxy.local".to_owned(),
977            port: 8080,
978            non_cleartext_only: true,
979        }));
980        assert_eq!(wire, "proxy HTTP proxy.local 8080 nct\n");
981    }
982
983    #[test]
984    fn encode_needok() {
985        use crate::need_ok::NeedOkResponse;
986        let wire = encode_to_string(OvpnCommand::NeedOk {
987            name: "token-insertion-request".to_owned(),
988            response: NeedOkResponse::Ok,
989        });
990        assert_eq!(wire, "needok token-insertion-request ok\n");
991    }
992
993    #[test]
994    fn encode_needstr() {
995        let wire = encode_to_string(OvpnCommand::NeedStr {
996            name: "name".to_owned(),
997            value: "John".to_owned(),
998        });
999        assert_eq!(wire, "needstr name \"John\"\n");
1000    }
1001
1002    #[test]
1003    fn encode_forget_passwords() {
1004        assert_eq!(
1005            encode_to_string(OvpnCommand::ForgetPasswords),
1006            "forget-passwords\n"
1007        );
1008    }
1009
1010    #[test]
1011    fn encode_hold_query() {
1012        assert_eq!(encode_to_string(OvpnCommand::HoldQuery), "hold\n");
1013    }
1014
1015    #[test]
1016    fn encode_echo_on_all() {
1017        assert_eq!(
1018            encode_to_string(OvpnCommand::Echo(StreamMode::OnAll)),
1019            "echo on all\n"
1020        );
1021    }
1022
1023    #[test]
1024    fn encode_client_pf() {
1025        let wire = encode_to_string(OvpnCommand::ClientPf {
1026            cid: 42,
1027            filter_lines: vec![
1028                "[CLIENTS ACCEPT]".to_owned(),
1029                "-accounting".to_owned(),
1030                "[SUBNETS DROP]".to_owned(),
1031                "+10.0.0.0/8".to_owned(),
1032                "[END]".to_owned(),
1033            ],
1034        });
1035        assert_eq!(
1036            wire,
1037            "client-pf 42\n\
1038             [CLIENTS ACCEPT]\n\
1039             -accounting\n\
1040             [SUBNETS DROP]\n\
1041             +10.0.0.0/8\n\
1042             [END]\n\
1043             END\n"
1044        );
1045    }
1046
1047    // ── Decoder tests ────────────────────────────────────────────
1048
1049    #[test]
1050    fn decode_success() {
1051        let msgs = decode_all("SUCCESS: pid=12345\n");
1052        assert_eq!(msgs.len(), 1);
1053        assert!(matches!(&msgs[0], OvpnMessage::Success(s) if s == "pid=12345"));
1054    }
1055
1056    #[test]
1057    fn decode_success_bare() {
1058        // Edge case: SUCCESS: with no trailing text.
1059        let msgs = decode_all("SUCCESS:\n");
1060        assert_eq!(msgs.len(), 1);
1061        assert!(matches!(&msgs[0], OvpnMessage::Success(s) if s.is_empty()));
1062    }
1063
1064    #[test]
1065    fn decode_error() {
1066        let msgs = decode_all("ERROR: unknown command\n");
1067        assert_eq!(msgs.len(), 1);
1068        assert!(matches!(&msgs[0], OvpnMessage::Error(s) if s == "unknown command"));
1069    }
1070
1071    #[test]
1072    fn decode_info_notification() {
1073        let msgs = decode_all(">INFO:OpenVPN Management Interface Version 5\n");
1074        assert_eq!(msgs.len(), 1);
1075        assert!(matches!(
1076            &msgs[0],
1077            OvpnMessage::Info(s) if s == "OpenVPN Management Interface Version 5"
1078        ));
1079    }
1080
1081    #[test]
1082    fn decode_state_notification() {
1083        let msgs = decode_all(">STATE:1234567890,CONNECTED,SUCCESS,,10.0.0.1\n");
1084        assert_eq!(msgs.len(), 1);
1085        match &msgs[0] {
1086            OvpnMessage::Notification(Notification::State {
1087                timestamp,
1088                name,
1089                description,
1090                local_ip,
1091                remote_ip,
1092                ..
1093            }) => {
1094                assert_eq!(*timestamp, 1234567890);
1095                assert_eq!(*name, OpenVpnState::Connected);
1096                assert_eq!(description, "SUCCESS");
1097                assert_eq!(local_ip, "");
1098                assert_eq!(remote_ip, "10.0.0.1");
1099            }
1100            other => panic!("unexpected: {other:?}"),
1101        }
1102    }
1103
1104    #[test]
1105    fn decode_multiline_with_command_tracking() {
1106        // After encoding a `status` command, the codec expects a multi-line
1107        // response. Lines that would otherwise be ambiguous are correctly
1108        // accumulated until END.
1109        let msgs = encode_then_decode(
1110            OvpnCommand::Status(StatusFormat::V1),
1111            "OpenVPN CLIENT LIST\nCommon Name,Real Address\ntest,1.2.3.4:1234\nEND\n",
1112        );
1113        assert_eq!(msgs.len(), 1);
1114        match &msgs[0] {
1115            OvpnMessage::MultiLine(lines) => {
1116                assert_eq!(lines.len(), 3);
1117                assert_eq!(lines[0], "OpenVPN CLIENT LIST");
1118                assert_eq!(lines[2], "test,1.2.3.4:1234");
1119            }
1120            other => panic!("unexpected: {other:?}"),
1121        }
1122    }
1123
1124    #[test]
1125    fn decode_hold_query_single_value() {
1126        // After encoding bare `hold`, the codec expects a single value line.
1127        let msgs = encode_then_decode(OvpnCommand::HoldQuery, "0\n");
1128        assert_eq!(msgs.len(), 1);
1129        assert!(matches!(&msgs[0], OvpnMessage::SingleValue(s) if s == "0"));
1130    }
1131
1132    #[test]
1133    fn decode_bare_state_single_value() {
1134        let msgs = encode_then_decode(
1135            OvpnCommand::State,
1136            "1234567890,CONNECTED,SUCCESS,,10.0.0.1,,\n",
1137        );
1138        assert_eq!(msgs.len(), 1);
1139        assert!(matches!(&msgs[0], OvpnMessage::SingleValue(s) if s.starts_with("1234567890")));
1140    }
1141
1142    #[test]
1143    fn decode_notification_during_multiline() {
1144        // A notification can arrive in the middle of a multi-line response.
1145        // It should be emitted immediately without breaking the accumulation.
1146        let msgs = encode_then_decode(
1147            OvpnCommand::Status(StatusFormat::V1),
1148            "header line\n>BYTECOUNT:1000,2000\ndata line\nEND\n",
1149        );
1150        assert_eq!(msgs.len(), 2);
1151        // First emitted message: the interleaved notification.
1152        assert!(matches!(
1153            &msgs[0],
1154            OvpnMessage::Notification(Notification::ByteCount {
1155                bytes_in: 1000,
1156                bytes_out: 2000
1157            })
1158        ));
1159        // Second: the completed multi-line block (notification is not included).
1160        match &msgs[1] {
1161            OvpnMessage::MultiLine(lines) => {
1162                assert_eq!(lines, &["header line", "data line"]);
1163            }
1164            other => panic!("unexpected: {other:?}"),
1165        }
1166    }
1167
1168    #[test]
1169    fn decode_client_connect_multiline_notification() {
1170        let input = "\
1171            >CLIENT:CONNECT,0,1\n\
1172            >CLIENT:ENV,untrusted_ip=1.2.3.4\n\
1173            >CLIENT:ENV,common_name=TestClient\n\
1174            >CLIENT:ENV,END\n";
1175        let msgs = decode_all(input);
1176        assert_eq!(msgs.len(), 1);
1177        match &msgs[0] {
1178            OvpnMessage::Notification(Notification::Client {
1179                event,
1180                cid,
1181                kid,
1182                env,
1183            }) => {
1184                assert_eq!(*event, ClientEvent::Connect);
1185                assert_eq!(*cid, 0);
1186                assert_eq!(*kid, Some(1));
1187                assert_eq!(env.len(), 2);
1188                assert_eq!(env[0], ("untrusted_ip".to_owned(), "1.2.3.4".to_owned()));
1189                assert_eq!(env[1], ("common_name".to_owned(), "TestClient".to_owned()));
1190            }
1191            other => panic!("unexpected: {other:?}"),
1192        }
1193    }
1194
1195    #[test]
1196    fn decode_client_address_single_line() {
1197        let msgs = decode_all(">CLIENT:ADDRESS,3,10.0.0.5,1\n");
1198        assert_eq!(msgs.len(), 1);
1199        match &msgs[0] {
1200            OvpnMessage::Notification(Notification::ClientAddress { cid, addr, primary }) => {
1201                assert_eq!(*cid, 3);
1202                assert_eq!(addr, "10.0.0.5");
1203                assert!(*primary);
1204            }
1205            other => panic!("unexpected: {other:?}"),
1206        }
1207    }
1208
1209    #[test]
1210    fn decode_client_disconnect() {
1211        let input = "\
1212            >CLIENT:DISCONNECT,5\n\
1213            >CLIENT:ENV,bytes_received=12345\n\
1214            >CLIENT:ENV,bytes_sent=67890\n\
1215            >CLIENT:ENV,END\n";
1216        let msgs = decode_all(input);
1217        assert_eq!(msgs.len(), 1);
1218        match &msgs[0] {
1219            OvpnMessage::Notification(Notification::Client {
1220                event,
1221                cid,
1222                kid,
1223                env,
1224            }) => {
1225                assert_eq!(*event, ClientEvent::Disconnect);
1226                assert_eq!(*cid, 5);
1227                assert_eq!(*kid, None);
1228                assert_eq!(env.len(), 2);
1229            }
1230            other => panic!("unexpected: {other:?}"),
1231        }
1232    }
1233
1234    #[test]
1235    fn decode_password_notification() {
1236        let msgs = decode_all(">PASSWORD:Need 'Auth' username/password\n");
1237        assert_eq!(msgs.len(), 1);
1238        match &msgs[0] {
1239            OvpnMessage::Notification(Notification::Password(PasswordNotification::NeedAuth {
1240                auth_type,
1241            })) => {
1242                assert_eq!(*auth_type, AuthType::Auth);
1243            }
1244            other => panic!("unexpected: {other:?}"),
1245        }
1246    }
1247
1248    #[test]
1249    fn quote_and_escape_special_chars() {
1250        assert_eq!(quote_and_escape(r#"foo"bar"#), r#""foo\"bar""#);
1251        assert_eq!(quote_and_escape(r"a\b"), r#""a\\b""#);
1252        assert_eq!(quote_and_escape("simple"), r#""simple""#);
1253    }
1254
1255    #[test]
1256    fn decode_empty_multiline() {
1257        // Some commands can return an empty multi-line block (just "END").
1258        let msgs = encode_then_decode(OvpnCommand::Status(StatusFormat::V1), "END\n");
1259        assert_eq!(msgs.len(), 1);
1260        assert!(matches!(&msgs[0], OvpnMessage::MultiLine(lines) if lines.is_empty()));
1261    }
1262
1263    #[test]
1264    fn decode_need_ok_notification() {
1265        let msgs = decode_all(
1266            ">NEED-OK:Need 'token-insertion-request' confirmation MSG:Please insert your token\n",
1267        );
1268        assert_eq!(msgs.len(), 1);
1269        match &msgs[0] {
1270            OvpnMessage::Notification(Notification::NeedOk { name, message }) => {
1271                assert_eq!(name, "token-insertion-request");
1272                assert_eq!(message, "Please insert your token");
1273            }
1274            other => panic!("unexpected: {other:?}"),
1275        }
1276    }
1277
1278    #[test]
1279    fn decode_hold_notification() {
1280        let msgs = decode_all(">HOLD:Waiting for hold release\n");
1281        assert_eq!(msgs.len(), 1);
1282        match &msgs[0] {
1283            OvpnMessage::Notification(Notification::Hold { text }) => {
1284                assert_eq!(text, "Waiting for hold release");
1285            }
1286            other => panic!("unexpected: {other:?}"),
1287        }
1288    }
1289}