From 954700b2d5c4090fdf9d7d25beef3d6529afa8d3 Mon Sep 17 00:00:00 2001 From: Flam3rboy <34555296+Flam3rboy@users.noreply.github.com> Date: Mon, 30 Aug 2021 12:14:32 +0200 Subject: :zap: only local rate limit to prevent to much pressure on the database --- api/src/middlewares/RateLimit.ts | 70 +++++++++++++++++++++++++++++----------- 1 file changed, 51 insertions(+), 19 deletions(-) (limited to 'api') diff --git a/api/src/middlewares/RateLimit.ts b/api/src/middlewares/RateLimit.ts index ed6b951a..dffbc0d9 100644 --- a/api/src/middlewares/RateLimit.ts +++ b/api/src/middlewares/RateLimit.ts @@ -1,11 +1,12 @@ -import { Config, listenEvent, emitEvent, RateLimit } from "@fosscord/util"; +import { Config, listenEvent } from "@fosscord/util"; import { NextFunction, Request, Response, Router } from "express"; -import { LessThan, MoreThan } from "typeorm"; import { getIpAdress } from "../util/ipAddress"; import { API_PREFIX_TRAILING_SLASH } from "./Authentication"; // Docs: https://discord.com/developers/docs/topics/rate-limits +// TODO: use better caching (e.g. redis) as else it creates to much pressure on the database + /* ? bucket limit? Max actions/sec per bucket? @@ -18,6 +19,14 @@ TODO: different for methods (GET/POST) */ +type RateLimit = { + id: "global" | "error" | string; + executor_id: string; + hits: number; + blocked: boolean; + expires_at: Date; +}; + var Cache = new Map(); const EventRateLimit = "RATELIMIT"; @@ -46,13 +55,22 @@ export default function rateLimit(opts: { const offender = Cache.get(executor_id + bucket_id); - if (offender && offender.blocked) { + if (offender) { const reset = offender.expires_at.getTime(); const resetAfterMs = reset - Date.now(); const resetAfterSec = resetAfterMs / 1000; - const global = bucket_id === "global"; - if (resetAfterMs > 0) { + if (resetAfterMs <= 0) { + offender.hits = 0; + offender.expires_at = new Date(Date.now() + opts.window * 1000); + offender.blocked = false; + + Cache.delete(executor_id + bucket_id); + } + + if (offender.blocked) { + const global = bucket_id === "global"; + console.log("blocked bucket: " + bucket_id, { resetAfterMs }); return ( res @@ -67,15 +85,9 @@ export default function rateLimit(opts: { // TODO: error rate limit message translation .send({ message: "You are being rate limited.", retry_after: resetAfterSec, global }) ); - } else { - offender.hits = 0; - offender.expires_at = new Date(Date.now() + opts.window * 1000); - offender.blocked = false; - // mongodb ttl didn't update yet -> manually update/delete - RateLimit.delete({ id: bucket_id, executor_id }); - Cache.delete(executor_id + bucket_id); } } + next(); const hitRouteOpts = { bucket_id, executor_id, max_hits, window: opts.window }; @@ -100,20 +112,20 @@ export async function initRateLimits(app: Router) { Cache.set(event.channel_id as string, event.data); event.acknowledge?.(); }); - await RateLimit.delete({ expires_at: MoreThan(new Date()) }); // cleans up if not already deleted, morethan -> older date - const limits = await RateLimit.find({ blocked: true }); - limits.forEach((limit) => { - Cache.set(limit.executor_id, limit); - }); + // await RateLimit.delete({ expires_at: LessThan(new Date().toISOString()) }); // cleans up if not already deleted, morethan -> older date + // const limits = await RateLimit.find({ blocked: true }); + // limits.forEach((limit) => { + // Cache.set(limit.executor_id, limit); + // }); setInterval(() => { Cache.forEach((x, key) => { if (new Date() > x.expires_at) { Cache.delete(key); - RateLimit.delete({ executor_id: key }); + // RateLimit.delete({ executor_id: key }); } }); - }, 1000 * 60 * 10); + }, 1000 * 60); app.use( rateLimit({ @@ -139,6 +151,25 @@ export async function initRateLimits(app: Router) { } async function hitRoute(opts: { executor_id: string; bucket_id: string; max_hits: number; window: number }) { + const id = opts.executor_id + opts.bucket_id; + var limit = Cache.get(id); + if (!limit) { + limit = { + id: opts.bucket_id, + executor_id: opts.executor_id, + expires_at: new Date(Date.now() + opts.window * 1000), + hits: 0, + blocked: false + }; + Cache.set(id, limit); + } + + limit.hits++; + if (limit.hits >= opts.max_hits) { + limit.blocked = true; + } + + /* var ratelimit = await RateLimit.findOne({ id: opts.bucket_id, executor_id: opts.executor_id }); if (!ratelimit) { ratelimit = new RateLimit({ @@ -167,4 +198,5 @@ async function hitRoute(opts: { executor_id: string; bucket_id: string; max_hits } await ratelimit.save(); + */ } -- cgit 1.4.1