Giter Site home page Giter Site logo

throttle's People

Contributors

admad avatar arusinowski avatar bcrowe avatar bradmcnaughton avatar bravo-kernel avatar chrisshick avatar horlogeskynet avatar jadb avatar jorisvaesen avatar matthewdeaves avatar smarek avatar swiffer avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

throttle's Issues

Extend response with rate limit X-headers

I think the plugin could benefit of sending (optional) X-headers containing throttling information like Github does (limit, remaining, reset in image below).

Please let me know if this can make it in so I can create a PR.

api-schema

Usage of Throttle with some custom helper methods

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

  • to handle event, usually request must be parsed (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
  • i did not went with subclassing core's RouteBuilder and Route, because it seemed unnecessary, and there is no way to change the default classes anyway
  • my implementation of ThrottleRouteBuilder could just bail out on events for requests, that are not throttle configured instead of providing the DEFAULT_THROTTLE_GROUP, for requests that has no _name configured

routes.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));
    }
}

Middleware support

Any plans to make this work with middleware now that DispatchFilters are deprecated?

short description

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

Customize response

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.

Ability to set extra custom headers for response

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 ๐Ÿ‘‹

Whitelist option

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?

Response type and body content mismatch.

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.

Cache file not being created?

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?

Headers not set for error responses

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.

Use without authentication

Feature request: throttle api usage without authentication by IP address.

I have a public api what does not require any authentication of the users.

Redis support?

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

Application.php

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.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.