From e79e978b1567b60ad5175ce8ec57750927ce0404 Mon Sep 17 00:00:00 2001 From: undefined Date: Sat, 5 Oct 2024 12:16:24 +0800 Subject: [PATCH] core: improved RateLimiter --- packages/hydrooj/package.json | 10 +++++----- packages/hydrooj/src/entry/common.ts | 2 +- packages/hydrooj/src/handler/problem.ts | 8 ++++---- packages/hydrooj/src/handler/user.ts | 16 ++++++++-------- packages/hydrooj/src/service/server.ts | 9 +++++---- 5 files changed, 23 insertions(+), 22 deletions(-) diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index 2eac321b3..254c2bf67 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -12,10 +12,10 @@ }, "preferUnplugged": true, "dependencies": { - "@aws-sdk/client-s3": "3.662.0", - "@aws-sdk/lib-storage": "3.662.0", - "@aws-sdk/s3-presigned-post": "3.662.0", - "@aws-sdk/s3-request-presigner": "3.662.0", + "@aws-sdk/client-s3": "3.664.0", + "@aws-sdk/lib-storage": "3.664.0", + "@aws-sdk/s3-presigned-post": "3.664.0", + "@aws-sdk/s3-request-presigner": "3.664.0", "@graphql-tools/schema": "^10.0.6", "@hydrooj/framework": "workspace:^", "@hydrooj/utils": "workspace:^", @@ -58,7 +58,7 @@ "@types/fs-extra": "^11.0.4", "@types/js-yaml": "^4.0.9", "@types/koa-static-cache": "^5.1.4", - "@types/lodash": "^4.17.9", + "@types/lodash": "^4.17.10", "@types/mime-types": "^2.1.4", "@types/mongodb-uri": "^0.9.4", "@types/nodemailer": "^6.4.16", diff --git a/packages/hydrooj/src/entry/common.ts b/packages/hydrooj/src/entry/common.ts index e58efab9f..5e53ab938 100644 --- a/packages/hydrooj/src/entry/common.ts +++ b/packages/hydrooj/src/entry/common.ts @@ -59,7 +59,7 @@ const getLoader = (type: LoadTask, filename: string, dontLoad = false) => async export const handler = getLoader('handler', 'handler', true); export const addon = getLoader('addon', 'index'); -export const model = getLoader('model', 'model', true); +export const model = getLoader('model', 'model'); export const lib = getLoader('lib', 'lib', true); export const script = getLoader('script', 'script', true); export const service = getLoader('service', 'service'); diff --git a/packages/hydrooj/src/handler/problem.ts b/packages/hydrooj/src/handler/problem.ts index cf9bb97df..93d905922 100644 --- a/packages/hydrooj/src/handler/problem.ts +++ b/packages/hydrooj/src/handler/problem.ts @@ -481,8 +481,8 @@ export class ProblemSubmitHandler extends ProblemDetailHandler { throw new ProblemNotAllowPretestError('type'); } } - await this.limitRate('add_record', 60, system.get('limit.submission_user'), true); - await this.limitRate('add_record', 60, system.get('limit.submission'), false); + await this.limitRate('add_record', 60, system.get('limit.submission_user'), '{{user}}'); + await this.limitRate('add_record', 60, system.get('limit.submission')); const files: Record = {}; if (!code) { const file = this.request.files?.file; @@ -554,8 +554,8 @@ export class ProblemHackHandler extends ProblemDetailHandler { @param('autoOrganizeInput', Types.Boolean, true) @param('tid', Types.ObjectId, true) async post(domainId: string, input = '', autoOrganizeInput = false, tid?: ObjectId) { - await this.limitRate('add_record', 60, system.get('limit.submission_user'), true); - await this.limitRate('add_record', 60, system.get('limit.submission'), false); + await this.limitRate('add_record', 60, system.get('limit.submission_user'), '{{user}}'); + await this.limitRate('add_record', 60, system.get('limit.submission')); const id = `${this.user._id}/${nanoid()}`; if (this.request.files?.file?.size > 0) { const file = this.request.files.file; diff --git a/packages/hydrooj/src/handler/user.ts b/packages/hydrooj/src/handler/user.ts index 5866fd224..6eb0dbcf3 100644 --- a/packages/hydrooj/src/handler/user.ts +++ b/packages/hydrooj/src/handler/user.ts @@ -58,8 +58,8 @@ class UserLoginHandler extends Handler { } } await Promise.all([ - this.limitRate('user_login', 60, 30, false), - this.limitRate(`user_login_${uname}`, 60, 5, false), + this.limitRate('user_login', 60, 30), + this.limitRate('user_login_id', 60, 5, uname), oplog.log(this, 'user.login', { redirect }), ]); if (udoc.tfa || udoc.authn) { @@ -99,7 +99,7 @@ class UserSudoHandler extends Handler { async post(domainId: string, password = '', tfa = '', authnChallenge = '') { if (!this.session.sudoArgs?.method) throw new ForbiddenError(); await Promise.all([ - this.limitRate('user_sudo', 60, 5, true), + this.limitRate('user_sudo', 60, 5, '{{user}}'), oplog.log(this, 'user.sudo', {}), ]); if (this.user.authn && authnChallenge) { @@ -206,8 +206,8 @@ export class UserRegisterHandler extends Handler { const mailDomain = mail.split('@')[1]; if (await BlackListModel.get(`mail::${mailDomain}`)) throw new BlacklistedError(mailDomain); await Promise.all([ - this.limitRate(`send_mail_${mail}`, 60, 3, false), - this.limitRate('send_mail', 3600, 30, false), + this.limitRate('send_mail', 60, 1, mail), + this.limitRate('send_mail', 3600, 30), oplog.log(this, 'user.register', {}), ]); const t = await token.add( @@ -239,7 +239,7 @@ class UserRegisterWithCodeHandler extends Handler { this.tdoc = await token.get(code, token.TYPE_REGISTRATION); if (!this.tdoc || (!this.tdoc.mail && !this.tdoc.phone)) { // prevent brute forcing tokens - await this.limitRate('user_register_with_code', 60, 5, false); + await this.limitRate('user_register_with_code', 60, 5); throw new InvalidTokenError(token.TYPE_TEXTS[token.TYPE_REGISTRATION], code); } } @@ -295,8 +295,8 @@ class UserLostPassHandler extends Handler { const udoc = await user.getByEmail('system', mail); if (!udoc) throw new UserNotFoundError(mail); await Promise.all([ - this.limitRate('send_mail', 3600, 30, false), - this.limitRate(`user_lostpass_${mail}`, 60, 3, false), + this.limitRate('send_mail', 3600, 30), + this.limitRate('send_mail', 60, 1, mail), oplog.log(this, 'user.lostpass', {}), ]); const [tid] = await token.add( diff --git a/packages/hydrooj/src/service/server.ts b/packages/hydrooj/src/service/server.ts index 48de0520c..131e18227 100644 --- a/packages/hydrooj/src/service/server.ts +++ b/packages/hydrooj/src/service/server.ts @@ -33,7 +33,7 @@ declare module '@hydrooj/framework' { paginate(cursor: FindCursor, page: number, key: string): Promise<[docs: T[], numPages: number, count: number]>; progress(message: string, params: any[]): void; - limitRate(op: string, periodSecs: number, maxOperations: number, withUserId?: boolean): Promise; + limitRate(op: string, periodSecs: number, maxOperations: number, defaultKey?: string): Promise; renderTitle(str: string): string; } } @@ -253,14 +253,15 @@ export async function apply(ctx: Context) { Hydro.model.message.sendInfo(this.user._id, JSON.stringify({ message, params })); }, async limitRate( - op: string, periodSecs: number, maxOperations: number, withUserId = system.get('limit.by_user'), + op: string, periodSecs: number, maxOperations: number, defaultKey = system.get('limit.by_user') ? '{{ip}}@{{user}}' : '{{ip}}', ) { if (ignoredLimit.includes(op)) return; if (this.user && this.user.hasPriv(PRIV.PRIV_UNLIMITED_ACCESS)) return; const overrideLimit = system.get(`limit.${op}`); if (overrideLimit) maxOperations = overrideLimit; - let id = this.request.ip; - if (withUserId) id += `@${this.user._id}`; + // deprecated: remove boolean support in future + if (typeof defaultKey === 'boolean') defaultKey = defaultKey ? '{{user}}' : '{{ip}}'; + const id = defaultKey.replace('{{ip}}', this.request.ip).replace('{{user}}', this.user._id); await opcount.inc(op, id, periodSecs, maxOperations); }, renderTitle(str: string) {