feat(api): rate-limit, honeypot, коды ошибок
rateLimit.ts: 5 запросов / 10 минут на IP в памяти, фиксированное окно, чистка устаревших записей по ходу запросов, 429 с Retry-After. IP из X-Real-IP только от loopback-сокета (плюс TRUSTED_PROXIES для шлюза docker), иначе адрес сокета; в базу IP не пишем. Honeypot → 201 без записи и уведомления. isPaused() → 503 на время восстановления базы. Тесты на каждый код ответа из таблицы плана.
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
// Лимит запросов с IP в памяти процесса. Фиксированное окно: проще скользящего,
|
||||
// а для защиты формы от перебора разницы нет. После рестарта счётчики обнуляются — это нормально.
|
||||
|
||||
export type RateDecision = { ok: true } | { ok: false; retryAfterSec: number };
|
||||
|
||||
export interface RateLimiter {
|
||||
hit(key: string, now: Date): RateDecision;
|
||||
}
|
||||
|
||||
export function createRateLimiter({ limit = 5, windowMs = 10 * 60 * 1000 } = {}) {
|
||||
const buckets = new Map<string, { start: number; count: number }>();
|
||||
let lastSweep = 0;
|
||||
|
||||
return {
|
||||
hit(key: string, now: Date): RateDecision {
|
||||
const t = now.getTime();
|
||||
// Чистим не таймером, а по ходу запросов: без трафика нечему и расти.
|
||||
if (t - lastSweep >= windowMs) {
|
||||
for (const [k, b] of buckets) if (t - b.start >= windowMs) buckets.delete(k);
|
||||
lastSweep = t;
|
||||
}
|
||||
|
||||
const bucket = buckets.get(key);
|
||||
if (!bucket || t - bucket.start >= windowMs) {
|
||||
buckets.set(key, { start: t, count: 1 });
|
||||
return { ok: true };
|
||||
}
|
||||
if (bucket.count < limit) {
|
||||
bucket.count++;
|
||||
return { ok: true };
|
||||
}
|
||||
return { ok: false, retryAfterSec: Math.max(1, Math.ceil((bucket.start + windowMs - t) / 1000)) };
|
||||
},
|
||||
size: () => buckets.size,
|
||||
};
|
||||
}
|
||||
|
||||
export const LOOPBACK = ['127.0.0.1', '::1', '::ffff:127.0.0.1'] as const;
|
||||
|
||||
/**
|
||||
* IP посетителя. X-Real-IP ставит nginx на той же машине; верим заголовку только от доверенного
|
||||
* адреса (loopback и явно перечисленные), иначе любой подставит чужой адрес и обойдёт лимит.
|
||||
*/
|
||||
export function clientIp(
|
||||
socketAddress: string | undefined,
|
||||
realIpHeader: string | undefined,
|
||||
trustedProxies: ReadonlySet<string>,
|
||||
): string {
|
||||
const realIp = realIpHeader?.trim();
|
||||
if (socketAddress && trustedProxies.has(socketAddress) && realIp) return realIp;
|
||||
return socketAddress ?? 'unknown';
|
||||
}
|
||||
Reference in New Issue
Block a user