use anyhow::Result; use deno_core::{JsRuntime, RuntimeOptions, v8}; use serde_json::Value; use std::collections::HashMap; pub struct DenoRuntime { // We create a new runtime for each execution to ensure isolation // In a production environment, we might want to pool runtimes or use isolates more efficiently } impl DenoRuntime { pub fn new() -> Self { Self {} } pub async fn execute(&self, code: String, payload: Option, headers: HashMap) -> Result<(String, String, u16, HashMap)> { let (tx, rx) = tokio::sync::oneshot::channel(); std::thread::spawn(move || { let rt = tokio::runtime::Builder::new_current_thread() .enable_all() .build() .unwrap(); rt.block_on(async { let result = Self::execute_inner(code, payload, headers).await; let _ = tx.send(result); }); }); rx.await.map_err(|_| anyhow::anyhow!("Deno execution thread panicked"))? } async fn execute_inner(code: String, payload: Option, headers: HashMap) -> Result<(String, String, u16, HashMap)> { // Initialize JS Runtime let mut runtime = JsRuntime::new(RuntimeOptions::default()); // 1. Inject Preamble (Polyfills for Deno.serve, Request, Response, Headers) let preamble = r#" globalThis.console = { log: (...args) => { Deno.core.print(args.map(a => String(a)).join(" ") + "\n"); }, error: (...args) => { Deno.core.print("[ERROR] " + args.map(a => String(a)).join(" ") + "\n", true); } }; class Headers { constructor(init) { this.map = new Map(); if (init) { if (init instanceof Headers) { init.forEach((v, k) => this.map.set(k.toLowerCase(), v)); } else if (Array.isArray(init)) { init.forEach(([k, v]) => this.map.set(k.toLowerCase(), v)); } else { Object.entries(init).forEach(([k, v]) => this.map.set(k.toLowerCase(), v)); } } } get(key) { return this.map.get(key.toLowerCase()) || null; } set(key, value) { this.map.set(key.toLowerCase(), value); } has(key) { return this.map.has(key.toLowerCase()); } forEach(callback) { this.map.forEach(callback); } entries() { return this.map.entries(); } } globalThis.Headers = Headers; globalThis.Deno = { serve: (handler) => { globalThis._handler = handler; }, core: Deno.core, env: { get: (key) => { return globalThis._env ? globalThis._env[key] : null; }, toObject: () => { return globalThis._env || {}; } } }; class Response { constructor(body, init) { this.body = body; this.status = init?.status || 200; this.headers = new Headers(init?.headers); } async text() { return String(this.body); } async json() { return JSON.parse(this.body); } } globalThis.Response = Response; class Request { constructor(url, init) { this.url = url; this.method = init?.method || "GET"; this._body = init?.body; this.headers = new Headers(init?.headers); } async json() { return typeof this._body === 'string' ? JSON.parse(this._body) : this._body; } async text() { return typeof this._body === 'string' ? this._body : JSON.stringify(this._body); } } globalThis.Request = Request; "#; runtime.execute_script("", preamble.to_string())?; // 2. Execute User Code runtime.execute_script("", code.to_string())?; // 3. Invoke Handler // Double-serialize to prevent JS injection: the outer JSON string is parsed // by JSON.parse() in JS, producing the original value safely. let payload_json = serde_json::to_string(&payload.unwrap_or(serde_json::json!({})))?; let headers_json = serde_json::to_string(&headers)?; let safe_payload = serde_json::to_string(&payload_json)?; let safe_headers = serde_json::to_string(&headers_json)?; let invoke_script = format!(r#" (async () => {{ if (!globalThis._handler) {{ return {{ error: "No handler registered via Deno.serve" }}; }} try {{ const headers = JSON.parse({1}); const body = JSON.parse({0}); const req = new Request("http://localhost", {{ method: "POST", body: typeof body === 'string' ? body : JSON.stringify(body), headers: headers }}); const res = await globalThis._handler(req); const text = await res.text(); const resHeaders = {{}}; if (res.headers && typeof res.headers.forEach === 'function') {{ res.headers.forEach((v, k) => resHeaders[k] = v); }} return {{ result: text, headers: resHeaders, status: res.status }}; }} catch (e) {{ return {{ error: String(e) }}; }} }})() "#, safe_payload, safe_headers); let result_val = runtime.execute_script("", invoke_script)?; #[allow(deprecated)] let result = runtime.resolve_value(result_val).await?; let scope = &mut runtime.handle_scope(); let local = v8::Local::new(scope, result); let deserialized_value: Value = deno_core::serde_v8::from_v8(scope, local)?; let stdout = if let Some(res) = deserialized_value.get("result") { res.as_str().unwrap_or("").to_string() } else { String::new() }; let stderr = if let Some(err) = deserialized_value.get("error") { err.as_str().unwrap_or("Unknown error").to_string() } else { String::new() }; let status = if let Some(s) = deserialized_value.get("status") { s.as_u64().unwrap_or(200) as u16 } else { 200 }; let mut headers = HashMap::new(); if let Some(h) = deserialized_value.get("headers") { if let Some(obj) = h.as_object() { for (k, v) in obj { if let Some(s) = v.as_str() { headers.insert(k.clone(), s.to_string()); } } } } Ok((stdout, stderr, status, headers)) } } #[cfg(test)] mod tests { use serde_json::{json, Value}; /// Validates that the double-serialization technique produces safe JS string /// literals, even when the payload contains characters that could break out /// of a JS template if interpolated naively. #[test] fn test_double_serialize_escapes_js_injection() { let malicious_payload = json!({ "key": "\"); process.exit(1); //" }); let first = serde_json::to_string(&malicious_payload).unwrap(); let double = serde_json::to_string(&first).unwrap(); // The double-serialized value must be a valid JSON string let recovered_first: String = serde_json::from_str(&double).unwrap(); let recovered: Value = serde_json::from_str(&recovered_first).unwrap(); assert_eq!(recovered, malicious_payload); } #[test] fn test_double_serialize_handles_backtick_injection() { let payload = json!({ "attack": "${globalThis.Deno.exit()}" }); let first = serde_json::to_string(&payload).unwrap(); let double = serde_json::to_string(&first).unwrap(); // The value when placed in a JS template literal is still just a string let recovered_first: String = serde_json::from_str(&double).unwrap(); let recovered: Value = serde_json::from_str(&recovered_first).unwrap(); assert_eq!(recovered, payload); } #[test] fn test_double_serialize_handles_empty() { let payload = json!({}); let first = serde_json::to_string(&payload).unwrap(); let double = serde_json::to_string(&first).unwrap(); let recovered_first: String = serde_json::from_str(&double).unwrap(); let recovered: Value = serde_json::from_str(&recovered_first).unwrap(); assert_eq!(recovered, payload); } #[test] fn test_double_serialize_preserves_unicode() { let payload = json!({"emoji": "🔐", "chinese": "安全"}); let first = serde_json::to_string(&payload).unwrap(); let double = serde_json::to_string(&first).unwrap(); let recovered_first: String = serde_json::from_str(&double).unwrap(); let recovered: Value = serde_json::from_str(&recovered_first).unwrap(); assert_eq!(recovered, payload); } }