Skip to content

Throttling and rate limiting in a cohesive and simple way.

License

Notifications You must be signed in to change notification settings

cmatosbc/charon

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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.

Software License

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.