chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
"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,
|
||||
};
|
||||
Reference in New Issue
Block a user