From 3b9de979e4c3026152ba288dc4dc1d9c94898a01 Mon Sep 17 00:00:00 2001 From: Fillip Hannisdal Date: Thu, 9 Feb 2017 23:29:15 +0000 Subject: [PATCH] Rate Limits by Route Allow the ability to update rate limits by route --- config/rate_limit.global.php.dist | 5 +++ src/Mvc/RateLimitRequestListener.php | 27 +++++++++++--- src/Options/RateLimitOptions.php | 55 ++++++++++++++++++++++++++++ src/Service/RateLimitService.php | 41 +++++++-------------- 4 files changed, 94 insertions(+), 34 deletions(-) diff --git a/config/rate_limit.global.php.dist b/config/rate_limit.global.php.dist index 3272a83..ddc5372 100644 --- a/config/rate_limit.global.php.dist +++ b/config/rate_limit.global.php.dist @@ -12,6 +12,11 @@ return [ */ 'routes' => [], + /** + * Lets you define separate limit / period settings per-route. Example: 'dragon-byte-tech.rpc.version-check' => ['limit' => 10, 'period' => 30] + */ + 'route_specific_limits' => [], + /** * This should be an array compatible with Zend Cache. */ diff --git a/src/Mvc/RateLimitRequestListener.php b/src/Mvc/RateLimitRequestListener.php index ef433b4..5fc6973 100644 --- a/src/Mvc/RateLimitRequestListener.php +++ b/src/Mvc/RateLimitRequestListener.php @@ -76,8 +76,7 @@ public function attach(EventManagerInterface $events, $priority = 1) public function onRoute(MvcEvent $event) { $request = $event->getRequest(); - $router = $event->getRouter(); - $routeMatch = $router->match($request); + $routeMatch = $event->getRouter()->match($request); if (!$request instanceof HttpRequest) { return; @@ -94,13 +93,13 @@ public function onRoute(MvcEvent $event) try { // Check if we're within the limit - $this->rateLimitService->rateLimitHandler(); + $this->rateLimitService->rateLimitHandler($routeMatch->getMatchedRouteName()); // Update the response $response = $event->getResponse(); // Add the headers to the response - $this->rateLimitService->ensureHeaders($response); + $this->ensureHeaders($response); // Set the response back $event->setResponse($response); @@ -113,13 +112,28 @@ public function onRoute(MvcEvent $event) ); // Add the headers so clients will know when they can try again - $this->rateLimitService->ensureHeaders($response); + $this->ensureHeaders($response); // And we're done here return $response; } } + /** + * @param HttpResponse $response + * @return \Zend\Http\Headers + */ + public function ensureHeaders(HttpResponse $response) + { + $headers = $response->getHeaders(); + + $headers->addHeaderLine('X-RateLimit-Limit', $this->rateLimitService->getLimit()); + $headers->addHeaderLine('X-RateLimit-Remaining', $this->rateLimitService->getRemainingCalls()); + $headers->addHeaderLine('X-RateLimit-Reset', $this->rateLimitService->getTimeToReset()); + + return $headers; + } + /** * @param RouteMatch $routeMatch * @return bool @@ -127,13 +141,14 @@ public function onRoute(MvcEvent $event) private function hasRoute(RouteMatch $routeMatch) { $routes = $this->rateLimitService->getRoutes(); + $currentRoute = $routeMatch->getMatchedRouteName(); if (!$routes) { return false; } foreach ($routes as $route) { - if (fnmatch($route, $routeMatch->getMatchedRouteName())) { + if (fnmatch($route, $currentRoute)) { return true; } } diff --git a/src/Options/RateLimitOptions.php b/src/Options/RateLimitOptions.php index 95f9daf..3eaf43a 100644 --- a/src/Options/RateLimitOptions.php +++ b/src/Options/RateLimitOptions.php @@ -38,6 +38,11 @@ class RateLimitOptions extends AbstractOptions */ protected $routes = []; + /** + * @var array + */ + protected $route_specific_limits = []; + /** * @var int */ @@ -48,6 +53,17 @@ class RateLimitOptions extends AbstractOptions */ protected $period = 0; + + /** + * Constructor + * + * @param array|Traversable|null $options + */ + public function __construct($options = null) + { + parent::__construct($options); + } + /** * @return string */ @@ -80,6 +96,22 @@ public function setRoutes($routes) $this->routes = $routes; } + /** + * @param array $routeSpecificLimits + */ + public function setRouteSpecificLimits($routeSpecificLimits) + { + $this->route_specific_limits = $routeSpecificLimits; + } + + /** + * @return array + */ + public function getRouteSpecificLimits() + { + return $this->route_specific_limits; + } + /** * @return int */ @@ -111,4 +143,27 @@ public function setPeriod($period) { $this->period = $period; } + + /** + * @param string $route + */ + public function setRouteSpecificLimitsFromRoute($route) + { + $routeSpecificLimits = $this->getRouteSpecificLimits(); + if (!$route OR !isset($routeSpecificLimits[$route])) + { + return; + } + + $options = $routeSpecificLimits[$route]; + + if (!is_array($options) AND !$options instanceof Traversable) + { + return; + } + + foreach ($options as $key => $value) { + $this->__set($key, $value); + } + } } diff --git a/src/Service/RateLimitService.php b/src/Service/RateLimitService.php index 11b7998..2872dbe 100644 --- a/src/Service/RateLimitService.php +++ b/src/Service/RateLimitService.php @@ -55,8 +55,14 @@ public function __construct(AbstractAdapter $storage, RateLimitOptions $rateLimi /** * @inheritdoc */ - public function rateLimitHandler() + public function rateLimitHandler($route = '') { + if ($route) + { + // Override the options based on this route + $this->rateLimitOptions->setRouteSpecificLimitsFromRoute($route); + } + if ($this->getRemainingCalls() == 0) { throw new TooManyRequestsHttpException('Too Many Requests'); } @@ -71,48 +77,27 @@ public function rateLimitHandler() $this->saveDataInStorage($data); } - /** - * @param HttpResponse $response - * @return \Zend\Http\Headers - */ - public function ensureHeaders(HttpResponse $response) - { - $headers = $response->getHeaders(); - - $headers->addHeaderLine('X-RateLimit-Limit', $this->getLimit()); - $headers->addHeaderLine('X-RateLimit-Remaining', $this->getRemainingCalls()); - $headers->addHeaderLine('X-RateLimit-Reset', $this->getTimeToReset()); - - return $headers; - } - /** * @return int */ - private function getLimit() + public function getLimit() { - $limit = $this->rateLimitOptions->getLimit(); - return $limit; + return $this->rateLimitOptions->getLimit(); } /** * @return int */ - private function getRemainingCalls() + public function getRemainingCalls() { - // Get the data from the cache - $data = $this->getDataFromStorage(); - - $limit = $this->rateLimitOptions->getLimit(); - $calls = count($data); - - return $limit - $calls; + // Make sure we never go below 0 as that will trip up the headers + return max(0, $this->getLimit() - count($this->getDataFromStorage())); } /** * @return int */ - private function getTimeToReset() + public function getTimeToReset() { // Get the data from the cache $data = $this->getDataFromStorage();