124 lines
4.2 KiB
PHP
Executable File
124 lines
4.2 KiB
PHP
Executable File
<?php
|
|
|
|
class RateLimiter {
|
|
private $maxRequestsPerMinute = 60;
|
|
private $maxRequestsPerHour = 1000;
|
|
|
|
public function __construct($maxPerMinute = 60, $maxPerHour = 1000) {
|
|
$this->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;
|
|
}
|
|
}
|