Compare commits

...

20 Commits

Author SHA1 Message Date
anatawa12 b161b9c99a
Merge bb921297c7 into 794cb9ffe2 2024-11-08 05:36:18 +09:00
4ster1sk 794cb9ffe2
fix(backend): followedMessageではなくdescriptionになっていたのを修正 (#14908) 2024-11-07 17:16:51 +09:00
syuilo 0b976064ca
Update CHANGELOG.md 2024-11-07 15:10:38 +09:00
4ster1sk bca690f256
fix(backend): フォロワーへのメッセージの絵文字をemojisに含めるように (#14904) 2024-11-07 15:10:10 +09:00
Sayamame-beans bb921297c7
Merge branch 'develop' into reimplement-rate-limit 2024-07-17 19:44:52 +09:00
syuilo 0265d913e5
Merge branch 'develop' into reimplement-rate-limit 2024-06-22 19:42:11 +09:00
Kisaragi 31daf10f87
fix lint error 2024-06-22 14:32:39 +09:00
anatawa12 d8941558c3
fix: レートリミットのfactorが二回適用されて二乗の効果がある問題を修正 2024-06-22 13:40:23 +09:00
anatawa12 e77bcf5ff7
Merge branch 'develop' into reimplement-rate-limit 2024-06-22 13:39:48 +09:00
anatawa12 f9dd3aca78
refactor: return rate limit instead of throwing an object 2024-06-22 13:34:52 +09:00
anatawa12 fce656b5e9
Merge branch 'develop' into reimplement-rate-limit 2024-06-15 11:08:06 +09:00
anatawa12 0ef7f14fa9
refactor: remove unnecessary max/min function 2024-06-14 17:49:52 +09:00
anatawa12 4af9c90d12
refactor: remove unnecessary return in min/max function 2024-06-14 17:49:52 +09:00
anatawa12 3eb1875804
refactor: check if there is rate limit inside min/max function 2024-06-14 17:49:52 +09:00
anatawa12 f72c768407
refactor: do not check long term limit inside min 2024-06-14 17:49:52 +09:00
anatawa12 b3561ce425
refactor: reimplement limit with async 2024-06-14 17:49:52 +09:00
anatawa12 1d6d90d62d
refactor: reimplement max/min with async 2024-06-14 17:49:52 +09:00
anatawa12 d8844aed6f
refactor: wrap ratelimiter with promise 2024-06-14 17:49:52 +09:00
anatawa12 28c8d318e1
fix: long term / short term limitがないときでもそれぞれ用のnew Limiterとlimiter.getが呼ばれる問題 2024-06-14 17:49:51 +09:00
anatawa12 03c2919b69
fix rate limit check never ends 2024-06-14 15:36:58 +09:00
5 changed files with 72 additions and 81 deletions

View File

@ -29,6 +29,7 @@
- Enhance: 起動前の疎通チェックで、DBとメイン以外のRedisの疎通確認も行うように - Enhance: 起動前の疎通チェックで、DBとメイン以外のRedisの疎通確認も行うように
(Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/588) (Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/588)
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/715) (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/715)
- fix(backend): フォロワーへのメッセージの絵文字をemojisに含めるように
- Fix: Nested proxy requestsを検出した際にブロックするように - Fix: Nested proxy requestsを検出した際にブロックするように
[ghsa-gq5q-c77c-v236](https://github.com/misskey-dev/misskey/security/advisories/ghsa-gq5q-c77c-v236) [ghsa-gq5q-c77c-v236](https://github.com/misskey-dev/misskey/security/advisories/ghsa-gq5q-c77c-v236)
- Fix: 招待コードの発行可能な残り数算出に使用すべきロールポリシーの値が違う問題を修正 - Fix: 招待コードの発行可能な残り数算出に使用すべきロールポリシーの値が違う問題を修正
@ -276,6 +277,7 @@
- Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036) - Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036)
- Fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059) - Fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059)
- Fix: FTT有効時、タイムライン用エンドポイントで`sinceId`にキャッシュ内最古のものより古いものを指定した場合に正しく結果が返ってこない問題を修正 - Fix: FTT有効時、タイムライン用エンドポイントで`sinceId`にキャッシュ内最古のものより古いものを指定した場合に正しく結果が返ってこない問題を修正
- Fix: レートリミットのfactorが二回適用されて二乗の効果がある問題を修正 (#13997)
- Fix: 自分以外のクリップ内のノート個数が見えることがあるのを修正 - Fix: 自分以外のクリップ内のノート個数が見えることがあるのを修正
- Fix: 空文字列のリアクションはフォールバックされるように - Fix: 空文字列のリアクションはフォールバックされるように
- Fix: リノートにリアクションできないように - Fix: リノートにリアクションできないように

View File

@ -326,19 +326,15 @@ export class ApiCallService implements OnApplicationShutdown {
if (factor > 0) { if (factor > 0) {
// Rate limit // Rate limit
await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor).catch(err => { const rateLimit = await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor);
if ('info' in err) { if (rateLimit != null) {
// errはLimiter.LimiterInfoであることが期待される throw new ApiError({
throw new ApiError({ message: 'Rate limit exceeded. Please try again later.',
message: 'Rate limit exceeded. Please try again later.', code: 'RATE_LIMIT_EXCEEDED',
code: 'RATE_LIMIT_EXCEEDED', id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', httpStatusCode: 429,
httpStatusCode: 429, }, rateLimit.info);
}, err.info); }
} else {
throw new TypeError('information must be a rate-limiter information.');
}
});
} }
} }

View File

@ -12,6 +12,14 @@ import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { IEndpointMeta } from './endpoints.js'; import type { IEndpointMeta } from './endpoints.js';
type RateLimitInfo = {
code: 'BRIEF_REQUEST_INTERVAL',
info: Limiter.LimiterInfo,
} | {
code: 'RATE_LIMIT_EXCEEDED',
info: Limiter.LimiterInfo,
}
@Injectable() @Injectable()
export class RateLimiterService { export class RateLimiterService {
private logger: Logger; private logger: Logger;
@ -31,77 +39,57 @@ export class RateLimiterService {
} }
@bindThis @bindThis
public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string, factor = 1) { private checkLimiter(options: Limiter.LimiterOption): Promise<Limiter.LimiterInfo> {
{ return new Promise<Limiter.LimiterInfo>((resolve, reject) => {
if (this.disabled) { new Limiter(options).get((err, info) => {
return Promise.resolve(); if (err) {
} return reject(err);
}
resolve(info);
});
});
}
// Short-term limit @bindThis
const min = new Promise<void>((ok, reject) => { public async limit(limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string, factor = 1): Promise<RateLimitInfo | null> {
const minIntervalLimiter = new Limiter({ if (this.disabled) {
id: `${actor}:${limitation.key}:min`, return null;
duration: limitation.minInterval! * factor, }
max: 1,
db: this.redisClient,
});
minIntervalLimiter.get((err, info) => { // Short-term limit
if (err) { if (limitation.minInterval != null) {
return reject({ code: 'ERR', info }); const info = await this.checkLimiter({
} id: `${actor}:${limitation.key}:min`,
duration: limitation.minInterval * factor,
this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`); max: 1,
db: this.redisClient,
if (info.remaining === 0) {
return reject({ code: 'BRIEF_REQUEST_INTERVAL', info });
} else {
if (hasLongTermLimit) {
return max.then(ok, reject);
} else {
return ok();
}
}
});
}); });
// Long term limit this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`);
const max = new Promise<void>((ok, reject) => {
const limiter = new Limiter({
id: `${actor}:${limitation.key}`,
duration: limitation.duration! * factor,
max: limitation.max! / factor,
db: this.redisClient,
});
limiter.get((err, info) => { if (info.remaining === 0) {
if (err) { // eslint-disable-next-line no-throw-literal
return reject({ code: 'ERR', info }); return { code: 'BRIEF_REQUEST_INTERVAL', info };
}
this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`);
if (info.remaining === 0) {
return reject({ code: 'RATE_LIMIT_EXCEEDED', info });
} else {
return ok();
}
});
});
const hasShortTermLimit = typeof limitation.minInterval === 'number';
const hasLongTermLimit =
typeof limitation.duration === 'number' &&
typeof limitation.max === 'number';
if (hasShortTermLimit) {
return min;
} else if (hasLongTermLimit) {
return max;
} else {
return Promise.resolve();
} }
} }
// Long term limit
if (limitation.duration != null && limitation.max != null) {
const info = await this.checkLimiter({
id: `${actor}:${limitation.key}`,
duration: limitation.duration,
max: limitation.max / factor,
db: this.redisClient,
});
this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`);
if (info.remaining === 0) {
// eslint-disable-next-line no-throw-literal
return { code: 'RATE_LIMIT_EXCEEDED', info };
}
}
return null;
} }
} }

View File

@ -89,10 +89,9 @@ export class SigninApiService {
return { error }; return { error };
} }
try {
// not more than 1 attempt per second and not more than 10 attempts per hour // 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)); const rateLimit = await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip));
} catch (err) { if (rateLimit != null) {
reply.code(429); reply.code(429);
return { return {
error: { error: {

View File

@ -465,6 +465,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const newName = updates.name === undefined ? user.name : updates.name; const newName = updates.name === undefined ? user.name : updates.name;
const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description; const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description;
const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields; const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields;
const newFollowedMessage = profileUpdates.followedMessage === undefined ? profile.followedMessage : profileUpdates.followedMessage;
if (newName != null) { if (newName != null) {
let hasProhibitedWords = false; let hasProhibitedWords = false;
@ -494,6 +495,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
]); ]);
} }
if (newFollowedMessage != null) {
const tokens = mfm.parse(newFollowedMessage);
emojis = emojis.concat(extractCustomEmojisFromMfm(tokens));
}
updates.emojis = emojis; updates.emojis = emojis;
updates.tags = tags; updates.tags = tags;