summaryrefslogtreecommitdiff
path: root/packages/backend/src/server/api
diff options
context:
space:
mode:
Diffstat (limited to 'packages/backend/src/server/api')
-rw-r--r--packages/backend/src/server/api/ApiCallService.ts22
-rw-r--r--packages/backend/src/server/api/RateLimiterService.ts110
-rw-r--r--packages/backend/src/server/api/SigninApiService.ts5
3 files changed, 60 insertions, 77 deletions
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index a42fdaf730..7a4af407a3 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -326,19 +326,15 @@ export class ApiCallService implements OnApplicationShutdown {
if (factor > 0) {
// Rate limit
- await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor).catch(err => {
- if ('info' in err) {
- // errはLimiter.LimiterInfoであることが期待される
- throw new ApiError({
- message: 'Rate limit exceeded. Please try again later.',
- code: 'RATE_LIMIT_EXCEEDED',
- id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
- httpStatusCode: 429,
- }, err.info);
- } else {
- throw new TypeError('information must be a rate-limiter information.');
- }
- });
+ const rateLimit = await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor);
+ if (rateLimit != null) {
+ throw new ApiError({
+ message: 'Rate limit exceeded. Please try again later.',
+ code: 'RATE_LIMIT_EXCEEDED',
+ id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
+ httpStatusCode: 429,
+ }, rateLimit.info);
+ }
}
}
diff --git a/packages/backend/src/server/api/RateLimiterService.ts b/packages/backend/src/server/api/RateLimiterService.ts
index 52d73baa0a..58fd6c7d8b 100644
--- a/packages/backend/src/server/api/RateLimiterService.ts
+++ b/packages/backend/src/server/api/RateLimiterService.ts
@@ -12,6 +12,14 @@ import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import type { IEndpointMeta } from './endpoints.js';
+type RateLimitInfo = {
+ code: 'BRIEF_REQUEST_INTERVAL',
+ info: Limiter.LimiterInfo,
+} | {
+ code: 'RATE_LIMIT_EXCEEDED',
+ info: Limiter.LimiterInfo,
+}
+
@Injectable()
export class RateLimiterService {
private logger: Logger;
@@ -31,77 +39,57 @@ export class RateLimiterService {
}
@bindThis
- public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string, factor = 1) {
- {
- if (this.disabled) {
- return Promise.resolve();
- }
-
- // Short-term limit
- const min = new Promise<void>((ok, reject) => {
- const minIntervalLimiter = new Limiter({
- id: `${actor}:${limitation.key}:min`,
- duration: limitation.minInterval! * factor,
- max: 1,
- db: this.redisClient,
- });
-
- minIntervalLimiter.get((err, info) => {
- if (err) {
- return reject({ code: 'ERR', info });
- }
+ private checkLimiter(options: Limiter.LimiterOption): Promise<Limiter.LimiterInfo> {
+ return new Promise<Limiter.LimiterInfo>((resolve, reject) => {
+ new Limiter(options).get((err, info) => {
+ if (err) {
+ return reject(err);
+ }
+ resolve(info);
+ });
+ });
+ }
- this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`);
+ @bindThis
+ public async limit(limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string, factor = 1): Promise<RateLimitInfo | null> {
+ if (this.disabled) {
+ return null;
+ }
- if (info.remaining === 0) {
- return reject({ code: 'BRIEF_REQUEST_INTERVAL', info });
- } else {
- if (hasLongTermLimit) {
- return max.then(ok, reject);
- } else {
- return ok();
- }
- }
- });
+ // Short-term limit
+ if (limitation.minInterval != null) {
+ const info = await this.checkLimiter({
+ id: `${actor}:${limitation.key}:min`,
+ duration: limitation.minInterval * factor,
+ max: 1,
+ db: this.redisClient,
});
- // Long term limit
- const max = new Promise<void>((ok, reject) => {
- const limiter = new Limiter({
- id: `${actor}:${limitation.key}`,
- duration: limitation.duration! * factor,
- max: limitation.max! / factor,
- db: this.redisClient,
- });
+ this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`);
- limiter.get((err, info) => {
- if (err) {
- return reject({ code: 'ERR', info });
- }
-
- this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`);
+ if (info.remaining === 0) {
+ // eslint-disable-next-line no-throw-literal
+ return { code: 'BRIEF_REQUEST_INTERVAL', info };
+ }
+ }
- if (info.remaining === 0) {
- return reject({ code: 'RATE_LIMIT_EXCEEDED', info });
- } else {
- return ok();
- }
- });
+ // Long term limit
+ if (limitation.duration != null && limitation.max != null) {
+ const info = await this.checkLimiter({
+ id: `${actor}:${limitation.key}`,
+ duration: limitation.duration,
+ max: limitation.max / factor,
+ db: this.redisClient,
});
- const hasShortTermLimit = typeof limitation.minInterval === 'number';
-
- const hasLongTermLimit =
- typeof limitation.duration === 'number' &&
- typeof limitation.max === 'number';
+ this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`);
- if (hasShortTermLimit) {
- return min;
- } else if (hasLongTermLimit) {
- return max;
- } else {
- return Promise.resolve();
+ if (info.remaining === 0) {
+ // eslint-disable-next-line no-throw-literal
+ return { code: 'RATE_LIMIT_EXCEEDED', info };
}
}
+
+ return null;
}
}
diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts
index 1d983ca4bc..3e889372d8 100644
--- a/packages/backend/src/server/api/SigninApiService.ts
+++ b/packages/backend/src/server/api/SigninApiService.ts
@@ -89,10 +89,9 @@ export class SigninApiService {
return { error };
}
- try {
// not more than 1 attempt per second and not more than 10 attempts per hour
- await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip));
- } catch (err) {
+ const rateLimit = await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip));
+ if (rateLimit != null) {
reply.code(429);
return {
error: {