implement rate limit support for mastodon endpoints

This commit is contained in:
sugar 2025-01-11 19:03:07 +01:00
parent 6b4f96a94b
commit d5983dc12e
2 changed files with 43 additions and 30 deletions

View file

@ -198,6 +198,11 @@ export class ApiCallService implements OnApplicationShutdown {
const [user] = await this.authenticateService.authenticate(token);
let res;
try {
try {
await this.#checkRateLimit(endpoint, user, request);
} catch (e) {
throw new MastodonApiError('too many requests', 429);
}
res = await endpoint.exec(body, user, token, null, request.ip, request.headers);
} catch (e) {
if (e instanceof MastodonApiError) {
@ -273,36 +278,15 @@ export class ApiCallService implements OnApplicationShutdown {
throw new ApiError(accessDenied);
}
const bypassRateLimit = this.config.bypassRateLimit?.some(({ header, value }) => request.headers[header] === value) ?? false;
if (ep.meta.limit && !bypassRateLimit) {
// koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
let limitActor: string;
if (user) {
limitActor = user.id;
} else {
limitActor = getIpHash(request.ip);
}
const limit = Object.assign({}, ep.meta.limit);
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) {
// Rate limit
await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor).catch(err => {
try {
await this.#checkRateLimit(ep, user, request);
} catch (e) {
throw new ApiError({
message: 'Rate limit exceeded. Please try again later.',
code: 'RATE_LIMIT_EXCEEDED',
id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
httpStatusCode: 429,
});
});
}
}
if (ep.meta.requireCredential || ep.meta.requireModerator || ep.meta.requireAdmin) {
@ -454,6 +438,33 @@ export class ApiCallService implements OnApplicationShutdown {
});
}
async #checkRateLimit(ep: IEndpoint | IMastodonEndpoint, user: MiLocalUser | null | undefined, request: FastifyRequest<{ Body: Record<string, unknown> | undefined, Querystring: Record<string, unknown> }>): Promise<void> {
const bypassRateLimit = this.config.bypassRateLimit?.some(({ header, value }) => request.headers[header] === value) ?? false;
if (ep.meta.limit && !bypassRateLimit) {
// koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
let limitActor: string;
if (user) {
limitActor = user.id;
} else {
limitActor = getIpHash(request.ip);
}
const limit = Object.assign({}, ep.meta.limit);
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) {
// Rate limit
await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor);
}
}
}
@bindThis
public dispose(): void {
clearInterval(this.userIpHistoriesClearIntervalId);

View file

@ -1,5 +1,6 @@
import { Schema } from '@/misc/json-schema.js';
import * as mep___accounts_lookup_v1_get from './mastodon/accounts/lookup/v1/get.js';
import { IEndpointMeta } from './endpoints.js';
const eps = [
['GET', 'v1/accounts/lookup', mep___accounts_lookup_v1_get],
@ -7,6 +8,7 @@ const eps = [
export interface IMastodonEndpointMeta {
readonly requireCredential: boolean;
readonly limit?: IEndpointMeta['limit'];
}
export interface IMastodonEndpoint {