add shitty ratelimiting

This commit is contained in:
JandereDev 2022-02-05 22:46:50 +01:00
parent ea5a72b837
commit 99d80da72a
No known key found for this signature in database
GPG key ID: 5D5E18ACB990F57A
4 changed files with 124 additions and 3 deletions

View file

@ -13,6 +13,7 @@ const logger: Log75 = new (Log75 as any).default(DEBUG ? LogLevel.Debug : LogLev
const db = buildDBClient();
const app = Express();
app.set('trust proxy', true);
app.use(Express.json());
export { logger, app, db, PORT, SESSION_LIFETIME }
@ -22,6 +23,8 @@ export { logger, app, db, PORT, SESSION_LIFETIME }
import('./middlewares/log'),
import('./middlewares/updateTokenExpiry'),
import('./middlewares/cors'),
import('./middlewares/ratelimit'),
import('./routes/internal/ws'),
import('./routes/root'),
import('./routes/login'),

View file

@ -0,0 +1,109 @@
import { Request, Response } from "express";
import { FindOneResult } from "monk";
import { app, db, logger } from "..";
const ratelimits = db.get('ratelimits');
type RateLimitObject = {
ip: string,
requests: { route: string, time: number }[],
lastActivity: number,
}
// Might use redis here later, idk
// I am also aware that there's better ways to do this
class RateLimiter {
route: string;
limit: number;
timeframe: number;
constructor(route: string, limits: { limit: number, timeframe: number }) {
this.route = route;
this.limit = limits.limit;
this.timeframe = limits.timeframe;
}
async execute(req: Request, res: Response, next: () => void) {
try {
const ip = req.ip;
const now = Date.now();
const entry: FindOneResult<RateLimitObject> = await ratelimits.findOne({ ip });
if (!entry) {
logger.debug('Ratelimiter: Request from new IP address, creating new document');
next();
await ratelimits.insert({
ip,
lastActivity: now,
requests: [{ route: this.route, time: now }],
});
return;
}
const reqs = entry.requests.filter(
r => r.route == this.route && r.time > now - (this.timeframe * 1000)
);
if (reqs.length >= this.limit) {
logger.debug(`Ratelimiter: IP address exceeded ratelimit for ${this.route} [${this.limit}/${this.timeframe}]`);
res
.status(429)
.send({
error: 'You are being rate limited.',
limit: this.limit,
timeframe: this.timeframe,
});
} else next();
// Can't put a $push and $pull into the same query
await Promise.all([
ratelimits.update({ ip }, {
$push: {
requests: { route: this.route, time: now }
},
$set: {
lastActivity: now
}
}),
ratelimits.update({ ip }, {
$pull: {
requests: {
route: this.route,
time: {
$lt: now - (this.timeframe * 1000)
}
}
}
}),
]);
} catch(e) { console.error(e) }
}
}
app.use('*', (...args) => (new RateLimiter('*', { limit: 20, timeframe: 1 })).execute(...args));
// Delete all documents where the last
// activity was more than 24 hours ago.
// This ensures that we don't store
// personally identifying data for longer
// than required.
const cleanDocuments = async () => {
try {
logger.info('Ratelimiter: Deleting old documents');
const { deletedCount } = await ratelimits.remove({
lastActivity: { $lt: Date.now() - 1000 * 60 * 60 * 24 }
}, { multi: true });
logger.done(`Ratelimiter: Deleted ${deletedCount ?? '??'} documents.`);
} catch(e) {
console.error(e);
}
}
setTimeout(cleanDocuments, 1000 * 10);
setInterval(cleanDocuments, 10000 * 60 * 60);
export { RateLimiter }

View file

@ -5,6 +5,7 @@ import { botReq } from './internal/ws';
import { db } from '..';
import { FindOneResult } from 'monk';
import { badRequest } from '../utils';
import { RateLimiter } from '../middlewares/ratelimit';
class BeginReqBody {
user: string;
@ -15,7 +16,10 @@ class CompleteReqBody {
code: string;
}
app.post('/login/begin', async (req: Request, res: Response) => {
const beginRatelimiter = new RateLimiter('/login/begin', { limit: 10, timeframe: 300 });
const completeRatelimiter = new RateLimiter('/login/complete', { limit: 5, timeframe: 30 });
app.post('/login/begin', (...args) => beginRatelimiter.execute(...args), async (req: Request, res: Response) => {
const body = req.body as BeginReqBody;
if (!body.user || typeof body.user != 'string') return badRequest(res);
@ -26,7 +30,7 @@ app.post('/login/begin', async (req: Request, res: Response) => {
res.status(200).send({ success: true, nonce: r.nonce, code: r.code, uid: r.uid });
});
app.post('/login/complete', async (req: Request, res: Response) => {
app.post('/login/complete', (...args) => completeRatelimiter.execute(...args), async (req: Request, res: Response) => {
const body = req.body as CompleteReqBody;
if ((!body.user || typeof body.user != 'string') ||
(!body.nonce || typeof body.nonce != 'string') ||

View file

@ -111,8 +111,13 @@ wsEvents.on('req:requestLogin', async (data: any, cb: (data: WSResponse) => void
const nonce = ulid();
const previousLogins = await bot.db.get('pending_logins').find({ user: user._id, confirmed: true });
const [previousLogins, currentValidLogins] = await Promise.all([
bot.db.get('pending_logins').find({ user: user._id, confirmed: true }),
bot.db.get('pending_logins').find({ user: user._id, confirmed: false, expires: { $gt: Date.now() } }),
]);
if (currentValidLogins.length >= 5) return cb({ success: false, statusCode: 403, error: 'Too many pending logins. Try again later.' });
await bot.db.get('pending_logins').insert({
code,
expires: Date.now() + (1000 * 60 * 15), // Expires in 15 minutes