// Лимит запросов с 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(); 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 { const realIp = realIpHeader?.trim(); if (socketAddress && trustedProxies.has(socketAddress) && realIp) return realIp; return socketAddress ?? 'unknown'; }