Switch to staging ratelimiter

This commit is contained in:
Madeline 2022-08-25 20:46:58 +10:00
parent f9edbab0c6
commit 9e0aaf5519

View File

@ -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();
*/
}