mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 13:52:09 +03:00
Integrated into release/v3.8.8. Applied review fixes: moved the SELECT 1 into a pingDb() db helper (no raw SQL in route, Hard Rule #5) + the 503 catch no longer leaks err.message (Hard Rule #12). Thanks @herjarsa!
This commit is contained in:
committed by
GitHub
parent
009ad13a91
commit
fd26e601a2
53
scripts/dev/peer-stamp.mjs
Normal file
53
scripts/dev/peer-stamp.mjs
Normal file
@@ -0,0 +1,53 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
/**
|
||||
* Trusted peer-IP stamping for the custom Node HTTP servers.
|
||||
*
|
||||
* The Next.js middleware runtime (proxy.ts → runAuthzPipeline) exposes NO socket
|
||||
* or peer IP — only request headers, ALL of which are client-controlled. The
|
||||
* LOCAL_ONLY route guard (spawn-capable routes) must decide locality from the
|
||||
* real TCP peer, never from the spoofable Host header.
|
||||
*
|
||||
* Our custom servers DO have the real `req.socket.remoteAddress`. They stamp it
|
||||
* into PEER_IP_HEADER as `<token>|<ip>`, where <token> is a per-process secret
|
||||
* (OMNIROUTE_PEER_STAMP_TOKEN). Any client-supplied value of PEER_IP_HEADER is
|
||||
* deleted first, so a remote caller cannot pre-populate it. The middleware
|
||||
* (src/server/authz/policies/management.ts → resolveStampedPeer) trusts the IP
|
||||
* ONLY when the token matches this process's secret; otherwise it fails closed.
|
||||
*
|
||||
* Keep PEER_IP_HEADER in sync with PEER_IP_HEADER in
|
||||
* src/server/authz/headers.ts (the TS side cannot import this .mjs).
|
||||
*/
|
||||
export const PEER_IP_HEADER = "x-omniroute-peer-ip";
|
||||
|
||||
/** Generate (once) and return the per-process stamp token, persisting it in env
|
||||
* so the middleware running in the same process reads the identical value. */
|
||||
export function ensurePeerStampToken() {
|
||||
process.env.OMNIROUTE_PEER_STAMP_TOKEN ||= randomUUID();
|
||||
return process.env.OMNIROUTE_PEER_STAMP_TOKEN;
|
||||
}
|
||||
|
||||
/** Strip any client-supplied PEER_IP_HEADER and stamp the real TCP peer IP,
|
||||
* token-prefixed. Never throws — a stamping failure must not block a request
|
||||
* (it degrades to "locality unknown" → fail closed in the middleware). */
|
||||
export function stampPeerIp(req) {
|
||||
try {
|
||||
if (!req || !req.headers) return;
|
||||
// Node lowercases incoming header names; delete kills any client value.
|
||||
delete req.headers[PEER_IP_HEADER];
|
||||
const ip = req.socket && req.socket.remoteAddress;
|
||||
if (ip) {
|
||||
req.headers[PEER_IP_HEADER] = `${ensurePeerStampToken()}|${ip}`;
|
||||
}
|
||||
} catch {
|
||||
/* never block a request on peer stamping */
|
||||
}
|
||||
}
|
||||
|
||||
/** Wrap a Node request listener so every request is peer-stamped first. */
|
||||
export function wrapRequestListenerWithPeerStamp(listener) {
|
||||
return function peerStampingRequestHandler(req, res) {
|
||||
stampPeerIp(req);
|
||||
return listener.call(this, req, res);
|
||||
};
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { bootstrapEnv } from "../build/bootstrap-env.mjs";
|
||||
import { resolveRuntimePorts, withRuntimePortEnv } from "../build/runtime-env.mjs";
|
||||
import { createOmnirouteWsBridge } from "./v1-ws-bridge.mjs";
|
||||
import { createResponsesWsProxy } from "./responses-ws-proxy.mjs";
|
||||
import { ensurePeerStampToken, stampPeerIp } from "./peer-stamp.mjs";
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
// Pre-read DATA_DIR from local .env before bootstrap resolves paths
|
||||
@@ -49,6 +50,9 @@ const { dashboardPort } = runtimePorts;
|
||||
const hostname = process.env.HOST || "0.0.0.0";
|
||||
const useTurbopack = dev && mergedEnv.OMNIROUTE_USE_TURBOPACK === "1";
|
||||
process.env.OMNIROUTE_WS_BRIDGE_SECRET ||= randomUUID();
|
||||
// Per-process secret used to prove the trusted peer-IP stamp came from this
|
||||
// server (read by the authz middleware in the same process). See peer-stamp.mjs.
|
||||
ensurePeerStampToken();
|
||||
|
||||
const nextApp = next({
|
||||
dev,
|
||||
@@ -71,7 +75,12 @@ async function start() {
|
||||
baseUrl: `http://127.0.0.1:${dashboardPort}`,
|
||||
});
|
||||
|
||||
const server = http.createServer((req, res) => requestHandler(req, res));
|
||||
const server = http.createServer((req, res) => {
|
||||
// Stamp the real TCP peer IP before Next sees the request, so the authz
|
||||
// middleware can decide LOCAL_ONLY locality without trusting the Host header.
|
||||
stampPeerIp(req);
|
||||
return requestHandler(req, res);
|
||||
});
|
||||
server.on("upgrade", async (req, socket, head) => {
|
||||
try {
|
||||
const responsesWsHandled = await responsesWsProxy.handleUpgrade(req, socket, head);
|
||||
|
||||
@@ -1,16 +1,34 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { existsSync } from "node:fs";
|
||||
import {
|
||||
resolveRuntimePorts,
|
||||
withRuntimePortEnv,
|
||||
resolveMaxOldSpaceMb,
|
||||
spawnWithForwardedSignals,
|
||||
} from "../build/runtime-env.mjs";
|
||||
import { bootstrapEnv } from "../build/bootstrap-env.mjs";
|
||||
|
||||
const env = bootstrapEnv();
|
||||
const runtimePorts = resolveRuntimePorts(env);
|
||||
const childEnv = withRuntimePortEnv(env, runtimePorts);
|
||||
|
||||
spawnWithForwardedSignals("node", ["server.js"], {
|
||||
// #2939: honor OMNIROUTE_MEMORY_MB (default 512), the same knob
|
||||
// `omniroute serve` uses, so Docker users can control the server heap under
|
||||
// load / large SQLite DBs. A trailing --max-old-space-size wins, so this
|
||||
// overrides the image fallback without clobbering any other NODE_OPTIONS flags.
|
||||
const maxOldSpaceMb = resolveMaxOldSpaceMb(childEnv.OMNIROUTE_MEMORY_MB);
|
||||
childEnv.NODE_OPTIONS =
|
||||
`${childEnv.NODE_OPTIONS || ""} --max-old-space-size=${maxOldSpaceMb}`.trim();
|
||||
|
||||
// Prefer the WS-aware wrapper (server-ws.mjs) over the bare Next standalone
|
||||
// server.js: it installs the trusted peer-IP stamp (scripts/dev/peer-stamp.mjs)
|
||||
// that the authz middleware needs to allow loopback/LAN access to LOCAL_ONLY
|
||||
// routes. Falling back to server.js fails CLOSED (every LOCAL_ONLY request 403s)
|
||||
// rather than trusting the spoofable Host header.
|
||||
const entry = existsSync("server-ws.mjs") ? "server-ws.mjs" : "server.js";
|
||||
|
||||
spawnWithForwardedSignals("node", [entry], {
|
||||
stdio: "inherit",
|
||||
env: withRuntimePortEnv(env, runtimePorts),
|
||||
env: childEnv,
|
||||
});
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import http from "node:http";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { createResponsesWsProxy } from "./responses-ws-proxy.mjs";
|
||||
import { ensurePeerStampToken, wrapRequestListenerWithPeerStamp } from "./peer-stamp.mjs";
|
||||
|
||||
const originalCreateServer = http.createServer.bind(http);
|
||||
const proxiesByPort = new Map();
|
||||
|
||||
process.env.OMNIROUTE_WS_BRIDGE_SECRET ||= randomUUID();
|
||||
// Per-process secret proving the trusted peer-IP stamp came from this server.
|
||||
ensurePeerStampToken();
|
||||
|
||||
function getPort(server) {
|
||||
const address = server.address?.();
|
||||
@@ -46,6 +49,13 @@ function wrapUpgradeListener(server, listener) {
|
||||
}
|
||||
|
||||
http.createServer = function createServerWithResponsesWs(...args) {
|
||||
// Next's standalone server.js may pass its request listener directly to
|
||||
// createServer; wrap it so the real TCP peer IP is stamped before Next runs.
|
||||
const lastFnIdx = args.map((a) => typeof a === "function").lastIndexOf(true);
|
||||
if (lastFnIdx >= 0) {
|
||||
args[lastFnIdx] = wrapRequestListenerWithPeerStamp(args[lastFnIdx]);
|
||||
}
|
||||
|
||||
const server = originalCreateServer(...args);
|
||||
const originalOn = server.on.bind(server);
|
||||
const originalAddListener = server.addListener.bind(server);
|
||||
@@ -54,6 +64,10 @@ http.createServer = function createServerWithResponsesWs(...args) {
|
||||
if (eventName === "upgrade" && typeof listener === "function") {
|
||||
return originalOn(eventName, wrapUpgradeListener(server, listener));
|
||||
}
|
||||
// …or it may attach the handler via server.on("request"): wrap that too.
|
||||
if (eventName === "request" && typeof listener === "function") {
|
||||
return originalOn(eventName, wrapRequestListenerWithPeerStamp(listener));
|
||||
}
|
||||
return originalOn(eventName, listener);
|
||||
};
|
||||
|
||||
@@ -61,6 +75,9 @@ http.createServer = function createServerWithResponsesWs(...args) {
|
||||
if (eventName === "upgrade" && typeof listener === "function") {
|
||||
return originalAddListener(eventName, wrapUpgradeListener(server, listener));
|
||||
}
|
||||
if (eventName === "request" && typeof listener === "function") {
|
||||
return originalAddListener(eventName, wrapRequestListenerWithPeerStamp(listener));
|
||||
}
|
||||
return originalAddListener(eventName, listener);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user