server.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  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", true, 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. export async function init() {
  34. const port = getEnvVar("HTTP_PORT", "number"),
  35. host = getEnvVar("HTTP_HOST").length < 1 ? "0.0.0.0" : getEnvVar("HTTP_HOST");
  36. if(await portUsed(port))
  37. return error(`TCP port ${port} is already used or invalid`, undefined, true);
  38. // on error
  39. app.use((err: unknown, req: Request, res: Response, next: NextFunction) => {
  40. if(typeof err === "string" || err instanceof Error)
  41. return respond(res, "serverError", `General error in HTTP server: ${err.toString()}`, req?.query?.format ? String(req.query.format) : undefined);
  42. else
  43. return next();
  44. });
  45. // preflight requests
  46. app.options("*", cors());
  47. // rate limiting
  48. app.use(async (req, res, next) => {
  49. const fmt = req?.query?.format ? String(req.query.format) : undefined;
  50. try {
  51. const { authorization } = req.headers;
  52. const authHeader = authorization?.trim().replace(/^Bearer\s+/i, "");
  53. res.setHeader("API-Info", `geniURL v${packageJson.version} (${packageJson.homepage})`);
  54. res.setHeader("API-Version", packageJson.version);
  55. if(authHeader && authTokens.has(authHeader))
  56. return next();
  57. const ipHash = await hashStr(getClientIp(req) ?? "IP_RESOLUTION_ERROR");
  58. if(rlIgnorePaths.every((path) => !req.path.match(new RegExp(`^(/?v\\d+)?${path}`)))) {
  59. rateLimiter.consume(ipHash)
  60. .then((rateLimiterRes: RateLimiterRes) => {
  61. setRateLimitHeaders(res, rateLimiterRes);
  62. return next();
  63. })
  64. .catch((err) => {
  65. if(err instanceof RateLimiterRes) {
  66. setRateLimitHeaders(res, err);
  67. return respond(res, 429, {
  68. error: true,
  69. matches: null,
  70. message: "You are being rate limited. Refer to the Retry-After header for when to try again."
  71. }, fmt);
  72. }
  73. else
  74. return respond(res, 500, {
  75. error: true,
  76. matches: null,
  77. message: `Encountered an internal error${err instanceof Error ? `: ${err.message}` : ""}. Please try again a little later.`,
  78. }, fmt);
  79. });
  80. }
  81. else
  82. return next();
  83. }
  84. catch(e) {
  85. return respond(res, "serverError", {
  86. error: true,
  87. matches: null,
  88. message: `Encountered an internal error while applying rate limiting and checking for authorization${e instanceof Error ? `: ${e.message}` : ""}`
  89. }, fmt);
  90. }
  91. });
  92. const listener = app.listen(port, host, () => {
  93. registerRoutes();
  94. console.log(k.green(`geniURL is listening on ${host}:${port}\n`));
  95. });
  96. listener.on("error", (err) => error("General server error", err, true));
  97. }
  98. function registerRoutes() {
  99. try {
  100. initRouter(app);
  101. }
  102. catch(err) {
  103. error("Error while initializing router", err instanceof Error ? err : undefined, true);
  104. }
  105. }
  106. /** Sets all rate-limiting related headers on a response given a RateLimiterRes object */
  107. function setRateLimitHeaders(res: Response, rateLimiterRes: RateLimiterRes) {
  108. if(rateLimiterRes.remainingPoints === 0)
  109. res.setHeader("Retry-After", Math.ceil(rateLimiterRes.msBeforeNext / 1000));
  110. res.setHeader("X-RateLimit-Limit", rateLimiter.points);
  111. res.setHeader("X-RateLimit-Remaining", rateLimiterRes.remainingPoints);
  112. res.setHeader("X-RateLimit-Reset", new Date(Date.now() + rateLimiterRes.msBeforeNext).toISOString());
  113. }