usemuffin / throttle Goto Github PK
View Code? Open in Web Editor NEW(API) Rate limiting requests in CakePHP
License: MIT License
(API) Rate limiting requests in CakePHP
License: MIT License
I would like to have two or three rates, based on a group. Is it possible to have multiple configurations with different rates or this value is fixed for all?
I tried to install this but failed so i searched for the package on the composer site and did not find it.
This is not issue/bug, just usability report, and possibly discussion about additional integration/helpers
I've implemented custom route-builder and route wrapper classes, so the integration in routes.php
looks nice.
ThrottleRoute allows you to config different throttling cases or none at all, ThrottleRouteBuilder then consolidates the configs provided and applies them in dispatched Throttle events
It might be interesting if Throttle integrated and supported something similar, to allow for more streamlined throttling configuration
The code posted is full of debug logging and other not-nice things, but I wanted to showcase the possibilities I see in this project now, and how we can be closer to symfony's RateLimitBundle
Also I wanted to say thanks for #38 because it really made this plugin usable and easy-to understand
Several things that might be worth addressing
Router::parseRequest()
) and since there is no caching in RouteCollection::parseRequest
this might not be the most effective way of doing things, and caching could be provided locally_name
configuredroutes.php
Router::defaultRouteClass(DashedRoute::class);
Router::scope('/', function (RouteBuilder $routes) {
// wrap the default builder
$routes = new ThrottleRouteBuilder($routes);
// use the wrapper to configure throttle of each route
$routes->connect('/login', ['controller'=>'Users', 'action' => 'login'], ['_name' => 'login_route'])
->setHost('myapp.example.com')
// i can disregard the default middleware config and throttle GET requests, eg. 100 get/fetch requests a minute
->throttleGET(100, 60)
// also I can, by one call, throttle all [POST, PUT, PATCH] methods, eg. 5 sign-in attemtps in 10 minutes period
->throttleSubmit(5, 60 * 10)
// or I can throttle by providing HTTP method, eg. HEAD only once a minute
->throttle(['HEAD'], 1, 60);
});
src/Routing/ThrottleRoute.php
<?php
declare(strict_types=1);
namespace App\Routing;
use Cake\Routing\Route\Route;
class ThrottleRoute
{
private ThrottleRouteBuilder $builder;
private Route $parent;
public function __construct(ThrottleRouteBuilder $_builder, Route $_parentRoute)
{
$this->builder = $_builder;
$this->parent = $_parentRoute;
}
public function setHost(string $host): ThrottleRoute
{
$this->parent->setHost($host);
return $this;
}
public function throttle(array $methods, ?int $limit = null, ?int $period = null)
{
$this->builder->setThrottleConfig($this->parent, $limit, $period, $methods);
}
public function throttlePOST(?int $limit = null, ?int $period = null): ThrottleRoute
{
$this->throttle(['POST'], $limit, $period);
return $this;
}
public function throttleGET(?int $limit = null, ?int $period = null): ThrottleRoute
{
$this->throttle(['GET'], $limit, $period);
return $this;
}
public function throttlePUT(?int $limit = null, ?int $period = null): ThrottleRoute
{
$this->throttle(['PUT'], $limit, $period);
return $this;
}
public function throttlePATCH(?int $limit = null, ?int $period = null): ThrottleRoute
{
$this->throttle(['PATCH'], $limit, $period);
return $this;
}
public function throttleSubmit(?int $limit = null, ?int $period = null): ThrottleRoute
{
$this->throttle(ThrottleRouteBuilder::ALL_HTTP_SUBMIT, $limit, $period);
return $this;
}
}
src/Routing/ThrottleRouteBuilder.php
<?php
declare(strict_types=1);
namespace App\Routing;
use Cake\Event\EventInterface;
use Cake\Event\EventManager;
use Cake\Http\ServerRequest;
use Cake\Log\Log;
use Cake\Routing\Route\Route;
use Cake\Routing\RouteBuilder;
use Cake\Routing\Router;
use Cake\Utility\Hash;
use Muffin\Throttle\Middleware\ThrottleMiddleware;
use Muffin\Throttle\ValueObject\RateLimitInfo;
use Muffin\Throttle\ValueObject\ThrottleInfo;
class ThrottleRouteBuilder
{
public const ALL_HTTP_SUBMIT = ['POST', 'PUT', 'PATCH'];
public const DEFAULT_THROTTLE_GROUP = '_global_throttle_';
private RouteBuilder $parent;
// name => http-method => [limit, period]
private array $throttleConfiguration = [];
public function __construct(RouteBuilder $_parentRouter)
{
$this->parent = $_parentRouter;
// hook get throttle info
EventManager::instance()->on(
ThrottleMiddleware::EVENT_GET_THROTTLE_INFO,
function (EventInterface $event, ServerRequest $request, ThrottleInfo $throtte): ?ThrottleInfo {
if ($event->isStopped()) {
Log::error('getThrottleInfo event is stopped');
return null;
}
return $this->getThrottleInfo($request, $throtte);
}
);
// hook get throttle key
EventManager::instance()->on(
ThrottleMiddleware::EVENT_GET_IDENTIFER,
function (EventInterface $event, ServerRequest $request): string {
if ($event->isStopped()) {
Log::error('getThrottleKey event is stopped');
}
return $this->getThrottleKey($request);
}
);
// hook before cache set to see the current limits/results of user actions
EventManager::instance()->on(
ThrottleMiddleware::EVENT_BEFORE_CACHE_SET,
function (EventInterface $event, RateLimitInfo $rateLimit, int $ttl): void {
Log::debug(sprintf("remaining(%d) resetTimestamp(%d) ttl(%d)", $rateLimit->getRemaining(), $rateLimit->getResetTimestamp(), $ttl));
}
);
}
/**
* @param Route|array $route
* @return string
*/
private function getRouteName($route): string
{
$_name = self::DEFAULT_THROTTLE_GROUP;
if ($route instanceof Route) {
$_name = $route->options['_name'] ?? self::DEFAULT_THROTTLE_GROUP;
}
if (is_array($route)) {
$_name = $route['_name'] ?? self::DEFAULT_THROTTLE_GROUP;
}
return $_name;
}
// sets the limit+period (if at least one of them provided) to all the methods in $httpMethod
public function setThrottleConfig(Route $route, ?int $limit = null, ?int $period = null, $httpMethod = self::ALL_HTTP_SUBMIT)
{
if (empty($limit) && empty($period)) {
return;
}
$httpMethod = is_array($httpMethod) ? $httpMethod : [$httpMethod];
$name = $this->getRouteName($route);
if (!isset($this->throttleConfiguration[$name])) {
$this->throttleConfiguration[$name] = [];
}
foreach ($httpMethod as $method) {
$this->throttleConfiguration[$name][strtolower($method)] = [
'limit' => $limit,
'period' => $period
];
}
}
public function getThrottleInfo(ServerRequest $request, ThrottleInfo $throttleInfo): ?ThrottleInfo
{
$route = Router::parseRequest($request);
$method = strtolower($request->getMethod());
$matchingConfig = Hash::extract($this->throttleConfiguration, sprintf("%s.%s", $this->getRouteName($route), $method));
if (!empty($matchingConfig)) {
$throttleInfo->appendToKey($method);
if (!empty($matchingConfig['limit'])) {
$throttleInfo->setLimit($matchingConfig['limit']);
}
if (!empty($matchingConfig['period'])) {
$throttleInfo->setPeriod($matchingConfig['period']);
}
}
Log::debug(sprintf("getThrottleInfo(after):: %s :: key(%s) limit(%d) period(%d)", $request->getRequestTarget(), $throttleInfo->getKey(), $throttleInfo->getLimit(), $throttleInfo->getPeriod()));
return $throttleInfo;
}
public function getThrottleKey(ServerRequest $request): string
{
$key = $request->clientIp() . '.' . $this->getRouteName(Router::parseRequest($request));
Log::debug(sprintf('getThrottleKey:: %s :: %s', $request->getRequestTarget(), $key));
return $key;
}
public function setRouteClass(string $routeClass): RouteBuilder
{
return $this->parent->setRouteClass($routeClass);
}
public function redirect(string $route, $url, array $options = []): ThrottleRoute
{
return new ThrottleRoute($this, $this->parent->redirect($route, $url, $options));
}
public function connect($route, $defaults = [], array $options = []): ThrottleRoute
{
return new ThrottleRoute($this, $this->parent->connect($route, $defaults, $options));
}
}
Any plans to make this work with middleware now that DispatchFilters are deprecated?
Latest version served/shown by Packagist is 45d2827 instead of most the recent commit. Something not updating there?
Is a CakePHP 4 compatible Release planned/in the making?
I think descriptions of the plugins issued for cake3 are very short.
(API) Rate limiting requests in CakePHP 3
I get no idea why this can be used for. I think adding few more sentences would be helpful
The RFC states that for a 429
HTTP status code a Retry-After
MAY be included indicating how long to wait before making a new request. Currently no Retry-After
header is set.
Customizing responses via event listeners would be handy, especially for when displaying different response bodies based on things like identifier, routes/paths, etc.
For instance:
public function getThrottleInfo(EventInterface $event, ServerRequest $request, ThrottleInfo $throttle): void
{
if (str_starts_with($request->getUri()->getPath(), '/api/auth')) {
$throttle->appendToKey('auth');
$throttle->setPeriod(60);
$throttle->setLimit(10);
// new feature:
$throttle->setResponse([
'body' => json_encode(['message' => 'my custom response']),
'type' => 'json',
]);
}
}
Maybe there is already a way to do this? I could maybe work around this with a beforeRender and reading the headers, but this seems like a better way to handle this. I just don't trust my API users to actually look at response headers.
Do you have a scheduled date to have a stable release for cake4?
Hi there !
A simple question (I'm ready to implement it if needed), would it be possible to let the developer set extra headers for the response sent, once the limitation is reached ?
For instance, if we want to throw a JSON-encoded message to the user, it'd be cool if the Content-Type
was set to application/json
.
What I would like to do currently is something like :
$routes->registerMiddleware('throttle', new ThrottleMiddleware([
'limit' => XXX,
'interval' => '+XXX minutes',
'message' => json_encode(['error' => 'A typical error message...']),
'headers' => [
'limit' => 'X-RateLimit-Limit',
'remaining' => 'X-RateLimit-Remaining',
'reset' => 'X-RateLimit-Reset',
'type' => 'json'
]
]));
Do you think it's a good idea ?
Thanks,
Bye ๐
It would be nice when we had an option to whitelist certain request so you could only limit unauthorized requests. Would it be an option to implement it to skip throttling when the identifier returns false or null or do you think of a better option?
How can I configure this plugin to not apply to all routes, because some routes don't have to be restricted ?
The default value for response type in case of rate limit exceeding is set to 'text/html'
but the body text is set to string Rate limit exceeded
which is not HTML. So response.type
config should probably default to text/plain
.
This is a enchantment request.
If the rate limit check logic will be exported in a separated class, this package can be used to throttling a specific code portion.
for example we can throttle a failed login.
Just added plain Muffin/Throttle and keep getting 'rate limit exceeded'. I believe because a cache file is not being created. Can you confirm a cache file should appear?
The rate limiting headers are not set when errors occur (test with 404 and 412 validation).
Is this expected behavior? If so, I will add a note to the docs.
Feature request: throttle api usage without authentication by IP address.
I have a public api what does not require any authentication of the users.
Hello,
how do I have to change this code to use the memcached cache instead of the _cake_core_cache?
According to documentation, cache engine supporting atomic operations and increments, must be used.
I've tried to use RedisEngine with middleware config 'interval' => '+1 minutes'
, but it seems that cached counts do not expire
RedisEngine has it's own cache configuration 'duration', number of seconds the items in cache last
However providing number of seconds as 'interval' configuration to ThrottleMiddleware will fail on _touch()
method parsing it as strtotime($this->getConfig('interval'), time())
Additionaly, TTL of _expires cache item is not bumped, because except for initial configuration, it's not touched the same way as the counter cache. So the whole throttle logic waits for both items to expire in cache, be evicted, and then allows for new non-throttled request. Which means as long as the requests are made periodically, faster than cache expiration, throttle will never, after initial limit hit, allow the request be made, and I'm not sure if that is intentional
I'm not sure how to fix that, so I'm writing this issue instead
Hello,
Here I want to use this rate limit in my application but I am not able to find Application.php file in my source code. Also I got new repository from Server but I can't get the file.
Please help me to get rid out of it.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.