mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-04 22:32:12 +03:00
fix(dashboard): Playground Compare tab loading + HTTP method guard (#4024)
randomUUID non-HTTPS fallback + static CompareTab import; raw HTTP TRACE->405 method guard wired into dev + standalone servers. Integrated into release/v3.8.27.
This commit is contained in:
82
scripts/dev/http-method-guard.cjs
Normal file
82
scripts/dev/http-method-guard.cjs
Normal file
@@ -0,0 +1,82 @@
|
||||
"use strict";
|
||||
|
||||
const http = require("node:http");
|
||||
|
||||
const HIGH_RISK_METHOD_RULES = [
|
||||
[/^\/api\/auth\/login\/?$/, ["POST"]],
|
||||
[/^\/api\/auth\/logout\/?$/, ["POST"]],
|
||||
[/^\/api\/keys\/?$/, ["GET", "POST"]],
|
||||
[/^\/api\/keys\/[^/]+\/?$/, ["GET", "PATCH", "DELETE"]],
|
||||
];
|
||||
|
||||
let installed = false;
|
||||
|
||||
function getPathname(req) {
|
||||
const rawUrl = typeof req?.url === "string" && req.url ? req.url : "/";
|
||||
try {
|
||||
return new URL(rawUrl, "http://localhost").pathname;
|
||||
} catch {
|
||||
return rawUrl.split("?")[0] || "/";
|
||||
}
|
||||
}
|
||||
|
||||
function getAllowedMethods(pathname) {
|
||||
for (const [pattern, methods] of HIGH_RISK_METHOD_RULES) {
|
||||
if (pattern.test(pathname)) return methods;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getAllowHeader(pathname) {
|
||||
const methods = getAllowedMethods(pathname);
|
||||
return methods ? methods.join(", ") : null;
|
||||
}
|
||||
|
||||
function maybeHandleDisallowedMethod(req, res) {
|
||||
const method = typeof req?.method === "string" ? req.method.toUpperCase() : "";
|
||||
const pathname = getPathname(req);
|
||||
const methods = getAllowedMethods(pathname);
|
||||
if (!methods || method === "OPTIONS" || methods.includes(method)) return false;
|
||||
|
||||
res.statusCode = 405;
|
||||
res.setHeader("Allow", methods.join(", "));
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
code: "METHOD_NOT_ALLOWED",
|
||||
message: `${method || "Method"} is not allowed`,
|
||||
},
|
||||
})
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
function wrapRequestListenerWithMethodGuard(listener) {
|
||||
return function methodGuardRequestHandler(req, res) {
|
||||
if (maybeHandleDisallowedMethod(req, res)) return;
|
||||
return listener.call(this, req, res);
|
||||
};
|
||||
}
|
||||
|
||||
function installHttpMethodGuard() {
|
||||
if (installed) return;
|
||||
installed = true;
|
||||
|
||||
const originalCreateServer = http.createServer.bind(http);
|
||||
http.createServer = function createServerWithMethodGuard(...args) {
|
||||
const lastFnIdx = args.map((arg) => typeof arg === "function").lastIndexOf(true);
|
||||
if (lastFnIdx >= 0) {
|
||||
args[lastFnIdx] = wrapRequestListenerWithMethodGuard(args[lastFnIdx]);
|
||||
}
|
||||
return originalCreateServer(...args);
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAllowHeader,
|
||||
maybeHandleDisallowedMethod,
|
||||
wrapRequestListenerWithMethodGuard,
|
||||
installHttpMethodGuard,
|
||||
};
|
||||
@@ -9,9 +9,12 @@ import { resolveRuntimePorts, withRuntimePortEnv } from "../build/runtime-env.mj
|
||||
import { createOmnirouteWsBridge } from "./v1-ws-bridge.mjs";
|
||||
import { createResponsesWsProxy } from "./responses-ws-proxy.mjs";
|
||||
import { ensurePeerStampToken, stampPeerIp } from "./peer-stamp.mjs";
|
||||
import methodGuard from "./http-method-guard.cjs";
|
||||
import { ensureNativeSqlite } from "./ensure-native-sqlite.mjs";
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
const { maybeHandleDisallowedMethod } = methodGuard;
|
||||
|
||||
// Pre-read DATA_DIR from local .env before bootstrap resolves paths
|
||||
if (!process.env.DATA_DIR) {
|
||||
try {
|
||||
@@ -83,6 +86,7 @@ async function start() {
|
||||
});
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
if (maybeHandleDisallowedMethod(req, res)) return;
|
||||
// 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);
|
||||
|
||||
@@ -3,9 +3,11 @@ import { randomUUID } from "node:crypto";
|
||||
import { createResponsesWsProxy } from "./responses-ws-proxy.mjs";
|
||||
import { ensurePeerStampToken, wrapRequestListenerWithPeerStamp } from "./peer-stamp.mjs";
|
||||
import { maybeHandleWebdav } from "./webdav-handler.mjs";
|
||||
import methodGuard from "./http-method-guard.cjs";
|
||||
|
||||
const originalCreateServer = http.createServer.bind(http);
|
||||
const proxiesByPort = new Map();
|
||||
const { wrapRequestListenerWithMethodGuard } = methodGuard;
|
||||
|
||||
process.env.OMNIROUTE_WS_BRIDGE_SECRET ||= randomUUID();
|
||||
// Per-process secret proving the trusted peer-IP stamp came from this server.
|
||||
@@ -71,9 +73,9 @@ http.createServer = function createServerWithResponsesWs(...args) {
|
||||
// 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) {
|
||||
// WebDAV intercept wraps outermost (first to run), then peer-stamp, then Next.
|
||||
args[lastFnIdx] = wrapRequestListenerWithWebdav(
|
||||
wrapRequestListenerWithPeerStamp(args[lastFnIdx])
|
||||
// Method guard runs before Next because Next 16 rejects TRACE while constructing requests.
|
||||
args[lastFnIdx] = wrapRequestListenerWithMethodGuard(
|
||||
wrapRequestListenerWithWebdav(wrapRequestListenerWithPeerStamp(args[lastFnIdx]))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -89,7 +91,9 @@ http.createServer = function createServerWithResponsesWs(...args) {
|
||||
if (eventName === "request" && typeof listener === "function") {
|
||||
return originalOn(
|
||||
eventName,
|
||||
wrapRequestListenerWithWebdav(wrapRequestListenerWithPeerStamp(listener))
|
||||
wrapRequestListenerWithMethodGuard(
|
||||
wrapRequestListenerWithWebdav(wrapRequestListenerWithPeerStamp(listener))
|
||||
)
|
||||
);
|
||||
}
|
||||
return originalOn(eventName, listener);
|
||||
@@ -102,7 +106,9 @@ http.createServer = function createServerWithResponsesWs(...args) {
|
||||
if (eventName === "request" && typeof listener === "function") {
|
||||
return originalAddListener(
|
||||
eventName,
|
||||
wrapRequestListenerWithWebdav(wrapRequestListenerWithPeerStamp(listener))
|
||||
wrapRequestListenerWithMethodGuard(
|
||||
wrapRequestListenerWithWebdav(wrapRequestListenerWithPeerStamp(listener))
|
||||
)
|
||||
);
|
||||
}
|
||||
return originalAddListener(eventName, listener);
|
||||
|
||||
Reference in New Issue
Block a user