headless_engine/js/
context.rs1use 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}