From 037bd43d16b11c85d3a521c7ad364b0ffe7c231a Mon Sep 17 00:00:00 2001 From: Erkin Alp Güney Date: Sun, 24 Apr 2022 14:57:26 +0300 Subject: Punitive rate limiting --- api/src/middlewares/RateLimit.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) (limited to 'api/src/middlewares') diff --git a/api/src/middlewares/RateLimit.ts b/api/src/middlewares/RateLimit.ts index 1a38cfcf..8368d14a 100644 --- a/api/src/middlewares/RateLimit.ts +++ b/api/src/middlewares/RateLimit.ts @@ -53,12 +53,12 @@ export default function rateLimit(opts: { 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 = (resetAfterMs + 999) / 1000; if (resetAfterMs <= 0) { offender.hits = 0; @@ -70,6 +70,10 @@ export default function rateLimit(opts: { if (offender.blocked) { const global = bucket_id === "global"; + reset = reset + opts.window * 1000; // each block violation pushes the expiry one full window further + offender.expires_at += opts.window * 1000; + resetAfterMs = reset - Date.now(); + resetAfterSec = (resetAfterMs + 999) / 1000; console.log("blocked bucket: " + bucket_id, { resetAfterMs }); return ( -- cgit 1.5.1 From 8769bb2868d1a48a7da34fc0d7f6152a51ea37c3 Mon Sep 17 00:00:00 2001 From: Erkin Alp Güney Date: Sun, 24 Apr 2022 17:35:09 +0300 Subject: fix the seconds rounding logic --- api/src/middlewares/RateLimit.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'api/src/middlewares') diff --git a/api/src/middlewares/RateLimit.ts b/api/src/middlewares/RateLimit.ts index 8368d14a..b4f32131 100644 --- a/api/src/middlewares/RateLimit.ts +++ b/api/src/middlewares/RateLimit.ts @@ -58,7 +58,7 @@ export default function rateLimit(opts: { if (offender) { let reset = offender.expires_at.getTime(); let resetAfterMs = reset - Date.now(); - let resetAfterSec = (resetAfterMs + 999) / 1000; + let resetAfterSec = Math.ceil(resetAfterMs / 1000); if (resetAfterMs <= 0) { offender.hits = 0; @@ -73,7 +73,7 @@ export default function rateLimit(opts: { reset = reset + opts.window * 1000; // each block violation pushes the expiry one full window further offender.expires_at += opts.window * 1000; resetAfterMs = reset - Date.now(); - resetAfterSec = (resetAfterMs + 999) / 1000; + resetAfterSec = Math.ceil(resetAfterMs / 1000); console.log("blocked bucket: " + bucket_id, { resetAfterMs }); return ( -- cgit 1.5.1 From ea1f188ccec325e2128d155fe6595e431b6aea5d Mon Sep 17 00:00:00 2001 From: Erkin Alp Güney Date: Sun, 24 Apr 2022 21:49:04 +0300 Subject: Update RateLimit.ts --- api/src/middlewares/RateLimit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'api/src/middlewares') diff --git a/api/src/middlewares/RateLimit.ts b/api/src/middlewares/RateLimit.ts index b4f32131..f31aa5da 100644 --- a/api/src/middlewares/RateLimit.ts +++ b/api/src/middlewares/RateLimit.ts @@ -71,7 +71,7 @@ export default function rateLimit(opts: { if (offender.blocked) { const global = bucket_id === "global"; reset = reset + opts.window * 1000; // each block violation pushes the expiry one full window further - offender.expires_at += opts.window * 1000; + offender.expires_at = offender.expires_at + opts.window * 1000; resetAfterMs = reset - Date.now(); resetAfterSec = Math.ceil(resetAfterMs / 1000); -- cgit 1.5.1 From aacf99d82a2b2dce609e7426396743c8cbcb7318 Mon Sep 17 00:00:00 2001 From: Erkin Alp Güney Date: Sun, 24 Apr 2022 23:04:55 +0300 Subject: Update RateLimit.ts --- api/src/middlewares/RateLimit.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'api/src/middlewares') diff --git a/api/src/middlewares/RateLimit.ts b/api/src/middlewares/RateLimit.ts index f31aa5da..4bbef520 100644 --- a/api/src/middlewares/RateLimit.ts +++ b/api/src/middlewares/RateLimit.ts @@ -70,8 +70,9 @@ export default function rateLimit(opts: { if (offender.blocked) { const global = bucket_id === "global"; - reset = reset + opts.window * 1000; // each block violation pushes the expiry one full window further - offender.expires_at = offender.expires_at + opts.window * 1000; + // each block violation pushes the expiry one full window further + reset = new Date(reset.getTime() + opts.window * 1000); + offender.expires_at = new Date(offender.expires_at.getTime() + opts.window * 1000); resetAfterMs = reset - Date.now(); resetAfterSec = Math.ceil(resetAfterMs / 1000); -- cgit 1.5.1 From 9bd5fce30c83aacb4175b4e301529592d4dcdd75 Mon Sep 17 00:00:00 2001 From: Erkin Alp Güney Date: Sun, 24 Apr 2022 23:07:25 +0300 Subject: eventually fix those errors --- api/src/middlewares/RateLimit.ts | 2 +- util/src/util/Constants.ts | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) (limited to 'api/src/middlewares') diff --git a/api/src/middlewares/RateLimit.ts b/api/src/middlewares/RateLimit.ts index 4bbef520..81668034 100644 --- a/api/src/middlewares/RateLimit.ts +++ b/api/src/middlewares/RateLimit.ts @@ -71,7 +71,7 @@ export default function rateLimit(opts: { if (offender.blocked) { const global = bucket_id === "global"; // each block violation pushes the expiry one full window further - reset = new Date(reset.getTime() + opts.window * 1000); + 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); diff --git a/util/src/util/Constants.ts b/util/src/util/Constants.ts index 8d61b9b4..a5d3fcd2 100644 --- a/util/src/util/Constants.ts +++ b/util/src/util/Constants.ts @@ -727,21 +727,23 @@ export const DiscordApiErrors = { * An error encountered while performing an API request (Fosscord only). Here are the potential errors: */ export const FosscordApiErrors = { - MANUALLY_TRIGGERED_ERROR: new ApiError("This is an artificial error", 1), + MANUALLY_TRIGGERED_ERROR: new ApiError("This is an artificial error", 1, 500), PREMIUM_DISABLED_FOR_GUILD: new ApiError("This guild cannot be boosted", 25001), NO_FURTHER_PREMIUM: new ApiError("This guild does not receive further boosts", 25002), - GUILD_PREMIUM_DISABLED_FOR_YOU: new ApiError("This guild cannot be boosted by you", 25003), + GUILD_PREMIUM_DISABLED_FOR_YOU: new ApiError("This guild cannot be boosted by you", 25003, 403), CANNOT_FRIEND_SELF: new ApiError("Cannot friend oneself", 25009), USER_SPECIFIC_INVITE_WRONG_RECIPIENT: new ApiError("This invite is not meant for you", 25010), USER_SPECIFIC_INVITE_FAILED: new ApiError("Failed to invite user", 25011), - CANNOT_MODIFY_USER_GROUP: new ApiError("This user cannot manipulate this group", 25050), + CANNOT_MODIFY_USER_GROUP: new ApiError("This user cannot manipulate this group", 25050, 403), CANNOT_REMOVE_SELF_FROM_GROUP: new ApiError("This user cannot remove oneself from user group", 25051), CANNOT_BAN_OPERATOR: new ApiError("Non-OPERATOR cannot ban OPERATOR from instance", 25052), - CANNOT_LEAVE_GUILD: new ApiError("You are not allowed to leave guilds that you joined by yourself", 25059), - EDITS_DISABLED: new ApiError("You are not allowed to edit your own messages", 25060), - DELETE_MESSAGE_DISABLED: new ApiError("You are not allowed to delete your own messages", 25061), - FEATURE_PERMANENTLY_DISABLED: new ApiError("This feature has been disabled server-side", 45006), + CANNOT_LEAVE_GUILD: new ApiError("You are not allowed to leave guilds that you joined by yourself", 25059, 403), + EDITS_DISABLED: new ApiError("You are not allowed to edit your own messages", 25060, 403), + DELETE_MESSAGE_DISABLED: new ApiError("You are not allowed to delete your own messages", 25061, 403), + FEATURE_PERMANENTLY_DISABLED: new ApiError("This feature has been disabled server-side", 45006, 501), MISSING_RIGHTS: new ApiError("You lack rights to perform that action ({})", 50013, undefined, [""]), + CANNOT_REPLACE_BY_BACKFILL: new ApiError("Cannot backfill to message ID that already exists", 55002, 409), + CANNOT_BACKFILL_TO_THE_FUTURE: new ApiError("You cannot backfill messages in the future", 55003), CANNOT_GRANT_PERMISSIONS_EXCEEDING_RIGHTS: new ApiError("You cannot grant permissions exceeding your own rights", 50050), ROUTES_LOOPING: new ApiError("Loops in the route definition ({})", 50060, undefined, [""]), CANNOT_REMOVE_ROUTE: new ApiError("Cannot remove message route while it is in effect and being used", 50061), @@ -787,3 +789,4 @@ function keyMirror(arr: string[]) { for (const value of arr) tmp[value] = value; return tmp; } + -- cgit 1.5.1 From 2e451d8fd0d85394cc219d7675f3537b8cc9ee90 Mon Sep 17 00:00:00 2001 From: Erkin Alp Güney Date: Thu, 28 Apr 2022 21:30:41 +0300 Subject: exempt users logic resolves #396 --- api/src/middlewares/RateLimit.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'api/src/middlewares') diff --git a/api/src/middlewares/RateLimit.ts b/api/src/middlewares/RateLimit.ts index 81668034..7d5c51e2 100644 --- a/api/src/middlewares/RateLimit.ts +++ b/api/src/middlewares/RateLimit.ts @@ -1,4 +1,4 @@ -import { Config, listenEvent } from "@fosscord/util"; +import { Config, getRights, listenEvent, Rights } from "@fosscord/util"; import { NextFunction, Request, Response, Router } from "express"; import { getIpAdress } from "@fosscord/api"; import { API_PREFIX_TRAILING_SLASH } from "./Authentication"; @@ -9,6 +9,7 @@ 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) @@ -44,9 +45,12 @@ export default function rateLimit(opts: { onlyIp?: boolean; }): any { return async (req: Request, res: Response, next: NextFunction): Promise => { + // exempt user? if so, immediately short circuit + if (getRights(req.user_id).has("BYPASS_RATE_LIMITS")) return; + const bucket_id = opts.bucket || req.originalUrl.replace(API_PREFIX_TRAILING_SLASH, ""); var 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; if (opts.bot && req.user_bot) max_hits = opts.bot; -- cgit 1.5.1 From f09daaa3de902641ca9772089b8af676e05543c3 Mon Sep 17 00:00:00 2001 From: Erkin Alp Güney Date: Thu, 28 Apr 2022 21:38:39 +0300 Subject: needs to be async --- api/src/middlewares/RateLimit.ts | 3 ++- api/src/routes/channels/#channel_id/messages/#message_id/index.ts | 2 +- bundle/package-lock.json | 6 +++--- 3 files changed, 6 insertions(+), 5 deletions(-) (limited to 'api/src/middlewares') diff --git a/api/src/middlewares/RateLimit.ts b/api/src/middlewares/RateLimit.ts index 7d5c51e2..ca6de98f 100644 --- a/api/src/middlewares/RateLimit.ts +++ b/api/src/middlewares/RateLimit.ts @@ -46,7 +46,8 @@ export default function rateLimit(opts: { }): any { return async (req: Request, res: Response, next: NextFunction): Promise => { // exempt user? if so, immediately short circuit - if (getRights(req.user_id).has("BYPASS_RATE_LIMITS")) return; + const rights = await getRights(req.user_id); + if (rights.has("BYPASS_RATE_LIMITS")) return; const bucket_id = opts.bucket || req.originalUrl.replace(API_PREFIX_TRAILING_SLASH, ""); var executor_id = getIpAdress(req); diff --git a/api/src/routes/channels/#channel_id/messages/#message_id/index.ts b/api/src/routes/channels/#channel_id/messages/#message_id/index.ts index 958954b6..63fee9b9 100644 --- a/api/src/routes/channels/#channel_id/messages/#message_id/index.ts +++ b/api/src/routes/channels/#channel_id/messages/#message_id/index.ts @@ -95,7 +95,7 @@ router.put( var body = req.body as MessageCreateSchema; const attachments: Attachment[] = []; - const rights = getRights(req.user_id); + const rights = await getRights(req.user_id); rights.hasThrow("SEND_MESSAGES"); // regex to check if message contains anything other than numerals ( also no decimals ) diff --git a/bundle/package-lock.json b/bundle/package-lock.json index 4742b4a4..6fbd6978 100644 --- a/bundle/package-lock.json +++ b/bundle/package-lock.json @@ -101,7 +101,7 @@ "name": "@fosscord/api", "version": "1.0.0", "hasInstallScript": true, - "license": "GPLV3", + "license": "AGPLV3", "dependencies": { "@babel/preset-env": "^7.15.8", "@babel/preset-typescript": "^7.15.0", @@ -164,7 +164,7 @@ "../cdn": { "name": "@fosscord/cdn", "version": "1.0.0", - "license": "GPLV3", + "license": "AGPLV3", "dependencies": { "@aws-sdk/client-s3": "^3.36.1", "@aws-sdk/node-http-handler": "^3.36.0", @@ -208,7 +208,7 @@ "name": "@fosscord/gateway", "version": "1.0.0", "hasInstallScript": true, - "license": "GPLV3", + "license": "AGPLV3", "dependencies": { "@fosscord/util": "file:../util", "amqplib": "^0.8.0", -- cgit 1.5.1