71 lines
2.4 KiB
PHP
71 lines
2.4 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Middleware;
|
|
|
|
use Closure;
|
|
use cPad\Plugins\Forum\Services\Security\BotProtectionService;
|
|
use Illuminate\Http\Exceptions\ThrottleRequestsException;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Routing\Middleware\ThrottleRequests;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
class ForumRateLimitMiddleware
|
|
{
|
|
public function __construct(
|
|
private readonly ThrottleRequests $throttleRequests,
|
|
private readonly BotProtectionService $botProtectionService,
|
|
) {
|
|
}
|
|
|
|
public function handle(Request $request, Closure $next): Response
|
|
{
|
|
$routeName = (string) optional($request->route())->getName();
|
|
$limiterName = match ($routeName) {
|
|
'forum.topic.store' => 'forum-thread-create',
|
|
default => 'forum-post-write',
|
|
};
|
|
|
|
try {
|
|
return $this->throttleRequests->handle($request, $next, $limiterName);
|
|
} catch (ThrottleRequestsException $exception) {
|
|
$maxAttempts = (int) ($exception->getHeaders()['X-RateLimit-Limit'] ?? 0);
|
|
|
|
$this->botProtectionService->recordRateLimitViolation(
|
|
$request,
|
|
$this->resolveActionName($routeName),
|
|
[
|
|
'limiter' => $limiterName,
|
|
'bucket' => $this->resolveBucket($limiterName, $maxAttempts),
|
|
'max_attempts' => $maxAttempts,
|
|
'retry_after' => (int) ($exception->getHeaders()['Retry-After'] ?? 0),
|
|
'reason' => sprintf('Forum write rate limit exceeded on %s.', $routeName !== '' ? $routeName : 'unknown route'),
|
|
],
|
|
);
|
|
|
|
throw $exception;
|
|
}
|
|
}
|
|
|
|
private function resolveActionName(string $routeName): string
|
|
{
|
|
return match ($routeName) {
|
|
'forum.topic.store' => 'forum_topic_create',
|
|
'forum.post.update' => 'forum_post_update',
|
|
default => 'forum_reply_create',
|
|
};
|
|
}
|
|
|
|
private function resolveBucket(string $limiterName, int $maxAttempts): string
|
|
{
|
|
return $maxAttempts <= $this->minuteLimitThreshold($limiterName) ? 'minute' : 'hour';
|
|
}
|
|
|
|
private function minuteLimitThreshold(string $limiterName): int
|
|
{
|
|
return match ($limiterName) {
|
|
'forum-thread-create', 'forum-post-write' => 3,
|
|
default => PHP_INT_MAX,
|
|
};
|
|
}
|
|
}
|