1pub mod versions;
3
4pub mod configs;
6pub mod profiles;
8pub mod spoof_gpu;
10#[cfg(feature = "headers")]
11pub mod spoof_headers;
13pub mod spoof_mouse_movement;
15pub mod spoof_refererer;
17pub mod spoof_user_agent;
19pub mod spoof_viewport;
21pub mod spoof_webgl;
23pub mod spoofs;
25
26mod referrers_domains_index;
28mod 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#[derive(PartialEq, Eq)]
67pub enum BrowserKind {
68 Chrome,
70 Brave,
72 Firefox,
74 Safari,
76 Edge,
78 Opera,
80 Other,
82}
83
84impl BrowserKind {
85 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; const P_OPR: usize = 1; const P_CHR: usize = 2; const P_AND: usize = 3; lazy_static::lazy_static! {
101 pub(crate) static ref MOBILE_PATTERNS: [&'static str; 38] = [
103 "iphone", "ipad", "ipod",
105 "android",
107 "mobi", "mobile", "touch",
109 "silk", "nexus", "pixel", "huawei", "honor", "xiaomi", "miui", "redmi",
111 "oneplus", "samsung", "galaxy", "lenovo", "oppo", "vivo", "realme",
112 "opera mini", "opera mobi", "ucbrowser", "ucweb", "baidubrowser", "qqbrowser",
114 "dolfin", "crmo", "fennec", "iemobile", "webos", "blackberry", "bb10",
115 "playbook", "palm", "nokia"
116 ];
117
118 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 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/", "opr/", "opera", "opios", "firefox", "fxios", "chrome/", "crios", "chromium", "safari", "brave", ])
142 .expect("valid device patterns");
143
144
145 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 static ref OS_PATTERNS:[&'static str; 12] =[
152 "iPhone", "iPad", "iOS",
154 "Android",
156 "Windows NT", "Windows", "Win64",
158 "Macintosh", "Mac OS X", "Mac",
160 "Linux",
162 "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 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), ];
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 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
216pub fn detect_is_mobile(ua: &str) -> &'static str {
218 let (ipad, iphone, ipod, android, mobile, tablet) = scan_flags(ua);
219
220 if ipad || iphone || ipod || android || mobile || tablet {
222 "?1"
223 } else {
224 "?0"
225 }
226}
227
228pub fn detect_form_factor(ua: &str) -> &'static str {
230 let (ipad, iphone, ipod, android, mobile, tablet) = scan_flags(ua);
231
232 if ipad {
235 return "Tablet";
236 }
237 if iphone || ipod {
239 return "Mobile";
240 }
241 if android {
243 return if mobile { "Mobile" } else { "Tablet" };
244 }
245 if tablet {
247 return "Tablet";
248 }
249 if mobile {
251 return "Mobile";
252 }
253 "Desktop"
254}
255
256pub 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, 3..=5 => opera = true, 6 | 7 => firefox = true, 8..=10 => chrome = true, 11 => safari = true, 12 => brave = true, _ => (),
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
294pub 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]
311pub 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
333pub 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
372pub fn is_mobile_user_agent(user_agent: &str) -> bool {
374 MOBILE_MATCHER.find(user_agent).is_some()
375}
376
377pub 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
384pub 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
390fn 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 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
496pub 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
502pub 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
508pub 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
517pub 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
527pub 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
537pub 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
546pub fn generate_hide_plugins() -> String {
548 format!(
549 "{}{}",
550 crate::spoofs::NAVIGATOR_SCRIPT,
551 crate::spoofs::PLUGIN_AND_MIMETYPE_SPOOF
552 )
553}
554
555pub fn wrap_eval_script(source: &str) -> String {
557 format!(r#"(()=>{{{}}})();"#, source)
558}
559
560#[derive(Debug, Default, Clone, Copy, PartialEq)]
562#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
563pub enum Fingerprint {
564 Basic,
566 NativeGPU,
569 #[default]
571 None,
572}
573
574impl Fingerprint {
575 pub fn valid(&self) -> bool {
577 matches!(self, Self::Basic | Self::NativeGPU)
578 }
579}
580#[derive(Default, Debug, Clone, Copy, PartialEq)]
582#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
583pub struct EmulationConfiguration {
584 pub tier: configs::Tier,
586 pub fingerprint: Fingerprint,
588 pub agent_os: AgentOs,
590 pub firefox_agent: bool,
592 pub user_agent_data: Option<bool>,
594 pub touch_screen: bool,
596 pub hardware_concurrency: bool,
598 pub dismiss_dialogs: bool,
600 pub disable_notifications: bool,
602 pub disable_permissions: bool,
604 pub disable_media_codecs: bool,
606 pub disable_speech_syntheses: bool,
608 pub disable_media_labels: bool,
610 pub disable_history_length: bool,
612 pub disable_user_agent_data: bool,
614 pub disable_screen: bool,
616 pub disable_touch_screen: bool,
618 pub disable_plugins: bool,
620 pub disable_stealth: bool,
622 pub enable_device_memory: bool,
625 pub enable_cdp_marker_cleanup: bool,
628 pub enable_selenium_marker_cleanup: bool,
631}
632
633pub 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
656pub 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
669impl EmulationConfiguration {
671 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; emulation_config.hardware_concurrency = true; emulation_config.disable_notifications = true; emulation_config.disable_media_codecs = true; emulation_config.disable_plugins = true; emulation_config
692 }
693}
694
695pub 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
704pub 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
716pub 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 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 &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 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
947pub 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
957pub 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 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 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 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 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 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 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 let firefox = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:118.0) \
1033 Gecko/20100101 Firefox/118.0";
1034
1035 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 ("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 ("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 ("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 ("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 ("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 ("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 ("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 ("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 ("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0",
1084 AgentOs::Unknown),
1085
1086 ("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}