From c3c8026041d29d7b50d54080d21518cadae97fff Mon Sep 17 00:00:00 2001 From: Flam3rboy <34555296+Flam3rboy@users.noreply.github.com> Date: Thu, 1 Jul 2021 21:27:46 +0200 Subject: [PATCH] :sparkles: route specific rate limits --- package-lock.json | Bin 644837 -> 645191 bytes package.json | 2 +- src/Server.ts | 9 +++++---- src/middlewares/RateLimit.ts | 36 ++++++++++++++++++++--------------- src/routes/auth/login.ts | 2 ++ src/routes/auth/register.ts | 2 ++ 6 files changed, 31 insertions(+), 20 deletions(-) diff --git a/package-lock.json b/package-lock.json index 728f252d755aa6335bb35a81fc2e82fb81269a0a..56a7449e5248941023eb6dfaa936ff5981969c03 100644 GIT binary patch delta 1060 zcmchV&ui0Q7{|%Fcgd!k;Lsr`hT*SKyDT$pR`JrUzg%njBW;rw6w@}YZPFx7)2^8Z z1(hBZb9nK*S-cb~3Pr?&CqdM?qbI$1-_Wy|sdLUhU@v?R@ArLppD)kz`Px1Erg!$i zH-vin{V)qx1(bjv?t&?pr~y3f)`^%D6y$1xZY4aqG})*yO|~SfQY2IG^Ywz0rPXNG zEa?G`b2BoTuZ06)f2PJ&vxYmyJG~U;O)N5Mp7!S=WQx>DVHR#&b>LQS4ej5*2YQHg z``DQEl0N{uZE&ccYXb~{*PjedK!QR>pZ(yTN1%0Skgz^JLgDAP06^_Mn1&7$!P8l` z#Rf|C6+Yq(IO$9>TGX-?iLVusd@LR`LNl$LD0mgx=TwDoDM!j|X1+>r&B$z0rzsB; z*HcwOB8{Y{A+DGqk7Sk=6K=dlw&RTtxu?mQX}2Y;p0_sFPQzgQ=K>ayi>7HnUo$vEXYGOQawNQ!*9M7r7=I^Ev$u#?Lfw@ND=(VUFBmcfD{3I|6%ij?p&GIcyE~aSq&a z?GJd!7B~*Sya2z*>xA~1t4#tsXWP~3Z6dcIIF>Q^)^`3hl4e%acAK3M^#VNszbuEk!7=_VC{#aTX%i5AYTx%$E0Q7)C4&PLh3 zMg|t;o<;7Zxn?PeF6LP#rv8P+rUpsje(q_7QISEVt{E8?6;X!fF6N1@<&!5aF#=iI z9J;JMbQ$CJ&}B^fHcbB@!YDTVZ4@&{dwm2m5VLHrk6^9PrAsU%eq@!|{-BL@tvmgK zA7tlrpq=cK6_)W#XFJO(KYibMRyE962&G9ZOb@)zj%vR|JO4#iAZ7z%b|B{1&VP~9 Gf*k { - const bucket_id = opts.bucket || req.path.replace(API_PREFIX_TRAILING_SLASH, ""); - const user_id = req.user_id || getIpAdress(req); + const bucket_id = opts.bucket || req.originalUrl.replace(API_PREFIX_TRAILING_SLASH, ""); + var user_id = getIpAdress(req); + if (!opts.onylIp && req.user_id) user_id = req.user_id; + var 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; @@ -59,9 +63,9 @@ export default function RateLimit(opts: { const resetAfterMs = reset - Date.now(); const resetAfterSec = resetAfterMs / 1000; const global = bucket_id === "global"; - console.log("blocked", { resetAfterMs }); if (resetAfterMs > 0) { + console.log("blocked", { resetAfterMs }); return ( res .status(429) @@ -76,26 +80,28 @@ export default function RateLimit(opts: { .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 - db.collection("ratelimits").updateOne( - { id: bucket_id, user_id }, - { $set: { hits: 0, expires_at: new Date(Date.now() + opts.window * 1000), blocked: false } } - ); + db.collection("ratelimits").updateOne({ id: bucket_id, user_id }, { $set: offender }); } } next(); + const hitRouteOpts = { bucket_id, user_id, max_hits, window: opts.window }; - if (opts.error) { + if (opts.error || opts.success) { res.once("finish", () => { // check if error and increment error rate limit - if (res.statusCode >= 400) { - // TODO: use config rate limit values - return hitRoute({ bucket_id: "error", user_id, max_hits: opts.error as number, window: opts.window }); + if (res.statusCode >= 400 && opts.error) { + return hitRoute(hitRouteOpts); + } else if (res.statusCode >= 200 && res.statusCode < 300 && opts.success) { + return hitRoute(hitRouteOpts); } }); + } else { + return hitRoute(hitRouteOpts); } - - return hitRoute({ user_id, bucket_id, max_hits, window: opts.window }); }; } @@ -121,7 +127,7 @@ function hitRoute(opts: { user_id: string; bucket_id: string; max_hits: number; { $set: { hits: { $sum: [{ $ifNull: ["$hits", 0] }, 1] }, - blocked: { $gt: ["$hits", opts.max_hits] } + blocked: { $gte: ["$hits", opts.max_hits] } } } ], diff --git a/src/routes/auth/login.ts b/src/routes/auth/login.ts index 2c4084ea..547d115b 100644 --- a/src/routes/auth/login.ts +++ b/src/routes/auth/login.ts @@ -4,12 +4,14 @@ import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; import { Config, UserModel } from "@fosscord/server-util"; import { adjustEmail } from "./register"; +import RateLimit from "../../middlewares/RateLimit"; const router: Router = Router(); export default router; router.post( "/", + RateLimit({ count: 5, window: 60, onylIp: true }), check({ login: new Length(String, 2, 100), // email or telephone password: new Length(String, 8, 64), diff --git a/src/routes/auth/register.ts b/src/routes/auth/register.ts index f39206f2..83f8dc8c 100644 --- a/src/routes/auth/register.ts +++ b/src/routes/auth/register.ts @@ -6,11 +6,13 @@ import "missing-native-js-functions"; import { generateToken } from "./login"; import { getIpAdress, IPAnalysis, isProxy } from "../../util/ipAddress"; import { HTTPError } from "lambert-server"; +import RateLimit from "../../middlewares/RateLimit"; const router: Router = Router(); router.post( "/", + RateLimit({ count: 2, window: 60 * 60 * 12, onylIp: true, success: true }), check({ username: new Length(String, 2, 32), // TODO: check min password length in config