✨ route specific rate limits
This commit is contained in:
		
							parent
							
								
									7b31ca10b3
								
							
						
					
					
						commit
						c3c8026041
					
				
							
								
								
									
										66
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										66
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -9,7 +9,7 @@ | ||||
| 			"version": "1.0.0", | ||||
| 			"license": "ISC", | ||||
| 			"dependencies": { | ||||
| 				"@fosscord/server-util": "^1.3.23", | ||||
| 				"@fosscord/server-util": "^1.3.24", | ||||
| 				"@types/jest": "^26.0.22", | ||||
| 				"@types/json-schema": "^7.0.7", | ||||
| 				"ajv": "^8.4.0", | ||||
| @ -550,9 +550,9 @@ | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/@fosscord/server-util": { | ||||
| 			"version": "1.3.23", | ||||
| 			"resolved": "https://registry.npmjs.org/@fosscord/server-util/-/server-util-1.3.23.tgz", | ||||
| 			"integrity": "sha512-YxkuMwsJmMpCN4zGCq0LHvUuV9zlR8yTriquPqWfp5Sbj1DdFz7Qqo6wz6cRYb3WRIINouHhV60cbljmUqLIJQ==", | ||||
| 			"version": "1.3.24", | ||||
| 			"resolved": "https://registry.npmjs.org/@fosscord/server-util/-/server-util-1.3.24.tgz", | ||||
| 			"integrity": "sha512-5r/OkFalmQ7xQ6ZU1ujNvShlokKVcDXsc+S7oLYyhrEWW5Nl1bqHEHDVqWmYt5OX+9449QJNob7DZK1T1r1a2Q==", | ||||
| 			"dependencies": { | ||||
| 				"@types/jsonwebtoken": "^8.5.0", | ||||
| 				"@types/mongoose-autopopulate": "^0.10.1", | ||||
| @ -1124,19 +1124,19 @@ | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/@types/mongoose-autopopulate": { | ||||
| 			"version": "0.10.1", | ||||
| 			"resolved": "https://registry.npmjs.org/@types/mongoose-autopopulate/-/mongoose-autopopulate-0.10.1.tgz", | ||||
| 			"integrity": "sha512-L67MAIE3WEoTtt7a7/spRYk+76lgp67FAP6I38Y9NcC1kQuzwqnukTaJzodfb8180wxHZM4qt68u6x6ptuDRaQ==", | ||||
| 			"version": "0.10.2", | ||||
| 			"resolved": "https://registry.npmjs.org/@types/mongoose-autopopulate/-/mongoose-autopopulate-0.10.2.tgz", | ||||
| 			"integrity": "sha512-YSxSEhszXK9E+7VRLdpYjkXqcRXOPFtG0xZea9n7A+oaHhZ1lSVBm/WvK2Rr746NPrTm/k1tR6uezyG6kyinyg==", | ||||
| 			"dependencies": { | ||||
| 				"@types/mongoose": "*" | ||||
| 				"@types/mongoose": "5.10.5" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/@types/mongoose-lean-virtuals": { | ||||
| 			"version": "0.5.1", | ||||
| 			"resolved": "https://registry.npmjs.org/@types/mongoose-lean-virtuals/-/mongoose-lean-virtuals-0.5.1.tgz", | ||||
| 			"integrity": "sha512-bNk+QLjP5VZU4EsJag4xQsjLAa8CEm/SKZDyiC2kM208wIrGum6daD7j45Oqs50bWNGfqZYRuEhh8xZ17D7aEw==", | ||||
| 			"version": "0.5.2", | ||||
| 			"resolved": "https://registry.npmjs.org/@types/mongoose-lean-virtuals/-/mongoose-lean-virtuals-0.5.2.tgz", | ||||
| 			"integrity": "sha512-TpAX2RkFXLtNjyciiYxdvYpVuCAv/g1alFTl4ErJWvSOA+DuNDNvfXSH3c8/DXC1ZBzO47TCwHaxI/PET4sqxQ==", | ||||
| 			"dependencies": { | ||||
| 				"@types/mongoose": "*" | ||||
| 				"@types/mongoose": "5.10.5" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/@types/multer": { | ||||
| @ -6413,6 +6413,26 @@ | ||||
| 			}, | ||||
| 			"optionalDependencies": { | ||||
| 				"saslprep": "^1.0.0" | ||||
| 			}, | ||||
| 			"peerDependenciesMeta": { | ||||
| 				"aws4": { | ||||
| 					"optional": true | ||||
| 				}, | ||||
| 				"bson-ext": { | ||||
| 					"optional": true | ||||
| 				}, | ||||
| 				"kerberos": { | ||||
| 					"optional": true | ||||
| 				}, | ||||
| 				"mongodb-client-encryption": { | ||||
| 					"optional": true | ||||
| 				}, | ||||
| 				"mongodb-extjson": { | ||||
| 					"optional": true | ||||
| 				}, | ||||
| 				"snappy": { | ||||
| 					"optional": true | ||||
| 				} | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/mongoose": { | ||||
| @ -10653,9 +10673,9 @@ | ||||
| 			} | ||||
| 		}, | ||||
| 		"@fosscord/server-util": { | ||||
| 			"version": "1.3.23", | ||||
| 			"resolved": "https://registry.npmjs.org/@fosscord/server-util/-/server-util-1.3.23.tgz", | ||||
| 			"integrity": "sha512-YxkuMwsJmMpCN4zGCq0LHvUuV9zlR8yTriquPqWfp5Sbj1DdFz7Qqo6wz6cRYb3WRIINouHhV60cbljmUqLIJQ==", | ||||
| 			"version": "1.3.24", | ||||
| 			"resolved": "https://registry.npmjs.org/@fosscord/server-util/-/server-util-1.3.24.tgz", | ||||
| 			"integrity": "sha512-5r/OkFalmQ7xQ6ZU1ujNvShlokKVcDXsc+S7oLYyhrEWW5Nl1bqHEHDVqWmYt5OX+9449QJNob7DZK1T1r1a2Q==", | ||||
| 			"requires": { | ||||
| 				"@types/jsonwebtoken": "^8.5.0", | ||||
| 				"@types/mongoose-autopopulate": "^0.10.1", | ||||
| @ -11169,19 +11189,19 @@ | ||||
| 			} | ||||
| 		}, | ||||
| 		"@types/mongoose-autopopulate": { | ||||
| 			"version": "0.10.1", | ||||
| 			"resolved": "https://registry.npmjs.org/@types/mongoose-autopopulate/-/mongoose-autopopulate-0.10.1.tgz", | ||||
| 			"integrity": "sha512-L67MAIE3WEoTtt7a7/spRYk+76lgp67FAP6I38Y9NcC1kQuzwqnukTaJzodfb8180wxHZM4qt68u6x6ptuDRaQ==", | ||||
| 			"version": "0.10.2", | ||||
| 			"resolved": "https://registry.npmjs.org/@types/mongoose-autopopulate/-/mongoose-autopopulate-0.10.2.tgz", | ||||
| 			"integrity": "sha512-YSxSEhszXK9E+7VRLdpYjkXqcRXOPFtG0xZea9n7A+oaHhZ1lSVBm/WvK2Rr746NPrTm/k1tR6uezyG6kyinyg==", | ||||
| 			"requires": { | ||||
| 				"@types/mongoose": "*" | ||||
| 				"@types/mongoose": "5.10.5" | ||||
| 			} | ||||
| 		}, | ||||
| 		"@types/mongoose-lean-virtuals": { | ||||
| 			"version": "0.5.1", | ||||
| 			"resolved": "https://registry.npmjs.org/@types/mongoose-lean-virtuals/-/mongoose-lean-virtuals-0.5.1.tgz", | ||||
| 			"integrity": "sha512-bNk+QLjP5VZU4EsJag4xQsjLAa8CEm/SKZDyiC2kM208wIrGum6daD7j45Oqs50bWNGfqZYRuEhh8xZ17D7aEw==", | ||||
| 			"version": "0.5.2", | ||||
| 			"resolved": "https://registry.npmjs.org/@types/mongoose-lean-virtuals/-/mongoose-lean-virtuals-0.5.2.tgz", | ||||
| 			"integrity": "sha512-TpAX2RkFXLtNjyciiYxdvYpVuCAv/g1alFTl4ErJWvSOA+DuNDNvfXSH3c8/DXC1ZBzO47TCwHaxI/PET4sqxQ==", | ||||
| 			"requires": { | ||||
| 				"@types/mongoose": "*" | ||||
| 				"@types/mongoose": "5.10.5" | ||||
| 			} | ||||
| 		}, | ||||
| 		"@types/multer": { | ||||
|  | ||||
| @ -29,7 +29,7 @@ | ||||
| 	}, | ||||
| 	"homepage": "https://github.com/fosscord/fosscord-api#readme", | ||||
| 	"dependencies": { | ||||
| 		"@fosscord/server-util": "^1.3.23", | ||||
| 		"@fosscord/server-util": "^1.3.24", | ||||
| 		"@types/jest": "^26.0.22", | ||||
| 		"@types/json-schema": "^7.0.7", | ||||
| 		"ajv": "^8.4.0", | ||||
|  | ||||
| @ -93,10 +93,11 @@ export class FosscordServer extends Server { | ||||
| 		const prefix = Router(); | ||||
| 		// @ts-ignore
 | ||||
| 		this.app = prefix; | ||||
| 		prefix.use(RateLimit({ bucket: "global", count: 10, error: 10, window: 5, bot: 250 })); | ||||
| 		prefix.use("/guilds/:id", RateLimit({ count: 10, window: 5 })); | ||||
| 		prefix.use("/webhooks/:id", RateLimit({ count: 10, window: 5 })); | ||||
| 		prefix.use("/channels/:id", RateLimit({ count: 10, window: 5 })); | ||||
| 		prefix.use(RateLimit({ bucket: "global", count: 10, window: 5, bot: 250 })); | ||||
| 		prefix.use(RateLimit({ bucket: "error", count: 5, error: true, window: 5, bot: 15, onylIp: true })); | ||||
| 		prefix.use("/guilds/:id", RateLimit({ count: 5, window: 5 })); | ||||
| 		prefix.use("/webhooks/:id", RateLimit({ count: 5, window: 5 })); | ||||
| 		prefix.use("/channels/:id", RateLimit({ count: 5, window: 5 })); | ||||
| 
 | ||||
| 		this.routes = await this.registerRoutes(path.join(__dirname, "routes", "/")); | ||||
| 		app.use("/api", prefix); // allow unversioned requests
 | ||||
|  | ||||
| @ -36,17 +36,21 @@ export default function RateLimit(opts: { | ||||
| 	window: number; | ||||
| 	count: number; | ||||
| 	bot?: number; | ||||
| 	error?: number; | ||||
| 	webhook?: number; | ||||
| 	oauth?: number; | ||||
| 	GET?: number; | ||||
| 	MODIFY?: number; | ||||
| 	error?: boolean; | ||||
| 	success?: boolean; | ||||
| 	onylIp?: boolean; | ||||
| }) { | ||||
| 	Cache.init(); // will only initalize it once
 | ||||
| 
 | ||||
| 	return async (req: Request, res: Response, next: NextFunction) => { | ||||
| 		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] } | ||||
| 				} | ||||
| 			} | ||||
| 		], | ||||
|  | ||||
| @ -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), | ||||
|  | ||||
| @ -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
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Flam3rboy
						Flam3rboy