summaryrefslogtreecommitdiff
path: root/packages/backend/src/server/api
diff options
context:
space:
mode:
authorHazelnoot <acomputerdog@gmail.com>2024-12-07 13:13:19 -0500
committerHazelnoot <acomputerdog@gmail.com>2024-12-07 13:13:19 -0500
commitf6b256620b9637ffe4bd29a07cfba1a7880c9bb1 (patch)
tree8f2e8ffee698b1f843079297921bb34c8997f876 /packages/backend/src/server/api
parentrespect rate limit factor in FileServerService (diff)
downloadsharkey-f6b256620b9637ffe4bd29a07cfba1a7880c9bb1.tar.gz
sharkey-f6b256620b9637ffe4bd29a07cfba1a7880c9bb1.tar.bz2
sharkey-f6b256620b9637ffe4bd29a07cfba1a7880c9bb1.zip
separate SkRateLimiterService from RateLimiterService and update all usages
Diffstat (limited to 'packages/backend/src/server/api')
-rw-r--r--packages/backend/src/server/api/ApiCallService.ts53
-rw-r--r--packages/backend/src/server/api/RateLimiterService.ts1
-rw-r--r--packages/backend/src/server/api/SigninApiService.ts17
-rw-r--r--packages/backend/src/server/api/SigninWithPasskeyApiService.ts17
-rw-r--r--packages/backend/src/server/api/SkRateLimiterService.ts28
-rw-r--r--packages/backend/src/server/api/StreamingApiServerService.ts20
6 files changed, 47 insertions, 89 deletions
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index 14367e02bb..38d33c761d 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -8,7 +8,6 @@ import * as fs from 'node:fs';
import * as stream from 'node:stream/promises';
import { Inject, Injectable } from '@nestjs/common';
import * as Sentry from '@sentry/node';
-import { LimiterInfo } from 'ratelimiter';
import { DI } from '@/di-symbols.js';
import { getIpHash } from '@/misc/get-ip-hash.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
@@ -19,9 +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 { isLimitInfo } from '@/server/api/SkRateLimiterService.js';
+import { sendRateLimitHeaders } from '@/misc/rate-limit-utils.js';
+import { LegacyRateLimit, 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';
@@ -51,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,
) {
@@ -67,21 +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 (isLimitInfo(info)) {
- // Number of seconds to wait before trying again. Left for backwards compatibility.
- reply.header('Retry-After', info.resetSec.toString());
- // Number of milliseconds to wait before trying again.
- reply.header('X-RateLimit-Reset', info.resetMs.toString());
- } else 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: ${JSON.stringify(info)}`);
- }
} else if (err.kind === 'client') {
reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="invalid_request", error_description="${err.message}"`);
statusCode = statusCode ?? 400;
@@ -347,40 +331,17 @@ export class ApiCallService implements OnApplicationShutdown {
if (factor > 0) {
// Rate limit
- const info = await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor)
- .then(info => {
- // We always want these headers, because clients need them for pacing.
- // Conditional check in case we somehow revert to the old limiter, which does not return info.
- if (info) {
- // Number of seconds until the limit has fully reset.
- reply.header('X-RateLimit-Clear', info.fullResetSec.toString());
- // Number of calls that can be made before being limited.
- reply.header('X-RateLimit-Remaining', info.remaining.toString());
+ const info = await this.rateLimiterService.limit(limit as LegacyRateLimit, limitActor, factor);
- // Only forward the info object if it's blocked, otherwise we'll reject *all* requests
- if (info.blocked) {
- return info;
- }
- }
+ sendRateLimitHeaders(reply, info);
- return undefined;
- })
- .catch(err => {
- // The old limiter throws info instead of returning it.
- if ('info' in err) {
- return err.info as LimiterInfo;
- } else {
- throw err;
- }
- });
-
- if (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,
- }, info);
+ });
}
}
}
diff --git a/packages/backend/src/server/api/RateLimiterService.ts b/packages/backend/src/server/api/RateLimiterService.ts
index 33db016a7c..6037f9bf92 100644
--- a/packages/backend/src/server/api/RateLimiterService.ts
+++ b/packages/backend/src/server/api/RateLimiterService.ts
@@ -14,6 +14,7 @@ import type { LimitInfo } from '@/server/api/SkRateLimiterService.js';
import { EnvService } from '@/core/EnvService.js';
import type { IEndpointMeta } from './endpoints.js';
+/** @deprecated Use SkRateLimiterService instead */
@Injectable()
export class RateLimiterService {
protected readonly logger: Logger;
diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts
index 64af7da7a6..1a4ce0a54c 100644
--- a/packages/backend/src/server/api/SigninApiService.ts
+++ b/packages/backend/src/server/api/SigninApiService.ts
@@ -21,12 +21,13 @@ import { IdService } from '@/core/IdService.js';
import { bindThis } from '@/decorators.js';
import { WebAuthnService } from '@/core/WebAuthnService.js';
import { UserAuthService } from '@/core/UserAuthService.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';
-import type { MiMeta } from '@/models/_.js';
@Injectable()
export class SigninApiService {
@@ -47,7 +48,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,
@@ -79,10 +80,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..ad08dad79c 100644
--- a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts
+++ b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts
@@ -21,10 +21,11 @@ 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 { SigninService } from './SigninService.js';
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
import type { FastifyReply, FastifyRequest } from 'fastify';
+import { sendRateLimitHeaders } from '@/misc/rate-limit-utils.js';
@Injectable()
export class SigninWithPasskeyApiService {
@@ -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
index 763de0029b..943348c41d 100644
--- a/packages/backend/src/server/api/SkRateLimiterService.ts
+++ b/packages/backend/src/server/api/SkRateLimiterService.ts
@@ -10,7 +10,7 @@ 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 { RateLimiterService } from './RateLimiterService.js';
+import type Logger from '@/logger.js';
/**
* Metadata about the current status of a rate limiter
@@ -51,18 +51,6 @@ export interface LimitInfo {
fullResetMs: number;
}
-export function isLimitInfo(info: unknown): info is LimitInfo {
- if (info == null) return false;
- if (typeof(info) !== 'object') return false;
- if (!('blocked' in info) || typeof(info.blocked) !== 'boolean') return false;
- if (!('remaining' in info) || typeof(info.remaining) !== 'number') return false;
- if (!('resetSec' in info) || typeof(info.resetSec) !== 'number') return false;
- if (!('resetMs' in info) || typeof(info.resetMs) !== 'number') return false;
- if (!('fullResetSec' in info) || typeof(info.fullResetSec) !== 'number') return false;
- if (!('fullResetMs' in info) || typeof(info.fullResetMs) !== 'number') return false;
- return true;
-}
-
/**
* Rate limit based on "leaky bucket" logic.
* The bucket count increases with each call, and decreases gradually at a given rate.
@@ -99,10 +87,10 @@ export interface RateLimit {
}
export type SupportedRateLimit = RateLimit | LegacyRateLimit;
-export type LegacyRateLimit = IEndpointMeta['limit'] & { key: NonNullable<string>, type: undefined | 'legacy' };
+export type LegacyRateLimit = IEndpointMeta['limit'] & { key: NonNullable<string>, type?: undefined };
export function isLegacyRateLimit(limit: SupportedRateLimit): limit is LegacyRateLimit {
- return limit.type === undefined || limit.type === 'legacy';
+ return limit.type === undefined;
}
export function hasMinLimit(limit: LegacyRateLimit): limit is LegacyRateLimit & { minInterval: number } {
@@ -110,13 +98,16 @@ export function hasMinLimit(limit: LegacyRateLimit): limit is LegacyRateLimit &
}
@Injectable()
-export class SkRateLimiterService extends RateLimiterService {
+export class SkRateLimiterService {
+ private readonly logger: Logger;
+ private readonly disabled: boolean;
+
constructor(
@Inject(TimeService)
private readonly timeService: TimeService,
@Inject(DI.redis)
- redisClient: Redis.Redis,
+ private readonly redisClient: Redis.Redis,
@Inject(LoggerService)
loggerService: LoggerService,
@@ -124,7 +115,8 @@ export class SkRateLimiterService extends RateLimiterService {
@Inject(EnvService)
envService: EnvService,
) {
- super(redisClient, loggerService, envService);
+ this.logger = loggerService.getLogger('limiter');
+ this.disabled = envService.env.NODE_ENV !== 'production';
}
public async limit(limit: SupportedRateLimit, actor: string, factor = 1): Promise<LimitInfo> {
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