Skip to main content

headless_engine/js/
context.rs

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}