1#![forbid(unsafe_code)]
15#![deny(unreachable_patterns)]
16#![warn(missing_docs)]
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25#[non_exhaustive]
26pub enum StealthProfile {
27 ChromeWindowsStable,
29 ChromeWindowsLegacy96,
31 ChromeMacStable,
33 EdgeWindowsStable,
35 Ie11Windows,
37 FirefoxLinux,
39 FirefoxWindows,
41 ChromeAndroid,
43 SafariIphone,
45 SafariIpad,
47 SafariMacStable,
49 ChromeLinux,
51 BraveWindows,
53 OperaWindows,
55 SamsungInternetAndroid,
57}
58
59pub const DEFAULT_STEALTH_PROFILE: StealthProfile = StealthProfile::ChromeWindowsStable;
61
62pub 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
81pub 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#[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#[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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202pub struct ProfileFacts {
203 pub user_agent: &'static str,
205 pub platform: &'static str,
207 pub languages: &'static [&'static str],
209 pub accept: &'static str,
211 pub accept_language: &'static str,
213 pub accept_encoding: &'static str,
215 pub mobile: bool,
217 pub screen_width: u32,
219 pub screen_height: u32,
221}
222
223#[derive(Debug, Clone, Copy, PartialEq, Eq)]
225pub enum UserAgentBrowser {
226 Chrome,
228 Edge,
230 Firefox,
232 Safari,
234 InternetExplorer,
236 Opera,
238 SamsungInternet,
240 Unknown,
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq)]
246pub enum UserAgentPlatform {
247 Android,
249 Ios,
251 MacOs,
253 Windows,
255 Linux,
257 Unknown,
259}
260
261impl UserAgentPlatform {
262 #[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 #[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 #[must_use]
289 pub const fn is_mobile(self) -> bool {
290 matches!(self, Self::Android | Self::Ios)
291 }
292}
293
294#[derive(Debug, Clone, Copy, PartialEq, Eq)]
296pub struct UserAgentFacts {
297 pub browser: UserAgentBrowser,
299 pub platform: UserAgentPlatform,
301 pub browser_major_version: Option<u32>,
303 pub chromium_major_version: Option<u32>,
305 pub headless: bool,
307 pub inferred_profile: Option<StealthProfile>,
309 pub mobile: bool,
311}
312
313impl UserAgentFacts {
314 #[must_use]
316 pub const fn client_hint_platform_value(self) -> Option<&'static str> {
317 self.platform.client_hint_value()
318 }
319
320 #[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#[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#[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
478pub const USER_AGENT_HEADER: &str = "user-agent";
480
481pub const ACCEPT_HEADER: &str = "accept";
483
484pub const ACCEPT_LANGUAGE_HEADER: &str = "accept-language";
486
487pub const ACCEPT_ENCODING_HEADER: &str = "accept-encoding";
489
490pub const UPGRADE_INSECURE_REQUESTS_HEADER: &str = "upgrade-insecure-requests";
492
493pub const SEC_FETCH_DEST_HEADER: &str = "sec-fetch-dest";
495
496pub const SEC_FETCH_MODE_HEADER: &str = "sec-fetch-mode";
498
499pub const SEC_FETCH_SITE_HEADER: &str = "sec-fetch-site";
501
502pub const SEC_FETCH_USER_HEADER: &str = "sec-fetch-user";
504
505#[derive(Debug, Clone, Copy, PartialEq, Eq)]
507#[non_exhaustive]
508pub enum BrowserRequestKind {
509 Navigation,
511 SameOriginNavigation,
513 CrossSiteNavigation,
515 SameOriginFetch,
517 SameOriginModeFetch,
519 CrossSiteFetch,
521 ImageSubresource,
523 AudioSubresource,
525}
526
527#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
550pub struct NavigationHeader {
551 pub name: &'static str,
553 pub value: &'static str,
555}
556
557const EMPTY_HEADER: NavigationHeader = NavigationHeader {
558 name: "",
559 value: "",
560};
561
562#[derive(Debug, Clone, Copy, PartialEq, Eq)]
564pub struct BrowserRequestHeaders {
565 entries: [NavigationHeader; 9],
566 len: usize,
567}
568
569impl BrowserRequestHeaders {
570 #[must_use]
572 pub fn as_slice(&self) -> &[NavigationHeader] {
573 &self.entries[..self.len]
574 }
575
576 #[must_use]
578 pub const fn len(&self) -> usize {
579 self.len
580 }
581
582 #[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
606pub 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
609pub const FIREFOX_NAVIGATION_ACCEPT: &str =
611 "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8";
612
613pub const IE11_NAVIGATION_ACCEPT: &str = "text/html, application/xhtml+xml, */*";
615
616pub const SAFARI_NAVIGATION_ACCEPT: &str =
618 "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
619
620pub const DEFAULT_ACCEPT_LANGUAGE: &str = "en-US,en;q=0.9";
622
623pub const FIREFOX_ACCEPT_LANGUAGE: &str = "en-US,en;q=0.5";
625
626pub const DEFAULT_ACCEPT_ENCODING: &str = "gzip, deflate, br";
628
629pub const LEGACY_ACCEPT_ENCODING: &str = "gzip, deflate";
631
632#[derive(Debug, Clone, Copy, PartialEq, Eq)]
641pub struct HeaderProfile {
642 pub name: &'static str,
644 pub user_agent: &'static str,
646 pub accept: &'static str,
648 pub accept_language: &'static str,
650 pub accept_encoding: &'static str,
652 pub sec_fetch_site: &'static str,
654 pub sec_fetch_mode: &'static str,
656 pub sec_fetch_dest: &'static str,
658}
659
660impl HeaderProfile {
661 #[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
689pub 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#[must_use]
715pub fn get_profile(name: &str) -> Option<&'static HeaderProfile> {
716 PROFILES.iter().find(|profile| profile.name == name)
717}
718
719#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
730pub struct ProfileHardware {
731 pub screen_width: u32,
733 pub screen_height: u32,
735 pub color_depth: u8,
737 pub device_memory: u8,
739 pub hardware_concurrency: u8,
741 pub webgl_vendor: &'static str,
743 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 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#[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#[must_use]
980pub const fn profile_hardware(profile: StealthProfile) -> ProfileHardware {
981 profile_hardware_variants(profile)[0]
982}
983
984#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
993pub struct ProfileClientHintBrand {
994 pub brand: &'static str,
996 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#[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#[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#[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
1159pub 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
1164pub 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
1168pub 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
1172pub const IE11_WINDOWS_USER_AGENT: &str =
1174 "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko";
1175
1176#[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#[must_use]
1361pub const fn default_profile_facts() -> ProfileFacts {
1362 profile_facts(DEFAULT_STEALTH_PROFILE)
1363}
1364
1365#[must_use]
1367pub const fn profile_user_agent(profile: StealthProfile) -> &'static str {
1368 profile_facts(profile).user_agent
1369}
1370
1371#[must_use]
1373pub const fn default_profile_user_agent() -> &'static str {
1374 default_profile_facts().user_agent
1375}
1376
1377#[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#[must_use]
1405pub const fn default_profile_navigation_headers() -> [NavigationHeader; 3] {
1406 profile_navigation_headers(DEFAULT_STEALTH_PROFILE)
1407}
1408
1409#[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#[must_use]
1439pub const fn default_profile_browser_headers() -> [NavigationHeader; 4] {
1440 profile_browser_headers(DEFAULT_STEALTH_PROFILE)
1441}
1442
1443#[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#[must_use]
1455pub const fn default_profile_request_headers(kind: BrowserRequestKind) -> BrowserRequestHeaders {
1456 profile_request_headers(DEFAULT_STEALTH_PROFILE, kind)
1457}
1458
1459#[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#[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 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}