File size: 2,294 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
"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,
};