jsdet_core/bridge.rs
1use crate::observation::Value;
2
3/// The bridge interface between JavaScript and the host.
4///
5/// Consumers implement this trait to provide fake API surfaces.
6/// Sear implements browser bridges (document, window, navigator, fetch).
7/// Soleno implements Chrome extension bridges (chrome.tabs, chrome.cookies, etc.).
8///
9/// Every call through the bridge is observable — the sandbox records what
10/// function was called, with what arguments, and what was returned.
11///
12/// # Design
13///
14/// The bridge is intentionally stringly-typed. JS APIs are a massive surface
15/// and encoding every possible API as a Rust enum would be both fragile and
16/// incomplete. Instead, the bridge receives the API name as a string and
17/// the arguments as `Vec<Value>`. The consumer pattern-matches on the string.
18///
19/// # Thread safety
20///
21/// Bridge implementations must be `Send + Sync` because the sandbox may run
22/// on a different thread from the caller. For single-threaded use, wrap
23/// interior state in `RefCell` behind a `Mutex`.
24pub trait Bridge: Send + Sync {
25 /// Handle a function call from JavaScript.
26 ///
27 /// `api` is the fully qualified name: `"document.createElement"`,
28 /// `"chrome.tabs.query"`, `"fetch"`, etc.
29 ///
30 /// Return `Ok(value)` to provide a return value to JS.
31 /// Return `Err(message)` to throw a JS exception.
32 ///
33 /// # Errors
34 ///
35 /// Returns an error if the API throws a JS exception.
36 fn call(&self, api: &str, args: &[Value]) -> Result<Value, String>;
37
38 /// Handle a property read from JavaScript.
39 ///
40 /// `object` is the object name: `"navigator"`, `"document"`, `"chrome.runtime"`.
41 /// `property` is the property name: `"userAgent"`, `"cookie"`, `"id"`.
42 ///
43 /// # Errors
44 ///
45 /// Returns an error if the property is undefined or reading it throws an exception.
46 fn get_property(&self, object: &str, property: &str) -> Result<Value, String>;
47
48 /// Handle a property write from JavaScript.
49 ///
50 /// # Errors
51 ///
52 /// Returns an error if writing the property throws an exception.
53 fn set_property(&self, object: &str, property: &str, value: &Value) -> Result<(), String>;
54
55 /// Return the list of global objects this bridge provides.
56 ///
57 /// These become `globalThis.{name}` in the JS environment.
58 /// Example: `["document", "window", "navigator", "fetch", "XMLHttpRequest"]`
59 /// Example: `["chrome"]`
60 fn provided_globals(&self) -> Vec<String>;
61
62 /// Return the JS bootstrap code that installs this bridge's API surface.
63 ///
64 /// This code runs BEFORE user scripts. It should define the global objects,
65 /// prototype chains, and proxy traps that make the bridge look like a real API.
66 ///
67 /// The bootstrap has access to `__jsdet_call(api, args)` and
68 /// `__jsdet_get(object, property)` host-imported functions for calling
69 /// back into the bridge.
70 fn bootstrap_js(&self) -> String;
71}
72
73/// A bridge that provides no APIs. Useful for testing the core sandbox
74/// in isolation — scripts execute but all API calls throw.
75pub struct EmptyBridge;
76
77impl Bridge for EmptyBridge {
78 fn call(&self, api: &str, _args: &[Value]) -> Result<Value, String> {
79 Err(format!("{api} is not defined"))
80 }
81
82 fn get_property(&self, object: &str, property: &str) -> Result<Value, String> {
83 Err(format!("{object}.{property} is not defined"))
84 }
85
86 fn set_property(&self, _object: &str, _property: &str, _value: &Value) -> Result<(), String> {
87 Ok(()) // silently ignore writes
88 }
89
90 fn provided_globals(&self) -> Vec<String> {
91 Vec::new()
92 }
93
94 fn bootstrap_js(&self) -> String {
95 String::new()
96 }
97}
98
99/// Compose multiple bridges into one.
100///
101/// Each bridge handles its own globals. When a call comes in, the composite
102/// tries each bridge in order until one succeeds. This lets you combine
103/// `jsdet-browser` + custom hooks without modifying either.
104pub struct CompositeBridge {
105 bridges: Vec<Box<dyn Bridge>>,
106}
107
108impl CompositeBridge {
109 #[must_use]
110 pub fn new(bridges: Vec<Box<dyn Bridge>>) -> Self {
111 Self { bridges }
112 }
113}
114
115impl Bridge for CompositeBridge {
116 fn call(&self, api: &str, args: &[Value]) -> Result<Value, String> {
117 for bridge in &self.bridges {
118 match bridge.call(api, args) {
119 Ok(value) => return Ok(value),
120 Err(e) if e.ends_with("is not defined") => {}
121 Err(e) => return Err(e),
122 }
123 }
124 Err(format!("{api} is not defined"))
125 }
126
127 fn get_property(&self, object: &str, property: &str) -> Result<Value, String> {
128 for bridge in &self.bridges {
129 match bridge.get_property(object, property) {
130 Ok(value) => return Ok(value),
131 Err(e) if e.ends_with("is not defined") => {}
132 Err(e) => return Err(e),
133 }
134 }
135 Err(format!("{object}.{property} is not defined"))
136 }
137
138 fn set_property(&self, object: &str, property: &str, value: &Value) -> Result<(), String> {
139 for bridge in &self.bridges {
140 match bridge.set_property(object, property, value) {
141 Ok(()) => return Ok(()),
142 Err(e) if e.ends_with("is not defined") => {}
143 Err(e) => return Err(e),
144 }
145 }
146 Ok(())
147 }
148
149 fn provided_globals(&self) -> Vec<String> {
150 self.bridges
151 .iter()
152 .flat_map(|b| b.provided_globals())
153 .collect()
154 }
155
156 fn bootstrap_js(&self) -> String {
157 self.bridges
158 .iter()
159 .map(|b| b.bootstrap_js())
160 .collect::<Vec<_>>()
161 .join("\n")
162 }
163}
164
165/// Hook for intercepting and modifying bridge calls.
166///
167/// Security researchers use this to:
168/// - Log all calls to a specific API
169/// - Modify return values (e.g., fake canvas fingerprints)
170/// - Block specific APIs
171/// - Inject faults
172pub trait Hook: Send + Sync {
173 /// Called BEFORE the bridge handles the request.
174 /// Return `Some(value)` to short-circuit — the bridge is not called.
175 /// Return `None` to let the bridge handle it normally.
176 fn before_call(&self, _api: &str, _args: &[Value]) -> Option<Result<Value, String>> {
177 None
178 }
179
180 /// Called AFTER the bridge returns.
181 /// Can modify the return value.
182 ///
183 /// # Errors
184 ///
185 /// Returns an error if the hook changes the modification to failure.
186 fn after_call(
187 &self,
188 _api: &str,
189 _args: &[Value],
190 result: Result<Value, String>,
191 ) -> Result<Value, String> {
192 result
193 }
194}
195
196/// A bridge wrapped with hooks for interception.
197pub struct HookedBridge {
198 inner: Box<dyn Bridge>,
199 hooks: Vec<Box<dyn Hook>>,
200}
201
202impl HookedBridge {
203 #[must_use]
204 pub fn new(inner: Box<dyn Bridge>, hooks: Vec<Box<dyn Hook>>) -> Self {
205 Self { inner, hooks }
206 }
207}
208
209impl Bridge for HookedBridge {
210 fn call(&self, api: &str, args: &[Value]) -> Result<Value, String> {
211 // Run before hooks — first one to return Some wins.
212 for hook in &self.hooks {
213 if let Some(result) = hook.before_call(api, args) {
214 return result;
215 }
216 }
217
218 let mut result = self.inner.call(api, args);
219
220 // Run after hooks in order.
221 for hook in &self.hooks {
222 result = hook.after_call(api, args, result);
223 }
224
225 result
226 }
227
228 fn get_property(&self, object: &str, property: &str) -> Result<Value, String> {
229 self.inner.get_property(object, property)
230 }
231
232 fn set_property(&self, object: &str, property: &str, value: &Value) -> Result<(), String> {
233 self.inner.set_property(object, property, value)
234 }
235
236 fn provided_globals(&self) -> Vec<String> {
237 self.inner.provided_globals()
238 }
239
240 fn bootstrap_js(&self) -> String {
241 self.inner.bootstrap_js()
242 }
243}