Complete Phase 1: MVC, DB migrations, Auth, RBAC, Security, and Views
This commit is contained in:
83
app/Middleware/RateLimit.php
Normal file
83
app/Middleware/RateLimit.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Middleware;
|
||||
|
||||
use App\Core\Request;
|
||||
use App\Core\Response;
|
||||
use Predis\Client as RedisClient;
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
class RateLimit implements MiddlewareInterface
|
||||
{
|
||||
private ?RedisClient $redis = null;
|
||||
private int $limit = 100; // Allow 100 requests
|
||||
private int $window = 60; // Per 60 seconds
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$config = require __DIR__ . '/../../config/redis.php';
|
||||
if (!empty($config['host'])) {
|
||||
try {
|
||||
$this->redis = new RedisClient([
|
||||
'scheme' => 'tcp',
|
||||
'host' => $config['host'],
|
||||
'port' => $config['port'],
|
||||
'password' => $config['password'],
|
||||
'timeout' => 0.5, // 500ms connection timeout to fail fast
|
||||
]);
|
||||
$this->redis->connect();
|
||||
} catch (Throwable $e) {
|
||||
// Degrade gracefully if Redis server is down
|
||||
$this->redis = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle rate limiting logic.
|
||||
*/
|
||||
public function handle(Request $request, Response $response, callable $next): void
|
||||
{
|
||||
if ($this->redis === null) {
|
||||
// Redis unavailable, skip throttle check to avoid service outage
|
||||
$next();
|
||||
return;
|
||||
}
|
||||
|
||||
$ip = $request->getIp();
|
||||
$path = $request->getPath();
|
||||
$key = "rate_limit:" . md5($ip . ":" . $path);
|
||||
|
||||
try {
|
||||
$current = $this->redis->get($key);
|
||||
|
||||
if ($current !== null && (int)$current >= $this->limit) {
|
||||
$ttl = $this->redis->ttl($key);
|
||||
$response->header('Retry-After', (string)max(1, $ttl));
|
||||
throw new Exception("Too Many Requests. Rate limit exceeded.", 429);
|
||||
}
|
||||
|
||||
if ($current === null) {
|
||||
// First request in the time frame window
|
||||
$this->redis->setex($key, $this->window, 1);
|
||||
$current = 0;
|
||||
} else {
|
||||
$this->redis->incr($key);
|
||||
}
|
||||
|
||||
// Set rate limit headers
|
||||
$remaining = $this->limit - ((int)$current + 1);
|
||||
$response->header('X-RateLimit-Limit', (string)$this->limit);
|
||||
$response->header('X-RateLimit-Remaining', (string)max(0, $remaining));
|
||||
|
||||
} catch (Throwable $e) {
|
||||
if ($e->getCode() === 429) {
|
||||
throw $e;
|
||||
}
|
||||
// Logging or catching connection dropping mid-request
|
||||
}
|
||||
|
||||
$next();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user