diff options
Diffstat (limited to 'packages/backend/src/server/api')
| -rw-r--r-- | packages/backend/src/server/api/ApiCallService.ts | 19 | ||||
| -rw-r--r-- | packages/backend/src/server/api/SkRateLimiterService.ts | 99 | ||||
| -rw-r--r-- | packages/backend/src/server/api/endpoints.ts | 26 |
3 files changed, 18 insertions, 126 deletions
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index 38d33c761d..6ad4bc8cb5 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -18,8 +18,8 @@ import { createTemp } from '@/misc/create-temp.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; import type { Config } from '@/config.js'; -import { sendRateLimitHeaders } from '@/misc/rate-limit-utils.js'; -import { LegacyRateLimit, SkRateLimiterService } from '@/server/api/SkRateLimiterService.js'; +import { RateLimit, sendRateLimitHeaders } from '@/misc/rate-limit-utils.js'; +import { SkRateLimiterService } from '@/server/api/SkRateLimiterService.js'; import { ApiError } from './error.js'; import { ApiLoggerService } from './ApiLoggerService.js'; import { AuthenticateService, AuthenticationError } from './AuthenticateService.js'; @@ -304,7 +304,7 @@ export class ApiCallService implements OnApplicationShutdown { } // For endpoints without a limit, the default is 10 calls per second - const endpointLimit: IEndpointMeta['limit'] = ep.meta.limit ?? { + const endpointLimit = ep.meta.limit ?? { duration: 1000, max: 10, }; @@ -320,18 +320,17 @@ export class ApiCallService implements OnApplicationShutdown { limitActor = getIpHash(request.ip); } - const limit = Object.assign({}, endpointLimit); - - if (limit.key == null) { - (limit as any).key = ep.name; - } - // TODO: 毎リクエスト計算するのもあれだしキャッシュしたい const factor = user ? (await this.roleService.getUserPolicies(user.id)).rateLimitFactor : 1; if (factor > 0) { + const limit = { + key: ep.name, + ...endpointLimit, + } as RateLimit; + // Rate limit - const info = await this.rateLimiterService.limit(limit as LegacyRateLimit, limitActor, factor); + const info = await this.rateLimiterService.limit(limit, limitActor, factor); sendRateLimitHeaders(reply, info); diff --git a/packages/backend/src/server/api/SkRateLimiterService.ts b/packages/backend/src/server/api/SkRateLimiterService.ts index b3c09d01c2..027d05310b 100644 --- a/packages/backend/src/server/api/SkRateLimiterService.ts +++ b/packages/backend/src/server/api/SkRateLimiterService.ts @@ -5,97 +5,12 @@ import { Inject, Injectable } from '@nestjs/common'; import Redis from 'ioredis'; -import type { IEndpointMeta } from '@/server/api/endpoints.js'; import { LoggerService } from '@/core/LoggerService.js'; import { TimeService } from '@/core/TimeService.js'; import { EnvService } from '@/core/EnvService.js'; import { DI } from '@/di-symbols.js'; import type Logger from '@/logger.js'; - -/** - * Metadata about the current status of a rate limiter - */ -export interface LimitInfo { - /** - * True if the limit has been reached, and the call should be blocked. - */ - blocked: boolean; - - /** - * Number of calls that can be made before the limit is triggered. - */ - remaining: number; - - /** - * Time in seconds until the next call can be made, or zero if the next call can be made immediately. - * Rounded up to the nearest second. - */ - resetSec: number; - - /** - * Time in milliseconds until the next call can be made, or zero if the next call can be made immediately. - * Rounded up to the nearest milliseconds. - */ - resetMs: number; - - /** - * Time in seconds until the limit has fully reset. - * Rounded up to the nearest second. - */ - fullResetSec: number; - - /** - * Time in milliseconds until the limit has fully reset. - * Rounded up to the nearest millisecond. - */ - fullResetMs: number; -} - -/** - * Rate limit based on "leaky bucket" logic. - * The bucket count increases with each call, and decreases gradually at a given rate. - * The subject is blocked until the bucket count drops below the limit. - */ -export interface RateLimit { - /** - * Unique key identifying the particular resource (or resource group) being limited. - */ - key: string; - - /** - * Constant value identifying the type of rate limit. - */ - type: 'bucket'; - - /** - * Size of the bucket, in number of requests. - * The subject will be blocked when the number of calls exceeds this size. - */ - size: number; - - /** - * How often the bucket should "drip" and reduce the counter, measured in milliseconds. - * Defaults to 1000 (1 second). - */ - dripRate?: number; - - /** - * Amount to reduce the counter on each drip. - * Defaults to 1. - */ - dripSize?: number; -} - -export type SupportedRateLimit = RateLimit | LegacyRateLimit; -export type LegacyRateLimit = IEndpointMeta['limit'] & { key: NonNullable<string>, type?: undefined }; - -export function isLegacyRateLimit(limit: SupportedRateLimit): limit is LegacyRateLimit { - return limit.type === undefined; -} - -export function hasMinLimit(limit: LegacyRateLimit): limit is LegacyRateLimit & { minInterval: number } { - return !!limit.minInterval; -} +import { BucketRateLimit, LegacyRateLimit, LimitInfo, RateLimit, hasMinLimit, isLegacyRateLimit } from '@/misc/rate-limit-utils.js'; @Injectable() export class SkRateLimiterService { @@ -116,10 +31,10 @@ export class SkRateLimiterService { envService: EnvService, ) { this.logger = loggerService.getLogger('limiter'); - this.disabled = envService.env.NODE_ENV !== 'production'; + this.disabled = envService.env.NODE_ENV !== 'production'; // TODO disable in TEST *only* } - public async limit(limit: SupportedRateLimit, actor: string, factor = 1): Promise<LimitInfo> { + public async limit(limit: RateLimit, actor: string, factor = 1): Promise<LimitInfo> { if (this.disabled) { return { blocked: false, @@ -211,7 +126,7 @@ export class SkRateLimiterService { return limitInfo; } - private async limitBucket(limit: RateLimit, actor: string, factor: number): Promise<LimitInfo> { + private async limitBucket(limit: BucketRateLimit, actor: string, factor: number): Promise<LimitInfo> { if (limit.size < 1) throw new Error(`Invalid rate limit ${limit.key}: size is less than 1 (${limit.size})`); if (limit.dripRate != null && limit.dripRate < 1) throw new Error(`Invalid rate limit ${limit.key}: dripRate is less than 1 (${limit.dripRate})`); if (limit.dripSize != null && limit.dripSize < 1) throw new Error(`Invalid rate limit ${limit.key}: dripSize is less than 1 (${limit.dripSize})`); @@ -251,7 +166,7 @@ export class SkRateLimiterService { return limitInfo; } - private async getLimitCounter(limit: SupportedRateLimit, actor: string, subject: string): Promise<LimitCounter> { + private async getLimitCounter(limit: RateLimit, actor: string, subject: string): Promise<LimitCounter> { const key = createLimitKey(limit, actor, subject); const value = await this.redisClient.get(key); @@ -262,7 +177,7 @@ export class SkRateLimiterService { return JSON.parse(value); } - private async setLimitCounter(limit: SupportedRateLimit, actor: string, counter: LimitCounter, expiration: number, subject: string): Promise<void> { + private async setLimitCounter(limit: RateLimit, actor: string, counter: LimitCounter, expiration: number, subject: string): Promise<void> { const key = createLimitKey(limit, actor, subject); const value = JSON.stringify(counter); const expirationSec = Math.max(expiration, 1); @@ -270,7 +185,7 @@ export class SkRateLimiterService { } } -function createLimitKey(limit: SupportedRateLimit, actor: string, subject: string): string { +function createLimitKey(limit: RateLimit, actor: string, subject: string): string { return `rl_${actor}_${limit.key}_${subject}`; } diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 14e002929a..a4193a64ec 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -5,6 +5,7 @@ import { permissions } from 'misskey-js'; import type { KeyOf, Schema } from '@/misc/json-schema.js'; +import type { RateLimit } from '@/misc/rate-limit-utils.js'; import * as ep___admin_abuseReport_notificationRecipient_list from '@/server/api/endpoints/admin/abuse-report/notification-recipient/list.js'; @@ -855,30 +856,7 @@ interface IEndpointMetaBase { * エンドポイントのリミテーションに関するやつ * 省略した場合はリミテーションは無いものとして解釈されます。 */ - readonly limit?: { - - /** - * 複数のエンドポイントでリミットを共有したい場合に指定するキー - */ - readonly key?: string; - - /** - * リミットを適用する期間(ms) - * このプロパティを設定する場合、max プロパティも設定する必要があります。 - */ - readonly duration?: number; - - /** - * durationで指定した期間内にいくつまでリクエストできるのか - * このプロパティを設定する場合、duration プロパティも設定する必要があります。 - */ - readonly max?: number; - - /** - * 最低でもどれくらいの間隔を開けてリクエストしなければならないか(ms) - */ - readonly minInterval?: number; - }; + readonly limit?: Readonly<RateLimit | Omit<RateLimit, 'key'>>; /** * ファイルの添付を必要とするか否か |