Files
moreminimore-vibe/worker/proxy_server.js

281 lines
8.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* proxy.js zero-dependency worker-based HTTP/WS forwarder
*/
const { 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");
/* ──────────────────────────── worker code ─────────────────────────────── */
const LISTEN_HOST = "localhost";
const LISTEN_PORT = workerData.port;
let rememberedOrigin = null; // e.g. "http://localhost:5173"
/* ---------- pre-configure rememberedOrigin from workerData ------- */
{
const fixed = workerData?.targetOrigin;
if (fixed) {
try {
rememberedOrigin = new URL(fixed).origin;
parentPort?.postMessage(
`[proxy-worker] fixed upstream: ${rememberedOrigin}`,
);
} catch {
throw new Error(
`Invalid target origin "${fixed}". Must be absolute http/https URL.`,
);
}
}
}
/* ---------- optional resources for HTML injection ---------------------- */
let stacktraceJsContent = null;
let dyadShimContent = null;
let dyadComponentSelectorClientContent = 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}`,
);
}
try {
const dyadComponentSelectorClientPath = path.join(
__dirname,
"dyad-component-selector-client.js",
);
dyadComponentSelectorClientContent = fs.readFileSync(
dyadComponentSelectorClientPath,
"utf-8",
);
parentPort?.postMessage(
"[proxy-worker] dyad-component-selector-client.js loaded.",
);
} catch (error) {
parentPort?.postMessage(
`[proxy-worker] Failed to read dyad-component-selector-client.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.
const legacyAppWithShim =
txt.includes("window-error") && txt.includes("unhandled-rejection");
const scripts = [];
if (!legacyAppWithShim) {
if (stacktraceJsContent) {
scripts.push(`<script>${stacktraceJsContent}</script>`);
} else {
scripts.push(
'<script>console.warn("[proxy-worker] stacktrace.js was not injected.");</script>',
);
}
if (dyadShimContent) {
scripts.push(`<script>${dyadShimContent}</script>`);
} else {
scripts.push(
'<script>console.warn("[proxy-worker] dyad shim was not injected.");</script>',
);
}
}
if (dyadComponentSelectorClientContent) {
scripts.push(`<script>${dyadComponentSelectorClientContent}</script>`);
} else {
scripts.push(
'<script>console.warn("[proxy-worker] dyad component selector client was not injected.");</script>',
);
}
const allScripts = scripts.join("\n");
const headRegex = /<head[^>]*>/i;
if (headRegex.test(txt)) {
txt = txt.replace(headRegex, `$&\n${allScripts}`);
} else {
txt = allScripts + "\n" + txt;
parentPort?.postMessage(
"[proxy-worker] Warning: <head> tag not found scripts prepended.",
);
}
return Buffer.from(txt, "utf8");
}
/* ---------------- helper: build upstream URL from request -------------- */
function buildTargetURL(clientReq) {
if (!rememberedOrigin) throw new Error("No upstream configured.");
// 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}`,
);
});