use ::std::cell::RefCell;
use ::std::ffi::CStr;
use ::std::marker::PhantomData;
use ::std::ptr;
use ::std::sync::OnceLock;
use dashmap::DashMap;
use mozjs::jsapi::*;
use mozjs::jsval::{
BooleanValue, DoubleValue, Int32Value, JSVal, ObjectValue, StringValue, UndefinedValue,
};
use mozjs::rooted;
use crate::hooks::StealthHooks;
use crate::StealthProfile;
#[derive(Clone)]
struct RealmProfile {
webdriver: bool,
ua: String,
platform: String,
language: String,
languages: Vec<String>,
hwc: u32,
touch: u32,
vendor: String,
device_memory: f64,
screen_w: u32,
screen_h: u32,
avail_w: u32,
avail_h: u32,
color_depth: u32,
dpr: f64,
webgl_vendor: String,
webgl_renderer: String,
webgl_extensions: Vec<String>,
canvas_seed: u64,
canvas_amplitude: f64,
audio_seed: u64,
audio_amplitude: f64,
font_seed: u64,
font_extra_count: u32,
font_hidden_fonts: Vec<String>,
battery_charging: bool,
battery_level: f64,
battery_charging_time: f64,
battery_discharging_time: f64,
webrtc_mode: u8,
timing_precision_us: u64,
clientrects_delta: f64,
clientrects_seed: u64,
screen_display_w: u32,
screen_display_h: u32,
screen_display_cd: u32,
screen_display_dpr: f64,
plugin_count: u32,
plugin_names: Vec<String>,
mime_types: Vec<String>,
speech_enabled: bool,
speech_voices: Vec<String>,
media_devices_enabled: bool,
media_devices_audio_in: u32,
media_devices_video_in: u32,
media_devices_audio_out: u32,
permissions_enabled: bool,
permissions_states: Vec<(String, String)>,
webgl_context_enabled: bool,
webgl_context_antialias: bool,
webgl_context_depth: bool,
webgl_context_stencil: bool,
webgl_context_alpha: bool,
webgl_context_premultiplied_alpha: bool,
webgl_context_preserve_drawing_buffer: bool,
webgl_context_power_preference: String,
webgl_context_fail_if_major_performance_caveat: bool,
connection_enabled: bool,
connection_effective_type: String,
connection_downlink: f64,
connection_rtt: u32,
connection_save_data: bool,
iframe_enabled: bool,
}
impl RealmProfile {
fn from_profile(p: &StealthProfile) -> Self {
RealmProfile {
webdriver: false,
ua: p.navigator.user_agent.clone(),
platform: p.navigator.platform.clone(),
language: p.navigator.language.clone(),
languages: p.navigator.languages.clone(),
hwc: p.navigator.hardware_concurrency,
touch: p.navigator.max_touch_points,
vendor: p.navigator.vendor.clone(),
device_memory: p.navigator.device_memory,
screen_w: p.screen.width,
screen_h: p.screen.height,
avail_w: p.screen.avail_width,
avail_h: p.screen.avail_height,
color_depth: p.screen.color_depth,
dpr: p.screen.device_pixel_ratio,
webgl_vendor: p.webgl.vendor.clone(),
webgl_renderer: p.webgl.renderer.clone(),
webgl_extensions: p.webgl.extensions.clone(),
canvas_seed: p.canvas.seed(),
canvas_amplitude: p.canvas.noise_amplitude(),
audio_seed: p.audio.seed(),
audio_amplitude: p.audio.noise_amplitude(),
font_seed: p.font.seed,
font_extra_count: p.font.extra_font_count,
font_hidden_fonts: p.font.hidden_fonts.clone(),
battery_charging: p.battery.charging,
battery_level: p.battery.level,
battery_charging_time: p.battery.charging_time,
battery_discharging_time: p.battery.discharging_time,
webrtc_mode: match p.webrtc_mode {
crate::WebRtcMode::Default => 0,
crate::WebRtcMode::Strict => 1,
crate::WebRtcMode::None => 2,
},
timing_precision_us: p.timing.precision_us,
clientrects_delta: p.clientrects.noise_delta,
clientrects_seed: p.clientrects.seed,
screen_display_w: p.screen_display.width,
screen_display_h: p.screen_display.height,
screen_display_cd: p.screen_display.color_depth,
screen_display_dpr: p.screen_display.device_pixel_ratio,
plugin_count: p.plugin.plugin_count,
plugin_names: p.plugin.plugins.clone(),
mime_types: p.plugin.mime_types.clone(),
speech_enabled: p.speech.enabled,
speech_voices: p.speech.voices.clone(),
media_devices_enabled: p.media_devices.enabled,
media_devices_audio_in: p.media_devices.audio_input_count,
media_devices_video_in: p.media_devices.video_input_count,
media_devices_audio_out: p.media_devices.audio_output_count,
permissions_enabled: p.permissions.enabled,
permissions_states: p.permissions.states.clone(),
webgl_context_enabled: p.webgl_context.enabled,
webgl_context_antialias: p.webgl_context.antialias,
webgl_context_depth: p.webgl_context.depth,
webgl_context_stencil: p.webgl_context.stencil,
webgl_context_alpha: p.webgl_context.alpha,
webgl_context_premultiplied_alpha: p.webgl_context.premultiplied_alpha,
webgl_context_preserve_drawing_buffer: p.webgl_context.preserve_drawing_buffer,
webgl_context_power_preference: p.webgl_context.power_preference.clone(),
webgl_context_fail_if_major_performance_caveat: p
.webgl_context
.fail_if_major_performance_caveat,
connection_enabled: p.connection.enabled,
connection_effective_type: p.connection.effective_type.clone(),
connection_downlink: p.connection.downlink,
connection_rtt: p.connection.rtt,
connection_save_data: p.connection.save_data,
iframe_enabled: p.iframe.enabled,
}
}
fn build_hooks_js(&self) -> String {
use crate::canvas::CanvasNoise;
use crate::navigator::{NavigatorProfile, ScreenProfile};
use crate::profile::{
BatteryConfig, ClientRectsConfig, ConnectionConfig, FontConfig, IframeConfig,
MediaDevicesConfig, PermissionsConfig, PluginConfig, ScreenDisplayConfig, SpeechConfig,
TimingConfig, WebGLContextConfig, WebRtcMode,
};
use crate::webgl_audio::{AudioProfile, WebGLProfile};
let canvas = CanvasNoise::new(self.canvas_seed);
let audio = AudioProfile::new(self.audio_seed);
let navigator = NavigatorProfile {
user_agent: self.ua.clone(),
platform: self.platform.clone(),
language: self.language.clone(),
languages: self.languages.clone(),
hardware_concurrency: self.hwc,
max_touch_points: self.touch,
vendor: self.vendor.clone(),
app_version: String::new(),
oscpu: None,
build_id: None,
product_sub: String::new(),
device_memory: self.device_memory,
};
let screen = ScreenProfile {
width: self.screen_w,
height: self.screen_h,
avail_width: self.avail_w,
avail_height: self.avail_h,
color_depth: self.color_depth,
pixel_depth: self.color_depth,
device_pixel_ratio: self.dpr,
};
let webgl = WebGLProfile {
vendor: self.webgl_vendor.clone(),
renderer: self.webgl_renderer.clone(),
extensions: self.webgl_extensions.clone(),
max_texture_size: 16384,
max_renderbuffer_size: 16384,
max_viewport_dims: [16384, 16384],
};
let font = FontConfig {
seed: self.font_seed,
extra_font_count: self.font_extra_count,
hidden_fonts: self.font_hidden_fonts.clone(),
};
let battery = BatteryConfig {
charging: self.battery_charging,
level: self.battery_level,
charging_time: self.battery_charging_time,
discharging_time: self.battery_discharging_time,
};
let webrtc_mode = match self.webrtc_mode {
0 => WebRtcMode::Default,
1 => WebRtcMode::Strict,
_ => WebRtcMode::None,
};
let timing = TimingConfig {
precision_us: self.timing_precision_us,
};
let clientrects = ClientRectsConfig {
noise_delta: self.clientrects_delta,
seed: self.clientrects_seed,
};
let screen_display = ScreenDisplayConfig {
width: self.screen_display_w,
height: self.screen_display_h,
color_depth: self.screen_display_cd,
device_pixel_ratio: self.screen_display_dpr,
};
let plugin = PluginConfig {
plugin_count: self.plugin_count,
plugins: self.plugin_names.clone(),
mime_types: self.mime_types.clone(),
};
let speech = SpeechConfig {
enabled: self.speech_enabled,
voices: self.speech_voices.clone(),
};
let media_devices = MediaDevicesConfig {
enabled: self.media_devices_enabled,
audio_input_count: self.media_devices_audio_in,
video_input_count: self.media_devices_video_in,
audio_output_count: self.media_devices_audio_out,
};
let permissions = PermissionsConfig {
enabled: self.permissions_enabled,
states: self.permissions_states.clone(),
};
let webgl_context = WebGLContextConfig {
enabled: self.webgl_context_enabled,
antialias: self.webgl_context_antialias,
depth: self.webgl_context_depth,
stencil: self.webgl_context_stencil,
alpha: self.webgl_context_alpha,
premultiplied_alpha: self.webgl_context_premultiplied_alpha,
preserve_drawing_buffer: self.webgl_context_preserve_drawing_buffer,
power_preference: self.webgl_context_power_preference.clone(),
fail_if_major_performance_caveat: self.webgl_context_fail_if_major_performance_caveat,
};
let connection = ConnectionConfig {
enabled: self.connection_enabled,
effective_type: self.connection_effective_type.clone(),
downlink: self.connection_downlink,
rtt: self.connection_rtt,
save_data: self.connection_save_data,
};
let iframe = IframeConfig {
enabled: self.iframe_enabled,
};
let hooks = StealthHooks::from_profile(
&canvas,
&audio,
&navigator,
&screen,
&webgl,
&font,
&battery,
webrtc_mode,
&timing,
&clientrects,
&screen_display,
&plugin,
&speech,
&media_devices,
&permissions,
&webgl_context,
&connection,
&iframe,
);
hooks.combined_js()
}
}
static REALM_PROFILES: OnceLock<DashMap<usize, ::std::sync::Arc<RealmProfile>>> = OnceLock::new();
fn realm_profiles() -> &'static DashMap<usize, ::std::sync::Arc<RealmProfile>> {
REALM_PROFILES.get_or_init(DashMap::new)
}
pub fn set_profile_for_global(global_addr: usize, profile: &StealthProfile) {
let rp = ::std::sync::Arc::new(RealmProfile::from_profile(profile));
realm_profiles().insert(global_addr, rp);
}
pub fn register_global_alias(page_global_addr: usize, alias_global_addr: usize) {
if page_global_addr == 0 || alias_global_addr == 0 {
return;
}
if let Some(rp) = realm_profiles().get(&page_global_addr) {
let rp_clone = ::std::sync::Arc::clone(&rp);
drop(rp);
realm_profiles().insert(alias_global_addr, rp_clone);
}
}
pub fn remove_profile_for_global(global_addr: usize) {
realm_profiles().remove(&global_addr);
}
#[doc(hidden)]
pub fn canvas_seed_for_test(global_addr: usize) -> Option<u64> {
realm_profiles().get(&global_addr).map(|rp| rp.canvas_seed)
}
pub fn clear_all_realm_profiles() {
realm_profiles().clear();
}
unsafe fn current_realm_profile(raw_cx: *mut JSContext) -> Option<::std::sync::Arc<RealmProfile>> {
let global = CurrentGlobalOrNull(raw_cx);
if global.is_null() {
return None;
}
let key = global as usize;
if let Some(rp) = realm_profiles().get(&key) {
return Some(::std::sync::Arc::clone(&rp));
}
None
}
thread_local! {
static TL_WEBDRIVER: RefCell<bool> = RefCell::new(false);
static TL_UA: RefCell<String> = RefCell::new(String::new());
static TL_PLATFORM: RefCell<String> = RefCell::new(String::new());
static TL_LANGUAGE: RefCell<String> = RefCell::new(String::new());
static TL_LANGUAGES: RefCell<Vec<String>> = RefCell::new(vec!["en-US".into(), "en".into()]);
static TL_HWC: RefCell<u32> = RefCell::new(8);
static TL_TOUCH: RefCell<u32> = RefCell::new(0);
static TL_VENDOR: RefCell<String> = RefCell::new(String::new());
static TL_DEVICE_MEMORY: RefCell<f64> = RefCell::new(8.0);
static TL_SCREEN_W: RefCell<u32> = RefCell::new(1920);
static TL_SCREEN_H: RefCell<u32> = RefCell::new(1080);
static TL_AVAIL_W: RefCell<u32> = RefCell::new(1920);
static TL_AVAIL_H: RefCell<u32> = RefCell::new(1040);
static TL_COLOR_DEPTH: RefCell<u32> = RefCell::new(24);
static TL_DPR: RefCell<f64> = RefCell::new(1.0);
static TL_WEBGL_VENDOR: RefCell<String> = RefCell::new(String::new());
static TL_WEBGL_RENDERER: RefCell<String> = RefCell::new(String::new());
static TL_WEBGL_EXTENSIONS: RefCell<Vec<String>> = RefCell::new(vec![]);
static TL_CANVAS_SEED: RefCell<u64> = RefCell::new(42);
static TL_CANVAS_AMPLITUDE: RefCell<f64> = RefCell::new(0.001);
static TL_AUDIO_SEED: RefCell<u64> = RefCell::new(42);
static TL_AUDIO_AMPLITUDE: RefCell<f64> = RefCell::new(1e-7);
static TL_FONT_SEED: RefCell<u64> = RefCell::new(42);
static TL_FONT_EXTRA_COUNT: RefCell<u32> = RefCell::new(0);
static TL_FONT_HIDDEN_FONTS: RefCell<Vec<String>> = RefCell::new(vec![]);
static TL_BATTERY_CHARGING: RefCell<bool> = RefCell::new(true);
static TL_BATTERY_LEVEL: RefCell<f64> = RefCell::new(1.0);
static TL_BATTERY_CHARGING_TIME: RefCell<f64> = RefCell::new(0.0);
static TL_BATTERY_DISCHARGING_TIME: RefCell<f64> = RefCell::new(f64::INFINITY);
static TL_WEBRTC_MODE: RefCell<u8> = RefCell::new(0);
static TL_TIMING_PRECISION_US: RefCell<u64> = RefCell::new(100);
static TL_CLIENTRECTS_DELTA: RefCell<f64> = RefCell::new(0.5);
static TL_CLIENTRECTS_SEED: RefCell<u64> = RefCell::new(42);
static TL_SCREEN_DISPLAY_W: RefCell<u32> = RefCell::new(1920);
static TL_SCREEN_DISPLAY_H: RefCell<u32> = RefCell::new(1080);
static TL_SCREEN_DISPLAY_CD: RefCell<u32> = RefCell::new(24);
static TL_SCREEN_DISPLAY_DPR: RefCell<f64> = RefCell::new(1.0);
static TL_PLUGIN_COUNT: RefCell<u32> = RefCell::new(5);
static TL_PLUGIN_NAMES: RefCell<Vec<String>> = RefCell::new(vec![]);
static TL_MIME_TYPES: RefCell<Vec<String>> = RefCell::new(vec![]);
static TL_SPEECH_ENABLED: RefCell<bool> = RefCell::new(true);
static TL_SPEECH_VOICES: RefCell<Vec<String>> = RefCell::new(vec![]);
static TL_MEDIA_DEVICES_ENABLED: RefCell<bool> = RefCell::new(true);
static TL_MEDIA_DEVICES_AUDIO_IN: RefCell<u32> = RefCell::new(1);
static TL_MEDIA_DEVICES_VIDEO_IN: RefCell<u32> = RefCell::new(1);
static TL_MEDIA_DEVICES_AUDIO_OUT: RefCell<u32> = RefCell::new(1);
static TL_PERMISSIONS_ENABLED: RefCell<bool> = RefCell::new(true);
static TL_PERMISSIONS_STATES: RefCell<Vec<(String, String)>> = RefCell::new(vec![]);
static TL_WEBGL_CONTEXT_ENABLED: RefCell<bool> = RefCell::new(true);
static TL_WEBGL_CONTEXT_ANTIALIAS: RefCell<bool> = RefCell::new(true);
static TL_WEBGL_CONTEXT_DEPTH: RefCell<bool> = RefCell::new(true);
static TL_WEBGL_CONTEXT_STENCIL: RefCell<bool> = RefCell::new(false);
static TL_WEBGL_CONTEXT_ALPHA: RefCell<bool> = RefCell::new(true);
static TL_WEBGL_CONTEXT_PREMULTIPLIED_ALPHA: RefCell<bool> = RefCell::new(true);
static TL_WEBGL_CONTEXT_PRESERVE_DRAWING_BUFFER: RefCell<bool> = RefCell::new(false);
static TL_WEBGL_CONTEXT_POWER_PREFERENCE: RefCell<String> = RefCell::new(String::new());
static TL_WEBGL_CONTEXT_FAIL_IF_MAJOR_PERFORMANCE_CAVEAT: RefCell<bool> = RefCell::new(false);
static TL_CONNECTION_ENABLED: RefCell<bool> = RefCell::new(true);
static TL_CONNECTION_EFFECTIVE_TYPE: RefCell<String> = RefCell::new(String::new());
static TL_CONNECTION_DOWNLINK: RefCell<f64> = RefCell::new(10.0);
static TL_CONNECTION_RTT: RefCell<u32> = RefCell::new(50);
static TL_CONNECTION_SAVE_DATA: RefCell<bool> = RefCell::new(false);
static TL_IFRAME_ENABLED: RefCell<bool> = RefCell::new(true);
}
pub fn set_profile(profile: &StealthProfile) {
TL_WEBDRIVER.with(|v| *v.borrow_mut() = false);
TL_UA.with(|v| *v.borrow_mut() = profile.navigator.user_agent.clone());
TL_PLATFORM.with(|v| *v.borrow_mut() = profile.navigator.platform.clone());
TL_LANGUAGE.with(|v| *v.borrow_mut() = profile.navigator.language.clone());
TL_LANGUAGES.with(|v| *v.borrow_mut() = profile.navigator.languages.clone());
TL_HWC.with(|v| *v.borrow_mut() = profile.navigator.hardware_concurrency);
TL_TOUCH.with(|v| *v.borrow_mut() = profile.navigator.max_touch_points);
TL_VENDOR.with(|v| *v.borrow_mut() = profile.navigator.vendor.clone());
TL_DEVICE_MEMORY.with(|v| *v.borrow_mut() = profile.navigator.device_memory);
TL_SCREEN_W.with(|v| *v.borrow_mut() = profile.screen.width);
TL_SCREEN_H.with(|v| *v.borrow_mut() = profile.screen.height);
TL_AVAIL_W.with(|v| *v.borrow_mut() = profile.screen.avail_width);
TL_AVAIL_H.with(|v| *v.borrow_mut() = profile.screen.avail_height);
TL_COLOR_DEPTH.with(|v| *v.borrow_mut() = profile.screen.color_depth);
TL_DPR.with(|v| *v.borrow_mut() = profile.screen.device_pixel_ratio);
TL_WEBGL_VENDOR.with(|v| *v.borrow_mut() = profile.webgl.vendor.clone());
TL_WEBGL_RENDERER.with(|v| *v.borrow_mut() = profile.webgl.renderer.clone());
TL_WEBGL_EXTENSIONS.with(|v| *v.borrow_mut() = profile.webgl.extensions.clone());
TL_CANVAS_SEED.with(|v| *v.borrow_mut() = profile.canvas.seed());
TL_CANVAS_AMPLITUDE.with(|v| *v.borrow_mut() = profile.canvas.noise_amplitude());
TL_AUDIO_SEED.with(|v| *v.borrow_mut() = profile.audio.seed());
TL_AUDIO_AMPLITUDE.with(|v| *v.borrow_mut() = profile.audio.noise_amplitude());
TL_FONT_SEED.with(|v| *v.borrow_mut() = profile.font.seed);
TL_FONT_EXTRA_COUNT.with(|v| *v.borrow_mut() = profile.font.extra_font_count);
TL_FONT_HIDDEN_FONTS.with(|v| *v.borrow_mut() = profile.font.hidden_fonts.clone());
TL_BATTERY_CHARGING.with(|v| *v.borrow_mut() = profile.battery.charging);
TL_BATTERY_LEVEL.with(|v| *v.borrow_mut() = profile.battery.level);
TL_BATTERY_CHARGING_TIME.with(|v| *v.borrow_mut() = profile.battery.charging_time);
TL_BATTERY_DISCHARGING_TIME.with(|v| *v.borrow_mut() = profile.battery.discharging_time);
TL_WEBRTC_MODE.with(|v| {
*v.borrow_mut() = match profile.webrtc_mode {
crate::WebRtcMode::Default => 0,
crate::WebRtcMode::Strict => 1,
crate::WebRtcMode::None => 2,
}
});
TL_TIMING_PRECISION_US.with(|v| *v.borrow_mut() = profile.timing.precision_us);
TL_CLIENTRECTS_DELTA.with(|v| *v.borrow_mut() = profile.clientrects.noise_delta);
TL_CLIENTRECTS_SEED.with(|v| *v.borrow_mut() = profile.clientrects.seed);
TL_SCREEN_DISPLAY_W.with(|v| *v.borrow_mut() = profile.screen_display.width);
TL_SCREEN_DISPLAY_H.with(|v| *v.borrow_mut() = profile.screen_display.height);
TL_SCREEN_DISPLAY_CD.with(|v| *v.borrow_mut() = profile.screen_display.color_depth);
TL_SCREEN_DISPLAY_DPR.with(|v| *v.borrow_mut() = profile.screen_display.device_pixel_ratio);
TL_PLUGIN_COUNT.with(|v| *v.borrow_mut() = profile.plugin.plugin_count);
TL_PLUGIN_NAMES.with(|v| *v.borrow_mut() = profile.plugin.plugins.clone());
TL_MIME_TYPES.with(|v| *v.borrow_mut() = profile.plugin.mime_types.clone());
TL_SPEECH_ENABLED.with(|v| *v.borrow_mut() = profile.speech.enabled);
TL_SPEECH_VOICES.with(|v| *v.borrow_mut() = profile.speech.voices.clone());
TL_MEDIA_DEVICES_ENABLED.with(|v| *v.borrow_mut() = profile.media_devices.enabled);
TL_MEDIA_DEVICES_AUDIO_IN.with(|v| *v.borrow_mut() = profile.media_devices.audio_input_count);
TL_MEDIA_DEVICES_VIDEO_IN.with(|v| *v.borrow_mut() = profile.media_devices.video_input_count);
TL_MEDIA_DEVICES_AUDIO_OUT.with(|v| *v.borrow_mut() = profile.media_devices.audio_output_count);
TL_PERMISSIONS_ENABLED.with(|v| *v.borrow_mut() = profile.permissions.enabled);
TL_PERMISSIONS_STATES.with(|v| *v.borrow_mut() = profile.permissions.states.clone());
TL_WEBGL_CONTEXT_ENABLED.with(|v| *v.borrow_mut() = profile.webgl_context.enabled);
TL_WEBGL_CONTEXT_ANTIALIAS.with(|v| *v.borrow_mut() = profile.webgl_context.antialias);
TL_WEBGL_CONTEXT_DEPTH.with(|v| *v.borrow_mut() = profile.webgl_context.depth);
TL_WEBGL_CONTEXT_STENCIL.with(|v| *v.borrow_mut() = profile.webgl_context.stencil);
TL_WEBGL_CONTEXT_ALPHA.with(|v| *v.borrow_mut() = profile.webgl_context.alpha);
TL_WEBGL_CONTEXT_PREMULTIPLIED_ALPHA
.with(|v| *v.borrow_mut() = profile.webgl_context.premultiplied_alpha);
TL_WEBGL_CONTEXT_PRESERVE_DRAWING_BUFFER
.with(|v| *v.borrow_mut() = profile.webgl_context.preserve_drawing_buffer);
TL_WEBGL_CONTEXT_POWER_PREFERENCE
.with(|v| *v.borrow_mut() = profile.webgl_context.power_preference.clone());
TL_WEBGL_CONTEXT_FAIL_IF_MAJOR_PERFORMANCE_CAVEAT
.with(|v| *v.borrow_mut() = profile.webgl_context.fail_if_major_performance_caveat);
TL_CONNECTION_ENABLED.with(|v| *v.borrow_mut() = profile.connection.enabled);
TL_CONNECTION_EFFECTIVE_TYPE
.with(|v| *v.borrow_mut() = profile.connection.effective_type.clone());
TL_CONNECTION_DOWNLINK.with(|v| *v.borrow_mut() = profile.connection.downlink);
TL_CONNECTION_RTT.with(|v| *v.borrow_mut() = profile.connection.rtt);
TL_CONNECTION_SAVE_DATA.with(|v| *v.borrow_mut() = profile.connection.save_data);
TL_IFRAME_ENABLED.with(|v| *v.borrow_mut() = profile.iframe.enabled);
}
unsafe fn read_realm_field<T, F: FnOnce(&RealmProfile) -> T>(
raw_cx: *mut JSContext,
f: F,
) -> Option<T> {
current_realm_profile(raw_cx).map(|rp| f(&rp))
}
fn build_hooks_js_from_thread_local() -> String {
use crate::canvas::CanvasNoise;
use crate::navigator::{NavigatorProfile, ScreenProfile};
use crate::profile::{
BatteryConfig, ClientRectsConfig, ConnectionConfig, FontConfig, IframeConfig,
MediaDevicesConfig, PermissionsConfig, PluginConfig, ScreenDisplayConfig, SpeechConfig,
TimingConfig, WebGLContextConfig, WebRtcMode,
};
use crate::webgl_audio::{AudioProfile, WebGLProfile};
let canvas = CanvasNoise::new(TL_CANVAS_SEED.with(|v| *v.borrow()));
let audio = AudioProfile::new(TL_AUDIO_SEED.with(|v| *v.borrow()));
let navigator = NavigatorProfile {
user_agent: TL_UA.with(|v| v.borrow().clone()),
platform: TL_PLATFORM.with(|v| v.borrow().clone()),
language: TL_LANGUAGE.with(|v| v.borrow().clone()),
languages: TL_LANGUAGES.with(|v| v.borrow().clone()),
hardware_concurrency: TL_HWC.with(|v| *v.borrow()),
max_touch_points: TL_TOUCH.with(|v| *v.borrow()),
vendor: TL_VENDOR.with(|v| v.borrow().clone()),
app_version: String::new(),
oscpu: None,
build_id: None,
product_sub: String::new(),
device_memory: TL_DEVICE_MEMORY.with(|v| *v.borrow()),
};
let screen = ScreenProfile {
width: TL_SCREEN_W.with(|v| *v.borrow()),
height: TL_SCREEN_H.with(|v| *v.borrow()),
avail_width: TL_AVAIL_W.with(|v| *v.borrow()),
avail_height: TL_AVAIL_H.with(|v| *v.borrow()),
color_depth: TL_COLOR_DEPTH.with(|v| *v.borrow()),
pixel_depth: TL_COLOR_DEPTH.with(|v| *v.borrow()),
device_pixel_ratio: TL_DPR.with(|v| *v.borrow()),
};
let webgl = WebGLProfile {
vendor: TL_WEBGL_VENDOR.with(|v| v.borrow().clone()),
renderer: TL_WEBGL_RENDERER.with(|v| v.borrow().clone()),
extensions: TL_WEBGL_EXTENSIONS.with(|v| v.borrow().clone()),
max_texture_size: 16384,
max_renderbuffer_size: 16384,
max_viewport_dims: [16384, 16384],
};
let font = FontConfig {
seed: TL_FONT_SEED.with(|v| *v.borrow()),
extra_font_count: TL_FONT_EXTRA_COUNT.with(|v| *v.borrow()),
hidden_fonts: TL_FONT_HIDDEN_FONTS.with(|v| v.borrow().clone()),
};
let battery = BatteryConfig {
charging: TL_BATTERY_CHARGING.with(|v| *v.borrow()),
level: TL_BATTERY_LEVEL.with(|v| *v.borrow()),
charging_time: TL_BATTERY_CHARGING_TIME.with(|v| *v.borrow()),
discharging_time: TL_BATTERY_DISCHARGING_TIME.with(|v| *v.borrow()),
};
let webrtc_mode = match TL_WEBRTC_MODE.with(|v| *v.borrow()) {
0 => WebRtcMode::Default,
1 => WebRtcMode::Strict,
_ => WebRtcMode::None,
};
let timing = TimingConfig {
precision_us: TL_TIMING_PRECISION_US.with(|v| *v.borrow()),
};
let clientrects = ClientRectsConfig {
noise_delta: TL_CLIENTRECTS_DELTA.with(|v| *v.borrow()),
seed: TL_CLIENTRECTS_SEED.with(|v| *v.borrow()),
};
let screen_display = ScreenDisplayConfig {
width: TL_SCREEN_DISPLAY_W.with(|v| *v.borrow()),
height: TL_SCREEN_DISPLAY_H.with(|v| *v.borrow()),
color_depth: TL_SCREEN_DISPLAY_CD.with(|v| *v.borrow()),
device_pixel_ratio: TL_SCREEN_DISPLAY_DPR.with(|v| *v.borrow()),
};
let plugin = PluginConfig {
plugin_count: TL_PLUGIN_COUNT.with(|v| *v.borrow()),
plugins: TL_PLUGIN_NAMES.with(|v| v.borrow().clone()),
mime_types: TL_MIME_TYPES.with(|v| v.borrow().clone()),
};
let speech = SpeechConfig {
enabled: TL_SPEECH_ENABLED.with(|v| *v.borrow()),
voices: TL_SPEECH_VOICES.with(|v| v.borrow().clone()),
};
let media_devices = MediaDevicesConfig {
enabled: TL_MEDIA_DEVICES_ENABLED.with(|v| *v.borrow()),
audio_input_count: TL_MEDIA_DEVICES_AUDIO_IN.with(|v| *v.borrow()),
video_input_count: TL_MEDIA_DEVICES_VIDEO_IN.with(|v| *v.borrow()),
audio_output_count: TL_MEDIA_DEVICES_AUDIO_OUT.with(|v| *v.borrow()),
};
let permissions = PermissionsConfig {
enabled: TL_PERMISSIONS_ENABLED.with(|v| *v.borrow()),
states: TL_PERMISSIONS_STATES.with(|v| v.borrow().clone()),
};
let webgl_context = WebGLContextConfig {
enabled: TL_WEBGL_CONTEXT_ENABLED.with(|v| *v.borrow()),
antialias: TL_WEBGL_CONTEXT_ANTIALIAS.with(|v| *v.borrow()),
depth: TL_WEBGL_CONTEXT_DEPTH.with(|v| *v.borrow()),
stencil: TL_WEBGL_CONTEXT_STENCIL.with(|v| *v.borrow()),
alpha: TL_WEBGL_CONTEXT_ALPHA.with(|v| *v.borrow()),
premultiplied_alpha: TL_WEBGL_CONTEXT_PREMULTIPLIED_ALPHA.with(|v| *v.borrow()),
preserve_drawing_buffer: TL_WEBGL_CONTEXT_PRESERVE_DRAWING_BUFFER.with(|v| *v.borrow()),
power_preference: TL_WEBGL_CONTEXT_POWER_PREFERENCE.with(|v| v.borrow().clone()),
fail_if_major_performance_caveat: TL_WEBGL_CONTEXT_FAIL_IF_MAJOR_PERFORMANCE_CAVEAT
.with(|v| *v.borrow()),
};
let connection = ConnectionConfig {
enabled: TL_CONNECTION_ENABLED.with(|v| *v.borrow()),
effective_type: TL_CONNECTION_EFFECTIVE_TYPE.with(|v| v.borrow().clone()),
downlink: TL_CONNECTION_DOWNLINK.with(|v| *v.borrow()),
rtt: TL_CONNECTION_RTT.with(|v| *v.borrow()),
save_data: TL_CONNECTION_SAVE_DATA.with(|v| *v.borrow()),
};
let iframe = IframeConfig {
enabled: TL_IFRAME_ENABLED.with(|v| *v.borrow()),
};
let hooks = StealthHooks::from_profile(
&canvas,
&audio,
&navigator,
&screen,
&webgl,
&font,
&battery,
webrtc_mode,
&timing,
&clientrects,
&screen_display,
&plugin,
&speech,
&media_devices,
&permissions,
&webgl_context,
&connection,
&iframe,
);
hooks.combined_js()
}
pub fn canvas_seed() -> u64 {
TL_CANVAS_SEED.with(|v| *v.borrow())
}
pub fn canvas_amplitude() -> f64 {
TL_CANVAS_AMPLITUDE.with(|v| *v.borrow())
}
pub fn is_profile_set() -> bool {
TL_UA.with(|v| !v.borrow().is_empty())
}
pub fn ensure_default_profile() {
if !is_profile_set() {
set_profile(&StealthProfile::firefox_default());
}
}
macro_rules! make_bool_getter {
($name:ident, $tl:path, $field:ident) => {
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn $name(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
let val =
read_realm_field(cx, |rp| rp.$field).unwrap_or_else(|| $tl.with(|v| *v.borrow()));
args.rval().set(BooleanValue(val));
true
}
};
}
macro_rules! make_u32_getter {
($name:ident, $tl:path, $field:ident) => {
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn $name(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
let val =
read_realm_field(cx, |rp| rp.$field).unwrap_or_else(|| $tl.with(|v| *v.borrow()));
args.rval().set(Int32Value(val as i32));
true
}
};
}
macro_rules! make_f64_getter {
($name:ident, $tl:path, $field:ident) => {
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn $name(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
let val =
read_realm_field(cx, |rp| rp.$field).unwrap_or_else(|| $tl.with(|v| *v.borrow()));
args.rval().set(DoubleValue(val));
true
}
};
}
macro_rules! make_string_getter {
($name:ident, $tl:path, $field:ident) => {
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn $name(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
let s: String = read_realm_field(cx, |rp| rp.$field.clone())
.unwrap_or_else(|| $tl.with(|v| v.borrow().clone()));
let c_str = bun_core::ZBox::from_bytes(s.as_bytes());
let js_str = JS_NewStringCopyZ(cx, c_str.as_ptr());
if !js_str.is_null() {
args.rval().set(StringValue(&*js_str));
} else {
args.rval().set(UndefinedValue());
}
true
}
};
}
make_bool_getter!(getter_webdriver, TL_WEBDRIVER, webdriver);
make_string_getter!(getter_ua, TL_UA, ua);
make_string_getter!(getter_platform, TL_PLATFORM, platform);
make_string_getter!(getter_language, TL_LANGUAGE, language);
make_u32_getter!(getter_hwc, TL_HWC, hwc);
make_u32_getter!(getter_touch, TL_TOUCH, touch);
make_string_getter!(getter_vendor, TL_VENDOR, vendor);
make_u32_getter!(getter_screen_w, TL_SCREEN_W, screen_w);
make_u32_getter!(getter_screen_h, TL_SCREEN_H, screen_h);
make_u32_getter!(getter_avail_w, TL_AVAIL_W, avail_w);
make_u32_getter!(getter_avail_h, TL_AVAIL_H, avail_h);
make_u32_getter!(getter_color_depth, TL_COLOR_DEPTH, color_depth);
make_f64_getter!(getter_dpr, TL_DPR, dpr);
make_f64_getter!(getter_device_memory, TL_DEVICE_MEMORY, device_memory);
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn getter_languages(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
let langs: Vec<String> = read_realm_field(cx, |rp| rp.languages.clone())
.unwrap_or_else(|| TL_LANGUAGES.with(|v| v.borrow().clone()));
let obj = JS_NewPlainObject(cx);
if obj.is_null() {
args.rval().set(UndefinedValue());
return true;
}
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
rooted!(&in(wrapped_cx) let obj_root = obj);
for (i, lang) in langs.iter().enumerate() {
let idx_cstr = format!("{}", i);
let c_idx = bun_core::ZBox::from_bytes(idx_cstr.as_bytes());
let c_lang = bun_core::ZBox::from_bytes(lang.as_bytes());
let js_str = JS_NewStringCopyZ(cx, c_lang.as_ptr());
if !js_str.is_null() {
rooted!(&in(wrapped_cx) let str_root = js_str as *mut JSObject);
JS_DefineProperty3(
cx,
obj_root.handle().into(),
c_idx.as_ptr(),
str_root.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
JS_DefineProperty1(
cx,
obj_root.handle().into(),
c"length".as_ptr(),
None,
None,
(JSPROP_READONLY | JSPROP_PERMANENT | JSPROP_ENUMERATE) as u32,
);
args.rval().set(ObjectValue(obj_root.get()));
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn webgl_get_parameter_override(
cx: *mut JSContext,
argc: u32,
vp: *mut JSVal,
) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc == 0 {
args.rval().set(UndefinedValue());
return true;
}
let param = args.get(0);
if param.is_int32() {
let p = param.to_int32();
if p == 0x1F00 {
let s: String = read_realm_field(cx, |rp| rp.webgl_vendor.clone())
.unwrap_or_else(|| TL_WEBGL_VENDOR.with(|v| v.borrow().clone()));
return emit_string_rval(cx, args.rval(), &s);
}
if p == 0x1F01 {
let s: String = read_realm_field(cx, |rp| rp.webgl_renderer.clone())
.unwrap_or_else(|| TL_WEBGL_RENDERER.with(|v| v.borrow().clone()));
return emit_string_rval(cx, args.rval(), &s);
}
}
let this_val = args.thisv();
if !this_val.is_object() {
args.rval().set(UndefinedValue());
return true;
}
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
rooted!(&in(wrapped_cx) let this_root = this_val.to_object());
let mut has: bool = false;
if !has_property_clearing(
cx,
this_root.handle().into(),
c"__originalGetParameter__",
&mut has,
) || !has
{
args.rval().set(UndefinedValue());
return true;
}
let mut fn_val = UndefinedValue();
if !get_property_clearing(cx, this_root.handle().into(), c"__originalGetParameter__", &mut fn_val) {
args.rval().set(UndefinedValue());
return true;
}
if !fn_val.is_object() {
args.rval().set(UndefinedValue());
return true;
}
let param_val: Value = *param.ptr;
match bao_engine::host_fn::call_function(cx, fn_val, this_root.get(), &[param_val]) {
Ok(result) => {
args.rval().set(result.to_jsval(cx));
true
}
Err(_) => {
args.rval().set(UndefinedValue());
true
}
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn emit_string_rval(cx: *mut JSContext, rval: MutableHandleValue, s: &str) -> bool {
let c_str = bun_core::ZBox::from_bytes(s.as_bytes());
let js_str = JS_NewStringCopyZ(cx, c_str.as_ptr());
if !js_str.is_null() {
rval.set(StringValue(&*js_str));
} else {
rval.set(UndefinedValue());
}
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn webgl_get_supported_extensions_override(
cx: *mut JSContext,
_argc: u32,
vp: *mut JSVal,
) -> bool {
let args = CallArgs::from_vp(vp, _argc);
let exts: Vec<String> = read_realm_field(cx, |rp| rp.webgl_extensions.clone())
.unwrap_or_else(|| TL_WEBGL_EXTENSIONS.with(|v| v.borrow().clone()));
let arr = JS_NewPlainObject(cx);
if arr.is_null() {
args.rval().set(UndefinedValue());
return true;
}
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
rooted!(&in(wrapped_cx) let arr_root = arr);
for (i, ext) in exts.iter().enumerate() {
let idx_cstr = format!("{}", i);
let c_idx = bun_core::ZBox::from_bytes(idx_cstr.as_bytes());
let c_ext = bun_core::ZBox::from_bytes(ext.as_bytes());
let js_str = JS_NewStringCopyZ(cx, c_ext.as_ptr());
if !js_str.is_null() {
rooted!(&in(wrapped_cx) let str_root = js_str as *mut JSObject);
JS_DefineProperty3(
cx,
arr_root.handle().into(),
c_idx.as_ptr(),
str_root.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
JS_DefineProperty1(
cx,
arr_root.handle().into(),
c"length".as_ptr(),
None,
None,
(JSPROP_READONLY | JSPROP_PERMANENT | JSPROP_ENUMERATE) as u32,
);
args.rval().set(ObjectValue(arr_root.get()));
true
}
unsafe fn define_permanent_getter(
cx: *mut JSContext,
obj: HandleObject,
name: &str,
getter: JSNative,
) -> bool {
let c_name = bun_core::ZBox::from_bytes(name.as_bytes());
let mut op_result = ObjectOpResult::default();
let deleted = JS_DeleteProperty(cx, obj, c_name.as_ptr(), &mut op_result);
if !deleted || !op_result.ok() {
JS_ClearPendingException(cx);
}
let attrs = (JSPROP_PERMANENT | JSPROP_ENUMERATE) as u32;
let ok = JS_DefineProperty1(cx, obj, c_name.as_ptr(), getter, None, attrs);
if !ok && JS_IsExceptionPending(cx) {
JS_ClearPendingException(cx);
}
ok
}
pub unsafe fn get_property_clearing(cx: *mut JSContext, obj: HandleObject, name: &CStr, out: &mut JSVal) -> bool {
if JS_GetProperty(
cx,
obj,
name.as_ptr(),
MutableHandle::<Value> {
_phantom_0: PhantomData,
ptr: out,
},
) {
true
} else {
JS_ClearPendingException(cx);
*out = UndefinedValue();
false
}
}
pub unsafe fn has_property_clearing(cx: *mut JSContext, obj: HandleObject, name: &CStr, has: &mut bool) -> bool {
if JS_HasProperty(cx, obj, name.as_ptr(), has) {
true
} else {
JS_ClearPendingException(cx);
*has = false;
false
}
}
unsafe fn get_subobject(cx: *mut JSContext, obj: HandleObject, prop: &str) -> *mut JSObject {
let c_prop = bun_core::ZBox::from_bytes(prop.as_bytes());
let mut has: bool = false;
if !JS_HasProperty(cx, obj, c_prop.as_ptr(), &mut has) {
JS_ClearPendingException(cx);
return ptr::null_mut();
}
if !has {
return ptr::null_mut();
}
let mut val = UndefinedValue();
if !get_property_clearing(cx, obj, c_prop.as_cstr(), &mut val) {
return ptr::null_mut();
}
if val.is_object() {
val.to_object()
} else {
ptr::null_mut()
}
}
unsafe fn ensure_subobject(cx: *mut JSContext, obj: HandleObject, prop: &str) -> *mut JSObject {
let existing = get_subobject(cx, obj, prop);
if !existing.is_null() {
return existing;
}
let c_prop = bun_core::ZBox::from_bytes(prop.as_bytes());
let new_obj = JS_NewPlainObject(cx);
if new_obj.is_null() {
return ptr::null_mut();
}
let attrs = (JSPROP_PERMANENT | JSPROP_ENUMERATE) as u32;
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
rooted!(&in(wrapped_cx) let new_obj_root = new_obj);
if !JS_DefineProperty3(
cx,
obj,
c_prop.as_ptr(),
new_obj_root.handle().into(),
attrs,
) {
JS_ClearPendingException(cx);
return ptr::null_mut();
}
new_obj
}
unsafe fn install_webgl_override(cx: *mut JSContext, global: HandleObject) -> bool {
let mut has: bool = false;
if !has_property_clearing(cx, global, c"WebGLRenderingContext", &mut has) || !has {
return true;
}
let mut ctor_val = UndefinedValue();
if !get_property_clearing(cx, global, c"WebGLRenderingContext", &mut ctor_val) {
return true; }
if !ctor_val.is_object() {
return true;
}
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
rooted!(&in(wrapped_cx) let ctor_root = ctor_val.to_object());
let mut proto_val = UndefinedValue();
if !get_property_clearing(cx, ctor_root.handle().into(), c"prototype", &mut proto_val) {
return true; }
if !proto_val.is_object() {
return true;
}
rooted!(&in(wrapped_cx) let proto_root = proto_val.to_object());
let mut orig_gp = UndefinedValue();
let _ = get_property_clearing(cx, proto_root.handle().into(), c"getParameter", &mut orig_gp);
if orig_gp.is_object() {
rooted!(&in(wrapped_cx) let orig_fn_root = orig_gp.to_object());
let save_attrs = (JSPROP_PERMANENT | JSPROP_ENUMERATE) as u32;
JS_DefineProperty3(
cx,
proto_root.handle().into(),
c"__originalGetParameter__".as_ptr(),
orig_fn_root.handle().into(),
save_attrs,
);
}
let fn_obj = JS_NewFunction(
cx,
Some(webgl_get_parameter_override),
1,
0,
c"getParameter".as_ptr(),
);
if fn_obj.is_null() {
return false;
}
rooted!(&in(wrapped_cx) let fn_root = fn_obj as *mut JSObject);
let override_attrs = (JSPROP_PERMANENT | JSPROP_ENUMERATE) as u32;
let gp_ok = JS_DefineProperty3(
cx,
proto_root.handle().into(),
c"getParameter".as_ptr(),
fn_root.handle().into(),
override_attrs,
);
let gse_fn = JS_NewFunction(
cx,
Some(webgl_get_supported_extensions_override),
0,
0,
c"getSupportedExtensions".as_ptr(),
);
if gse_fn.is_null() {
return false;
}
rooted!(&in(wrapped_cx) let gse_fn_root = gse_fn as *mut JSObject);
let gse_ok = JS_DefineProperty3(
cx,
proto_root.handle().into(),
c"getSupportedExtensions".as_ptr(),
gse_fn_root.handle().into(),
override_attrs,
);
gp_ok && gse_ok
}
unsafe fn delete_cdp_leaked_properties(cx: *mut JSContext, global: HandleObject) -> bool {
let all_ok = true;
let mut op_result = ObjectOpResult::default();
{
let mut has_chrome: bool = false;
if has_property_clearing(cx, global, c"chrome", &mut has_chrome) && has_chrome {
let chrome_obj = get_subobject(cx, global, "chrome");
if !chrome_obj.is_null() {
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
rooted!(&in(wrapped_cx) let chrome_root = chrome_obj);
let mut has_runtime: bool = false;
if has_property_clearing(
cx,
chrome_root.handle().into(),
c"runtime",
&mut has_runtime,
) && has_runtime
{
if !JS_DeleteProperty(
cx,
chrome_root.handle().into(),
c"runtime".as_ptr(),
&mut op_result,
) {
JS_ClearPendingException(cx);
}
}
}
}
}
let cdc_globals = [
"cdc_adoQpoasnfa76pfcZLmcfl_Array",
"cdc_adoQpoasnfa76pfcZLmcfl_Promise",
"cdc_adoQpoasnfa76pfcZLmcfl_Symbol",
];
for cdc_name in &cdc_globals {
let c_name = bun_core::ZBox::from_bytes(cdc_name.as_bytes());
let mut has: bool = false;
if has_property_clearing(cx, global, c_name.as_cstr(), &mut has) && has {
if !JS_DeleteProperty(cx, global, c_name.as_ptr(), &mut op_result) {
JS_ClearPendingException(cx);
}
}
}
all_ok
}
unsafe fn inject_js_hooks(raw_cx: *mut JSContext, global: HandleObject) -> bool {
use ::std::ptr::NonNull;
use mozjs::context::JSContext;
use mozjs::rooted;
use mozjs::rust::{evaluate_script, CompileOptionsWrapper, Handle as RustHandle};
let js_code = if let Some(rp) = current_realm_profile(raw_cx) {
rp.build_hooks_js()
} else if is_profile_set() {
build_hooks_js_from_thread_local()
} else {
let profile = StealthProfile::firefox_default();
let hooks = StealthHooks::from_profile(
&profile.canvas,
&profile.audio,
&profile.navigator,
&profile.screen,
&profile.webgl,
&profile.font,
&profile.battery,
profile.webrtc_mode,
&profile.timing,
&profile.clientrects,
&profile.screen_display,
&profile.plugin,
&profile.speech,
&profile.media_devices,
&profile.permissions,
&profile.webgl_context,
&profile.connection,
&profile.iframe,
);
hooks.combined_js()
};
let cx_nn = match NonNull::new(raw_cx) {
Some(nn) => nn,
None => return true,
};
let mut cx = JSContext::from_ptr(cx_nn);
let filename = c"<bao-stealth-hooks>".to_owned();
let options = CompileOptionsWrapper::new(&mut cx, filename, 1);
rooted!(&in(cx) let mut rval = UndefinedValue());
let global_handle = RustHandle::from_marked_location(&*global.ptr as *const _);
match evaluate_script(&mut cx, global_handle, &js_code, rval.handle_mut(), options) {
Ok(_) => true,
Err(_) => {
unsafe {
mozjs::jsapi::JS_ClearPendingException(cx.raw_cx());
}
true
}
}
}
pub unsafe fn install_stealth_props(cx: *mut JSContext, global: *mut JSObject) -> bool {
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
rooted!(&in(wrapped_cx) let global_root = global);
let mut all_ok = true;
let nav = ensure_subobject(cx, global_root.handle().into(), "navigator");
if !nav.is_null() {
rooted!(&in(wrapped_cx) let nav_root = nav);
all_ok &= define_permanent_getter(
cx,
nav_root.handle().into(),
"webdriver",
Some(getter_webdriver),
);
all_ok &=
define_permanent_getter(cx, nav_root.handle().into(), "userAgent", Some(getter_ua));
all_ok &= define_permanent_getter(
cx,
nav_root.handle().into(),
"platform",
Some(getter_platform),
);
all_ok &= define_permanent_getter(
cx,
nav_root.handle().into(),
"language",
Some(getter_language),
);
all_ok &= define_permanent_getter(
cx,
nav_root.handle().into(),
"hardwareConcurrency",
Some(getter_hwc),
);
all_ok &= define_permanent_getter(
cx,
nav_root.handle().into(),
"maxTouchPoints",
Some(getter_touch),
);
all_ok &=
define_permanent_getter(cx, nav_root.handle().into(), "vendor", Some(getter_vendor));
all_ok &= define_permanent_getter(
cx,
nav_root.handle().into(),
"languages",
Some(getter_languages),
);
all_ok &= define_permanent_getter(
cx,
nav_root.handle().into(),
"deviceMemory",
Some(getter_device_memory),
);
}
let screen = ensure_subobject(cx, global_root.handle().into(), "screen");
if !screen.is_null() {
rooted!(&in(wrapped_cx) let scr_root = screen);
all_ok &=
define_permanent_getter(cx, scr_root.handle().into(), "width", Some(getter_screen_w));
all_ok &= define_permanent_getter(
cx,
scr_root.handle().into(),
"height",
Some(getter_screen_h),
);
all_ok &= define_permanent_getter(
cx,
scr_root.handle().into(),
"availWidth",
Some(getter_avail_w),
);
all_ok &= define_permanent_getter(
cx,
scr_root.handle().into(),
"availHeight",
Some(getter_avail_h),
);
all_ok &= define_permanent_getter(
cx,
scr_root.handle().into(),
"colorDepth",
Some(getter_color_depth),
);
all_ok &= define_permanent_getter(
cx,
scr_root.handle().into(),
"pixelDepth",
Some(getter_color_depth),
);
}
all_ok &= define_permanent_getter(
cx,
global_root.handle().into(),
"devicePixelRatio",
Some(getter_dpr),
);
all_ok &= install_webgl_override(cx, global_root.handle().into());
all_ok &= delete_cdp_leaked_properties(cx, global_root.handle().into());
all_ok &= inject_js_hooks(cx, global_root.handle().into());
all_ok
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn set_profile_stores_all_values() {
let profile = StealthProfile::chrome_default();
set_profile(&profile);
TL_WEBDRIVER.with(|v| assert_eq!(*v.borrow(), false));
TL_UA.with(|v| assert_eq!(*v.borrow(), profile.navigator.user_agent));
TL_PLATFORM.with(|v| assert_eq!(*v.borrow(), profile.navigator.platform));
TL_LANGUAGE.with(|v| assert_eq!(*v.borrow(), profile.navigator.language));
TL_HWC.with(|v| assert_eq!(*v.borrow(), profile.navigator.hardware_concurrency));
TL_TOUCH.with(|v| assert_eq!(*v.borrow(), profile.navigator.max_touch_points));
TL_VENDOR.with(|v| assert_eq!(*v.borrow(), profile.navigator.vendor));
TL_LANGUAGES.with(|v| assert_eq!(*v.borrow(), profile.navigator.languages));
TL_DEVICE_MEMORY.with(|v| {
assert!((*v.borrow() - profile.navigator.device_memory).abs() < f64::EPSILON)
});
TL_SCREEN_W.with(|v| assert_eq!(*v.borrow(), profile.screen.width));
TL_SCREEN_H.with(|v| assert_eq!(*v.borrow(), profile.screen.height));
TL_AVAIL_W.with(|v| assert_eq!(*v.borrow(), profile.screen.avail_width));
TL_AVAIL_H.with(|v| assert_eq!(*v.borrow(), profile.screen.avail_height));
TL_COLOR_DEPTH.with(|v| assert_eq!(*v.borrow(), profile.screen.color_depth));
TL_DPR.with(|v| {
assert!((*v.borrow() - profile.screen.device_pixel_ratio).abs() < f64::EPSILON)
});
}
#[test]
fn set_profile_firefox_values() {
let profile = StealthProfile::firefox_default();
set_profile(&profile);
TL_UA.with(|v| assert!(v.borrow().contains("Firefox")));
TL_VENDOR.with(|v| assert_eq!(*v.borrow(), ""));
}
#[test]
fn set_profile_custom_values() {
let mut profile = StealthProfile::chrome_default();
profile.navigator.user_agent = "TestUA".into();
profile.navigator.hardware_concurrency = 16;
profile.screen.width = 2560;
profile.screen.height = 1440;
profile.screen.device_pixel_ratio = 2.0;
set_profile(&profile);
TL_UA.with(|v| assert_eq!(*v.borrow(), "TestUA"));
TL_HWC.with(|v| assert_eq!(*v.borrow(), 16));
TL_SCREEN_W.with(|v| assert_eq!(*v.borrow(), 2560));
TL_SCREEN_H.with(|v| assert_eq!(*v.borrow(), 1440));
TL_DPR.with(|v| assert!((*v.borrow() - 2.0).abs() < f64::EPSILON));
}
#[test]
fn webdriver_always_false() {
let profile = StealthProfile::chrome_default();
set_profile(&profile);
TL_WEBDRIVER.with(|v| assert_eq!(*v.borrow(), false));
}
#[test]
fn set_profile_overwrites_previous() {
let p1 = StealthProfile::chrome_default();
set_profile(&p1);
TL_HWC.with(|v| assert_eq!(*v.borrow(), p1.navigator.hardware_concurrency));
let p2 = StealthProfile::firefox_default();
set_profile(&p2);
TL_HWC.with(|v| assert_eq!(*v.borrow(), p2.navigator.hardware_concurrency));
}
#[test]
fn webgl_vendor_renderer_stored() {
let profile = StealthProfile::chrome_default();
set_profile(&profile);
TL_WEBGL_VENDOR.with(|v| assert_eq!(*v.borrow(), profile.webgl.vendor));
TL_WEBGL_RENDERER.with(|v| assert_eq!(*v.borrow(), profile.webgl.renderer));
}
#[test]
fn webgl_vendor_firefox() {
let profile = StealthProfile::firefox_default();
set_profile(&profile);
TL_WEBGL_VENDOR.with(|v| assert!(!v.borrow().is_empty()));
TL_WEBGL_RENDERER.with(|v| assert!(!v.borrow().is_empty()));
}
#[test]
fn webgl_extensions_stored_chrome() {
let profile = StealthProfile::chrome_default();
set_profile(&profile);
TL_WEBGL_EXTENSIONS.with(|v| {
let exts = v.borrow();
assert!(!exts.is_empty(), "WebGL extensions must not be empty");
assert!(
exts.contains(&"WEBGL_debug_renderer_info".to_string()),
"Extensions must contain WEBGL_debug_renderer_info"
);
assert_eq!(*exts, profile.webgl.extensions);
});
}
#[test]
fn webgl_extensions_stored_firefox() {
let profile = StealthProfile::firefox_default();
set_profile(&profile);
TL_WEBGL_EXTENSIONS.with(|v| {
let exts = v.borrow();
assert!(!exts.is_empty(), "WebGL extensions must not be empty");
assert!(
exts.len() > profile.webgl.extensions.len()
|| exts.len() == profile.webgl.extensions.len()
);
assert_eq!(*exts, profile.webgl.extensions);
});
}
#[test]
fn webgl_extensions_differ_between_profiles() {
let chrome = StealthProfile::chrome_default();
set_profile(&chrome);
let ch_exts: Vec<String> = TL_WEBGL_EXTENSIONS.with(|v| v.borrow().clone());
let firefox = StealthProfile::firefox_default();
set_profile(&firefox);
let ff_exts: Vec<String> = TL_WEBGL_EXTENSIONS.with(|v| v.borrow().clone());
assert_ne!(
ch_exts.len(),
ff_exts.len(),
"Chrome and Firefox must have different extension counts"
);
assert!(
ff_exts.len() > ch_exts.len(),
"Firefox should have more WebGL extensions than Chrome"
);
}
#[test]
fn canvas_seed_stored_from_profile() {
let profile = StealthProfile::chrome_default();
set_profile(&profile);
TL_CANVAS_SEED.with(|v| assert_eq!(*v.borrow(), profile.canvas.seed()));
}
#[test]
fn canvas_amplitude_stored_from_profile() {
let profile = StealthProfile::chrome_default();
set_profile(&profile);
TL_CANVAS_AMPLITUDE.with(|v| {
assert!((*v.borrow() - profile.canvas.noise_amplitude()).abs() < f64::EPSILON);
});
}
#[test]
fn audio_seed_stored_from_profile() {
let profile = StealthProfile::firefox_default();
set_profile(&profile);
TL_AUDIO_SEED.with(|v| assert_eq!(*v.borrow(), profile.audio.seed()));
}
#[test]
fn audio_amplitude_stored_from_profile() {
let profile = StealthProfile::firefox_default();
set_profile(&profile);
TL_AUDIO_AMPLITUDE.with(|v| {
assert!((*v.borrow() - profile.audio.noise_amplitude()).abs() < f64::EPSILON);
});
}
#[test]
fn canvas_audio_seeds_differ_between_profiles() {
let chrome = StealthProfile::chrome_default();
set_profile(&chrome);
let ch_canvas = TL_CANVAS_SEED.with(|v| *v.borrow());
let ch_audio = TL_AUDIO_SEED.with(|v| *v.borrow());
let firefox = StealthProfile::firefox_default();
set_profile(&firefox);
let ff_canvas = TL_CANVAS_SEED.with(|v| *v.borrow());
let ff_audio = TL_AUDIO_SEED.with(|v| *v.borrow());
assert_ne!(
ch_canvas, ff_canvas,
"Canvas seeds must differ between profiles"
);
assert_ne!(
ch_audio, ff_audio,
"Audio seeds must differ between profiles"
);
}
#[test]
fn set_profile_overwrites_canvas_audio_seeds() {
let p1 = StealthProfile::chrome_default();
set_profile(&p1);
TL_CANVAS_SEED.with(|v| assert_eq!(*v.borrow(), p1.canvas.seed()));
let p2 = StealthProfile::firefox_default();
set_profile(&p2);
TL_CANVAS_SEED.with(|v| assert_eq!(*v.borrow(), p2.canvas.seed()));
}
#[test]
fn canvas_seed_accessible() {
let profile = StealthProfile::chrome_default();
set_profile(&profile);
assert_eq!(canvas_seed(), profile.canvas.seed());
assert!((canvas_amplitude() - profile.canvas.noise_amplitude()).abs() < f64::EPSILON);
}
#[test]
fn audio_js_hook_contains_seed() {
let profile = StealthProfile::firefox_default();
set_profile(&profile);
let seed = TL_AUDIO_SEED.with(|v| *v.borrow());
let expected = format!("var SEED = {}n;", seed);
assert!(expected.contains(&seed.to_string()));
}
#[test]
fn canvas_hook_includes_get_image_data() {
let profile = StealthProfile::chrome_default();
set_profile(&profile);
let template = "CanvasRenderingContext2D.prototype.getImageData";
assert!(!template.is_empty());
}
#[test]
fn audio_hook_includes_get_channel_data() {
let profile = StealthProfile::chrome_default();
set_profile(&profile);
let template = "proto.getChannelData";
assert!(!template.is_empty());
}
static PER_REALM_TEST_LOCK: ::std::sync::OnceLock<::std::sync::Mutex<()>> =
::std::sync::OnceLock::new();
fn per_realm_lock() -> &'static ::std::sync::Mutex<()> {
PER_REALM_TEST_LOCK.get_or_init(|| ::std::sync::Mutex::new(()))
}
#[test]
fn per_realm_profiles_isolated_between_pages() {
let _guard = per_realm_lock().lock().unwrap();
clear_all_realm_profiles();
let page_a_global: usize = 0xAA00_0000;
let page_b_global: usize = 0xBB00_0000;
let chrome = StealthProfile::chrome_default();
let firefox = StealthProfile::firefox_default();
assert_ne!(
chrome.navigator.user_agent, firefox.navigator.user_agent,
"test setup: profiles must differ"
);
set_profile_for_global(page_a_global, &chrome);
set_profile_for_global(page_b_global, &firefox);
let a_rp = realm_profiles().get(&page_a_global).unwrap().clone();
let b_rp = realm_profiles().get(&page_b_global).unwrap().clone();
assert_eq!(a_rp.ua, chrome.navigator.user_agent);
assert_eq!(b_rp.ua, firefox.navigator.user_agent);
assert_ne!(a_rp.ua, b_rp.ua, "BUG-ENG-366: per-page UA must differ");
assert_ne!(
a_rp.canvas_seed, b_rp.canvas_seed,
"BUG-ENG-366: per-page Canvas seed must differ"
);
clear_all_realm_profiles();
}
#[test]
fn per_realm_alias_shares_profile_with_node_realm() {
let _guard = per_realm_lock().lock().unwrap();
clear_all_realm_profiles();
let page_global: usize = 0x1000_0001;
let node_global: usize = 0x2000_0002;
let profile = StealthProfile::chrome_default();
set_profile_for_global(page_global, &profile);
register_global_alias(page_global, node_global);
let page_rp = realm_profiles().get(&page_global).unwrap().clone();
let node_rp = realm_profiles().get(&node_global).unwrap().clone();
assert_eq!(
page_rp.canvas_seed, node_rp.canvas_seed,
"BUG-ENG-366: Node Realm must share page Canvas seed"
);
assert_eq!(
page_rp.ua, node_rp.ua,
"BUG-ENG-366: Node Realm must share page UA"
);
clear_all_realm_profiles();
}
#[test]
fn per_realm_alias_null_pointers_ignored() {
let _guard = per_realm_lock().lock().unwrap();
clear_all_realm_profiles();
let profile = StealthProfile::firefox_default();
set_profile_for_global(0x3000, &profile);
register_global_alias(0, 0x4000);
register_global_alias(0x3000, 0);
assert!(realm_profiles().get(&0x4000).is_none());
assert!(realm_profiles().get(&0x3000).is_some());
clear_all_realm_profiles();
}
#[test]
fn per_realm_remove_drops_profile() {
let _guard = per_realm_lock().lock().unwrap();
clear_all_realm_profiles();
let g: usize = 0x5000;
let profile = StealthProfile::firefox_default();
set_profile_for_global(g, &profile);
assert!(realm_profiles().get(&g).is_some());
remove_profile_for_global(g);
assert!(
realm_profiles().get(&g).is_none(),
"remove must drop the profile"
);
clear_all_realm_profiles();
}
#[test]
fn per_realm_navigation_rekeys_profile() {
let _guard = per_realm_lock().lock().unwrap();
clear_all_realm_profiles();
let old_global: usize = 0x6000;
let new_global: usize = 0x6004;
let profile = StealthProfile::chrome_default();
set_profile_for_global(old_global, &profile);
register_global_alias(old_global, new_global);
let old_rp = realm_profiles().get(&old_global).unwrap().clone();
let new_rp = realm_profiles().get(&new_global).unwrap().clone();
assert_eq!(
old_rp.canvas_seed, new_rp.canvas_seed,
"BUG-ENG-366: navigation must preserve Canvas seed"
);
clear_all_realm_profiles();
}
#[test]
fn per_realm_force_isolate_false_simulation_still_isolated() {
let _guard = per_realm_lock().lock().unwrap();
clear_all_realm_profiles();
let profiles = [
(0xA1, StealthProfile::chrome_default()),
(0xA2, StealthProfile::firefox_default()),
(0xA3, StealthProfile::chrome_default()),
];
let mut third = StealthProfile::chrome_default();
third.canvas = crate::CanvasNoise::new(999);
let profiles = [
(profiles[0].0, profiles[0].1.clone()),
(profiles[1].0, profiles[1].1.clone()),
(0xA3, third),
];
for (addr, p) in &profiles {
set_profile_for_global(*addr, p);
}
let seeds: Vec<u64> = profiles
.iter()
.map(|(addr, _)| realm_profiles().get(addr).unwrap().canvas_seed)
.collect();
assert_eq!(seeds[0], profiles[0].1.canvas.seed());
assert_eq!(seeds[1], profiles[1].1.canvas.seed());
assert_eq!(seeds[2], profiles[2].1.canvas.seed());
assert_ne!(seeds[0], seeds[1]);
assert_ne!(seeds[0], seeds[2]);
assert_ne!(seeds[1], seeds[2]);
clear_all_realm_profiles();
}
#[test]
fn per_realm_fallback_to_thread_local_when_unregistered() {
let _guard = per_realm_lock().lock().unwrap();
clear_all_realm_profiles();
set_profile(&StealthProfile::firefox_default());
let seed = canvas_seed();
assert_eq!(seed, StealthProfile::firefox_default().canvas.seed());
clear_all_realm_profiles();
}
#[test]
fn worker_scope_config_inherits_all_navigator_fields() {
let profile = StealthProfile::chrome_default();
let ua = profile.navigator.user_agent.clone();
let platform = profile.navigator.platform.clone();
let hwc = profile.navigator.hardware_concurrency;
let lang = profile.navigator.language.clone();
let langs = profile.navigator.languages.clone();
assert!(!ua.is_empty(), "userAgent must be non-empty");
assert!(!platform.is_empty(), "platform must be non-empty");
assert!(hwc > 0, "hardwareConcurrency must be > 0");
assert!(!lang.is_empty(), "language must be non-empty");
assert!(!langs.is_empty(), "languages must be non-empty");
assert_eq!(langs[0], lang, "languages[0] must equal language");
}
#[test]
fn worker_scope_config_stealth_profile_preserved() {
let profile = StealthProfile::firefox_default();
let canvas_seed = profile.canvas.seed();
let audio_seed = profile.audio.seed();
let webgl_vendor = profile.webgl.vendor.clone();
let webgl_renderer = profile.webgl.renderer.clone();
assert!(canvas_seed > 0, "Canvas seed must be > 0");
assert!(audio_seed > 0, "Audio seed must be > 0");
assert!(!webgl_vendor.is_empty(), "WebGL vendor must be non-empty");
assert!(
!webgl_renderer.is_empty(),
"WebGL renderer must be non-empty"
);
}
#[test]
fn worker_scope_config_navigator_matches_profile() {
let chrome = StealthProfile::chrome_default();
let firefox = StealthProfile::firefox_default();
assert!(chrome.navigator.user_agent.contains("Chrome"));
assert_eq!(chrome.navigator.platform, "Linux x86_64");
assert_eq!(chrome.navigator.hardware_concurrency, 8);
assert_eq!(chrome.navigator.language, "en-US");
assert_eq!(chrome.navigator.languages, vec!["en-US", "en"]);
assert!(firefox.navigator.user_agent.contains("Firefox"));
assert_eq!(firefox.navigator.platform, "Linux x86_64");
assert_eq!(firefox.navigator.hardware_concurrency, 8);
assert_eq!(firefox.navigator.language, "en-US");
assert_eq!(firefox.navigator.languages, vec!["en-US", "en"]);
assert_ne!(chrome.navigator.user_agent, firefox.navigator.user_agent);
}
#[test]
fn worker_scope_config_canvas_audio_seed_consistency() {
let profile = StealthProfile::chrome_default();
let seed1 = profile.canvas.seed();
let seed2 = profile.canvas.seed();
assert_eq!(
seed1, seed2,
"Canvas seed must be deterministic within profile"
);
assert_eq!(
profile.audio.seed(),
profile.audio.seed(),
"Audio seed must be deterministic within profile"
);
let other = StealthProfile::firefox_default();
assert_ne!(
seed1,
other.canvas.seed(),
"Different profiles must have different Canvas seeds"
);
assert_ne!(
profile.audio.seed(),
other.audio.seed(),
"Different profiles must have different Audio seeds"
);
}
}