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
21        let init_script = format!(
22            r#"
23            globalThis.window = globalThis;
24            globalThis.self = globalThis;
25            globalThis.top = globalThis;
26
27            globalThis.navigator = {{
28                userAgent: "{ua}",
29                appVersion: "{ua}",
30                platform: "{platform}",
31                language: "en-US",
32                languages: ["en-US", "en"],
33                webdriver: false,
34                cookieEnabled: true,
35                hardwareConcurrency: 8,
36                deviceMemory: 8,
37                vendor: "Google Inc.",
38                vendorSub: "",
39                product: "Gecko",
40                productSub: "20030107",
41                maxTouchPoints: {touch}
42            }};
43
44            globalThis.window.chrome = {{
45                runtime: {{}},
46                loadTimes: function() {{}},
47                csi: function() {{}},
48                app: {{}}
49            }};
50
51            globalThis.screen = {{
52                width: {sw},
53                height: {sh},
54                availWidth: {sw},
55                availHeight: {sh},
56                colorDepth: 24,
57                pixelDepth: 24
58            }};
59
60            globalThis.document = {{
61                title: "",
62                cookie: "",
63                referrer: "",
64                readyState: "complete",
65                location: {{
66                    href: ""
67                }},
68                getElementById: function(id) {{ return null; }},
69                getElementsByTagName: function(tag) {{ return []; }},
70                querySelector: function(sel) {{ return null; }},
71                querySelectorAll: function(sel) {{ return []; }}
72            }};
73
74            globalThis.location = {{
75                href: "",
76                origin: "",
77                protocol: "https:",
78                host: "",
79                hostname: "",
80                pathname: "/",
81                search: ""
82            }};
83            "#,
84            ua = fp.user_agent,
85            platform = fp.platform,
86            touch = max_touch_points,
87            sw = fp.screen_width,
88            sh = fp.screen_height
89        );
90
91        context
92            .eval(Source::from_bytes(&init_script))
93            .map_err(|e| anyhow::anyhow!("Failed to initialize JS runtime: {}", e))?;
94
95        Ok(Self { context })
96    }
97
98    pub fn update_page_state(&mut self, url: &str, title: &str) -> Result<()> {
99        let escaped_url = url.replace('\\', "\\\\").replace('"', "\\\"");
100        let escaped_title = title.replace('\\', "\\\\").replace('"', "\\\"");
101
102        let update_script = format!(
103            r#"
104            document.title = "{}";
105            document.location.href = "{}";
106            location.href = "{}";
107            "#,
108            escaped_title, escaped_url, escaped_url
109        );
110
111        self.context
112            .eval(Source::from_bytes(&update_script))
113            .map_err(|e| anyhow::anyhow!("Failed to update JS page state: {}", e))?;
114
115        Ok(())
116    }
117
118    pub fn evaluate(&mut self, code: &str) -> Result<String> {
119        match self.context.eval(Source::from_bytes(code)) {
120            Ok(res) => Ok(res.display().to_string()),
121            Err(err) => Err(anyhow::anyhow!("JS Eval error: {}", err)),
122        }
123    }
124}