add shared (cross-resource) rate limit for proxy
This commit is contained in:
parent
b4a278ae54
commit
a47590e64c
|
@ -31,6 +31,7 @@ import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.
|
||||||
import { RateLimiterService } from '@/server/api/RateLimiterService.js';
|
import { RateLimiterService } from '@/server/api/RateLimiterService.js';
|
||||||
import { getIpHash } from '@/misc/get-ip-hash.js';
|
import { getIpHash } from '@/misc/get-ip-hash.js';
|
||||||
import { AuthenticateService } from '@/server/api/AuthenticateService.js';
|
import { AuthenticateService } from '@/server/api/AuthenticateService.js';
|
||||||
|
import type { IEndpointMeta } from '@/server/api/endpoints.js';
|
||||||
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
|
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
|
||||||
import type Limiter from 'ratelimiter';
|
import type Limiter from 'ratelimiter';
|
||||||
|
|
||||||
|
@ -82,7 +83,7 @@ export class FileServerService {
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<{ Params: { key: string; } }>('/files/:key', async (request, reply) => {
|
fastify.get<{ Params: { key: string; } }>('/files/:key', async (request, reply) => {
|
||||||
if (!await this.checkRateLimit(request, reply, `/files/${request.params.key}`)) return;
|
if (!await this.checkRateLimit(request, reply, '/files/', request.params.key)) return;
|
||||||
|
|
||||||
return await this.sendDriveFile(request, reply)
|
return await this.sendDriveFile(request, reply)
|
||||||
.catch(err => this.errorHandler(request, reply, err));
|
.catch(err => this.errorHandler(request, reply, err));
|
||||||
|
@ -109,7 +110,7 @@ export class FileServerService {
|
||||||
keyUrl.username = '';
|
keyUrl.username = '';
|
||||||
keyUrl.password = '';
|
keyUrl.password = '';
|
||||||
|
|
||||||
if (!await this.checkRateLimit(request, reply, `/proxy/${keyUrl}`)) return;
|
if (!await this.checkRateLimit(request, reply, '/proxy/', keyUrl.href)) return;
|
||||||
|
|
||||||
return await this.proxyHandler(request, reply)
|
return await this.proxyHandler(request, reply)
|
||||||
.catch(err => this.errorHandler(request, reply, err));
|
.catch(err => this.errorHandler(request, reply, err));
|
||||||
|
@ -603,7 +604,8 @@ export class FileServerService {
|
||||||
Params?: Record<string, unknown> | unknown,
|
Params?: Record<string, unknown> | unknown,
|
||||||
}>,
|
}>,
|
||||||
reply: FastifyReply,
|
reply: FastifyReply,
|
||||||
rateLimitKey: string,
|
group: string,
|
||||||
|
resource: string,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const body = request.method === 'GET'
|
const body = request.method === 'GET'
|
||||||
? request.query
|
? request.query
|
||||||
|
@ -622,29 +624,48 @@ export class FileServerService {
|
||||||
const [user] = await this.authenticateService.authenticate(token);
|
const [user] = await this.authenticateService.authenticate(token);
|
||||||
const actor = user?.id ?? getIpHash(request.ip);
|
const actor = user?.id ?? getIpHash(request.ip);
|
||||||
|
|
||||||
|
// Call both limits: the per-resource limit and the shared cross-resource limit
|
||||||
|
return await this.checkResourceLimit(reply, actor, group, resource) && await this.checkSharedLimit(reply, actor, group);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async checkResourceLimit(reply: FastifyReply, actor: string, group: string, resource: string): Promise<boolean> {
|
||||||
const limit = {
|
const limit = {
|
||||||
// Group by resource
|
// Group by resource
|
||||||
key: rateLimitKey,
|
key: `${group}${resource}`,
|
||||||
|
|
||||||
// Maximum of 10 requests / 10 minutes
|
// Maximum of 10 requests / 10 minutes
|
||||||
max: 10,
|
max: 10,
|
||||||
duration: 1000 * 60 * 10,
|
duration: 1000 * 60 * 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Rate limit proxy requests
|
return await this.checkLimit(reply, actor, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async checkSharedLimit(reply: FastifyReply, actor: string, group: string): Promise<boolean> {
|
||||||
|
const limit = {
|
||||||
|
key: group,
|
||||||
|
|
||||||
|
// Maximum of 3600 requests per hour, which is an average of 1 per second.
|
||||||
|
max: 3600,
|
||||||
|
duration: 1000 * 60 * 60,
|
||||||
|
};
|
||||||
|
|
||||||
|
return await this.checkLimit(reply, actor, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async checkLimit(reply: FastifyReply, actor: string, limit: IEndpointMeta['limit'] & { key: NonNullable<string> }): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
await this.rateLimiterService.limit(limit, actor);
|
await this.rateLimiterService.limit(limit, actor);
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// errはLimiter.LimiterInfoであることが期待される
|
// errはLimiter.LimiterInfoであることが期待される
|
||||||
reply.code(429);
|
|
||||||
|
|
||||||
if (hasRateLimitInfo(err)) {
|
if (hasRateLimitInfo(err)) {
|
||||||
const cooldownInSeconds = Math.ceil((err.info.resetMs - Date.now()) / 1000);
|
const cooldownInSeconds = Math.ceil((err.info.resetMs - Date.now()) / 1000);
|
||||||
// もしかするとマイナスになる可能性がなくはないのでマイナスだったら0にしておく
|
// もしかするとマイナスになる可能性がなくはないのでマイナスだったら0にしておく
|
||||||
reply.header('Retry-After', Math.max(cooldownInSeconds, 0).toString(10));
|
reply.header('Retry-After', Math.max(cooldownInSeconds, 0).toString(10));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reply.code(429);
|
||||||
reply.send({
|
reply.send({
|
||||||
message: 'Rate limit exceeded. Please try again later.',
|
message: 'Rate limit exceeded. Please try again later.',
|
||||||
code: 'RATE_LIMIT_EXCEEDED',
|
code: 'RATE_LIMIT_EXCEEDED',
|
||||||
|
|
Loading…
Reference in New Issue