Skip to main content

rustenium_identity/script/
mod.rs

1use crate::identity::{Browser, Identity, Os};
2
3const PROPERTY_MODIFIER_JS: &str = include_str!("../../js/property_modifier.js");
4const MAIN_WORLD_JS: &str = include_str!("../../js/main_world.js");
5const BROWSER_CHROME_JS: &str = include_str!("../../js/browser_chrome.js");
6const BROWSER_SAFARI_JS: &str = include_str!("../../js/browser_safari.js");
7const BROWSER_EDGE_JS: &str = include_str!("../../js/browser_edge.js");
8
9/// Build the full stealth bootstrap JS string with identity values substituted.
10pub fn build_stealth_script(identity: &Identity) -> String {
11    let lang0 = identity
12        .language
13        .first()
14        .map(|s| s.as_str())
15        .unwrap_or("en-US");
16    let languages_json =
17        serde_json::to_string(&identity.language).unwrap_or_else(|_| r#"["en-US"]"#.into());
18    let max_touch = if identity.has_touch { 5 } else { 0 };
19    let avail_height = identity.screen.logical_height.saturating_sub(40);
20    let charging = !identity.has_battery || identity.has_mouse;
21    let charging_time = if identity.has_battery { "Infinity" } else { "0" };
22    let discharging_time = if identity.has_battery { "7200" } else { "Infinity" };
23    let battery_percentage: u8 = rand::random_range(20..=100);
24
25    let history_block = match identity.history_count {
26        Some(count) => format!(
27            r#"(function() {{
28  var historyCount = {};
29  for (var n = 0; n < historyCount; ++n) {{
30    if (window.history.length >= historyCount) break;
31    window.history.pushState(null, '');
32  }}
33}})()"#,
34            count
35        ),
36        None => String::new(),
37    };
38
39    // On iOS all browsers use WebKit — use Safari block regardless of browser enum
40    let is_ios = matches!(identity.os, Os::Ios);
41    let browser_block = if is_ios {
42        BROWSER_SAFARI_JS.to_string()
43    } else {
44        match identity.browser {
45            Browser::Chrome => BROWSER_CHROME_JS.to_string(),
46            Browser::Safari => BROWSER_SAFARI_JS.to_string(),
47            Browser::Edge => BROWSER_EDGE_JS.to_string(),
48        }
49    };
50
51    let script = MAIN_WORLD_JS
52        .replace("{{NAVIGATOR_PLATFORM}}", identity.platform.navigator_platform.as_str())
53        .replace("{{HARDWARE_CONCURRENCY}}", &identity.hardware_concurrency.to_string())
54        .replace("{{MEMORY}}", &identity.memory.to_string())
55        .replace("{{LANGUAGES_JSON}}", &languages_json)
56        .replace("{{LANGUAGE_0}}", lang0)
57        .replace("{{MAX_TOUCH_POINTS}}", &max_touch.to_string())
58        .replace("{{LOGICAL_WIDTH}}", &identity.screen.logical_width.to_string())
59        .replace("{{AVAIL_HEIGHT}}", &avail_height.to_string())
60        .replace("{{WEBGL_VENDOR}}", &escape_js(&identity.gpu.webgl_vendor))
61        .replace("{{WEBGL_RENDERER}}", &escape_js(&identity.gpu.webgl_renderer))
62        .replace("{{HISTORY_BLOCK}}", &history_block)
63        .replace("{{CHARGING}}", &charging.to_string())
64        .replace("{{CHARGING_TIME}}", charging_time)
65        .replace("{{DISCHARGING_TIME}}", discharging_time)
66        .replace("{{BATTERY_PERCENTAGE}}", &battery_percentage.to_string())
67        .replace("{{BROWSER_BLOCK}}", &browser_block);
68
69    format!("{}\n{}", PROPERTY_MODIFIER_JS, script)
70}
71
72fn escape_js(s: &str) -> String {
73    s.replace('\\', "\\\\")
74        .replace('"', "\\\"")
75        .replace('\n', "\\n")
76}