fix(dashboard): use lightweight ping endpoint for MaintenanceBanner (fixes #3040) (#3043)

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:
Hernan Javier Ardila Sanchez
2026-06-01 19:30:17 +02:00
committed by GitHub
parent 009ad13a91
commit fd26e601a2
1266 changed files with 153840 additions and 50869 deletions

View 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);
};
}

View File

@@ -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);

View File

@@ -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,
});

View File

@@ -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);
};