Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions EventListener/HeaderModificationListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,9 @@ public function onKernelResponse(FilterResponseEvent $event)
}

/** @var RateLimitInfo $rateLimitInfo */

$remaining = $rateLimitInfo->getLimit() - $rateLimitInfo->getCalls();
if ($remaining < 0) {
$remaining = 0;
}

$response = $event->getResponse();
$response->headers->set($this->getParameter('header_limit_name'), $rateLimitInfo->getLimit());
$response->headers->set($this->getParameter('header_remaining_name'), $remaining);
$response->headers->set($this->getParameter('header_remaining_name'), $rateLimitInfo->getRemainingAttempts());
$response->headers->set($this->getParameter('header_reset_name'), $rateLimitInfo->getResetTimestamp());
}
}
93 changes: 29 additions & 64 deletions EventListener/RateLimitAnnotationListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
use Noxlogic\RateLimitBundle\Events\GenerateKeyEvent;
use Noxlogic\RateLimitBundle\Events\RateLimitEvents;
use Noxlogic\RateLimitBundle\Exception\RateLimitExceptionInterface;
use Noxlogic\RateLimitBundle\LimitProcessorInterface;
use Noxlogic\RateLimitBundle\Service\RateLimitService;
use Noxlogic\RateLimitBundle\Util\AnnotationLimitProcessor;
use Noxlogic\RateLimitBundle\Util\PathLimitProcessor;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Routing\Route;

class RateLimitAnnotationListener extends BaseListener
{
Expand All @@ -35,7 +36,9 @@ class RateLimitAnnotationListener extends BaseListener
protected $pathLimitProcessor;

/**
* @param RateLimitService $rateLimitService
* @param EventDispatcherInterface $eventDispatcher
* @param RateLimitService $rateLimitService
* @param PathLimitProcessor $pathLimitProcessor
*/
public function __construct(
EventDispatcherInterface $eventDispatcher,
Expand Down Expand Up @@ -64,7 +67,13 @@ public function onKernelController(FilterControllerEvent $event)

// Find the best match
$annotations = $event->getRequest()->attributes->get('_x-rate-limit', array());
$rateLimit = $this->findBestMethodMatch($event->getRequest(), $annotations);

$limitProcessor = $this->pathLimitProcessor;
if ($annotations) {
$limitProcessor = new AnnotationLimitProcessor($annotations, $event->getController());
}

$rateLimit = $limitProcessor->getRateLimit($event->getRequest());

// Another treatment before applying RateLimit ?
$checkedRateLimitEvent = new CheckedRateLimitEvent($event->getRequest(), $rateLimit);
Expand All @@ -76,38 +85,21 @@ public function onKernelController(FilterControllerEvent $event)
return;
}

$key = $this->getKey($event, $rateLimit, $annotations);
$key = $this->getKey($limitProcessor, $rateLimit, $event->getRequest());

// Ratelimit the call
$rateLimitInfo = $this->rateLimitService->limitRate($key);
if (! $rateLimitInfo) {
// Create new rate limit entry for this call
$rateLimitInfo = $this->rateLimitService->createRate($key, $rateLimit->getLimit(), $rateLimit->getPeriod());
if (! $rateLimitInfo) {
// @codeCoverageIgnoreStart
return;
// @codeCoverageIgnoreEnd
}
$rateLimitInfo = $this->rateLimitService->getRateLimitInfo($key, $rateLimit);
if (!$rateLimitInfo) {
// @codeCoverageIgnoreStart
return;
// @codeCoverageIgnoreEnd
}


// Store the current rating info in the request attributes
$request = $event->getRequest();
$request->attributes->set('rate_limit_info', $rateLimitInfo);

// Reset the rate limits
if(time() >= $rateLimitInfo->getResetTimestamp()) {
$this->rateLimitService->resetRate($key);
$rateLimitInfo = $this->rateLimitService->createRate($key, $rateLimit->getLimit(), $rateLimit->getPeriod());
if (! $rateLimitInfo) {
// @codeCoverageIgnoreStart
return;
// @codeCoverageIgnoreEnd
}
}

// When we exceeded our limit, return a custom error response
if ($rateLimitInfo->getCalls() > $rateLimitInfo->getLimit()) {
if ($rateLimitInfo->isExceeded()) {

// Throw an exception if configured.
if ($this->getParameter('rate_response_exception')) {
Expand All @@ -134,12 +126,17 @@ public function onKernelController(FilterControllerEvent $event)

}


/**
* @param Request $request
* @param RateLimit[] $annotations
* @return RateLimit|null
*
* @deprecated since 1.15, use the "\Noxlogic\RateLimitBundle\LimitProcessorInterface::getRateLimit()" method instead.
*/
protected function findBestMethodMatch(Request $request, array $annotations)
{
@trigger_error(sprintf('The "%s()" method is deprecated since version 1.15, use the "\Noxlogic\RateLimitBundle\LimitProcessorInterface::getRateLimit()" method instead.', __METHOD__), E_USER_DEPRECATED);

// Empty array, check the path limits
if (count($annotations) == 0) {
return $this->pathLimitProcessor->getRateLimit($request);
Expand All @@ -163,48 +160,16 @@ protected function findBestMethodMatch(Request $request, array $annotations)
return $best_match;
}

private function getKey(FilterControllerEvent $event, RateLimit $rateLimit, array $annotations)
private function getKey(LimitProcessorInterface $limitProcessor, RateLimit $rateLimit, Request $request)
{
// Let listeners manipulate the key
$keyEvent = new GenerateKeyEvent($event->getRequest(), '', $rateLimit->getPayload());
$keyEvent = new GenerateKeyEvent($request, '', $rateLimit->getPayload());

$rateLimitMethods = join('.', $rateLimit->getMethods());
$keyEvent->addToKey($rateLimitMethods);

$rateLimitAlias = count($annotations) === 0
? str_replace('/', '.', $this->pathLimitProcessor->getMatchedPath($event->getRequest()))
: $this->getAliasForRequest($event);
$keyEvent->addToKey($rateLimitAlias);
$keyEvent->addToKey(join('.', $rateLimit->getMethods()));
$keyEvent->addToKey($limitProcessor->getRateLimitAlias($request));

$this->eventDispatcher->dispatch(RateLimitEvents::GENERATE_KEY, $keyEvent);

return $keyEvent->getKey();
}

private function getAliasForRequest(FilterControllerEvent $event)
{
if (($route = $event->getRequest()->attributes->get('_route'))) {
return $route;
}

$controller = $event->getController();

if (is_string($controller) && false !== strpos($controller, '::')) {
$controller = explode('::', $controller);
}

if (is_array($controller)) {
return str_replace('\\', '.', is_string($controller[0]) ? $controller[0] : get_class($controller[0])) . '.' . $controller[1];
}

if ($controller instanceof \Closure) {
return 'closure';
}

if (is_object($controller)) {
return str_replace('\\', '.', get_class($controller[0]));
}

return 'other';
}
}
21 changes: 21 additions & 0 deletions LimitProcessorInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Noxlogic\RateLimitBundle;

use Noxlogic\RateLimitBundle\Annotation\RateLimit;
use Symfony\Component\HttpFoundation\Request;

interface LimitProcessorInterface
{
/**
* @param Request $request
* @return RateLimit|null
*/
public function getRateLimit(Request $request);

/**
* @param Request $request
* @return string
*/
public function getRateLimitAlias(Request $request);
}
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ This bundle is partially inspired by a GitHub gist from Ruud Kamphuis: https://g

## Features

* Simple usage through annotations
* Simple usage through annotations, configuration file
* Multilayer rules. General rules with redeclaration
* Customize rates per controller, action and even per HTTP method
* Multiple storage backends: Redis, Memcached and Doctrine cache

Expand Down Expand Up @@ -294,6 +295,11 @@ class IpBasedRateLimitGenerateKeyListener
}
```

## Using with other frameworks
Package can be integrated in any framework based on ``Symfony\Component\HttpFoundation\Request``.

[Example integration in Laravel middleware](docs/laravel-middleware-example.md)


## Throwing exceptions

Expand Down
21 changes: 21 additions & 0 deletions Service/RateLimitInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,25 @@ public function setResetTimestamp($resetTimestamp)
{
$this->resetTimestamp = $resetTimestamp;
}

/**
* @return int
*/
public function getRemainingAttempts()
{
$remaining = $this->getLimit() - $this->getCalls();
if ($remaining < 0) {
$remaining = 0;
}

return $remaining;
}

/**
* @return bool
*/
public function isExceeded()
{
return $this->getCalls() > $this->getLimit();
}
}
24 changes: 24 additions & 0 deletions Service/RateLimitService.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Noxlogic\RateLimitBundle\Service;

use Noxlogic\RateLimitBundle\Annotation\RateLimit;
use Noxlogic\RateLimitBundle\Service\Storage\StorageInterface;

class RateLimitService
Expand Down Expand Up @@ -54,4 +55,27 @@ public function resetRate($key)
{
return $this->storage->resetRate($key);
}


/**
* @param string $key
* @param RateLimit $rateLimit
* @return RateLimitInfo|null
*/
public function getRateLimitInfo($key, RateLimit $rateLimit)
{
$rateLimitInfo = $this->limitRate($key);
if (!$rateLimitInfo) {
// Create new rate limit entry for this call
return $this->createRate($key, $rateLimit->getLimit(), $rateLimit->getPeriod());
}

// Reset the rate limits
if (time() >= $rateLimitInfo->getResetTimestamp()) {
$this->resetRate($key);
$rateLimitInfo = $this->createRate($key, $rateLimit->getLimit(), $rateLimit->getPeriod());
}

return $rateLimitInfo;
}
}
4 changes: 2 additions & 2 deletions Tests/EventListener/MockStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ public function resetRate($key)
unset($this->rates[$key]);
}

public function createMockRate($key, $limit, $period, $calls)
public function createMockRate($key, $limit, $period, $calls, $resetTime = null)
{
$this->rates[$key] = array('calls' => $calls, 'limit' => $limit, 'reset' => (time() + $period));
$this->rates[$key] = array('calls' => $calls, 'limit' => $limit, 'reset' => $resetTime ? $resetTime : (time() + $period));
return $this->getRateInfo($key);
}
}
1 change: 0 additions & 1 deletion Tests/EventListener/RateLimitAnnotationListenerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

use Noxlogic\RateLimitBundle\Annotation\RateLimit;
use Noxlogic\RateLimitBundle\EventListener\RateLimitAnnotationListener;
use Noxlogic\RateLimitBundle\Events\GenerateKeyEvent;
use Noxlogic\RateLimitBundle\Events\RateLimitEvents;
use Noxlogic\RateLimitBundle\Service\RateLimitService;
use Noxlogic\RateLimitBundle\Tests\EventListener\MockStorage;
Expand Down
41 changes: 37 additions & 4 deletions Tests/Service/RateLimitInfoTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,11 @@

namespace Noxlogic\RateLimitBundle\Tests\Annotation;

use Noxlogic\RateLimitBundle\EventListener\OauthKeyGenerateListener;
use Noxlogic\RateLimitBundle\Events\GenerateKeyEvent;
use Noxlogic\RateLimitBundle\Service\RateLimitInfo;
use Noxlogic\RateLimitBundle\Tests\TestCase;
use Symfony\Component\HttpFoundation\Request;

class RateLimitInfoTest extends TestCase
{

public function testRateInfoSetters()
{
$rateInfo = new RateLimitInfo();
Expand All @@ -25,4 +21,41 @@ public function testRateInfoSetters()
$this->assertEquals(100000, $rateInfo->getResetTimestamp());
}

public function testRemainingAttempts()
{
$rateInfo = new RateLimitInfo();

$rateInfo->setLimit(10);
$rateInfo->setCalls(9);
$this->assertEquals(1, $rateInfo->getRemainingAttempts());

$rateInfo->setLimit(10);
$rateInfo->setCalls(10);
$this->assertEquals(0, $rateInfo->getRemainingAttempts());

$rateInfo->setLimit(10);
$rateInfo->setCalls(20);
$this->assertEquals(0, $rateInfo->getRemainingAttempts());
}

public function testIsExceededLimit()
{
$rateInfo = new RateLimitInfo();

$rateInfo->setLimit(10);
$rateInfo->setCalls(9);
$this->assertFalse($rateInfo->isExceeded());

$rateInfo->setLimit(10);
$rateInfo->setCalls(10);
$this->assertFalse($rateInfo->isExceeded());

$rateInfo->setLimit(10);
$rateInfo->setCalls(11);
$this->assertTrue($rateInfo->isExceeded());

$rateInfo->setLimit(10);
$rateInfo->setCalls(20);
$this->assertTrue($rateInfo->isExceeded());
}
}
Loading