79 lines
2.7 KiB
JavaScript
79 lines
2.7 KiB
JavaScript
// SPDX-FileCopyrightText: 2026 Milovann Yanatchkov
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
// proxy/node.mjs — the Node entry point for the Studio forge proxy.
|
|
//
|
|
// Bridges Node's http request/response to the Web Fetch handler in
|
|
// `proxy.mjs`, and starts a server when run directly:
|
|
//
|
|
// FORGE_ALLOWED_HOSTS=artefacts.bimr.net,bimr.net node proxy/node.mjs
|
|
//
|
|
// It is also imported by the Vite dev server to mount the same handler, so
|
|
// development and production share one implementation.
|
|
|
|
import http from "node:http";
|
|
import { Readable } from "node:stream";
|
|
import { pathToFileURL } from "node:url";
|
|
import { handleFetch } from "./proxy.mjs";
|
|
|
|
/** Build a Web Request from a Node incoming message. */
|
|
export function toWebRequest(req) {
|
|
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
const headers = new Headers();
|
|
for (const [name, value] of Object.entries(req.headers)) {
|
|
if (value === undefined) continue;
|
|
if (Array.isArray(value)) for (const entry of value) headers.append(name, entry);
|
|
else headers.set(name, value);
|
|
}
|
|
|
|
const method = req.method ?? "GET";
|
|
const init = { method, headers };
|
|
if (method !== "GET" && method !== "HEAD") {
|
|
init.body = Readable.toWeb(req);
|
|
init.duplex = "half";
|
|
}
|
|
return new Request(url, init);
|
|
}
|
|
|
|
/** Write a Web Response to a Node server response. */
|
|
export async function sendWebResponse(res, response) {
|
|
res.statusCode = response.status;
|
|
if (response.statusText) res.statusMessage = response.statusText;
|
|
response.headers.forEach((value, name) => res.setHeader(name, value));
|
|
if (response.body) Readable.fromWeb(response.body).pipe(res);
|
|
else res.end();
|
|
}
|
|
|
|
/** Handle one Node request/response pair through the proxy handler. */
|
|
export async function handleNode(req, res) {
|
|
try {
|
|
const response = await handleFetch(toWebRequest(req));
|
|
await sendWebResponse(res, response);
|
|
} catch (err) {
|
|
if (!res.headersSent) res.statusCode = 502;
|
|
res.end(`proxy error: ${err?.message ?? err}`);
|
|
}
|
|
}
|
|
|
|
export function startServer({
|
|
port = Number(process.env.PORT ?? 8787),
|
|
host = process.env.HOST ?? "127.0.0.1",
|
|
} = {}) {
|
|
const server = http.createServer((req, res) => {
|
|
void handleNode(req, res);
|
|
});
|
|
server.listen(port, host, () => {
|
|
console.log(`studio forge proxy: http://${host}:${port}/forge-proxy/`);
|
|
if (!process.env.FORGE_ALLOWED_HOSTS && process.env.FORGE_ALLOW_ANY !== "1") {
|
|
console.warn(
|
|
"studio forge proxy: FORGE_ALLOWED_HOSTS is empty — all targets are denied. " +
|
|
"Set it, or FORGE_ALLOW_ANY=1 for development.",
|
|
);
|
|
}
|
|
});
|
|
return server;
|
|
}
|
|
|
|
const invokedDirectly =
|
|
process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
if (invokedDirectly) startServer();
|