Files
2026-07-13 13:39:12 +08:00

105 lines
3.1 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"]],
[/^\/api\/keys\/[^/]+\/devices\/?$/, ["GET"]],
];
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,
};