| |
| |
| |
| |
| |
| |
| |
| |
|
|
| use rquickjs::function::Rest; |
| use rquickjs::{Ctx, Function, Object, Value}; |
|
|
| |
| pub fn install(ctx: &Ctx<'_>) -> rquickjs::Result<()> { |
| install_base64(ctx)?; |
| install_encoding(ctx)?; |
| install_console(ctx)?; |
| install_timers(ctx)?; |
| install_url(ctx)?; |
| install_crypto(ctx)?; |
| install_globals(ctx)?; |
| remove_dangerous(ctx)?; |
| Ok(()) |
| } |
|
|
| fn install_base64(ctx: &Ctx<'_>) -> rquickjs::Result<()> { |
| |
| use base64::{engine::general_purpose::STANDARD, Engine as _}; |
|
|
| ctx.globals().set( |
| "atob", |
| Function::new(ctx.clone(), |s: String| -> rquickjs::Result<String> { |
| let cleaned = s |
| .chars() |
| .filter(|c| !c.is_whitespace()) |
| .collect::<String>(); |
| let decoded = STANDARD |
| .decode(&cleaned) |
| .map_err(|_| rquickjs::Error::Exception)?; |
| |
| Ok(decoded.iter().map(|&b| b as char).collect()) |
| })?, |
| )?; |
|
|
| ctx.globals().set( |
| "btoa", |
| Function::new(ctx.clone(), |s: String| -> rquickjs::Result<String> { |
| let bytes: Result<Vec<u8>, _> = s |
| .chars() |
| .map(|c| { |
| if c as u32 > 255 { |
| Err(rquickjs::Error::Exception) |
| } else { |
| Ok(c as u8) |
| } |
| }) |
| .collect(); |
| Ok(STANDARD.encode(bytes?)) |
| })?, |
| )?; |
|
|
| Ok(()) |
| } |
|
|
| fn install_encoding(ctx: &Ctx<'_>) -> rquickjs::Result<()> { |
| |
| ctx.eval::<(), _>( |
| r#" |
| globalThis.TextEncoder = class TextEncoder { |
| constructor() { this.encoding = 'utf-8'; } |
| encode(str) { |
| const bytes = []; |
| for (let i = 0; i < str.length; i++) { |
| let cp = str.codePointAt(i); |
| if (cp > 0xFFFF) i++; |
| if (cp <= 0x7F) { |
| bytes.push(cp); |
| } else if (cp <= 0x7FF) { |
| bytes.push(0xC0 | (cp >> 6), 0x80 | (cp & 0x3F)); |
| } else if (cp <= 0xFFFF) { |
| bytes.push(0xE0|(cp>>12), 0x80|((cp>>6)&0x3F), 0x80|(cp&0x3F)); |
| } else { |
| bytes.push(0xF0|(cp>>18), 0x80|((cp>>12)&0x3F), 0x80|((cp>>6)&0x3F), 0x80|(cp&0x3F)); |
| } |
| } |
| return new Uint8Array(bytes); |
| } |
| encodeInto(str, dest) { |
| const enc = this.encode(str); |
| const len = Math.min(enc.length, dest.length); |
| dest.set(enc.subarray(0, len)); |
| return { read: str.length, written: len }; |
| } |
| }; |
| |
| globalThis.TextDecoder = class TextDecoder { |
| constructor(enc) { this.encoding = (enc||'utf-8').toLowerCase(); } |
| decode(buf) { |
| const bytes = buf instanceof Uint8Array ? buf : |
| buf instanceof ArrayBuffer ? new Uint8Array(buf) : |
| new Uint8Array(Array.from(buf)); |
| let str = '', i = 0; |
| while (i < bytes.length) { |
| const b = bytes[i]; |
| let cp; |
| if ((b & 0x80) === 0) { cp = b; i+=1; } |
| else if ((b & 0xE0) === 0xC0) { cp = ((b&0x1F)<<6)|(bytes[i+1]&0x3F); i+=2; } |
| else if ((b & 0xF0) === 0xE0) { cp = ((b&0xF)<<12)|((bytes[i+1]&0x3F)<<6)|(bytes[i+2]&0x3F); i+=3; } |
| else { cp = ((b&7)<<18)|((bytes[i+1]&0x3F)<<12)|((bytes[i+2]&0x3F)<<6)|(bytes[i+3]&0x3F); i+=4; } |
| str += cp > 0xFFFF ? String.fromCodePoint(cp) : String.fromCharCode(cp); |
| } |
| return str; |
| } |
| }; |
| "#, |
| )?; |
| Ok(()) |
| } |
|
|
| fn install_console(ctx: &Ctx<'_>) -> rquickjs::Result<()> { |
| let console = Object::new(ctx.clone())?; |
|
|
| macro_rules! add_level { |
| ($name:literal, $macro:ident) => {{ |
| let name = $name; |
| console.set( |
| name, |
| Function::new(ctx.clone(), move |args: Rest<Value<'_>>| { |
| let parts: Vec<String> = args |
| .0 |
| .iter() |
| .map(|v| { |
| v.as_string() |
| .and_then(|s| s.to_string().ok()) |
| .unwrap_or_else(|| format!("{:?}", v)) |
| }) |
| .collect(); |
| tracing::$macro!(target: "bex_js", "{}", parts.join(" ")); |
| })?, |
| )?; |
| }}; |
| } |
|
|
| add_level!("log", debug); |
| add_level!("info", debug); |
| add_level!("debug", trace); |
| add_level!("warn", warn); |
| add_level!("error", error); |
|
|
| |
| console.set( |
| "time", |
| Function::new(ctx.clone(), |_: String| {})?, |
| )?; |
| console.set( |
| "timeEnd", |
| Function::new(ctx.clone(), |_: String| {})?, |
| )?; |
|
|
| ctx.globals().set("console", console)?; |
| Ok(()) |
| } |
|
|
| fn install_timers(ctx: &Ctx<'_>) -> rquickjs::Result<()> { |
| ctx.eval::<(), _>( |
| r#" |
| // Call callback synchronously (compat for setTimeout(fn, 0) patterns) |
| globalThis.setTimeout = function(fn, ms) { |
| if (typeof fn === 'function') { try { fn(); } catch(e) {} } |
| return 0; |
| }; |
| globalThis.clearTimeout = function(id) {}; |
| globalThis.setInterval = function(fn, ms) { |
| if (typeof fn === 'function') { try { fn(); } catch(e) {} } |
| return 0; |
| }; |
| globalThis.clearInterval = function(id) {}; |
| globalThis.queueMicrotask = function(fn) { |
| if (typeof fn === 'function') { try { fn(); } catch(e) {} } |
| }; |
| "#, |
| )?; |
| Ok(()) |
| } |
|
|
| fn install_url(ctx: &Ctx<'_>) -> rquickjs::Result<()> { |
| ctx.eval::<(), _>( |
| r#" |
| globalThis.URLSearchParams = class URLSearchParams { |
| constructor(init) { |
| this._map = {}; |
| if (typeof init === 'string') { |
| init.replace(/^\?/, '').split('&').filter(Boolean).forEach(pair => { |
| const eq = pair.indexOf('='); |
| const k = eq >= 0 ? pair.substring(0, eq) : pair; |
| const v = eq >= 0 ? pair.substring(eq + 1) : ''; |
| this._map[decodeURIComponent(k)] = decodeURIComponent(v); |
| }); |
| } |
| } |
| get(k) { return this._map[k] !== undefined ? this._map[k] : null; } |
| set(k, v) { this._map[k] = String(v); } |
| delete(k) { delete this._map[k]; } |
| has(k) { return k in this._map; } |
| append(k, v) { this._map[k] = v; } |
| toString() { |
| return Object.entries(this._map) |
| .map(([k,v]) => encodeURIComponent(k)+'='+encodeURIComponent(v)) |
| .join('&'); |
| } |
| entries() { return Object.entries(this._map)[Symbol.iterator](); } |
| }; |
| |
| globalThis.URL = class URL { |
| constructor(url, base) { |
| this.href = url; |
| const m = url.match(/^([\w+.-]+:)\/\/([^/?#@]*@)?([^/?#:]*)(?::(\d+))?(\/[^?#]*)?(\?[^#]*)?(#.*)?$/); |
| if (m) { |
| this.protocol = m[1] || 'https:'; |
| this.username = ''; this.password = ''; |
| this.hostname = m[3] || ''; |
| this.port = m[4] || ''; |
| this.host = this.hostname + (this.port ? ':' + this.port : ''); |
| this.pathname = m[5] || '/'; |
| this.search = m[6] || ''; |
| this.hash = m[7] || ''; |
| this.origin = this.protocol + '//' + this.host; |
| this.searchParams = new URLSearchParams(this.search); |
| } |
| } |
| toString() { return this.href; } |
| }; |
| "#, |
| )?; |
| Ok(()) |
| } |
|
|
| fn install_crypto(ctx: &Ctx<'_>) -> rquickjs::Result<()> { |
| |
| ctx.globals().set( |
| "__randomBytes", |
| Function::new(ctx.clone(), |len: u32| -> rquickjs::Result<Vec<u8>> { |
| use rand::RngCore; |
| let mut buf = vec![0u8; len as usize]; |
| rand::thread_rng().fill_bytes(&mut buf); |
| Ok(buf) |
| })?, |
| )?; |
|
|
| |
| ctx.eval::<(), _>( |
| r#" |
| globalThis.crypto = { |
| getRandomValues(arr) { |
| if (!(arr instanceof Uint8Array)) throw new TypeError('Expected Uint8Array'); |
| const bytes = __randomBytes(arr.length); |
| for (let i = 0; i < arr.length; i++) arr[i] = bytes[i]; |
| return arr; |
| }, |
| randomUUID() { |
| const bytes = __randomBytes(16); |
| bytes[6] = (bytes[6] & 0x0f) | 0x40; |
| bytes[8] = (bytes[8] & 0x3f) | 0x80; |
| const hex = Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join(''); |
| return hex.slice(0,8) + '-' + hex.slice(8,12) + '-' + hex.slice(12,16) + '-' + hex.slice(16,20) + '-' + hex.slice(20); |
| } |
| }; |
| "#, |
| )?; |
|
|
| |
| ctx.eval::<(), _>(include_str!("../assets/crypto_subtle.js"))?; |
|
|
| Ok(()) |
| } |
|
|
| fn install_globals(ctx: &Ctx<'_>) -> rquickjs::Result<()> { |
| ctx.eval::<(), _>( |
| r#" |
| // Standard browser/worker global aliases |
| globalThis.self = globalThis; |
| globalThis.window = globalThis; |
| |
| // performance.now() stub (monotonic ms) |
| globalThis.performance = { |
| _start: Date.now(), |
| now() { return Date.now() - this._start; } |
| }; |
| |
| // structuredClone (deep copy via JSON round-trip) |
| globalThis.structuredClone = function(obj) { |
| return JSON.parse(JSON.stringify(obj)); |
| }; |
| |
| // navigator stub |
| globalThis.navigator = { |
| userAgent: 'Mozilla/5.0 (compatible; BexEngine/6.0)', |
| language: 'en-US', |
| }; |
| |
| // location stub (helps window.location patterns) |
| globalThis.location = { |
| href: 'https://bex.engine/', |
| hostname: 'bex.engine', |
| protocol: 'https:', |
| assign(url) { this.href = url; }, |
| replace(url) { this.href = url; }, |
| }; |
| "#, |
| )?; |
| Ok(()) |
| } |
|
|
| fn remove_dangerous(ctx: &Ctx<'_>) -> rquickjs::Result<()> { |
| ctx.eval::<(), _>( |
| r#" |
| // Remove APIs that could be used to escape the sandbox or identify the host |
| delete globalThis.WebAssembly; |
| // Note: eval() is intentionally KEPT — many sites use eval(atob(...)) patterns |
| // Note: Function() constructor is kept for compat but monitored |
| "#, |
| )?; |
| Ok(()) |
| } |
|
|