hns_browser_loopback_proxy/chromium/
event.rs1use std::fmt;
8use std::net::IpAddr;
9use std::time::Duration;
10use thiserror::Error;
11
12#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
15pub struct ObservedHost(String);
16
17impl ObservedHost {
18 pub fn new(host: &str) -> Result<Self, ObservedHostError> {
19 if host.is_empty() {
20 return Err(ObservedHostError::Empty);
21 }
22 if host.len() > 253 || !host.is_ascii() {
23 return Err(ObservedHostError::InvalidDnsName);
24 }
25 if let Ok(address) = host.parse::<IpAddr>() {
26 if address.to_string() == host.to_ascii_lowercase() {
27 return Ok(Self(host.to_ascii_lowercase()));
28 }
29 return Err(ObservedHostError::InvalidDnsName);
30 }
31 for label in host.split('.') {
32 if label.is_empty() || label.len() > 63 {
33 return Err(ObservedHostError::InvalidDnsName);
34 }
35 let bytes = label.as_bytes();
36 if !bytes.first().is_some_and(u8::is_ascii_alphanumeric)
37 || !bytes.last().is_some_and(u8::is_ascii_alphanumeric)
38 || !bytes
39 .iter()
40 .all(|byte| byte.is_ascii_alphanumeric() || *byte == b'-')
41 {
42 return Err(ObservedHostError::InvalidDnsName);
43 }
44 }
45 Ok(Self(host.to_ascii_lowercase()))
46 }
47
48 pub fn as_str(&self) -> &str {
49 &self.0
50 }
51}
52
53impl fmt::Debug for ObservedHost {
54 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
55 formatter
56 .debug_tuple("ObservedHost")
57 .field(&self.0)
58 .finish()
59 }
60}
61
62impl fmt::Display for ObservedHost {
63 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
64 formatter.write_str(&self.0)
65 }
66}
67
68impl TryFrom<&str> for ObservedHost {
69 type Error = ObservedHostError;
70
71 fn try_from(value: &str) -> Result<Self, Self::Error> {
72 Self::new(value)
73 }
74}
75
76#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
77pub enum ObservedHostError {
78 #[error("observed host is empty")]
79 Empty,
80 #[error("observed host is not a canonical ASCII DNS name")]
81 InvalidDnsName,
82}
83
84#[derive(Clone, Copy, Debug, Eq, PartialEq)]
85pub enum ObservedMethod {
86 Get,
87 Head,
88 Post,
89 Put,
90 Delete,
91 Options,
92 Patch,
93 Connect,
94 Other,
95}
96
97impl ObservedMethod {
98 pub fn from_token(method: &str) -> Self {
99 if method.eq_ignore_ascii_case("GET") {
100 Self::Get
101 } else if method.eq_ignore_ascii_case("HEAD") {
102 Self::Head
103 } else if method.eq_ignore_ascii_case("POST") {
104 Self::Post
105 } else if method.eq_ignore_ascii_case("PUT") {
106 Self::Put
107 } else if method.eq_ignore_ascii_case("DELETE") {
108 Self::Delete
109 } else if method.eq_ignore_ascii_case("OPTIONS") {
110 Self::Options
111 } else if method.eq_ignore_ascii_case("PATCH") {
112 Self::Patch
113 } else if method.eq_ignore_ascii_case("CONNECT") {
114 Self::Connect
115 } else {
116 Self::Other
117 }
118 }
119}
120
121#[derive(Clone, Copy, Debug, Eq, PartialEq)]
122pub enum LifecycleEvent {
123 Listening { port: u16 },
124 Stopping,
125 Stopped { reason: StopReason },
126}
127
128#[derive(Clone, Copy, Debug, Eq, PartialEq)]
129pub enum StopReason {
130 Requested,
131 Cancelled,
132 ListenerFailure,
133}
134
135#[derive(Clone, Copy, Debug, Eq, PartialEq)]
136pub enum ClientRejectionReason {
137 ActiveClientLimit,
138 AuthenticationRequired,
139 InvalidRequest,
140 Cancelled,
141 InvalidPeer,
142}
143
144#[derive(Clone, Copy, Debug, Eq, PartialEq)]
145pub enum RequestRejectionReason {
146 AuthenticationRequired,
147 InvalidRequest,
148 HostOutsideScope,
149 GlobalRateLimit,
150 HostRateLimit,
151 RequestTooLarge,
152 Cancelled,
153}
154
155#[derive(Clone, Copy, Debug, Eq, PartialEq)]
156pub enum BackendFailureKind {
157 Cancelled,
158 InvalidRequest,
159 PolicyDenied,
160 Resolution,
161 TlsValidation,
162 Upstream,
163 InvalidResponse,
164 ResponseTooLarge,
165 Internal,
166}
167
168#[derive(Clone, Copy, Debug, Eq, PartialEq)]
169pub enum RequestPhase {
170 Accepted,
171 Completed {
172 status_code: u16,
173 elapsed: Duration,
174 },
175 Rejected {
176 reason: RequestRejectionReason,
177 },
178 BackendFailed {
179 kind: BackendFailureKind,
180 elapsed: Duration,
181 },
182}
183
184#[derive(Clone, Debug, Eq, PartialEq)]
186pub enum ProxyEvent {
187 Lifecycle {
188 generation: u64,
189 event: LifecycleEvent,
190 },
191 ClientRejected {
192 generation: u64,
193 reason: ClientRejectionReason,
194 },
195 Request {
196 generation: u64,
197 host: ObservedHost,
198 method: ObservedMethod,
199 phase: RequestPhase,
200 },
201}
202
203pub trait ProxyObserver: Send + Sync + 'static {
204 fn observe(&self, event: &ProxyEvent);
207}
208
209impl<F> ProxyObserver for F
210where
211 F: Fn(&ProxyEvent) + Send + Sync + 'static,
212{
213 fn observe(&self, event: &ProxyEvent) {
214 self(event);
215 }
216}
217
218#[derive(Clone, Copy, Debug, Default)]
219pub struct NoopProxyObserver;
220
221impl ProxyObserver for NoopProxyObserver {
222 fn observe(&self, _event: &ProxyEvent) {}
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228 use std::sync::{Arc, Mutex};
229
230 #[test]
231 fn observed_host_accepts_only_dns_host_syntax() {
232 assert_eq!(
233 ObservedHost::new("MiXeD.Example")
234 .expect("valid host")
235 .as_str(),
236 "mixed.example"
237 );
238 for unsafe_host in [
239 "user:pass@example",
240 "example/path",
241 "example?query",
242 "example#fragment",
243 "example:443",
244 "example header",
245 "example%2fpath",
246 ] {
247 assert!(ObservedHost::new(unsafe_host).is_err(), "{unsafe_host}");
248 }
249 }
250
251 #[test]
252 fn unknown_method_does_not_retain_input() {
253 assert_eq!(
254 ObservedMethod::from_token("SECRET /path?query"),
255 ObservedMethod::Other
256 );
257 assert_eq!(format!("{:?}", ObservedMethod::Other), "Other");
258 }
259
260 #[test]
261 fn observer_receives_typed_sanitized_event() {
262 let observed = Arc::new(Mutex::new(Vec::new()));
263 let sink = Arc::clone(&observed);
264 let observer = move |event: &ProxyEvent| {
265 sink.lock()
266 .unwrap_or_else(|poisoned| poisoned.into_inner())
267 .push(event.clone());
268 };
269 let event = ProxyEvent::Request {
270 generation: 9,
271 host: ObservedHost::new("example").expect("valid host"),
272 method: ObservedMethod::Get,
273 phase: RequestPhase::Rejected {
274 reason: RequestRejectionReason::AuthenticationRequired,
275 },
276 };
277 observer.observe(&event);
278 assert_eq!(
279 observed
280 .lock()
281 .unwrap_or_else(|poisoned| poisoned.into_inner())
282 .as_slice(),
283 &[event]
284 );
285 }
286}