summary refs log tree commit diff
diff options
context:
space:
mode:
authorMadeline <46743919+MaddyUnderStars@users.noreply.github.com>2022-08-25 20:46:58 +1000
committerMadeline <46743919+MaddyUnderStars@users.noreply.github.com>2022-08-25 20:46:58 +1000
commit9e0aaf55199ac5709a5bd9dd2d5796457a2c1740 (patch)
tree4d5932a44da6d181edfc274046e3e5c9f5dff056
parentConfigurable max attachment size (diff)
downloadserver-9e0aaf55199ac5709a5bd9dd2d5796457a2c1740.tar.xz
Switch to staging ratelimiter
-rw-r--r--api/src/middlewares/RateLimit.ts45
1 files changed, 25 insertions, 20 deletions
diff --git a/api/src/middlewares/RateLimit.ts b/api/src/middlewares/RateLimit.ts
index 1a38cfcf..6613e25a 100644
--- a/api/src/middlewares/RateLimit.ts
+++ b/api/src/middlewares/RateLimit.ts
@@ -1,6 +1,6 @@
-import { Config, listenEvent } from "@fosscord/util";
-import { NextFunction, Request, Response, Router } from "express";
 import { getIpAdress } from "@fosscord/api";
+import { Config, getRights, listenEvent } from "@fosscord/util";
+import { NextFunction, Request, Response, Router } from "express";
 import { API_PREFIX_TRAILING_SLASH } from "./Authentication";
 
 // Docs: https://discord.com/developers/docs/topics/rate-limits
@@ -9,14 +9,11 @@ import { API_PREFIX_TRAILING_SLASH } from "./Authentication";
 
 /*
 ? bucket limit? Max actions/sec per bucket?
-
+(ANSWER: a small fosscord instance might not need a complex rate limiting system)
 TODO: delay database requests to include multiple queries
 TODO: different for methods (GET/POST)
-
 > IP addresses that make too many invalid HTTP requests are automatically and temporarily restricted from accessing the Discord API. Currently, this limit is 10,000 per 10 minutes. An invalid request is one that results in 401, 403, or 429 statuses.
-
 > All bots can make up to 50 requests per second to our API. This is independent of any individual rate limit on a route. If your bot gets big enough, based on its functionality, it may be impossible to stay below 50 requests per second during normal operations.
-
 */
 
 type RateLimit = {
@@ -27,7 +24,7 @@ type RateLimit = {
 	expires_at: Date;
 };
 
-var Cache = new Map<string, RateLimit>();
+let Cache = new Map<string, RateLimit>();
 const EventRateLimit = "RATELIMIT";
 
 export default function rateLimit(opts: {
@@ -44,21 +41,27 @@ export default function rateLimit(opts: {
 	onlyIp?: boolean;
 }): any {
 	return async (req: Request, res: Response, next: NextFunction): Promise<any> => {
+		// exempt user? if so, immediately short circuit
+		if (req.user_id) {
+			const rights = await getRights(req.user_id);
+			if (rights.has("BYPASS_RATE_LIMITS")) return next();
+		}
+
 		const bucket_id = opts.bucket || req.originalUrl.replace(API_PREFIX_TRAILING_SLASH, "");
-		var executor_id = getIpAdress(req);
+		let executor_id = getIpAdress(req);
 		if (!opts.onlyIp && req.user_id) executor_id = req.user_id;
 
-		var max_hits = opts.count;
+		let max_hits = opts.count;
 		if (opts.bot && req.user_bot) max_hits = opts.bot;
 		if (opts.GET && ["GET", "OPTIONS", "HEAD"].includes(req.method)) max_hits = opts.GET;
 		else if (opts.MODIFY && ["POST", "DELETE", "PATCH", "PUT"].includes(req.method)) max_hits = opts.MODIFY;
 
-		const offender = Cache.get(executor_id + bucket_id);
+		let offender = Cache.get(executor_id + bucket_id);
 
 		if (offender) {
-			const reset = offender.expires_at.getTime();
-			const resetAfterMs = reset - Date.now();
-			const resetAfterSec = resetAfterMs / 1000;
+			let reset = offender.expires_at.getTime();
+			let resetAfterMs = reset - Date.now();
+			let resetAfterSec = Math.ceil(resetAfterMs / 1000);
 
 			if (resetAfterMs <= 0) {
 				offender.hits = 0;
@@ -70,6 +73,11 @@ export default function rateLimit(opts: {
 
 			if (offender.blocked) {
 				const global = bucket_id === "global";
+				// each block violation pushes the expiry one full window further
+				reset += opts.window * 1000;
+				offender.expires_at = new Date(offender.expires_at.getTime() + opts.window * 1000);
+				resetAfterMs = reset - Date.now();
+				resetAfterSec = Math.ceil(resetAfterMs / 1000);
 
 				console.log("blocked bucket: " + bucket_id, { resetAfterMs });
 				return (
@@ -109,6 +117,7 @@ export default function rateLimit(opts: {
 export async function initRateLimits(app: Router) {
 	const { routes, global, ip, error, disabled } = Config.get().limits.rate;
 	if (disabled) return;
+	console.log("Enabling rate limits...");
 	await listenEvent(EventRateLimit, (event) => {
 		Cache.set(event.channel_id as string, event.data);
 		event.acknowledge?.();
@@ -153,7 +162,7 @@ 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);
+	let limit = Cache.get(id);
 	if (!limit) {
 		limit = {
 			id: opts.bucket_id,
@@ -171,7 +180,7 @@ async function hitRoute(opts: { executor_id: string; bucket_id: string; max_hits
 	}
 
 	/*
-	var ratelimit = await RateLimit.findOne({ id: opts.bucket_id, executor_id: opts.executor_id });
+	let ratelimit = await RateLimit.findOne({ where: { id: opts.bucket_id, executor_id: opts.executor_id } });
 	if (!ratelimit) {
 		ratelimit = new RateLimit({
 			id: opts.bucket_id,
@@ -181,11 +190,8 @@ async function hitRoute(opts: { executor_id: string; bucket_id: string; max_hits
 			blocked: false
 		});
 	}
-
 	ratelimit.hits++;
-
 	const updateBlock = !ratelimit.blocked && ratelimit.hits >= opts.max_hits;
-
 	if (updateBlock) {
 		ratelimit.blocked = true;
 		Cache.set(opts.executor_id + opts.bucket_id, ratelimit);
@@ -197,7 +203,6 @@ async function hitRoute(opts: { executor_id: string; bucket_id: string; max_hits
 	} else {
 		Cache.delete(opts.executor_id);
 	}
-
 	await ratelimit.save();
 	*/
-}
+}
\ No newline at end of file