Skip to main content

http_quik/client/
request.rs

1use crate::profile::ChromeProfile;
2use http::header::{HeaderMap, HeaderValue, ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, USER_AGENT};
3
4/// Defines the context of the network request, mimicking browser fetch metadata.
5#[allow(dead_code)]
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum RequestContext {
8    /// A top-level page navigation (e.g., clicking a link or typing in the URL bar).
9    Navigate,
10    /// An asynchronous XMLHttpRequest or Fetch API call.
11    Xhr,
12    /// A form submission navigation.
13    Form,
14    /// An iframe navigation.
15    Iframe,
16    /// A parser-blocking or async script subresource.
17    NoCorsScript,
18    /// A stylesheet subresource.
19    NoCorsStyle,
20    /// An image subresource.
21    NoCorsImage,
22    /// A font subresource.
23    NoCorsFont,
24    /// A media subresource.
25    NoCorsMedia,
26    /// A Web Worker script.
27    Worker,
28    /// A Service Worker script.
29    ServiceWorker,
30    /// A prefetch request.
31    Prefetch,
32}
33
34/// Injects Chrome-identical headers into the provided request map.
35///
36/// Populates navigation metadata, Client Hints, compression preferences,
37/// and HPACK sensitivity flags in the exact order and format emitted by
38/// the target Chrome version.
39///
40/// ## Cross-Platform Consistency
41/// The `sec-ch-ua-platform` and `sec-ch-ua-platform-version` values are
42/// sourced from the active [`ChromeProfile`], ensuring they match the
43/// OS persona declared during the TLS handshake.
44///
45/// ## HPACK Sensitivity
46/// `cookie` and `authorization` headers are marked as sensitive to force
47/// the HPACK encoder into "Literal Never Indexed" mode, preventing
48/// side-channel leaks (CRIME mitigation).
49pub fn inject_chrome_headers(
50    headers: &mut HeaderMap,
51    profile: &ChromeProfile,
52    sec_fetch_site: &str,
53    is_initial_navigation: bool,
54    context: RequestContext,
55    accept_ch: bool,
56    referer: Option<&str>,
57) {
58    // 1. Client Hints (Sec-CH-UA)
59    // These headers provide granular version and platform information to the server.
60    if let Ok(val) = HeaderValue::from_str(&profile.headers.sec_ch_ua) {
61        headers.insert("sec-ch-ua", val);
62    }
63    headers.insert("sec-ch-ua-mobile", HeaderValue::from_static("?0"));
64    if let Ok(val) = HeaderValue::from_str(&profile.headers.sec_ch_ua_platform) {
65        headers.insert("sec-ch-ua-platform", val);
66    }
67    // Chrome only sends platform-version if explicitly solicited via Accept-CH in previous responses.
68    if accept_ch {
69        if let Ok(val) = HeaderValue::from_str(&profile.headers.sec_ch_ua_platform_version) {
70            headers.insert("sec-ch-ua-platform-version", val);
71        }
72    }
73
74    // 2. Navigation / Fetch metadata
75    headers.insert("upgrade-insecure-requests", HeaderValue::from_static("1"));
76    if let Ok(val) = HeaderValue::from_str(&profile.headers.user_agent) {
77        headers.insert(USER_AGENT, val);
78    }
79
80    // Inject dynamic sec-fetch-* state based on the current redirect context.
81    if let Ok(val) = HeaderValue::from_str(sec_fetch_site) {
82        headers.insert("sec-fetch-site", val);
83    }
84    let (mode, dest, accept_val) = match context {
85        RequestContext::Navigate | RequestContext::Form => ("navigate", "document", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"),
86        RequestContext::Iframe => ("navigate", "iframe", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"),
87        RequestContext::Xhr => ("cors", "empty", "*/*"),
88        RequestContext::NoCorsScript => ("no-cors", "script", "*/*"),
89        RequestContext::NoCorsStyle => ("no-cors", "style", "text/css,*/*;q=0.1"),
90        RequestContext::NoCorsImage => ("no-cors", "image", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"),
91        RequestContext::NoCorsFont => ("no-cors", "font", "*/*"),
92        RequestContext::NoCorsMedia => ("no-cors", "video", "*/*"),
93        RequestContext::Worker => ("same-origin", "worker", "*/*"),
94        RequestContext::ServiceWorker => ("same-origin", "serviceworker", "*/*"),
95        RequestContext::Prefetch => ("no-cors", "empty", "*/*"),
96    };
97    headers.insert(ACCEPT, HeaderValue::from_static(accept_val));
98    headers.insert("sec-fetch-mode", HeaderValue::from_static(mode));
99
100    // The 'sec-fetch-user' header is present ONLY on the first hop of a user-initiated navigation.
101    if is_initial_navigation
102        && (context == RequestContext::Navigate
103            || context == RequestContext::Form
104            || context == RequestContext::Iframe)
105    {
106        headers.insert("sec-fetch-user", HeaderValue::from_static("?1"));
107    }
108
109    headers.insert("sec-fetch-dest", HeaderValue::from_static(dest));
110
111    if let Some(r) = referer {
112        if let Ok(val) = HeaderValue::from_str(r) {
113            headers.insert(http::header::REFERER, val);
114        }
115    }
116
117    // 3. Compression & Language
118    let encoding = if profile.headers.zstd_encoding {
119        "gzip, deflate, br, zstd"
120    } else {
121        "gzip, deflate, br"
122    };
123    headers.insert(ACCEPT_ENCODING, HeaderValue::from_static(encoding));
124    if let Ok(val) = HeaderValue::from_str(&profile.headers.accept_language) {
125        headers.insert(ACCEPT_LANGUAGE, val);
126    }
127
128    // 4. Chrome Priority Header (u=0, i for navigations)
129    if profile.headers.include_priority_header {
130        headers.insert("priority", HeaderValue::from_static("u=0, i"));
131    }
132
133    // 5. HPACK "Never Index" (Sensitive) markers.
134    // Chrome explicitly marks cookies and auth headers as sensitive. This forces
135    // the HPACK encoder to use the "Literal Header Field Never Indexed" representation,
136    // which prevents these values from entering the dynamic table (CRIME mitigation).
137    for (name, value) in headers.iter_mut() {
138        if name == "cookie" || name == "authorization" {
139            value.set_sensitive(true);
140        }
141    }
142
143    // TODO(agent): Intelligent `:path` indexing.
144    // If the request path exceeds a certain entropy/length threshold (e.g., > 40 chars
145    // for unique REST API IDs), we should flag the `:path` pseudo-header as sensitive
146    // to prevent dynamic table bloat, matching Chrome's behavior. This requires a patch
147    // in the upstream `0x676e67/http2` fork to support `no_index` on pseudo-headers.
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use crate::profile::chrome_134::chrome_134_windows_x64;
154
155    /// Verifies correct mapping of various request contexts to standard Fetch Metadata headers.
156    ///
157    /// The mapping matches Chrome's behavioral matrix, ensuring that header fields like
158    /// `sec-fetch-dest`, `sec-fetch-mode`, and `sec-fetch-user` are accurately set based on
159    /// the context (e.g., Navigate, Xhr, NoCorsImage, ServiceWorker).
160    #[test]
161    fn test_inject_chrome_headers_context_mapping() {
162        let profile = chrome_134_windows_x64();
163
164        // Scenario 1: Navigation context (Navigate)
165        // High-entropy platform hints should NOT be leaked unsolicited on the initial request.
166        let mut headers = HeaderMap::new();
167        inject_chrome_headers(
168            &mut headers,
169            &profile,
170            "same-origin",
171            true,
172            RequestContext::Navigate,
173            false,
174            None,
175        );
176        assert_eq!(
177            headers.get("sec-fetch-dest").unwrap().to_str().unwrap(),
178            "document"
179        );
180        assert_eq!(
181            headers.get("sec-fetch-mode").unwrap().to_str().unwrap(),
182            "navigate"
183        );
184        assert_eq!(
185            headers.get("sec-fetch-user").unwrap().to_str().unwrap(),
186            "?1"
187        );
188        assert!(headers.get("sec-ch-ua-platform-version").is_none());
189
190        // Scenario 2: Standard API request (Xhr)
191        // Platform hints are present if solicited or configured statefully.
192        let mut headers = HeaderMap::new();
193        inject_chrome_headers(
194            &mut headers,
195            &profile,
196            "cross-site",
197            false,
198            RequestContext::Xhr,
199            true,
200            None,
201        );
202        assert_eq!(
203            headers.get("sec-fetch-dest").unwrap().to_str().unwrap(),
204            "empty"
205        );
206        assert_eq!(
207            headers.get("sec-fetch-mode").unwrap().to_str().unwrap(),
208            "cors"
209        );
210        assert!(headers.get("sec-fetch-user").is_none());
211        assert_eq!(
212            headers
213                .get("sec-ch-ua-platform-version")
214                .unwrap()
215                .to_str()
216                .unwrap(),
217            "\"15.0.0\""
218        ); // Windows 11 platform version with double quotes
219
220        // Scenario 3: Image fetch (NoCorsImage)
221        // Checks that the specific Accept header is set to Chrome's default image formats.
222        let mut headers = HeaderMap::new();
223        inject_chrome_headers(
224            &mut headers,
225            &profile,
226            "same-site",
227            false,
228            RequestContext::NoCorsImage,
229            false,
230            None,
231        );
232        assert_eq!(
233            headers.get("sec-fetch-dest").unwrap().to_str().unwrap(),
234            "image"
235        );
236        assert_eq!(
237            headers.get("sec-fetch-mode").unwrap().to_str().unwrap(),
238            "no-cors"
239        );
240        assert!(headers
241            .get("accept")
242            .unwrap()
243            .to_str()
244            .unwrap()
245            .contains("image/avif"));
246
247        // Scenario 4: Background script execution (ServiceWorker)
248        // Tests that specific background process contexts map appropriately.
249        let mut headers = HeaderMap::new();
250        inject_chrome_headers(
251            &mut headers,
252            &profile,
253            "same-origin",
254            false,
255            RequestContext::ServiceWorker,
256            false,
257            None,
258        );
259        assert_eq!(
260            headers.get("sec-fetch-dest").unwrap().to_str().unwrap(),
261            "serviceworker"
262        );
263        assert_eq!(
264            headers.get("sec-fetch-mode").unwrap().to_str().unwrap(),
265            "same-origin"
266        );
267    }
268
269    /// Verifies that sensitive authorization and session headers are explicitly marked.
270    ///
271    /// Marking headers like `cookie` and `authorization` as sensitive ensures they are
272    /// flagged as "never indexed" in HTTP/2 HPACK compression context, defending against
273    /// local/remote side-channel extraction attacks.
274    #[test]
275    fn test_inject_sensitive_headers_marked_properly() {
276        let profile = chrome_134_windows_x64();
277        let mut headers = HeaderMap::new();
278        headers.insert("cookie", HeaderValue::from_static("session=123"));
279        headers.insert("authorization", HeaderValue::from_static("Bearer token"));
280        headers.insert("host", HeaderValue::from_static("example.com"));
281
282        inject_chrome_headers(
283            &mut headers,
284            &profile,
285            "none",
286            true,
287            RequestContext::Navigate,
288            false,
289            None,
290        );
291
292        // Assert sensitive flags are strictly flipped to active
293        assert!(headers.get("cookie").unwrap().is_sensitive());
294        assert!(headers.get("authorization").unwrap().is_sensitive());
295
296        // Non-sensitive transport headers must not be marked as sensitive
297        assert!(!headers.get("host").unwrap().is_sensitive());
298    }
299}