Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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());
}
}
85 changes: 25 additions & 60 deletions EventListener/RateLimitAnnotationListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@
use Noxlogic\RateLimitBundle\Events\GenerateKeyEvent;
use Noxlogic\RateLimitBundle\Events\RateLimitEvents;
use Noxlogic\RateLimitBundle\Exception\RateLimitExceptionInterface;
use Noxlogic\RateLimitBundle\LimitProcessorInterface;
use Noxlogic\RateLimitBundle\Service\RateLimitInfoManager;
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 Down Expand Up @@ -64,7 +66,12 @@ public function onKernelController(FilterControllerEvent $event)

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

$rateLimit = $this->findBestMethodMatch($event->getRequest(), $annotations);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if findBestMethodMatch has been deprecated, this library should not use it too

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch!
Indeed, I already moved the logic into class methods, so do not use findBestMethodMatch and keep it just for BC

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

// Another treatment before applying RateLimit ?
$checkedRateLimitEvent = new CheckedRateLimitEvent($event->getRequest(), $rateLimit);
Expand All @@ -76,36 +83,20 @@ 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
}
$rateLimitManager = new RateLimitInfoManager($this->rateLimitService);
$rateLimitInfo = $rateLimitManager->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()) {

Expand Down Expand Up @@ -136,10 +127,16 @@ 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());

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

$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 mixed|RateLimit|null
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why mixed here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true, will fix

*/
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
13 changes: 13 additions & 0 deletions Service/RateLimitInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,17 @@ public function setResetTimestamp($resetTimestamp)
{
$this->resetTimestamp = $resetTimestamp;
}

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

return $remaining;
}
}
40 changes: 40 additions & 0 deletions Service/RateLimitInfoManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace Noxlogic\RateLimitBundle\Service;

use Noxlogic\RateLimitBundle\Annotation\RateLimit;

class RateLimitInfoManager
{
/**
* @var RateLimitService
*/
private $rateLimitService;

public function __construct(RateLimitService $rateLimitService)
{
$this->rateLimitService = $rateLimitService;
}

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

// Reset the rate limits
if (time() >= $rateLimitInfo->getResetTimestamp()) {
$this->rateLimitService->resetRate($key);
$rateLimitInfo = $this->rateLimitService->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
66 changes: 66 additions & 0 deletions Tests/Service/RateLimitInfoManagerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

namespace Noxlogic\RateLimitBundle\Tests\Annotation;

use Noxlogic\RateLimitBundle\Annotation\RateLimit;
use Noxlogic\RateLimitBundle\Service\RateLimitInfoManager;
use Noxlogic\RateLimitBundle\Service\RateLimitService;
use Noxlogic\RateLimitBundle\Tests\EventListener\MockStorage;
use Noxlogic\RateLimitBundle\Tests\TestCase;

class RateLimitInfoManagerTest extends TestCase
{
public function testNoRateLimitInStorage()
{
$rateLimitService = new RateLimitService();
$rateLimitService->setStorage(new MockStorage());

$rateLimitInfoManager = new RateLimitInfoManager($rateLimitService);

$rateLimit = new RateLimit(array('methods' => 'POST', 'limit' => 1234, 'period' => 1000));

$rateLimitInfo = $rateLimitInfoManager->getRateLimitInfo('api', $rateLimit);

$this->assertInstanceOf('Noxlogic\\RateLimitBundle\\Service\\RateLimitInfo', $rateLimitInfo);
$this->assertEquals(1, $rateLimitInfo->getCalls());
$this->assertEquals(1234, $rateLimitInfo->getLimit());
$this->assertLessThanOrEqual(time() + 1000, $rateLimitInfo->getResetTimestamp());
}

public function testRateLimitInfoExistsInStorage()
{
$rateLimitService = new RateLimitService();
$mockStorage = new MockStorage();
$storageRateLimitInfo = $mockStorage->createMockRate('api', 1234, 1000, 800);
$rateLimitService->setStorage($mockStorage);

$rateLimitInfoManager = new RateLimitInfoManager($rateLimitService);
$rateLimit = new RateLimit(array('methods' => 'POST', 'limit' => 1234, 'period' => 1000));

$rateLimitInfo = $rateLimitInfoManager->getRateLimitInfo('api', $rateLimit);

$storageRateLimitInfo->setCalls(801);
$this->assertEquals($storageRateLimitInfo, $rateLimitInfo);
}

public function testRateLimitInfoResetCauseGreater()
{
$rateLimitService = new RateLimitService();
$mockStorage = new MockStorage();
$storageRateLimitInfo = $mockStorage->createMockRate('api', 1234, 1000, 800, time() - 1);
$rateLimitService->setStorage($mockStorage);

$rateLimitInfoManager = new RateLimitInfoManager($rateLimitService);

$rateLimit = new RateLimit(array('methods' => 'POST', 'limit' => 1234, 'period' => 1000));

$rateLimitInfo = $rateLimitInfoManager->getRateLimitInfo('api', $rateLimit);

$this->assertNotEquals($storageRateLimitInfo, $rateLimitInfo);

//New rateLimitInfo created
$this->assertInstanceOf('Noxlogic\\RateLimitBundle\\Service\\RateLimitInfo', $rateLimitInfo);
$this->assertEquals(1, $rateLimitInfo->getCalls());
$this->assertEquals(1234, $rateLimitInfo->getLimit());
}
}
20 changes: 16 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,20 @@ 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());
}
}
Loading