Files
saqel/backend/app/Middlewares/RateLimitMiddleware.php
T

85 lines
2.7 KiB
PHP

<?php
namespace App\Middlewares;
use App\Core\Request;
use App\Core\Response;
use App\Core\RedisClient;
/**
* Rate Limit Middleware (Redis Powered)
* Limits the number of requests per IP address using Redis atomic counters.
* Protects sensitive endpoints (login, register, otp) from Brute Force attacks.
*/
class RateLimitMiddleware
{
private int $maxAttempts;
private int $decaySeconds;
public function __construct(int $maxAttempts = 5, int $decaySeconds = 60)
{
$this->maxAttempts = $maxAttempts;
$this->decaySeconds = $decaySeconds;
}
public function handle(Request $request, Response $response): void
{
$ip = $this->getClientIp();
$key = 'rate_limit:' . md5($ip . '_' . $request->getPath());
try {
$redis = RedisClient::getInstance();
$current = $redis->get($key);
if ($current !== false && (int)$current >= $this->maxAttempts) {
$retryAfter = $redis->ttl($key);
$retryAfter = $retryAfter > 0 ? $retryAfter : $this->decaySeconds;
$response->setHeader('Retry-After', (string)$retryAfter);
$response->json([
'error' => 'Too Many Requests',
'message' => "لقد تجاوزت الحد الأقصى للمحاولات ({$this->maxAttempts}). يرجى المحاولة بعد {$retryAfter} ثانية."
], 429);
exit; // End request
}
// Increment atomically
$count = $redis->incr($key);
if ($count === 1) {
// First request, set expiration
$redis->expire($key, $this->decaySeconds);
}
} catch (\Exception $e) {
// If Redis fails, log it but don't block the request completely,
// or we could choose to block it. We'll let it pass to avoid downtime.
error_log("RateLimit Redis Error: " . $e->getMessage());
}
}
/**
* Get real client IP, accounting for proxies
*/
private function getClientIp(): string
{
$headers = [
'HTTP_CF_CONNECTING_IP', // Cloudflare
'HTTP_X_FORWARDED_FOR',
'HTTP_X_REAL_IP',
'REMOTE_ADDR'
];
foreach ($headers as $header) {
if (!empty($_SERVER[$header])) {
// X-Forwarded-For can be a comma-separated list; take first
$ip = trim(explode(',', $_SERVER[$header])[0]);
if (filter_var($ip, FILTER_VALIDATE_IP)) {
return $ip;
}
}
}
return '0.0.0.0';
}
}