browser_commander/fingerprint/
init_script.rs1use serde_json::{json, Map, Value};
17
18use super::profile::FingerprintProfile;
19
20pub const FINGERPRINT_PAYLOAD_SOURCE: &str = include_str!("init_payload.js");
22
23#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
25pub struct InitScriptOptions {
26 pub patch_webdriver: bool,
31 pub patch_languages: bool,
34}
35
36pub fn build_init_script_config(
40 profile: &FingerprintProfile,
41 options: InitScriptOptions,
42) -> Option<Value> {
43 let mut config = Map::new();
44
45 if options.patch_webdriver {
46 config.insert("webdriver".to_string(), json!(false));
47 }
48 if let Some(device_memory) = profile.device_memory {
49 config.insert("deviceMemory".to_string(), json!(device_memory));
50 }
51 if let Some(vendor) = &profile.vendor {
52 config.insert("vendor".to_string(), json!(vendor));
53 }
54 if let Some(do_not_track) = &profile.do_not_track {
55 config.insert("doNotTrack".to_string(), json!(do_not_track));
56 }
57 if options.patch_languages {
58 if let Some(languages) = &profile.languages {
59 config.insert("languages".to_string(), json!(languages));
60 }
61 }
62 if let Some(webgl) = &profile.webgl {
63 config.insert("webgl".to_string(), json!(webgl));
64 }
65 if let Some(screen) = &profile.screen {
66 let mut patched = Map::new();
69 for (name, value) in [
70 ("availWidth", screen.avail_width),
71 ("availHeight", screen.avail_height),
72 ("colorDepth", screen.color_depth),
73 ("pixelDepth", screen.pixel_depth),
74 ] {
75 if let Some(value) = value {
76 patched.insert(name.to_string(), json!(value));
77 }
78 }
79 if !patched.is_empty() {
80 config.insert("screen".to_string(), Value::Object(patched));
81 }
82 }
83
84 if config.is_empty() {
85 None
86 } else {
87 Some(Value::Object(config))
88 }
89}
90
91pub fn build_fingerprint_init_script(
93 profile: &FingerprintProfile,
94 options: InitScriptOptions,
95) -> Option<String> {
96 let config = build_init_script_config(profile, options)?;
97 Some(format!(
101 "(() => {{\n{FINGERPRINT_PAYLOAD_SOURCE}\nfingerprintPayload({config});\n}})();"
102 ))
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use crate::fingerprint::profile::{ScreenProfile, WebglProfile};
109
110 fn config(profile: FingerprintProfile, options: InitScriptOptions) -> Option<Value> {
111 build_init_script_config(&profile.resolve().expect("profile resolves"), options)
112 }
113
114 fn script(profile: FingerprintProfile, options: InitScriptOptions) -> Option<String> {
115 build_fingerprint_init_script(&profile.resolve().expect("profile resolves"), options)
116 }
117
118 #[test]
119 fn returns_nothing_when_the_browser_side_overrides_cover_everything() {
120 let profile = FingerprintProfile::default()
121 .user_agent("Mozilla/5.0 (X11; Linux x86_64) Chrome/140.0.0.0")
122 .timezone_id("UTC")
123 .hardware_concurrency(8)
124 .max_touch_points(0);
125
126 assert_eq!(config(profile.clone(), InitScriptOptions::default()), None);
127 assert_eq!(script(profile, InitScriptOptions::default()), None);
128 }
129
130 #[test]
131 fn patches_only_the_fields_the_emulation_domain_has_no_command_for() {
132 let config = config(
135 FingerprintProfile::default()
136 .device_memory(8.0)
137 .vendor("Google Inc.")
138 .do_not_track("1")
139 .hardware_concurrency(8)
140 .timezone_id("UTC"),
141 InitScriptOptions::default(),
142 )
143 .expect("something is left to patch");
144 let mut keys: Vec<_> = config
145 .as_object()
146 .expect("object")
147 .keys()
148 .map(String::as_str)
149 .collect();
150 keys.sort_unstable();
151
152 assert_eq!(keys, vec!["deviceMemory", "doNotTrack", "vendor"]);
153 }
154
155 #[test]
156 fn adds_webdriver_only_when_the_caller_asks_for_it() {
157 let profile = FingerprintProfile::default().vendor("Google Inc.");
158
159 assert_eq!(
160 config(profile.clone(), InitScriptOptions::default()).expect("config")["webdriver"],
161 Value::Null
162 );
163 assert_eq!(
164 config(
165 profile,
166 InitScriptOptions {
167 patch_webdriver: true,
168 ..InitScriptOptions::default()
169 }
170 )
171 .expect("config")["webdriver"],
172 json!(false)
173 );
174 }
175
176 #[test]
177 fn patches_webdriver_even_for_an_otherwise_empty_profile() {
178 let script = script(
179 FingerprintProfile::default(),
180 InitScriptOptions {
181 patch_webdriver: true,
182 ..InitScriptOptions::default()
183 },
184 )
185 .expect("a script is produced");
186
187 assert!(script.contains("\"webdriver\":false"));
188 }
189
190 #[test]
191 fn leaves_languages_to_the_browser_unless_explicitly_asked() {
192 let profile = FingerprintProfile::default().languages(["fr-FR", "fr"]);
193
194 assert_eq!(config(profile.clone(), InitScriptOptions::default()), None);
195 assert_eq!(
196 config(
197 profile,
198 InitScriptOptions {
199 patch_languages: true,
200 ..InitScriptOptions::default()
201 }
202 )
203 .expect("config")["languages"],
204 json!(["fr-FR", "fr"])
205 );
206 }
207
208 #[test]
209 fn drops_the_screen_dimensions_set_device_metrics_override_already_enforces() {
210 let config = config(
211 FingerprintProfile::default().screen(ScreenProfile {
212 width: Some(1920),
213 height: Some(1080),
214 avail_width: Some(1920),
215 avail_height: Some(1032),
216 color_depth: Some(24),
217 pixel_depth: Some(24),
218 }),
219 InitScriptOptions::default(),
220 )
221 .expect("config");
222
223 assert_eq!(
224 config["screen"],
225 json!({
226 "availWidth": 1920,
227 "availHeight": 1032,
228 "colorDepth": 24,
229 "pixelDepth": 24,
230 })
231 );
232 }
233
234 #[test]
235 fn skips_the_screen_patch_when_only_width_and_height_are_given() {
236 assert_eq!(
237 config(
238 FingerprintProfile::default().screen(ScreenProfile {
239 width: Some(1920),
240 height: Some(1080),
241 ..ScreenProfile::default()
242 }),
243 InitScriptOptions::default()
244 ),
245 None
246 );
247 }
248
249 #[test]
250 fn wraps_the_shared_payload_and_calls_it_with_the_config() {
251 let script = script(
252 FingerprintProfile::default().webgl(WebglProfile {
253 unmasked_vendor: Some("Google Inc. (NVIDIA)".to_string()),
254 ..WebglProfile::default()
255 }),
256 InitScriptOptions::default(),
257 )
258 .expect("a script is produced");
259
260 assert!(script.starts_with("(() => {\n"));
261 assert!(script.ends_with("\n})();"));
262 assert!(script.contains(FINGERPRINT_PAYLOAD_SOURCE));
263 assert!(script.contains("fingerprintPayload({"));
264 assert!(script.contains("Google Inc. (NVIDIA)"));
265 }
266
267 #[test]
271 fn embeds_the_shared_payload_asset_verbatim() {
272 assert!(FINGERPRINT_PAYLOAD_SOURCE.contains("function fingerprintPayload(config) {"));
273 for line in FINGERPRINT_PAYLOAD_SOURCE.lines() {
274 assert!(
275 !line.starts_with("import ") && !line.starts_with("export "),
276 "the payload must stay a classic script: {line}"
277 );
278 }
279 }
280}