mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
- fix(dashboard): /dashboard/system/proxy 500'd on EVERY render — #5918 put useProxyBatchOperations(load) before the const load declaration (TDZ ReferenceError, digest 539380095). Hook block moved after load; SSR renderToString regression test added (the exact crash mode). - fix(server): TRACE/TRACK/CONNECT crashed Next's middleware adapter (undici cannot represent them) into a raw 500 on every route — the raw HTTP method guard now answers 405 + Allow up-front (dast-smoke Schemathesis finding on /api/keys/{id}/devices); guard test added. - fix(api): restore Zod validation on the provider-scoped chat route via a .passthrough() schema preserving #5907's relaxed semantics (t06 gate). - docs(openapi): /api/keys/{id}/devices 401 now refs the management error envelope (Schemathesis schema-conformance). - quality: rebaseline i18nUiCoverage 77.5->76.8 (+~1352 new en.json UI keys from the cycle await the async translation workflow; v3.8.39 precedent). - CodeQL: dismissed 2 incomplete-url-substring FPs on unit-test asserts (v3.8.35 precedent) with Hard Rule #14 justifications. - changelog: bullets for the above + 42 i18n mirrors re-synced
104 lines
3.0 KiB
JavaScript
104 lines
3.0 KiB
JavaScript
"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;
|
|
}
|
|
|
|
// Methods undici/fetch cannot represent: Next's middleware adapter throws
|
|
// `TypeError: 'TRACE' HTTP method is unsupported.` while building the Request,
|
|
// which surfaces as an unhandled 500 on EVERY route (caught by the dast-smoke
|
|
// Schemathesis negative tests). Reject them up-front with a clean 405.
|
|
const UNSUPPORTED_METHODS = new Set(["TRACE", "TRACK", "CONNECT"]);
|
|
|
|
function maybeHandleDisallowedMethod(req, res) {
|
|
const method = typeof req?.method === "string" ? req.method.toUpperCase() : "";
|
|
const pathname = getPathname(req);
|
|
if (UNSUPPORTED_METHODS.has(method)) {
|
|
res.statusCode = 405;
|
|
res.setHeader("Allow", getAllowHeader(pathname) || "GET, POST, OPTIONS");
|
|
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} is not allowed`,
|
|
},
|
|
})
|
|
);
|
|
return true;
|
|
}
|
|
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,
|
|
};
|