server.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. import compression from "compression";
  2. import express, { NextFunction, Request, Response } from "express";
  3. import { check as portUsed } from "tcp-port-used";
  4. import helmet from "helmet";
  5. import { RateLimiterMemory, RateLimiterRes } from "rate-limiter-flexible";
  6. import k from "kleur";
  7. import cors from "cors";
  8. import { getClientIp } from "request-ip";
  9. import { error } from "@src/error.js";
  10. import { hashStr, respond } from "@src/utils.js";
  11. import { envVarEquals, getEnvVar } from "@src/env.js";
  12. import { rateLimitOptions, rlIgnorePaths } from "@src/constants.js";
  13. import { initRouter } from "@routes/index.js";
  14. import packageJson from "@root/package.json" with { type: "json" };
  15. const app = express();
  16. app.use(cors({
  17. methods: "GET,HEAD,OPTIONS",
  18. origin: "*",
  19. }));
  20. app.use(helmet({
  21. dnsPrefetchControl: true,
  22. }));
  23. app.use(compression({
  24. threshold: 256
  25. }));
  26. app.use(express.json());
  27. if(!envVarEquals("TRUST_PROXY", false, false))
  28. app.enable("trust proxy");
  29. app.disable("x-powered-by");
  30. const rateLimiter = new RateLimiterMemory(rateLimitOptions);
  31. const toks = getEnvVar("AUTH_TOKENS", "stringArray");
  32. const authTokens = new Set<string>(Array.isArray(toks) ? toks : []);
  33. const authRequired = envVarEquals("AUTH_REQUIRED", true, false);
  34. export async function init() {
  35. const port = getEnvVar("HTTP_PORT", "number"),
  36. host = getEnvVar("HTTP_HOST").length < 1 ? "0.0.0.0" : getEnvVar("HTTP_HOST");
  37. if(await portUsed(port))
  38. return error(`TCP port ${port} is already used or invalid`, undefined, true);
  39. // on error
  40. app.use((err: unknown, req: Request, res: Response, next: NextFunction) => {
  41. if(typeof err === "string" || err instanceof Error)
  42. return respond(res, "serverError", `General error in HTTP server: ${err.toString()}`, req?.query?.format ? String(req.query.format) : undefined);
  43. else
  44. return next();
  45. });
  46. // preflight requests
  47. app.options("*", cors());
  48. // if auth tokens are required, check for them before doing anything else
  49. if(authRequired && authTokens.size < 1) {
  50. app.use((req, res, next) => {
  51. if(!req.headers.authorization || !authTokens.has(req.headers.authorization.trim().replace(/^Bearer\s+/i, "")))
  52. return respond(res, 401, {
  53. error: true,
  54. matches: null,
  55. message: "Unauthorized"
  56. }, req?.query?.format ? String(req.query.format) : undefined);
  57. else
  58. return next();
  59. });
  60. }
  61. // rate limiting
  62. app.use(async (req, res, next) => {
  63. const fmt = req?.query?.format ? String(req.query.format) : undefined;
  64. try {
  65. const { authorization } = req.headers;
  66. const authHeader = authorization?.trim().replace(/^Bearer\s+/i, "");
  67. res.setHeader("API-Info", `geniURL v${packageJson.version} (${packageJson.homepage})`);
  68. res.setHeader("API-Version", packageJson.version);
  69. if(authHeader && authTokens.has(authHeader))
  70. return next();
  71. const ipHash = await hashStr(getClientIp(req) ?? "IP_RESOLUTION_ERROR");
  72. if(rlIgnorePaths.every((path) => !req.path.match(new RegExp(`^(/?v\\d+)?${path}`)))) {
  73. rateLimiter.consume(ipHash)
  74. .then((rateLimiterRes: RateLimiterRes) => {
  75. setRateLimitHeaders(res, rateLimiterRes);
  76. return next();
  77. })
  78. .catch((err) => {
  79. if(err instanceof RateLimiterRes) {
  80. setRateLimitHeaders(res, err);
  81. return respond(res, 429, {
  82. error: true,
  83. matches: null,
  84. message: "You are being rate limited. Refer to the Retry-After header for when to try again."
  85. }, fmt);
  86. }
  87. else
  88. return respond(res, 500, {
  89. error: true,
  90. matches: null,
  91. message: `Encountered an internal error${err instanceof Error ? `: ${err.message}` : ""}. Please try again a little later.`,
  92. }, fmt);
  93. });
  94. }
  95. else
  96. return next();
  97. }
  98. catch(e) {
  99. return respond(res, "serverError", {
  100. error: true,
  101. matches: null,
  102. message: `Encountered an internal error while applying rate limiting and checking for authorization${e instanceof Error ? `: ${e.message}` : ""}`
  103. }, fmt);
  104. }
  105. });
  106. const listener = app.listen(port, host, () => {
  107. try {
  108. initRouter(app);
  109. console.log(k.green(`geniURL was successfully started on ${k.blue(k.underline(`http://127.0.0.1:${port}`))}\n`));
  110. }
  111. catch(err) {
  112. error("Error while initializing router", err instanceof Error ? err : undefined, true);
  113. }
  114. });
  115. listener.on("error", (err) => error("General server error", err, true));
  116. }
  117. /** Sets all rate-limiting related headers on a response given a RateLimiterRes object */
  118. function setRateLimitHeaders(res: Response, rateLimiterRes: RateLimiterRes) {
  119. if(rateLimiterRes.remainingPoints === 0)
  120. res.setHeader("Retry-After", Math.ceil(rateLimiterRes.msBeforeNext / 1000));
  121. res.setHeader("X-RateLimit-Limit", rateLimiter.points);
  122. res.setHeader("X-RateLimit-Remaining", rateLimiterRes.remainingPoints);
  123. res.setHeader("X-RateLimit-Reset", new Date(Date.now() + rateLimiterRes.msBeforeNext).toISOString());
  124. }