Skip to main content

guise_profiles/
lib.rs

1//! Pure browser fingerprint data — the canonical source for browser identity.
2//!
3//! Holds the `StealthProfile` selector, per-profile `ProfileFacts` (User-Agent,
4//! navigation headers, hardware, client hints), and the canonical `HeaderProfile`
5//! header catalog. This crate intentionally has no runtime, HTTP, TLS, browser, or
6//! async dependencies. It sits below `scanclient` and `stealth` so both crates can
7//! derive browser identity from one source without creating a dependency cycle.
8//!
9//! Naming: `StealthProfile` is the canonical selector; `ProfileFacts` is its data;
10//! a `<Domain>Profile` (e.g. `HeaderProfile`) is a pure derived projection for one
11//! domain. One type per projection — `HeaderProfile` is the single browser-header
12//! catalog type, re-exported unchanged by `scanclient` and `stealth`.
13
14#![forbid(unsafe_code)]
15#![deny(unreachable_patterns)]
16#![warn(missing_docs)]
17
18/// Named browser fingerprint variants. Each one identifies a coherent
19/// (browser, OS, GPU class) tuple used by higher-level stealth crates.
20///
21/// `#[non_exhaustive]` - adding a new variant is a minor-version change,
22/// removing one is major. UA strings inside each variant are also additive
23/// maintenance data, but the browser/OS family shape stays stable.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25#[non_exhaustive]
26pub enum StealthProfile {
27    /// Chrome stable on Windows 10/11, Intel iGPU.
28    ChromeWindowsStable,
29    /// Chrome 96 on Windows 10 for legacy differential and compatibility probes.
30    ChromeWindowsLegacy96,
31    /// Chrome stable on macOS, Apple Silicon GPU.
32    ChromeMacStable,
33    /// Microsoft Edge on Windows, same Chromium core but distinct brand.
34    EdgeWindowsStable,
35    /// Internet Explorer 11 on Windows 8.1 for legacy resolver fallback paths.
36    Ie11Windows,
37    /// Firefox on Linux desktop.
38    FirefoxLinux,
39    /// Firefox on Windows desktop.
40    FirefoxWindows,
41    /// Chrome on Android.
42    ChromeAndroid,
43    /// Safari on iPhone.
44    SafariIphone,
45    /// Safari on iPad.
46    SafariIpad,
47    /// Safari on macOS desktop.
48    SafariMacStable,
49    /// Chrome on Linux desktop.
50    ChromeLinux,
51    /// Brave browser on Windows.
52    BraveWindows,
53    /// Opera on Windows.
54    OperaWindows,
55    /// Samsung Internet on Galaxy phone.
56    SamsungInternetAndroid,
57}
58
59/// Canonical default stealth identity for fleet-owned browser and HTTP launches.
60pub const DEFAULT_STEALTH_PROFILE: StealthProfile = StealthProfile::ChromeWindowsStable;
61
62/// Every canonical browser identity profile in the catalog.
63pub const ALL_PROFILES: &[StealthProfile] = &[
64    DEFAULT_STEALTH_PROFILE,
65    StealthProfile::ChromeWindowsLegacy96,
66    StealthProfile::ChromeMacStable,
67    StealthProfile::EdgeWindowsStable,
68    StealthProfile::Ie11Windows,
69    StealthProfile::FirefoxLinux,
70    StealthProfile::FirefoxWindows,
71    StealthProfile::ChromeAndroid,
72    StealthProfile::SafariIphone,
73    StealthProfile::SafariIpad,
74    StealthProfile::SafariMacStable,
75    StealthProfile::ChromeLinux,
76    StealthProfile::BraveWindows,
77    StealthProfile::OperaWindows,
78    StealthProfile::SamsungInternetAndroid,
79];
80
81/// Profiles intended for deterministic fleet rotation.
82///
83/// Legacy compatibility personas such as IE11 and Chrome 96 are intentionally
84/// excluded from normal rotation; callers can still request them explicitly by
85/// variant or by [`named_profile`].
86pub const ROTATION_PROFILES: &[StealthProfile] = &[
87    DEFAULT_STEALTH_PROFILE,
88    StealthProfile::ChromeMacStable,
89    StealthProfile::EdgeWindowsStable,
90    StealthProfile::FirefoxLinux,
91    StealthProfile::FirefoxWindows,
92    StealthProfile::ChromeAndroid,
93    StealthProfile::SafariIphone,
94    StealthProfile::SafariIpad,
95    StealthProfile::SafariMacStable,
96    StealthProfile::ChromeLinux,
97    StealthProfile::BraveWindows,
98    StealthProfile::OperaWindows,
99    StealthProfile::SamsungInternetAndroid,
100];
101
102/// Stable lowercase profile name for operator-facing config.
103#[must_use]
104pub const fn profile_name(profile: StealthProfile) -> &'static str {
105    match profile {
106        StealthProfile::ChromeWindowsStable => "chrome",
107        StealthProfile::ChromeWindowsLegacy96 => "chrome-windows-legacy-96",
108        StealthProfile::ChromeMacStable => "chrome-macos",
109        StealthProfile::EdgeWindowsStable => "edge",
110        StealthProfile::Ie11Windows => "ie11-windows",
111        StealthProfile::FirefoxLinux => "firefox",
112        StealthProfile::FirefoxWindows => "firefox-windows",
113        StealthProfile::ChromeAndroid => "chrome-android",
114        StealthProfile::SafariIphone => "safari-iphone",
115        StealthProfile::SafariIpad => "safari-ipad",
116        StealthProfile::SafariMacStable => "safari",
117        StealthProfile::ChromeLinux => "chrome-linux",
118        StealthProfile::BraveWindows => "brave",
119        StealthProfile::OperaWindows => "opera",
120        StealthProfile::SamsungInternetAndroid => "samsung-internet",
121    }
122}
123
124/// Stable enum-style profile name for human-readable listings.
125#[must_use]
126pub const fn profile_display_name(profile: StealthProfile) -> &'static str {
127    match profile {
128        StealthProfile::ChromeWindowsStable => "ChromeWindowsStable",
129        StealthProfile::ChromeWindowsLegacy96 => "ChromeWindowsLegacy96",
130        StealthProfile::ChromeMacStable => "ChromeMacStable",
131        StealthProfile::EdgeWindowsStable => "EdgeWindowsStable",
132        StealthProfile::Ie11Windows => "Ie11Windows",
133        StealthProfile::FirefoxLinux => "FirefoxLinux",
134        StealthProfile::FirefoxWindows => "FirefoxWindows",
135        StealthProfile::ChromeAndroid => "ChromeAndroid",
136        StealthProfile::SafariIphone => "SafariIphone",
137        StealthProfile::SafariIpad => "SafariIpad",
138        StealthProfile::SafariMacStable => "SafariMacStable",
139        StealthProfile::ChromeLinux => "ChromeLinux",
140        StealthProfile::BraveWindows => "BraveWindows",
141        StealthProfile::OperaWindows => "OperaWindows",
142        StealthProfile::SamsungInternetAndroid => "SamsungInternetAndroid",
143    }
144}
145
146/// Resolve a config profile name or common alias to a canonical profile.
147#[must_use]
148pub fn named_profile(name: &str) -> Option<StealthProfile> {
149    let normalized = name.trim().to_ascii_lowercase();
150    match normalized.as_str() {
151        "chrome"
152        | "chrome-windows"
153        | "chrome-win"
154        | "chrome_131_windows"
155        | "chrome_windows"
156        | "chromewindowsstable" => Some(StealthProfile::ChromeWindowsStable),
157        "chrome-windows-legacy-96"
158        | "chrome_96_windows"
159        | "chrome_windows_legacy_96"
160        | "chromewindowslegacy96" => Some(StealthProfile::ChromeWindowsLegacy96),
161        "chrome-macos" | "chrome-mac" | "chrome-osx" | "chrome_131_macos" | "chrome_mac"
162        | "chromemacstable" => Some(StealthProfile::ChromeMacStable),
163        "edge" | "edge-windows" | "edge_131" | "edge_windows" | "edgewindowsstable" => {
164            Some(StealthProfile::EdgeWindowsStable)
165        }
166        "ie11" | "ie" | "internet-explorer" | "ie11-windows" | "ie11_windows" | "ie11windows" => {
167            Some(StealthProfile::Ie11Windows)
168        }
169        "firefox" | "firefox-linux" | "firefox_133" | "firefox_linux" | "firefoxlinux" => {
170            Some(StealthProfile::FirefoxLinux)
171        }
172        "firefox-windows" | "firefox_windows" | "firefox_133_windows" | "firefoxwindows" => {
173            Some(StealthProfile::FirefoxWindows)
174        }
175        "chrome-android" | "chrome_android" | "android" | "chromeandroid" => {
176            Some(StealthProfile::ChromeAndroid)
177        }
178        "safari-iphone" | "safari_iphone" | "iphone" | "safariiphone" => {
179            Some(StealthProfile::SafariIphone)
180        }
181        "safari-ipad" | "safari_ipad" | "ipad" | "safariipad" => Some(StealthProfile::SafariIpad),
182        "safari" | "safari-mac" | "safari_17_5" | "safari_mac" | "safarimacstable" => {
183            Some(StealthProfile::SafariMacStable)
184        }
185        "chrome-linux" | "chrome_linux" | "chromelinux" => Some(StealthProfile::ChromeLinux),
186        "brave" | "brave-windows" | "brave_windows" | "bravewindows" => {
187            Some(StealthProfile::BraveWindows)
188        }
189        "opera" | "opera-windows" | "opera_windows" | "operawindows" => {
190            Some(StealthProfile::OperaWindows)
191        }
192        "samsung-internet" | "samsung_internet" | "samsung" | "samsunginternetandroid" => {
193            Some(StealthProfile::SamsungInternetAndroid)
194        }
195        _ => None,
196    }
197}
198
199/// Stable browser identity facts shared by HTTP clients and browser
200/// fingerprint layers.
201#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202pub struct ProfileFacts {
203    /// Canonical User-Agent string.
204    pub user_agent: &'static str,
205    /// `navigator.platform` value coherent with the User-Agent OS family.
206    pub platform: &'static str,
207    /// `navigator.languages` / Accept-Language base language list.
208    pub languages: &'static [&'static str],
209    /// Browser-shaped Accept header for top-level document navigation.
210    pub accept: &'static str,
211    /// Browser-shaped Accept-Language header for top-level document navigation.
212    pub accept_language: &'static str,
213    /// Browser-shaped Accept-Encoding header for top-level document navigation.
214    pub accept_encoding: &'static str,
215    /// `userAgentData.mobile` browser-family flag.
216    pub mobile: bool,
217    /// Default screen width for this persona.
218    pub screen_width: u32,
219    /// Default screen height for this persona.
220    pub screen_height: u32,
221}
222
223/// Browser family parsed from a User-Agent string.
224#[derive(Debug, Clone, Copy, PartialEq, Eq)]
225pub enum UserAgentBrowser {
226    /// Chromium or Google Chrome.
227    Chrome,
228    /// Microsoft Edge.
229    Edge,
230    /// Mozilla Firefox.
231    Firefox,
232    /// Apple Safari.
233    Safari,
234    /// Internet Explorer.
235    InternetExplorer,
236    /// Opera.
237    Opera,
238    /// Samsung Internet.
239    SamsungInternet,
240    /// No supported browser token was present.
241    Unknown,
242}
243
244/// Operating-system family parsed from a User-Agent string.
245#[derive(Debug, Clone, Copy, PartialEq, Eq)]
246pub enum UserAgentPlatform {
247    /// Android.
248    Android,
249    /// iPhone or iPad iOS/iPadOS.
250    Ios,
251    /// macOS.
252    MacOs,
253    /// Windows.
254    Windows,
255    /// Linux desktop.
256    Linux,
257    /// No supported platform token was present.
258    Unknown,
259}
260
261impl UserAgentPlatform {
262    /// Low-entropy Client Hint platform value, including browser-required quotes.
263    #[must_use]
264    pub const fn client_hint_value(self) -> Option<&'static str> {
265        match self {
266            Self::Android => Some("\"Android\""),
267            Self::Ios => Some("\"iOS\""),
268            Self::MacOs => Some("\"macOS\""),
269            Self::Windows => Some("\"Windows\""),
270            Self::Linux => Some("\"Linux\""),
271            Self::Unknown => None,
272        }
273    }
274
275    /// Platform label used by the Chrome TLS diagnostic catalogue.
276    #[must_use]
277    pub const fn chrome_tls_label(self) -> Option<&'static str> {
278        match self {
279            Self::Android => Some("Android"),
280            Self::MacOs => Some("macOS"),
281            Self::Windows => Some("Windows"),
282            Self::Linux => Some("Linux"),
283            Self::Ios | Self::Unknown => None,
284        }
285    }
286
287    /// Whether this platform is a mobile browser surface.
288    #[must_use]
289    pub const fn is_mobile(self) -> bool {
290        matches!(self, Self::Android | Self::Ios)
291    }
292}
293
294/// Parsed browser identity facts from a User-Agent string.
295#[derive(Debug, Clone, Copy, PartialEq, Eq)]
296pub struct UserAgentFacts {
297    /// Parsed browser family.
298    pub browser: UserAgentBrowser,
299    /// Parsed platform family.
300    pub platform: UserAgentPlatform,
301    /// Major version for the parsed browser family.
302    pub browser_major_version: Option<u32>,
303    /// Major Chromium engine version when the UA carries a Chromium token.
304    pub chromium_major_version: Option<u32>,
305    /// Whether the UA leaks a headless Chromium token.
306    pub headless: bool,
307    /// Best-effort mapping to a canonical stealth profile.
308    pub inferred_profile: Option<StealthProfile>,
309    /// Whether the UA represents a mobile browser surface.
310    pub mobile: bool,
311}
312
313impl UserAgentFacts {
314    /// Client Hint platform value derived from the parsed platform.
315    #[must_use]
316    pub const fn client_hint_platform_value(self) -> Option<&'static str> {
317        self.platform.client_hint_value()
318    }
319
320    /// Client Hint mobile value derived from the parsed platform and mobile token.
321    #[must_use]
322    pub const fn client_hint_mobile_value(self) -> &'static str {
323        if self.mobile {
324            "?1"
325        } else {
326            "?0"
327        }
328    }
329}
330
331/// Parse browser, platform, version, and stealth-profile facts from a User-Agent string.
332#[must_use]
333pub fn user_agent_facts(user_agent: &str) -> UserAgentFacts {
334    let browser = user_agent_browser(user_agent);
335    let platform = user_agent_platform(user_agent);
336    let headless =
337        user_agent.contains("HeadlessChrome/") || user_agent.contains("HeadlessChromium/");
338    let mobile = platform.is_mobile() || user_agent.contains("Mobile");
339    let chromium_major_version = first_major_after(
340        user_agent,
341        &[
342            "HeadlessChrome/",
343            "HeadlessChromium/",
344            "Chrome/",
345            "Chromium/",
346        ],
347    );
348    let browser_major_version = match browser {
349        UserAgentBrowser::Chrome => chromium_major_version,
350        UserAgentBrowser::Edge => major_after(user_agent, "Edg/"),
351        UserAgentBrowser::Firefox => major_after(user_agent, "Firefox/"),
352        UserAgentBrowser::Safari => major_after(user_agent, "Version/"),
353        UserAgentBrowser::InternetExplorer => {
354            major_after(user_agent, "MSIE ").or_else(|| major_after(user_agent, "rv:"))
355        }
356        UserAgentBrowser::Opera => major_after(user_agent, "OPR/"),
357        UserAgentBrowser::SamsungInternet => major_after(user_agent, "SamsungBrowser/"),
358        UserAgentBrowser::Unknown => None,
359    };
360
361    UserAgentFacts {
362        browser,
363        platform,
364        browser_major_version,
365        chromium_major_version,
366        headless,
367        inferred_profile: profile_from_user_agent_facts(
368            user_agent,
369            browser,
370            platform,
371            browser_major_version,
372        ),
373        mobile,
374    }
375}
376
377/// Infer the closest canonical stealth profile from a User-Agent string.
378#[must_use]
379pub fn infer_profile_from_user_agent(user_agent: &str) -> Option<StealthProfile> {
380    user_agent_facts(user_agent).inferred_profile
381}
382
383fn user_agent_browser(user_agent: &str) -> UserAgentBrowser {
384    if user_agent.contains("Trident/") || user_agent.contains("MSIE ") {
385        UserAgentBrowser::InternetExplorer
386    } else if user_agent.contains("Edg/") {
387        UserAgentBrowser::Edge
388    } else if user_agent.contains("SamsungBrowser/") {
389        UserAgentBrowser::SamsungInternet
390    } else if user_agent.contains("OPR/") {
391        UserAgentBrowser::Opera
392    } else if user_agent.contains("Firefox/") {
393        UserAgentBrowser::Firefox
394    } else if user_agent.contains("HeadlessChrome/")
395        || user_agent.contains("HeadlessChromium/")
396        || user_agent.contains("Chrome/")
397        || user_agent.contains("Chromium/")
398    {
399        UserAgentBrowser::Chrome
400    } else if user_agent.contains("Safari/") && user_agent.contains("Version/") {
401        UserAgentBrowser::Safari
402    } else {
403        UserAgentBrowser::Unknown
404    }
405}
406
407fn user_agent_platform(user_agent: &str) -> UserAgentPlatform {
408    if user_agent.contains("Android") {
409        UserAgentPlatform::Android
410    } else if user_agent.contains("iPhone") || user_agent.contains("iPad") {
411        UserAgentPlatform::Ios
412    } else if user_agent.contains("Macintosh") || user_agent.contains("Mac OS X") {
413        UserAgentPlatform::MacOs
414    } else if user_agent.contains("Windows") {
415        UserAgentPlatform::Windows
416    } else if user_agent.contains("Linux") || user_agent.contains("X11") {
417        UserAgentPlatform::Linux
418    } else {
419        UserAgentPlatform::Unknown
420    }
421}
422
423fn profile_from_user_agent_facts(
424    user_agent: &str,
425    browser: UserAgentBrowser,
426    platform: UserAgentPlatform,
427    browser_major_version: Option<u32>,
428) -> Option<StealthProfile> {
429    match browser {
430        UserAgentBrowser::InternetExplorer => Some(StealthProfile::Ie11Windows),
431        UserAgentBrowser::Edge => Some(StealthProfile::EdgeWindowsStable),
432        UserAgentBrowser::Firefox => match platform {
433            UserAgentPlatform::Windows => Some(StealthProfile::FirefoxWindows),
434            _ => Some(StealthProfile::FirefoxLinux),
435        },
436        UserAgentBrowser::Safari => {
437            if user_agent.contains("iPhone") {
438                Some(StealthProfile::SafariIphone)
439            } else if user_agent.contains("iPad") {
440                Some(StealthProfile::SafariIpad)
441            } else {
442                Some(StealthProfile::SafariMacStable)
443            }
444        }
445        UserAgentBrowser::SamsungInternet => Some(StealthProfile::SamsungInternetAndroid),
446        UserAgentBrowser::Opera => Some(StealthProfile::OperaWindows),
447        UserAgentBrowser::Chrome => match platform {
448            UserAgentPlatform::Android => Some(StealthProfile::ChromeAndroid),
449            UserAgentPlatform::MacOs => Some(StealthProfile::ChromeMacStable),
450            UserAgentPlatform::Linux => Some(StealthProfile::ChromeLinux),
451            UserAgentPlatform::Windows => {
452                if browser_major_version.is_some_and(|major| major <= 96) {
453                    Some(StealthProfile::ChromeWindowsLegacy96)
454                } else {
455                    Some(StealthProfile::ChromeWindowsStable)
456                }
457            }
458            UserAgentPlatform::Ios | UserAgentPlatform::Unknown => None,
459        },
460        UserAgentBrowser::Unknown => None,
461    }
462}
463
464fn first_major_after(user_agent: &str, tokens: &[&str]) -> Option<u32> {
465    tokens
466        .iter()
467        .find_map(|token| major_after(user_agent, token))
468}
469
470fn major_after(user_agent: &str, token: &str) -> Option<u32> {
471    let rest = user_agent.split_once(token)?.1;
472    let end = rest
473        .find(|ch: char| ch == '.' || ch == ' ' || ch == ';' || ch == ')')
474        .unwrap_or(rest.len());
475    rest[..end].parse().ok()
476}
477
478/// HTTP header name for the profile User-Agent identity header.
479pub const USER_AGENT_HEADER: &str = "user-agent";
480
481/// HTTP header name for the profile top-level navigation Accept header.
482pub const ACCEPT_HEADER: &str = "accept";
483
484/// HTTP header name for the profile Accept-Language identity header.
485pub const ACCEPT_LANGUAGE_HEADER: &str = "accept-language";
486
487/// HTTP header name for the profile Accept-Encoding negotiation header.
488pub const ACCEPT_ENCODING_HEADER: &str = "accept-encoding";
489
490/// HTTP header name for `Upgrade-Insecure-Requests`.
491pub const UPGRADE_INSECURE_REQUESTS_HEADER: &str = "upgrade-insecure-requests";
492
493/// HTTP header name for `Sec-Fetch-Dest`.
494pub const SEC_FETCH_DEST_HEADER: &str = "sec-fetch-dest";
495
496/// HTTP header name for `Sec-Fetch-Mode`.
497pub const SEC_FETCH_MODE_HEADER: &str = "sec-fetch-mode";
498
499/// HTTP header name for `Sec-Fetch-Site`.
500pub const SEC_FETCH_SITE_HEADER: &str = "sec-fetch-site";
501
502/// HTTP header name for `Sec-Fetch-User`.
503pub const SEC_FETCH_USER_HEADER: &str = "sec-fetch-user";
504
505/// Browser request surface to materialize.
506#[derive(Debug, Clone, Copy, PartialEq, Eq)]
507#[non_exhaustive]
508pub enum BrowserRequestKind {
509    /// Top-level document navigation.
510    Navigation,
511    /// Top-level document navigation from the same origin.
512    SameOriginNavigation,
513    /// Top-level document navigation from another site.
514    CrossSiteNavigation,
515    /// Same-origin `fetch`/XHR request to an application endpoint.
516    SameOriginFetch,
517    /// Same-origin `fetch`/XHR request with explicit `same-origin` mode.
518    SameOriginModeFetch,
519    /// Cross-site `fetch`/XHR request to an application endpoint.
520    CrossSiteFetch,
521    /// Image element fetch for an image resource.
522    ImageSubresource,
523    /// Media element fetch for an audio resource.
524    AudioSubresource,
525}
526
527/// Browser-compatible display casing for a canonical navigation header name.
528///
529/// Profile catalog header names stay lower-case so `http::HeaderName::from_static`
530/// can consume them directly. String-map transports that preserve caller-facing
531/// header names use this helper to emit the same browser-shaped casing everywhere.
532#[must_use]
533pub fn canonical_navigation_header_name(name: &str) -> &str {
534    match name {
535        USER_AGENT_HEADER => "User-Agent",
536        ACCEPT_HEADER => "Accept",
537        ACCEPT_LANGUAGE_HEADER => "Accept-Language",
538        ACCEPT_ENCODING_HEADER => "Accept-Encoding",
539        UPGRADE_INSECURE_REQUESTS_HEADER => "Upgrade-Insecure-Requests",
540        SEC_FETCH_DEST_HEADER => "Sec-Fetch-Dest",
541        SEC_FETCH_MODE_HEADER => "Sec-Fetch-Mode",
542        SEC_FETCH_SITE_HEADER => "Sec-Fetch-Site",
543        SEC_FETCH_USER_HEADER => "Sec-Fetch-User",
544        _ => name,
545    }
546}
547
548/// Header/value pair for browser identity defaults.
549#[derive(Debug, Clone, Copy, PartialEq, Eq)]
550pub struct NavigationHeader {
551    /// Lower-case HTTP header name suitable for `http::HeaderName::from_static`.
552    pub name: &'static str,
553    /// Header value derived from the selected browser profile.
554    pub value: &'static str,
555}
556
557const EMPTY_HEADER: NavigationHeader = NavigationHeader {
558    name: "",
559    value: "",
560};
561
562/// Fixed browser request header set.
563#[derive(Debug, Clone, Copy, PartialEq, Eq)]
564pub struct BrowserRequestHeaders {
565    entries: [NavigationHeader; 9],
566    len: usize,
567}
568
569impl BrowserRequestHeaders {
570    /// Borrow only the populated header entries.
571    #[must_use]
572    pub fn as_slice(&self) -> &[NavigationHeader] {
573        &self.entries[..self.len]
574    }
575
576    /// Number of populated header entries.
577    #[must_use]
578    pub const fn len(&self) -> usize {
579        self.len
580    }
581
582    /// True if no entries are populated.
583    #[must_use]
584    pub const fn is_empty(&self) -> bool {
585        self.len == 0
586    }
587}
588
589const WILDCARD_ACCEPT: &str = "*/*";
590const UPGRADE_INSECURE_REQUESTS_VALUE: &str = "1";
591const DOCUMENT_DEST_VALUE: &str = "document";
592const EMPTY_DEST_VALUE: &str = "empty";
593const IMAGE_DEST_VALUE: &str = "image";
594const AUDIO_DEST_VALUE: &str = "audio";
595const NAVIGATE_MODE_VALUE: &str = "navigate";
596const CORS_MODE_VALUE: &str = "cors";
597const SAME_ORIGIN_MODE_VALUE: &str = "same-origin";
598const NO_CORS_MODE_VALUE: &str = "no-cors";
599const NONE_SITE_VALUE: &str = "none";
600const SAME_ORIGIN_SITE_VALUE: &str = "same-origin";
601const CROSS_SITE_VALUE: &str = "cross-site";
602const FETCH_USER_ACTIVATED_VALUE: &str = "?1";
603
604const EN_US_EN: &[&str] = &["en-US", "en"];
605
606/// Chromium-class top-level navigation Accept header.
607pub const CHROMIUM_NAVIGATION_ACCEPT: &str = "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";
608
609/// Firefox top-level navigation Accept header.
610pub const FIREFOX_NAVIGATION_ACCEPT: &str =
611    "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8";
612
613/// Internet Explorer 11 top-level navigation Accept header.
614pub const IE11_NAVIGATION_ACCEPT: &str = "text/html, application/xhtml+xml, */*";
615
616/// Safari top-level navigation Accept header.
617pub const SAFARI_NAVIGATION_ACCEPT: &str =
618    "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
619
620/// Default Chromium/Safari Accept-Language weighting.
621pub const DEFAULT_ACCEPT_LANGUAGE: &str = "en-US,en;q=0.9";
622
623/// Default Firefox Accept-Language weighting.
624pub const FIREFOX_ACCEPT_LANGUAGE: &str = "en-US,en;q=0.5";
625
626/// Default browser Accept-Encoding set for shared Santh scanner transports.
627pub const DEFAULT_ACCEPT_ENCODING: &str = "gzip, deflate, br";
628
629/// Legacy browser Accept-Encoding set for pre-Brotli navigation stacks.
630pub const LEGACY_ACCEPT_ENCODING: &str = "gzip, deflate";
631
632/// Canonical browser HTTP **header projection** of a [`StealthProfile`].
633///
634/// One of the `<Domain>Profile` facets (see the crate-level vocabulary): a pure,
635/// `const`-friendly view of the request headers a given browser identity sends.
636/// `scanclient`/`karyx` re-export this directly (header data without the heavy
637/// `stealth` stack); `stealth::http`/`stealth::fingerprint` re-export it as the
638/// single header-catalog type — it replaces the former duplicate `BrowserProfile`
639/// structs in `stealth::fingerprint::browser_catalog` and this crate.
640#[derive(Debug, Clone, Copy, PartialEq, Eq)]
641pub struct HeaderProfile {
642    /// Stable profile name used by config and CLI flags.
643    pub name: &'static str,
644    /// User-Agent header value.
645    pub user_agent: &'static str,
646    /// Accept header value.
647    pub accept: &'static str,
648    /// Accept-Language header value.
649    pub accept_language: &'static str,
650    /// Accept-Encoding header value.
651    pub accept_encoding: &'static str,
652    /// `Sec-Fetch-Site` navigation value.
653    pub sec_fetch_site: &'static str,
654    /// `Sec-Fetch-Mode` navigation value.
655    pub sec_fetch_mode: &'static str,
656    /// `Sec-Fetch-Dest` navigation value.
657    pub sec_fetch_dest: &'static str,
658}
659
660impl HeaderProfile {
661    /// Browser-shaped HTTP headers represented by this compatibility profile.
662    ///
663    /// Unlike [`profile_navigation_headers`], this legacy compatibility view
664    /// includes `Accept-Encoding` because `scanclient::tls_profiles` has
665    /// exposed that field since before the canonical profile catalog existed.
666    #[must_use]
667    pub const fn headers(self) -> [NavigationHeader; 4] {
668        [
669            NavigationHeader {
670                name: USER_AGENT_HEADER,
671                value: self.user_agent,
672            },
673            NavigationHeader {
674                name: ACCEPT_HEADER,
675                value: self.accept,
676            },
677            NavigationHeader {
678                name: ACCEPT_LANGUAGE_HEADER,
679                value: self.accept_language,
680            },
681            NavigationHeader {
682                name: ACCEPT_ENCODING_HEADER,
683                value: self.accept_encoding,
684            },
685        ]
686    }
687}
688
689/// Common browser HTTP profiles used by scanner transports.
690pub static PROFILES: &[HeaderProfile] = &[
691    browser_profile("chrome", StealthProfile::ChromeWindowsStable),
692    browser_profile("firefox", StealthProfile::FirefoxLinux),
693    browser_profile("safari", StealthProfile::SafariMacStable),
694    browser_profile("edge", StealthProfile::EdgeWindowsStable),
695];
696
697const DEFAULT_PROFILE: HeaderProfile = browser_profile("default", DEFAULT_STEALTH_PROFILE);
698
699const fn browser_profile(name: &'static str, profile: StealthProfile) -> HeaderProfile {
700    let facts = profile_facts(profile);
701    HeaderProfile {
702        name,
703        user_agent: facts.user_agent,
704        accept: facts.accept,
705        accept_language: facts.accept_language,
706        accept_encoding: facts.accept_encoding,
707        sec_fetch_site: "none",
708        sec_fetch_mode: "navigate",
709        sec_fetch_dest: "document",
710    }
711}
712
713/// Resolve a common browser HTTP profile by its stable config name.
714#[must_use]
715pub fn get_profile(name: &str) -> Option<&'static HeaderProfile> {
716    PROFILES.iter().find(|profile| profile.name == name)
717}
718
719/// Deterministically rotate through common browser HTTP profiles.
720#[must_use]
721pub fn rotate(index: usize) -> &'static HeaderProfile {
722    if PROFILES.is_empty() {
723        return &DEFAULT_PROFILE;
724    }
725    &PROFILES[index % PROFILES.len()]
726}
727
728/// A coherent hardware/display tuple for a browser fingerprint profile.
729#[derive(Debug, Clone, Copy, PartialEq, Eq)]
730pub struct ProfileHardware {
731    /// `screen.width`.
732    pub screen_width: u32,
733    /// `screen.height`.
734    pub screen_height: u32,
735    /// `screen.colorDepth` / `screen.pixelDepth`.
736    pub color_depth: u8,
737    /// `navigator.deviceMemory` GB.
738    pub device_memory: u8,
739    /// `navigator.hardwareConcurrency`.
740    pub hardware_concurrency: u8,
741    /// WebGL `UNMASKED_VENDOR_WEBGL`.
742    pub webgl_vendor: &'static str,
743    /// WebGL `UNMASKED_RENDERER_WEBGL`.
744    pub webgl_renderer: &'static str,
745}
746
747const CHROME_WINDOWS_HARDWARE: &[ProfileHardware] = &[
748    ProfileHardware {
749        screen_width: 1920,
750        screen_height: 1080,
751        color_depth: 24,
752        device_memory: 8,
753        hardware_concurrency: 8,
754        webgl_vendor: "Google Inc. (Intel)",
755        webgl_renderer:
756            "ANGLE (Intel, Intel(R) Iris(R) Xe Graphics Direct3D11 vs_5_0 ps_5_0, D3D11)",
757    },
758    ProfileHardware {
759        screen_width: 1920,
760        screen_height: 1080,
761        color_depth: 24,
762        device_memory: 16,
763        hardware_concurrency: 12,
764        webgl_vendor: "Google Inc. (NVIDIA)",
765        webgl_renderer: "ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Direct3D11 vs_5_0 ps_5_0, D3D11)",
766    },
767    ProfileHardware {
768        screen_width: 2560,
769        screen_height: 1440,
770        color_depth: 24,
771        device_memory: 16,
772        hardware_concurrency: 16,
773        webgl_vendor: "Google Inc. (AMD)",
774        webgl_renderer: "ANGLE (AMD, AMD Radeon RX 6700 XT Direct3D11 vs_5_0 ps_5_0, D3D11)",
775    },
776    ProfileHardware {
777        screen_width: 1366,
778        screen_height: 768,
779        color_depth: 24,
780        device_memory: 8,
781        hardware_concurrency: 8,
782        webgl_vendor: "Google Inc. (Intel)",
783        webgl_renderer: "ANGLE (Intel, Intel(R) UHD Graphics 630 Direct3D11 vs_5_0 ps_5_0, D3D11)",
784    },
785    ProfileHardware {
786        screen_width: 1920,
787        screen_height: 1080,
788        color_depth: 24,
789        device_memory: 32,
790        hardware_concurrency: 16,
791        webgl_vendor: "Google Inc. (NVIDIA)",
792        webgl_renderer: "ANGLE (NVIDIA, NVIDIA GeForce RTX 4070 Direct3D11 vs_5_0 ps_5_0, D3D11)",
793    },
794];
795
796const CHROME_WINDOWS_LEGACY_96_HARDWARE: &[ProfileHardware] = &[ProfileHardware {
797    screen_width: 1366,
798    screen_height: 768,
799    color_depth: 24,
800    device_memory: 8,
801    hardware_concurrency: 8,
802    webgl_vendor: "Google Inc. (Intel)",
803    webgl_renderer: "ANGLE (Intel, Intel(R) UHD Graphics 630 Direct3D11 vs_5_0 ps_5_0, D3D11)",
804}];
805
806const IE11_WINDOWS_HARDWARE: &[ProfileHardware] = &[ProfileHardware {
807    screen_width: 1366,
808    screen_height: 768,
809    color_depth: 24,
810    device_memory: 4,
811    hardware_concurrency: 4,
812    webgl_vendor: "Microsoft",
813    webgl_renderer: "Internet Explorer 11",
814}];
815
816const CHROME_MAC_HARDWARE: &[ProfileHardware] = &[ProfileHardware {
817    screen_width: 1728,
818    screen_height: 1117,
819    color_depth: 30,
820    device_memory: 16,
821    hardware_concurrency: 10,
822    webgl_vendor: "Google Inc. (Apple)",
823    webgl_renderer: "ANGLE (Apple, ANGLE Metal Renderer: Apple M1 Pro, Unspecified Version)",
824}];
825
826const EDGE_WINDOWS_HARDWARE: &[ProfileHardware] = &[ProfileHardware {
827    screen_width: 1920,
828    screen_height: 1080,
829    color_depth: 24,
830    device_memory: 8,
831    hardware_concurrency: 8,
832    webgl_vendor: "Google Inc. (Intel)",
833    webgl_renderer: "ANGLE (Intel, Intel(R) UHD Graphics Direct3D11 vs_5_0 ps_5_0, D3D11)",
834}];
835
836const FIREFOX_LINUX_HARDWARE: &[ProfileHardware] = &[ProfileHardware {
837    screen_width: 1920,
838    screen_height: 1080,
839    color_depth: 24,
840    device_memory: 8,
841    hardware_concurrency: 8,
842    // Native passthrough: empty = expose the host's real, Gecko-sanitized WebGL
843    // adapter rather than a constant. FirefoxLinux is a matched-host persona
844    // (Firefox on Linux, run on Firefox/Linux), so the real adapter
845    // ("NVIDIA Corporation" / "NVIDIA GeForce <card>, or similar") is already
846    // low-entropy AND its rendered pixels match — strictly more coherent than
847    // the former "Mesa Intel Iris Xe" string, which both claimed an iGPU the
848    // host may not have and contradicted the actual pixels. A genuinely
849    // cross-OS Firefox persona must carry a coherent renderer for its claimed
850    // OS instead (see `profile_js`, which only pins WebGL when this is set).
851    webgl_vendor: "",
852    webgl_renderer: "",
853}];
854
855const FIREFOX_WINDOWS_HARDWARE: &[ProfileHardware] = &[ProfileHardware {
856    screen_width: 1920,
857    screen_height: 1080,
858    color_depth: 24,
859    device_memory: 8,
860    hardware_concurrency: 8,
861    webgl_vendor: "Google Inc. (Intel)",
862    webgl_renderer: "ANGLE (Intel, Intel(R) UHD Graphics Direct3D11 vs_5_0 ps_5_0, D3D11)",
863}];
864
865const CHROME_ANDROID_HARDWARE: &[ProfileHardware] = &[ProfileHardware {
866    screen_width: 412,
867    screen_height: 915,
868    color_depth: 24,
869    device_memory: 6,
870    hardware_concurrency: 8,
871    webgl_vendor: "Qualcomm",
872    webgl_renderer: "Adreno (TM) 740",
873}];
874
875const SAFARI_IPHONE_HARDWARE: &[ProfileHardware] = &[ProfileHardware {
876    screen_width: 390,
877    screen_height: 844,
878    color_depth: 24,
879    device_memory: 4,
880    hardware_concurrency: 6,
881    webgl_vendor: "Apple Inc.",
882    webgl_renderer: "Apple GPU",
883}];
884
885const SAFARI_IPAD_HARDWARE: &[ProfileHardware] = &[ProfileHardware {
886    screen_width: 1024,
887    screen_height: 1366,
888    color_depth: 24,
889    device_memory: 8,
890    hardware_concurrency: 8,
891    webgl_vendor: "Apple Inc.",
892    webgl_renderer: "Apple GPU",
893}];
894
895const SAFARI_MAC_HARDWARE: &[ProfileHardware] = &[ProfileHardware {
896    screen_width: 1728,
897    screen_height: 1117,
898    color_depth: 30,
899    device_memory: 16,
900    hardware_concurrency: 10,
901    webgl_vendor: "Apple Inc.",
902    webgl_renderer: "Apple M2",
903}];
904
905const CHROME_LINUX_HARDWARE: &[ProfileHardware] = &[
906    ProfileHardware {
907        screen_width: 1920,
908        screen_height: 1080,
909        color_depth: 24,
910        device_memory: 8,
911        hardware_concurrency: 8,
912        webgl_vendor: "Mesa",
913        webgl_renderer: "Mesa Intel(R) UHD Graphics 770 (ADL-S GT1)",
914    },
915    ProfileHardware {
916        screen_width: 1920,
917        screen_height: 1080,
918        color_depth: 24,
919        device_memory: 32,
920        hardware_concurrency: 8,
921        webgl_vendor: "Google Inc. (NVIDIA)",
922        webgl_renderer: "ANGLE (NVIDIA, NVIDIA GeForce GTX 1660 SUPER/PCIe/SSE2, OpenGL 4.5)",
923    },
924];
925
926const BRAVE_WINDOWS_HARDWARE: &[ProfileHardware] = &[ProfileHardware {
927    screen_width: 1920,
928    screen_height: 1080,
929    color_depth: 24,
930    device_memory: 8,
931    hardware_concurrency: 8,
932    webgl_vendor: "Brave",
933    webgl_renderer: "Brave",
934}];
935
936const OPERA_WINDOWS_HARDWARE: &[ProfileHardware] = &[ProfileHardware {
937    screen_width: 1920,
938    screen_height: 1080,
939    color_depth: 24,
940    device_memory: 8,
941    hardware_concurrency: 8,
942    webgl_vendor: "Google Inc. (Intel)",
943    webgl_renderer: "ANGLE (Intel, Intel(R) UHD Graphics 770 Direct3D11 vs_5_0 ps_5_0, D3D11)",
944}];
945
946const SAMSUNG_INTERNET_HARDWARE: &[ProfileHardware] = &[ProfileHardware {
947    screen_width: 412,
948    screen_height: 915,
949    color_depth: 24,
950    device_memory: 8,
951    hardware_concurrency: 8,
952    webgl_vendor: "Qualcomm",
953    webgl_renderer: "Adreno (TM) 750",
954}];
955
956/// Hardware/display tuples coherent with a browser fingerprint profile.
957#[must_use]
958pub const fn profile_hardware_variants(profile: StealthProfile) -> &'static [ProfileHardware] {
959    match profile {
960        StealthProfile::ChromeWindowsStable => CHROME_WINDOWS_HARDWARE,
961        StealthProfile::ChromeWindowsLegacy96 => CHROME_WINDOWS_LEGACY_96_HARDWARE,
962        StealthProfile::ChromeMacStable => CHROME_MAC_HARDWARE,
963        StealthProfile::EdgeWindowsStable => EDGE_WINDOWS_HARDWARE,
964        StealthProfile::Ie11Windows => IE11_WINDOWS_HARDWARE,
965        StealthProfile::FirefoxLinux => FIREFOX_LINUX_HARDWARE,
966        StealthProfile::FirefoxWindows => FIREFOX_WINDOWS_HARDWARE,
967        StealthProfile::ChromeAndroid => CHROME_ANDROID_HARDWARE,
968        StealthProfile::SafariIphone => SAFARI_IPHONE_HARDWARE,
969        StealthProfile::SafariIpad => SAFARI_IPAD_HARDWARE,
970        StealthProfile::SafariMacStable => SAFARI_MAC_HARDWARE,
971        StealthProfile::ChromeLinux => CHROME_LINUX_HARDWARE,
972        StealthProfile::BraveWindows => BRAVE_WINDOWS_HARDWARE,
973        StealthProfile::OperaWindows => OPERA_WINDOWS_HARDWARE,
974        StealthProfile::SamsungInternetAndroid => SAMSUNG_INTERNET_HARDWARE,
975    }
976}
977
978/// Default hardware/display tuple for a browser fingerprint profile.
979#[must_use]
980pub const fn profile_hardware(profile: StealthProfile) -> ProfileHardware {
981    profile_hardware_variants(profile)[0]
982}
983
984/// Deterministically select a profile-coherent hardware/display tuple.
985#[must_use]
986pub const fn profile_hardware_at(profile: StealthProfile, index: usize) -> ProfileHardware {
987    let variants = profile_hardware_variants(profile);
988    variants[index % variants.len()]
989}
990
991/// One low-entropy User-Agent Client Hint brand entry for a browser profile.
992#[derive(Debug, Clone, Copy, PartialEq, Eq)]
993pub struct ProfileClientHintBrand {
994    /// Browser brand token.
995    pub brand: &'static str,
996    /// Major version string paired with the brand.
997    pub version: &'static str,
998}
999
1000const NO_CLIENT_HINT_BRANDS: &[ProfileClientHintBrand] = &[];
1001
1002const CHROMIUM_131_BRANDS: &[ProfileClientHintBrand] = &[
1003    ProfileClientHintBrand {
1004        brand: "Chromium",
1005        version: "131",
1006    },
1007    ProfileClientHintBrand {
1008        brand: "Google Chrome",
1009        version: "131",
1010    },
1011    ProfileClientHintBrand {
1012        brand: "Not?A_Brand",
1013        version: "99",
1014    },
1015];
1016
1017const CHROMIUM_96_BRANDS: &[ProfileClientHintBrand] = &[
1018    ProfileClientHintBrand {
1019        brand: "Chromium",
1020        version: "96",
1021    },
1022    ProfileClientHintBrand {
1023        brand: "Google Chrome",
1024        version: "96",
1025    },
1026    ProfileClientHintBrand {
1027        brand: "Not?A_Brand",
1028        version: "99",
1029    },
1030];
1031
1032const EDGE_131_BRANDS: &[ProfileClientHintBrand] = &[
1033    ProfileClientHintBrand {
1034        brand: "Chromium",
1035        version: "131",
1036    },
1037    ProfileClientHintBrand {
1038        brand: "Microsoft Edge",
1039        version: "131",
1040    },
1041    ProfileClientHintBrand {
1042        brand: "Not?A_Brand",
1043        version: "99",
1044    },
1045];
1046
1047const BRAVE_131_BRANDS: &[ProfileClientHintBrand] = &[
1048    ProfileClientHintBrand {
1049        brand: "Brave",
1050        version: "131",
1051    },
1052    ProfileClientHintBrand {
1053        brand: "Chromium",
1054        version: "131",
1055    },
1056    ProfileClientHintBrand {
1057        brand: "Not?A_Brand",
1058        version: "99",
1059    },
1060];
1061
1062const OPERA_116_BRANDS: &[ProfileClientHintBrand] = &[
1063    ProfileClientHintBrand {
1064        brand: "Chromium",
1065        version: "131",
1066    },
1067    ProfileClientHintBrand {
1068        brand: "Opera",
1069        version: "116",
1070    },
1071    ProfileClientHintBrand {
1072        brand: "Not?A_Brand",
1073        version: "99",
1074    },
1075];
1076
1077const SAMSUNG_INTERNET_26_BRANDS: &[ProfileClientHintBrand] = &[
1078    ProfileClientHintBrand {
1079        brand: "Samsung Internet",
1080        version: "26",
1081    },
1082    ProfileClientHintBrand {
1083        brand: "Chromium",
1084        version: "126",
1085    },
1086    ProfileClientHintBrand {
1087        brand: "Not?A_Brand",
1088        version: "99",
1089    },
1090];
1091
1092/// `navigator.vendor` value coherent with a browser fingerprint profile.
1093#[must_use]
1094pub const fn profile_navigator_vendor(profile: StealthProfile) -> &'static str {
1095    match profile {
1096        StealthProfile::ChromeWindowsStable
1097        | StealthProfile::ChromeWindowsLegacy96
1098        | StealthProfile::ChromeMacStable
1099        | StealthProfile::EdgeWindowsStable
1100        | StealthProfile::ChromeAndroid
1101        | StealthProfile::ChromeLinux
1102        | StealthProfile::BraveWindows
1103        | StealthProfile::OperaWindows
1104        | StealthProfile::SamsungInternetAndroid => "Google Inc.",
1105        StealthProfile::SafariIphone
1106        | StealthProfile::SafariIpad
1107        | StealthProfile::SafariMacStable => "Apple Computer, Inc.",
1108        StealthProfile::Ie11Windows
1109        | StealthProfile::FirefoxLinux
1110        | StealthProfile::FirefoxWindows => "",
1111    }
1112}
1113
1114/// Low-entropy User-Agent Client Hint brands coherent with a browser profile.
1115#[must_use]
1116pub const fn profile_client_hint_brands(
1117    profile: StealthProfile,
1118) -> &'static [ProfileClientHintBrand] {
1119    match profile {
1120        StealthProfile::ChromeWindowsStable
1121        | StealthProfile::ChromeMacStable
1122        | StealthProfile::ChromeAndroid
1123        | StealthProfile::ChromeLinux => CHROMIUM_131_BRANDS,
1124        StealthProfile::ChromeWindowsLegacy96 => CHROMIUM_96_BRANDS,
1125        StealthProfile::EdgeWindowsStable => EDGE_131_BRANDS,
1126        StealthProfile::BraveWindows => BRAVE_131_BRANDS,
1127        StealthProfile::OperaWindows => OPERA_116_BRANDS,
1128        StealthProfile::SamsungInternetAndroid => SAMSUNG_INTERNET_26_BRANDS,
1129        StealthProfile::Ie11Windows
1130        | StealthProfile::FirefoxLinux
1131        | StealthProfile::FirefoxWindows
1132        | StealthProfile::SafariIphone
1133        | StealthProfile::SafariIpad
1134        | StealthProfile::SafariMacStable => NO_CLIENT_HINT_BRANDS,
1135    }
1136}
1137
1138/// Low-entropy `Sec-CH-UA-Platform` value coherent with a browser profile.
1139#[must_use]
1140pub const fn profile_client_hint_platform(profile: StealthProfile) -> Option<&'static str> {
1141    match profile {
1142        StealthProfile::ChromeWindowsStable
1143        | StealthProfile::ChromeWindowsLegacy96
1144        | StealthProfile::EdgeWindowsStable
1145        | StealthProfile::BraveWindows
1146        | StealthProfile::OperaWindows => Some("Windows"),
1147        StealthProfile::ChromeMacStable => Some("macOS"),
1148        StealthProfile::ChromeAndroid | StealthProfile::SamsungInternetAndroid => Some("Android"),
1149        StealthProfile::ChromeLinux => Some("Linux"),
1150        StealthProfile::Ie11Windows
1151        | StealthProfile::FirefoxLinux
1152        | StealthProfile::FirefoxWindows
1153        | StealthProfile::SafariIphone
1154        | StealthProfile::SafariIpad
1155        | StealthProfile::SafariMacStable => None,
1156    }
1157}
1158
1159/// Canonical User-Agent for [`StealthProfile::ChromeWindowsStable`].
1160pub const CHROME_WINDOWS_STABLE_USER_AGENT: &str =
1161    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 \
1162                         (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
1163
1164/// Canonical User-Agent for [`StealthProfile::ChromeWindowsLegacy96`].
1165pub const CHROME_WINDOWS_LEGACY_96_USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
1166     AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36";
1167
1168/// Canonical User-Agent for [`StealthProfile::FirefoxWindows`].
1169pub const FIREFOX_WINDOWS_STABLE_USER_AGENT: &str =
1170    "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0";
1171
1172/// Canonical User-Agent for [`StealthProfile::Ie11Windows`].
1173pub const IE11_WINDOWS_USER_AGENT: &str =
1174    "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko";
1175
1176/// Canonical identity facts for a stealth profile.
1177#[must_use]
1178pub const fn profile_facts(profile: StealthProfile) -> ProfileFacts {
1179    match profile {
1180        StealthProfile::ChromeWindowsStable => ProfileFacts {
1181            user_agent: CHROME_WINDOWS_STABLE_USER_AGENT,
1182            platform: "Win32",
1183            languages: EN_US_EN,
1184            accept: CHROMIUM_NAVIGATION_ACCEPT,
1185            accept_language: DEFAULT_ACCEPT_LANGUAGE,
1186            accept_encoding: DEFAULT_ACCEPT_ENCODING,
1187            mobile: false,
1188            screen_width: 1920,
1189            screen_height: 1080,
1190        },
1191        StealthProfile::ChromeWindowsLegacy96 => ProfileFacts {
1192            user_agent: CHROME_WINDOWS_LEGACY_96_USER_AGENT,
1193            platform: "Win32",
1194            languages: EN_US_EN,
1195            accept: CHROMIUM_NAVIGATION_ACCEPT,
1196            accept_language: DEFAULT_ACCEPT_LANGUAGE,
1197            accept_encoding: DEFAULT_ACCEPT_ENCODING,
1198            mobile: false,
1199            screen_width: 1366,
1200            screen_height: 768,
1201        },
1202        StealthProfile::ChromeMacStable => ProfileFacts {
1203            user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 \
1204                         (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
1205            platform: "MacIntel",
1206            languages: EN_US_EN,
1207            accept: CHROMIUM_NAVIGATION_ACCEPT,
1208            accept_language: DEFAULT_ACCEPT_LANGUAGE,
1209            accept_encoding: DEFAULT_ACCEPT_ENCODING,
1210            mobile: false,
1211            screen_width: 1728,
1212            screen_height: 1117,
1213        },
1214        StealthProfile::EdgeWindowsStable => ProfileFacts {
1215            user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 \
1216                         (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0",
1217            platform: "Win32",
1218            languages: EN_US_EN,
1219            accept: CHROMIUM_NAVIGATION_ACCEPT,
1220            accept_language: DEFAULT_ACCEPT_LANGUAGE,
1221            accept_encoding: DEFAULT_ACCEPT_ENCODING,
1222            mobile: false,
1223            screen_width: 1920,
1224            screen_height: 1080,
1225        },
1226        StealthProfile::Ie11Windows => ProfileFacts {
1227            user_agent: IE11_WINDOWS_USER_AGENT,
1228            platform: "Win32",
1229            languages: EN_US_EN,
1230            accept: IE11_NAVIGATION_ACCEPT,
1231            accept_language: DEFAULT_ACCEPT_LANGUAGE,
1232            accept_encoding: LEGACY_ACCEPT_ENCODING,
1233            mobile: false,
1234            screen_width: 1366,
1235            screen_height: 768,
1236        },
1237        StealthProfile::FirefoxLinux => ProfileFacts {
1238            user_agent: "Mozilla/5.0 (X11; Linux x86_64; rv:133.0) Gecko/20100101 Firefox/133.0",
1239            platform: "Linux x86_64",
1240            languages: EN_US_EN,
1241            accept: FIREFOX_NAVIGATION_ACCEPT,
1242            accept_language: FIREFOX_ACCEPT_LANGUAGE,
1243            accept_encoding: DEFAULT_ACCEPT_ENCODING,
1244            mobile: false,
1245            screen_width: 1920,
1246            screen_height: 1080,
1247        },
1248        StealthProfile::FirefoxWindows => ProfileFacts {
1249            user_agent: FIREFOX_WINDOWS_STABLE_USER_AGENT,
1250            platform: "Win32",
1251            languages: EN_US_EN,
1252            accept: FIREFOX_NAVIGATION_ACCEPT,
1253            accept_language: FIREFOX_ACCEPT_LANGUAGE,
1254            accept_encoding: DEFAULT_ACCEPT_ENCODING,
1255            mobile: false,
1256            screen_width: 1920,
1257            screen_height: 1080,
1258        },
1259        StealthProfile::ChromeAndroid => ProfileFacts {
1260            user_agent: "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 \
1261                         (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36",
1262            platform: "Linux armv8l",
1263            languages: EN_US_EN,
1264            accept: CHROMIUM_NAVIGATION_ACCEPT,
1265            accept_language: DEFAULT_ACCEPT_LANGUAGE,
1266            accept_encoding: DEFAULT_ACCEPT_ENCODING,
1267            mobile: true,
1268            screen_width: 412,
1269            screen_height: 915,
1270        },
1271        StealthProfile::SafariIphone => ProfileFacts {
1272            user_agent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) \
1273                         AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 \
1274                         Mobile/15E148 Safari/604.1",
1275            platform: "iPhone",
1276            languages: EN_US_EN,
1277            accept: SAFARI_NAVIGATION_ACCEPT,
1278            accept_language: DEFAULT_ACCEPT_LANGUAGE,
1279            accept_encoding: DEFAULT_ACCEPT_ENCODING,
1280            mobile: true,
1281            screen_width: 390,
1282            screen_height: 844,
1283        },
1284        StealthProfile::SafariIpad => ProfileFacts {
1285            user_agent: "Mozilla/5.0 (iPad; CPU OS 17_5 like Mac OS X) AppleWebKit/605.1.15 \
1286                         (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1",
1287            platform: "iPad",
1288            languages: EN_US_EN,
1289            accept: SAFARI_NAVIGATION_ACCEPT,
1290            accept_language: DEFAULT_ACCEPT_LANGUAGE,
1291            accept_encoding: DEFAULT_ACCEPT_ENCODING,
1292            mobile: true,
1293            screen_width: 1024,
1294            screen_height: 1366,
1295        },
1296        StealthProfile::SafariMacStable => ProfileFacts {
1297            user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) \
1298                         AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15",
1299            platform: "MacIntel",
1300            languages: EN_US_EN,
1301            accept: SAFARI_NAVIGATION_ACCEPT,
1302            accept_language: DEFAULT_ACCEPT_LANGUAGE,
1303            accept_encoding: DEFAULT_ACCEPT_ENCODING,
1304            mobile: false,
1305            screen_width: 1728,
1306            screen_height: 1117,
1307        },
1308        StealthProfile::ChromeLinux => ProfileFacts {
1309            user_agent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 \
1310                         (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
1311            platform: "Linux x86_64",
1312            languages: EN_US_EN,
1313            accept: CHROMIUM_NAVIGATION_ACCEPT,
1314            accept_language: DEFAULT_ACCEPT_LANGUAGE,
1315            accept_encoding: DEFAULT_ACCEPT_ENCODING,
1316            mobile: false,
1317            screen_width: 1920,
1318            screen_height: 1080,
1319        },
1320        StealthProfile::BraveWindows => ProfileFacts {
1321            user_agent: CHROME_WINDOWS_STABLE_USER_AGENT,
1322            platform: "Win32",
1323            languages: EN_US_EN,
1324            accept: CHROMIUM_NAVIGATION_ACCEPT,
1325            accept_language: DEFAULT_ACCEPT_LANGUAGE,
1326            accept_encoding: DEFAULT_ACCEPT_ENCODING,
1327            mobile: false,
1328            screen_width: 1920,
1329            screen_height: 1080,
1330        },
1331        StealthProfile::OperaWindows => ProfileFacts {
1332            user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 \
1333                         (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 OPR/116.0.0.0",
1334            platform: "Win32",
1335            languages: EN_US_EN,
1336            accept: CHROMIUM_NAVIGATION_ACCEPT,
1337            accept_language: DEFAULT_ACCEPT_LANGUAGE,
1338            accept_encoding: DEFAULT_ACCEPT_ENCODING,
1339            mobile: false,
1340            screen_width: 1920,
1341            screen_height: 1080,
1342        },
1343        StealthProfile::SamsungInternetAndroid => ProfileFacts {
1344            user_agent: "Mozilla/5.0 (Linux; Android 14; SM-S928B) AppleWebKit/537.36 \
1345                         (KHTML, like Gecko) SamsungBrowser/26.0 Chrome/126.0.0.0 \
1346                         Mobile Safari/537.36",
1347            platform: "Linux armv8l",
1348            languages: EN_US_EN,
1349            accept: CHROMIUM_NAVIGATION_ACCEPT,
1350            accept_language: DEFAULT_ACCEPT_LANGUAGE,
1351            accept_encoding: DEFAULT_ACCEPT_ENCODING,
1352            mobile: true,
1353            screen_width: 412,
1354            screen_height: 915,
1355        },
1356    }
1357}
1358
1359/// Canonical identity facts for [`DEFAULT_STEALTH_PROFILE`].
1360#[must_use]
1361pub const fn default_profile_facts() -> ProfileFacts {
1362    profile_facts(DEFAULT_STEALTH_PROFILE)
1363}
1364
1365/// Canonical User-Agent for a stealth profile.
1366#[must_use]
1367pub const fn profile_user_agent(profile: StealthProfile) -> &'static str {
1368    profile_facts(profile).user_agent
1369}
1370
1371/// Canonical User-Agent for [`DEFAULT_STEALTH_PROFILE`].
1372#[must_use]
1373pub const fn default_profile_user_agent() -> &'static str {
1374    default_profile_facts().user_agent
1375}
1376
1377/// Canonical profile-backed headers for top-level browser-like HTTP navigation.
1378///
1379/// The returned names are lower-case so crates using the `http`/`reqwest`
1380/// header types can install them with `HeaderName::from_static` without
1381/// allocation or fallible parsing. `Accept-Encoding` is intentionally left to
1382/// the transport so compression negotiation and automatic decompression remain
1383/// controlled by the HTTP stack.
1384#[must_use]
1385pub const fn profile_navigation_headers(profile: StealthProfile) -> [NavigationHeader; 3] {
1386    let facts = profile_facts(profile);
1387    [
1388        NavigationHeader {
1389            name: USER_AGENT_HEADER,
1390            value: facts.user_agent,
1391        },
1392        NavigationHeader {
1393            name: ACCEPT_HEADER,
1394            value: facts.accept,
1395        },
1396        NavigationHeader {
1397            name: ACCEPT_LANGUAGE_HEADER,
1398            value: facts.accept_language,
1399        },
1400    ]
1401}
1402
1403/// Canonical profile-backed navigation headers for [`DEFAULT_STEALTH_PROFILE`].
1404#[must_use]
1405pub const fn default_profile_navigation_headers() -> [NavigationHeader; 3] {
1406    profile_navigation_headers(DEFAULT_STEALTH_PROFILE)
1407}
1408
1409/// Canonical browser HTTP headers including compression negotiation.
1410///
1411/// Scanner transports that own response decompression can use this complete
1412/// browser-shaped set. Transports that need to avoid advertising compressed
1413/// bodies should use [`profile_navigation_headers`] instead.
1414#[must_use]
1415pub const fn profile_browser_headers(profile: StealthProfile) -> [NavigationHeader; 4] {
1416    let facts = profile_facts(profile);
1417    [
1418        NavigationHeader {
1419            name: USER_AGENT_HEADER,
1420            value: facts.user_agent,
1421        },
1422        NavigationHeader {
1423            name: ACCEPT_HEADER,
1424            value: facts.accept,
1425        },
1426        NavigationHeader {
1427            name: ACCEPT_LANGUAGE_HEADER,
1428            value: facts.accept_language,
1429        },
1430        NavigationHeader {
1431            name: ACCEPT_ENCODING_HEADER,
1432            value: facts.accept_encoding,
1433        },
1434    ]
1435}
1436
1437/// Canonical browser HTTP headers for [`DEFAULT_STEALTH_PROFILE`].
1438#[must_use]
1439pub const fn default_profile_browser_headers() -> [NavigationHeader; 4] {
1440    profile_browser_headers(DEFAULT_STEALTH_PROFILE)
1441}
1442
1443/// Canonical browser request headers for a request surface, including
1444/// compression negotiation.
1445#[must_use]
1446pub const fn profile_request_headers(
1447    profile: StealthProfile,
1448    kind: BrowserRequestKind,
1449) -> BrowserRequestHeaders {
1450    profile_request_headers_inner(profile, kind, true)
1451}
1452
1453/// Canonical request-surface headers for [`DEFAULT_STEALTH_PROFILE`].
1454#[must_use]
1455pub const fn default_profile_request_headers(kind: BrowserRequestKind) -> BrowserRequestHeaders {
1456    profile_request_headers(DEFAULT_STEALTH_PROFILE, kind)
1457}
1458
1459/// Canonical browser request headers for a request surface without
1460/// `Accept-Encoding`.
1461///
1462/// Use this for transports that do not own transparent response
1463/// decompression but still need the request's browser fetch metadata.
1464#[must_use]
1465pub const fn profile_request_headers_without_compression(
1466    profile: StealthProfile,
1467    kind: BrowserRequestKind,
1468) -> BrowserRequestHeaders {
1469    profile_request_headers_inner(profile, kind, false)
1470}
1471
1472/// Canonical request-surface headers without `Accept-Encoding` for [`DEFAULT_STEALTH_PROFILE`].
1473#[must_use]
1474pub const fn default_profile_request_headers_without_compression(
1475    kind: BrowserRequestKind,
1476) -> BrowserRequestHeaders {
1477    profile_request_headers_without_compression(DEFAULT_STEALTH_PROFILE, kind)
1478}
1479
1480const fn profile_request_headers_inner(
1481    profile: StealthProfile,
1482    kind: BrowserRequestKind,
1483    include_compression: bool,
1484) -> BrowserRequestHeaders {
1485    let facts = profile_facts(profile);
1486    let surface = request_surface_facts(kind, facts.accept);
1487
1488    if surface.upgrade_insecure_requests {
1489        if include_compression {
1490            BrowserRequestHeaders {
1491                entries: [
1492                    NavigationHeader {
1493                        name: USER_AGENT_HEADER,
1494                        value: facts.user_agent,
1495                    },
1496                    NavigationHeader {
1497                        name: ACCEPT_HEADER,
1498                        value: surface.accept,
1499                    },
1500                    NavigationHeader {
1501                        name: ACCEPT_LANGUAGE_HEADER,
1502                        value: facts.accept_language,
1503                    },
1504                    NavigationHeader {
1505                        name: ACCEPT_ENCODING_HEADER,
1506                        value: facts.accept_encoding,
1507                    },
1508                    NavigationHeader {
1509                        name: UPGRADE_INSECURE_REQUESTS_HEADER,
1510                        value: UPGRADE_INSECURE_REQUESTS_VALUE,
1511                    },
1512                    NavigationHeader {
1513                        name: SEC_FETCH_DEST_HEADER,
1514                        value: surface.dest,
1515                    },
1516                    NavigationHeader {
1517                        name: SEC_FETCH_MODE_HEADER,
1518                        value: surface.mode,
1519                    },
1520                    NavigationHeader {
1521                        name: SEC_FETCH_SITE_HEADER,
1522                        value: surface.site,
1523                    },
1524                    NavigationHeader {
1525                        name: SEC_FETCH_USER_HEADER,
1526                        value: surface.fetch_user,
1527                    },
1528                ],
1529                len: 9,
1530            }
1531        } else {
1532            BrowserRequestHeaders {
1533                entries: [
1534                    NavigationHeader {
1535                        name: USER_AGENT_HEADER,
1536                        value: facts.user_agent,
1537                    },
1538                    NavigationHeader {
1539                        name: ACCEPT_HEADER,
1540                        value: surface.accept,
1541                    },
1542                    NavigationHeader {
1543                        name: ACCEPT_LANGUAGE_HEADER,
1544                        value: facts.accept_language,
1545                    },
1546                    NavigationHeader {
1547                        name: UPGRADE_INSECURE_REQUESTS_HEADER,
1548                        value: UPGRADE_INSECURE_REQUESTS_VALUE,
1549                    },
1550                    NavigationHeader {
1551                        name: SEC_FETCH_DEST_HEADER,
1552                        value: surface.dest,
1553                    },
1554                    NavigationHeader {
1555                        name: SEC_FETCH_MODE_HEADER,
1556                        value: surface.mode,
1557                    },
1558                    NavigationHeader {
1559                        name: SEC_FETCH_SITE_HEADER,
1560                        value: surface.site,
1561                    },
1562                    NavigationHeader {
1563                        name: SEC_FETCH_USER_HEADER,
1564                        value: surface.fetch_user,
1565                    },
1566                    EMPTY_HEADER,
1567                ],
1568                len: 8,
1569            }
1570        }
1571    } else if include_compression {
1572        BrowserRequestHeaders {
1573            entries: [
1574                NavigationHeader {
1575                    name: USER_AGENT_HEADER,
1576                    value: facts.user_agent,
1577                },
1578                NavigationHeader {
1579                    name: ACCEPT_HEADER,
1580                    value: surface.accept,
1581                },
1582                NavigationHeader {
1583                    name: ACCEPT_LANGUAGE_HEADER,
1584                    value: facts.accept_language,
1585                },
1586                NavigationHeader {
1587                    name: ACCEPT_ENCODING_HEADER,
1588                    value: facts.accept_encoding,
1589                },
1590                NavigationHeader {
1591                    name: SEC_FETCH_DEST_HEADER,
1592                    value: surface.dest,
1593                },
1594                NavigationHeader {
1595                    name: SEC_FETCH_MODE_HEADER,
1596                    value: surface.mode,
1597                },
1598                NavigationHeader {
1599                    name: SEC_FETCH_SITE_HEADER,
1600                    value: surface.site,
1601                },
1602                EMPTY_HEADER,
1603                EMPTY_HEADER,
1604            ],
1605            len: 7,
1606        }
1607    } else {
1608        BrowserRequestHeaders {
1609            entries: [
1610                NavigationHeader {
1611                    name: USER_AGENT_HEADER,
1612                    value: facts.user_agent,
1613                },
1614                NavigationHeader {
1615                    name: ACCEPT_HEADER,
1616                    value: surface.accept,
1617                },
1618                NavigationHeader {
1619                    name: ACCEPT_LANGUAGE_HEADER,
1620                    value: facts.accept_language,
1621                },
1622                NavigationHeader {
1623                    name: SEC_FETCH_DEST_HEADER,
1624                    value: surface.dest,
1625                },
1626                NavigationHeader {
1627                    name: SEC_FETCH_MODE_HEADER,
1628                    value: surface.mode,
1629                },
1630                NavigationHeader {
1631                    name: SEC_FETCH_SITE_HEADER,
1632                    value: surface.site,
1633                },
1634                EMPTY_HEADER,
1635                EMPTY_HEADER,
1636                EMPTY_HEADER,
1637            ],
1638            len: 6,
1639        }
1640    }
1641}
1642
1643struct RequestSurfaceFacts {
1644    accept: &'static str,
1645    dest: &'static str,
1646    mode: &'static str,
1647    site: &'static str,
1648    upgrade_insecure_requests: bool,
1649    fetch_user: &'static str,
1650}
1651
1652const fn request_surface_facts(
1653    kind: BrowserRequestKind,
1654    navigation_accept: &'static str,
1655) -> RequestSurfaceFacts {
1656    match kind {
1657        BrowserRequestKind::Navigation => RequestSurfaceFacts {
1658            accept: navigation_accept,
1659            dest: DOCUMENT_DEST_VALUE,
1660            mode: NAVIGATE_MODE_VALUE,
1661            site: NONE_SITE_VALUE,
1662            upgrade_insecure_requests: true,
1663            fetch_user: FETCH_USER_ACTIVATED_VALUE,
1664        },
1665        BrowserRequestKind::SameOriginNavigation => RequestSurfaceFacts {
1666            accept: navigation_accept,
1667            dest: DOCUMENT_DEST_VALUE,
1668            mode: NAVIGATE_MODE_VALUE,
1669            site: SAME_ORIGIN_SITE_VALUE,
1670            upgrade_insecure_requests: true,
1671            fetch_user: FETCH_USER_ACTIVATED_VALUE,
1672        },
1673        BrowserRequestKind::CrossSiteNavigation => RequestSurfaceFacts {
1674            accept: navigation_accept,
1675            dest: DOCUMENT_DEST_VALUE,
1676            mode: NAVIGATE_MODE_VALUE,
1677            site: CROSS_SITE_VALUE,
1678            upgrade_insecure_requests: true,
1679            fetch_user: FETCH_USER_ACTIVATED_VALUE,
1680        },
1681        BrowserRequestKind::SameOriginFetch => RequestSurfaceFacts {
1682            accept: WILDCARD_ACCEPT,
1683            dest: EMPTY_DEST_VALUE,
1684            mode: CORS_MODE_VALUE,
1685            site: SAME_ORIGIN_SITE_VALUE,
1686            upgrade_insecure_requests: false,
1687            fetch_user: "",
1688        },
1689        BrowserRequestKind::SameOriginModeFetch => RequestSurfaceFacts {
1690            accept: WILDCARD_ACCEPT,
1691            dest: EMPTY_DEST_VALUE,
1692            mode: SAME_ORIGIN_MODE_VALUE,
1693            site: SAME_ORIGIN_SITE_VALUE,
1694            upgrade_insecure_requests: false,
1695            fetch_user: "",
1696        },
1697        BrowserRequestKind::CrossSiteFetch => RequestSurfaceFacts {
1698            accept: WILDCARD_ACCEPT,
1699            dest: EMPTY_DEST_VALUE,
1700            mode: CORS_MODE_VALUE,
1701            site: CROSS_SITE_VALUE,
1702            upgrade_insecure_requests: false,
1703            fetch_user: "",
1704        },
1705        BrowserRequestKind::ImageSubresource => RequestSurfaceFacts {
1706            accept: WILDCARD_ACCEPT,
1707            dest: IMAGE_DEST_VALUE,
1708            mode: NO_CORS_MODE_VALUE,
1709            site: CROSS_SITE_VALUE,
1710            upgrade_insecure_requests: false,
1711            fetch_user: "",
1712        },
1713        BrowserRequestKind::AudioSubresource => RequestSurfaceFacts {
1714            accept: WILDCARD_ACCEPT,
1715            dest: AUDIO_DEST_VALUE,
1716            mode: NO_CORS_MODE_VALUE,
1717            site: CROSS_SITE_VALUE,
1718            upgrade_insecure_requests: false,
1719            fetch_user: "",
1720        },
1721    }
1722}
1723
1724#[cfg(test)]
1725mod tests {
1726    use super::*;
1727
1728    #[test]
1729    fn chrome_windows_const_matches_profile() {
1730        assert_eq!(
1731            profile_user_agent(StealthProfile::ChromeWindowsStable),
1732            CHROME_WINDOWS_STABLE_USER_AGENT
1733        );
1734    }
1735
1736    #[test]
1737    fn firefox_windows_const_matches_profile() {
1738        assert_eq!(
1739            profile_user_agent(StealthProfile::FirefoxWindows),
1740            FIREFOX_WINDOWS_STABLE_USER_AGENT
1741        );
1742    }
1743
1744    #[test]
1745    fn firefox_windows_stays_windows_and_firefox() {
1746        let ua = profile_user_agent(StealthProfile::FirefoxWindows);
1747        assert!(ua.contains("Windows NT"));
1748        assert!(ua.contains("Firefox/133.0"));
1749        assert!(!ua.contains("Chrome/"));
1750    }
1751
1752    #[test]
1753    fn profile_facts_match_user_agent_api() {
1754        for profile in ALL_PROFILES {
1755            assert_eq!(
1756                profile_facts(*profile).user_agent,
1757                profile_user_agent(*profile)
1758            );
1759            assert_eq!(profile_facts(*profile).languages[0], "en-US");
1760            assert!(!profile_facts(*profile).accept.is_empty());
1761            assert!(!profile_facts(*profile).accept_language.is_empty());
1762            let expected_encoding = match profile {
1763                &StealthProfile::Ie11Windows => LEGACY_ACCEPT_ENCODING,
1764                _ => DEFAULT_ACCEPT_ENCODING,
1765            };
1766            assert_eq!(profile_facts(*profile).accept_encoding, expected_encoding);
1767        }
1768    }
1769
1770    #[test]
1771    fn default_profile_accessors_delegate_to_default_profile() {
1772        assert_eq!(
1773            default_profile_facts(),
1774            profile_facts(DEFAULT_STEALTH_PROFILE)
1775        );
1776        assert_eq!(
1777            default_profile_user_agent(),
1778            profile_user_agent(DEFAULT_STEALTH_PROFILE)
1779        );
1780        assert_eq!(
1781            default_profile_navigation_headers(),
1782            profile_navigation_headers(DEFAULT_STEALTH_PROFILE)
1783        );
1784        assert_eq!(
1785            default_profile_browser_headers(),
1786            profile_browser_headers(DEFAULT_STEALTH_PROFILE)
1787        );
1788        assert_eq!(
1789            default_profile_request_headers(BrowserRequestKind::Navigation),
1790            profile_request_headers(DEFAULT_STEALTH_PROFILE, BrowserRequestKind::Navigation)
1791        );
1792        assert_eq!(
1793            default_profile_request_headers_without_compression(BrowserRequestKind::Navigation),
1794            profile_request_headers_without_compression(
1795                DEFAULT_STEALTH_PROFILE,
1796                BrowserRequestKind::Navigation
1797            )
1798        );
1799    }
1800
1801    #[test]
1802    fn profile_names_and_aliases_are_canonical() {
1803        for profile in ALL_PROFILES {
1804            assert_eq!(named_profile(profile_name(*profile)), Some(*profile));
1805            assert_eq!(
1806                named_profile(profile_display_name(*profile)),
1807                Some(*profile)
1808            );
1809        }
1810
1811        assert_eq!(
1812            named_profile("chrome-win"),
1813            Some(StealthProfile::ChromeWindowsStable)
1814        );
1815        assert_eq!(
1816            named_profile("chrome-osx"),
1817            Some(StealthProfile::ChromeMacStable)
1818        );
1819        assert_eq!(named_profile("ie11"), Some(StealthProfile::Ie11Windows));
1820        assert_eq!(named_profile("unknown"), None);
1821    }
1822
1823    #[test]
1824    fn user_agent_facts_parse_chrome_windows() {
1825        let facts = user_agent_facts(profile_user_agent(StealthProfile::ChromeWindowsStable));
1826
1827        assert_eq!(facts.browser, UserAgentBrowser::Chrome);
1828        assert_eq!(facts.platform, UserAgentPlatform::Windows);
1829        assert_eq!(facts.browser_major_version, Some(131));
1830        assert_eq!(facts.chromium_major_version, Some(131));
1831        assert_eq!(
1832            facts.inferred_profile,
1833            Some(StealthProfile::ChromeWindowsStable)
1834        );
1835        assert_eq!(facts.client_hint_platform_value(), Some("\"Windows\""));
1836        assert_eq!(facts.client_hint_mobile_value(), "?0");
1837        assert_eq!(facts.platform.chrome_tls_label(), Some("Windows"));
1838    }
1839
1840    #[test]
1841    fn user_agent_facts_parse_mobile_and_safari_profiles() {
1842        let android = user_agent_facts(profile_user_agent(StealthProfile::ChromeAndroid));
1843        assert_eq!(android.platform, UserAgentPlatform::Android);
1844        assert_eq!(
1845            android.inferred_profile,
1846            Some(StealthProfile::ChromeAndroid)
1847        );
1848        assert_eq!(android.client_hint_mobile_value(), "?1");
1849        assert_eq!(android.platform.chrome_tls_label(), Some("Android"));
1850
1851        let iphone = user_agent_facts(profile_user_agent(StealthProfile::SafariIphone));
1852        assert_eq!(iphone.browser, UserAgentBrowser::Safari);
1853        assert_eq!(iphone.platform, UserAgentPlatform::Ios);
1854        assert_eq!(iphone.inferred_profile, Some(StealthProfile::SafariIphone));
1855        assert_eq!(iphone.platform.chrome_tls_label(), None);
1856    }
1857
1858    #[test]
1859    fn user_agent_facts_parse_chromium_vendor_profiles() {
1860        let edge = user_agent_facts(profile_user_agent(StealthProfile::EdgeWindowsStable));
1861        assert_eq!(edge.browser, UserAgentBrowser::Edge);
1862        assert_eq!(edge.browser_major_version, Some(131));
1863        assert_eq!(edge.chromium_major_version, Some(131));
1864        assert_eq!(
1865            edge.inferred_profile,
1866            Some(StealthProfile::EdgeWindowsStable)
1867        );
1868
1869        let opera = user_agent_facts(profile_user_agent(StealthProfile::OperaWindows));
1870        assert_eq!(opera.browser, UserAgentBrowser::Opera);
1871        assert_eq!(opera.browser_major_version, Some(116));
1872        assert_eq!(opera.chromium_major_version, Some(131));
1873        assert_eq!(opera.inferred_profile, Some(StealthProfile::OperaWindows));
1874
1875        let samsung = user_agent_facts(profile_user_agent(StealthProfile::SamsungInternetAndroid));
1876        assert_eq!(samsung.browser, UserAgentBrowser::SamsungInternet);
1877        assert_eq!(samsung.browser_major_version, Some(26));
1878        assert_eq!(samsung.chromium_major_version, Some(126));
1879        assert_eq!(
1880            samsung.inferred_profile,
1881            Some(StealthProfile::SamsungInternetAndroid)
1882        );
1883    }
1884
1885    #[test]
1886    fn user_agent_facts_parse_firefox_ie_and_legacy_chrome() {
1887        let firefox = user_agent_facts(profile_user_agent(StealthProfile::FirefoxWindows));
1888        assert_eq!(firefox.browser, UserAgentBrowser::Firefox);
1889        assert_eq!(firefox.platform, UserAgentPlatform::Windows);
1890        assert_eq!(firefox.browser_major_version, Some(133));
1891        assert_eq!(firefox.chromium_major_version, None);
1892        assert_eq!(
1893            firefox.inferred_profile,
1894            Some(StealthProfile::FirefoxWindows)
1895        );
1896
1897        let ie = user_agent_facts(profile_user_agent(StealthProfile::Ie11Windows));
1898        assert_eq!(ie.browser, UserAgentBrowser::InternetExplorer);
1899        assert_eq!(ie.platform, UserAgentPlatform::Windows);
1900        assert_eq!(ie.browser_major_version, Some(11));
1901        assert_eq!(ie.inferred_profile, Some(StealthProfile::Ie11Windows));
1902
1903        let legacy = user_agent_facts(profile_user_agent(StealthProfile::ChromeWindowsLegacy96));
1904        assert_eq!(legacy.browser_major_version, Some(96));
1905        assert_eq!(
1906            legacy.inferred_profile,
1907            Some(StealthProfile::ChromeWindowsLegacy96)
1908        );
1909    }
1910
1911    #[test]
1912    fn user_agent_facts_reject_unknown_and_flags_headless() {
1913        let unknown = user_agent_facts("curl/8.0");
1914        assert_eq!(unknown.browser, UserAgentBrowser::Unknown);
1915        assert_eq!(unknown.platform, UserAgentPlatform::Unknown);
1916        assert_eq!(unknown.inferred_profile, None);
1917        assert_eq!(unknown.browser_major_version, None);
1918
1919        let headless = user_agent_facts(
1920            "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 \
1921             (KHTML, like Gecko) HeadlessChrome/134.0.0.0 Safari/537.36",
1922        );
1923        assert!(headless.headless);
1924        assert_eq!(headless.browser, UserAgentBrowser::Chrome);
1925        assert_eq!(headless.chromium_major_version, Some(134));
1926        assert_eq!(headless.inferred_profile, Some(StealthProfile::ChromeLinux));
1927    }
1928
1929    #[test]
1930    fn rotation_profiles_exclude_legacy_personas() {
1931        assert!(!ROTATION_PROFILES.contains(&StealthProfile::ChromeWindowsLegacy96));
1932        assert!(!ROTATION_PROFILES.contains(&StealthProfile::Ie11Windows));
1933        assert!(ROTATION_PROFILES.contains(&StealthProfile::ChromeWindowsStable));
1934    }
1935
1936    #[test]
1937    fn default_stealth_profile_is_catalogued_and_rotatable() {
1938        assert!(ALL_PROFILES.contains(&DEFAULT_STEALTH_PROFILE));
1939        assert!(ROTATION_PROFILES.contains(&DEFAULT_STEALTH_PROFILE));
1940        assert_eq!(
1941            named_profile(profile_name(DEFAULT_STEALTH_PROFILE)),
1942            Some(DEFAULT_STEALTH_PROFILE)
1943        );
1944    }
1945
1946    #[test]
1947    fn profile_hardware_defaults_share_profile_screen_facts() {
1948        for profile in ALL_PROFILES {
1949            let facts = profile_facts(*profile);
1950            let variants = profile_hardware_variants(*profile);
1951            let hardware = profile_hardware(*profile);
1952
1953            assert!(
1954                !variants.is_empty(),
1955                "{profile:?} must expose at least one hardware tuple"
1956            );
1957            assert_eq!(hardware.screen_width, facts.screen_width);
1958            assert_eq!(hardware.screen_height, facts.screen_height);
1959            assert!(hardware.color_depth > 0);
1960            assert!(hardware.device_memory > 0);
1961            assert!(hardware.hardware_concurrency > 0);
1962            // WebGL vendor/renderer must be coherent: both empty (native
1963            // passthrough — expose the host's real Gecko-sanitized adapter) or
1964            // both set (a cross-OS persona pinning a coherent adapter). A
1965            // half-state is the exact incoherence the old override shipped.
1966            assert_eq!(
1967                hardware.webgl_vendor.is_empty(),
1968                hardware.webgl_renderer.is_empty(),
1969                "{profile:?} half-spoofed WebGL adapter (vendor/renderer emptiness disagree)"
1970            );
1971        }
1972    }
1973
1974    #[test]
1975    fn browser_surface_metadata_tracks_profile_family() {
1976        for profile in ALL_PROFILES {
1977            let vendor = profile_navigator_vendor(*profile);
1978            let brands = profile_client_hint_brands(*profile);
1979            let client_hint_platform = profile_client_hint_platform(*profile);
1980
1981            match profile {
1982                StealthProfile::FirefoxLinux
1983                | StealthProfile::FirefoxWindows
1984                | StealthProfile::Ie11Windows => {
1985                    assert_eq!(vendor, "");
1986                    assert!(brands.is_empty(), "{profile:?} should not expose UA-CH");
1987                    assert_eq!(client_hint_platform, None);
1988                }
1989                StealthProfile::SafariIphone
1990                | StealthProfile::SafariIpad
1991                | StealthProfile::SafariMacStable => {
1992                    assert_eq!(vendor, "Apple Computer, Inc.");
1993                    assert!(brands.is_empty(), "{profile:?} should not expose UA-CH");
1994                    assert_eq!(client_hint_platform, None);
1995                }
1996                _ => {
1997                    assert_eq!(vendor, "Google Inc.");
1998                    assert!(!brands.is_empty(), "{profile:?} missing UA-CH brands");
1999                    assert!(
2000                        client_hint_platform.is_some(),
2001                        "{profile:?} missing Sec-CH-UA-Platform"
2002                    );
2003                    assert!(
2004                        brands
2005                            .iter()
2006                            .any(|brand| brand.brand == "Not?A_Brand" && brand.version == "99"),
2007                        "{profile:?} missing GREASE brand"
2008                    );
2009                }
2010            }
2011        }
2012    }
2013
2014    #[test]
2015    fn ie11_profile_keeps_legacy_http_shape() {
2016        let facts = profile_facts(StealthProfile::Ie11Windows);
2017
2018        assert_eq!(facts.user_agent, IE11_WINDOWS_USER_AGENT);
2019        assert!(facts.user_agent.contains("Trident/7.0"));
2020        assert!(!facts.user_agent.contains("Chrome/"));
2021        assert_eq!(facts.platform, "Win32");
2022        assert_eq!(facts.accept, IE11_NAVIGATION_ACCEPT);
2023        assert_eq!(facts.accept_encoding, LEGACY_ACCEPT_ENCODING);
2024        assert!(!facts.accept_encoding.contains("br"));
2025    }
2026
2027    #[test]
2028    fn chrome_legacy_96_profile_keeps_legacy_chromium_shape() {
2029        let facts = profile_facts(StealthProfile::ChromeWindowsLegacy96);
2030
2031        assert_eq!(facts.user_agent, CHROME_WINDOWS_LEGACY_96_USER_AGENT);
2032        assert!(facts.user_agent.contains("Chrome/96.0.4664.110"));
2033        assert_eq!(facts.platform, "Win32");
2034        assert_eq!(facts.accept, CHROMIUM_NAVIGATION_ACCEPT);
2035        assert_eq!(facts.accept_encoding, DEFAULT_ACCEPT_ENCODING);
2036        assert_eq!(facts.screen_width, 1366);
2037        assert_eq!(facts.screen_height, 768);
2038    }
2039
2040    #[test]
2041    fn common_browser_profiles_delegate_to_profile_facts() {
2042        for (name, profile) in [
2043            ("chrome", StealthProfile::ChromeWindowsStable),
2044            ("firefox", StealthProfile::FirefoxLinux),
2045            ("safari", StealthProfile::SafariMacStable),
2046            ("edge", StealthProfile::EdgeWindowsStable),
2047        ] {
2048            let entry = get_profile(name).expect("profile should exist");
2049            let facts = profile_facts(profile);
2050
2051            assert_eq!(entry.user_agent, facts.user_agent, "{name} UA drifted");
2052            assert_eq!(entry.accept, facts.accept, "{name} Accept drifted");
2053            assert_eq!(
2054                entry.accept_language, facts.accept_language,
2055                "{name} Accept-Language drifted"
2056            );
2057            assert_eq!(
2058                entry.accept_encoding, facts.accept_encoding,
2059                "{name} Accept-Encoding drifted"
2060            );
2061        }
2062    }
2063
2064    #[test]
2065    fn common_browser_profile_rotation_matches_legacy_contract() {
2066        let first = rotate(0);
2067        assert_eq!(first.name, "chrome");
2068        assert_eq!(rotate(PROFILES.len()), first);
2069        assert_ne!(rotate(1).name, first.name);
2070        assert!(get_profile("unknown").is_none());
2071    }
2072
2073    #[test]
2074    fn navigation_headers_delegate_to_profile_facts() {
2075        let facts = profile_facts(StealthProfile::ChromeWindowsStable);
2076        assert_eq!(
2077            profile_navigation_headers(StealthProfile::ChromeWindowsStable),
2078            [
2079                NavigationHeader {
2080                    name: USER_AGENT_HEADER,
2081                    value: facts.user_agent,
2082                },
2083                NavigationHeader {
2084                    name: ACCEPT_HEADER,
2085                    value: facts.accept,
2086                },
2087                NavigationHeader {
2088                    name: ACCEPT_LANGUAGE_HEADER,
2089                    value: facts.accept_language,
2090                },
2091            ]
2092        );
2093    }
2094
2095    #[test]
2096    fn browser_profile_headers_include_legacy_accept_encoding() {
2097        let profile = rotate(0);
2098        assert_eq!(
2099            profile.headers(),
2100            [
2101                NavigationHeader {
2102                    name: USER_AGENT_HEADER,
2103                    value: profile.user_agent,
2104                },
2105                NavigationHeader {
2106                    name: ACCEPT_HEADER,
2107                    value: profile.accept,
2108                },
2109                NavigationHeader {
2110                    name: ACCEPT_LANGUAGE_HEADER,
2111                    value: profile.accept_language,
2112                },
2113                NavigationHeader {
2114                    name: ACCEPT_ENCODING_HEADER,
2115                    value: profile.accept_encoding,
2116                },
2117            ]
2118        );
2119    }
2120
2121    #[test]
2122    fn browser_headers_delegate_to_profile_facts_with_accept_encoding() {
2123        for profile in ALL_PROFILES {
2124            let facts = profile_facts(*profile);
2125            assert_eq!(
2126                profile_browser_headers(*profile),
2127                [
2128                    NavigationHeader {
2129                        name: USER_AGENT_HEADER,
2130                        value: facts.user_agent,
2131                    },
2132                    NavigationHeader {
2133                        name: ACCEPT_HEADER,
2134                        value: facts.accept,
2135                    },
2136                    NavigationHeader {
2137                        name: ACCEPT_LANGUAGE_HEADER,
2138                        value: facts.accept_language,
2139                    },
2140                    NavigationHeader {
2141                        name: ACCEPT_ENCODING_HEADER,
2142                        value: facts.accept_encoding,
2143                    },
2144                ]
2145            );
2146        }
2147    }
2148
2149    #[test]
2150    fn navigation_request_headers_include_fetch_metadata() {
2151        let facts = profile_facts(StealthProfile::ChromeWindowsStable);
2152        let headers = profile_request_headers_without_compression(
2153            StealthProfile::ChromeWindowsStable,
2154            BrowserRequestKind::Navigation,
2155        );
2156
2157        assert_eq!(headers.len(), 8);
2158        assert_eq!(
2159            headers.as_slice(),
2160            &[
2161                NavigationHeader {
2162                    name: USER_AGENT_HEADER,
2163                    value: facts.user_agent,
2164                },
2165                NavigationHeader {
2166                    name: ACCEPT_HEADER,
2167                    value: facts.accept,
2168                },
2169                NavigationHeader {
2170                    name: ACCEPT_LANGUAGE_HEADER,
2171                    value: facts.accept_language,
2172                },
2173                NavigationHeader {
2174                    name: UPGRADE_INSECURE_REQUESTS_HEADER,
2175                    value: "1",
2176                },
2177                NavigationHeader {
2178                    name: SEC_FETCH_DEST_HEADER,
2179                    value: "document",
2180                },
2181                NavigationHeader {
2182                    name: SEC_FETCH_MODE_HEADER,
2183                    value: "navigate",
2184                },
2185                NavigationHeader {
2186                    name: SEC_FETCH_SITE_HEADER,
2187                    value: "none",
2188                },
2189                NavigationHeader {
2190                    name: SEC_FETCH_USER_HEADER,
2191                    value: "?1",
2192                },
2193            ]
2194        );
2195    }
2196
2197    #[test]
2198    fn same_origin_fetch_request_headers_are_not_navigation_shaped() {
2199        let facts = profile_facts(StealthProfile::ChromeWindowsStable);
2200        let headers = profile_request_headers_without_compression(
2201            StealthProfile::ChromeWindowsStable,
2202            BrowserRequestKind::SameOriginFetch,
2203        );
2204
2205        assert_eq!(headers.len(), 6);
2206        assert_eq!(
2207            headers.as_slice(),
2208            &[
2209                NavigationHeader {
2210                    name: USER_AGENT_HEADER,
2211                    value: facts.user_agent,
2212                },
2213                NavigationHeader {
2214                    name: ACCEPT_HEADER,
2215                    value: "*/*",
2216                },
2217                NavigationHeader {
2218                    name: ACCEPT_LANGUAGE_HEADER,
2219                    value: facts.accept_language,
2220                },
2221                NavigationHeader {
2222                    name: SEC_FETCH_DEST_HEADER,
2223                    value: "empty",
2224                },
2225                NavigationHeader {
2226                    name: SEC_FETCH_MODE_HEADER,
2227                    value: "cors",
2228                },
2229                NavigationHeader {
2230                    name: SEC_FETCH_SITE_HEADER,
2231                    value: "same-origin",
2232                },
2233            ]
2234        );
2235    }
2236
2237    #[test]
2238    fn same_origin_navigation_uses_document_surface_with_same_origin_site() {
2239        let facts = profile_facts(StealthProfile::ChromeWindowsStable);
2240        let headers = profile_request_headers_without_compression(
2241            StealthProfile::ChromeWindowsStable,
2242            BrowserRequestKind::SameOriginNavigation,
2243        );
2244        let by_name = |name: &str| {
2245            headers
2246                .as_slice()
2247                .iter()
2248                .find(|header| header.name == name)
2249                .map(|header| header.value)
2250        };
2251
2252        assert_eq!(headers.len(), 8);
2253        assert_eq!(by_name(ACCEPT_HEADER), Some(facts.accept));
2254        assert_eq!(by_name(UPGRADE_INSECURE_REQUESTS_HEADER), Some("1"));
2255        assert_eq!(by_name(SEC_FETCH_DEST_HEADER), Some("document"));
2256        assert_eq!(by_name(SEC_FETCH_MODE_HEADER), Some("navigate"));
2257        assert_eq!(by_name(SEC_FETCH_SITE_HEADER), Some("same-origin"));
2258        assert_eq!(by_name(SEC_FETCH_USER_HEADER), Some("?1"));
2259    }
2260
2261    #[test]
2262    fn cross_site_fetch_request_headers_are_cors_cross_site() {
2263        let headers = profile_request_headers_without_compression(
2264            StealthProfile::ChromeWindowsStable,
2265            BrowserRequestKind::CrossSiteFetch,
2266        );
2267        let by_name = |name: &str| {
2268            headers
2269                .as_slice()
2270                .iter()
2271                .find(|header| header.name == name)
2272                .map(|header| header.value)
2273        };
2274
2275        assert_eq!(headers.len(), 6);
2276        assert_eq!(by_name(ACCEPT_HEADER), Some("*/*"));
2277        assert_eq!(by_name(SEC_FETCH_DEST_HEADER), Some("empty"));
2278        assert_eq!(by_name(SEC_FETCH_MODE_HEADER), Some("cors"));
2279        assert_eq!(by_name(SEC_FETCH_SITE_HEADER), Some("cross-site"));
2280        assert_eq!(by_name(UPGRADE_INSECURE_REQUESTS_HEADER), None);
2281        assert_eq!(by_name(SEC_FETCH_USER_HEADER), None);
2282    }
2283
2284    #[test]
2285    fn same_origin_mode_fetch_request_headers_preserve_explicit_fetch_mode() {
2286        let headers = profile_request_headers_without_compression(
2287            StealthProfile::ChromeWindowsStable,
2288            BrowserRequestKind::SameOriginModeFetch,
2289        );
2290        let by_name = |name: &str| {
2291            headers
2292                .as_slice()
2293                .iter()
2294                .find(|header| header.name == name)
2295                .map(|header| header.value)
2296        };
2297
2298        assert_eq!(headers.len(), 6);
2299        assert_eq!(by_name(ACCEPT_HEADER), Some("*/*"));
2300        assert_eq!(by_name(SEC_FETCH_DEST_HEADER), Some("empty"));
2301        assert_eq!(by_name(SEC_FETCH_MODE_HEADER), Some("same-origin"));
2302        assert_eq!(by_name(SEC_FETCH_SITE_HEADER), Some("same-origin"));
2303        assert_eq!(by_name(SEC_FETCH_USER_HEADER), None);
2304    }
2305
2306    #[test]
2307    fn image_request_headers_are_image_subresource_shaped() {
2308        let headers = profile_request_headers_without_compression(
2309            StealthProfile::ChromeWindowsStable,
2310            BrowserRequestKind::ImageSubresource,
2311        );
2312        let by_name = |name: &str| {
2313            headers
2314                .as_slice()
2315                .iter()
2316                .find(|header| header.name == name)
2317                .map(|header| header.value)
2318        };
2319
2320        assert_eq!(headers.len(), 6);
2321        assert_eq!(by_name(ACCEPT_HEADER), Some("*/*"));
2322        assert_eq!(by_name(SEC_FETCH_DEST_HEADER), Some("image"));
2323        assert_eq!(by_name(SEC_FETCH_MODE_HEADER), Some("no-cors"));
2324        assert_eq!(by_name(SEC_FETCH_SITE_HEADER), Some("cross-site"));
2325        assert_eq!(by_name(ACCEPT_ENCODING_HEADER), None);
2326        assert_eq!(by_name(SEC_FETCH_USER_HEADER), None);
2327    }
2328
2329    #[test]
2330    fn audio_request_headers_are_media_subresource_shaped() {
2331        let headers = profile_request_headers_without_compression(
2332            StealthProfile::ChromeWindowsStable,
2333            BrowserRequestKind::AudioSubresource,
2334        );
2335        let by_name = |name: &str| {
2336            headers
2337                .as_slice()
2338                .iter()
2339                .find(|header| header.name == name)
2340                .map(|header| header.value)
2341        };
2342
2343        assert_eq!(headers.len(), 6);
2344        assert_eq!(by_name(ACCEPT_HEADER), Some("*/*"));
2345        assert_eq!(by_name(SEC_FETCH_DEST_HEADER), Some("audio"));
2346        assert_eq!(by_name(SEC_FETCH_MODE_HEADER), Some("no-cors"));
2347        assert_eq!(by_name(SEC_FETCH_SITE_HEADER), Some("cross-site"));
2348        assert_eq!(by_name(ACCEPT_ENCODING_HEADER), None);
2349        assert_eq!(by_name(SEC_FETCH_USER_HEADER), None);
2350    }
2351
2352    #[test]
2353    fn compressed_request_headers_include_accept_encoding() {
2354        let facts = profile_facts(StealthProfile::ChromeWindowsStable);
2355        let headers = profile_request_headers(
2356            StealthProfile::ChromeWindowsStable,
2357            BrowserRequestKind::Navigation,
2358        );
2359        let by_name = |name: &str| {
2360            headers
2361                .as_slice()
2362                .iter()
2363                .find(|header| header.name == name)
2364                .map(|header| header.value)
2365        };
2366
2367        assert_eq!(headers.len(), 9);
2368        assert_eq!(by_name(ACCEPT_ENCODING_HEADER), Some(facts.accept_encoding));
2369    }
2370
2371    #[test]
2372    fn canonical_navigation_header_names_use_browser_casing() {
2373        assert_eq!(
2374            canonical_navigation_header_name(USER_AGENT_HEADER),
2375            "User-Agent"
2376        );
2377        assert_eq!(canonical_navigation_header_name(ACCEPT_HEADER), "Accept");
2378        assert_eq!(
2379            canonical_navigation_header_name(ACCEPT_LANGUAGE_HEADER),
2380            "Accept-Language"
2381        );
2382        assert_eq!(
2383            canonical_navigation_header_name(ACCEPT_ENCODING_HEADER),
2384            "Accept-Encoding"
2385        );
2386        assert_eq!(
2387            canonical_navigation_header_name(SEC_FETCH_MODE_HEADER),
2388            "Sec-Fetch-Mode"
2389        );
2390        assert_eq!(
2391            canonical_navigation_header_name("x-custom-header"),
2392            "x-custom-header"
2393        );
2394    }
2395}