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'; } }