server.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  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 packageJson from "../package.json";
  9. import { error } from "./error";
  10. import { initRouter } from "./routes";
  11. import { respond } from "./utils";
  12. import { rateLimitOptions } from "./constants";
  13. const { env } = process;
  14. const app = express();
  15. app.use(cors({
  16. methods: "GET,HEAD,OPTIONS",
  17. origin: "*",
  18. }));
  19. app.use(helmet({
  20. dnsPrefetchControl: true,
  21. }));
  22. app.use(compression({
  23. threshold: 256
  24. }));
  25. app.use(express.json());
  26. if(env.NODE_ENV?.toLowerCase() === "production")
  27. app.enable("trust proxy");
  28. app.disable("x-powered-by");
  29. const rateLimiter = new RateLimiterMemory(rateLimitOptions);
  30. const authTokens = getAuthTokens();
  31. export async function init() {
  32. const port = parseInt(String(env.HTTP_PORT ?? "").trim());
  33. const hostRaw = String(env.HTTP_HOST ?? "").trim();
  34. const host = hostRaw.length < 1 ? "0.0.0.0" : hostRaw;
  35. if(await portUsed(port))
  36. return error(`TCP port ${port} is already used or invalid`, undefined, true);
  37. // on error
  38. app.use((err: unknown, req: Request, res: Response, next: NextFunction) => {
  39. if(typeof err === "string" || err instanceof Error)
  40. return respond(res, "serverError", `General error in HTTP server: ${err.toString()}`, req?.query?.format ? String(req.query.format) : undefined);
  41. else
  42. return next();
  43. });
  44. // preflight requests
  45. app.options("*", cors());
  46. // rate limiting
  47. app.use(async (req, res, next) => {
  48. const fmt = req?.query?.format ? String(req.query.format) : undefined;
  49. const { authorization } = req.headers;
  50. const authHeader = authorization?.startsWith("Bearer ") ? authorization.substring(7) : authorization;
  51. res.setHeader("API-Info", `geniURL v${packageJson.version} (${packageJson.homepage})`);
  52. if(authHeader && authTokens.has(authHeader))
  53. return next();
  54. const setRateLimitHeaders = (rateLimiterRes: RateLimiterRes) => {
  55. if(rateLimiterRes.remainingPoints === 0)
  56. res.setHeader("Retry-After", Math.ceil(rateLimiterRes.msBeforeNext / 1000));
  57. res.setHeader("X-RateLimit-Limit", rateLimiter.points);
  58. res.setHeader("X-RateLimit-Remaining", rateLimiterRes.remainingPoints);
  59. res.setHeader("X-RateLimit-Reset", new Date(Date.now() + rateLimiterRes.msBeforeNext).toISOString());
  60. };
  61. rateLimiter.consume(req.ip)
  62. .then((rateLimiterRes: RateLimiterRes) => {
  63. setRateLimitHeaders(rateLimiterRes);
  64. return next();
  65. })
  66. .catch((err) => {
  67. if(err instanceof RateLimiterRes) {
  68. setRateLimitHeaders(err);
  69. return respond(res, 429, { error: true, matches: null, message: "You are being rate limited. Refer to the Retry-After header for when to try again." }, fmt);
  70. }
  71. else return respond(res, 500, { message: "Encountered an internal error. Please try again a little later." }, fmt);
  72. });
  73. });
  74. const listener = app.listen(port, host, () => {
  75. registerRoutes();
  76. console.log(k.green(`Listening on ${host}:${port}`));
  77. });
  78. listener.on("error", (err) => error("General server error", err, true));
  79. }
  80. function registerRoutes()
  81. {
  82. try {
  83. initRouter(app);
  84. }
  85. catch(err) {
  86. error("Error while initializing router", err instanceof Error ? err : undefined, true);
  87. }
  88. }
  89. function getAuthTokens() {
  90. const envVal = process.env["AUTH_TOKENS"];
  91. let tokens: string[] = [];
  92. if(!envVal || envVal.length === 0)
  93. tokens = [];
  94. else
  95. tokens = envVal.split(/,/g);
  96. return new Set<string>(tokens);
  97. }