1use crate::network::fingerprint::Fingerprint;
2use anyhow::Result;
3use boa_engine::{Context, Source};
4
5pub struct JsRuntime {
6 context: Context<'static>,
7}
8
9impl JsRuntime {
10 pub fn new() -> Result<Self> {
11 let default_fp =
12 Fingerprint::for_profile(crate::network::fingerprint::DeviceProfile::ChromeWindows);
13 Self::with_fingerprint(&default_fp)
14 }
15
16 pub fn with_fingerprint(fp: &Fingerprint) -> Result<Self> {
17 let mut context = Context::default();
18
19 let max_touch_points = if fp.is_mobile { 5 } else { 0 };
20 let dpr = if fp.is_mobile { "3.0" } else { "1.0" };
21 let mobile_bool = if fp.is_mobile { "true" } else { "false" };
22
23 let init_script = format!(
24 r#"
25 // 1. Core Window & Global References
26 globalThis.window = globalThis;
27 globalThis.self = globalThis;
28 globalThis.top = globalThis;
29 globalThis.parent = globalThis;
30 globalThis.devicePixelRatio = {dpr};
31
32 // 2. Deep Navigator Stealth Emulation
33 const pluginArray = [
34 {{
35 name: "Chrome PDF Plugin",
36 filename: "internal-pdf-viewer",
37 description: "Portable Document Format",
38 length: 1,
39 0: {{
40 type: "application/x-google-chrome-pdf",
41 suffixes: "pdf",
42 description: "Portable Document Format",
43 enabledPlugin: null
44 }}
45 }},
46 {{
47 name: "Chrome PDF Viewer",
48 filename: "mhjfbmdgcfjbbpaeojofohoefgiehjai",
49 description: "",
50 length: 1,
51 0: {{
52 type: "application/pdf",
53 suffixes: "pdf",
54 description: "",
55 enabledPlugin: null
56 }}
57 }},
58 {{
59 name: "Native Client",
60 filename: "internal-nacl-plugin",
61 description: "",
62 length: 2,
63 0: {{
64 type: "application/x-nacl",
65 suffixes: "",
66 description: "Native Client Executable",
67 enabledPlugin: null
68 }},
69 1: {{
70 type: "application/x-pnacl",
71 suffixes: "",
72 description: "Portable Native Client Executable",
73 enabledPlugin: null
74 }}
75 }}
76 ];
77 pluginArray.item = function(index) {{ return this[index] || null; }};
78 pluginArray.namedItem = function(name) {{
79 for (let i = 0; i < this.length; i++) {{
80 if (this[i].name === name) return this[i];
81 }}
82 return null;
83 }};
84 pluginArray.refresh = function() {{}};
85
86 const mimeTypeArray = [
87 {{
88 type: "application/pdf",
89 suffixes: "pdf",
90 description: "",
91 enabledPlugin: pluginArray[1]
92 }},
93 {{
94 type: "application/x-google-chrome-pdf",
95 suffixes: "pdf",
96 description: "Portable Document Format",
97 enabledPlugin: pluginArray[0]
98 }},
99 {{
100 type: "application/x-nacl",
101 suffixes: "",
102 description: "Native Client Executable",
103 enabledPlugin: pluginArray[2]
104 }},
105 {{
106 type: "application/x-pnacl",
107 suffixes: "",
108 description: "Portable Native Client Executable",
109 enabledPlugin: pluginArray[2]
110 }}
111 ];
112 mimeTypeArray.item = function(index) {{ return this[index] || null; }};
113 mimeTypeArray.namedItem = function(type) {{
114 for (let i = 0; i < this.length; i++) {{
115 if (this[i].type === type) return this[i];
116 }}
117 return null;
118 }};
119
120 globalThis.navigator = {{
121 userAgent: "{ua}",
122 appVersion: "{ua}",
123 platform: "{platform}",
124 appName: "Netscape",
125 appCodeName: "Mozilla",
126 language: "en-US",
127 languages: ["en-US", "en"],
128 webdriver: false,
129 cookieEnabled: true,
130 hardwareConcurrency: 8,
131 deviceMemory: 8,
132 maxTouchPoints: {touch},
133 vendor: "Google Inc.",
134 vendorSub: "",
135 product: "Gecko",
136 productSub: "20030107",
137 plugins: pluginArray,
138 mimeTypes: mimeTypeArray,
139 doNotTrack: null,
140 userAgentData: {{
141 brands: [
142 {{ brand: "Not(A:Brand", version: "99" }},
143 {{ brand: "Google Chrome", version: "133" }},
144 {{ brand: "Chromium", version: "133" }}
145 ],
146 mobile: {is_mobile},
147 platform: "{platform}",
148 getHighEntropyValues: function(hints) {{
149 return Promise.resolve({{
150 architecture: "x86",
151 bitness: "64",
152 brands: [
153 {{ brand: "Not(A:Brand", version: "99" }},
154 {{ brand: "Google Chrome", version: "133" }},
155 {{ brand: "Chromium", version: "133" }}
156 ],
157 mobile: {is_mobile},
158 model: "",
159 platform: "{platform}",
160 platformVersion: "15.0.0",
161 uaFullVersion: "133.0.6943.127"
162 }});
163 }}
164 }},
165 permissions: {{
166 query: function(param) {{
167 return Promise.resolve({{
168 state: "default",
169 onchange: null
170 }});
171 }}
172 }}
173 }};
174
175 // 3. Complete window.chrome Emulation
176 globalThis.window.chrome = {{
177 app: {{
178 isInstalled: false,
179 InstallState: {{
180 DISABLED: "disabled",
181 INSTALLED: "installed",
182 NOT_INSTALLED: "not_installed"
183 }},
184 RunningState: {{
185 CANNOT_RUN: "cannot_run",
186 READY_TO_RUN: "ready_to_run",
187 RUNNING: "running"
188 }},
189 getDetails: function() {{ return null; }},
190 getIsInstalled: function() {{ return false; }},
191 runningState: function() {{ return "cannot_run"; }}
192 }},
193 runtime: {{
194 OnInstalledReason: {{
195 CHROME_UPDATE: "chrome_update",
196 INSTALL: "install",
197 SHARED_MODULE_UPDATE: "shared_module_update",
198 UPDATE: "update"
199 }},
200 OnRestartRequiredReason: {{
201 APP_UPDATE: "app_update",
202 OS_UPDATE: "os_update",
203 PERIODIC: "periodic"
204 }},
205 PlatformArch: {{
206 ARM: "arm",
207 ARM64: "arm64",
208 MIPS: "mips",
209 MIPS64: "mips64",
210 X86_32: "x86-32",
211 X86_64: "x86-64"
212 }},
213 PlatformNaclArch: {{
214 ARM: "arm",
215 MIPS: "mips",
216 MIPS64: "mips64",
217 X86_32: "x86-32",
218 X86_64: "x86-64"
219 }},
220 PlatformOs: {{
221 ANDROID: "android",
222 CROS: "cros",
223 LINUX: "linux",
224 MAC: "mac",
225 OPENBSD: "openbsd",
226 WIN: "win"
227 }},
228 RequestUpdateCheckStatus: {{
229 NO_UPDATE: "no_update",
230 THROTTLED: "throttled",
231 UPDATE_AVAILABLE: "update_available"
232 }},
233 connect: function() {{}},
234 sendMessage: function() {{}}
235 }},
236 csi: function() {{
237 return {{
238 onloadT: Date.now(),
239 pageT: 142.5,
240 startE: Date.now() - 150,
241 tran: 15
242 }};
243 }},
244 loadTimes: function() {{
245 return {{
246 commitLoadTime: Date.now() / 1000 - 0.1,
247 connectionInfo: "h2",
248 finishDocumentLoadTime: Date.now() / 1000,
249 finishLoadTime: Date.now() / 1000,
250 firstPaintAfterLoadTime: 0,
251 firstPaintTime: Date.now() / 1000 - 0.05,
252 navigationType: "Other",
253 npnNegotiatedProtocol: "h2",
254 requestTime: Date.now() / 1000 - 0.15,
255 startLoadTime: Date.now() / 1000 - 0.15,
256 wasAlternateProtocolAvailable: false,
257 wasFetchedViaSpdy: true,
258 wasNpnNegotiated: true
259 }};
260 }}
261 }};
262
263 // 4. Screen and Display Metrics
264 globalThis.screen = {{
265 width: {sw},
266 height: {sh},
267 availWidth: {sw},
268 availHeight: {sh},
269 colorDepth: 24,
270 pixelDepth: 24,
271 availLeft: 0,
272 availTop: 0,
273 orientation: {{
274 angle: 0,
275 type: "landscape-primary",
276 onchange: null
277 }}
278 }};
279 globalThis.outerWidth = {sw};
280 globalThis.outerHeight = {sh};
281 globalThis.innerWidth = {sw};
282 globalThis.innerHeight = {sh};
283
284 // 5. Notification API
285 globalThis.Notification = {{
286 permission: "default",
287 maxActions: 2,
288 requestPermission: function() {{
289 return Promise.resolve("default");
290 }}
291 }};
292
293 // 6. WebGL Vendor & Renderer Spoofing
294 globalThis.WebGLRenderingContext = function() {{}};
295 globalThis.WebGLRenderingContext.prototype = {{
296 getParameter: function(param) {{
297 // UNMASKED_VENDOR_WEBGL
298 if (param === 37445) return "Google Inc. (NVIDIA)";
299 // UNMASKED_RENDERER_WEBGL
300 if (param === 37446) return "ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Direct3D11 vs_5_0 ps_5_0, D3D11)";
301 // VENDOR
302 if (param === 7936) return "WebKit";
303 // RENDERER
304 if (param === 7937) return "WebKit WebGL";
305 return null;
306 }}
307 }};
308 globalThis.WebGL2RenderingContext = globalThis.WebGLRenderingContext;
309
310 // 7. Document & Location Defaults
311 globalThis.document = {{
312 title: "",
313 cookie: "",
314 referrer: "",
315 readyState: "complete",
316 characterSet: "UTF-8",
317 compatMode: "CSS1Compat",
318 location: {{
319 href: ""
320 }},
321 getElementById: function(id) {{ return null; }},
322 getElementsByTagName: function(tag) {{ return []; }},
323 querySelector: function(sel) {{ return null; }},
324 querySelectorAll: function(sel) {{ return []; }},
325 createElement: function(tag) {{
326 return {{
327 tagName: tag.toUpperCase(),
328 setAttribute: function() {{}},
329 getAttribute: function() {{ return null; }},
330 appendChild: function() {{}},
331 style: {{}}
332 }};
333 }}
334 }};
335
336 globalThis.location = {{
337 href: "",
338 origin: "",
339 protocol: "https:",
340 host: "",
341 hostname: "",
342 pathname: "/",
343 search: ""
344 }};
345 "#,
346 ua = fp.user_agent,
347 platform = fp.platform,
348 touch = max_touch_points,
349 dpr = dpr,
350 is_mobile = mobile_bool,
351 sw = fp.screen_width,
352 sh = fp.screen_height
353 );
354
355 context
356 .eval(Source::from_bytes(&init_script))
357 .map_err(|e| anyhow::anyhow!("Failed to initialize JS runtime: {}", e))?;
358
359 Ok(Self { context })
360 }
361
362 pub fn update_page_state(&mut self, url: &str, title: &str) -> Result<()> {
363 let escaped_url = url.replace('\\', "\\\\").replace('"', "\\\"");
364 let escaped_title = title.replace('\\', "\\\\").replace('"', "\\\"");
365
366 let update_script = format!(
367 r#"
368 document.title = "{}";
369 document.location.href = "{}";
370 location.href = "{}";
371 "#,
372 escaped_title, escaped_url, escaped_url
373 );
374
375 self.context
376 .eval(Source::from_bytes(&update_script))
377 .map_err(|e| anyhow::anyhow!("Failed to update JS page state: {}", e))?;
378
379 Ok(())
380 }
381
382 pub fn evaluate(&mut self, code: &str) -> Result<String> {
383 match self.context.eval(Source::from_bytes(code)) {
384 Ok(res) => Ok(res.display().to_string()),
385 Err(err) => Err(anyhow::anyhow!("JS Eval error: {}", err)),
386 }
387 }
388}