mirror of
https://github.com/spacebarchat/server.git
synced 2024-11-07 11:22:35 +01:00
✨ finished Rate Limit
This commit is contained in:
parent
efc522a0c6
commit
79e1adc5ce
50
package-lock.json
generated
50
package-lock.json
generated
@ -9,7 +9,7 @@
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@fosscord/server-util": "^1.3.21",
|
||||
"@fosscord/server-util": "^1.3.23",
|
||||
"@types/jest": "^26.0.22",
|
||||
"@types/json-schema": "^7.0.7",
|
||||
"ajv": "^8.4.0",
|
||||
@ -36,7 +36,8 @@
|
||||
"mongoose-autopopulate": "^0.12.3",
|
||||
"mongoose-long": "^0.3.2",
|
||||
"multer": "^1.4.2",
|
||||
"node-fetch": "^2.6.1"
|
||||
"node-fetch": "^2.6.1",
|
||||
"require_optional": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcrypt": "^3.0.0",
|
||||
@ -549,9 +550,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@fosscord/server-util": {
|
||||
"version": "1.3.21",
|
||||
"resolved": "https://registry.npmjs.org/@fosscord/server-util/-/server-util-1.3.21.tgz",
|
||||
"integrity": "sha512-Mb4FqgkMeI2V+et5mpQkJqeijWNTjv79LZ6rRvgFdz+DwOw75S3A2vLMRqPX9lUB6uT9ZOMd5StQjR6YZNH9Sw==",
|
||||
"version": "1.3.23",
|
||||
"resolved": "https://registry.npmjs.org/@fosscord/server-util/-/server-util-1.3.23.tgz",
|
||||
"integrity": "sha512-YxkuMwsJmMpCN4zGCq0LHvUuV9zlR8yTriquPqWfp5Sbj1DdFz7Qqo6wz6cRYb3WRIINouHhV60cbljmUqLIJQ==",
|
||||
"dependencies": {
|
||||
"@types/jsonwebtoken": "^8.5.0",
|
||||
"@types/mongoose-autopopulate": "^0.10.1",
|
||||
@ -7846,6 +7847,23 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require_optional": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz",
|
||||
"integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==",
|
||||
"dependencies": {
|
||||
"resolve-from": "^2.0.0",
|
||||
"semver": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require_optional/node_modules/resolve-from": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz",
|
||||
"integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
@ -10635,9 +10653,9 @@
|
||||
}
|
||||
},
|
||||
"@fosscord/server-util": {
|
||||
"version": "1.3.21",
|
||||
"resolved": "https://registry.npmjs.org/@fosscord/server-util/-/server-util-1.3.21.tgz",
|
||||
"integrity": "sha512-Mb4FqgkMeI2V+et5mpQkJqeijWNTjv79LZ6rRvgFdz+DwOw75S3A2vLMRqPX9lUB6uT9ZOMd5StQjR6YZNH9Sw==",
|
||||
"version": "1.3.23",
|
||||
"resolved": "https://registry.npmjs.org/@fosscord/server-util/-/server-util-1.3.23.tgz",
|
||||
"integrity": "sha512-YxkuMwsJmMpCN4zGCq0LHvUuV9zlR8yTriquPqWfp5Sbj1DdFz7Qqo6wz6cRYb3WRIINouHhV60cbljmUqLIJQ==",
|
||||
"requires": {
|
||||
"@types/jsonwebtoken": "^8.5.0",
|
||||
"@types/mongoose-autopopulate": "^0.10.1",
|
||||
@ -16816,6 +16834,22 @@
|
||||
"is-finite": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"require_optional": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz",
|
||||
"integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==",
|
||||
"requires": {
|
||||
"resolve-from": "^2.0.0",
|
||||
"semver": "^5.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"resolve-from": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz",
|
||||
"integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c="
|
||||
}
|
||||
}
|
||||
},
|
||||
"require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
|
@ -29,7 +29,7 @@
|
||||
},
|
||||
"homepage": "https://github.com/fosscord/fosscord-api#readme",
|
||||
"dependencies": {
|
||||
"@fosscord/server-util": "^1.3.21",
|
||||
"@fosscord/server-util": "^1.3.23",
|
||||
"@types/jest": "^26.0.22",
|
||||
"@types/json-schema": "^7.0.7",
|
||||
"ajv": "^8.4.0",
|
||||
@ -56,7 +56,8 @@
|
||||
"mongoose-autopopulate": "^0.12.3",
|
||||
"mongoose-long": "^0.3.2",
|
||||
"multer": "^1.4.2",
|
||||
"node-fetch": "^2.6.1"
|
||||
"node-fetch": "^2.6.1",
|
||||
"require_optional": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcrypt": "^3.0.0",
|
||||
|
@ -56,7 +56,7 @@ export class FosscordServer extends Server {
|
||||
db.collection("emojis").createIndex({ id: 1 }, { unique: true }),
|
||||
db.collection("invites").createIndex({ code: 1 }, { unique: true }),
|
||||
db.collection("invites").createIndex({ expires_at: 1 }, { expireAfterSeconds: 0 }), // after 0 seconds of expires_at the invite will get delete
|
||||
db.collection("ratelimits").createIndex({ created_at: 1 }, { expireAfterSeconds: 1000 })
|
||||
db.collection("ratelimits").createIndex({ expires_at: 1 }, { expireAfterSeconds: 0 })
|
||||
]);
|
||||
}
|
||||
|
||||
@ -69,7 +69,6 @@ export class FosscordServer extends Server {
|
||||
|
||||
this.app.use(CORS);
|
||||
this.app.use(Authentication);
|
||||
this.app.use(RateLimit({ count: 10, error: 10, window: 5 }));
|
||||
this.app.use(BodyParser({ inflate: true, limit: 1024 * 1024 * 2 }));
|
||||
const languages = await fs.readdir(path.join(__dirname, "..", "locales"));
|
||||
const namespaces = await fs.readdir(path.join(__dirname, "..", "locales", "en"));
|
||||
@ -94,6 +93,7 @@ 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 }));
|
||||
|
@ -1,8 +1,16 @@
|
||||
import { db, MongooseCache, Bucket } from "@fosscord/server-util";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { API_PREFIX, API_PREFIX_TRAILING_SLASH } from "./Authentication";
|
||||
import { getIpAdress } from "../util/ipAddress";
|
||||
import { API_PREFIX_TRAILING_SLASH } from "./Authentication";
|
||||
|
||||
const Cache = new MongooseCache(db.collection("ratelimits"), [{ $match: { blocked: true } }], { onlyEvents: false, array: true });
|
||||
const Cache = new MongooseCache(
|
||||
db.collection("ratelimits"),
|
||||
[
|
||||
// TODO: uncomment $match and fix error: not receiving change events
|
||||
// { $match: { blocked: true } }
|
||||
],
|
||||
{ onlyEvents: false, array: true }
|
||||
);
|
||||
|
||||
// Docs: https://discord.com/developers/docs/topics/rate-limits
|
||||
|
||||
@ -37,52 +45,86 @@ export default function RateLimit(opts: {
|
||||
Cache.init(); // will only initalize it once
|
||||
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
const bucket_id = req.path.replace(API_PREFIX_TRAILING_SLASH, "");
|
||||
const user_id = req.user_id;
|
||||
const max_hits = req.user_bot ? opts.bot : opts.count;
|
||||
const offender = Cache.data.find((x: Bucket) => x.user && x.id === bucket_id) as Bucket | null;
|
||||
const bucket_id = opts.bucket || req.path.replace(API_PREFIX_TRAILING_SLASH, "");
|
||||
const user_id = req.user_id || getIpAdress(req);
|
||||
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;
|
||||
else if (opts.MODIFY && ["POST", "DELETE", "PATCH", "PUT"].includes(req.method)) max_hits = opts.MODIFY;
|
||||
|
||||
const offender = Cache.data?.find((x: Bucket) => x.user_id == user_id && x.id === bucket_id) as Bucket | null;
|
||||
|
||||
if (offender && offender.blocked) {
|
||||
const reset = offender.created_at.getTime() + opts.window;
|
||||
const reset = offender.expires_at.getTime();
|
||||
const resetAfterMs = reset - Date.now();
|
||||
const resetAfterSec = resetAfterMs / 1000;
|
||||
const global = bucket_id === "global";
|
||||
console.log("blocked", { resetAfterMs });
|
||||
|
||||
return (
|
||||
res
|
||||
.status(429)
|
||||
.set("X-RateLimit-Limit", `${max_hits}`)
|
||||
.set("X-RateLimit-Remaining", "0")
|
||||
.set("X-RateLimit-Reset", `${reset}`)
|
||||
.set("X-RateLimit-Reset-After", `${resetAfterSec}`)
|
||||
.set("X-RateLimit-Global", `${global}`)
|
||||
.set("Retry-After", `${Math.ceil(resetAfterSec)}`)
|
||||
.set("X-RateLimit-Bucket", `${bucket_id}`)
|
||||
// TODO: error rate limit message translation
|
||||
.send({ message: "You are being rate limited.", retry_after: resetAfterSec, global })
|
||||
);
|
||||
if (resetAfterMs > 0) {
|
||||
return (
|
||||
res
|
||||
.status(429)
|
||||
.set("X-RateLimit-Limit", `${max_hits}`)
|
||||
.set("X-RateLimit-Remaining", "0")
|
||||
.set("X-RateLimit-Reset", `${reset}`)
|
||||
.set("X-RateLimit-Reset-After", `${resetAfterSec}`)
|
||||
.set("X-RateLimit-Global", `${global}`)
|
||||
.set("Retry-After", `${Math.ceil(resetAfterSec)}`)
|
||||
.set("X-RateLimit-Bucket", `${bucket_id}`)
|
||||
// TODO: error rate limit message translation
|
||||
.send({ message: "You are being rate limited.", retry_after: resetAfterSec, global })
|
||||
);
|
||||
} else {
|
||||
// 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 } }
|
||||
);
|
||||
}
|
||||
}
|
||||
next();
|
||||
console.log(req.route);
|
||||
|
||||
if (opts.error) {
|
||||
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 });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
db.collection("ratelimits").updateOne(
|
||||
{ bucket: bucket_id },
|
||||
{
|
||||
$set: {
|
||||
id: bucket_id,
|
||||
user_id,
|
||||
created_at: new Date(),
|
||||
$cond: { if: { $gt: ["$hits", max_hits] }, then: true, else: false }
|
||||
},
|
||||
$inc: { hits: 1 }
|
||||
},
|
||||
{ upsert: true }
|
||||
);
|
||||
return hitRoute({ user_id, bucket_id, max_hits, window: opts.window });
|
||||
};
|
||||
}
|
||||
|
||||
function hitRoute(opts: { user_id: string; bucket_id: string; max_hits: number; window: number }) {
|
||||
return db.collection("ratelimits").updateOne(
|
||||
{ id: opts.bucket_id, user_id: opts.user_id },
|
||||
[
|
||||
{
|
||||
$replaceRoot: {
|
||||
newRoot: {
|
||||
// similar to $setOnInsert
|
||||
$mergeObjects: [
|
||||
{
|
||||
id: opts.bucket_id,
|
||||
user_id: opts.user_id,
|
||||
expires_at: new Date(Date.now() + opts.window * 1000)
|
||||
},
|
||||
"$$ROOT"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
hits: { $sum: [{ $ifNull: ["$hits", 0] }, 1] },
|
||||
blocked: { $gt: ["$hits", opts.max_hits] }
|
||||
}
|
||||
}
|
||||
],
|
||||
{ upsert: true }
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user