, "reset_at": }. * Ao expirar a janela (now >= reset_at) o contador zera automaticamente. */ final class RateLimiter { /** * Diretório de armazenamento. Configurável por RATELIMIT_DIR; por padrão * usa o diretório temporário do sistema. */ private static function dir(): string { $dir = $_ENV['RATELIMIT_DIR'] ?? (sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'nettown_ratelimit'); if (!is_dir($dir) && !@mkdir($dir, 0700, true) && !is_dir($dir)) { // Sem diretório utilizável o limiter vira no-op silencioso (fail-open). Logger::warning('RateLimiter storage dir unavailable', ['dir' => $dir]); } return $dir; } private static function path(string $key): string { return self::dir() . DIRECTORY_SEPARATOR . hash('sha256', $key) . '.json'; } private static function read(string $key): array { $file = self::path($key); if (!is_file($file)) { return ['count' => 0, 'reset_at' => 0]; } $raw = @file_get_contents($file); $data = $raw !== false ? json_decode($raw, true) : null; if (!is_array($data) || !isset($data['count'], $data['reset_at'])) { return ['count' => 0, 'reset_at' => 0]; } return ['count' => (int) $data['count'], 'reset_at' => (int) $data['reset_at']]; } private static function write(string $key, array $data): void { $encoded = json_encode($data); if ($encoded === false) { return; } @file_put_contents(self::path($key), $encoded, LOCK_EX); } /** * Segundos restantes de bloqueio, ou 0 se ainda há tentativas disponíveis. */ public static function retryAfter(string $key, int $maxAttempts): int { $data = self::read($key); $now = time(); if ($now >= $data['reset_at']) { return 0; } if ($data['count'] < $maxAttempts) { return 0; } return $data['reset_at'] - $now; } /** * Registra uma tentativa dentro da janela. Inicia uma nova janela quando * a anterior expirou. */ public static function hit(string $key, int $windowSeconds): void { $data = self::read($key); $now = time(); if ($now >= $data['reset_at']) { $data = ['count' => 0, 'reset_at' => $now + $windowSeconds]; } $data['count']++; self::write($key, $data); } /** * Zera o contador (ex.: após autenticação bem-sucedida). */ public static function clear(string $key): void { $file = self::path($key); if (is_file($file)) { @unlink($file); } } }