mirror of
https://github.com/spacebarchat/server.git
synced 2024-11-11 05:02:37 +01:00
✨ 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…
Reference in New Issue
Block a user