proxy.mjs
Rama main · réplica de lectura del repositorio original.
/**
* One public port for the whole of Indy.
*
* Next and the document server each listen on loopback. This forwards the
* /collab websocket to the document server and everything else to Next, so an
* install needs one port, one TLS certificate and one address. The document
* server's HTTP endpoints are for the app alone and are not reachable here.
*/
import http from "node:http";
import net from "node:net";
const COLLAB_PREFIX = "/collab";
/** Plain HTTP to /collab is the document server's internal API: never from outside. */
function isCollab(url) {
return url === COLLAB_PREFIX || url.startsWith(`${COLLAB_PREFIX}/`) || url.startsWith(`${COLLAB_PREFIX}?`);
}
function target(url, ports) {
if (url === COLLAB_PREFIX || url.startsWith(`${COLLAB_PREFIX}/`) || url.startsWith(`${COLLAB_PREFIX}?`)) {
return { port: ports.collab, path: url.slice(COLLAB_PREFIX.length) || "/" };
}
return { port: ports.app, path: url };
}
function forwardedHeaders(req) {
const headers = { ...req.headers };
const remote = req.socket.remoteAddress ?? "";
headers["x-forwarded-for"] = headers["x-forwarded-for"] ? `${headers["x-forwarded-for"]}, ${remote}` : remote;
// The host is the one the request was sent to; a client-supplied header
// naming another would otherwise steer the addresses Indy hands out.
if (headers.host) headers["x-forwarded-host"] = headers.host;
if (!headers["x-forwarded-proto"]) headers["x-forwarded-proto"] = req.socket.encrypted ? "https" : "http";
return headers;
}
/** Headers that describe one connection, not the request, and must not be passed on. */
const HOP = ["connection", "keep-alive", "proxy-connection", "transfer-encoding", "te", "trailer", "upgrade"];
function withoutHopHeaders(headers) {
const out = { ...headers };
for (const name of HOP) delete out[name];
return out;
}
export function startProxy({ port, host = "0.0.0.0", appPort, collabPort, log = console.log }) {
const ports = { app: appPort, collab: collabPort };
const server = http.createServer((req, res) => {
if (isCollab(req.url ?? "/")) {
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
res.end("Not found.");
return;
}
const { port: to, path } = target(req.url ?? "/", ports);
let reply = null;
const upstream = http.request(
// A fresh loopback connection per request. Reusing one after a streamed
// (event-stream) response let the next request land on a socket Next had
// finished with, and it came back as an empty 400.
{ host: "127.0.0.1", port: to, method: req.method, path, headers: withoutHopHeaders(forwardedHeaders(req)), agent: false },
(answer) => {
reply = answer;
// The loopback hop closes after each request; the client's connection
// to us should not, so its hop headers stay here.
res.writeHead(answer.statusCode ?? 502, answer.statusMessage, withoutHopHeaders(answer.headers));
answer.pipe(res);
},
);
upstream.on("error", (err) => {
if (!res.headersSent) {
res.writeHead(502, { "content-type": "text/plain; charset=utf-8" });
res.end("Indy is starting up. Try again in a moment.");
} else res.destroy(err);
});
// When the browser goes away, so does the request to Next. Otherwise a
// closed tab's live-update stream keeps running there indefinitely.
res.on("close", () => {
if (!res.writableFinished) {
upstream.destroy();
reply?.destroy();
}
});
// Server-sent events and long polls must not be buffered or timed out here.
req.pipe(upstream);
});
server.on("upgrade", (req, socket, head) => {
const { port: to, path } = target(req.url ?? "/", ports);
const upstream = net.connect(to, "127.0.0.1", () => {
const headers = forwardedHeaders(req);
const lines = [`${req.method} ${path} HTTP/${req.httpVersion}`];
for (const [key, value] of Object.entries(headers)) {
for (const v of Array.isArray(value) ? value : [value]) lines.push(`${key}: ${v}`);
}
upstream.write(`${lines.join("\r\n")}\r\n\r\n`);
if (head?.length) upstream.write(head);
upstream.pipe(socket);
socket.pipe(upstream);
});
const close = () => {
upstream.destroy();
socket.destroy();
};
upstream.on("error", close);
socket.on("error", close);
});
// A request Node cannot parse never reaches a handler; say why instead of
// answering an empty 400.
server.on("clientError", (err, socket) => {
log(`[indy] rejected a malformed request: ${err.code ?? ""} ${err.message}`);
if (socket.writable) socket.end("HTTP/1.1 400 Bad Request\r\nconnection: close\r\n\r\n");
});
server.keepAliveTimeout = 65_000;
server.requestTimeout = 0;
server.listen(port, host, () => log(`[indy] listening on ${host}:${port} (app ${appPort}, documents ${collabPort})`));
return server;
}