/** * proxy.js – zero-dependency worker-based HTTP/WS forwarder */ const { Worker, isMainThread, parentPort, workerData, } = require("worker_threads"); const http = require("http"); const https = require("https"); const { URL } = require("url"); const fs = require("fs"); const path = require("path"); /* ─────────────────── configuration (main thread only) ─────────────────── */ const LISTEN_HOST = "localhost"; if (isMainThread) { // Stand-alone mode: fork the worker and pass through the env as-is const w = new Worker(__filename, { workerData: { targetOrigin: process.env.TARGET_URL, // may be undefined }, }); w.on("message", (m) => console.log("[proxy-worker]", m)); w.on("error", (e) => console.error("[proxy-worker] error:", e)); w.on("exit", (c) => console.log("[proxy-worker] exited", c)); console.log("proxy worker launching …"); return; // do not execute the rest of the file in the main thread } /* ──────────────────────────── worker code ─────────────────────────────── */ const LISTEN_PORT = process.env.LISTEN_PORT || workerData.port; let rememberedOrigin = null; // e.g. "http://localhost:5173" /* ---------- pre-configure rememberedOrigin from env or workerData ------- */ { const fixed = process.env.TARGET_URL || workerData?.targetOrigin; if (fixed) { try { rememberedOrigin = new URL(fixed).origin; parentPort?.postMessage( `[proxy-worker] fixed upstream: ${rememberedOrigin}`, ); } catch { throw new Error( `Invalid TARGET_URL "${fixed}". Must be absolute http/https URL.`, ); } } } /* ---------- optional resources for HTML injection ---------------------- */ let stacktraceJsContent = null; let dyadShimContent = null; try { const stackTraceLibPath = path.join( __dirname, "..", "node_modules", "stacktrace-js", "dist", "stacktrace.min.js", ); stacktraceJsContent = fs.readFileSync(stackTraceLibPath, "utf-8"); parentPort?.postMessage("[proxy-worker] stacktrace.js loaded."); } catch (error) { parentPort?.postMessage( `[proxy-worker] Failed to read stacktrace.js: ${error.message}`, ); } try { const dyadShimPath = path.join(__dirname, "dyad-shim.js"); dyadShimContent = fs.readFileSync(dyadShimPath, "utf-8"); parentPort?.postMessage("[proxy-worker] dyad-shim.js loaded."); } catch (error) { parentPort?.postMessage( `[proxy-worker] Failed to read dyad-shim.js: ${error.message}`, ); } /* ---------------------- helper: need to inject? ------------------------ */ function needsInjection(pathname) { return pathname.endsWith("index.html") || pathname === "/"; } function injectHTML(buf) { let txt = buf.toString("utf8"); // These are strings that were used since the first version of the dyad shim. // If the dyad shim is used from legacy apps which came pre-baked with the shim // as a vite plugin, then do not inject the shim twice to avoid weird behaviors. if (txt.includes("window-error") && txt.includes("unhandled-rejection")) { return buf; } const scripts = []; if (stacktraceJsContent) scripts.push(``); else scripts.push( '', ); if (dyadShimContent) scripts.push(``); else scripts.push( '', ); const allScripts = scripts.join("\n"); const headRegex = /]*>/i; if (headRegex.test(txt)) { txt = txt.replace(headRegex, `$&\n${allScripts}`); } else { txt = allScripts + "\n" + txt; parentPort?.postMessage( "[proxy-worker] Warning: tag not found – scripts prepended.", ); } return Buffer.from(txt, "utf8"); } /* ---------------- helper: build upstream URL from request -------------- */ function buildTargetURL(clientReq) { // Support the old "?url=" mechanism const parsedLocal = new URL(clientReq.url, `http://${LISTEN_HOST}`); const urlParam = parsedLocal.searchParams.get("url"); if (urlParam) { const abs = new URL(urlParam); if (!/^https?:$/.test(abs.protocol)) throw new Error("only http/https targets allowed"); rememberedOrigin = abs.origin; // remember for later return abs; } if (!rememberedOrigin) throw new Error( "No upstream configured. Use ?url=… once or set TARGET_URL env var.", ); // Forward to the remembered origin keeping path & query return new URL(clientReq.url, rememberedOrigin); } /* ----------------------------------------------------------------------- */ /* 1. Plain HTTP request / response */ /* ----------------------------------------------------------------------- */ const server = http.createServer((clientReq, clientRes) => { let target; try { target = buildTargetURL(clientReq); } catch (err) { clientRes.writeHead(400, { "content-type": "text/plain" }); return void clientRes.end("Bad request: " + err.message); } const isTLS = target.protocol === "https:"; const lib = isTLS ? https : http; /* Copy request headers but rewrite Host / Origin / Referer */ const headers = { ...clientReq.headers, host: target.host }; if (headers.origin) headers.origin = target.origin; if (headers.referer) { try { const ref = new URL(headers.referer); headers.referer = target.origin + ref.pathname + ref.search; } catch { delete headers.referer; } } if (needsInjection) { // Request uncompressed content from upstream delete headers["accept-encoding"]; } if (headers["if-none-match"] && needsInjection(target.pathname)) delete headers["if-none-match"]; const upOpts = { protocol: target.protocol, hostname: target.hostname, port: target.port || (isTLS ? 443 : 80), path: target.pathname + target.search, method: clientReq.method, headers, }; const upReq = lib.request(upOpts, (upRes) => { const inject = needsInjection(target.pathname); if (!inject) { clientRes.writeHead(upRes.statusCode, upRes.headers); return void upRes.pipe(clientRes); } const chunks = []; upRes.on("data", (c) => chunks.push(c)); upRes.on("end", () => { try { const merged = Buffer.concat(chunks); const patched = injectHTML(merged); const hdrs = { ...upRes.headers, "content-length": Buffer.byteLength(patched), }; // If we injected content, it's no longer encoded in the original way delete hdrs["content-encoding"]; // Also, remove ETag as content has changed delete hdrs["etag"]; clientRes.writeHead(upRes.statusCode, hdrs); clientRes.end(patched); } catch (e) { clientRes.writeHead(500, { "content-type": "text/plain" }); clientRes.end("Injection failed: " + e.message); } }); }); clientReq.pipe(upReq); upReq.on("error", (e) => { clientRes.writeHead(502, { "content-type": "text/plain" }); clientRes.end("Upstream error: " + e.message); }); }); /* ----------------------------------------------------------------------- */ /* 2. WebSocket / generic Upgrade tunnelling */ /* ----------------------------------------------------------------------- */ server.on("upgrade", (req, socket, _head) => { let target; try { target = buildTargetURL(req); } catch (err) { socket.write("HTTP/1.1 400 Bad Request\r\n\r\n" + err.message); return socket.destroy(); } const isTLS = target.protocol === "https:"; const headers = { ...req.headers, host: target.host }; if (headers.origin) headers.origin = target.origin; const upReq = (isTLS ? https : http).request({ protocol: target.protocol, hostname: target.hostname, port: target.port || (isTLS ? 443 : 80), path: target.pathname + target.search, method: "GET", headers, }); upReq.on("upgrade", (upRes, upSocket, upHead) => { socket.write( "HTTP/1.1 101 Switching Protocols\r\n" + Object.entries(upRes.headers) .map(([k, v]) => `${k}: ${v}`) .join("\r\n") + "\r\n\r\n", ); if (upHead && upHead.length) socket.write(upHead); upSocket.pipe(socket).pipe(upSocket); }); upReq.on("error", () => socket.destroy()); upReq.end(); }); /* ----------------------------------------------------------------------- */ server.listen(LISTEN_PORT, LISTEN_HOST, () => { parentPort?.postMessage( `proxy-server-start url=http://${LISTEN_HOST}:${LISTEN_PORT}`, ); });