throttle middleware
This commit is contained in:
@@ -11,6 +11,7 @@ use Din9xtrCloud\Controllers\StorageController;
|
|||||||
use Din9xtrCloud\Controllers\StorageTusController;
|
use Din9xtrCloud\Controllers\StorageTusController;
|
||||||
use Din9xtrCloud\Middlewares\AuthMiddleware;
|
use Din9xtrCloud\Middlewares\AuthMiddleware;
|
||||||
use Din9xtrCloud\Middlewares\CsrfMiddleware;
|
use Din9xtrCloud\Middlewares\CsrfMiddleware;
|
||||||
|
use Din9xtrCloud\Middlewares\ThrottleMiddleware;
|
||||||
use Din9xtrCloud\Router;
|
use Din9xtrCloud\Router;
|
||||||
use Din9xtrCloud\Storage\Drivers\LocalStorageDriver;
|
use Din9xtrCloud\Storage\Drivers\LocalStorageDriver;
|
||||||
use Din9xtrCloud\Storage\Drivers\StorageDriverInterface;
|
use Din9xtrCloud\Storage\Drivers\StorageDriverInterface;
|
||||||
@@ -119,7 +120,7 @@ $routes = static function (RouteCollector $r): void {
|
|||||||
|
|
||||||
// route,middlewares
|
// route,middlewares
|
||||||
$router = new Router($routes, $container);
|
$router = new Router($routes, $container);
|
||||||
//$router->middlewareFor('/', AuthMiddleware::class);
|
$router->middlewareFor('/login', ThrottleMiddleware::class);
|
||||||
//$router->middlewareFor('/login', AuthMiddleware::class);
|
//$router->middlewareFor('/login', AuthMiddleware::class);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -48,8 +48,6 @@ final readonly class DashboardController
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->logger->info('Dashboard loaded successfully');
|
|
||||||
|
|
||||||
} catch (Throwable $exception) {
|
} catch (Throwable $exception) {
|
||||||
$this->logger->error($exception->getMessage(), ['exception' => $exception]);
|
$this->logger->error($exception->getMessage(), ['exception' => $exception]);
|
||||||
return View::display(new DashboardViewModel(
|
return View::display(new DashboardViewModel(
|
||||||
|
|||||||
@@ -3,63 +3,124 @@
|
|||||||
namespace Din9xtrCloud\Middlewares;
|
namespace Din9xtrCloud\Middlewares;
|
||||||
|
|
||||||
use Nyholm\Psr7\Response;
|
use Nyholm\Psr7\Response;
|
||||||
|
use PDO;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Psr\Http\Server\MiddlewareInterface;
|
use Psr\Http\Server\MiddlewareInterface;
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
use PDO;
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
final class ThrottleMiddleware implements MiddlewareInterface
|
final readonly class ThrottleMiddleware implements MiddlewareInterface
|
||||||
{
|
{
|
||||||
private int $maxAttempts = 5;
|
private const int MAX_ATTEMPTS = 10;
|
||||||
private int $lockTime = 300; // seconds
|
private const int LOCK_TIME = 300;
|
||||||
|
|
||||||
public function __construct(private readonly PDO $db)
|
public function __construct(
|
||||||
|
private PDO $db,
|
||||||
|
private LoggerInterface $logger,
|
||||||
|
)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
public function process(
|
||||||
|
ServerRequestInterface $request,
|
||||||
|
RequestHandlerInterface $handler
|
||||||
|
): ResponseInterface
|
||||||
{
|
{
|
||||||
$ip = getClientIp();
|
$ip = getClientIp();
|
||||||
|
|
||||||
$stmt = $this->db->prepare("SELECT * FROM login_throttle WHERE ip = :ip ORDER BY id DESC LIMIT 1");
|
|
||||||
$stmt->execute(['ip' => $ip]);
|
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
$now = time();
|
$now = time();
|
||||||
$blockedUntil = null;
|
|
||||||
|
|
||||||
if ($row) {
|
$row = $this->getLastAttempt($ip);
|
||||||
if ($row['attempts'] >= $this->maxAttempts && ($now - $row['last_attempt']) < $this->lockTime) {
|
|
||||||
$blockedUntil = $row['last_attempt'] + $this->lockTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($blockedUntil && $now < $blockedUntil) {
|
if ($row !== null) {
|
||||||
return new Response(429, [], 'Too Many Requests');
|
$attempts = (int)$row['attempts'];
|
||||||
}
|
$lastAttempt = (int)$row['last_attempt'];
|
||||||
|
|
||||||
$response = $handler->handle($request);
|
if ($now - $lastAttempt > self::LOCK_TIME) {
|
||||||
|
$this->clearAttempts($ip);
|
||||||
|
$this->logger->info('Throttle window expired, reset attempts', [
|
||||||
|
'ip' => $ip,
|
||||||
|
]);
|
||||||
|
|
||||||
if ($request->getUri()->getPath() === '/login') {
|
$row = null;
|
||||||
$attempts = ($row['attempts'] ?? 0);
|
} elseif ($attempts >= self::MAX_ATTEMPTS) {
|
||||||
if ($response->getStatusCode() === 302) {
|
$retryAfter = ($lastAttempt + self::LOCK_TIME) - $now;
|
||||||
$this->db->prepare("
|
|
||||||
INSERT INTO login_throttle (ip, attempts, last_attempt)
|
$this->logger->warning('Login throttled', [
|
||||||
VALUES (:ip, 0, :last_attempt)
|
|
||||||
")->execute(['ip' => $ip, 'last_attempt' => $now]);
|
|
||||||
} else {
|
|
||||||
$attempts++;
|
|
||||||
$this->db->prepare("
|
|
||||||
INSERT INTO login_throttle (ip, attempts, last_attempt)
|
|
||||||
VALUES (:ip, :attempts, :last_attempt)
|
|
||||||
")->execute([
|
|
||||||
'ip' => $ip,
|
'ip' => $ip,
|
||||||
'attempts' => $attempts,
|
'attempts' => $attempts,
|
||||||
'last_attempt' => $now
|
'retry_after' => $retryAfter,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
429,
|
||||||
|
['Retry-After' => (string)max(1, $retryAfter)],
|
||||||
|
'Too Many Requests'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $response;
|
|
||||||
|
$this->registerAttempt($ip, $now, $row !== null);
|
||||||
|
|
||||||
|
return $handler->handle($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getLastAttempt(string $ip): ?array
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare(
|
||||||
|
"SELECT id, attempts, last_attempt
|
||||||
|
FROM login_throttle
|
||||||
|
WHERE ip = :ip
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT 1"
|
||||||
|
);
|
||||||
|
|
||||||
|
$stmt->execute(['ip' => $ip]);
|
||||||
|
|
||||||
|
return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function registerAttempt(string $ip, int $now, bool $exists): void
|
||||||
|
{
|
||||||
|
if ($exists) {
|
||||||
|
$stmt = $this->db->prepare(
|
||||||
|
"UPDATE login_throttle
|
||||||
|
SET attempts = attempts + 1,
|
||||||
|
last_attempt = :time
|
||||||
|
WHERE id = (
|
||||||
|
SELECT id FROM login_throttle
|
||||||
|
WHERE ip = :ip
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT 1
|
||||||
|
)"
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->logger->info('Throttle attempt incremented', [
|
||||||
|
'ip' => $ip,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$stmt = $this->db->prepare(
|
||||||
|
"INSERT INTO login_throttle (ip, attempts, last_attempt)
|
||||||
|
VALUES (:ip, 1, :time)"
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->logger->info('Throttle first attempt registered', [
|
||||||
|
'ip' => $ip,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
'ip' => $ip,
|
||||||
|
'time' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function clearAttempts(string $ip): void
|
||||||
|
{
|
||||||
|
$stmt = $this->db->prepare(
|
||||||
|
"DELETE FROM login_throttle WHERE ip = :ip"
|
||||||
|
);
|
||||||
|
|
||||||
|
$stmt->execute(['ip' => $ip]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user