Skip to main content

spider_fingerprint/
lib.rs

1/// Versions for chrome.
2pub mod versions;
3
4/// Builder types.
5pub mod configs;
6/// Custom static profiles.
7pub mod profiles;
8/// GPU spoofs.
9pub mod spoof_gpu;
10#[cfg(feature = "headers")]
11/// Spoof HTTP headers.
12pub mod spoof_headers;
13/// Spoof mouse-movement.
14pub mod spoof_mouse_movement;
15/// Referer headers.
16pub mod spoof_refererer;
17/// User agent.
18pub mod spoof_user_agent;
19/// Spoof viewport.
20pub mod spoof_viewport;
21/// WebGL spoofs.
22pub mod spoof_webgl;
23/// Generic spoofs.
24pub mod spoofs;
25
26/// Referrer domains index.
27mod referrers_domains_index;
28/// High quality referrer index.
29mod referrers_hq_index;
30
31#[cfg(feature = "headers")]
32pub use spoof_headers::emulate_headers;
33pub use spoof_refererer::spoof_referrer;
34
35use configs::{AgentOs, Tier};
36use profiles::{
37    gpu::{select_random_gpu_profile, GpuProfile},
38    gpu_limits::{build_gpu_request_adapter_script_from_limits, GpuLimits},
39};
40use rand::prelude::IndexedRandom;
41use rand::Rng;
42use spoof_gpu::{
43    build_gpu_spoof_script_wgsl, FP_JS, FP_JS_GPU_LINUX, FP_JS_GPU_MAC, FP_JS_GPU_WINDOWS,
44    FP_JS_LINUX, FP_JS_MAC, FP_JS_WINDOWS,
45};
46use spoofs::{
47    resolve_dpr, spoof_device_memory, spoof_history_length_script, spoof_media_codecs_script,
48    spoof_media_labels_script, spoof_screen_script_rng, spoof_touch_screen, CLEANUP_CDP_MARKERS,
49    DISABLE_DIALOGS, HIDE_SELENIUM_MARKERS, SPOOF_NOTIFICATIONS, SPOOF_PERMISSIONS_QUERY,
50};
51
52#[cfg(feature = "headers")]
53pub use http;
54pub use url;
55
56pub use versions::{
57    BASE_CHROME_VERSION, CHROME_NOT_A_BRAND_VERSION, CHROME_VERSIONS_BY_MAJOR, CHROME_VERSION_FULL,
58    LATEST_CHROME_FULL_VERSION_FULL,
59};
60
61use crate::spoofs::{
62    PATCH_SPEECH_SYNTHESIS, PLUGIN_AND_MIMETYPE_SPOOF, PLUGIN_AND_MIMETYPE_SPOOF_CHROME,
63};
64
65/// The kind of browser.
66#[derive(PartialEq, Eq)]
67pub enum BrowserKind {
68    /// Chrome
69    Chrome,
70    /// Brave
71    Brave,
72    /// Firefox
73    Firefox,
74    /// Safari
75    Safari,
76    /// Edge
77    Edge,
78    /// Opera
79    Opera,
80    /// Other
81    Other,
82}
83
84impl BrowserKind {
85    /// Is the browser chromium based.
86    fn is_chromium(&self) -> bool {
87        match &self {
88            BrowserKind::Chrome | BrowserKind::Opera | BrowserKind::Brave | BrowserKind::Edge => {
89                true
90            }
91            _ => false,
92        }
93    }
94}
95const P_EDG: usize = 0; // "edg/"
96const P_OPR: usize = 1; // "opr/"
97const P_CHR: usize = 2; // "chrome/"
98const P_AND: usize = 3; // "android"
99
100lazy_static::lazy_static! {
101    /// Common mobile device patterns.
102    pub(crate) static ref MOBILE_PATTERNS: [&'static str; 38] = [
103        // Apple
104        "iphone", "ipad", "ipod",
105        // Android
106        "android",
107        // Generic mobile
108        "mobi", "mobile", "touch",
109        // Specific Android browsers/devices
110        "silk", "nexus", "pixel", "huawei", "honor", "xiaomi", "miui", "redmi",
111        "oneplus", "samsung", "galaxy", "lenovo", "oppo", "vivo", "realme",
112        // Mobile browsers
113        "opera mini", "opera mobi", "ucbrowser", "ucweb", "baidubrowser", "qqbrowser",
114        "dolfin", "crmo", "fennec", "iemobile", "webos", "blackberry", "bb10",
115        "playbook", "palm", "nokia"
116    ];
117
118    /// Common mobile indicators for user-agent detection.
119    pub(crate) static ref MOBILE_MATCHER: aho_corasick::AhoCorasick = aho_corasick::AhoCorasickBuilder::new()
120        .ascii_case_insensitive(true)
121        .build(MOBILE_PATTERNS.as_ref())
122        .expect("failed to compile AhoCorasick patterns");
123
124
125    /// Allowed ua data for chromium based browsers.
126    pub(crate) static ref ALLOWED_UA_DATA: aho_corasick::AhoCorasick = aho_corasick::AhoCorasickBuilder::new()
127            .ascii_case_insensitive(true)
128            .match_kind(aho_corasick::MatchKind::LeftmostFirst)
129            .build(&["edg/", "opr/", "chrome/", "android"])
130            .expect("valid device patterns");
131
132    pub(crate) static ref BROWSER_MATCH: aho_corasick::AhoCorasick = aho_corasick::AhoCorasickBuilder::new()
133            .ascii_case_insensitive(true)
134            .build(&[
135                "edg/", "edgios", "edge/",        // Edge
136                "opr/", "opera", "opios",         // Opera
137                "firefox", "fxios",               // Firefox
138                "chrome/", "crios", "chromium",   // Chrome
139                "safari",                         // Safari
140                "brave",                          // Brave (incase future changes add.)
141            ])
142            .expect("valid device patterns");
143
144
145        // Detect Chrome/CriOS first (we only classify Chrome-family UAs).
146        static ref CHROME_AC: aho_corasick::AhoCorasick = aho_corasick::AhoCorasickBuilder::new()
147                .ascii_case_insensitive(true)
148                .build(["Chrome", "CriOS"]) .expect("valid device patterns");
149
150        /// OS patterns. Order doesn’t matter; we store priorities separately.
151        static ref OS_PATTERNS:[&'static str; 12] =[
152            // iOS family first (iPad/iPhone contain "Mac OS X" too, so give them better priority)
153            "iPhone", "iPad", "iOS",
154            // Android
155            "Android",
156            // Windows
157            "Windows NT", "Windows", "Win64",
158            // Mac
159            "Macintosh", "Mac OS X", "Mac",
160            // Linux
161            "Linux",
162            // ChromeOS if you later add an enum variant:
163            "CrOS",
164        ];
165
166        static ref OS_AC: aho_corasick::AhoCorasick = aho_corasick::AhoCorasickBuilder::new()
167                .ascii_case_insensitive(true)
168                .build(*OS_PATTERNS)
169                .expect("valid device patterns");
170
171        /// Map each pattern index -> (AgentOs, priority). Lower priority wins on ties.
172        static ref OS_MAP: [ (AgentOs, u8); 12 ] = [
173            (AgentOs::IPhone,  0),
174            (AgentOs::IPad,    0),
175            (AgentOs::IPhone,  1),
176            (AgentOs::Android, 0),
177            (AgentOs::Windows, 2),
178            (AgentOs::Windows, 3),
179            (AgentOs::Windows, 4),
180            (AgentOs::Mac,     5),
181            (AgentOs::Mac,     6),
182            (AgentOs::Mac,     7),
183            (AgentOs::Linux,   9),
184            (AgentOs::Linux,   8), // CrOS → Linux fallback
185        ];
186
187        static ref FF_PATTERNS: [&'static str; 6] = [
188            "iPad", "iPhone", "iPod", "Android", "Mobile", "Tablet",
189        ];
190
191        static ref FF_AC: aho_corasick::AhoCorasick = aho_corasick::AhoCorasickBuilder::new()
192                .ascii_case_insensitive(true)
193                .build(*FF_PATTERNS)
194                .expect("valid device patterns");
195}
196
197#[inline]
198fn scan_flags(ua: &str) -> (bool, bool, bool, bool, bool, bool) {
199    // (ipad, iphone, ipod, android, mobile, tablet)
200    let (mut ipad, mut iphone, mut ipod, mut android, mut mobile, mut tablet) =
201        (false, false, false, false, false, false);
202    for m in FF_AC.find_iter(ua) {
203        match m.pattern().as_u32() {
204            0 => ipad = true,
205            1 => iphone = true,
206            2 => ipod = true,
207            3 => android = true,
208            4 => mobile = true,
209            5 => tablet = true,
210            _ => {}
211        }
212    }
213    (ipad, iphone, ipod, android, mobile, tablet)
214}
215
216/// Return "?1" (mobile) or "?0" (not mobile).
217pub fn detect_is_mobile(ua: &str) -> &'static str {
218    let (ipad, iphone, ipod, android, mobile, tablet) = scan_flags(ua);
219
220    // Tablet devices are considered "mobile = true" per your C++ mapping.
221    if ipad || iphone || ipod || android || mobile || tablet {
222        "?1"
223    } else {
224        "?0"
225    }
226}
227
228/// Return the form factor: "Mobile" | "Tablet" | "Desktop".
229pub fn detect_form_factor(ua: &str) -> &'static str {
230    let (ipad, iphone, ipod, android, mobile, tablet) = scan_flags(ua);
231
232    // Priority:
233    // 1) iPad => Tablet (even if "Mobile" token appears)
234    if ipad {
235        return "Tablet";
236    }
237    // 2) iPhone/iPod => Mobile
238    if iphone || ipod {
239        return "Mobile";
240    }
241    // 3) Android: "Mobile" token => Mobile phone, otherwise Tablet
242    if android {
243        return if mobile { "Mobile" } else { "Tablet" };
244    }
245    // 4) Explicit "Tablet" token
246    if tablet {
247        return "Tablet";
248    }
249    // 5) Generic "Mobile" token
250    if mobile {
251        return "Mobile";
252    }
253    "Desktop"
254}
255
256/// Detect the browser type.
257pub fn detect_browser(ua: &str) -> &'static str {
258    let mut edge = false;
259    let mut opera = false;
260    let mut firefox = false;
261    let mut chrome = false;
262    let mut safari = false;
263    let mut brave = false;
264
265    for m in BROWSER_MATCH.find_iter(ua) {
266        match m.pattern().as_u32() {
267            0..=2 => edge = true,    // edg..., edge/
268            3..=5 => opera = true,   // opr/opera/opios
269            6 | 7 => firefox = true, // firefox/fxios
270            8..=10 => chrome = true, // chrome/, crios, chromium
271            11 => safari = true,     // safari
272            12 => brave = true,      // brave
273            _ => (),
274        }
275    }
276
277    if brave && chrome && !edge && !opera {
278        "brave"
279    } else if chrome && !edge && !opera {
280        "chrome"
281    } else if safari && !chrome && !edge && !opera && !firefox {
282        "safari"
283    } else if edge {
284        "edge"
285    } else if firefox {
286        "firefox"
287    } else if opera {
288        "opera"
289    } else {
290        "unknown"
291    }
292}
293
294/// Detect the browser type to BrowserKind.
295pub fn detect_browser_kind(ua: &str) -> BrowserKind {
296    let s = detect_browser(ua);
297
298    match s {
299        "chrome" => BrowserKind::Chrome,
300        "brave" => BrowserKind::Brave,
301        "safari" => BrowserKind::Safari,
302        "edge" => BrowserKind::Edge,
303        "firefox" => BrowserKind::Firefox,
304        "opera" => BrowserKind::Opera,
305        "unknown" => BrowserKind::Other,
306        _ => BrowserKind::Other,
307    }
308}
309
310#[inline]
311/// Parse the major after.
312pub fn parse_major_after(s: &str, end_token: usize) -> Option<u32> {
313    if end_token >= s.len() {
314        return None;
315    }
316    let bytes = s.as_bytes();
317    let mut i = end_token;
318    let mut n: u32 = 0;
319    let mut saw = false;
320    while i < bytes.len() {
321        let b = bytes[i];
322        if (b'0'..=b'9').contains(&b) {
323            saw = true;
324            n = n.saturating_mul(10) + (b - b'0') as u32;
325            i += 1;
326        } else {
327            break;
328        }
329    }
330    saw.then_some(n)
331}
332
333/// The user-agent allows navigator.userAgentData.getHighEntropyValues
334pub fn ua_allows_gethighentropy(ua: &str) -> bool {
335    let mut seen: u32 = 0;
336    let mut endpos: [Option<usize>; 4] = [None; 4];
337
338    for m in ALLOWED_UA_DATA.find_iter(ua) {
339        let idx = m.pattern().as_usize();
340        if endpos[idx].is_none() {
341            endpos[idx] = Some(m.end());
342            seen |= 1u32 << idx;
343        }
344    }
345
346    let has = |i: usize| (seen & (1u32 << i)) != 0;
347    let is_android = has(P_AND);
348
349    if let Some(end) = endpos[P_EDG] {
350        if is_android {
351            return false;
352        }
353        return parse_major_after(ua, end).is_some_and(|v| v >= 90);
354    }
355    if let Some(end) = endpos[P_OPR] {
356        return parse_major_after(ua, end).is_some_and(
357            |v| {
358                if is_android {
359                    v >= 64
360                } else {
361                    v >= 76
362                }
363            },
364        );
365    }
366    if let Some(end) = endpos[P_CHR] {
367        return parse_major_after(ua, end).is_some_and(|v| v >= 90);
368    }
369    false
370}
371
372/// Returns `true` if the user-agent is likely a mobile browser.
373pub fn is_mobile_user_agent(user_agent: &str) -> bool {
374    MOBILE_MATCHER.find(user_agent).is_some()
375}
376
377/// Does the user-agent matches a mobile device indicator.
378pub fn mobile_model_from_user_agent(user_agent: &str) -> Option<&'static str> {
379    MOBILE_MATCHER
380        .find(user_agent)
381        .map(|m| MOBILE_PATTERNS[m.pattern()])
382}
383
384/// Get a random device hardware concurrency.
385pub fn get_random_hardware_concurrency(user_agent: &str) -> usize {
386    let gpu_profile = select_random_gpu_profile(get_agent_os(user_agent));
387    gpu_profile.hardware_concurrency
388}
389
390/// Generate the initial stealth script to send in one command.
391fn build_stealth_script_base(
392    gpu_profile: &'static GpuProfile,
393    tier: Tier,
394    os: AgentOs,
395    concurrency: bool,
396    browser: BrowserKind,
397) -> String {
398    use crate::spoofs::{
399        spoof_hardware_concurrency, unified_worker_override, worker_override, HIDE_CHROME,
400        HIDE_CONSOLE, HIDE_WEBDRIVER, NAVIGATOR_SCRIPT, REMOVE_CHROME,
401    };
402
403    // tmp used for chrome only os checking.
404    let chrome = browser.is_chromium() || os != AgentOs::Unknown;
405
406    let spoof_worker = if tier == Tier::BasicNoWorker {
407        Default::default()
408    } else if concurrency {
409        unified_worker_override(
410            gpu_profile.hardware_concurrency,
411            gpu_profile.webgl_vendor,
412            gpu_profile.webgl_renderer,
413            !matches!(
414                tier,
415                |Tier::BasicNoWebglWithGPU| Tier::BasicNoWebglWithGPUNoExtra
416                    | Tier::BasicNoWebglWithGPUcWithConsole
417            ),
418        )
419    } else {
420        worker_override(gpu_profile.webgl_vendor, gpu_profile.webgl_renderer)
421    };
422
423    let spoof_concurrency = if concurrency {
424        spoof_hardware_concurrency(gpu_profile.hardware_concurrency)
425    } else {
426        Default::default()
427    };
428
429    let gpu_limit = GpuLimits::for_os(os).with_variation(gpu_profile.hardware_concurrency);
430
431    let spoof_gpu_adapter = build_gpu_request_adapter_script_from_limits(
432        gpu_profile.webgpu_vendor,
433        gpu_profile.webgpu_architecture,
434        "",
435        "",
436        &gpu_limit,
437    );
438
439    let chrome_spoof = if chrome { HIDE_CHROME } else { REMOVE_CHROME };
440
441    match tier {
442        Tier::Basic | Tier::BasicNoWorker | Tier::BasicNoExtra => {
443            format!(
444                r#"{chrome_spoof};{HIDE_CONSOLE};{spoof_worker};{spoof_gpu_adapter};{NAVIGATOR_SCRIPT}"#
445            )
446        }
447        Tier::BasicWithConsole => {
448            format!(
449                r#"{chrome_spoof};{spoof_worker};{spoof_concurrency};{spoof_gpu_adapter};{NAVIGATOR_SCRIPT}"#
450            )
451        }
452        Tier::BasicNoWebgl | Tier::BasicNoWebglWithGPU | Tier::BasicNoWebglWithGPUNoExtra => {
453            format!(
454                r#"{chrome_spoof};{HIDE_CONSOLE};{spoof_worker};{spoof_concurrency};{NAVIGATOR_SCRIPT}"#
455            )
456        }
457        Tier::BasicNoWebglWithGPUcWithConsole => {
458            format!(r#"{chrome_spoof};{spoof_worker};{spoof_concurrency};{NAVIGATOR_SCRIPT}"#)
459        }
460        Tier::HideOnly => {
461            format!(r#"{chrome_spoof};{HIDE_CONSOLE};{HIDE_WEBDRIVER}"#)
462        }
463        Tier::HideOnlyWithConsole => {
464            format!(r#"{chrome_spoof};{HIDE_WEBDRIVER}"#)
465        }
466        Tier::HideOnlyChrome => chrome_spoof.into(),
467        Tier::Low => {
468            format!(
469                r#"{chrome_spoof};{HIDE_CONSOLE};{spoof_worker};{spoof_concurrency};{spoof_gpu_adapter};{HIDE_WEBDRIVER}"#
470            )
471        }
472        Tier::LowWithPlugins => {
473            format!(
474                r#"{chrome_spoof};{HIDE_CONSOLE};{spoof_worker};{spoof_concurrency};{spoof_gpu_adapter};{HIDE_WEBDRIVER}"#
475            )
476        }
477        Tier::LowWithNavigator => {
478            format!(
479                r#"{chrome_spoof};{HIDE_CONSOLE};{spoof_worker};{spoof_concurrency};{spoof_gpu_adapter};{HIDE_WEBDRIVER};{NAVIGATOR_SCRIPT}"#
480            )
481        }
482        Tier::Mid => {
483            format!(
484                r#"{chrome_spoof};{HIDE_CONSOLE};{spoof_worker};{spoof_concurrency};{spoof_gpu_adapter};{NAVIGATOR_SCRIPT};{HIDE_WEBDRIVER}"#
485            )
486        }
487        Tier::Full => {
488            let spoof_gpu = build_gpu_spoof_script_wgsl(gpu_profile.canvas_format);
489
490            format!("{chrome_spoof};{HIDE_CONSOLE};{spoof_worker};{spoof_concurrency};{spoof_gpu_adapter};{HIDE_WEBDRIVER};{NAVIGATOR_SCRIPT};{spoof_gpu}")
491        }
492        _ => Default::default(),
493    }
494}
495
496/// Generate the initial stealth script to send in one command.
497pub fn build_stealth_script(tier: Tier, os: AgentOs) -> String {
498    let gpu_profile = select_random_gpu_profile(os);
499    build_stealth_script_base(gpu_profile, tier, os, true, BrowserKind::Other)
500}
501
502/// Generate the initial stealth script to send in one command without hardware concurrency.
503pub fn build_stealth_script_no_concurrency(tier: Tier, os: AgentOs) -> String {
504    let gpu_profile = select_random_gpu_profile(os);
505    build_stealth_script_base(gpu_profile, tier, os, false, BrowserKind::Other)
506}
507
508/// Generate the initial stealth script to send in one command and profile.
509pub fn build_stealth_script_with_profile(
510    gpu_profile: &'static GpuProfile,
511    tier: Tier,
512    os: AgentOs,
513) -> String {
514    build_stealth_script_base(gpu_profile, tier, os, true, BrowserKind::Other)
515}
516
517/// Generate the initial stealth script to send in one command and profile.
518pub fn build_stealth_script_with_profile_and_browser(
519    gpu_profile: &'static GpuProfile,
520    tier: Tier,
521    os: AgentOs,
522    browser: BrowserKind,
523) -> String {
524    build_stealth_script_base(gpu_profile, tier, os, true, browser)
525}
526
527/// Generate the initial stealth script to send in one command without hardware concurrency and profile.
528pub fn build_stealth_script_no_concurrency_with_profile_and_browser(
529    gpu_profile: &'static GpuProfile,
530    tier: Tier,
531    os: AgentOs,
532    browser: BrowserKind,
533) -> String {
534    build_stealth_script_base(gpu_profile, tier, os, false, browser)
535}
536
537/// Generate the initial stealth script to send in one command without hardware concurrency and profile.
538pub fn build_stealth_script_no_concurrency_with_profile(
539    gpu_profile: &'static GpuProfile,
540    tier: Tier,
541    os: AgentOs,
542) -> String {
543    build_stealth_script_base(gpu_profile, tier, os, false, BrowserKind::Other)
544}
545
546/// Generate the hide plugins script.
547pub fn generate_hide_plugins() -> String {
548    format!(
549        "{}{}",
550        crate::spoofs::NAVIGATOR_SCRIPT,
551        crate::spoofs::PLUGIN_AND_MIMETYPE_SPOOF
552    )
553}
554
555/// Simple function to wrap the eval script safely.
556pub fn wrap_eval_script(source: &str) -> String {
557    format!(r#"(()=>{{{}}})();"#, source)
558}
559
560/// The fingerprint type to use.
561#[derive(Debug, Default, Clone, Copy, PartialEq)]
562#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
563pub enum Fingerprint {
564    /// Basic finterprint that includes webgl and gpu attempt spoof.
565    Basic,
566    /// Basic fingerprint that does not spoof the gpu. Used for real gpu based headless instances.
567    /// This will bypass the most advanced anti-bots without the speed reduction of a virtual display.
568    NativeGPU,
569    /// None - no fingerprint and use the default browser fingerprinting. This may be a good option to use at times.
570    #[default]
571    None,
572}
573
574impl Fingerprint {
575    /// Fingerprint should be used.
576    pub fn valid(&self) -> bool {
577        matches!(self, Self::Basic | Self::NativeGPU)
578    }
579}
580/// Configuration options for browser fingerprinting and automation.
581#[derive(Default, Debug, Clone, Copy, PartialEq)]
582#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
583pub struct EmulationConfiguration {
584    /// Enables stealth mode to help avoid detection by anti-bot mechanisms.
585    pub tier: configs::Tier,
586    /// The detailed fingerprint configuration for the browser session.
587    pub fingerprint: Fingerprint,
588    /// The agent os.
589    pub agent_os: AgentOs,
590    /// Is this firefox?
591    pub firefox_agent: bool,
592    /// Add userAgentData. Usually can be disabled when set via CDP for accuracy.
593    pub user_agent_data: Option<bool>,
594    /// Touch screen enabling or disabling emulation based on device?
595    pub touch_screen: bool,
596    /// Hardware concurrency emulation?
597    pub hardware_concurrency: bool,
598    /// If enabled, will auto-dismiss browser popups and dialogs.
599    pub dismiss_dialogs: bool,
600    /// Disable notification emulation.
601    pub disable_notifications: bool,
602    /// Disable permissions emulation.
603    pub disable_permissions: bool,
604    /// Disable media codecs emulation.
605    pub disable_media_codecs: bool,
606    /// Disable speech syntheses.
607    pub disable_speech_syntheses: bool,
608    /// Disable media labels.
609    pub disable_media_labels: bool,
610    /// Disable navigator history length
611    pub disable_history_length: bool,
612    /// Disable the user agent data emulation - extra guard for user_agent_data.
613    pub disable_user_agent_data: bool,
614    /// Disable screen emulation.
615    pub disable_screen: bool,
616    /// Disable touch screen emulation.
617    pub disable_touch_screen: bool,
618    /// Disable the plugins spoof.
619    pub disable_plugins: bool,
620    /// Disable the stealth emulation.
621    pub disable_stealth: bool,
622    /// Enable device memory spoofing (navigator.deviceMemory).
623    /// Disabled by default - opt-in for extra stealth.
624    pub enable_device_memory: bool,
625    /// Enable cleanup of CDP/automation markers (cdc_, $cdc_, etc).
626    /// Disabled by default - opt-in, useful when using ChromeDriver or WebDriver.
627    pub enable_cdp_marker_cleanup: bool,
628    /// Enable cleanup of Selenium-specific markers.
629    /// Disabled by default - opt-in, useful when using Selenium.
630    pub enable_selenium_marker_cleanup: bool,
631}
632
633/// Fast Chrome-only OS detection using Aho-Corasick (ASCII case-insensitive).
634pub fn get_agent_os(user_agent: &str) -> AgentOs {
635    if !CHROME_AC.is_match(user_agent) {
636        return AgentOs::Unknown;
637    }
638    let mut best: Option<(u8, usize, AgentOs)> = None;
639    for m in OS_AC.find_iter(user_agent) {
640        let (os, pri) = OS_MAP[m.pattern()];
641        let cand = (pri, m.len(), os);
642        best = match best {
643            None => Some(cand),
644            Some(cur) => {
645                if cand.0 < cur.0 || (cand.0 == cur.0 && cand.1 > cur.1) {
646                    Some(cand)
647                } else {
648                    Some(cur)
649                }
650            }
651        };
652    }
653    best.map(|t| t.2).unwrap_or(AgentOs::Unknown)
654}
655
656/// Agent Operating system to string
657pub fn agent_os_strings(os: AgentOs) -> &'static str {
658    match os {
659        AgentOs::Android => "Android",
660        AgentOs::IPhone | AgentOs::IPad => "iOS",
661        AgentOs::Mac => "macOS",
662        AgentOs::Windows => "Windows",
663        AgentOs::Linux => "Linux",
664        AgentOs::ChromeOS => "Chrome OS",
665        AgentOs::Unknown => "Unknown",
666    }
667}
668
669/// Setup the emulation defaults.
670impl EmulationConfiguration {
671    /// Setup the defaults.
672    pub fn setup_defaults(user_agent: &str) -> EmulationConfiguration {
673        let mut firefox_agent = false;
674
675        let agent_os = get_agent_os(user_agent);
676
677        if agent_os == AgentOs::Unknown {
678            firefox_agent = user_agent.contains("Firefox");
679        }
680
681        let mut emulation_config = Self::default();
682
683        emulation_config.firefox_agent = firefox_agent;
684        emulation_config.agent_os = agent_os;
685        emulation_config.touch_screen = false; // by default spider_chrome emulates touch over CDP.
686        emulation_config.hardware_concurrency = true; // should be disabled and moved to CDP to cover all frames.
687        emulation_config.disable_notifications = true; // fix
688        emulation_config.disable_media_codecs = true; // fix
689        emulation_config.disable_plugins = true; // fix
690
691        emulation_config
692    }
693}
694
695/// Join the scrips pre-allocated.
696pub fn join_scripts<I: IntoIterator<Item = impl AsRef<str>>>(parts: I) -> String {
697    let mut script = String::with_capacity(4096);
698    for part in parts {
699        script.push_str(part.as_ref());
700    }
701    script
702}
703
704/// Join the scrips pre-allocated.
705pub fn join_scripts_with_capacity<I: IntoIterator<Item = impl AsRef<str>>>(
706    parts: I,
707    capacity: usize,
708) -> String {
709    let mut script = String::with_capacity(capacity);
710    for part in parts {
711        script.push_str(part.as_ref());
712    }
713    script
714}
715
716/// Emulate a real chrome browser.
717pub fn emulate_base(
718    user_agent: &str,
719    config: &EmulationConfiguration,
720    viewport: &Option<&crate::spoof_viewport::Viewport>,
721    evaluate_on_new_document: &Option<Box<String>>,
722    gpu_profile: Option<&'static GpuProfile>,
723) -> Option<String> {
724    let stealth = config.tier.stealth();
725    let agent_os = if config.agent_os == AgentOs::Unknown {
726        get_agent_os(user_agent)
727    } else {
728        config.agent_os
729    };
730    let spoof_user_agent_data = if stealth
731        && config.user_agent_data.unwrap_or(true)
732        && ua_allows_gethighentropy(user_agent)
733    {
734        &crate::spoof_user_agent::spoof_user_agent_data_high_entropy_values(
735            &crate::spoof_user_agent::build_high_entropy_data(&Some(user_agent)),
736        )
737    } else {
738        &Default::default()
739    };
740
741    let spoof_speech_syn = if stealth && agent_os != AgentOs::Unknown {
742        PATCH_SPEECH_SYNTHESIS
743    } else {
744        Default::default()
745    };
746    let linux = agent_os == AgentOs::Linux;
747
748    let no_extra =
749        config.tier == Tier::BasicNoExtra || config.tier == Tier::BasicNoWebglWithGPUNoExtra;
750
751    let (fingerprint, fingerprint_gpu) = match config.fingerprint {
752        Fingerprint::Basic => (true, false),
753        Fingerprint::NativeGPU => (true, true),
754        _ => (false, false),
755    };
756
757    let fp_script = if fingerprint {
758        if linux {
759            if fingerprint_gpu {
760                &*FP_JS_GPU_LINUX
761            } else {
762                &*FP_JS_LINUX
763            }
764        } else if agent_os == AgentOs::Mac {
765            if fingerprint_gpu {
766                &*FP_JS_GPU_MAC
767            } else {
768                &*FP_JS_MAC
769            }
770        } else if agent_os == AgentOs::Windows {
771            if fingerprint_gpu {
772                &*FP_JS_GPU_WINDOWS
773            } else {
774                &*FP_JS_WINDOWS
775            }
776        } else {
777            &*FP_JS
778        }
779    } else {
780        &Default::default()
781    };
782
783    let mut mobile_device = false;
784
785    let screen_spoof = if let Some(viewport) = &viewport {
786        mobile_device = viewport.emulating_mobile;
787        let dpr = resolve_dpr(
788            viewport.emulating_mobile,
789            viewport.device_scale_factor,
790            agent_os,
791        );
792
793        spoof_screen_script_rng(
794            viewport.width,
795            viewport.height,
796            dpr,
797            viewport.emulating_mobile,
798            &mut rand::rng(),
799            agent_os,
800        )
801    } else {
802        Default::default()
803    };
804
805    let gpu_profile = gpu_profile.unwrap_or(select_random_gpu_profile(agent_os));
806    let browser_kind = detect_browser_kind(user_agent);
807
808    let plugin_spoof = if browser_kind == BrowserKind::Chrome {
809        PLUGIN_AND_MIMETYPE_SPOOF_CHROME
810    } else {
811        PLUGIN_AND_MIMETYPE_SPOOF
812    };
813
814    let st = if config.hardware_concurrency {
815        crate::build_stealth_script_with_profile_and_browser(
816            gpu_profile,
817            config.tier,
818            agent_os,
819            browser_kind,
820        )
821    } else {
822        crate::build_stealth_script_no_concurrency_with_profile_and_browser(
823            gpu_profile,
824            config.tier,
825            agent_os,
826            browser_kind,
827        )
828    };
829
830    let touch_screen_script = if config.touch_screen {
831        spoof_touch_screen(mobile_device)
832    } else {
833        Default::default()
834    };
835
836    let eval_script = if let Some(script) = evaluate_on_new_document.as_deref() {
837        wrap_eval_script(script)
838    } else {
839        Default::default()
840    };
841
842    // Device memory spoof (opt-in) - realistic values per platform
843    let device_memory_script = if config.enable_device_memory {
844        let memory = match agent_os {
845            AgentOs::Android | AgentOs::IPhone | AgentOs::IPad => {
846                *[2, 3, 4].choose(&mut rand::rng()).unwrap_or(&4)
847            }
848            _ => *[4, 8].choose(&mut rand::rng()).unwrap_or(&8),
849        };
850        spoof_device_memory(memory)
851    } else {
852        Default::default()
853    };
854
855    let stealth_scripts = if stealth {
856        join_scripts([
857            if no_extra || config.disable_speech_syntheses {
858                Default::default()
859            } else {
860                spoof_speech_syn
861            },
862            if no_extra || config.disable_user_agent_data {
863                Default::default()
864            } else {
865                spoof_user_agent_data
866            },
867            if no_extra || config.dismiss_dialogs {
868                DISABLE_DIALOGS
869            } else {
870                ""
871            },
872            if no_extra || config.disable_screen {
873                Default::default()
874            } else {
875                &screen_spoof
876            },
877            if no_extra || config.disable_notifications {
878                Default::default()
879            } else {
880                SPOOF_NOTIFICATIONS
881            },
882            if no_extra || config.disable_permissions {
883                Default::default()
884            } else {
885                SPOOF_PERMISSIONS_QUERY
886            },
887            if no_extra || config.disable_media_codecs {
888                Default::default()
889            } else {
890                spoof_media_codecs_script()
891            },
892            if no_extra || config.disable_touch_screen {
893                Default::default()
894            } else {
895                touch_screen_script
896            },
897            &if no_extra || config.disable_media_labels {
898                Default::default()
899            } else {
900                spoof_media_labels_script(agent_os)
901            },
902            &if no_extra || config.disable_history_length {
903                Default::default()
904            } else {
905                spoof_history_length_script(rand::rng().random_range(1..=6))
906            },
907            &if no_extra || config.disable_plugins && config.tier != Tier::LowWithPlugins {
908                Default::default()
909            } else {
910                plugin_spoof
911            },
912            // Opt-in spoofs for extra stealth (non-intrusive, safe across profiles)
913            &device_memory_script,
914            if config.enable_cdp_marker_cleanup {
915                CLEANUP_CDP_MARKERS
916            } else {
917                ""
918            },
919            if config.enable_selenium_marker_cleanup {
920                HIDE_SELENIUM_MARKERS
921            } else {
922                ""
923            },
924            &if config.disable_stealth {
925                Default::default()
926            } else {
927                st
928            },
929        ])
930    } else {
931        Default::default()
932    };
933
934    // Final combined script to inject
935    if stealth || fingerprint {
936        Some(join_scripts_with_capacity(
937            [fp_script, &stealth_scripts, &eval_script],
938            fp_script.capacity() + stealth_scripts.capacity() + eval_script.capacity(),
939        ))
940    } else if !eval_script.is_empty() {
941        Some(eval_script)
942    } else {
943        None
944    }
945}
946
947/// Emulate a real chrome browser.
948pub fn emulate(
949    user_agent: &str,
950    config: &EmulationConfiguration,
951    viewport: &Option<&crate::spoof_viewport::Viewport>,
952    evaluate_on_new_document: &Option<Box<String>>,
953) -> Option<String> {
954    emulate_base(user_agent, config, viewport, evaluate_on_new_document, None)
955}
956
957/// Emulate a real chrome browser with a gpu profile.
958pub fn emulate_with_profile(
959    user_agent: &str,
960    config: &EmulationConfiguration,
961    viewport: &Option<&crate::spoof_viewport::Viewport>,
962    evaluate_on_new_document: &Option<Box<String>>,
963    gpu_profile: &'static GpuProfile,
964) -> Option<String> {
965    emulate_base(
966        user_agent,
967        config,
968        viewport,
969        evaluate_on_new_document,
970        Some(gpu_profile),
971    )
972}
973
974#[cfg(test)]
975mod tests {
976    use super::{
977        detect_form_factor, detect_is_mobile, emulate, get_agent_os, ua_allows_gethighentropy,
978        AgentOs, EmulationConfiguration,
979    };
980
981    #[test]
982    fn emulation() {
983        let ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36";
984        let config: EmulationConfiguration = EmulationConfiguration::setup_defaults(&ua);
985        let data = emulate(ua, &config, &None, &None);
986        assert!(data.is_some());
987        if let Some(data) = data {
988            println!("{}", data);
989        }
990    }
991
992    #[test]
993    fn ua_green_supported_positive() {
994        // Chrome desktop ≥90
995        let chrome_win = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
996            AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36";
997
998        // Chrome Android ≥90
999        let chrome_android = "Mozilla/5.0 (Linux; Android 11; Pixel 4) \
1000            AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.61 Mobile Safari/537.36";
1001
1002        // Edge (Chromium) desktop ≥90
1003        let edge_win = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
1004            AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1823.55";
1005
1006        // Opera desktop ≥76 (has OPR and Chrome base)
1007        let opera_win = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
1008            AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36 OPR/76.0.4017.94";
1009
1010        // Opera Android ≥64
1011        let opera_android = "Mozilla/5.0 (Linux; Android 10; SM-G973F) \
1012            AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Mobile Safari/537.36 OPR/64.0.2254.62069";
1013
1014        for ua in [
1015            chrome_win,
1016            chrome_android,
1017            edge_win,
1018            opera_win,
1019            opera_android,
1020        ] {
1021            assert!(ua_allows_gethighentropy(ua), "expected supported: {ua}");
1022        }
1023    }
1024
1025    #[test]
1026    fn ua_green_supported_negative() {
1027        // Chrome desktop 89 (below threshold)
1028        let chrome_89 = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
1029            AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36";
1030
1031        // Firefox (no support)
1032        let firefox = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:118.0) \
1033            Gecko/20100101 Firefox/118.0";
1034
1035        // Safari desktop (no Chrome token)
1036        let safari_mac = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) \
1037            AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15";
1038
1039        for ua in [chrome_89, firefox, safari_mac] {
1040            assert!(
1041                !ua_allows_gethighentropy(ua),
1042                "expected NOT supported: {ua}"
1043            );
1044        }
1045    }
1046
1047    #[test]
1048    fn detects_agent_os_across_platforms() {
1049        let cases: &[(&str, AgentOs)] = &[
1050            // Windows (Chrome)
1051            ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
1052             AgentOs::Windows),
1053
1054            // macOS (Chrome)
1055            ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
1056             AgentOs::Mac),
1057
1058            // Linux (Chrome)
1059            ("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
1060             AgentOs::Linux),
1061
1062            // Android (Chrome)
1063            ("Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36",
1064             AgentOs::Android),
1065
1066            // iPhone (Chrome on iOS uses CriOS)
1067            ("Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/124.0.0.0 Mobile/15E148 Safari/604.1",
1068             AgentOs::IPhone),
1069
1070            // iPad (CriOS) — should still resolve to iOS
1071            ("Mozilla/5.0 (iPad; CPU OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/123.0.0.0 Mobile/15E148 Safari/604.1",
1072             AgentOs::IPad),
1073
1074            // Edge (Chromium) still contains Chrome token -> Windows
1075            ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0",
1076             AgentOs::Windows),
1077
1078            // Mixed case (should be matched case-insensitively) -> Linux
1079            ("mozilla/5.0 (x11; linux x86_64) applewebkit/537.36 (khtml, like gecko) chrome/120.0.0.0 safari/537.36",
1080             AgentOs::Linux),
1081
1082            // Non-Chrome (Firefox) -> Unknown due to Chrome/CriOS gate
1083            ("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0",
1084             AgentOs::Unknown),
1085
1086            // Not a browser UA
1087            ("curl/8.0.1",
1088             AgentOs::Unknown),
1089        ];
1090
1091        for (ua, expected) in cases {
1092            let got = get_agent_os(ua);
1093            assert_eq!(got, *expected, "UA: {}", ua);
1094        }
1095    }
1096
1097    #[test]
1098    fn prioritizes_ios_over_mac_tokens() {
1099        let ua = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/124.0.0.0 Mobile/15E148 Safari/604.1";
1100        assert_eq!(get_agent_os(ua), AgentOs::IPhone);
1101    }
1102
1103    #[test]
1104    fn android_phone() {
1105        let ua = "Mozilla/5.0 (Linux; Android 13; Pixel 7) ... Mobile Safari/537.36";
1106        assert_eq!(detect_is_mobile(ua), "?1");
1107        assert_eq!(detect_form_factor(ua), "Mobile");
1108    }
1109
1110    #[test]
1111    fn android_tablet() {
1112        let ua = "Mozilla/5.0 (Linux; Android 12; SM-T970) ... Safari/537.36";
1113        assert_eq!(detect_is_mobile(ua), "?1");
1114        assert_eq!(detect_form_factor(ua), "Tablet");
1115    }
1116
1117    #[test]
1118    fn iphone_and_ipad() {
1119        let iphone = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 ...) CriOS/124.0.0.0 Mobile/15E148";
1120        assert_eq!(detect_is_mobile(iphone), "?1");
1121        assert_eq!(detect_form_factor(iphone), "Mobile");
1122
1123        let ipad = "Mozilla/5.0 (iPad; CPU OS 16_6 ...) CriOS/123.0.0.0 Mobile/15E148";
1124        assert_eq!(detect_is_mobile(ipad), "?1");
1125        assert_eq!(detect_form_factor(ipad), "Tablet");
1126    }
1127
1128    #[test]
1129    fn desktop_linux() {
1130        let ua = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ... Chrome/124 Safari/537.36";
1131        assert_eq!(detect_is_mobile(ua), "?0");
1132        assert_eq!(detect_form_factor(ua), "Desktop");
1133    }
1134}