use serde::{Deserialize, Serialize};
use crate::options::Viewport;
use crate::seed::{BrowserPersonaPreset, PersonaSeed};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NetworkFingerprint {
pub user_agent: String,
pub accept_language: String,
pub accept_header: String,
pub sec_ch_ua: String,
pub sec_ch_ua_mobile: String,
pub sec_ch_ua_platform: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AutomationPolicy {
pub webdriver: bool,
pub expose_webdriver_helpers: bool,
pub expose_automation_globals: bool,
}
impl Default for AutomationPolicy {
fn default() -> Self {
Self {
webdriver: false,
expose_webdriver_helpers: false,
expose_automation_globals: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ScreenFingerprint {
pub width: u32,
pub height: u32,
pub avail_width: u32,
pub avail_height: u32,
pub avail_left: i32,
pub avail_top: i32,
pub left: i32,
pub top: i32,
pub color_depth: u32,
pub pixel_depth: u32,
pub device_scale_factor: u32,
pub is_extended: bool,
pub orientation_type: String,
pub orientation_angle: u16,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WindowFingerprint {
pub outer_width: u32,
pub outer_height: u32,
pub screen_x: i32,
pub screen_y: i32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CssFingerprint {
pub moz_prefix_enabled: bool,
pub webkit_prefix_enabled: bool,
}
impl WindowFingerprint {
pub fn from_viewport(viewport: &Viewport) -> Self {
Self {
outer_width: viewport.width,
outer_height: viewport.height,
screen_x: 0,
screen_y: 0,
}
}
}
impl ScreenFingerprint {
pub fn from_viewport(viewport: &Viewport) -> Self {
Self {
width: viewport.width,
height: viewport.height,
avail_width: viewport.width,
avail_height: viewport.height,
avail_left: 0,
avail_top: 0,
left: 0,
top: 0,
color_depth: 24,
pixel_depth: 24,
device_scale_factor: viewport.device_scale_factor,
is_extended: false,
orientation_type: if viewport.width >= viewport.height {
"landscape-primary"
} else {
"portrait-primary"
}
.to_string(),
orientation_angle: 0,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GraphicsFingerprint {
pub webgl_vendor: String,
pub webgl_renderer: String,
pub webgl_masked_vendor: String,
pub webgl_masked_renderer: String,
pub webgl_seed: PersonaSeed,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JsFingerprint {
pub app_version: String,
pub platform: String,
pub language: String,
pub languages: Vec<String>,
pub hardware_concurrency: u32,
pub device_memory_gb: u32,
pub max_touch_points: u32,
pub do_not_track: String,
pub expose_global_privacy_control: bool,
pub global_privacy_control: bool,
pub permissions_enabled: bool,
pub bluetooth_enabled: bool,
pub bluetooth_available: bool,
pub media_devices_enabled: bool,
pub webgpu_enabled: bool,
pub offscreen_canvas_enabled: bool,
pub service_worker_enabled: bool,
pub ua_platform_version: String,
pub ua_architecture: String,
pub ua_bitness: String,
pub ua_model: String,
pub timezone: String,
pub vendor: String,
pub product_sub: String,
pub pdf_viewer_enabled: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GeoFingerprint {
pub timezone: String,
pub locale: String,
pub latitude: Option<String>,
pub longitude: Option<String>,
pub accuracy_meters: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WebRtcFingerprint {
pub ipv4: Option<String>,
pub ipv6: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CanvasFingerprint {
pub seed: PersonaSeed,
pub noise_enabled: bool,
pub noise_amplitude: u8,
pub blink_low_entropy_probe: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DomRectFingerprint {
pub seed: PersonaSeed,
pub enabled: bool,
pub quantization_steps_per_px: u32,
pub fill_empty_client_rects: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EngineFingerprint {
pub enabled: bool,
pub to_fixed_range_error_message: String,
pub array_constructor_source: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AudioFingerprint {
pub seed: PersonaSeed,
pub sample_rate: u32,
pub output_latency_ms: u32,
pub max_channel_count: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FontFingerprint {
pub seed: PersonaSeed,
pub families: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MediaFingerprint {
pub speech_voices: Vec<String>,
pub audio_inputs: u32,
pub video_inputs: u32,
pub audio_outputs: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BatteryFingerprint {
pub charging: bool,
pub charging_time_seconds: u32,
pub discharging_time_seconds: Option<u32>,
pub level_percent: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StorageFingerprint {
pub quota_bytes: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResolvedPersona {
pub seed: PersonaSeed,
pub preset: BrowserPersonaPreset,
pub startup_url: Option<String>,
pub browser_name: String,
pub browser_version: String,
pub platform_name: String,
pub viewport: Viewport,
pub screen: ScreenFingerprint,
pub window: WindowFingerprint,
pub css: CssFingerprint,
pub network: NetworkFingerprint,
pub graphics: GraphicsFingerprint,
pub js: JsFingerprint,
pub geo: GeoFingerprint,
pub webrtc: WebRtcFingerprint,
pub canvas: CanvasFingerprint,
pub domrect: DomRectFingerprint,
pub engine: EngineFingerprint,
pub audio: AudioFingerprint,
pub fonts: FontFingerprint,
pub media: MediaFingerprint,
pub battery: BatteryFingerprint,
pub storage: StorageFingerprint,
pub automation: AutomationPolicy,
}
pub type BrowserPersona = ResolvedPersona;
impl ResolvedPersona {
pub fn from_preset(preset: BrowserPersonaPreset) -> Self {
Self::from_preset_and_seed(
preset,
PersonaSeed::from_stable_input(format!("preset:{preset:?}")),
)
}
pub fn from_preset_and_seed(preset: BrowserPersonaPreset, seed: PersonaSeed) -> Self {
match preset {
BrowserPersonaPreset::ChromeStable => Self::chrome_stable(seed),
}
}
pub fn chrome_stable(seed: PersonaSeed) -> Self {
let viewport = Viewport::default();
Self {
seed: seed.clone(),
preset: BrowserPersonaPreset::ChromeStable,
startup_url: None,
browser_name: "Chrome".to_string(),
browser_version: "136.0.7103.49".to_string(),
platform_name: "macOS".to_string(),
viewport: viewport.clone(),
screen: ScreenFingerprint::from_viewport(&viewport),
window: WindowFingerprint::from_viewport(&viewport),
css: CssFingerprint {
moz_prefix_enabled: false,
webkit_prefix_enabled: true,
},
network: NetworkFingerprint {
user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36".to_string(),
accept_language: "en-US,en;q=0.9".to_string(),
accept_header: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8".to_string(),
sec_ch_ua: "\"Chromium\";v=\"136\", \"Google Chrome\";v=\"136\", \"Not.A/Brand\";v=\"99\"".to_string(),
sec_ch_ua_mobile: "?0".to_string(),
sec_ch_ua_platform: "\"macOS\"".to_string(),
},
graphics: GraphicsFingerprint {
webgl_vendor: "Intel Inc.".to_string(),
webgl_renderer: "Intel(R) Iris(TM) Plus Graphics OpenGL Engine".to_string(),
webgl_masked_vendor: "WebKit".to_string(),
webgl_masked_renderer: "WebKit WebGL".to_string(),
webgl_seed: PersonaSeed::from_stable_input(format!("{seed}:webgl")),
},
js: JsFingerprint {
app_version: "5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36".to_string(),
platform: "MacIntel".to_string(),
language: "en-US".to_string(),
languages: vec!["en-US".to_string(), "en".to_string()],
hardware_concurrency: 8,
device_memory_gb: 8,
max_touch_points: 0,
do_not_track: String::new(),
expose_global_privacy_control: false,
global_privacy_control: false,
permissions_enabled: true,
bluetooth_enabled: true,
bluetooth_available: true,
media_devices_enabled: true,
webgpu_enabled: true,
offscreen_canvas_enabled: true,
service_worker_enabled: true,
ua_platform_version: "15.7.0".to_string(),
ua_architecture: "x86".to_string(),
ua_bitness: "64".to_string(),
ua_model: String::new(),
timezone: "America/Los_Angeles".to_string(),
vendor: "Google Inc.".to_string(),
product_sub: "20030107".to_string(),
pdf_viewer_enabled: true,
},
geo: GeoFingerprint {
timezone: "America/Los_Angeles".to_string(),
locale: "en-US".to_string(),
latitude: None,
longitude: None,
accuracy_meters: None,
},
webrtc: WebRtcFingerprint {
ipv4: None,
ipv6: None,
},
canvas: CanvasFingerprint {
seed: PersonaSeed::from_stable_input(format!("{seed}:canvas")),
noise_enabled: true,
noise_amplitude: 1,
blink_low_entropy_probe: true,
},
domrect: DomRectFingerprint {
seed: PersonaSeed::from_stable_input(format!("{seed}:domrect")),
enabled: true,
quantization_steps_per_px: 64,
fill_empty_client_rects: true,
},
engine: EngineFingerprint {
enabled: true,
to_fixed_range_error_message: "toFixed() digits argument must be between 0-100"
.to_string(),
array_constructor_source: "function Array() { [native code] }".to_string(),
},
audio: AudioFingerprint {
seed: PersonaSeed::from_stable_input(format!("{seed}:audio")),
sample_rate: 48_000,
output_latency_ms: 20,
max_channel_count: 2,
},
fonts: FontFingerprint {
seed: PersonaSeed::from_stable_input(format!("{seed}:fonts")),
families: vec![
"Arial".to_string(),
"Helvetica".to_string(),
"Times New Roman".to_string(),
"Courier New".to_string(),
],
},
media: MediaFingerprint {
speech_voices: vec!["Samantha".to_string(), "Alex".to_string()],
audio_inputs: 1,
video_inputs: 1,
audio_outputs: 1,
},
battery: BatteryFingerprint {
charging: true,
charging_time_seconds: 0,
discharging_time_seconds: None,
level_percent: 100,
},
storage: StorageFingerprint { quota_bytes: None },
automation: AutomationPolicy::default(),
}
}
}