maxRequestsPerMinute = $maxPerMinute; $this->maxRequestsPerHour = $maxPerHour; } public function checkRateLimit($identifier) { $now = time(); $oneMinuteAgo = $now - 60; $oneHourAgo = $now - 3600; $minuteKey = "rate_limit_minute:$identifier"; $hourKey = "rate_limit_hour:$identifier"; // Obtener datos actuales $minuteData = $this->getRateData($minuteKey); $hourData = $this->getRateData($hourKey); // Limpiar peticiones viejas $minuteRequests = array_filter($minuteData, function($timestamp) use ($oneMinuteAgo) { return $timestamp >= $oneMinuteAgo; }); $hourRequests = array_filter($hourData, function($timestamp) use ($oneHourAgo) { return $timestamp >= $oneHourAgo; }); // Calcular peticiones restantes $remainingMinute = max(0, $this->maxRequestsPerMinute - count($minuteRequests)); $remainingHour = max(0, $this->maxRequestsPerHour - count($hourRequests)); // Si excede límite por minuto if (count($minuteRequests) >= $this->maxRequestsPerMinute) { $retryAfter = min($minuteRequests) + 60; $this->setRateLimitHeaders($remainingMinute, $this->maxRequestsPerMinute, $retryAfter - $now); throw new RateLimitException('Demasiadas peticiones. Máximo ' . $this->maxRequestsPerMinute . ' por minuto.', $retryAfter - $now); } // Si excede límite por hora if (count($hourRequests) >= $this->maxRequestsPerHour) { $retryAfter = min($hourRequests) + 3600; $this->setRateLimitHeaders($remainingHour, $this->maxRequestsPerHour, $retryAfter - $now); throw new RateLimitException('Demasiadas peticiones. Máximo ' . $this->maxRequestsPerHour . ' por hora.', $retryAfter - $now); } // Agregar petición actual $minuteRequests[] = $now; $hourRequests[] = $now; // Guardar datos actualizados $this->setRateData($minuteKey, $minuteRequests, 120); $this->setRateData($hourKey, $hourRequests, 3600); // Establecer headers de rate limit $this->setRateLimitHeaders($remainingMinute, $this->maxRequestsPerMinute, null); return true; } private function getRateData($key) { // Usar sesión para rate limiting if (session_status() === PHP_SESSION_ACTIVE && isset($_SESSION[$key])) { return $_SESSION[$key]; } return []; } private function setRateData($key, $data, $ttl = null) { // Usar sesión para rate limiting if (session_status() === PHP_SESSION_ACTIVE) { $_SESSION[$key] = $data; } } private function setRateLimitHeaders($remaining, $limit, $retryAfter = null) { header("X-RateLimit-Limit: $limit"); header("X-RateLimit-Remaining: $remaining"); if ($retryAfter !== null) { header("X-RateLimit-Reset: $retryAfter"); header("Retry-After: $retryAfter"); } } public function getIdentifierFromRequest() { // Usar IP como identificador por defecto $ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['HTTP_CLIENT_IP'] ?? $_SERVER['REMOTE_ADDR'] ?? 'unknown'; // Si hay token JWT, usar user_id + IP para limitar por usuario $authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? ''; if (preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) { require_once __DIR__ . '/JWT.php'; $token = $matches[1]; $decoded = JWT::decode($token); if ($decoded && isset($decoded['user_id'])) { return 'user_' . $decoded['user_id']; } } return 'ip_' . $ip; } } class RateLimitException extends Exception { private $retryAfter; public function __construct($message, $retryAfter) { parent::__construct($message); $this->retryAfter = $retryAfter; } public function getRetryAfter() { return $this->retryAfter; } }