core: improved RateLimiter

This commit is contained in:
undefined committed 2024-10-05 12:16:24 +08:00
1 parent c227b6dd34
commit e79e978b15
5 files changed
+23 -22

No files matched your search

+5 -5
View File
@@ -12,10 +12,10 @@
}, },
"preferUnplugged": true, "preferUnplugged": true,
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "3.662.0", "@aws-sdk/client-s3": "3.664.0",
"@aws-sdk/lib-storage": "3.662.0", "@aws-sdk/lib-storage": "3.664.0",
"@aws-sdk/s3-presigned-post": "3.662.0", "@aws-sdk/s3-presigned-post": "3.664.0",
"@aws-sdk/s3-request-presigner": "3.662.0", "@aws-sdk/s3-request-presigner": "3.664.0",
"@graphql-tools/schema": "^10.0.6", "@graphql-tools/schema": "^10.0.6",
"@hydrooj/framework": "workspace:^", "@hydrooj/framework": "workspace:^",
"@hydrooj/utils": "workspace:^", "@hydrooj/utils": "workspace:^",
@@ -58,7 +58,7 @@
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/koa-static-cache": "^5.1.4", "@types/koa-static-cache": "^5.1.4",
"@types/lodash": "^4.17.9", "@types/lodash": "^4.17.10",
"@types/mime-types": "^2.1.4", "@types/mime-types": "^2.1.4",
"@types/mongodb-uri": "^0.9.4", "@types/mongodb-uri": "^0.9.4",
"@types/nodemailer": "^6.4.16", "@types/nodemailer": "^6.4.16",
+1 -1
View File
@@ -59,7 +59,7 @@ const getLoader = (type: LoadTask, filename: string, dontLoad = false) => async
export const handler = getLoader('handler', 'handler', true); export const handler = getLoader('handler', 'handler', true);
export const addon = getLoader('addon', 'index'); 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 lib = getLoader('lib', 'lib', true);
export const script = getLoader('script', 'script', true); export const script = getLoader('script', 'script', true);
export const service = getLoader('service', 'service'); export const service = getLoader('service', 'service');
+4 -4
View File
@@ -481,8 +481,8 @@ export class ProblemSubmitHandler extends ProblemDetailHandler {
throw new ProblemNotAllowPretestError('type'); 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_user'), '{{user}}');
await this.limitRate('add_record', 60, system.get('limit.submission'), false); await this.limitRate('add_record', 60, system.get('limit.submission'));
const files: Record<string, string> = {}; const files: Record<string, string> = {};
if (!code) { if (!code) {
const file = this.request.files?.file; const file = this.request.files?.file;
@@ -554,8 +554,8 @@ export class ProblemHackHandler extends ProblemDetailHandler {
@param('autoOrganizeInput', Types.Boolean, true) @param('autoOrganizeInput', Types.Boolean, true)
@param('tid', Types.ObjectId, true) @param('tid', Types.ObjectId, true)
async post(domainId: string, input = '', autoOrganizeInput = false, tid?: ObjectId) { 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_user'), '{{user}}');
await this.limitRate('add_record', 60, system.get('limit.submission'), false); await this.limitRate('add_record', 60, system.get('limit.submission'));
const id = `${this.user._id}/${nanoid()}`; const id = `${this.user._id}/${nanoid()}`;
if (this.request.files?.file?.size > 0) { if (this.request.files?.file?.size > 0) {
const file = this.request.files.file; const file = this.request.files.file;
+8 -8
View File
@@ -58,8 +58,8 @@ class UserLoginHandler extends Handler {
} }
} }
await Promise.all([ await Promise.all([
this.limitRate('user_login', 60, 30, false), this.limitRate('user_login', 60, 30),
this.limitRate(`user_login_${uname}`, 60, 5, false), this.limitRate('user_login_id', 60, 5, uname),
oplog.log(this, 'user.login', { redirect }), oplog.log(this, 'user.login', { redirect }),
]); ]);
if (udoc.tfa || udoc.authn) { if (udoc.tfa || udoc.authn) {
@@ -99,7 +99,7 @@ class UserSudoHandler extends Handler {
async post(domainId: string, password = '', tfa = '', authnChallenge = '') { async post(domainId: string, password = '', tfa = '', authnChallenge = '') {
if (!this.session.sudoArgs?.method) throw new ForbiddenError(); if (!this.session.sudoArgs?.method) throw new ForbiddenError();
await Promise.all([ await Promise.all([
this.limitRate('user_sudo', 60, 5, true), this.limitRate('user_sudo', 60, 5, '{{user}}'),
oplog.log(this, 'user.sudo', {}), oplog.log(this, 'user.sudo', {}),
]); ]);
if (this.user.authn && authnChallenge) { if (this.user.authn && authnChallenge) {
@@ -206,8 +206,8 @@ export class UserRegisterHandler extends Handler {
const mailDomain = mail.split('@')[1]; const mailDomain = mail.split('@')[1];
if (await BlackListModel.get(`mail::${mailDomain}`)) throw new BlacklistedError(mailDomain); if (await BlackListModel.get(`mail::${mailDomain}`)) throw new BlacklistedError(mailDomain);
await Promise.all([ await Promise.all([
this.limitRate(`send_mail_${mail}`, 60, 3, false), this.limitRate('send_mail', 60, 1, mail),
this.limitRate('send_mail', 3600, 30, false), this.limitRate('send_mail', 3600, 30),
oplog.log(this, 'user.register', {}), oplog.log(this, 'user.register', {}),
]); ]);
const t = await token.add( const t = await token.add(
@@ -239,7 +239,7 @@ class UserRegisterWithCodeHandler extends Handler {
this.tdoc = await token.get(code, token.TYPE_REGISTRATION); this.tdoc = await token.get(code, token.TYPE_REGISTRATION);
if (!this.tdoc || (!this.tdoc.mail && !this.tdoc.phone)) { if (!this.tdoc || (!this.tdoc.mail && !this.tdoc.phone)) {
// prevent brute forcing tokens // 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); 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); const udoc = await user.getByEmail('system', mail);
if (!udoc) throw new UserNotFoundError(mail); if (!udoc) throw new UserNotFoundError(mail);
await Promise.all([ await Promise.all([
this.limitRate('send_mail', 3600, 30, false), this.limitRate('send_mail', 3600, 30),
this.limitRate(`user_lostpass_${mail}`, 60, 3, false), this.limitRate('send_mail', 60, 1, mail),
oplog.log(this, 'user.lostpass', {}), oplog.log(this, 'user.lostpass', {}),
]); ]);
const [tid] = await token.add( const [tid] = await token.add(
+5 -4
View File
@@ -33,7 +33,7 @@ declare module '@hydrooj/framework' {
paginate<T>(cursor: FindCursor<T>, page: number, key: string): Promise<[docs: T[], numPages: number, count: number]>; paginate<T>(cursor: FindCursor<T>, page: number, key: string): Promise<[docs: T[], numPages: number, count: number]>;
progress(message: string, params: any[]): void; progress(message: string, params: any[]): void;
limitRate(op: string, periodSecs: number, maxOperations: number, withUserId?: boolean): Promise<void>; limitRate(op: string, periodSecs: number, maxOperations: number, defaultKey?: string): Promise<void>;
renderTitle(str: string): string; 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 })); Hydro.model.message.sendInfo(this.user._id, JSON.stringify({ message, params }));
}, },
async limitRate( 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 (ignoredLimit.includes(op)) return;
if (this.user && this.user.hasPriv(PRIV.PRIV_UNLIMITED_ACCESS)) return; if (this.user && this.user.hasPriv(PRIV.PRIV_UNLIMITED_ACCESS)) return;
const overrideLimit = system.get(`limit.${op}`); const overrideLimit = system.get(`limit.${op}`);
if (overrideLimit) maxOperations = overrideLimit; if (overrideLimit) maxOperations = overrideLimit;
let id = this.request.ip; // deprecated: remove boolean support in future
if (withUserId) id += `@${this.user._id}`; 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); await opcount.inc(op, id, periodSecs, maxOperations);
}, },
renderTitle(str: string) { renderTitle(str: string) {