spider_fingerprint/
lib.rs

1include!(concat!(env!("OUT_DIR"), "/chrome_versions.rs"));
2
3/// Builder types.
4pub mod configs;
5/// Custom static profiles.
6pub mod profiles;
7/// GPU spoofs.
8pub mod spoof_gpu;
9/// Spoof HTTP headers.
10pub mod spoof_headers;
11/// Spoof mouse-movement.
12pub mod spoof_mouse_movement;
13/// Referer headers.
14pub mod spoof_refererer;
15/// User agent.
16pub mod spoof_user_agent;
17/// Spoof viewport.
18pub mod spoof_viewport;
19/// WebGL spoofs.
20pub mod spoof_webgl;
21/// Generic spoofs.
22pub mod spoofs;
23
24use profiles::{
25    gpu::select_random_gpu_profile,
26    gpu_limits::{build_gpu_request_adapter_script_from_limits, GpuLimits},
27};
28use spoof_gpu::build_gpu_spoof_script_wgsl;
29
30use crate::configs::{AgentOs, Tier};
31use aho_corasick::AhoCorasick;
32pub use spoof_headers::emulate_headers;
33pub use spoof_refererer::spoof_referrer;
34
35pub use http;
36pub use url;
37
38lazy_static::lazy_static! {
39    // Get the latest chrome version as the base to use.
40    pub static ref LATEST_CHROME_FULL_VERSION_FULL: &'static str = CHROME_VERSIONS_BY_MAJOR
41        .get("latest")
42        .and_then(|arr| arr.first().copied())
43        .unwrap_or(&"137.0.7151.56");
44    /// The latest Chrome version major ex: 137.
45    pub static ref BASE_CHROME_VERSION: u32 = LATEST_CHROME_FULL_VERSION_FULL
46        .split('.')
47        .next()
48        .and_then(|v| v.parse::<u32>().ok())
49        .unwrap_or(137);
50    /// The latest Chrome not a brand version, configurable via the `CHROME_NOT_A_BRAND_VERSION` env variable.
51    pub static ref CHROME_NOT_A_BRAND_VERSION: String = std::env::var("CHROME_NOT_A_BRAND_VERSION")
52        .ok()
53        .and_then(|v| if v.is_empty() { None } else { Some(v) })
54        .unwrap_or("99.0.0.0".into());
55
56    pub static ref MOBILE_PATTERNS: [&'static str; 38] = [
57        // Apple
58        "iphone", "ipad", "ipod",
59        // Android
60        "android",
61        // Generic mobile
62        "mobi", "mobile", "touch",
63        // Specific Android browsers/devices
64        "silk", "nexus", "pixel", "huawei", "honor", "xiaomi", "miui", "redmi",
65        "oneplus", "samsung", "galaxy", "lenovo", "oppo", "vivo", "realme",
66        // Mobile browsers
67        "opera mini", "opera mobi", "ucbrowser", "ucweb", "baidubrowser", "qqbrowser",
68        "dolfin", "crmo", "fennec", "iemobile", "webos", "blackberry", "bb10",
69        "playbook", "palm", "nokia"
70    ];
71
72    /// Common mobile indicators for user-agent detection.
73    pub static ref MOBILE_MATCHER: AhoCorasick = aho_corasick::AhoCorasickBuilder::new()
74        .ascii_case_insensitive(true)
75        .build(MOBILE_PATTERNS.as_ref())
76        .expect("failed to compile AhoCorasick patterns");
77}
78
79/// Returns `true` if the user-agent is likely a mobile browser.
80pub fn is_mobile_user_agent(user_agent: &str) -> bool {
81    MOBILE_MATCHER.find(user_agent).is_some()
82}
83
84/// Does the user-agent matches a mobile device indicator.
85pub fn mobile_model_from_user_agent(user_agent: &str) -> Option<&'static str> {
86    MOBILE_MATCHER
87        .find(&user_agent)
88        .map(|m| MOBILE_PATTERNS[m.pattern()])
89}
90
91/// Generate the initial stealth script to send in one command.
92pub fn build_stealth_script(tier: Tier, os: AgentOs) -> String {
93    use crate::spoofs::{
94        spoof_hardware_concurrency, unified_worker_override, HIDE_CHROME, HIDE_CONSOLE,
95        HIDE_WEBDRIVER, NAVIGATOR_SCRIPT, PLUGIN_AND_MIMETYPE_SPOOF,
96    };
97
98    let gpu_profile = select_random_gpu_profile(os);
99    let spoof_gpu = build_gpu_spoof_script_wgsl(gpu_profile.canvas_format);
100    let spoof_webgl = unified_worker_override(
101        gpu_profile.hardware_concurrency,
102        gpu_profile.webgl_vendor,
103        gpu_profile.webgl_renderer,
104    );
105    let spoof_concurrency = spoof_hardware_concurrency(gpu_profile.hardware_concurrency);
106
107    let mut gpu_limit = GpuLimits::for_os(os);
108
109    if gpu_profile.webgl_renderer
110        != "ANGLE (Apple, ANGLE Metal Renderer: Apple M1, Unspecified Version)"
111    {
112        gpu_limit = gpu_limit.with_variation(gpu_profile.hardware_concurrency);
113    }
114
115    let spoof_gpu_adapter = build_gpu_request_adapter_script_from_limits(
116        gpu_profile.webgpu_vendor,
117        gpu_profile.webgpu_architecture,
118        "",
119        "",
120        &gpu_limit,
121    );
122
123    if tier == Tier::Basic {
124        format!(
125            r#"{HIDE_CHROME}{HIDE_CONSOLE}{spoof_webgl}{spoof_gpu_adapter}{NAVIGATOR_SCRIPT}{PLUGIN_AND_MIMETYPE_SPOOF}"#
126        )
127    } else if tier == Tier::BasicWithConsole {
128        format!(
129            r#"{HIDE_CHROME}{spoof_webgl}{spoof_gpu_adapter}{NAVIGATOR_SCRIPT}{PLUGIN_AND_MIMETYPE_SPOOF}"#
130        )
131    } else if tier == Tier::BasicNoWebgl {
132        format!(
133            r#"{HIDE_CHROME}{HIDE_CONSOLE}{spoof_concurrency}{NAVIGATOR_SCRIPT}{PLUGIN_AND_MIMETYPE_SPOOF}"#
134        )
135    } else if tier == Tier::Mid {
136        format!(
137            r#"{HIDE_CHROME}{HIDE_CONSOLE}{spoof_webgl}{spoof_gpu_adapter}{HIDE_WEBDRIVER}{NAVIGATOR_SCRIPT}{PLUGIN_AND_MIMETYPE_SPOOF}"#
138        )
139    } else if tier == Tier::Full {
140        format!("{HIDE_CHROME}{HIDE_CONSOLE}{spoof_webgl}{spoof_gpu_adapter}{HIDE_WEBDRIVER}{NAVIGATOR_SCRIPT}{PLUGIN_AND_MIMETYPE_SPOOF}{spoof_gpu}")
141    } else {
142        Default::default()
143    }
144}
145
146/// Generate the hide plugins script.
147pub fn generate_hide_plugins() -> String {
148    format!(
149        "{}{}",
150        crate::spoofs::NAVIGATOR_SCRIPT,
151        crate::spoofs::PLUGIN_AND_MIMETYPE_SPOOF
152    )
153}
154
155/// Simple function to wrap the eval script safely.
156pub fn wrap_eval_script(source: &str) -> String {
157    format!(r#"(()=>{{{}}})();"#, source)
158}
159
160/// The fingerprint type to use.
161#[derive(Debug, Default, Clone, Copy, PartialEq)]
162#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
163pub enum Fingerprint {
164    /// Basic finterprint that includes webgl and gpu attempt spoof.
165    Basic,
166    /// Basic fingerprint that does not spoof the gpu. Used for real gpu based headless instances.
167    /// This will bypass the most advanced anti-bots without the speed reduction of a virtual display.
168    NativeGPU,
169    /// None - no fingerprint and use the default browser fingerprinting. This may be a good option to use at times.
170    #[default]
171    None,
172}
173
174impl Fingerprint {
175    /// Fingerprint should be used.
176    pub fn valid(&self) -> bool {
177        match &self {
178            Self::Basic | Self::NativeGPU => true,
179            _ => false,
180        }
181    }
182}
183/// Configuration options for browser fingerprinting and automation.
184#[derive(Default, Debug, Clone, Copy, PartialEq)]
185#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
186pub struct EmulationConfiguration {
187    /// Enables stealth mode to help avoid detection by anti-bot mechanisms.
188    pub tier: configs::Tier,
189    /// If enabled, will auto-dismiss browser popups and dialogs.
190    pub dismiss_dialogs: bool,
191    /// The detailed fingerprint configuration for the browser session.
192    pub fingerprint: Fingerprint,
193    /// The agent os.
194    pub agent_os: AgentOs,
195    /// Is this firefox?
196    pub firefox_agent: bool,
197    /// Add userAgentData. Usually can be disabled when set via CDP for accuracy.
198    pub user_agent_data: Option<bool>,
199}
200
201/// Get the OS being used.
202pub fn get_agent_os(user_agent: &str) -> AgentOs {
203    let mut agent_os = AgentOs::Unknown;
204
205    if user_agent.contains("Chrome") {
206        if user_agent.contains("Linux") {
207            agent_os = AgentOs::Linux;
208        } else if user_agent.contains("Mac") {
209            agent_os = AgentOs::Mac;
210        } else if user_agent.contains("Windows") {
211            agent_os = AgentOs::Windows;
212        } else if user_agent.contains("Android") {
213            agent_os = AgentOs::Android;
214        }
215    }
216
217    agent_os
218}
219
220/// Setup the emulation defaults.
221impl EmulationConfiguration {
222    /// Setup the defaults.
223    pub fn setup_defaults(user_agent: &str) -> EmulationConfiguration {
224        let mut firefox_agent = false;
225
226        let agent_os = get_agent_os(user_agent);
227
228        if agent_os == AgentOs::Unknown {
229            firefox_agent = user_agent.contains("Firefox");
230        }
231
232        let mut emulation_config = Self::default();
233
234        emulation_config.firefox_agent = firefox_agent;
235        emulation_config.agent_os = agent_os;
236
237        emulation_config
238    }
239}
240
241/// Join the scrips pre-allocated.
242fn join_scripts<I: IntoIterator<Item = impl AsRef<str>>>(parts: I) -> String {
243    // Heuristically preallocate some capacity (tweak as needed for your use-case).
244    let mut script = String::with_capacity(4096);
245    for part in parts {
246        script.push_str(part.as_ref());
247    }
248    script
249}
250
251/// Emulate a real chrome browser.
252pub fn emulate(
253    user_agent: &str,
254    config: &EmulationConfiguration,
255    viewport: &Option<&crate::spoof_viewport::Viewport>,
256    evaluate_on_new_document: &Option<Box<String>>,
257) -> Option<String> {
258    use crate::spoof_gpu::{
259        FP_JS, FP_JS_GPU_LINUX, FP_JS_GPU_MAC, FP_JS_GPU_WINDOWS, FP_JS_LINUX, FP_JS_MAC,
260        FP_JS_WINDOWS,
261    };
262    use crate::spoofs::{
263        resolve_dpr, spoof_history_length_script, spoof_media_codecs_script,
264        spoof_media_labels_script, spoof_screen_script_rng, spoof_touch_screen, DISABLE_DIALOGS,
265        SPOOF_NOTIFICATIONS, SPOOF_PERMISSIONS_QUERY,
266    };
267    use rand::Rng;
268
269    let stealth = config.tier.stealth();
270    let dismiss_dialogs = config.dismiss_dialogs;
271    let agent_os = config.agent_os;
272    let firefox_agent = config.firefox_agent;
273
274    let spoof_script = if stealth && !firefox_agent && config.user_agent_data.unwrap_or(true) {
275        &crate::spoof_user_agent::spoof_user_agent_data_high_entropy_values(
276            &crate::spoof_user_agent::build_high_entropy_data(&Some(user_agent)),
277        )
278    } else {
279        &Default::default()
280    };
281
282    let linux = agent_os == AgentOs::Linux;
283
284    let mut fingerprint_gpu = false;
285    let fingerprint = match config.fingerprint {
286        Fingerprint::Basic => true,
287        Fingerprint::NativeGPU => {
288            fingerprint_gpu = true;
289            true
290        }
291        _ => false,
292    };
293
294    let fp_script = if fingerprint {
295        let fp_script = if linux {
296            if fingerprint_gpu {
297                &*FP_JS_GPU_LINUX
298            } else {
299                &*FP_JS_LINUX
300            }
301        } else if agent_os == AgentOs::Mac {
302            if fingerprint_gpu {
303                &*FP_JS_GPU_MAC
304            } else {
305                &*FP_JS_MAC
306            }
307        } else if agent_os == AgentOs::Windows {
308            if fingerprint_gpu {
309                &*FP_JS_GPU_WINDOWS
310            } else {
311                &*FP_JS_WINDOWS
312            }
313        } else {
314            &*FP_JS
315        };
316        fp_script
317    } else {
318        &Default::default()
319    };
320
321    let disable_dialogs = if dismiss_dialogs { DISABLE_DIALOGS } else { "" };
322    let mut mobile_device = false;
323
324    let screen_spoof = if let Some(viewport) = &viewport {
325        mobile_device = viewport.emulating_mobile;
326        let dpr = resolve_dpr(
327            viewport.emulating_mobile,
328            viewport.device_scale_factor,
329            agent_os,
330        );
331
332        spoof_screen_script_rng(
333            viewport.width,
334            viewport.height,
335            dpr,
336            viewport.emulating_mobile,
337            &mut rand::rng(),
338            agent_os,
339        )
340    } else {
341        Default::default()
342    };
343
344    let st = crate::build_stealth_script(config.tier, agent_os);
345
346    // Final combined script to inject
347    let merged_script = if let Some(script) = evaluate_on_new_document.as_deref() {
348        if fingerprint {
349            let mut b = join_scripts([
350                &fp_script,
351                &spoof_script,
352                disable_dialogs,
353                &screen_spoof,
354                SPOOF_NOTIFICATIONS,
355                SPOOF_PERMISSIONS_QUERY,
356                &spoof_media_codecs_script(),
357                &spoof_touch_screen(mobile_device),
358                &spoof_media_labels_script(agent_os),
359                &spoof_history_length_script(rand::rng().random_range(1..=6)),
360                &st,
361                &wrap_eval_script(script),
362            ]);
363
364            b.push_str(&wrap_eval_script(script));
365
366            Some(b)
367        } else {
368            let mut b = join_scripts([
369                &spoof_script,
370                disable_dialogs,
371                &screen_spoof,
372                SPOOF_NOTIFICATIONS,
373                SPOOF_PERMISSIONS_QUERY,
374                &spoof_media_codecs_script(),
375                &spoof_touch_screen(mobile_device),
376                &spoof_media_labels_script(agent_os),
377                &spoof_history_length_script(rand::rng().random_range(1..=6)),
378                &st,
379                &wrap_eval_script(script),
380            ]);
381            b.push_str(&wrap_eval_script(script));
382
383            Some(b)
384        }
385    } else if fingerprint {
386        Some(join_scripts([
387            &fp_script,
388            &spoof_script,
389            disable_dialogs,
390            &screen_spoof,
391            SPOOF_NOTIFICATIONS,
392            SPOOF_PERMISSIONS_QUERY,
393            &spoof_media_codecs_script(),
394            &spoof_touch_screen(mobile_device),
395            &spoof_media_labels_script(agent_os),
396            &spoof_history_length_script(rand::rng().random_range(1..=6)),
397            &st,
398        ]))
399    } else if stealth {
400        Some(join_scripts([
401            &spoof_script,
402            disable_dialogs,
403            &screen_spoof,
404            SPOOF_NOTIFICATIONS,
405            SPOOF_PERMISSIONS_QUERY,
406            &spoof_media_codecs_script(),
407            &spoof_touch_screen(mobile_device),
408            &spoof_media_labels_script(agent_os),
409            &spoof_history_length_script(rand::rng().random_range(1..=6)),
410            &st,
411        ]))
412    } else {
413        None
414    };
415
416    merged_script
417}