summaryrefslogtreecommitdiff
path: root/packages/backend/src/server/api
diff options
context:
space:
mode:
authordakkar <dakkar@thenautilus.net>2024-12-09 09:43:55 +0000
committerdakkar <dakkar@thenautilus.net>2024-12-09 09:43:55 +0000
commit1837ccc618859e425766a66ff597b9f11b3e4e49 (patch)
tree7dc17bd0205e53c180bb5ff912fe3a491c8a2acd /packages/backend/src/server/api
parentfix a bunch of CSS variables (diff)
parentmerge: Implement new SkRateLimiterServer with Leaky Bucket rate limits (resol... (diff)
downloadsharkey-1837ccc618859e425766a66ff597b9f11b3e4e49.tar.gz
sharkey-1837ccc618859e425766a66ff597b9f11b3e4e49.tar.bz2
sharkey-1837ccc618859e425766a66ff597b9f11b3e4e49.zip
Merge branch 'develop' into feature/2024.10
Diffstat (limited to 'packages/backend/src/server/api')
-rw-r--r--packages/backend/src/server/api/ApiCallService.ts58
-rw-r--r--packages/backend/src/server/api/RateLimiterService.ts4
-rw-r--r--packages/backend/src/server/api/SigninApiService.ts16
-rw-r--r--packages/backend/src/server/api/SigninWithPasskeyApiService.ts17
-rw-r--r--packages/backend/src/server/api/SkRateLimiterService.ts198
-rw-r--r--packages/backend/src/server/api/StreamingApiServerService.ts20
-rw-r--r--packages/backend/src/server/api/endpoints.ts26
-rw-r--r--packages/backend/src/server/api/endpoints/endpoint.ts9
8 files changed, 262 insertions, 86 deletions
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index 6f51825494..c6c33f7303 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -18,8 +18,9 @@ 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 { SkRateLimiterService } from '@/server/api/SkRateLimiterService.js';
import { ApiError } from './error.js';
-import { RateLimiterService } from './RateLimiterService.js';
import { ApiLoggerService } from './ApiLoggerService.js';
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
import type { FastifyRequest, FastifyReply } from 'fastify';
@@ -49,7 +50,7 @@ export class ApiCallService implements OnApplicationShutdown {
private userIpsRepository: UserIpsRepository,
private authenticateService: AuthenticateService,
- private rateLimiterService: RateLimiterService,
+ private rateLimiterService: SkRateLimiterService,
private roleService: RoleService,
private apiLoggerService: ApiLoggerService,
) {
@@ -65,16 +66,6 @@ export class ApiCallService implements OnApplicationShutdown {
let statusCode = err.httpStatusCode;
if (err.httpStatusCode === 401) {
reply.header('WWW-Authenticate', 'Bearer realm="Misskey"');
- } else if (err.code === 'RATE_LIMIT_EXCEEDED') {
- const info: unknown = err.info;
- const unixEpochInSeconds = Date.now();
- if (typeof(info) === 'object' && info && 'resetMs' in info && typeof(info.resetMs) === 'number') {
- const cooldownInSeconds = Math.ceil((info.resetMs - unixEpochInSeconds) / 1000);
- // もしかするとマイナスになる可能性がなくはないのでマイナスだったら0にしておく
- reply.header('Retry-After', Math.max(cooldownInSeconds, 0).toString(10));
- } else {
- this.logger.warn(`rate limit information has unexpected type ${typeof(err.info?.reset)}`);
- }
} else if (err.kind === 'client') {
reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="invalid_request", error_description="${err.message}"`);
statusCode = statusCode ?? 400;
@@ -168,7 +159,7 @@ export class ApiCallService implements OnApplicationShutdown {
return;
}
this.authenticateService.authenticate(token).then(([user, app]) => {
- this.call(endpoint, user, app, body, null, request).then((res) => {
+ this.call(endpoint, user, app, body, null, request, reply).then((res) => {
if (request.method === 'GET' && endpoint.meta.cacheSec && !token && !user) {
reply.header('Cache-Control', `public, max-age=${endpoint.meta.cacheSec}`);
}
@@ -229,7 +220,7 @@ export class ApiCallService implements OnApplicationShutdown {
this.call(endpoint, user, app, fields, {
name: multipartData.filename,
path: path,
- }, request).then((res) => {
+ }, request, reply).then((res) => {
this.send(reply, res);
}).catch((err: ApiError) => {
this.#sendApiError(reply, err);
@@ -304,6 +295,7 @@ export class ApiCallService implements OnApplicationShutdown {
path: string;
} | null,
request: FastifyRequest<{ Body: Record<string, unknown> | undefined, Querystring: Record<string, unknown> }>,
+ reply: FastifyReply,
) {
const isSecure = user != null && token == null;
@@ -312,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,
};
@@ -328,30 +320,28 @@ 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,
+ };
+
// 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 info = await this.rateLimiterService.limit(limit, limitActor, factor);
+
+ sendRateLimitHeaders(reply, info);
+
+ if (info.blocked) {
+ throw new ApiError({
+ message: 'Rate limit exceeded. Please try again later.',
+ code: 'RATE_LIMIT_EXCEEDED',
+ id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
+ httpStatusCode: 429,
+ });
+ }
}
}
diff --git a/packages/backend/src/server/api/RateLimiterService.ts b/packages/backend/src/server/api/RateLimiterService.ts
index e9afb9d05a..879529090f 100644
--- a/packages/backend/src/server/api/RateLimiterService.ts
+++ b/packages/backend/src/server/api/RateLimiterService.ts
@@ -10,8 +10,10 @@ import { DI } from '@/di-symbols.js';
import type Logger from '@/logger.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
+import { LegacyRateLimit } from '@/misc/rate-limit-utils.js';
import type { IEndpointMeta } from './endpoints.js';
+/** @deprecated Use SkRateLimiterService instead */
@Injectable()
export class RateLimiterService {
private logger: Logger;
@@ -31,7 +33,7 @@ export class RateLimiterService {
}
@bindThis
- public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string, factor = 1) {
+ public limit(limitation: LegacyRateLimit & { key: NonNullable<string> }, actor: string, factor = 1) {
return new Promise<void>((ok, reject) => {
if (this.disabled) ok();
diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts
index 2945384e14..d1e58fb536 100644
--- a/packages/backend/src/server/api/SigninApiService.ts
+++ b/packages/backend/src/server/api/SigninApiService.ts
@@ -25,11 +25,13 @@ import { WebAuthnService } from '@/core/WebAuthnService.js';
import { UserAuthService } from '@/core/UserAuthService.js';
import { CaptchaService } from '@/core/CaptchaService.js';
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
-import { RateLimiterService } from './RateLimiterService.js';
+import { isSystemAccount } from '@/misc/is-system-account.js';
+import type { MiMeta } from '@/models/_.js';
+import { SkRateLimiterService } from '@/server/api/SkRateLimiterService.js';
+import { sendRateLimitHeaders } from '@/misc/rate-limit-utils.js';
import { SigninService } from './SigninService.js';
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
import type { FastifyReply, FastifyRequest } from 'fastify';
-import { isSystemAccount } from '@/misc/is-system-account.js';
@Injectable()
export class SigninApiService {
@@ -53,7 +55,7 @@ export class SigninApiService {
private signinsRepository: SigninsRepository,
private idService: IdService,
- private rateLimiterService: RateLimiterService,
+ private rateLimiterService: SkRateLimiterService,
private signinService: SigninService,
private userAuthService: UserAuthService,
private webAuthnService: WebAuthnService,
@@ -92,10 +94,12 @@ 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));
+
+ sendRateLimitHeaders(reply, rateLimit);
+
+ if (rateLimit.blocked) {
reply.code(429);
return {
error: {
diff --git a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts
index 9ba23c54e2..e94d2b6b68 100644
--- a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts
+++ b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts
@@ -21,7 +21,8 @@ import { WebAuthnService } from '@/core/WebAuthnService.js';
import Logger from '@/logger.js';
import { LoggerService } from '@/core/LoggerService.js';
import type { IdentifiableError } from '@/misc/identifiable-error.js';
-import { RateLimiterService } from './RateLimiterService.js';
+import { SkRateLimiterService } from '@/server/api/SkRateLimiterService.js';
+import { sendRateLimitHeaders } from '@/misc/rate-limit-utils.js';
import { SigninService } from './SigninService.js';
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
import type { FastifyReply, FastifyRequest } from 'fastify';
@@ -43,7 +44,7 @@ export class SigninWithPasskeyApiService {
private signinsRepository: SigninsRepository,
private idService: IdService,
- private rateLimiterService: RateLimiterService,
+ private rateLimiterService: SkRateLimiterService,
private signinService: SigninService,
private webAuthnService: WebAuthnService,
private loggerService: LoggerService,
@@ -84,11 +85,13 @@ export class SigninWithPasskeyApiService {
return error(status ?? 500, failure ?? { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' });
};
- try {
- // Not more than 1 API call per 250ms and not more than 100 attempts per 30min
- // NOTE: 1 Sign-in require 2 API calls
- await this.rateLimiterService.limit({ key: 'signin-with-passkey', duration: 60 * 30 * 1000, max: 200, minInterval: 250 }, getIpHash(request.ip));
- } catch (err) {
+ // Not more than 1 API call per 250ms and not more than 100 attempts per 30min
+ // NOTE: 1 Sign-in require 2 API calls
+ const rateLimit = await this.rateLimiterService.limit({ key: 'signin-with-passkey', duration: 60 * 30 * 1000, max: 200, minInterval: 250 }, getIpHash(request.ip));
+
+ sendRateLimitHeaders(reply, rateLimit);
+
+ if (rateLimit.blocked) {
reply.code(429);
return {
error: {
diff --git a/packages/backend/src/server/api/SkRateLimiterService.ts b/packages/backend/src/server/api/SkRateLimiterService.ts
new file mode 100644
index 0000000000..6415ee905c
--- /dev/null
+++ b/packages/backend/src/server/api/SkRateLimiterService.ts
@@ -0,0 +1,198 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import Redis from 'ioredis';
+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';
+import { BucketRateLimit, LegacyRateLimit, LimitInfo, RateLimit, hasMinLimit, isLegacyRateLimit, Keyed } from '@/misc/rate-limit-utils.js';
+
+@Injectable()
+export class SkRateLimiterService {
+ private readonly logger: Logger;
+ private readonly disabled: boolean;
+
+ constructor(
+ @Inject(TimeService)
+ private readonly timeService: TimeService,
+
+ @Inject(DI.redis)
+ private readonly redisClient: Redis.Redis,
+
+ @Inject(LoggerService)
+ loggerService: LoggerService,
+
+ @Inject(EnvService)
+ envService: EnvService,
+ ) {
+ this.logger = loggerService.getLogger('limiter');
+ this.disabled = envService.env.NODE_ENV !== 'production'; // TODO disable in TEST *only*
+ }
+
+ public async limit(limit: Keyed<RateLimit>, actor: string, factor = 1): Promise<LimitInfo> {
+ if (this.disabled || factor === 0) {
+ return {
+ blocked: false,
+ remaining: Number.MAX_SAFE_INTEGER,
+ resetSec: 0,
+ resetMs: 0,
+ fullResetSec: 0,
+ fullResetMs: 0,
+ };
+ }
+
+ if (factor < 0) {
+ throw new Error(`Rate limit factor is zero or negative: ${factor}`);
+ }
+
+ if (isLegacyRateLimit(limit)) {
+ return await this.limitLegacy(limit, actor, factor);
+ } else {
+ return await this.limitBucket(limit, actor, factor);
+ }
+ }
+
+ private async limitLegacy(limit: Keyed<LegacyRateLimit>, actor: string, factor: number): Promise<LimitInfo> {
+ const promises: Promise<LimitInfo | null>[] = [];
+
+ // The "min" limit - if present - is handled directly.
+ if (hasMinLimit(limit)) {
+ promises.push(
+ this.limitMin(limit, actor, factor),
+ );
+ }
+
+ // Convert the "max" limit into a leaky bucket with 1 drip / second rate.
+ if (limit.max != null && limit.duration != null) {
+ promises.push(
+ this.limitBucket({
+ type: 'bucket',
+ key: limit.key,
+ size: limit.max,
+ dripRate: Math.max(Math.round(limit.duration / limit.max), 1),
+ }, actor, factor),
+ );
+ }
+
+ const [lim1, lim2] = await Promise.all(promises);
+ return {
+ blocked: (lim1?.blocked || lim2?.blocked) ?? false,
+ remaining: Math.min(lim1?.remaining ?? Number.MAX_SAFE_INTEGER, lim2?.remaining ?? Number.MAX_SAFE_INTEGER),
+ resetSec: Math.max(lim1?.resetSec ?? 0, lim2?.resetSec ?? 0),
+ resetMs: Math.max(lim1?.resetMs ?? 0, lim2?.resetMs ?? 0),
+ fullResetSec: Math.max(lim1?.fullResetSec ?? 0, lim2?.fullResetSec ?? 0),
+ fullResetMs: Math.max(lim1?.fullResetMs ?? 0, lim2?.fullResetMs ?? 0),
+ };
+ }
+
+ private async limitMin(limit: Keyed<LegacyRateLimit> & { minInterval: number }, actor: string, factor: number): Promise<LimitInfo | null> {
+ if (limit.minInterval === 0) return null;
+ if (limit.minInterval < 0) throw new Error(`Invalid rate limit ${limit.key}: minInterval is negative (${limit.minInterval})`);
+
+ const counter = await this.getLimitCounter(limit, actor, 'min');
+ const minInterval = Math.max(Math.ceil(limit.minInterval * factor), 0);
+
+ // Update expiration
+ if (counter.c > 0) {
+ const isCleared = this.timeService.now - counter.t >= minInterval;
+ if (isCleared) {
+ counter.c = 0;
+ }
+ }
+
+ const blocked = counter.c > 0;
+ if (!blocked) {
+ counter.c++;
+ counter.t = this.timeService.now;
+ }
+
+ // Calculate limit status
+ const resetMs = Math.max(Math.ceil(minInterval - (this.timeService.now - counter.t)), 0);
+ const resetSec = Math.ceil(resetMs / 1000);
+ const limitInfo: LimitInfo = { blocked, remaining: 0, resetSec, resetMs, fullResetSec: resetSec, fullResetMs: resetMs };
+
+ // Update the limit counter, but not if blocked
+ if (!blocked) {
+ // Don't await, or we will slow down the API.
+ this.setLimitCounter(limit, actor, counter, resetSec, 'min')
+ .catch(err => this.logger.error(`Failed to update limit ${limit.key}:min for ${actor}:`, err));
+ }
+
+ return limitInfo;
+ }
+
+ private async limitBucket(limit: Keyed<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})`);
+
+ const counter = await this.getLimitCounter(limit, actor, 'bucket');
+ const bucketSize = Math.max(Math.ceil(limit.size / factor), 1);
+ const dripRate = Math.ceil(limit.dripRate ?? 1000);
+ const dripSize = Math.ceil(limit.dripSize ?? 1);
+
+ // Update drips
+ if (counter.c > 0) {
+ const dripsSinceLastTick = Math.floor((this.timeService.now - counter.t) / dripRate) * dripSize;
+ counter.c = Math.max(counter.c - dripsSinceLastTick, 0);
+ }
+
+ const blocked = counter.c >= bucketSize;
+ if (!blocked) {
+ counter.c++;
+ counter.t = this.timeService.now;
+ }
+
+ // Calculate limit status
+ const remaining = Math.max(bucketSize - counter.c, 0);
+ const resetMs = remaining > 0 ? 0 : Math.max(dripRate - (this.timeService.now - counter.t), 0);
+ const resetSec = Math.ceil(resetMs / 1000);
+ const fullResetMs = Math.ceil(counter.c / dripSize) * dripRate;
+ const fullResetSec = Math.ceil(fullResetMs / 1000);
+ const limitInfo: LimitInfo = { blocked, remaining, resetSec, resetMs, fullResetSec, fullResetMs };
+
+ // Update the limit counter, but not if blocked
+ if (!blocked) {
+ // Don't await, or we will slow down the API.
+ this.setLimitCounter(limit, actor, counter, fullResetSec, 'bucket')
+ .catch(err => this.logger.error(`Failed to update limit ${limit.key} for ${actor}:`, err));
+ }
+
+ return limitInfo;
+ }
+
+ private async getLimitCounter(limit: Keyed<RateLimit>, actor: string, subject: string): Promise<LimitCounter> {
+ const key = createLimitKey(limit, actor, subject);
+
+ const value = await this.redisClient.get(key);
+ if (value == null) {
+ return { t: 0, c: 0 };
+ }
+
+ return JSON.parse(value);
+ }
+
+ private async setLimitCounter(limit: Keyed<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);
+ await this.redisClient.set(key, value, 'EX', expirationSec);
+ }
+}
+
+function createLimitKey(limit: Keyed<RateLimit>, actor: string, subject: string): string {
+ return `rl_${actor}_${limit.key}_${subject}`;
+}
+
+export interface LimitCounter {
+ /** Timestamp */
+ t: number;
+
+ /** Counter */
+ c: number;
+}
diff --git a/packages/backend/src/server/api/StreamingApiServerService.ts b/packages/backend/src/server/api/StreamingApiServerService.ts
index 9b8464f705..e3fd1312ae 100644
--- a/packages/backend/src/server/api/StreamingApiServerService.ts
+++ b/packages/backend/src/server/api/StreamingApiServerService.ts
@@ -7,6 +7,8 @@ import { EventEmitter } from 'events';
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import * as WebSocket from 'ws';
+import proxyAddr from 'proxy-addr';
+import ms from 'ms';
import { DI } from '@/di-symbols.js';
import type { UsersRepository, MiAccessToken } from '@/models/_.js';
import { NoteReadService } from '@/core/NoteReadService.js';
@@ -16,18 +18,15 @@ import { CacheService } from '@/core/CacheService.js';
import { MiLocalUser } from '@/models/User.js';
import { UserService } from '@/core/UserService.js';
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
+import { RoleService } from '@/core/RoleService.js';
+import { getIpHash } from '@/misc/get-ip-hash.js';
+import { LoggerService } from '@/core/LoggerService.js';
+import { SkRateLimiterService } from '@/server/api/SkRateLimiterService.js';
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
import MainStreamConnection from './stream/Connection.js';
import { ChannelsService } from './stream/ChannelsService.js';
-import { RateLimiterService } from './RateLimiterService.js';
-import { RoleService } from '@/core/RoleService.js';
-import { getIpHash } from '@/misc/get-ip-hash.js';
-import proxyAddr from 'proxy-addr';
-import ms from 'ms';
import type * as http from 'node:http';
import type { IEndpointMeta } from './endpoints.js';
-import { LoggerService } from '@/core/LoggerService.js';
-import type Logger from '@/logger.js';
@Injectable()
export class StreamingApiServerService {
@@ -49,7 +48,7 @@ export class StreamingApiServerService {
private notificationService: NotificationService,
private usersService: UserService,
private channelFollowingService: ChannelFollowingService,
- private rateLimiterService: RateLimiterService,
+ private rateLimiterService: SkRateLimiterService,
private roleService: RoleService,
private loggerService: LoggerService,
) {
@@ -73,9 +72,8 @@ export class StreamingApiServerService {
if (factor <= 0) return false;
// Rate limit
- return await this.rateLimiterService.limit(limit, limitActor, factor)
- .then(() => { return false; })
- .catch(err => { return true; });
+ const rateLimit = await this.rateLimiterService.limit(limit, limitActor, factor);
+ return rateLimit.blocked;
}
@bindThis
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 3dc287331c..1a93c53283 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';
@@ -859,30 +860,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>;
/**
* ファイルの添付を必要とするか否か
diff --git a/packages/backend/src/server/api/endpoints/endpoint.ts b/packages/backend/src/server/api/endpoints/endpoint.ts
index 7629cd7a67..a1dbb26431 100644
--- a/packages/backend/src/server/api/endpoints/endpoint.ts
+++ b/packages/backend/src/server/api/endpoints/endpoint.ts
@@ -29,10 +29,13 @@ export const meta = {
},
},
- // 5 calls per second
+ // 1000 max @ 1/10ms drip = 10/sec average.
+ // Large bucket is ok because this is a fairly lightweight endpoint.
limit: {
- duration: 1000,
- max: 5,
+ type: 'bucket',
+
+ size: 1000,
+ dripRate: 10,
},
} as const;