Skip to main content

openvpn_mgmt_codec/
client_event.rs

1use std::str::FromStr;
2
3/// Error returned when a string is not a recognized client event.
4#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
5#[error("unrecognized client event: {0:?}")]
6pub struct ParseClientEventError(pub String);
7
8/// The sub-type of a `>CLIENT:` notification.
9#[derive(Debug, Clone, PartialEq, Eq, strum::Display)]
10#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
11pub enum ClientEvent {
12    /// A new client is connecting (`>CLIENT:CONNECT`).
13    Connect,
14
15    /// An existing client is re-authenticating (`>CLIENT:REAUTH`).
16    Reauth,
17
18    /// A client connection has been fully established (`>CLIENT:ESTABLISHED`).
19    Established,
20
21    /// A client has disconnected (`>CLIENT:DISCONNECT`).
22    Disconnect,
23
24    /// A client challenge-response (`>CLIENT:CR_RESPONSE,{CID},{KID},{base64}`).
25    ///
26    /// The base64-encoded response is carried inline because it appears as
27    /// the third comma-separated field on the header line (after CID and KID),
28    /// not in the ENV block. Both cedws/openvpn-mgmt-go and
29    /// jkroepke/openvpn-auth-oauth2 handle this as a distinct event type.
30    CrResponse(String),
31
32    /// An unrecognized event type (forward compatibility).
33    #[strum(default)]
34    Unknown(String),
35}
36
37impl FromStr for ClientEvent {
38    type Err = ParseClientEventError;
39
40    /// Parse a recognized client event string.
41    ///
42    /// Recognized values: `CONNECT`, `REAUTH`, `ESTABLISHED`, `DISCONNECT`.
43    /// Returns `Err` for anything else — use [`ClientEvent::Unknown`]
44    /// explicitly if forward-compatible fallback is desired.
45    ///
46    /// Note: `CR_RESPONSE` is handled separately in the codec because it
47    /// carries an inline base64 field; it is not recognized by `FromStr`.
48    fn from_str(input: &str) -> Result<Self, Self::Err> {
49        match input {
50            "CONNECT" => Ok(Self::Connect),
51            "REAUTH" => Ok(Self::Reauth),
52            "ESTABLISHED" => Ok(Self::Established),
53            "DISCONNECT" => Ok(Self::Disconnect),
54            other => Err(ParseClientEventError(other.to_string())),
55        }
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn parse_all_known_variants() {
65        assert_eq!(
66            "CONNECT".parse::<ClientEvent>().unwrap(),
67            ClientEvent::Connect
68        );
69        assert_eq!(
70            "REAUTH".parse::<ClientEvent>().unwrap(),
71            ClientEvent::Reauth
72        );
73        assert_eq!(
74            "ESTABLISHED".parse::<ClientEvent>().unwrap(),
75            ClientEvent::Established
76        );
77        assert_eq!(
78            "DISCONNECT".parse::<ClientEvent>().unwrap(),
79            ClientEvent::Disconnect
80        );
81    }
82
83    #[test]
84    fn display_roundtrip() {
85        for variant in [
86            ClientEvent::Connect,
87            ClientEvent::Reauth,
88            ClientEvent::Established,
89            ClientEvent::Disconnect,
90        ] {
91            let s = variant.to_string();
92            assert_eq!(s.parse::<ClientEvent>().unwrap(), variant);
93        }
94    }
95
96    #[test]
97    fn unknown_string_is_err() {
98        assert!("BOGUS".parse::<ClientEvent>().is_err());
99        assert!("connect".parse::<ClientEvent>().is_err()); // case-sensitive
100    }
101
102    #[test]
103    fn cr_response_not_recognised_by_from_str() {
104        // CR_RESPONSE is handled separately in the codec; FromStr rejects it.
105        assert!("CR_RESPONSE".parse::<ClientEvent>().is_err());
106    }
107
108    #[test]
109    fn display_cr_response() {
110        let cr = ClientEvent::CrResponse("abc".to_string());
111        assert_eq!(cr.to_string(), "CR_RESPONSE");
112    }
113
114    #[test]
115    fn display_unknown() {
116        let u = ClientEvent::Unknown("FUTURE".to_string());
117        assert_eq!(u.to_string(), "FUTURE");
118    }
119}