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 db = buildDBClient();
|
||||||
const app = Express();
|
const app = Express();
|
||||||
|
|
||||||
|
app.set('trust proxy', true);
|
||||||
app.use(Express.json());
|
app.use(Express.json());
|
||||||
|
|
||||||
export { logger, app, db, PORT, SESSION_LIFETIME }
|
export { logger, app, db, PORT, SESSION_LIFETIME }
|
||||||
|
@ -22,6 +23,8 @@ export { logger, app, db, PORT, SESSION_LIFETIME }
|
||||||
import('./middlewares/log'),
|
import('./middlewares/log'),
|
||||||
import('./middlewares/updateTokenExpiry'),
|
import('./middlewares/updateTokenExpiry'),
|
||||||
import('./middlewares/cors'),
|
import('./middlewares/cors'),
|
||||||
|
import('./middlewares/ratelimit'),
|
||||||
|
|
||||||
import('./routes/internal/ws'),
|
import('./routes/internal/ws'),
|
||||||
import('./routes/root'),
|
import('./routes/root'),
|
||||||
import('./routes/login'),
|
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 { db } from '..';
|
||||||
import { FindOneResult } from 'monk';
|
import { FindOneResult } from 'monk';
|
||||||
import { badRequest } from '../utils';
|
import { badRequest } from '../utils';
|
||||||
|
import { RateLimiter } from '../middlewares/ratelimit';
|
||||||
|
|
||||||
class BeginReqBody {
|
class BeginReqBody {
|
||||||
user: string;
|
user: string;
|
||||||
|
@ -15,7 +16,10 @@ class CompleteReqBody {
|
||||||
code: string;
|
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;
|
const body = req.body as BeginReqBody;
|
||||||
if (!body.user || typeof body.user != 'string') return badRequest(res);
|
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 });
|
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;
|
const body = req.body as CompleteReqBody;
|
||||||
if ((!body.user || typeof body.user != 'string') ||
|
if ((!body.user || typeof body.user != 'string') ||
|
||||||
(!body.nonce || typeof body.nonce != 'string') ||
|
(!body.nonce || typeof body.nonce != 'string') ||
|
||||||
|
|
|
@ -111,8 +111,13 @@ wsEvents.on('req:requestLogin', async (data: any, cb: (data: WSResponse) => void
|
||||||
|
|
||||||
const nonce = ulid();
|
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({
|
await bot.db.get('pending_logins').insert({
|
||||||
code,
|
code,
|
||||||
expires: Date.now() + (1000 * 60 * 15), // Expires in 15 minutes
|
expires: Date.now() + (1000 * 60 * 15), // Expires in 15 minutes
|
||||||
|
|
Loading…
Reference in a new issue