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,
"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",
+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 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');
+4 -4
View File
@@ -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<string, string> = {};
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;
+8 -8
View File
@@ -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(
+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]>;
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;
}
}
@@ -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) {