Skip to main content

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}