use crate::stealth::{DeviceClass, StealthProfile};
pub fn nav_headers(profile: &StealthProfile, accept_ch_upgraded: bool) -> Vec<(String, String)> {
match profile.browser_name.as_str() {
"Firefox" => firefox_headers(profile),
"Safari" => safari_headers(profile),
_ if accept_ch_upgraded => chrome_headers_with_accept_ch(profile),
_ => chrome_headers(profile),
}
}
pub fn nav_headers_for_url(
profile: &StealthProfile,
url: &str,
accept_ch_upgraded: bool,
) -> Vec<(String, String)> {
let mut hdrs = nav_headers(profile, accept_ch_upgraded);
apply_region_accept_language(&mut hdrs, url, &profile.browser_name);
hdrs
}
pub fn apply_region_accept_language(hdrs: &mut [(String, String)], url: &str, browser_name: &str) {
let Some(langs) = region_languages_for_url(url) else {
return;
};
let value = match browser_name {
"Firefox" => build_firefox_accept_language(&langs),
"Safari" => build_safari_accept_language(&langs),
_ => build_accept_language(&langs),
};
for (k, v) in hdrs.iter_mut() {
if k.eq_ignore_ascii_case("accept-language") {
*v = value;
return;
}
}
}
pub fn region_languages_for_url(url: &str) -> Option<Vec<String>> {
let parsed = url::Url::parse(url).ok()?;
let host = parsed.host_str()?.to_ascii_lowercase();
let host = host.trim_start_matches("www.");
let tld = if host.ends_with(".co.jp") {
".co.jp"
} else if host.ends_with(".com.br") {
".com.br"
} else if host.ends_with(".com.mx") {
".com.mx"
} else if host.ends_with(".com.tr") {
".com.tr"
} else if host.ends_with(".com.cn") {
".com.cn"
} else {
let dot = host.rfind('.')?;
&host[dot..]
};
let langs: &[&str] = match tld {
".fr" => &["fr-FR", "fr", "en-US", "en"],
".de" => &["de-DE", "de", "en-US", "en"],
".co.jp" | ".jp" => &["ja-JP", "ja", "en-US", "en"],
".it" => &["it-IT", "it", "en-US", "en"],
".es" => &["es-ES", "es", "en-US", "en"],
".nl" => &["nl-NL", "nl", "en-US", "en"],
".pl" => &["pl-PL", "pl", "en-US", "en"],
".se" => &["sv-SE", "sv", "en-US", "en"],
".no" => &["nb-NO", "no", "en-US", "en"],
".dk" => &["da-DK", "da", "en-US", "en"],
".fi" => &["fi-FI", "fi", "en-US", "en"],
".pt" => &["pt-PT", "pt", "en-US", "en"],
".com.br" => &["pt-BR", "pt", "en-US", "en"],
".com.mx" => &["es-MX", "es", "en-US", "en"],
".com.tr" | ".tr" => &["tr-TR", "tr", "en-US", "en"],
".com.cn" | ".cn" => &["zh-CN", "zh", "en-US", "en"],
".ru" => &["ru-RU", "ru", "en-US", "en"],
".kr" => &["ko-KR", "ko", "en-US", "en"],
".tw" => &["zh-TW", "zh", "en-US", "en"],
".vn" => &["vi-VN", "vi", "en-US", "en"],
_ => return None,
};
Some(langs.iter().map(|s| s.to_string()).collect())
}
pub fn nav_headers_reload(
profile: &StealthProfile,
referer: &str,
accept_ch_upgraded: bool,
) -> Vec<(String, String)> {
match profile.browser_name.as_str() {
"Firefox" => firefox_headers_reload(profile, referer),
"Safari" => safari_headers_reload(profile, referer),
_ => chrome_headers_reload(profile, referer, accept_ch_upgraded),
}
}
pub fn nav_headers_fetch(
profile: &StealthProfile,
target_url: &str,
origin: Option<&str>,
) -> Vec<(String, String)> {
let mut hdrs = match profile.browser_name.as_str() {
"Firefox" => firefox_headers_fetch(profile, target_url, origin),
"Safari" => safari_headers_fetch(profile, target_url, origin),
_ => chrome_headers_fetch(profile, target_url, origin),
};
let key_url = origin.unwrap_or(target_url);
apply_region_accept_language(&mut hdrs, key_url, &profile.browser_name);
hdrs
}
pub fn chrome_headers(profile: &StealthProfile) -> Vec<(String, String)> {
chrome_headers_impl(profile, false)
}
pub fn chrome_headers_reload(
profile: &StealthProfile,
referer: &str,
accept_ch_upgraded: bool,
) -> Vec<(String, String)> {
let mut hdrs: Vec<(String, String)> = chrome_headers_impl(profile, accept_ch_upgraded)
.into_iter()
.filter(|(k, _)| k != "sec-fetch-user")
.map(|(k, v)| {
if k == "sec-fetch-site" {
(k, "same-origin".to_string())
} else {
(k, v)
}
})
.collect();
hdrs.push(("referer".to_string(), referer.to_string()));
hdrs
}
pub fn chrome_headers_fetch(
profile: &StealthProfile,
target_url: &str,
origin: Option<&str>,
) -> Vec<(String, String)> {
let mut headers = Vec::with_capacity(12);
headers.push(("user-agent".to_string(), profile.user_agent.clone()));
headers.push(("accept".to_string(), "*/*".to_string()));
let sec_ch_ua = build_sec_ch_ua(profile);
headers.push(("sec-ch-ua".to_string(), sec_ch_ua));
let is_mobile = matches!(
profile.device_class,
DeviceClass::MobileAndroid | DeviceClass::MobileIOS
);
headers.push((
"sec-ch-ua-mobile".to_string(),
if is_mobile { "?1" } else { "?0" }.to_string(),
));
headers.push((
"sec-ch-ua-platform".to_string(),
format!("\"{}\"", profile.os_name),
));
let site = match origin {
Some(origin) => {
let t = url::Url::parse(target_url).ok();
let o = url::Url::parse(origin).ok();
match (t, o) {
(Some(tu), Some(ou)) => {
if tu.host_str() == ou.host_str() {
"same-origin"
} else if same_site(&tu, &ou) {
"same-site"
} else {
"cross-site"
}
}
_ => "cross-site",
}
}
None => "cross-site",
};
headers.push(("sec-fetch-site".to_string(), site.to_string()));
headers.push(("sec-fetch-mode".to_string(), "cors".to_string()));
headers.push(("sec-fetch-dest".to_string(), "empty".to_string()));
headers.push((
"accept-encoding".to_string(),
"gzip, deflate, br, zstd".to_string(),
));
headers.push((
"accept-language".to_string(),
build_accept_language(&profile.languages),
));
headers.push(("priority".to_string(), "u=1, i".to_string()));
if let Some(o) = origin {
headers.push(("origin".to_string(), o.to_string()));
headers.push((
"referer".to_string(),
format!("{}/", o.trim_end_matches('/')),
));
}
headers
}
fn same_site(a: &url::Url, b: &url::Url) -> bool {
fn tail2(u: &url::Url) -> Option<String> {
let host = u.host_str()?;
let mut parts: Vec<&str> = host.rsplit('.').collect();
if parts.len() < 2 {
return Some(host.to_string());
}
parts.truncate(2);
parts.reverse();
Some(parts.join("."))
}
tail2(a) == tail2(b)
}
pub fn chrome_headers_with_accept_ch(profile: &StealthProfile) -> Vec<(String, String)> {
chrome_headers_impl(profile, true)
}
fn chrome_headers_impl(
profile: &StealthProfile,
include_high_entropy: bool,
) -> Vec<(String, String)> {
let mut headers = Vec::with_capacity(if include_high_entropy { 20 } else { 13 });
let sec_ch_ua = build_sec_ch_ua(profile);
headers.push(("sec-ch-ua".to_string(), sec_ch_ua.clone()));
let is_mobile = matches!(
profile.device_class,
DeviceClass::MobileAndroid | DeviceClass::MobileIOS
);
headers.push((
"sec-ch-ua-mobile".to_string(),
if is_mobile { "?1" } else { "?0" }.to_string(),
));
headers.push((
"sec-ch-ua-platform".to_string(),
format!("\"{}\"", profile.os_name),
));
if include_high_entropy {
headers.push((
"sec-ch-ua-arch".to_string(),
format!("\"{}\"", profile.cpu_architecture),
));
headers.push((
"sec-ch-ua-bitness".to_string(),
format!("\"{}\"", profile.cpu_bitness),
));
headers.push((
"sec-ch-ua-full-version-list".to_string(),
build_sec_ch_ua_full_version_list(profile),
));
headers.push((
"sec-ch-ua-full-version".to_string(),
format!("\"{}\"", profile.browser_version),
));
headers.push((
"sec-ch-ua-model".to_string(),
format!("\"{}\"", profile.ua_model),
));
headers.push((
"sec-ch-ua-platform-version".to_string(),
format!(
"\"{}\"",
chrome_platform_version(&profile.os_name, &profile.os_version)
),
));
headers.push((
"sec-ch-ua-wow64".to_string(),
if profile.ua_wow64 { "?1" } else { "?0" }.to_string(),
));
headers.push((
"sec-ch-ua-form-factors".to_string(),
if is_mobile {
"\"Mobile\""
} else {
"\"Desktop\""
}
.to_string(),
));
headers.push((
"sec-ch-device-memory".to_string(),
format!("{}", quantize_device_memory(profile.device_memory as f64)),
));
}
headers.push(("upgrade-insecure-requests".to_string(), "1".to_string()));
headers.push(("user-agent".to_string(), profile.user_agent.clone()));
headers.push((
"accept".to_string(),
"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".to_string(),
));
headers.push(("sec-fetch-site".to_string(), "none".to_string()));
headers.push(("sec-fetch-mode".to_string(), "navigate".to_string()));
headers.push(("sec-fetch-user".to_string(), "?1".to_string()));
headers.push(("sec-fetch-dest".to_string(), "document".to_string()));
headers.push((
"accept-encoding".to_string(),
"gzip, deflate, br, zstd".to_string(),
));
let accept_language = build_accept_language(&profile.languages);
headers.push(("accept-language".to_string(), accept_language));
headers.push(("priority".to_string(), "u=0, i".to_string()));
headers
}
fn quantize_device_memory(gb: f64) -> f64 {
const SPEC: [f64; 6] = [0.25, 0.5, 1.0, 2.0, 4.0, 8.0];
if gb < SPEC[0] {
return SPEC[0];
}
let mut out = SPEC[0];
for &v in &SPEC {
if v <= gb {
out = v;
}
}
out
}
fn chrome_platform_version(os_name: &str, os_version: &str) -> String {
let parts: Vec<&str> = os_version.split('.').collect();
if parts.len() >= 3 {
return os_version.to_string();
}
match os_name {
"Windows" => {
match parts.len() {
1 => format!("{}.0.0", parts[0]),
2 => format!("{}.{}.0", parts[0], parts[1]),
_ => os_version.to_string(),
}
}
"macOS" => {
match parts.len() {
1 => format!("{}.0.0", parts[0]),
2 => format!("{}.{}.0", parts[0], parts[1]),
_ => os_version.to_string(),
}
}
"Linux" => String::new(),
_ => os_version.to_string(),
}
}
fn build_sec_ch_ua_full_version_list(profile: &StealthProfile) -> String {
let v = &profile.browser_version;
format!("\"Google Chrome\";v=\"{v}\", \"Not.A/Brand\";v=\"8.0.0.0\", \"Chromium\";v=\"{v}\"")
}
fn build_sec_ch_ua(profile: &StealthProfile) -> String {
let major_version = profile.browser_version.split('.').next().unwrap_or("147");
format!(
"\"Google Chrome\";v=\"{v}\", \"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"{v}\"",
v = major_version
)
}
pub fn firefox_headers(profile: &StealthProfile) -> Vec<(String, String)> {
firefox_headers_impl(profile, "none", true)
}
pub fn firefox_headers_reload(profile: &StealthProfile, referer: &str) -> Vec<(String, String)> {
let mut hdrs = firefox_headers_impl(profile, "same-origin", false);
hdrs.push(("referer".to_string(), referer.to_string()));
hdrs
}
pub fn firefox_headers_fetch(
profile: &StealthProfile,
target_url: &str,
origin: Option<&str>,
) -> Vec<(String, String)> {
let mut headers = Vec::with_capacity(10);
headers.push(("user-agent".to_string(), profile.user_agent.clone()));
headers.push(("accept".to_string(), "*/*".to_string()));
headers.push((
"accept-language".to_string(),
build_firefox_accept_language(&profile.languages),
));
headers.push((
"accept-encoding".to_string(),
"gzip, deflate, br, zstd".to_string(),
));
let site = match origin {
Some(origin) => {
let t = url::Url::parse(target_url).ok();
let o = url::Url::parse(origin).ok();
match (t, o) {
(Some(tu), Some(ou)) => {
if tu.host_str() == ou.host_str() {
"same-origin"
} else if same_site(&tu, &ou) {
"same-site"
} else {
"cross-site"
}
}
_ => "cross-site",
}
}
None => "cross-site",
};
headers.push(("sec-fetch-dest".to_string(), "empty".to_string()));
headers.push(("sec-fetch-mode".to_string(), "cors".to_string()));
headers.push(("sec-fetch-site".to_string(), site.to_string()));
if let Some(o) = origin {
headers.push(("origin".to_string(), o.to_string()));
headers.push((
"referer".to_string(),
format!("{}/", o.trim_end_matches('/')),
));
}
headers
}
fn firefox_headers_impl(
profile: &StealthProfile,
sec_fetch_site: &str,
include_sec_fetch_user: bool,
) -> Vec<(String, String)> {
let mut headers = Vec::with_capacity(9);
headers.push(("user-agent".to_string(), profile.user_agent.clone()));
headers.push((
"accept".to_string(),
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8".to_string(),
));
headers.push((
"accept-language".to_string(),
build_firefox_accept_language(&profile.languages),
));
headers.push((
"accept-encoding".to_string(),
"gzip, deflate, br, zstd".to_string(),
));
headers.push(("upgrade-insecure-requests".to_string(), "1".to_string()));
headers.push(("sec-fetch-dest".to_string(), "document".to_string()));
headers.push(("sec-fetch-mode".to_string(), "navigate".to_string()));
headers.push(("sec-fetch-site".to_string(), sec_fetch_site.to_string()));
if include_sec_fetch_user {
headers.push(("sec-fetch-user".to_string(), "?1".to_string()));
}
headers
}
pub fn safari_headers(profile: &StealthProfile) -> Vec<(String, String)> {
safari_headers_impl(profile, None)
}
pub fn safari_headers_reload(profile: &StealthProfile, referer: &str) -> Vec<(String, String)> {
safari_headers_impl(profile, Some(referer))
}
pub fn safari_headers_fetch(
profile: &StealthProfile,
target_url: &str,
origin: Option<&str>,
) -> Vec<(String, String)> {
let mut headers = Vec::with_capacity(7);
headers.push(("accept".to_string(), "*/*".to_string()));
headers.push((
"accept-language".to_string(),
build_safari_accept_language(&profile.languages),
));
headers.push((
"accept-encoding".to_string(),
"gzip, deflate, br".to_string(),
));
headers.push(("user-agent".to_string(), profile.user_agent.clone()));
if let Some(o) = origin {
headers.push(("origin".to_string(), o.to_string()));
headers.push((
"referer".to_string(),
format!("{}/", o.trim_end_matches('/')),
));
}
let _ = target_url;
headers
}
fn safari_headers_impl(profile: &StealthProfile, referer: Option<&str>) -> Vec<(String, String)> {
let mut headers = Vec::with_capacity(9);
headers.push(("sec-fetch-dest".to_string(), "document".to_string()));
headers.push(("user-agent".to_string(), profile.user_agent.clone()));
headers.push((
"accept".to_string(),
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8".to_string(),
));
let site = if referer.is_some() {
"same-origin"
} else {
"none"
};
headers.push(("sec-fetch-site".to_string(), site.to_string()));
headers.push(("sec-fetch-mode".to_string(), "navigate".to_string()));
headers.push((
"accept-language".to_string(),
build_safari_accept_language(&profile.languages),
));
headers.push(("priority".to_string(), "u=0, i".to_string()));
headers.push((
"accept-encoding".to_string(),
"gzip, deflate, br".to_string(),
));
if let Some(r) = referer {
headers.push(("referer".to_string(), r.to_string()));
}
headers
}
fn build_safari_accept_language(languages: &[String]) -> String {
build_accept_language(languages)
}
fn build_firefox_accept_language(languages: &[String]) -> String {
if languages.is_empty() {
return "en-US,en;q=0.5".to_string();
}
let mut parts = Vec::with_capacity(languages.len());
for (i, lang) in languages.iter().enumerate() {
if i == 0 {
parts.push(lang.clone());
} else {
let q = 0.5 - ((i - 1) as f64 * 0.2);
if q > 0.0 {
parts.push(format!("{};q={:.1}", lang, q));
}
}
}
parts.join(",")
}
fn build_accept_language(languages: &[String]) -> String {
if languages.is_empty() {
return "en-US,en;q=0.9".to_string();
}
let mut parts = Vec::with_capacity(languages.len());
for (i, lang) in languages.iter().enumerate() {
if i == 0 {
parts.push(lang.clone());
} else {
let q = 1.0 - (i as f64 * 0.1);
if q > 0.0 {
parts.push(format!("{};q={:.1}", lang, q));
}
}
}
parts.join(",")
}
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CoopValue {
UnsafeNone,
SameOriginAllowPopups,
SameOrigin,
NoopenerAllowPopups,
RestrictProperties,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CoepValue {
UnsafeNone,
RequireCorp,
Credentialless,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DocumentPolicy {
pub coop: CoopValue,
pub coep: CoepValue,
}
impl Default for DocumentPolicy {
fn default() -> Self {
Self {
coop: CoopValue::UnsafeNone,
coep: CoepValue::UnsafeNone,
}
}
}
fn lookup_header<'a>(headers: &'a HashMap<String, String>, name: &str) -> Option<&'a str> {
let lower = name.to_ascii_lowercase();
headers
.iter()
.find(|(k, _)| k.to_ascii_lowercase() == lower)
.map(|(_, v)| v.as_str())
}
fn bare_value(raw: &str) -> &str {
let head = raw.split(';').next().unwrap_or(raw).trim();
head.trim_matches('"')
}
fn parse_coop(raw: &str) -> CoopValue {
match bare_value(raw).to_ascii_lowercase().as_str() {
"same-origin" => CoopValue::SameOrigin,
"same-origin-allow-popups" => CoopValue::SameOriginAllowPopups,
"noopener-allow-popups" => CoopValue::NoopenerAllowPopups,
"restrict-properties" => CoopValue::RestrictProperties,
_ => CoopValue::UnsafeNone,
}
}
fn parse_coep(raw: &str) -> CoepValue {
match bare_value(raw).to_ascii_lowercase().as_str() {
"require-corp" => CoepValue::RequireCorp,
"credentialless" => CoepValue::Credentialless,
_ => CoepValue::UnsafeNone,
}
}
pub fn parse_document_policy(headers: &HashMap<String, String>) -> DocumentPolicy {
DocumentPolicy {
coop: lookup_header(headers, "cross-origin-opener-policy")
.map(parse_coop)
.unwrap_or(CoopValue::UnsafeNone),
coep: lookup_header(headers, "cross-origin-embedder-policy")
.map(parse_coep)
.unwrap_or(CoepValue::UnsafeNone),
}
}
pub fn is_cross_origin_isolated(policy: &DocumentPolicy) -> bool {
matches!(policy.coop, CoopValue::SameOrigin)
&& matches!(
policy.coep,
CoepValue::RequireCorp | CoepValue::Credentialless
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accept_language_single() {
let result = build_accept_language(&["en-US".to_string()]);
assert_eq!(result, "en-US");
}
#[test]
fn accept_language_multiple() {
let result = build_accept_language(&["en-US".to_string(), "en".to_string()]);
assert_eq!(result, "en-US,en;q=0.9");
}
#[test]
fn accept_language_empty() {
let result = build_accept_language(&[]);
assert_eq!(result, "en-US,en;q=0.9");
}
#[test]
fn fetch_headers_mobile_flag_matches_nav() {
let pixel = crate::stealth::presets::pixel_9_pro_chrome_148();
let fh: std::collections::HashMap<_, _> = chrome_headers_fetch(
&pixel,
"https://example.com/x.js",
Some("https://example.com"),
)
.into_iter()
.collect();
assert_eq!(
fh.get("sec-ch-ua-mobile").map(String::as_str),
Some("?1"),
"mobile profile fetch must emit sec-ch-ua-mobile: ?1 (matches nav)"
);
assert_eq!(
fh.get("sec-ch-ua-platform").map(String::as_str),
Some("\"Android\""),
"mobile profile fetch must emit Android platform"
);
let desk = crate::stealth::presets::chrome_148_macos();
let dfh: std::collections::HashMap<_, _> = chrome_headers_fetch(
&desk,
"https://example.com/x.js",
Some("https://example.com"),
)
.into_iter()
.collect();
assert_eq!(
dfh.get("sec-ch-ua-mobile").map(String::as_str),
Some("?0"),
"desktop profile fetch must stay sec-ch-ua-mobile: ?0"
);
}
#[test]
fn pixel_android_emits_mobile_client_hints() {
let profile = crate::stealth::presets::pixel_9_pro_chrome_148();
assert_eq!(profile.device_class, DeviceClass::MobileAndroid);
let headers = chrome_headers_with_accept_ch(&profile);
let h: std::collections::HashMap<_, _> = headers.iter().cloned().collect();
assert_eq!(
h.get("sec-ch-ua-mobile").map(String::as_str),
Some("?1"),
"Pixel preset must emit sec-ch-ua-mobile: ?1"
);
assert_eq!(
h.get("sec-ch-ua-platform").map(String::as_str),
Some("\"Android\""),
"Pixel preset must emit sec-ch-ua-platform: \"Android\""
);
assert_eq!(
h.get("sec-ch-ua-model").map(String::as_str),
Some("\"Pixel 9 Pro\""),
"Pixel preset must emit sec-ch-ua-model: \"Pixel 9 Pro\""
);
assert_eq!(
h.get("sec-ch-ua-form-factors").map(String::as_str),
Some("\"Mobile\""),
"Pixel preset must emit sec-ch-ua-form-factors: \"Mobile\""
);
assert!(
profile.user_agent.contains("Mobile"),
"Pixel UA must contain Mobile token, got: {}",
profile.user_agent
);
}
#[test]
fn desktop_chrome_emits_desktop_client_hints() {
let profile = crate::stealth::presets::chrome_148_macos();
assert_eq!(profile.device_class, DeviceClass::Desktop);
let headers = chrome_headers_with_accept_ch(&profile);
let h: std::collections::HashMap<_, _> = headers.iter().cloned().collect();
assert_eq!(
h.get("sec-ch-ua-mobile").map(String::as_str),
Some("?0"),
"Desktop must keep emitting sec-ch-ua-mobile: ?0"
);
assert_eq!(
h.get("sec-ch-ua-form-factors").map(String::as_str),
Some("\"Desktop\"")
);
assert_eq!(h.get("sec-ch-ua-model").map(String::as_str), Some("\"\""));
}
#[test]
fn firefox_headers_have_no_sec_ch_ua() {
let profile = crate::stealth::presets::firefox_135_macos();
let headers = firefox_headers(&profile);
for (k, _) in &headers {
assert!(
!k.starts_with("sec-ch-ua"),
"Firefox headers must not contain {k}"
);
}
assert!(
!headers.iter().any(|(k, _)| k == "priority"),
"Firefox should not emit `priority` header"
);
}
#[test]
fn firefox_headers_have_correct_accept() {
let profile = crate::stealth::presets::firefox_135_macos();
let headers = firefox_headers(&profile);
let accept = headers.iter().find(|(k, _)| k == "accept").unwrap();
assert_eq!(
accept.1,
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
);
}
#[test]
fn firefox_accept_language_uses_q_05() {
let result = build_firefox_accept_language(&["en-US".to_string(), "en".to_string()]);
assert_eq!(result, "en-US,en;q=0.5");
}
#[test]
fn firefox_headers_count_is_nine() {
let profile = crate::stealth::presets::firefox_135_macos();
let headers = firefox_headers(&profile);
assert_eq!(headers.len(), 9);
}
#[test]
fn chrome_headers_first_visit_is_low_entropy_only() {
let profile = crate::stealth::chrome_148_windows();
let headers = chrome_headers(&profile);
let names: Vec<&str> = headers.iter().map(|(k, _)| k.as_str()).collect();
assert_eq!(
headers.len(),
13,
"first-visit headers must match Chrome 130's count (13), got {}",
headers.len()
);
for required in &["sec-ch-ua", "sec-ch-ua-mobile", "sec-ch-ua-platform"] {
assert!(
names.contains(required),
"expected header '{required}' missing",
);
}
for forbidden in &[
"sec-ch-ua-arch",
"sec-ch-ua-bitness",
"sec-ch-ua-full-version-list",
"sec-ch-ua-model",
"sec-ch-ua-platform-version",
"sec-ch-ua-wow64",
] {
assert!(
!names.contains(forbidden),
"header '{forbidden}' leaked onto first-visit request — Chrome only sends this after Accept-CH",
);
}
}
#[test]
fn chrome_headers_with_accept_ch_includes_high_entropy() {
let profile = crate::stealth::chrome_148_windows();
let headers = chrome_headers_with_accept_ch(&profile);
let names: Vec<&str> = headers.iter().map(|(k, _)| k.as_str()).collect();
for required in &[
"sec-ch-ua",
"sec-ch-ua-mobile",
"sec-ch-ua-platform",
"sec-ch-ua-arch",
"sec-ch-ua-bitness",
"sec-ch-ua-full-version-list",
"sec-ch-ua-model",
"sec-ch-ua-platform-version",
"sec-ch-ua-wow64",
] {
assert!(
names.contains(required),
"expected header '{required}' missing from accept-ch variant",
);
}
}
#[test]
fn sec_ch_ua_full_version_list_has_chrome_version() {
let profile = crate::stealth::chrome_148_linux();
let value = build_sec_ch_ua_full_version_list(&profile);
assert!(value.contains("Google Chrome"));
assert!(value.contains(&profile.browser_version));
assert!(value.contains("Not.A/Brand"));
let google_idx = value.find("Google Chrome").unwrap();
let not_idx = value.find("Not.A/Brand").unwrap();
let chromium_idx = value.find("Chromium").unwrap();
assert!(google_idx < not_idx);
assert!(not_idx < chromium_idx);
}
#[test]
fn platform_version_triple_padded() {
assert_eq!(chrome_platform_version("Windows", "10.0"), "10.0.0");
assert_eq!(chrome_platform_version("Windows", "11"), "11.0.0");
assert_eq!(chrome_platform_version("macOS", "15.2"), "15.2.0");
assert_eq!(chrome_platform_version("Linux", "anything"), "");
}
#[test]
fn device_memory_quantizes_to_w3_spec_set() {
assert_eq!(quantize_device_memory(8.0), 8.0);
assert_eq!(quantize_device_memory(4.0), 4.0);
assert_eq!(quantize_device_memory(2.0), 2.0);
assert_eq!(quantize_device_memory(1.0), 1.0);
assert_eq!(quantize_device_memory(0.5), 0.5);
assert_eq!(quantize_device_memory(0.25), 0.25);
assert_eq!(quantize_device_memory(16.0), 8.0);
assert_eq!(quantize_device_memory(32.0), 8.0);
assert_eq!(quantize_device_memory(6.0), 4.0);
assert_eq!(quantize_device_memory(3.0), 2.0);
assert_eq!(quantize_device_memory(1.5), 1.0);
assert_eq!(quantize_device_memory(0.7), 0.5);
assert_eq!(quantize_device_memory(0.3), 0.25);
assert_eq!(quantize_device_memory(0.1), 0.25);
assert_eq!(quantize_device_memory(0.0), 0.25);
}
#[test]
fn sec_ch_device_memory_emits_quantized_value() {
let mut profile = crate::stealth::chrome_148_macos();
profile.device_memory = 16; let headers = chrome_headers_with_accept_ch(&profile);
let dm = headers
.iter()
.find(|(k, _)| k == "sec-ch-device-memory")
.expect("sec-ch-device-memory present in accept-ch variant");
assert_eq!(dm.1, "8");
profile.device_memory = 6; let headers = chrome_headers_with_accept_ch(&profile);
let dm = headers
.iter()
.find(|(k, _)| k == "sec-ch-device-memory")
.unwrap();
assert_eq!(dm.1, "4");
}
#[test]
fn sec_ch_ua_arch_reads_profile_cpu_architecture() {
let mut profile = crate::stealth::chrome_148_macos();
profile.cpu_architecture = "arm".into();
let headers = chrome_headers_with_accept_ch(&profile);
let arch = headers
.iter()
.find(|(k, _)| k == "sec-ch-ua-arch")
.expect("sec-ch-ua-arch present in accept-ch variant");
assert_eq!(
arch.1, "\"arm\"",
"arch must reflect profile.cpu_architecture"
);
profile.cpu_architecture = "x86".into();
let headers = chrome_headers_with_accept_ch(&profile);
let arch = headers.iter().find(|(k, _)| k == "sec-ch-ua-arch").unwrap();
assert_eq!(arch.1, "\"x86\"");
}
#[test]
fn sec_ch_ua_bitness_reads_profile_cpu_bitness() {
let mut profile = crate::stealth::chrome_148_windows();
profile.cpu_bitness = "32".into();
profile.ua_wow64 = true;
let headers = chrome_headers_with_accept_ch(&profile);
let bitness = headers
.iter()
.find(|(k, _)| k == "sec-ch-ua-bitness")
.unwrap();
assert_eq!(bitness.1, "\"32\"");
let wow = headers
.iter()
.find(|(k, _)| k == "sec-ch-ua-wow64")
.unwrap();
assert_eq!(wow.1, "?1", "wow64 hint must reflect profile.ua_wow64");
}
#[test]
fn coi_default_when_headers_absent() {
let headers: HashMap<String, String> = HashMap::new();
let policy = parse_document_policy(&headers);
assert_eq!(policy.coop, CoopValue::UnsafeNone);
assert_eq!(policy.coep, CoepValue::UnsafeNone);
assert!(!is_cross_origin_isolated(&policy));
}
#[test]
fn coi_true_with_same_origin_and_require_corp() {
let mut headers: HashMap<String, String> = HashMap::new();
headers.insert("cross-origin-opener-policy".into(), "same-origin".into());
headers.insert("cross-origin-embedder-policy".into(), "require-corp".into());
let policy = parse_document_policy(&headers);
assert!(is_cross_origin_isolated(&policy));
}
#[test]
fn coi_true_with_same_origin_and_credentialless() {
let mut headers: HashMap<String, String> = HashMap::new();
headers.insert("cross-origin-opener-policy".into(), "same-origin".into());
headers.insert(
"cross-origin-embedder-policy".into(),
"credentialless".into(),
);
let policy = parse_document_policy(&headers);
assert!(is_cross_origin_isolated(&policy));
}
#[test]
fn coi_false_with_only_coop() {
let mut headers: HashMap<String, String> = HashMap::new();
headers.insert("cross-origin-opener-policy".into(), "same-origin".into());
let policy = parse_document_policy(&headers);
assert!(!is_cross_origin_isolated(&policy));
}
#[test]
fn coi_false_with_same_origin_allow_popups() {
let mut headers: HashMap<String, String> = HashMap::new();
headers.insert(
"cross-origin-opener-policy".into(),
"same-origin-allow-popups".into(),
);
headers.insert("cross-origin-embedder-policy".into(), "require-corp".into());
let policy = parse_document_policy(&headers);
assert!(!is_cross_origin_isolated(&policy));
}
#[test]
fn coi_parser_strips_directives_and_quotes() {
let mut headers: HashMap<String, String> = HashMap::new();
headers.insert(
"cross-origin-opener-policy".into(),
"\"same-origin\"; report-to=\"foo\"".into(),
);
headers.insert("cross-origin-embedder-policy".into(), "require-corp".into());
let policy = parse_document_policy(&headers);
assert_eq!(policy.coop, CoopValue::SameOrigin);
assert!(is_cross_origin_isolated(&policy));
}
#[test]
fn coi_case_insensitive_header_lookup() {
let mut headers: HashMap<String, String> = HashMap::new();
headers.insert("Cross-Origin-Opener-Policy".into(), "same-origin".into());
headers.insert("Cross-Origin-Embedder-Policy".into(), "require-corp".into());
let policy = parse_document_policy(&headers);
assert!(is_cross_origin_isolated(&policy));
}
#[test]
fn client_hints_match_profile_version() {
let profile = crate::stealth::chrome_148_windows();
let headers = chrome_headers_with_accept_ch(&profile);
let sec_ch_ua = headers
.iter()
.find(|(k, _)| k == "sec-ch-ua")
.unwrap()
.1
.clone();
let fvl = headers
.iter()
.find(|(k, _)| k == "sec-ch-ua-full-version-list")
.unwrap()
.1
.clone();
let major = profile.browser_version.split('.').next().unwrap();
assert!(sec_ch_ua.contains(major));
assert!(fvl.contains(&profile.browser_version));
}
#[test]
fn region_languages_amazon_fr() {
let langs = region_languages_for_url("https://www.amazon.fr/").unwrap();
assert_eq!(langs, vec!["fr-FR", "fr", "en-US", "en"]);
}
#[test]
fn region_languages_amazon_jp_compound_tld() {
let langs = region_languages_for_url("https://www.amazon.co.jp/").unwrap();
assert_eq!(langs, vec!["ja-JP", "ja", "en-US", "en"]);
}
#[test]
fn region_languages_amazon_com_no_override() {
assert!(region_languages_for_url("https://www.amazon.com/").is_none());
}
#[test]
fn region_languages_amazon_co_uk_no_override() {
assert!(region_languages_for_url("https://www.amazon.co.uk/").is_none());
}
#[test]
fn nav_headers_for_url_overrides_amazon_fr() {
let profile = crate::stealth::chrome_148_macos();
let hdrs = nav_headers_for_url(&profile, "https://www.amazon.fr/", false);
let al = hdrs
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case("accept-language"))
.expect("accept-language present");
assert_eq!(al.1, "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7");
}
#[test]
fn nav_headers_for_url_overrides_amazon_de_with_firefox_q_step() {
let profile = crate::stealth::firefox_135_macos();
let hdrs = nav_headers_for_url(&profile, "https://www.amazon.de/", false);
let al = hdrs
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case("accept-language"))
.expect("accept-language present");
assert_eq!(al.1, "de-DE,de;q=0.5,en-US;q=0.3,en;q=0.1");
}
#[test]
fn nav_headers_for_url_no_change_on_amazon_com() {
let profile = crate::stealth::chrome_148_macos();
let hdrs = nav_headers_for_url(&profile, "https://www.amazon.com/", false);
let al = hdrs
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case("accept-language"))
.expect("accept-language present");
let baseline = nav_headers(&profile, false);
let baseline_al = baseline
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case("accept-language"))
.unwrap();
assert_eq!(al.1, baseline_al.1);
}
}