implement rate limit support for mastodon endpoints
All checks were successful
Lint / pnpm_install (pull_request) Successful in 2m26s
Test (backend) / unit (22.x) (pull_request) Successful in 7m4s
Test (backend) / e2e (22.x) (pull_request) Successful in 9m3s
Test (frontend) / vitest (22.x) (pull_request) Successful in 3m11s
Test (production install and build) / production (22.x) (pull_request) Successful in 2m58s
Test (backend) / validate-api-json (22.x) (pull_request) Successful in 3m21s
Lint / lint (backend) (pull_request) Successful in 2m40s
Lint / lint (frontend) (pull_request) Successful in 9m27s
Lint / lint (misskey-js) (pull_request) Successful in 2m54s
Lint / lint (sw) (pull_request) Successful in 2m17s
Lint / typecheck (backend) (pull_request) Successful in 3m47s
Lint / typecheck (misskey-js) (pull_request) Successful in 2m16s
All checks were successful
Lint / pnpm_install (pull_request) Successful in 2m26s
Test (backend) / unit (22.x) (pull_request) Successful in 7m4s
Test (backend) / e2e (22.x) (pull_request) Successful in 9m3s
Test (frontend) / vitest (22.x) (pull_request) Successful in 3m11s
Test (production install and build) / production (22.x) (pull_request) Successful in 2m58s
Test (backend) / validate-api-json (22.x) (pull_request) Successful in 3m21s
Lint / lint (backend) (pull_request) Successful in 2m40s
Lint / lint (frontend) (pull_request) Successful in 9m27s
Lint / lint (misskey-js) (pull_request) Successful in 2m54s
Lint / lint (sw) (pull_request) Successful in 2m17s
Lint / typecheck (backend) (pull_request) Successful in 3m47s
Lint / typecheck (misskey-js) (pull_request) Successful in 2m16s
This commit is contained in:
parent
6b4f96a94b
commit
d5983dc12e
2 changed files with 43 additions and 30 deletions
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue