123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147 |
- import compression from "compression";
- import express, { NextFunction, Request, Response } from "express";
- import { check as portUsed } from "tcp-port-used";
- import helmet from "helmet";
- import { RateLimiterMemory, RateLimiterRes } from "rate-limiter-flexible";
- import k from "kleur";
- import cors from "cors";
- import { getClientIp } from "request-ip";
- import { error } from "@src/error.js";
- import { hashStr, respond } from "@src/utils.js";
- import { envVarEquals, getEnvVar } from "@src/env.js";
- import { rateLimitOptions, rlIgnorePaths } from "@src/constants.js";
- import { initRouter } from "@routes/index.js";
- import packageJson from "@root/package.json" with { type: "json" };
- const app = express();
- app.use(cors({
- methods: "GET,HEAD,OPTIONS",
- origin: "*",
- }));
- app.use(helmet({
- dnsPrefetchControl: true,
- }));
- app.use(compression({
- threshold: 256
- }));
- app.use(express.json());
- if(!envVarEquals("TRUST_PROXY", false, false))
- app.enable("trust proxy");
- app.disable("x-powered-by");
- const rateLimiter = new RateLimiterMemory(rateLimitOptions);
- const toks = getEnvVar("AUTH_TOKENS", "stringArray");
- const authTokens = new Set<string>(Array.isArray(toks) ? toks : []);
- const authRequired = envVarEquals("AUTH_REQUIRED", true, false);
- export async function init() {
- const port = getEnvVar("HTTP_PORT", "number"),
- host = getEnvVar("HTTP_HOST").length < 1 ? "0.0.0.0" : getEnvVar("HTTP_HOST");
- if(await portUsed(port))
- return error(`TCP port ${port} is already used or invalid`, undefined, true);
- // on error
- app.use((err: unknown, req: Request, res: Response, next: NextFunction) => {
- if(typeof err === "string" || err instanceof Error)
- return respond(res, "serverError", `General error in HTTP server: ${err.toString()}`, req?.query?.format ? String(req.query.format) : undefined);
- else
- return next();
- });
- // preflight requests
- app.options("*", cors());
- // if auth tokens are required, check for them before doing anything else
- if(authRequired && authTokens.size < 1) {
- app.use((req, res, next) => {
- if(!req.headers.authorization || !authTokens.has(req.headers.authorization.trim().replace(/^Bearer\s+/i, "")))
- return respond(res, 401, {
- error: true,
- matches: null,
- message: "Unauthorized"
- }, req?.query?.format ? String(req.query.format) : undefined);
- else
- return next();
- });
- }
- // rate limiting
- app.use(async (req, res, next) => {
- const fmt = req?.query?.format ? String(req.query.format) : undefined;
- try {
- const { authorization } = req.headers;
- const authHeader = authorization?.trim().replace(/^Bearer\s+/i, "");
- res.setHeader("API-Info", `geniURL v${packageJson.version} (${packageJson.homepage})`);
- res.setHeader("API-Version", packageJson.version);
- if(authHeader && authTokens.has(authHeader))
- return next();
- const ipHash = await hashStr(getClientIp(req) ?? "IP_RESOLUTION_ERROR");
- if(rlIgnorePaths.every((path) => !req.path.match(new RegExp(`^(/?v\\d+)?${path}`)))) {
- rateLimiter.consume(ipHash)
- .then((rateLimiterRes: RateLimiterRes) => {
- setRateLimitHeaders(res, rateLimiterRes);
- return next();
- })
- .catch((err) => {
- if(err instanceof RateLimiterRes) {
- setRateLimitHeaders(res, err);
- 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);
- }
- else
- return respond(res, 500, {
- error: true,
- matches: null,
- message: `Encountered an internal error${err instanceof Error ? `: ${err.message}` : ""}. Please try again a little later.`,
- }, fmt);
- });
- }
- else
- return next();
- }
- catch(e) {
- return respond(res, "serverError", {
- error: true,
- matches: null,
- message: `Encountered an internal error while applying rate limiting and checking for authorization${e instanceof Error ? `: ${e.message}` : ""}`
- }, fmt);
- }
- });
- const listener = app.listen(port, host, () => {
- try {
- initRouter(app);
- console.log(k.green(`geniURL was successfully started on ${k.blue(k.underline(`http://127.0.0.1:${port}`))}\n`));
- }
- catch(err) {
- error("Error while initializing router", err instanceof Error ? err : undefined, true);
- }
- });
- listener.on("error", (err) => error("General server error", err, true));
- }
- /** Sets all rate-limiting related headers on a response given a RateLimiterRes object */
- function setRateLimitHeaders(res: Response, rateLimiterRes: RateLimiterRes) {
- if(rateLimiterRes.remainingPoints === 0)
- res.setHeader("Retry-After", Math.ceil(rateLimiterRes.msBeforeNext / 1000));
- res.setHeader("X-RateLimit-Limit", rateLimiter.points);
- res.setHeader("X-RateLimit-Remaining", rateLimiterRes.remainingPoints);
- res.setHeader("X-RateLimit-Reset", new Date(Date.now() + rateLimiterRes.msBeforeNext).toISOString());
- }
|