Switch to staging ratelimiter
This commit is contained in:
parent
f9edbab0c6
commit
9e0aaf5519
@ -1,6 +1,6 @@
|
|||||||
import { Config, listenEvent } from "@fosscord/util";
|
|
||||||
import { NextFunction, Request, Response, Router } from "express";
|
|
||||||
import { getIpAdress } from "@fosscord/api";
|
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";
|
import { API_PREFIX_TRAILING_SLASH } from "./Authentication";
|
||||||
|
|
||||||
// Docs: https://discord.com/developers/docs/topics/rate-limits
|
// 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?
|
? 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: delay database requests to include multiple queries
|
||||||
TODO: different for methods (GET/POST)
|
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.
|
> 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.
|
> 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 = {
|
type RateLimit = {
|
||||||
@ -27,7 +24,7 @@ type RateLimit = {
|
|||||||
expires_at: Date;
|
expires_at: Date;
|
||||||
};
|
};
|
||||||
|
|
||||||
var Cache = new Map<string, RateLimit>();
|
let Cache = new Map<string, RateLimit>();
|
||||||
const EventRateLimit = "RATELIMIT";
|
const EventRateLimit = "RATELIMIT";
|
||||||
|
|
||||||
export default function rateLimit(opts: {
|
export default function rateLimit(opts: {
|
||||||
@ -44,21 +41,27 @@ export default function rateLimit(opts: {
|
|||||||
onlyIp?: boolean;
|
onlyIp?: boolean;
|
||||||
}): any {
|
}): any {
|
||||||
return async (req: Request, res: Response, next: NextFunction): Promise<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, "");
|
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;
|
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.bot && req.user_bot) max_hits = opts.bot;
|
||||||
if (opts.GET && ["GET", "OPTIONS", "HEAD"].includes(req.method)) max_hits = opts.GET;
|
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;
|
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) {
|
if (offender) {
|
||||||
const reset = offender.expires_at.getTime();
|
let reset = offender.expires_at.getTime();
|
||||||
const resetAfterMs = reset - Date.now();
|
let resetAfterMs = reset - Date.now();
|
||||||
const resetAfterSec = resetAfterMs / 1000;
|
let resetAfterSec = Math.ceil(resetAfterMs / 1000);
|
||||||
|
|
||||||
if (resetAfterMs <= 0) {
|
if (resetAfterMs <= 0) {
|
||||||
offender.hits = 0;
|
offender.hits = 0;
|
||||||
@ -70,6 +73,11 @@ export default function rateLimit(opts: {
|
|||||||
|
|
||||||
if (offender.blocked) {
|
if (offender.blocked) {
|
||||||
const global = bucket_id === "global";
|
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 });
|
console.log("blocked bucket: " + bucket_id, { resetAfterMs });
|
||||||
return (
|
return (
|
||||||
@ -109,6 +117,7 @@ export default function rateLimit(opts: {
|
|||||||
export async function initRateLimits(app: Router) {
|
export async function initRateLimits(app: Router) {
|
||||||
const { routes, global, ip, error, disabled } = Config.get().limits.rate;
|
const { routes, global, ip, error, disabled } = Config.get().limits.rate;
|
||||||
if (disabled) return;
|
if (disabled) return;
|
||||||
|
console.log("Enabling rate limits...");
|
||||||
await listenEvent(EventRateLimit, (event) => {
|
await listenEvent(EventRateLimit, (event) => {
|
||||||
Cache.set(event.channel_id as string, event.data);
|
Cache.set(event.channel_id as string, event.data);
|
||||||
event.acknowledge?.();
|
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 }) {
|
async function hitRoute(opts: { executor_id: string; bucket_id: string; max_hits: number; window: number }) {
|
||||||
const id = opts.executor_id + opts.bucket_id;
|
const id = opts.executor_id + opts.bucket_id;
|
||||||
var limit = Cache.get(id);
|
let limit = Cache.get(id);
|
||||||
if (!limit) {
|
if (!limit) {
|
||||||
limit = {
|
limit = {
|
||||||
id: opts.bucket_id,
|
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) {
|
if (!ratelimit) {
|
||||||
ratelimit = new RateLimit({
|
ratelimit = new RateLimit({
|
||||||
id: opts.bucket_id,
|
id: opts.bucket_id,
|
||||||
@ -181,11 +190,8 @@ async function hitRoute(opts: { executor_id: string; bucket_id: string; max_hits
|
|||||||
blocked: false
|
blocked: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ratelimit.hits++;
|
ratelimit.hits++;
|
||||||
|
|
||||||
const updateBlock = !ratelimit.blocked && ratelimit.hits >= opts.max_hits;
|
const updateBlock = !ratelimit.blocked && ratelimit.hits >= opts.max_hits;
|
||||||
|
|
||||||
if (updateBlock) {
|
if (updateBlock) {
|
||||||
ratelimit.blocked = true;
|
ratelimit.blocked = true;
|
||||||
Cache.set(opts.executor_id + opts.bucket_id, ratelimit);
|
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 {
|
} else {
|
||||||
Cache.delete(opts.executor_id);
|
Cache.delete(opts.executor_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
await ratelimit.save();
|
await ratelimit.save();
|
||||||
*/
|
*/
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user