add shitty ratelimiting
This commit is contained in:
parent
ea5a72b837
commit
99d80da72a
4 changed files with 124 additions and 3 deletions
|
@ -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'),
|
||||
|
|
109
api/src/middlewares/ratelimit.ts
Normal file
109
api/src/middlewares/ratelimit.ts
Normal 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 }
|
|
@ -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') ||
|
||||
|
|
|
@ -111,7 +111,12 @@ 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,
|
||||
|
|
Loading…
Reference in a new issue