From 51d8580196716f14b6cfda0b50eb9065bcc17578 Mon Sep 17 00:00:00 2001 From: amanto Date: Wed, 3 Apr 2019 17:19:42 +0300 Subject: [PATCH 01/10] Refactor annotation and path limits logic working in same way. * Extract annotation logic into separate class AnnotationLimitProcessor, cover with tests moved logic from getAliasForRequest method * Deprecate findBestMethodMatch method. Keep it Backward Compatible * Deprecate PathLimitProcessor->getMatchedPath. Keep it Backward Compatible * Create LimitProcessorInterface, add to PathLimitProcessor, AnnotationLimitProcessor * Fix getAliasForRequest, controller as object with __invoke method implemented --- EventListener/RateLimitAnnotationListener.php | 85 ++++--------- LimitProcessorInterface.php | 21 ++++ .../RateLimitAnnotationListenerTest.php | 1 - .../AnnotationLimitProcessorGetAliasTest.php | 116 ++++++++++++++++++ ...notationLimitProcessorGetRateLimitTest.php | 110 +++++++++++++++++ Tests/Util/PathLimitProcessorTest.php | 68 +++++++++- Util/AnnotationLimitProcessor.php | 73 +++++++++++ Util/PathLimitProcessor.php | 44 +++++-- 8 files changed, 443 insertions(+), 75 deletions(-) create mode 100644 LimitProcessorInterface.php create mode 100644 Tests/Util/AnnotationLimitProcessorGetAliasTest.php create mode 100644 Tests/Util/AnnotationLimitProcessorGetRateLimitTest.php create mode 100644 Util/AnnotationLimitProcessor.php diff --git a/EventListener/RateLimitAnnotationListener.php b/EventListener/RateLimitAnnotationListener.php index 00fc499..119f748 100644 --- a/EventListener/RateLimitAnnotationListener.php +++ b/EventListener/RateLimitAnnotationListener.php @@ -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 { @@ -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); + $limitProcessor = $this->pathLimitProcessor; + if ($annotations) { + $limitProcessor = new AnnotationLimitProcessor($annotations, $event->getController()); + } // Another treatment before applying RateLimit ? $checkedRateLimitEvent = new CheckedRateLimitEvent($event->getRequest(), $rateLimit); @@ -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()) { @@ -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); @@ -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'; - } } diff --git a/LimitProcessorInterface.php b/LimitProcessorInterface.php new file mode 100644 index 0000000..9819ca0 --- /dev/null +++ b/LimitProcessorInterface.php @@ -0,0 +1,21 @@ +attributes->set('_route', 'api.users'); + + $this->assertEquals('api.users', $annotationPathLimit->getRateLimitAlias($request)); + } + + /** + * @dataProvider provideControllerCallables + * + * @param $testName + * @param $controllerCallable + * @param $expected + */ + public function testGetRateLimitAliasControllerCallable($testName, $controllerCallable, $expected) + { + $annotationProcessor = new AnnotationLimitProcessor(array(), $controllerCallable); + $this->assertEquals($expected, $annotationProcessor->getRateLimitAlias(new Request()), $testName); + } + + public function provideControllerCallables() + { + //Controller examples taken from Symfony\Component\HttpKernel\Tests\DataCollector\RequestDataCollectorTest + return array( + array( + '"Regular" callable', + array($this, 'testControllerInspection'), + 'Tests.Util.AnnotationLimitProcessorGetAliasTest.testControllerInspection', + ), + + array( + 'Closure', + function () {}, + 'closure', + ), + + array( + 'Static callback as string', + 'Tests\Util\AnnotationLimitProcessorGetAliasTest::staticControllerMethod', + 'Tests.Util.AnnotationLimitProcessorGetAliasTest.staticControllerMethod', + ), + + array( + 'Static callable with instance', + array($this, 'staticControllerMethod'), + 'Tests.Util.AnnotationLimitProcessorGetAliasTest.staticControllerMethod', + ), + + array( + 'Static callable with class name', + array('Tests\Util\AnnotationLimitProcessorGetAliasTest', 'staticControllerMethod'), + 'Tests.Util.AnnotationLimitProcessorGetAliasTest.staticControllerMethod', + ), + + array( + 'Callable with instance depending on __call()', + array($this, 'magicMethod'), + 'Tests.Util.AnnotationLimitProcessorGetAliasTest.magicMethod', + ), + + array( + 'Callable with class name depending on __callStatic()', + array('Tests\Util\AnnotationLimitProcessorGetAliasTest', 'magicMethod'), + 'Tests.Util.AnnotationLimitProcessorGetAliasTest.magicMethod', + ), + + array( + 'Invokable controller', + $this, + 'Tests.Util.AnnotationLimitProcessorGetAliasTest', + ), + ); + } + + /** + * Dummy method used as controller callable. + */ + public static function staticControllerMethod() + { + throw new LogicException('Unexpected method call'); + } + + /** + * Magic method to allow non existing methods to be called and delegated. + */ + public function __call($method, $args) + { + throw new LogicException('Unexpected method call'); + } + + /** + * Magic method to allow non existing methods to be called and delegated. + */ + public static function __callStatic($method, $args) + { + throw new LogicException('Unexpected method call'); + } + + public function __invoke() + { + throw new LogicException('Unexpected method call'); + } +} diff --git a/Tests/Util/AnnotationLimitProcessorGetRateLimitTest.php b/Tests/Util/AnnotationLimitProcessorGetRateLimitTest.php new file mode 100644 index 0000000..f10abd8 --- /dev/null +++ b/Tests/Util/AnnotationLimitProcessorGetRateLimitTest.php @@ -0,0 +1,110 @@ + 'GET', 'limit' => 100, 'period' => 3600)), + ); + + $annotationLimitProcess = $this->getAnnotationLimitProcessor($annotations); + + $request->setMethod('PUT'); + $this->assertNull($annotationLimitProcess->getRateLimit($request)); + + $request->setMethod('GET'); + $this->assertEquals( + $annotations[0], + $annotationLimitProcess->getRateLimit($request) + ); + } + + public function testGetRateLimitMatchingMultipleAnnotations() + { + $request = new Request(); + + $annotations = array( + new RateLimit(array('methods' => 'GET', 'limit' => 100, 'period' => 3600)), + new RateLimit(array('methods' => array('GET','PUT'), 'limit' => 200, 'period' => 7200)), + ); + + $annotationLimitProcess = $this->getAnnotationLimitProcessor($annotations); + + $request->setMethod('PUT'); + $this->assertEquals($annotations[1], $annotationLimitProcess->getRateLimit($request)); + + $request->setMethod('GET'); + $this->assertEquals($annotations[1], $annotationLimitProcess->getRateLimit($request)); + } + + public function testBestMethodMatch() + { + $request = new Request(); + + $annotations = array( + new RateLimit(array('limit' => 100, 'period' => 3600)), + new RateLimit(array('methods' => 'GET', 'limit' => 100, 'period' => 3600)), + new RateLimit(array('methods' => array('POST', 'PUT'), 'limit' => 100, 'period' => 3600)), + ); + + $annotationLimitProcess = $this->getAnnotationLimitProcessor($annotations); + + // Find the method that matches the string + $request->setMethod('GET'); + $this->assertEquals( + $annotations[1], + $annotationLimitProcess->getRateLimit($request) + ); + + // Method not found, use the default one + $request->setMethod('DELETE'); + $this->assertEquals( + $annotations[0], + $annotationLimitProcess->getRateLimit($request) + ); + + // Find best match based in methods in array + $request->setMethod('PUT'); + $this->assertEquals( + $annotations[2], + $annotationLimitProcess->getRateLimit($request) + ); + } + + public function testFindNoAnnotations() + { + $request = new Request(); + + $annotations = array(); + + $annotationLimitProcess = $this->getAnnotationLimitProcessor($annotations); + + $request->setMethod('PUT'); + $this->assertNull($annotationLimitProcess->getRateLimit($request)); + + $request->setMethod('GET'); + $this->assertNull($annotationLimitProcess->getRateLimit($request)); + } +} \ No newline at end of file diff --git a/Tests/Util/PathLimitProcessorTest.php b/Tests/Util/PathLimitProcessorTest.php index 99b5dbb..749cb98 100644 --- a/Tests/Util/PathLimitProcessorTest.php +++ b/Tests/Util/PathLimitProcessorTest.php @@ -272,6 +272,25 @@ function itReturnsTheMatchedPath() $this->assertEquals('api', $path); } + /** @test */ + function itReturnsTheMatchedPathAlias() + { + $plp = new PathLimitProcessor(array( + 'api' => array( + 'path' => 'api/users/email', + 'methods' => array('GET', 'POST'), + 'limit' => 1000, + 'period' => 600 + ) + )); + + $path = $plp->getRateLimitAlias( + Request::create('/api/users/email', 'POST') + ); + + $this->assertEquals('api.users.email', $path); + } + /** @test */ function itReturnsTheCorrectPathForADifferentSetup() { @@ -297,12 +316,37 @@ function itReturnsTheCorrectPathForADifferentSetup() $this->assertEquals('api/users/emails', $path); } + /** @test */ + function itReturnsTheCorrectPathAliasForADifferentSetup() + { + $plp = new PathLimitProcessor(array( + 'api' => array( + 'path' => 'api', + 'methods' => array('GET'), + 'limit' => 5, + 'period' => 1 + ), + 'api_emails' => array( + 'path' => 'api/users/emails', + 'methods' => array('GET'), + 'limit' => 100, + 'period' => 60 + ) + )); + + $path = $plp->getRateLimitAlias( + Request::create('/api/users/emails', 'GET') + ); + + $this->assertEquals('api.users.emails', $path); + } + /** @test */ function itReturnsTheCorrectMatchedPathForSubPaths() { $plp = new PathLimitProcessor(array( 'api' => array( - 'path' => 'api/', + 'path' => 'api/users', 'methods' => array('GET'), 'limit' => 100, 'period' => 60 @@ -313,7 +357,25 @@ function itReturnsTheCorrectMatchedPathForSubPaths() Request::create('/api/users/emails', 'GET') ); - $this->assertEquals('api', $path); + $this->assertEquals('api/users', $path); + } + + /** @test */ + function itReturnsTheCorrectMatchedPathAliasForSubPaths() + { + $plp = new PathLimitProcessor(array( + 'api' => array( + 'path' => 'api/users', + 'methods' => array('GET'), + 'limit' => 100, + 'period' => 60 + ) + )); + + $path = $plp->getRateLimitAlias( + Request::create('/api/users/emails', 'GET') + ); + + $this->assertEquals('api.users', $path); } } - \ No newline at end of file diff --git a/Util/AnnotationLimitProcessor.php b/Util/AnnotationLimitProcessor.php new file mode 100644 index 0000000..0e6876c --- /dev/null +++ b/Util/AnnotationLimitProcessor.php @@ -0,0 +1,73 @@ +annotations = $annotations; + $this->controller = $controller; + } + + public function getRateLimit(Request $request) + { + $best_match = null; + foreach ($this->annotations as $annotation) { + // cast methods to array, even method holds a string + $methods = is_array($annotation->getMethods()) ? $annotation->getMethods() : array($annotation->getMethods()); + + if (in_array($request->getMethod(), $methods)) { + $best_match = $annotation; + } + + // Only match "default" annotation when we don't have a best match + if (count($annotation->getMethods()) == 0 && $best_match == null) { + $best_match = $annotation; + } + } + + return $best_match; + } + + public function getRateLimitAlias(Request $request) + { + if (($route = $request->attributes->get('_route'))) { + return $route; + } + + $controller = $this->controller; + + 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)); + } + + return 'other'; + } +} \ No newline at end of file diff --git a/Util/PathLimitProcessor.php b/Util/PathLimitProcessor.php index ebe4723..7a69639 100644 --- a/Util/PathLimitProcessor.php +++ b/Util/PathLimitProcessor.php @@ -4,9 +4,10 @@ namespace Noxlogic\RateLimitBundle\Util; use Noxlogic\RateLimitBundle\Annotation\RateLimit; +use Noxlogic\RateLimitBundle\LimitProcessorInterface; use Symfony\Component\HttpFoundation\Request; -class PathLimitProcessor +class PathLimitProcessor implements LimitProcessorInterface { private $pathLimits; @@ -44,18 +45,21 @@ public function getRateLimit(Request $request) return null; } + /** + * @deprecated since version 1.15, use getRateLimitAlias method instead. + * + * @param Request $request + * @return string + */ public function getMatchedPath(Request $request) { - $path = trim($request->getPathInfo(), '/'); - $method = $request->getMethod(); - - foreach ($this->pathLimits as $pathLimit) { - if ($this->requestMatched($pathLimit, $path, $method)) { - return $pathLimit['path']; - } - } + @trigger_error(sprintf('The "%s()" method is deprecated since version 1.15, use the "getRateLimitAlias()" method instead.', __METHOD__), E_USER_DEPRECATED); + return $this->getMatchedLimitPath($request); + } - return ''; + public function getRateLimitAlias(Request $request) + { + return str_replace('/', '.', $this->getMatchedLimitPath($request)); } private function requestMatched($pathLimit, $path, $method) @@ -92,4 +96,22 @@ private function pathMatched($expectedPath, $path) return true; } -} \ No newline at end of file + + /** + * @param Request $request + * @return string + */ + private function getMatchedLimitPath(Request $request) + { + $path = trim($request->getPathInfo(), '/'); + $method = $request->getMethod(); + + foreach ($this->pathLimits as $pathLimit) { + if ($this->requestMatched($pathLimit, $path, $method)) { + return $pathLimit['path']; + } + } + + return ''; + } +} From ee7fd51e159a0a5b9394219d1a043b0bacc31b3c Mon Sep 17 00:00:00 2001 From: amanto Date: Wed, 3 Apr 2019 22:07:46 +0300 Subject: [PATCH 02/10] Move RateLimitInfo retrieving logic into separate class, add tests. * Created RateLimitInfoManager with all RateLimitInfo logic * Set request rate_limit_info attr in the end, after all RateLimitInfo logic * Tests/EventListener/MockStorage::createMockRate add optional reset time param * Move remaining attempts calculation into RateLimitInfo::getRemainingAttempts, cover with tests --- EventListener/HeaderModificationListener.php | 8 +-- LimitProcessorInterface.php | 2 +- Service/RateLimitInfo.php | 13 ++++ Service/RateLimitInfoManager.php | 40 +++++++++++ Tests/EventListener/MockStorage.php | 4 +- Tests/Service/RateLimitInfoManagerTest.php | 66 +++++++++++++++++++ Tests/Service/RateLimitInfoTest.php | 20 ++++-- ...notationLimitProcessorGetRateLimitTest.php | 2 +- Util/AnnotationLimitProcessor.php | 2 +- Util/PathLimitProcessor.php | 1 - 10 files changed, 141 insertions(+), 17 deletions(-) create mode 100644 Service/RateLimitInfoManager.php create mode 100644 Tests/Service/RateLimitInfoManagerTest.php diff --git a/EventListener/HeaderModificationListener.php b/EventListener/HeaderModificationListener.php index db89178..065757b 100644 --- a/EventListener/HeaderModificationListener.php +++ b/EventListener/HeaderModificationListener.php @@ -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()); } } diff --git a/LimitProcessorInterface.php b/LimitProcessorInterface.php index 9819ca0..9d9f77f 100644 --- a/LimitProcessorInterface.php +++ b/LimitProcessorInterface.php @@ -18,4 +18,4 @@ public function getRateLimit(Request $request); * @return string */ public function getRateLimitAlias(Request $request); -} \ No newline at end of file +} diff --git a/Service/RateLimitInfo.php b/Service/RateLimitInfo.php index 49eb5b0..cecd6a0 100644 --- a/Service/RateLimitInfo.php +++ b/Service/RateLimitInfo.php @@ -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; + } } diff --git a/Service/RateLimitInfoManager.php b/Service/RateLimitInfoManager.php new file mode 100644 index 0000000..392cf25 --- /dev/null +++ b/Service/RateLimitInfoManager.php @@ -0,0 +1,40 @@ +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; + } +} diff --git a/Tests/EventListener/MockStorage.php b/Tests/EventListener/MockStorage.php index 56d8a92..a7402ca 100644 --- a/Tests/EventListener/MockStorage.php +++ b/Tests/EventListener/MockStorage.php @@ -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); } } diff --git a/Tests/Service/RateLimitInfoManagerTest.php b/Tests/Service/RateLimitInfoManagerTest.php new file mode 100644 index 0000000..2b0f64d --- /dev/null +++ b/Tests/Service/RateLimitInfoManagerTest.php @@ -0,0 +1,66 @@ +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()); + } +} diff --git a/Tests/Service/RateLimitInfoTest.php b/Tests/Service/RateLimitInfoTest.php index 7412913..02ec32e 100644 --- a/Tests/Service/RateLimitInfoTest.php +++ b/Tests/Service/RateLimitInfoTest.php @@ -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(); @@ -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()); + } } diff --git a/Tests/Util/AnnotationLimitProcessorGetRateLimitTest.php b/Tests/Util/AnnotationLimitProcessorGetRateLimitTest.php index f10abd8..a5df6a3 100644 --- a/Tests/Util/AnnotationLimitProcessorGetRateLimitTest.php +++ b/Tests/Util/AnnotationLimitProcessorGetRateLimitTest.php @@ -107,4 +107,4 @@ public function testFindNoAnnotations() $request->setMethod('GET'); $this->assertNull($annotationLimitProcess->getRateLimit($request)); } -} \ No newline at end of file +} diff --git a/Util/AnnotationLimitProcessor.php b/Util/AnnotationLimitProcessor.php index 0e6876c..1155356 100644 --- a/Util/AnnotationLimitProcessor.php +++ b/Util/AnnotationLimitProcessor.php @@ -70,4 +70,4 @@ public function getRateLimitAlias(Request $request) return 'other'; } -} \ No newline at end of file +} diff --git a/Util/PathLimitProcessor.php b/Util/PathLimitProcessor.php index 7a69639..e77545e 100644 --- a/Util/PathLimitProcessor.php +++ b/Util/PathLimitProcessor.php @@ -1,6 +1,5 @@ Date: Sat, 6 Apr 2019 13:17:45 +0300 Subject: [PATCH 03/10] Documentation update. * Highlight some existing features * Adding example of integration in Laravel --- README.md | 8 +- docs/laravel-middleware-example.md | 133 +++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 docs/laravel-middleware-example.md diff --git a/README.md b/README.md index 0933fc6..167733a 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/docs/laravel-middleware-example.md b/docs/laravel-middleware-example.md new file mode 100644 index 0000000..960c5b8 --- /dev/null +++ b/docs/laravel-middleware-example.md @@ -0,0 +1,133 @@ +Laravel comes with ``Illuminate\Routing\Middleware\ThrottleRequests`` middleware. +Those solution developed to be declared only once in application, applying it twice brings unexpected behavior in headers response. + +Example replace ``Illuminate\Routing\Middleware\ThrottleRequests`` with ``Noxlogic\RateLimitBundle`` based on configuration file rate limit rules, +repeating default ``Illuminate\Routing\Middleware\ThrottleRequests`` behaviour with blocking by client ip. + +``rate-limit.php`` +```php + [ + 'path' => 'api/', + 'methods' => ['*'], + 'limit' => 60, + 'period' => 60 + ] +]; +``` + +``RateLimit.php`` +```php +setStorage(new Redis($redisFactory->client())); + $this->rateLimitService = $rateLimitService; + $this->pathLimitProcessor = new PathLimitProcessor(config('rate-limit')); + } + + /** + * Handle an incoming request. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @return mixed + */ + public function handle($request, Closure $next) + { + $rateLimit = $this->pathLimitProcessor->getRateLimit($request); + + $key = trim(join('.', $rateLimit->getMethods()) . '.' . $this->pathLimitProcessor->getRateLimitAlias($request) . '.' . $request->getClientIp(), '.'); + + $rateLimitManager = new RateLimitInfoManager($this->rateLimitService); + $rateLimitInfo = $rateLimitManager->getRateLimitInfo($key, $rateLimit); + + // When we exceeded our limit, return a custom error response + if ($rateLimitInfo->getCalls() > $rateLimitInfo->getLimit()) { + throw new ThrottleRequestsException( + 'Too Many Attempts.', + null, + $this->getHeaders($rateLimitInfo) + ); + } + + $response = $next($request); + + return $this->addHeaders($response, $rateLimitInfo); + } + + /** + * Add the limit header information to the given response. + * + * @param Response $response + * @param RateLimitInfo $rateLimitInfo + * @return Response + */ + protected function addHeaders(Response $response, RateLimitInfo $rateLimitInfo) + { + $response->headers->add( + $this->getHeaders($rateLimitInfo) + ); + + return $response; + } + + /** + * Get the limit headers information. + * + * @param RateLimitInfo $rateLimitInfo + * @return array + */ + protected function getHeaders(RateLimitInfo $rateLimitInfo) + { + return [ + 'X-RateLimit-Limit' => $rateLimitInfo->getLimit(), + 'X-RateLimit-Remaining' => $rateLimitInfo->getRemainingAttempts(), + 'Retry-After' => $rateLimitInfo->getResetTimestamp() - time(), + 'X-RateLimit-Reset' => $rateLimitInfo->getResetTimestamp() + ]; + } +} +``` +``Kernel.php`` +```php + protected $middlewareGroups = [ + 'api' => [ + //'throttle:60,1', + 'rate-limit', + ], + ]; + + protected $routeMiddleware = [ + //'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, + 'rate-limit' => \Project\Middlewares\RateLimit::class, + ]; +``` \ No newline at end of file From c8ecf8c695ad4bbdde6b79a38264ff2be9204bd2 Mon Sep 17 00:00:00 2001 From: amanto Date: Tue, 16 Apr 2019 17:33:36 +0300 Subject: [PATCH 04/10] RateLimitService add getRateLimitInfo method from RateLimitInfoManager --- EventListener/RateLimitAnnotationListener.php | 8 +-- Service/RateLimitInfoManager.php | 40 ----------- Service/RateLimitService.php | 24 +++++++ Tests/Service/RateLimitInfoManagerTest.php | 66 ------------------- Tests/Service/RateLimitServiceTest.php | 55 ++++++++++++++-- 5 files changed, 79 insertions(+), 114 deletions(-) delete mode 100644 Service/RateLimitInfoManager.php delete mode 100644 Tests/Service/RateLimitInfoManagerTest.php diff --git a/EventListener/RateLimitAnnotationListener.php b/EventListener/RateLimitAnnotationListener.php index 119f748..66a9cfa 100644 --- a/EventListener/RateLimitAnnotationListener.php +++ b/EventListener/RateLimitAnnotationListener.php @@ -8,7 +8,6 @@ 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; @@ -37,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, @@ -85,8 +86,7 @@ public function onKernelController(FilterControllerEvent $event) $key = $this->getKey($limitProcessor, $rateLimit, $event->getRequest()); - $rateLimitManager = new RateLimitInfoManager($this->rateLimitService); - $rateLimitInfo = $rateLimitManager->getRateLimitInfo($key, $rateLimit); + $rateLimitInfo = $this->rateLimitService->getRateLimitInfo($key, $rateLimit); if (!$rateLimitInfo) { // @codeCoverageIgnoreStart return; diff --git a/Service/RateLimitInfoManager.php b/Service/RateLimitInfoManager.php deleted file mode 100644 index 392cf25..0000000 --- a/Service/RateLimitInfoManager.php +++ /dev/null @@ -1,40 +0,0 @@ -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; - } -} diff --git a/Service/RateLimitService.php b/Service/RateLimitService.php index 1dcf891..e7787e0 100644 --- a/Service/RateLimitService.php +++ b/Service/RateLimitService.php @@ -2,6 +2,7 @@ namespace Noxlogic\RateLimitBundle\Service; +use Noxlogic\RateLimitBundle\Annotation\RateLimit; use Noxlogic\RateLimitBundle\Service\Storage\StorageInterface; class RateLimitService @@ -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; + } } diff --git a/Tests/Service/RateLimitInfoManagerTest.php b/Tests/Service/RateLimitInfoManagerTest.php deleted file mode 100644 index 2b0f64d..0000000 --- a/Tests/Service/RateLimitInfoManagerTest.php +++ /dev/null @@ -1,66 +0,0 @@ -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()); - } -} diff --git a/Tests/Service/RateLimitServiceTest.php b/Tests/Service/RateLimitServiceTest.php index 39c109b..3ecd0a4 100644 --- a/Tests/Service/RateLimitServiceTest.php +++ b/Tests/Service/RateLimitServiceTest.php @@ -2,12 +2,10 @@ namespace Noxlogic\RateLimitBundle\Tests\Annotation; -use Noxlogic\RateLimitBundle\EventListener\OauthKeyGenerateListener; -use Noxlogic\RateLimitBundle\Events\GenerateKeyEvent; -use Noxlogic\RateLimitBundle\Service\RateLimitInfo; +use Noxlogic\RateLimitBundle\Annotation\RateLimit; use Noxlogic\RateLimitBundle\Service\RateLimitService; +use Noxlogic\RateLimitBundle\Tests\EventListener\MockStorage; use Noxlogic\RateLimitBundle\Tests\TestCase; -use Symfony\Component\HttpFoundation\Request; class RateLimitServiceTest extends TestCase { @@ -70,4 +68,53 @@ public function testResetRate() $service->setStorage($mockStorage); $service->resetRate('testkey'); } + + public function testNoRateLimitInStorage() + { + $rateLimitService = new RateLimitService(); + $rateLimitService->setStorage(new MockStorage()); + + $rateLimit = new RateLimit(array('methods' => 'POST', 'limit' => 1234, 'period' => 1000)); + + $rateLimitInfo = $rateLimitService->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); + + $rateLimit = new RateLimit(array('methods' => 'POST', 'limit' => 1234, 'period' => 1000)); + + $rateLimitInfo = $rateLimitService->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); + + $rateLimit = new RateLimit(array('methods' => 'POST', 'limit' => 1234, 'period' => 1000)); + + $rateLimitInfo = $rateLimitService->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()); + } } From aeb32ace2b0bb1c3b00797cc4585313001e237ef Mon Sep 17 00:00:00 2001 From: amanto Date: Tue, 16 Apr 2019 17:34:38 +0300 Subject: [PATCH 05/10] Update docs. RateLimitService add getRateLimitInfo method from RateLimitInfoManager --- docs/laravel-middleware-example.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/laravel-middleware-example.md b/docs/laravel-middleware-example.md index 960c5b8..e6ff580 100644 --- a/docs/laravel-middleware-example.md +++ b/docs/laravel-middleware-example.md @@ -28,7 +28,6 @@ use Closure; use Illuminate\Http\Exceptions\ThrottleRequestsException; use Illuminate\Redis\RedisManager; use Noxlogic\RateLimitBundle\Service\RateLimitInfo; -use Noxlogic\RateLimitBundle\Service\RateLimitInfoManager; use Noxlogic\RateLimitBundle\Service\RateLimitService; use Noxlogic\RateLimitBundle\Service\Storage\Redis; use Symfony\Component\HttpFoundation\Response; @@ -67,8 +66,7 @@ class RateLimit $key = trim(join('.', $rateLimit->getMethods()) . '.' . $this->pathLimitProcessor->getRateLimitAlias($request) . '.' . $request->getClientIp(), '.'); - $rateLimitManager = new RateLimitInfoManager($this->rateLimitService); - $rateLimitInfo = $rateLimitManager->getRateLimitInfo($key, $rateLimit); + $rateLimitInfo = $this->rateLimitService->getRateLimitInfo($key, $rateLimit); // When we exceeded our limit, return a custom error response if ($rateLimitInfo->getCalls() > $rateLimitInfo->getLimit()) { @@ -130,4 +128,4 @@ class RateLimit //'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'rate-limit' => \Project\Middlewares\RateLimit::class, ]; -``` \ No newline at end of file +``` From 649c724d3b98c3f7f8ca5b807e8811ecf1fa7a9d Mon Sep 17 00:00:00 2001 From: amanto Date: Tue, 16 Apr 2019 17:39:58 +0300 Subject: [PATCH 06/10] RateLimitInfo add isExceeded method. --- EventListener/RateLimitAnnotationListener.php | 2 +- Service/RateLimitInfo.php | 8 +++++++ Tests/Service/RateLimitInfoTest.php | 21 +++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/EventListener/RateLimitAnnotationListener.php b/EventListener/RateLimitAnnotationListener.php index 66a9cfa..4fef76f 100644 --- a/EventListener/RateLimitAnnotationListener.php +++ b/EventListener/RateLimitAnnotationListener.php @@ -98,7 +98,7 @@ public function onKernelController(FilterControllerEvent $event) $request->attributes->set('rate_limit_info', $rateLimitInfo); // 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')) { diff --git a/Service/RateLimitInfo.php b/Service/RateLimitInfo.php index cecd6a0..290687e 100644 --- a/Service/RateLimitInfo.php +++ b/Service/RateLimitInfo.php @@ -68,4 +68,12 @@ public function getRemainingAttempts() return $remaining; } + + /** + * @return bool + */ + public function isExceeded() + { + return $this->getCalls() > $this->getLimit(); + } } diff --git a/Tests/Service/RateLimitInfoTest.php b/Tests/Service/RateLimitInfoTest.php index 02ec32e..41c3dc6 100644 --- a/Tests/Service/RateLimitInfoTest.php +++ b/Tests/Service/RateLimitInfoTest.php @@ -37,4 +37,25 @@ public function testRemainingAttempts() $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()); + } } From a891fc8a0541c4084050c3e68ba828dfb315d345 Mon Sep 17 00:00:00 2001 From: amanto Date: Tue, 16 Apr 2019 17:40:15 +0300 Subject: [PATCH 07/10] Docs. RateLimitService add getRateLimitInfo method from RateLimitInfoManager --- docs/laravel-middleware-example.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/laravel-middleware-example.md b/docs/laravel-middleware-example.md index e6ff580..92799fb 100644 --- a/docs/laravel-middleware-example.md +++ b/docs/laravel-middleware-example.md @@ -69,7 +69,7 @@ class RateLimit $rateLimitInfo = $this->rateLimitService->getRateLimitInfo($key, $rateLimit); // When we exceeded our limit, return a custom error response - if ($rateLimitInfo->getCalls() > $rateLimitInfo->getLimit()) { + if ($rateLimitInfo->isExceeded()) { throw new ThrottleRequestsException( 'Too Many Attempts.', null, From 1fbdd819ec3f3bedf268ac08c802494b8bca404b Mon Sep 17 00:00:00 2001 From: amanto Date: Fri, 6 Sep 2019 14:36:37 +0300 Subject: [PATCH 08/10] RateLimitAnnotationListener - do not use deprecated findBestMethodMatch --- EventListener/RateLimitAnnotationListener.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/EventListener/RateLimitAnnotationListener.php b/EventListener/RateLimitAnnotationListener.php index 4fef76f..95db149 100644 --- a/EventListener/RateLimitAnnotationListener.php +++ b/EventListener/RateLimitAnnotationListener.php @@ -68,12 +68,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); $this->eventDispatcher->dispatch(RateLimitEvents::CHECKED_RATE_LIMIT, $checkedRateLimitEvent); From 3d8798f6b1ffb738534de8a22b4c29ea7ee171c2 Mon Sep 17 00:00:00 2001 From: amanto Date: Fri, 6 Sep 2019 14:36:59 +0300 Subject: [PATCH 09/10] LimitProcessorInterface - fix phpdoc --- LimitProcessorInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LimitProcessorInterface.php b/LimitProcessorInterface.php index 9d9f77f..469a045 100644 --- a/LimitProcessorInterface.php +++ b/LimitProcessorInterface.php @@ -9,7 +9,7 @@ interface LimitProcessorInterface { /** * @param Request $request - * @return mixed|RateLimit|null + * @return RateLimit|null */ public function getRateLimit(Request $request); From da4910e3051c35259d422b784aa6c6d0cf76f0fb Mon Sep 17 00:00:00 2001 From: amanto Date: Fri, 6 Sep 2019 15:06:25 +0300 Subject: [PATCH 10/10] RateLimitAnnotationListener - remove line to trigger travis build --- EventListener/RateLimitAnnotationListener.php | 1 - 1 file changed, 1 deletion(-) diff --git a/EventListener/RateLimitAnnotationListener.php b/EventListener/RateLimitAnnotationListener.php index 95db149..76558dc 100644 --- a/EventListener/RateLimitAnnotationListener.php +++ b/EventListener/RateLimitAnnotationListener.php @@ -126,7 +126,6 @@ public function onKernelController(FilterControllerEvent $event) } - /** * @param Request $request * @param RateLimit[] $annotations