1use serde::{Deserialize, Serialize};
2
3use crate::options::Viewport;
4use crate::seed::{BrowserPersonaPreset, PersonaSeed};
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8pub struct NetworkFingerprint {
9 pub user_agent: String,
10 pub accept_language: String,
11 pub accept_header: String,
12 pub sec_ch_ua: String,
13 pub sec_ch_ua_mobile: String,
14 pub sec_ch_ua_platform: String,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub struct AutomationPolicy {
20 pub webdriver: bool,
21 pub expose_webdriver_helpers: bool,
22 pub expose_automation_globals: bool,
23}
24
25impl Default for AutomationPolicy {
26 fn default() -> Self {
27 Self {
28 webdriver: false,
29 expose_webdriver_helpers: false,
30 expose_automation_globals: false,
31 }
32 }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37pub struct ScreenFingerprint {
38 pub width: u32,
39 pub height: u32,
40 pub avail_width: u32,
41 pub avail_height: u32,
42 pub avail_left: i32,
43 pub avail_top: i32,
44 pub left: i32,
45 pub top: i32,
46 pub color_depth: u32,
47 pub pixel_depth: u32,
48 pub device_scale_factor: u32,
49 pub is_extended: bool,
50 pub orientation_type: String,
51 pub orientation_angle: u16,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56pub struct WindowFingerprint {
57 pub outer_width: u32,
58 pub outer_height: u32,
59 pub screen_x: i32,
60 pub screen_y: i32,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65pub struct CssFingerprint {
66 pub moz_prefix_enabled: bool,
67 pub webkit_prefix_enabled: bool,
68}
69
70impl WindowFingerprint {
71 pub fn from_viewport(viewport: &Viewport) -> Self {
72 Self {
73 outer_width: viewport.width,
74 outer_height: viewport.height,
75 screen_x: 0,
76 screen_y: 0,
77 }
78 }
79}
80
81impl ScreenFingerprint {
82 pub fn from_viewport(viewport: &Viewport) -> Self {
83 Self {
84 width: viewport.width,
85 height: viewport.height,
86 avail_width: viewport.width,
87 avail_height: viewport.height,
88 avail_left: 0,
89 avail_top: 0,
90 left: 0,
91 top: 0,
92 color_depth: 24,
93 pixel_depth: 24,
94 device_scale_factor: viewport.device_scale_factor,
95 is_extended: false,
96 orientation_type: if viewport.width >= viewport.height {
97 "landscape-primary"
98 } else {
99 "portrait-primary"
100 }
101 .to_string(),
102 orientation_angle: 0,
103 }
104 }
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109pub struct GraphicsFingerprint {
110 pub webgl_vendor: String,
111 pub webgl_renderer: String,
112 pub webgl_masked_vendor: String,
113 pub webgl_masked_renderer: String,
114 pub webgl_seed: PersonaSeed,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119pub struct JsFingerprint {
120 pub app_version: String,
121 pub platform: String,
122 pub language: String,
123 pub languages: Vec<String>,
124 pub hardware_concurrency: u32,
125 pub device_memory_gb: u32,
126 pub max_touch_points: u32,
127 pub do_not_track: String,
128 pub expose_global_privacy_control: bool,
129 pub global_privacy_control: bool,
130 pub permissions_enabled: bool,
131 pub bluetooth_enabled: bool,
132 pub bluetooth_available: bool,
133 pub media_devices_enabled: bool,
134 pub webgpu_enabled: bool,
135 pub offscreen_canvas_enabled: bool,
136 pub service_worker_enabled: bool,
137 pub ua_platform_version: String,
138 pub ua_architecture: String,
139 pub ua_bitness: String,
140 pub ua_model: String,
141 pub timezone: String,
142 pub vendor: String,
143 pub product_sub: String,
144 pub pdf_viewer_enabled: bool,
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
149pub struct GeoFingerprint {
150 pub timezone: String,
151 pub locale: String,
152 pub latitude: Option<String>,
153 pub longitude: Option<String>,
154 pub accuracy_meters: Option<u32>,
155}
156
157#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
159pub struct WebRtcFingerprint {
160 pub ipv4: Option<String>,
161 pub ipv6: Option<String>,
162}
163
164#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
166pub struct CanvasFingerprint {
167 pub seed: PersonaSeed,
168 pub noise_enabled: bool,
169 pub noise_amplitude: u8,
170 pub blink_low_entropy_probe: bool,
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
175pub struct DomRectFingerprint {
176 pub seed: PersonaSeed,
177 pub enabled: bool,
178 pub quantization_steps_per_px: u32,
179 pub fill_empty_client_rects: bool,
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
184pub struct EngineFingerprint {
185 pub enabled: bool,
186 pub to_fixed_range_error_message: String,
187 pub array_constructor_source: String,
188}
189
190#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
192pub struct AudioFingerprint {
193 pub seed: PersonaSeed,
194 pub sample_rate: u32,
195 pub output_latency_ms: u32,
196 pub max_channel_count: u32,
197}
198
199#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
201pub struct FontFingerprint {
202 pub seed: PersonaSeed,
203 pub families: Vec<String>,
204}
205
206#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
208pub struct MediaFingerprint {
209 pub speech_voices: Vec<String>,
210 pub audio_inputs: u32,
211 pub video_inputs: u32,
212 pub audio_outputs: u32,
213}
214
215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
217pub struct BatteryFingerprint {
218 pub charging: bool,
219 pub charging_time_seconds: u32,
220 pub discharging_time_seconds: Option<u32>,
221 pub level_percent: u32,
222}
223
224#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
226pub struct StorageFingerprint {
227 pub quota_bytes: Option<u64>,
228}
229
230#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
232pub struct ResolvedPersona {
233 pub seed: PersonaSeed,
234 pub preset: BrowserPersonaPreset,
235 pub startup_url: Option<String>,
236 pub browser_name: String,
237 pub browser_version: String,
238 pub platform_name: String,
239 pub viewport: Viewport,
240 pub screen: ScreenFingerprint,
241 pub window: WindowFingerprint,
242 pub css: CssFingerprint,
243 pub network: NetworkFingerprint,
244 pub graphics: GraphicsFingerprint,
245 pub js: JsFingerprint,
246 pub geo: GeoFingerprint,
247 pub webrtc: WebRtcFingerprint,
248 pub canvas: CanvasFingerprint,
249 pub domrect: DomRectFingerprint,
250 pub engine: EngineFingerprint,
251 pub audio: AudioFingerprint,
252 pub fonts: FontFingerprint,
253 pub media: MediaFingerprint,
254 pub battery: BatteryFingerprint,
255 pub storage: StorageFingerprint,
256 pub automation: AutomationPolicy,
257}
258
259pub type BrowserPersona = ResolvedPersona;
260
261impl ResolvedPersona {
262 pub fn from_preset(preset: BrowserPersonaPreset) -> Self {
263 Self::from_preset_and_seed(
264 preset,
265 PersonaSeed::from_stable_input(format!("preset:{preset:?}")),
266 )
267 }
268
269 pub fn from_preset_and_seed(preset: BrowserPersonaPreset, seed: PersonaSeed) -> Self {
270 match preset {
271 BrowserPersonaPreset::ChromeStable => Self::chrome_stable(seed),
272 }
273 }
274
275 pub fn chrome_stable(seed: PersonaSeed) -> Self {
276 let viewport = Viewport::default();
277 Self {
278 seed: seed.clone(),
279 preset: BrowserPersonaPreset::ChromeStable,
280 startup_url: None,
281 browser_name: "Chrome".to_string(),
282 browser_version: "136.0.7103.49".to_string(),
283 platform_name: "macOS".to_string(),
284 viewport: viewport.clone(),
285 screen: ScreenFingerprint::from_viewport(&viewport),
286 window: WindowFingerprint::from_viewport(&viewport),
287 css: CssFingerprint {
288 moz_prefix_enabled: false,
289 webkit_prefix_enabled: true,
290 },
291 network: NetworkFingerprint {
292 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(),
293 accept_language: "en-US,en;q=0.9".to_string(),
294 accept_header: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8".to_string(),
295 sec_ch_ua: "\"Chromium\";v=\"136\", \"Google Chrome\";v=\"136\", \"Not.A/Brand\";v=\"99\"".to_string(),
296 sec_ch_ua_mobile: "?0".to_string(),
297 sec_ch_ua_platform: "\"macOS\"".to_string(),
298 },
299 graphics: GraphicsFingerprint {
300 webgl_vendor: "Intel Inc.".to_string(),
301 webgl_renderer: "Intel(R) Iris(TM) Plus Graphics OpenGL Engine".to_string(),
302 webgl_masked_vendor: "WebKit".to_string(),
303 webgl_masked_renderer: "WebKit WebGL".to_string(),
304 webgl_seed: PersonaSeed::from_stable_input(format!("{seed}:webgl")),
305 },
306 js: JsFingerprint {
307 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(),
308 platform: "MacIntel".to_string(),
309 language: "en-US".to_string(),
310 languages: vec!["en-US".to_string(), "en".to_string()],
311 hardware_concurrency: 8,
312 device_memory_gb: 8,
313 max_touch_points: 0,
314 do_not_track: String::new(),
315 expose_global_privacy_control: false,
316 global_privacy_control: false,
317 permissions_enabled: true,
318 bluetooth_enabled: true,
319 bluetooth_available: true,
320 media_devices_enabled: true,
321 webgpu_enabled: true,
322 offscreen_canvas_enabled: true,
323 service_worker_enabled: true,
324 ua_platform_version: "15.7.0".to_string(),
325 ua_architecture: "x86".to_string(),
326 ua_bitness: "64".to_string(),
327 ua_model: String::new(),
328 timezone: "America/Los_Angeles".to_string(),
329 vendor: "Google Inc.".to_string(),
330 product_sub: "20030107".to_string(),
331 pdf_viewer_enabled: true,
332 },
333 geo: GeoFingerprint {
334 timezone: "America/Los_Angeles".to_string(),
335 locale: "en-US".to_string(),
336 latitude: None,
337 longitude: None,
338 accuracy_meters: None,
339 },
340 webrtc: WebRtcFingerprint {
341 ipv4: None,
342 ipv6: None,
343 },
344 canvas: CanvasFingerprint {
345 seed: PersonaSeed::from_stable_input(format!("{seed}:canvas")),
346 noise_enabled: true,
347 noise_amplitude: 1,
348 blink_low_entropy_probe: true,
349 },
350 domrect: DomRectFingerprint {
351 seed: PersonaSeed::from_stable_input(format!("{seed}:domrect")),
352 enabled: true,
353 quantization_steps_per_px: 64,
354 fill_empty_client_rects: true,
355 },
356 engine: EngineFingerprint {
357 enabled: true,
358 to_fixed_range_error_message: "toFixed() digits argument must be between 0-100"
359 .to_string(),
360 array_constructor_source: "function Array() { [native code] }".to_string(),
361 },
362 audio: AudioFingerprint {
363 seed: PersonaSeed::from_stable_input(format!("{seed}:audio")),
364 sample_rate: 48_000,
365 output_latency_ms: 20,
366 max_channel_count: 2,
367 },
368 fonts: FontFingerprint {
369 seed: PersonaSeed::from_stable_input(format!("{seed}:fonts")),
370 families: vec![
371 "Arial".to_string(),
372 "Helvetica".to_string(),
373 "Times New Roman".to_string(),
374 "Courier New".to_string(),
375 ],
376 },
377 media: MediaFingerprint {
378 speech_voices: vec!["Samantha".to_string(), "Alex".to_string()],
379 audio_inputs: 1,
380 video_inputs: 1,
381 audio_outputs: 1,
382 },
383 battery: BatteryFingerprint {
384 charging: true,
385 charging_time_seconds: 0,
386 discharging_time_seconds: None,
387 level_percent: 100,
388 },
389 storage: StorageFingerprint { quota_bytes: None },
390 automation: AutomationPolicy::default(),
391 }
392 }
393}