Skip to main content

hns_browser_loopback_proxy/chromium/
metadata.rs

1//! Typed delivery of trusted, sensitive response metadata.
2//!
3//! This surface is deliberately separate from [`crate::ProxyEvent`]. It
4//! exposes the response status and trusted `X-HNS-*` metadata needed by native
5//! browser security UI without adding fields for a request target, query,
6//! headers, or body. Trusted metadata values remain sensitive and may
7//! themselves contain diagnostic request details; callers must keep them
8//! bounded to the in-memory security UI and diagnostics are redacted.
9
10use crate::event::{ObservedHost, ObservedMethod};
11use crate::response::InternalResponseMetadata;
12use std::fmt;
13
14/// Whether an observation describes an origin HTTP response or the
15/// pre-response decision to preserve browser-owned WebPKI.
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub enum ProxyMetadataObservationKind {
18    OriginResponse,
19    WebPkiConnectDecision,
20}
21
22/// Trusted, sensitive metadata from one validated response head.
23///
24/// Observations contain no dedicated request-target, query, request-header,
25/// response-body, or request-body fields. Allowlisted metadata values may
26/// themselves contain request details and must be treated as sensitive. They
27/// are redacted from `Debug` output.
28#[derive(Clone, Eq, PartialEq)]
29pub struct ProxyResponseMetadataObservation {
30    generation: u64,
31    host: ObservedHost,
32    method: ObservedMethod,
33    status_code: u16,
34    likely_main_frame: bool,
35    observation_id: Option<u64>,
36    kind: ProxyMetadataObservationKind,
37    port: Option<u16>,
38    correlation_epoch: Option<u64>,
39    metadata: InternalResponseMetadata,
40}
41
42impl ProxyResponseMetadataObservation {
43    // Keep the complete trust-boundary tuple explicit at each construction
44    // site; grouping these fields would make security-relevant omissions less
45    // visible during review.
46    #[allow(clippy::too_many_arguments)]
47    pub(crate) fn new(
48        generation: u64,
49        host: ObservedHost,
50        method: ObservedMethod,
51        status_code: u16,
52        likely_main_frame: bool,
53        observation_id: Option<u64>,
54        kind: ProxyMetadataObservationKind,
55        port: Option<u16>,
56        correlation_epoch: Option<u64>,
57        metadata: InternalResponseMetadata,
58    ) -> Self {
59        Self {
60            generation,
61            host,
62            method,
63            status_code,
64            likely_main_frame,
65            observation_id,
66            kind,
67            port,
68            correlation_epoch,
69            metadata,
70        }
71    }
72
73    /// Returns the proxy generation that produced this response.
74    pub fn generation(&self) -> u64 {
75        self.generation
76    }
77
78    /// Returns the validated canonical HNS host, without a port or user info.
79    pub fn host(&self) -> &ObservedHost {
80        &self.host
81    }
82
83    /// Returns the request method as a closed, payload-free classification.
84    pub fn method(&self) -> ObservedMethod {
85        self.method
86    }
87
88    /// Returns the validated response status selected for browser delivery.
89    /// Observation precedes the socket write, so client disconnects can still
90    /// prevent the browser from receiving it.
91    pub fn status_code(&self) -> u16 {
92        self.status_code
93    }
94
95    /// Whether the request matched the browser shell's conservative
96    /// main-frame-navigation heuristic.
97    pub fn is_likely_main_frame(&self) -> bool {
98        self.likely_main_frame
99    }
100
101    /// Opaque backend-local typed-status correlation. This value is never
102    /// serialized to the browser or native-messaging protocol.
103    pub fn observation_id(&self) -> Option<u64> {
104        self.observation_id
105    }
106
107    pub const fn kind(&self) -> ProxyMetadataObservationKind {
108        self.kind
109    }
110
111    pub const fn port(&self) -> Option<u16> {
112        self.port
113    }
114
115    pub const fn correlation_epoch(&self) -> Option<u64> {
116        self.correlation_epoch
117    }
118
119    /// Returns allowlisted internal metadata removed from the browser-facing
120    /// response head.
121    pub fn metadata(&self) -> &InternalResponseMetadata {
122        &self.metadata
123    }
124}
125
126impl fmt::Debug for ProxyResponseMetadataObservation {
127    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
128        formatter
129            .debug_struct("ProxyResponseMetadataObservation")
130            .field("generation", &self.generation)
131            .field("host", &self.host)
132            .field("method", &self.method)
133            .field("status_code", &self.status_code)
134            .field("likely_main_frame", &self.likely_main_frame)
135            .field("observation_id_present", &self.observation_id.is_some())
136            .field("kind", &self.kind)
137            .field("port", &self.port)
138            .field("correlation_epoch", &self.correlation_epoch)
139            .field("metadata", &self.metadata)
140            .finish()
141    }
142}
143
144/// Observer for validated, browser-relevant, sensitive response metadata.
145pub trait ProxyResponseMetadataObserver: Send + Sync + 'static {
146    /// Receives one typed, sensitive observation. Implementations must return
147    /// promptly, retain it only as narrowly as needed, and must not panic; the
148    /// proxy isolates unwinding observers.
149    fn observe(&self, observation: &ProxyResponseMetadataObservation);
150}
151
152impl<F> ProxyResponseMetadataObserver for F
153where
154    F: Fn(&ProxyResponseMetadataObservation) + Send + Sync + 'static,
155{
156    fn observe(&self, observation: &ProxyResponseMetadataObservation) {
157        self(observation);
158    }
159}
160
161/// Metadata observer used by the compatibility [`crate::RunningProxy::start`]
162/// entry point.
163#[derive(Clone, Copy, Debug, Default)]
164pub struct NoopProxyResponseMetadataObserver;
165
166impl ProxyResponseMetadataObserver for NoopProxyResponseMetadataObserver {
167    fn observe(&self, _observation: &ProxyResponseMetadataObservation) {}
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use crate::sanitize_response_headers;
174
175    #[test]
176    fn observation_debug_redacts_sensitive_metadata_values() {
177        let headers = sanitize_response_headers(&[
178            (
179                "X-HNS-Resolution-Trace".to_owned(),
180                "private?token=secret".to_owned(),
181            ),
182            ("X-HNS-Security-Path".to_owned(), "dane".to_owned()),
183        ])
184        .unwrap();
185        let observation = ProxyResponseMetadataObservation::new(
186            7,
187            ObservedHost::new("welcome").unwrap(),
188            ObservedMethod::Get,
189            200,
190            true,
191            Some(9),
192            ProxyMetadataObservationKind::OriginResponse,
193            None,
194            None,
195            headers.metadata().clone(),
196        );
197
198        assert_eq!(observation.generation(), 7);
199        assert_eq!(observation.host().as_str(), "welcome");
200        assert_eq!(observation.method(), ObservedMethod::Get);
201        assert_eq!(observation.status_code(), 200);
202        assert!(observation.is_likely_main_frame());
203        assert_eq!(observation.observation_id(), Some(9));
204        assert_eq!(
205            observation.metadata().get("X-HNS-Security-Path"),
206            Some("dane")
207        );
208        let diagnostic = format!("{observation:?}");
209        assert!(!diagnostic.contains("private"));
210        assert!(!diagnostic.contains("token"));
211        assert!(!diagnostic.contains("secret"));
212        assert!(!diagnostic.contains("path"));
213        assert!(!diagnostic.contains("query"));
214        assert!(!diagnostic.contains("body"));
215    }
216}