105 lines
3.1 KiB
JavaScript
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,
|
|
};
|