1include!(concat!(env!("OUT_DIR"), "/chrome_versions.rs"));
2
3pub mod configs;
5pub mod profiles;
7pub mod spoof_gpu;
9pub mod spoof_headers;
11pub mod spoof_mouse_movement;
13pub mod spoof_refererer;
15pub mod spoof_user_agent;
17pub mod spoof_viewport;
19pub mod spoof_webgl;
21pub 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 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 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 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 "iphone", "ipad", "ipod",
59 "android",
61 "mobi", "mobile", "touch",
63 "silk", "nexus", "pixel", "huawei", "honor", "xiaomi", "miui", "redmi",
65 "oneplus", "samsung", "galaxy", "lenovo", "oppo", "vivo", "realme",
66 "opera mini", "opera mobi", "ucbrowser", "ucweb", "baidubrowser", "qqbrowser",
68 "dolfin", "crmo", "fennec", "iemobile", "webos", "blackberry", "bb10",
69 "playbook", "palm", "nokia"
70 ];
71
72 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
79pub fn is_mobile_user_agent(user_agent: &str) -> bool {
81 MOBILE_MATCHER.find(user_agent).is_some()
82}
83
84pub 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
91pub 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
146pub fn generate_hide_plugins() -> String {
148 format!(
149 "{}{}",
150 crate::spoofs::NAVIGATOR_SCRIPT,
151 crate::spoofs::PLUGIN_AND_MIMETYPE_SPOOF
152 )
153}
154
155pub fn wrap_eval_script(source: &str) -> String {
157 format!(r#"(()=>{{{}}})();"#, source)
158}
159
160#[derive(Debug, Default, Clone, Copy, PartialEq)]
162#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
163pub enum Fingerprint {
164 Basic,
166 NativeGPU,
169 #[default]
171 None,
172}
173
174impl Fingerprint {
175 pub fn valid(&self) -> bool {
177 match &self {
178 Self::Basic | Self::NativeGPU => true,
179 _ => false,
180 }
181 }
182}
183#[derive(Default, Debug, Clone, Copy, PartialEq)]
185#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
186pub struct EmulationConfiguration {
187 pub tier: configs::Tier,
189 pub dismiss_dialogs: bool,
191 pub fingerprint: Fingerprint,
193 pub agent_os: AgentOs,
195 pub firefox_agent: bool,
197 pub user_agent_data: Option<bool>,
199}
200
201pub 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
220impl EmulationConfiguration {
222 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
241fn join_scripts<I: IntoIterator<Item = impl AsRef<str>>>(parts: I) -> String {
243 let mut script = String::with_capacity(4096);
245 for part in parts {
246 script.push_str(part.as_ref());
247 }
248 script
249}
250
251pub 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 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}