server.ts 3.6 KB

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