Charon
A simple yet powerful PSR-15 compliant rate limiting middleware for PHP applications. Charon provides an effective way to protect your applications from abuse through configurable request throttling.
Features
-
? PSR-15 Middleware compliant
-
? PSR-16 Simple Cache support for storage
-
? Optional PSR-3 Logger integration
-
? Efficient rate limiting using sliding window
-
? IP and User-Agent based throttling
-
? Configurable rate limits and time windows
-
? Standard rate limit headers (X-RateLimit-*)
-
? Automatic blacklisting for repeat offenders
Installation
You can install the package via composer:
composer require cmatosbc/charon
Usage
Basic Usage
use Charon\ThrottleMiddleware;
// Create the middleware with basic configuration
$middleware = new ThrottleMiddleware(
limit: 100, // Maximum requests allowed
windowPeriod: 3600, // Time window in seconds (1 hour)
cache: $cacheImpl // PSR-16 cache implementation
);
// Add it to your middleware stack
$app->add($middleware);
With Logging
use Charon\ThrottleMiddleware;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
// Create a PSR-3 logger
$logger = new Logger('rate-limits');
$logger->pushHandler(new StreamHandler('path/to/rate-limit.log', Logger::WARNING));
// Create middleware with logging
$middleware = new ThrottleMiddleware(
limit: 100,
windowPeriod: 3600,
cache: $cacheImpl,
logger: $logger, // PSR-3 logger
logAllRequests: false // Set to true to log all requests
);
With Automatic Blacklisting
use Charon\ThrottleMiddleware;
$middleware = new ThrottleMiddleware(
limit: 100,
windowPeriod: 3600,
cache: $cacheImpl,
logger: $logger
);
// Blacklist clients after 5 rate limit violations
$middleware->maybeBlacklist(5);
When blacklisting is enabled:
- Clients exceeding rate limits multiple times will be tracked
- After reaching the specified number of violations, the client will be blacklisted
- Blacklisted clients receive a 403 Forbidden response
- Violations are tracked across multiple time windows
- Blacklist status is stored in cache with client signature
Framework Integration Examples
Slim 4
use Slim\Factory\AppFactory;
use Charon\ThrottleMiddleware;
$app = AppFactory::create();
// Add the middleware with blacklisting
$app->add((new ThrottleMiddleware(
limit: 100,
windowPeriod: 3600,
cache: $cache
))->maybeBlacklist(5));
Laravel
use Charon\ThrottleMiddleware;
// In a service provider
public function boot()
{
$this->app->middleware([
(new ThrottleMiddleware(
limit: 100,
windowPeriod: 3600,
cache: app()->make('cache.store')
))->maybeBlacklist(5)
]);
}
Symfony
use Charon\ThrottleMiddleware;
use Symfony\Component\Cache\Adapter\RedisAdapter;
use Symfony\Component\Cache\Psr16Cache;
// In services.yaml
services:
Charon\ThrottleMiddleware:
arguments:
$limit: 100
$windowPeriod: 3600
$cache: '@cache.app'
calls:
- maybeBlacklist: [5]
tags:
- { name: 'kernel.event_listener', event: 'kernel.request', priority: 300 }
// Or in a Controller/EventSubscriber
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class RateLimitSubscriber implements EventSubscriberInterface
{
private ThrottleMiddleware $throttle;
public function __construct(
private Psr16Cache $cache
) {
$this->throttle = (new ThrottleMiddleware(
limit: 100,
windowPeriod: 3600,
cache: $this->cache
))->maybeBlacklist(5);
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => ['onKernelRequest', 300]
];
}
public function onKernelRequest(RequestEvent $event): void
{
if (!$event->isMainRequest()) {
return;
}
$request = $event->getRequest();
$handler = new class implements RequestHandlerInterface {
public function handle(ServerRequestInterface $request): ResponseInterface
{
return new Response();
}
};
$response = $this->throttle->process($request, $handler);
if ($response->getStatusCode() !== 200) {
$event->setResponse($response);
}
}
}
WordPress REST API
use Charon\ThrottleMiddleware;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Psr16Cache;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7\ServerRequest;
// In your plugin file or functions.php
add_action('rest_api_init', function () {
$cache = new Psr16Cache(new FilesystemAdapter());
$throttle = (new ThrottleMiddleware(
limit: 100,
windowPeriod: 3600,
cache: $cache
))->maybeBlacklist(5);
// Apply to all REST API endpoints
add_filter('rest_pre_dispatch', function ($result, $server, $request) use ($throttle) {
if (null !== $result) {
return $result;
}
// Convert WordPress request to PSR-7
$psr17Factory = new Psr17Factory();
$psrRequest = new ServerRequest(
$request->get_method(),
$request->get_route(),
getallheaders(),
null,
'1.1',
array_merge($_SERVER, ['REMOTE_ADDR' => $_SERVER['REMOTE_ADDR']])
);
// Handle rate limiting
$handler = new class implements RequestHandlerInterface {
public function handle(ServerRequestInterface $request): ResponseInterface
{
return (new Psr17Factory())->createResponse(200);
}
};
$response = $throttle->process($psrRequest, $handler);
// Check if request should be blocked
if ($response->getStatusCode() !== 200) {
return new WP_Error(
'rest_throttled',
$response->getReasonPhrase(),
['status' => $response->getStatusCode()]
);
}
// Add rate limit headers to WordPress response
add_filter('rest_post_dispatch', function ($response) use ($throttle) {
if ($response instanceof WP_REST_Response) {
foreach ($response->get_headers() as $key => $value) {
if (strpos($key, 'X-RateLimit') === 0) {
$response->header($key, $value);
}
}
}
return $response;
});
return $result;
}, 10, 3);
});
Response Headers
The middleware adds standard rate limit headers to responses:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 99
X-RateLimit-Reset: 1635789600
When the rate limit is exceeded, a 429 (Too Many Requests) response is returned with:
Status: 429 Too Many Requests
Retry-After: 3600
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1635789600
When a blacklisted client attempts to access the resource:
Status: 403 Forbidden
Content-Type: application/json
{
"error": "Access denied due to repeated rate limit violations"
}
Logging
When logging is enabled, the middleware logs the following information:
Rate Limit Exceeded (Warning Level)
{
"message": "Rate limit exceeded",
"context": {
"client": {
"ip": "192.168.1.1",
"user_agent": "Mozilla/5.0...",
"method": "GET",
"path": "/api/resource"
},
"requests": 101,
"limit": 100,
"reset_time": 1635789600
}
}
Client Blacklisted (Alert Level)
{
"message": "Client blacklisted due to recurring rate limit violations",
"context": {
"client": {
"ip": "192.168.1.1",
"user_agent": "Mozilla/5.0...",
"method": "GET",
"path": "/api/resource"
},
"violations": 5,
"threshold": 5
}
}
Request Processed (Info Level, when logAllRequests is true)
{
"message": "Request processed",
"context": {
"client": {
"ip": "192.168.1.1",
"user_agent": "Mozilla/5.0...",
"method": "GET",
"path": "/api/resource"
},
"requests": 50,
"limit": 100,
"remaining": 50
}
}
Use Cases
-
API Rate Limiting: Protect your API from abuse by limiting requests per client
-
Login Throttling: Prevent brute force attacks by limiting login attempts
-
Resource Protection: Protect expensive operations from overuse
-
DDoS Mitigation: Basic protection against distributed denial of service attacks
-
Fair Usage: Ensure fair resource distribution among clients
-
Abuse Prevention: Automatically block repeat offenders with blacklisting
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
The GNU General Public License v3.0. Please see License File for more information.