Ctrl K
ComunidadGuías…

proxy.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/**
 * 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;
}