├── Resources
├── doc
│ └── index.rst
├── views
│ └── Default
│ │ └── index.html.twig
├── translations
│ └── messages.fr.xlf
├── meta
│ └── LICENSE
└── config
│ └── services.xml
├── .gitignore
├── Tests
├── EventListener
│ ├── MockController.php
│ ├── MockListener.php
│ ├── MockControllerWithAttributes.php
│ ├── BaseListenerTest.php
│ ├── MockStorage.php
│ ├── OauthKeyGenerateListenerTest.php
│ ├── HeaderModificationListenerTest.php
│ └── RateLimitAnnotationListenerTest.php
├── Exception
│ └── TestException.php
├── Events
│ ├── RateLimitEventsTest.php
│ ├── CheckedRateLimitEventsTest.php
│ └── GenerateKeyEventsTest.php
├── WebTestCase.php
├── Service
│ ├── Storage
│ │ ├── PhpRedisClusterTest.php
│ │ ├── SimpleCacheTest.php
│ │ ├── DoctrineCacheTest.php
│ │ ├── MemcacheTest.php
│ │ ├── PhpRedisTest.php
│ │ ├── RedisTest.php
│ │ └── PsrCacheTest.php
│ ├── RateLimitInfoTest.php
│ └── RateLimitServiceTest.php
├── NoxlogicRateLimitBundleTest.php
├── TestCase.php
├── bootstrap.php
├── Attribute
│ └── RateLimitTest.php
├── DependencyInjection
│ ├── NoxlogicRateLimitExtensionTest.php
│ └── ConfigurationTest.php
└── Util
│ └── PathLimitProcessorTest.php
├── NoxlogicRateLimitBundle.php
├── UPGRADE-2.0.md
├── Events
├── RateLimitEvents.php
├── CheckedRateLimitEvent.php
└── GenerateKeyEvent.php
├── Exception
└── RateLimitExceptionInterface.php
├── Service
├── Storage
│ ├── PhpRedisCluster.php
│ ├── StorageInterface.php
│ ├── SimpleCache.php
│ ├── DoctrineCache.php
│ ├── Memcache.php
│ ├── PsrCache.php
│ ├── PhpRedis.php
│ └── Redis.php
├── RateLimitInfo.php
└── RateLimitService.php
├── .scrutinizer.yml
├── phpstan.dist.neon
├── EventListener
├── BaseListener.php
├── OauthKeyGenerateListener.php
├── HeaderModificationListener.php
└── RateLimitAnnotationListener.php
├── phpunit.xml.dist
├── LICENSE
├── composer.json
├── Attribute
└── RateLimit.php
├── .github
└── workflows
│ └── ci.yml
├── Util
└── PathLimitProcessor.php
├── CHANGELOG.md
├── DependencyInjection
├── NoxlogicRateLimitExtension.php
└── Configuration.php
├── README.md
└── phpstan-baseline.neon
/Resources/doc/index.rst:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Resources/views/Default/index.html.twig:
--------------------------------------------------------------------------------
1 | Hello {{ name }}!
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | clover.xml
2 | html/
3 | vendor/*
4 | composer.lock
5 | /.idea
6 | /.phpunit.result.cache
--------------------------------------------------------------------------------
/Tests/EventListener/MockController.php:
--------------------------------------------------------------------------------
1 | client = $client;
13 | }
14 |
15 | }
--------------------------------------------------------------------------------
/.scrutinizer.yml:
--------------------------------------------------------------------------------
1 | inherit: true
2 |
3 | tools:
4 | external_code_coverage: true
5 | sensiolabs_security_checker:
6 | enabled: true
7 |
8 | filter:
9 | excluded_paths:
10 | - 'vendor/*'
11 | - 'tests/*'
12 | - 'app/*'
13 | - 'bin/*'
14 | - 'library/*'
15 | - 'Tests/*'
16 |
--------------------------------------------------------------------------------
/Tests/EventListener/MockControllerWithAttributes.php:
--------------------------------------------------------------------------------
1 | payload = $payload;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Resources/translations/messages.fr.xlf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Symfony2 is great
7 | J'aime Symfony2
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/Tests/Events/RateLimitEventsTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('ratelimit.generate.key', RateLimitEvents::GENERATE_KEY);
15 | $this->assertEquals('ratelimit.checked.ratelimit', RateLimitEvents::CHECKED_RATE_LIMIT);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Tests/WebTestCase.php:
--------------------------------------------------------------------------------
1 | markTestSkipped('Php Redis client not installed');
14 | }
15 | }
16 |
17 | protected function getRedisMock() {
18 | return $this->getMockBuilder('\RedisCluster')->disableOriginalConstructor();
19 | }
20 |
21 | protected function getStorage($client) {
22 | return new PhpRedisCluster($client);
23 | }
24 | }
--------------------------------------------------------------------------------
/Tests/Service/RateLimitInfoTest.php:
--------------------------------------------------------------------------------
1 | setLimit(1234);
16 | $this->assertEquals(1234, $rateInfo->getLimit());
17 |
18 | $rateInfo->setCalls(5);
19 | $this->assertEquals(5, $rateInfo->getCalls());
20 |
21 | $rateInfo->setResetTimestamp(100000);
22 | $this->assertEquals(100000, $rateInfo->getResetTimestamp());
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/EventListener/BaseListener.php:
--------------------------------------------------------------------------------
1 | parameters[$name] = $value;
20 | }
21 |
22 | /**
23 | * @param $name
24 | * @param mixed $default
25 | * @return mixed
26 | */
27 | public function getParameter($name, $default = null)
28 | {
29 | return isset($this->parameters[$name]) ? $this->parameters[$name] : $default;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Tests/NoxlogicRateLimitBundleTest.php:
--------------------------------------------------------------------------------
1 | getMock('\\Symfony\\Component\\DependencyInjection\\ContainerBuilder');
13 | // $container->expects($this->exactly(0))
14 | // ->method('addCompilerPass')
15 | // ->with($this->isInstanceOf('\\Symfony\\Component\\DependencyInjection\\Compiler\\CompilerPassInterface'));
16 | //
17 | $bundle = new NoxlogicRateLimitBundle();
18 | $this->assertInstanceOf('Noxlogic\\RateLimitBundle\\NoxlogicRateLimitBundle', $bundle);
19 | // $bundle->build($container);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Tests/TestCase.php:
--------------------------------------------------------------------------------
1 | setParameter('foo', 'bar');
14 | $this->assertEquals('bar', $base->getParameter('foo'));
15 |
16 | $base->setParameter('foo', 'baz');
17 | $this->assertEquals('baz', $base->getParameter('foo'));
18 | }
19 |
20 | public function testDefaultValues()
21 | {
22 | $base = new MockListener();
23 |
24 | $base->setParameter('foo', 'bar');
25 | $this->assertEquals('baz', $base->getParameter('doesnotexist', 'baz'));
26 |
27 | $this->assertEquals('bar', $base->getParameter('foo', 'baz'));
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Events/CheckedRateLimitEvent.php:
--------------------------------------------------------------------------------
1 | request = $request;
18 | $this->rateLimit = $rateLimit;
19 | }
20 |
21 | public function getRateLimit(): ?RateLimit
22 | {
23 | return $this->rateLimit;
24 | }
25 |
26 | public function setRateLimit(?RateLimit $rateLimit = null): void
27 | {
28 | $this->rateLimit = $rateLimit;
29 | }
30 |
31 | public function getRequest(): Request
32 | {
33 | return $this->request;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/EventListener/OauthKeyGenerateListener.php:
--------------------------------------------------------------------------------
1 | tokenStorage = $tokenStorage;
21 | }
22 |
23 | /**
24 | * @param GenerateKeyEvent $event
25 | */
26 | public function onGenerateKey(GenerateKeyEvent $event)
27 | {
28 | $token = $this->tokenStorage->getToken();
29 | if (! $token instanceof \FOS\OAuthServerBundle\Security\Authentication\Token\OAuthToken) {
30 | return;
31 | }
32 |
33 | $event->addToKey($token->getToken());
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
15 | Tests
16 |
17 |
18 |
19 |
20 |
21 | .
22 |
23 | Resources
24 | Tests
25 | vendor
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/Tests/bootstrap.php:
--------------------------------------------------------------------------------
1 | limit;
17 | }
18 |
19 | /**
20 | * @param mixed $limit
21 | */
22 | public function setLimit($limit)
23 | {
24 | $this->limit = $limit;
25 | }
26 |
27 | /**
28 | * @return mixed
29 | */
30 | public function getCalls()
31 | {
32 | return $this->calls;
33 | }
34 |
35 | /**
36 | * @param mixed $calls
37 | */
38 | public function setCalls($calls)
39 | {
40 | $this->calls = $calls;
41 | }
42 |
43 | /**
44 | * @return mixed
45 | */
46 | public function getResetTimestamp()
47 | {
48 | return $this->resetTimestamp;
49 | }
50 |
51 | /**
52 | * @param mixed $resetTimestamp
53 | */
54 | public function setResetTimestamp($resetTimestamp)
55 | {
56 | $this->resetTimestamp = $resetTimestamp;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Service/Storage/StorageInterface.php:
--------------------------------------------------------------------------------
1 | assertEquals(null, $event->getRateLimit());
19 | }
20 |
21 | public function testRequest(): void
22 | {
23 | $request = new Request();
24 | $event = new CheckedRateLimitEvent($request, null);
25 |
26 | $this->assertEquals($request, $event->getRequest());
27 | }
28 |
29 | public function testSetRateLimit(): void
30 | {
31 | $request = new Request();
32 | $rateLimit = new RateLimit();
33 |
34 | $event = new CheckedRateLimitEvent($request, $rateLimit);
35 |
36 | $this->assertEquals($rateLimit, $event->getRateLimit());
37 |
38 | $event->setRateLimit($rateLimit);
39 | $this->assertEquals($rateLimit, $event->getRateLimit());
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Service/RateLimitService.php:
--------------------------------------------------------------------------------
1 | storage = $storage;
20 | }
21 |
22 | /**
23 | * @return ?StorageInterface
24 | */
25 | public function getStorage()
26 | {
27 | if (! $this->storage) {
28 | throw new \RuntimeException('Storage engine must be set prior to using the rate limit service');
29 | }
30 |
31 | return $this->storage;
32 | }
33 |
34 | /**
35 | *
36 | */
37 | public function limitRate($key)
38 | {
39 | return $this->storage->limitRate($key);
40 | }
41 |
42 | /**
43 | *
44 | */
45 | public function createRate($key, $limit, $period)
46 | {
47 | return $this->storage->createRate($key, $limit, $period);
48 | }
49 |
50 | /**
51 | *
52 | */
53 | public function resetRate($key)
54 | {
55 | return $this->storage->resetRate($key);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Events/GenerateKeyEvent.php:
--------------------------------------------------------------------------------
1 | request = $request;
23 | $this->key = $key;
24 | $this->payload = $payload;
25 | }
26 |
27 | /**
28 | * @return string
29 | */
30 | public function getKey()
31 | {
32 | return $this->key;
33 | }
34 |
35 | /**
36 | * @param string $key
37 | */
38 | public function setKey($key)
39 | {
40 | $this->key = $key;
41 | }
42 |
43 | /**
44 | * @return Request
45 | */
46 | public function getRequest()
47 | {
48 | return $this->request;
49 | }
50 |
51 | /**
52 | * @param $part
53 | */
54 | public function addToKey($part)
55 | {
56 | if ($this->key) {
57 | $this->key .= '.'.$part;
58 | } else {
59 | $this->key = $part;
60 | }
61 | }
62 |
63 | /**
64 | * @return mixed
65 | */
66 | public function getPayload()
67 | {
68 | return $this->payload;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "noxlogic/ratelimit-bundle",
3 | "description": "This bundle provides functionality to limit calls to actions based on rate limits",
4 | "keywords": [ "x-rate-limit", "api", "rest" ],
5 | "type": "symfony-bundle",
6 | "license": "MIT",
7 | "authors": [
8 | {
9 | "name": "Joshua Thijssen",
10 | "email": "jthijssen@noxlogic.nl"
11 | }
12 | ],
13 | "require": {
14 | "php": "^8.0",
15 | "symfony/framework-bundle": "^5.4.2|^6.4|^7.3"
16 | },
17 | "require-dev": {
18 | "symfony/phpunit-bridge": ">=5.4",
19 | "psr/simple-cache": "^1.0|^2.0",
20 | "doctrine/cache": "^1.5",
21 | "psr/cache": "^1.0|^2.0|^3.0",
22 | "predis/predis": "^1.1|^2.0",
23 | "friendsofsymfony/oauth-server-bundle": "^1.5|^2.0@dev",
24 | "phpstan/phpstan": "^2.1"
25 | },
26 | "suggest": {
27 | "snc/redis-bundle": "Use Redis as a storage engine.",
28 | "leaseweb/memcache-bundle": "Use Memcache as a storage engine.",
29 | "doctrine/doctrine-cache-bundle": "Use Doctrine Cache as a storage engine.",
30 | "friendsofsymfony/oauth-server-bundle": "Throttle using OAuth access tokens."
31 | },
32 | "autoload": {
33 | "psr-4": {
34 | "Noxlogic\\RateLimitBundle\\": ""
35 | }
36 | },
37 | "extra": {
38 | "branch-alias": {
39 | "dev-main": "2.x-dev"
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Tests/Attribute/RateLimitTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(-1, $attribute->limit);
15 | $this->assertEmpty($attribute->methods);
16 | $this->assertEquals(3600, $attribute->period);
17 | }
18 |
19 | public function testConstructionWithValues(): void
20 | {
21 | $attribute = new RateLimit(
22 | [],
23 | 1234,
24 | 1000
25 | );
26 | $this->assertEquals(1234, $attribute->limit);
27 | $this->assertEquals(1000, $attribute->period);
28 |
29 | $attribute = new RateLimit(
30 | ['POST'],
31 | 1234,
32 | 1000
33 | );
34 | $this->assertEquals(1234, $attribute->limit);
35 | $this->assertEquals(1000, $attribute->period);
36 | $this->assertEquals(['POST'], $attribute->methods);
37 | }
38 |
39 | public function testConstructionWithMethods(): void
40 | {
41 | $attribute = new RateLimit(
42 | ['POST', 'GET'],
43 | 1234,
44 | 1000
45 | );
46 | $this->assertCount(2, $attribute->methods);
47 | }
48 |
49 | public function testConstructWithStringAsMethods(): void
50 | {
51 | $attribute = new RateLimit(
52 | 'POST',
53 | 1234,
54 | 1000
55 | );
56 | $this->assertEquals(['POST'], $attribute->methods);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/EventListener/HeaderModificationListener.php:
--------------------------------------------------------------------------------
1 | parameters = $defaultParameters;
17 | }
18 |
19 | /**
20 | * @param ResponseEvent $event
21 | */
22 | public function onKernelResponse($event)
23 | {
24 | $request = $event->getRequest();
25 |
26 | // Check if we have a rate-limit-info object in our request attributes. If not, we didn't need to limit.
27 | $rateLimitInfo = $request->attributes->get('rate_limit_info', null);
28 | if (! $rateLimitInfo) {
29 | return;
30 | }
31 |
32 | // Check if we need to add our x-rate-limits to the headers
33 | if (! $this->getParameter('display_headers')) {
34 | return;
35 | }
36 |
37 | /** @var RateLimitInfo $rateLimitInfo */
38 |
39 | $remaining = $rateLimitInfo->getLimit() - $rateLimitInfo->getCalls();
40 | if ($remaining < 0) {
41 | $remaining = 0;
42 | }
43 |
44 | $response = $event->getResponse();
45 | $response->headers->set($this->getParameter('header_limit_name'), $rateLimitInfo->getLimit());
46 | $response->headers->set($this->getParameter('header_remaining_name'), (string) $remaining);
47 | $response->headers->set($this->getParameter('header_reset_name'), $rateLimitInfo->getResetTimestamp());
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Tests/Events/GenerateKeyEventsTest.php:
--------------------------------------------------------------------------------
1 | assertEquals("", $event->getKey());
18 | }
19 |
20 | public function testRequest()
21 | {
22 | $request = new Request();
23 | $event = new GenerateKeyEvent($request, "");
24 |
25 | $this->assertEquals($request, $event->getRequest());
26 | }
27 |
28 | public function testPayload()
29 | {
30 | $request = new Request();
31 | $event = new GenerateKeyEvent($request, "", 'bar');
32 |
33 | $this->assertSame('bar', $event->getPayload());
34 | }
35 |
36 | public function testAddKey()
37 | {
38 | $request = new Request();
39 | $event = new GenerateKeyEvent($request, "foo");
40 |
41 | $this->assertEquals("foo", $event->getKey());
42 |
43 | $event->addToKey("bar");
44 | $this->assertEquals("foo.bar", $event->getKey());
45 |
46 | $event->addToKey("baz");
47 | $this->assertEquals("foo.bar.baz", $event->getKey());
48 |
49 | $event->addToKey("");
50 | $this->assertEquals("foo.bar.baz.", $event->getKey());
51 | }
52 |
53 | public function testSetKey()
54 | {
55 | $request = new Request();
56 | $event = new GenerateKeyEvent($request, "foo");
57 |
58 | $this->assertEquals("foo", $event->getKey());
59 |
60 | $event->setKey("bar");
61 | $this->assertEquals("bar", $event->getKey());
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Service/Storage/SimpleCache.php:
--------------------------------------------------------------------------------
1 | client = $client;
18 | }
19 |
20 | public function getRateInfo($key)
21 | {
22 | $info = $this->client->get($key);
23 | if ($info === null || !array_key_exists('limit', $info)) {
24 | return false;
25 | }
26 |
27 | return $this->createRateInfo($info);
28 | }
29 |
30 | public function limitRate($key)
31 | {
32 | $info = $this->client->get($key);
33 | if ($info === null || !array_key_exists('limit', $info)) {
34 | return false;
35 | }
36 |
37 | $info['calls']++;
38 | $ttl = $info['reset'] - time();
39 |
40 | $this->client->set($key, $info, $ttl);
41 |
42 | return $this->createRateInfo($info);
43 | }
44 |
45 | public function createRate($key, $limit, $period)
46 | {
47 | $info = [
48 | 'limit' => $limit,
49 | 'calls' => 1,
50 | 'reset' => time() + $period,
51 | ];
52 | $this->client->set($key, $info, $period);
53 |
54 | return $this->createRateInfo($info);
55 | }
56 |
57 | public function resetRate($key)
58 | {
59 | $this->client->delete($key);
60 |
61 | return true;
62 | }
63 |
64 | private function createRateInfo(array $info)
65 | {
66 | $rateLimitInfo = new RateLimitInfo();
67 | $rateLimitInfo->setLimit($info['limit']);
68 | $rateLimitInfo->setCalls($info['calls']);
69 | $rateLimitInfo->setResetTimestamp($info['reset']);
70 |
71 | return $rateLimitInfo;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Service/Storage/DoctrineCache.php:
--------------------------------------------------------------------------------
1 | client = $client;
18 | }
19 |
20 | public function getRateInfo($key)
21 | {
22 | $info = $this->client->fetch($key);
23 | if ($info === false || !array_key_exists('limit', $info)) {
24 | return false;
25 | }
26 |
27 | return $this->createRateInfo($info);
28 | }
29 |
30 | public function limitRate($key)
31 | {
32 | $info = $this->client->fetch($key);
33 | if ($info === false || !array_key_exists('limit', $info)) {
34 | return false;
35 | }
36 |
37 | $info['calls']++;
38 |
39 | $expire = $info['reset'] - time();
40 |
41 | $this->client->save($key, $info, $expire);
42 |
43 | return $this->createRateInfo($info);
44 | }
45 |
46 | public function createRate($key, $limit, $period)
47 | {
48 | $info = array();
49 | $info['limit'] = $limit;
50 | $info['calls'] = 1;
51 | $info['reset'] = time() + $period;
52 |
53 | $this->client->save($key, $info, $period);
54 |
55 | return $this->createRateInfo($info);
56 | }
57 |
58 | public function resetRate($key)
59 | {
60 | $this->client->delete($key);
61 |
62 | return true;
63 | }
64 |
65 | private function createRateInfo(array $info)
66 | {
67 | $rateLimitInfo = new RateLimitInfo();
68 | $rateLimitInfo->setLimit($info['limit']);
69 | $rateLimitInfo->setCalls($info['calls']);
70 | $rateLimitInfo->setResetTimestamp($info['reset']);
71 |
72 | return $rateLimitInfo;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/Attribute/RateLimit.php:
--------------------------------------------------------------------------------
1 | methods = [$methods];
34 | } else {
35 | $this->methods = $methods;
36 | }
37 | }
38 |
39 | public function getLimit(): int
40 | {
41 | return $this->limit;
42 | }
43 |
44 | public function setLimit(int $limit): void
45 | {
46 | $this->limit = $limit;
47 | }
48 |
49 | public function getMethods(): array
50 | {
51 | return $this->methods;
52 | }
53 |
54 | public function setMethods($methods): void
55 | {
56 | $this->methods = (array) $methods;
57 | }
58 |
59 | public function getPeriod(): int
60 | {
61 | return $this->period;
62 | }
63 |
64 | public function setPeriod(int $period): void
65 | {
66 | $this->period = $period;
67 | }
68 |
69 | public function getPayload(): mixed
70 | {
71 | return $this->payload;
72 | }
73 |
74 | public function setPayload(mixed $payload): void
75 | {
76 | $this->payload = $payload;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/Tests/EventListener/MockStorage.php:
--------------------------------------------------------------------------------
1 | rates[$key];
21 |
22 | $rateLimitInfo = new RateLimitInfo();
23 | $rateLimitInfo->setCalls($info['calls']);
24 | $rateLimitInfo->setResetTimestamp($info['reset']);
25 | $rateLimitInfo->setLimit($info['limit']);
26 | return $rateLimitInfo;
27 | }
28 |
29 | /**
30 | * Limit the rate by one
31 | *
32 | * @param string $key
33 | * @return RateLimitInfo Rate limit info
34 | */
35 | public function limitRate($key)
36 | {
37 | if (! isset($this->rates[$key])) {
38 | return null;
39 | }
40 |
41 | $this->rates[$key]['calls']++;
42 | return $this->getRateInfo($key);
43 | }
44 |
45 | /**
46 | * Create a new rate entry
47 | *
48 | * @param string $key
49 | * @param integer $limit
50 | * @param integer $period
51 | * @return \Noxlogic\RateLimitBundle\Service\RateLimitInfo
52 | */
53 | public function createRate($key, $limit, $period)
54 | {
55 | $this->rates[$key] = array('calls' => 1, 'limit' => $limit, 'reset' => (time() + $period));
56 | return $this->getRateInfo($key);
57 | }
58 |
59 | /**
60 | * Reset the rating
61 | *
62 | * @param $key
63 | */
64 | public function resetRate($key)
65 | {
66 | unset($this->rates[$key]);
67 | }
68 |
69 | public function createMockRate($key, $limit, $period, $calls)
70 | {
71 | $this->rates[$key] = array('calls' => $calls, 'limit' => $limit, 'reset' => (time() + $period));
72 | return $this->getRateInfo($key);
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/Service/Storage/Memcache.php:
--------------------------------------------------------------------------------
1 | client = $client;
17 | }
18 |
19 | public function getRateInfo($key)
20 | {
21 | $info = $this->client->get($key);
22 |
23 | return $this->createRateInfo($info);
24 | }
25 |
26 | public function limitRate($key)
27 | {
28 | $cas = null;
29 | $i = 0;
30 | do {
31 | if (defined('Memcached::GET_EXTENDED')) {
32 | $_o = $this->client->get($key, null, \Memcached::GET_EXTENDED);
33 | $info = $_o['value'] ?? null;
34 | $cas = $_o['cas'] ?? null;
35 | } else {
36 | $info = $this->client->get($key, null, $cas);
37 | }
38 | if (!$info) {
39 | return false;
40 | }
41 |
42 | $info['calls']++;
43 | $this->client->cas($cas, $key, $info);
44 | } while ($this->client->getResultCode() == \Memcached::RES_DATA_EXISTS && $i++ < 5);
45 |
46 | return $this->createRateInfo($info);
47 | }
48 |
49 | public function createRate($key, $limit, $period)
50 | {
51 | $info = array();
52 | $info['limit'] = $limit;
53 | $info['calls'] = 1;
54 | $info['reset'] = time() + $period;
55 |
56 | $this->client->set($key, $info, $period);
57 |
58 | return $this->createRateInfo($info);
59 | }
60 |
61 | public function resetRate($key)
62 | {
63 | $this->client->delete($key);
64 | return true;
65 | }
66 |
67 | private function createRateInfo(array $info)
68 | {
69 | $rateLimitInfo = new RateLimitInfo();
70 | $rateLimitInfo->setLimit($info['limit']);
71 | $rateLimitInfo->setCalls($info['calls']);
72 | $rateLimitInfo->setResetTimestamp($info['reset']);
73 |
74 | return $rateLimitInfo;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/Service/Storage/PsrCache.php:
--------------------------------------------------------------------------------
1 | client = $client;
19 | }
20 |
21 | public function getRateInfo($key)
22 | {
23 | $item = $this->client->getItem($key);
24 | if (!$item->isHit()) {
25 | return false;
26 | }
27 |
28 | return $this->createRateInfo($item->get());
29 | }
30 |
31 | public function limitRate($key)
32 | {
33 | $item = $this->client->getItem($key);
34 | if (!$item->isHit()) {
35 | return false;
36 | }
37 |
38 | $info = $item->get();
39 |
40 | $info['calls']++;
41 | $item->set($info);
42 | $item->expiresAfter($info['reset'] - time());
43 |
44 | $this->client->save($item);
45 |
46 | return $this->createRateInfo($info);
47 | }
48 |
49 | public function createRate($key, $limit, $period)
50 | {
51 | $info = [
52 | 'limit' => $limit,
53 | 'calls' => 1,
54 | 'reset' => time() + $period,
55 | ];
56 | $item = $this->client->getItem($key);
57 | $item->set($info);
58 | $item->expiresAfter($period);
59 |
60 | $this->client->save($item);
61 |
62 | return $this->createRateInfo($info);
63 | }
64 |
65 | public function resetRate($key)
66 | {
67 | $this->client->deleteItem($key);
68 |
69 | return true;
70 | }
71 |
72 | private function createRateInfo(array $info)
73 | {
74 | $rateLimitInfo = new RateLimitInfo();
75 | $rateLimitInfo->setLimit($info['limit']);
76 | $rateLimitInfo->setCalls($info['calls']);
77 | $rateLimitInfo->setResetTimestamp($info['reset']);
78 |
79 | return $rateLimitInfo;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Service/Storage/PhpRedis.php:
--------------------------------------------------------------------------------
1 | client = $client;
19 | }
20 |
21 | public function getRateInfo($key)
22 | {
23 | $key = $this->sanitizeRedisKey($key);
24 |
25 | $info = $this->client->hgetall($key);
26 | if (!isset($info['limit']) || !isset($info['calls']) || !isset($info['reset'])) {
27 | return false;
28 | }
29 |
30 | $rateLimitInfo = new RateLimitInfo();
31 | $rateLimitInfo->setLimit($info['limit']);
32 | $rateLimitInfo->setCalls($info['calls']);
33 | $rateLimitInfo->setResetTimestamp($info['reset']);
34 |
35 | return $rateLimitInfo;
36 | }
37 |
38 | public function limitRate($key)
39 | {
40 | $key = $this->sanitizeRedisKey($key);
41 |
42 | $info = $this->getRateInfo($key);
43 | if (!$info) {
44 | return false;
45 | }
46 |
47 | $calls = $this->client->hincrby($key, 'calls', 1);
48 | $info->setCalls($calls);
49 |
50 | return $info;
51 | }
52 |
53 | public function createRate($key, $limit, $period)
54 | {
55 | $key = $this->sanitizeRedisKey($key);
56 |
57 | $reset = time() + $period;
58 |
59 | $this->client->hset($key, 'limit', $limit);
60 | $this->client->hset($key, 'calls', 1);
61 | $this->client->hset($key, 'reset', $reset);
62 | $this->client->expire($key, $period);
63 |
64 | $rateLimitInfo = new RateLimitInfo();
65 | $rateLimitInfo->setLimit($limit);
66 | $rateLimitInfo->setCalls(1);
67 | $rateLimitInfo->setResetTimestamp($reset);
68 |
69 | return $rateLimitInfo;
70 | }
71 |
72 | public function resetRate($key)
73 | {
74 | $key = $this->sanitizeRedisKey($key);
75 |
76 | $this->client->del($key);
77 |
78 | return true;
79 | }
80 |
81 | /**
82 | * Sanitizies key so it can be used safely in REDIS
83 | *
84 | * @param $key
85 | * @return string|string[]
86 | */
87 | protected function sanitizeRedisKey($key) {
88 | return str_replace(str_split('@{}()/\:'), '_', $key);
89 | }
90 |
91 | }
92 |
--------------------------------------------------------------------------------
/Service/Storage/Redis.php:
--------------------------------------------------------------------------------
1 | client = $client;
18 | }
19 |
20 | public function getRateInfo($key)
21 | {
22 | $key = $this->sanitizeRedisKey($key);
23 |
24 | $info = $this->client->hgetall($key);
25 | if (!isset($info['limit']) || !isset($info['calls']) || !isset($info['reset'])) {
26 | return false;
27 | }
28 |
29 | $rateLimitInfo = new RateLimitInfo();
30 | $rateLimitInfo->setLimit($info['limit']);
31 | $rateLimitInfo->setCalls($info['calls']);
32 | $rateLimitInfo->setResetTimestamp($info['reset']);
33 |
34 | return $rateLimitInfo;
35 | }
36 |
37 | public function limitRate($key)
38 | {
39 | $key = $this->sanitizeRedisKey($key);
40 |
41 | $info = $this->getRateInfo($key);
42 | if (!$info) {
43 | return false;
44 | }
45 |
46 | $calls = $this->client->hincrby($key, 'calls', 1);
47 | $info->setCalls($calls);
48 |
49 | return $info;
50 | }
51 |
52 | public function createRate($key, $limit, $period)
53 | {
54 | $key = $this->sanitizeRedisKey($key);
55 |
56 | $reset = time() + $period;
57 |
58 | $this->client->hset($key, 'limit', (string) $limit);
59 | $this->client->hset($key, 'calls', '1');
60 | $this->client->hset($key, 'reset', (string) $reset);
61 | $this->client->expire($key, $period);
62 |
63 | $rateLimitInfo = new RateLimitInfo();
64 | $rateLimitInfo->setLimit($limit);
65 | $rateLimitInfo->setCalls(1);
66 | $rateLimitInfo->setResetTimestamp($reset);
67 |
68 | return $rateLimitInfo;
69 | }
70 |
71 | public function resetRate($key)
72 | {
73 | $key = $this->sanitizeRedisKey($key);
74 |
75 | $this->client->del($key);
76 |
77 | return true;
78 | }
79 |
80 | /**
81 | * Sanitizies key so it can be used safely in REDIS
82 | *
83 | * @param $key
84 | * @return string|string[]
85 | */
86 | protected function sanitizeRedisKey($key) {
87 | return str_replace(str_split('@{}()/\:'), '_', $key);
88 | }
89 |
90 | }
91 |
--------------------------------------------------------------------------------
/Tests/Service/RateLimitServiceTest.php:
--------------------------------------------------------------------------------
1 | getMockBuilder('Noxlogic\\RateLimitBundle\\Service\\Storage\\StorageInterface')->getMock();
18 |
19 | $service = new RateLimitService();
20 | $service->setStorage($mockStorage);
21 |
22 | $this->assertEquals($mockStorage, $service->getStorage());
23 | }
24 |
25 | public function testRuntimeExceptionWhenNoStorageIsSet()
26 | {
27 | $this->expectException(\RuntimeException::class);
28 | $service = new RateLimitService();
29 | $service->getStorage();
30 | }
31 |
32 |
33 | public function testLimitRate()
34 | {
35 | $mockStorage = $this->getMockBuilder('Noxlogic\\RateLimitBundle\\Service\\Storage\\StorageInterface')->getMock();
36 | $mockStorage
37 | ->expects($this->once())
38 | ->method('limitRate')
39 | ->with('testkey');
40 |
41 | $service = new RateLimitService();
42 | $service->setStorage($mockStorage);
43 | $service->limitRate('testkey');
44 | }
45 |
46 | public function testcreateRate()
47 | {
48 | $mockStorage = $this->getMockBuilder('Noxlogic\\RateLimitBundle\\Service\\Storage\\StorageInterface')->getMock();
49 | $mockStorage
50 | ->expects($this->once())
51 | ->method('createRate')
52 | ->with('testkey', 10, 100);
53 |
54 | $service = new RateLimitService();
55 | $service->setStorage($mockStorage);
56 | $service->createRate('testkey', 10, 100);
57 | }
58 |
59 | public function testResetRate()
60 | {
61 | $mockStorage = $this->getMockBuilder('Noxlogic\\RateLimitBundle\\Service\\Storage\\StorageInterface')->getMock();
62 | $mockStorage
63 | ->expects($this->once())
64 | ->method('resetRate')
65 | ->with('testkey');
66 |
67 | $service = new RateLimitService();
68 | $service->setStorage($mockStorage);
69 | $service->resetRate('testkey');
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Tests/EventListener/OauthKeyGenerateListenerTest.php:
--------------------------------------------------------------------------------
1 | mockContext = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface')->getMock();
17 | } else {
18 | $this->mockContext = $this->getMockBuilder('Symfony\Component\Security\Core\SecurityContextInterface')->getMock();
19 | }
20 |
21 | if (! class_exists('FOS\\OAuthServerBundle\\Security\\Authentication\\Token\\OAuthToken')) {
22 | $this->markTestSkipped("OAuth not found");
23 | }
24 | }
25 |
26 | public function testListener()
27 | {
28 | $mockToken = $this->createMockToken();
29 |
30 | $mockContext = $this->mockContext;
31 | $mockContext
32 | ->expects($this->any())
33 | ->method('getToken')
34 | ->will($this->returnValue($mockToken));
35 |
36 | $event = new GenerateKeyEvent(new Request(), 'foo');
37 |
38 | $listener = new OauthKeyGenerateListener($mockContext);
39 | $listener->onGenerateKey($event);
40 |
41 | $this->assertEquals('foo.mocktoken', $event->getKey());
42 | }
43 |
44 | public function testListenerWithoutOAuthToken()
45 | {
46 | $mockContext = $this->mockContext;
47 | $mockContext
48 | ->expects($this->any())
49 | ->method('getToken')
50 | ->will($this->returnValue(new \StdClass()));
51 |
52 | $event = new GenerateKeyEvent(new Request(), 'foo');
53 |
54 | $listener = new OauthKeyGenerateListener($mockContext);
55 | $listener->onGenerateKey($event);
56 |
57 | $this->assertEquals('foo', $event->getKey());
58 | }
59 |
60 | private function createMockToken()
61 | {
62 | $oauthToken = $this->getMockBuilder('FOS\\OAuthServerBundle\\Security\\Authentication\\Token\\OAuthToken')->getMock();
63 | $oauthToken
64 | ->expects($this->any())
65 | ->method('getToken')
66 | ->will($this->returnValue('mocktoken'))
67 | ;
68 |
69 | return $oauthToken;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI Tests
2 |
3 | on:
4 | push:
5 | pull_request:
6 |
7 | jobs:
8 | run-tests:
9 | runs-on: ubuntu-24.04
10 | strategy:
11 | fail-fast: false
12 | matrix:
13 | php: [ "8.0", "8.1", "8.2", "8.3", "8.4" ]
14 | composer_flags: [ "", "--prefer-lowest" ]
15 | symfony_version: [ "^5.4", "^6.4", "^7.3" ]
16 | exclude:
17 | # Symfony 6 requires PHP 8.1+
18 | - php: "8.0"
19 | symfony_version: "^6.4"
20 | # Symfony 7 requires PHP 8.2+
21 | - php: "8.0"
22 | symfony_version: "^7.3"
23 | - php: "8.1"
24 | symfony_version: "^7.3"
25 |
26 | name: PHP ${{ matrix.php }} SF ${{ matrix.symfony_version }} ${{ matrix.composer_flags}}
27 | env:
28 | PHP: ${{ matrix.os }}
29 | COMPOSER_MEMORY_LIMIT: -1
30 | COMPOSER_FLAGS: ${{ matrix.composer_flags }}
31 | SYMFONY_VERSION: ${{ matrix.symfony_version }}
32 | PHP_VERSION: ${{ matrix.php }}
33 | steps:
34 | - name: Setup PHP
35 | uses: shivammathur/setup-php@v2
36 | with:
37 | php-version: ${{ matrix.php }}
38 | extensions: redis
39 | ini-values: memory_limit=256M,post_max_size=256M
40 | - name: Checkout ratelimit bundle
41 | uses: actions/checkout@v2
42 | with:
43 | fetch-depth: 2
44 | - name: Install dependencies
45 | run: |
46 | composer self-update
47 | if [ "$SYMFONY_VERSION" != "" ]; then composer require "symfony/symfony:${SYMFONY_VERSION}" --no-update; fi;
48 | if [ "$SYMFONY_VERSION" != "^5.4" ]; then composer remove --dev "friendsofsymfony/oauth-server-bundle" --no-update; fi;
49 | COMPOSER_MEMORY_LIMIT=-1 composer update --prefer-dist --no-interaction $COMPOSER_FLAGS
50 | - name: Static analysis
51 | run: |
52 | ./vendor/bin/phpstan --memory-limit=-1
53 | - name: Run tests
54 | run: |
55 | SYMFONY_DEPRECATIONS_HELPER=weak vendor/bin/simple-phpunit --coverage-text --coverage-clover=coverage.clover
56 | - name: Upload coverage
57 | if: ${{ matrix.php == '8.2' && github.repository == 'jaytaph/RateLimitBundle' }}
58 | uses: sudo-bot/action-scrutinizer@latest
59 | with:
60 | cli-args: "--format=php-clover coverage.clover"
61 |
--------------------------------------------------------------------------------
/Tests/Service/Storage/SimpleCacheTest.php:
--------------------------------------------------------------------------------
1 | getMockBuilder('Psr\\SimpleCache\\CacheInterface')
13 | ->getMock();
14 | $client->expects($this->once())
15 | ->method('get')
16 | ->with('foo')
17 | ->will($this->returnValue(array('limit' => 100, 'calls' => 50, 'reset' => 1234)));
18 |
19 | $storage = new SimpleCache($client);
20 | $rli = $storage->getRateInfo('foo');
21 | $this->assertInstanceOf('Noxlogic\\RateLimitBundle\\Service\\RateLimitInfo', $rli);
22 | $this->assertEquals(100, $rli->getLimit());
23 | $this->assertEquals(50, $rli->getCalls());
24 | $this->assertEquals(1234, $rli->getResetTimestamp());
25 | }
26 |
27 | public function testCreateRate()
28 | {
29 | $client = $this->getMockBuilder('Psr\\SimpleCache\\CacheInterface')
30 | ->getMock();
31 | $client->expects($this->once())
32 | ->method('set');
33 |
34 | $storage = new SimpleCache($client);
35 | $storage->createRate('foo', 100, 123);
36 | }
37 |
38 |
39 | public function testLimitRateNoKey()
40 | {
41 | $client = $this->getMockBuilder('Psr\\SimpleCache\\CacheInterface')
42 | ->getMock();
43 | $client->expects($this->once())
44 | ->method('get')
45 | ->with('foo')
46 | ->will($this->returnValue(null));
47 |
48 | $storage = new SimpleCache($client);
49 | $this->assertFalse($storage->limitRate('foo'));
50 | }
51 |
52 | public function testLimitRateWithKey()
53 | {
54 | $client = $this->getMockBuilder('Psr\\SimpleCache\\CacheInterface')
55 | ->getMock();
56 |
57 | $info['limit'] = 100;
58 | $info['calls'] = 50;
59 | $info['reset'] = 1234;
60 |
61 | $client->expects($this->exactly(1))
62 | ->method('get')
63 | ->with('foo')
64 | ->will($this->returnValue($info));
65 | $client->expects($this->once())
66 | ->method('set');
67 |
68 | $storage = new SimpleCache($client);
69 | $storage->limitRate('foo');
70 | }
71 |
72 | public function testResetRate()
73 | {
74 | $client = $this->getMockBuilder('Psr\\SimpleCache\\CacheInterface')
75 | ->getMock();
76 | $client->expects($this->once())
77 | ->method('delete')
78 | ->with('foo');
79 |
80 | $storage = new SimpleCache($client);
81 | $this->assertTrue($storage->resetRate('foo'));
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/Tests/Service/Storage/DoctrineCacheTest.php:
--------------------------------------------------------------------------------
1 | getMockBuilder('Doctrine\\Common\\Cache\\Cache')
14 | ->getMock();
15 | $client->expects($this->once())
16 | ->method('fetch')
17 | ->with('foo')
18 | ->will($this->returnValue(array('limit' => 100, 'calls' => 50, 'reset' => 1234)));
19 |
20 | $storage = new DoctrineCache($client);
21 | $rli = $storage->getRateInfo('foo');
22 | $this->assertInstanceOf('Noxlogic\\RateLimitBundle\\Service\\RateLimitInfo', $rli);
23 | $this->assertEquals(100, $rli->getLimit());
24 | $this->assertEquals(50, $rli->getCalls());
25 | $this->assertEquals(1234, $rli->getResetTimestamp());
26 | }
27 |
28 | public function testCreateRate()
29 | {
30 | $client = $this->getMockBuilder('Doctrine\\Common\\Cache\\Cache')
31 | ->getMock();
32 | $client->expects($this->once())
33 | ->method('save');
34 |
35 | $storage = new DoctrineCache($client);
36 | $storage->createRate('foo', 100, 123);
37 | }
38 |
39 |
40 | public function testLimitRateNoKey()
41 | {
42 | $client = $this->getMockBuilder('Doctrine\\Common\\Cache\\Cache')
43 | ->getMock();
44 | $client->expects($this->once())
45 | ->method('fetch')
46 | ->with('foo')
47 | ->will($this->returnValue(false));
48 |
49 | $storage = new DoctrineCache($client);
50 | $this->assertFalse($storage->limitRate('foo'));
51 | }
52 |
53 | public function testLimitRateWithKey()
54 | {
55 | $client = $this->getMockBuilder('Doctrine\\Common\\Cache\\Cache')
56 | ->getMock();
57 |
58 | $info['limit'] = 100;
59 | $info['calls'] = 50;
60 | $info['reset'] = 1234;
61 |
62 | $client->expects($this->exactly(1))
63 | ->method('fetch')
64 | ->with('foo')
65 | ->will($this->returnValue($info));
66 | $client->expects($this->once())
67 | ->method('save');
68 |
69 | $storage = new DoctrineCache($client);
70 | $storage->limitRate('foo');
71 | }
72 |
73 |
74 |
75 | public function testResetRate()
76 | {
77 | $client = $this->getMockBuilder('Doctrine\\Common\\Cache\\Cache')
78 | ->getMock();
79 | $client->expects($this->once())
80 | ->method('delete')
81 | ->with('foo');
82 |
83 | $storage = new DoctrineCache($client);
84 | $this->assertTrue($storage->resetRate('foo'));
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/Util/PathLimitProcessor.php:
--------------------------------------------------------------------------------
1 | , limit: int<-1, max>, period: positive-int}> $pathLimits
13 | */
14 | public function __construct(private array $pathLimits)
15 | {
16 | // Clean up any extra slashes from the config
17 | foreach ($this->pathLimits as &$pathLimit) {
18 | $pathLimit['path'] = trim($pathLimit['path'], '/');
19 | }
20 |
21 | // Order the configs so that the most specific paths
22 | // are matched first
23 | usort($this->pathLimits, static function($a, $b): int {
24 | return substr_count($b['path'], '/') - substr_count($a['path'], '/');
25 | });
26 | }
27 |
28 | public function getRateLimit(Request $request): ?RateLimit
29 | {
30 | $path = trim(urldecode($request->getPathInfo()), '/');
31 | $method = $request->getMethod();
32 |
33 | foreach ($this->pathLimits as $pathLimit) {
34 | if ($this->requestMatched($pathLimit, $path, $method)) {
35 | return new RateLimit(
36 | $pathLimit['methods'],
37 | $pathLimit['limit'],
38 | $pathLimit['period']
39 | );
40 | }
41 | }
42 |
43 | return null;
44 | }
45 |
46 | public function getMatchedPath(Request $request): string
47 | {
48 | $path = trim($request->getPathInfo(), '/');
49 | $method = $request->getMethod();
50 |
51 | foreach ($this->pathLimits as $pathLimit) {
52 | if ($this->requestMatched($pathLimit, $path, $method)) {
53 | return $pathLimit['path'];
54 | }
55 | }
56 |
57 | return '';
58 | }
59 |
60 | private function requestMatched($pathLimit, $path, $method): bool
61 | {
62 | return $this->methodMatched($pathLimit['methods'], $method)
63 | && $this->pathMatched($pathLimit['path'], $path);
64 | }
65 |
66 | private function methodMatched(array $expectedMethods, $method): bool
67 | {
68 | foreach ($expectedMethods as $expectedMethod) {
69 | if ($expectedMethod === '*' || $expectedMethod === $method) {
70 | return true;
71 | }
72 | }
73 |
74 | return false;
75 | }
76 |
77 | private function pathMatched($expectedPath, $path): bool
78 | {
79 | if ($expectedPath === '*') {
80 | return true;
81 | }
82 |
83 | $expectedParts = explode('/', $expectedPath);
84 | $actualParts = explode('/', $path);
85 |
86 | if (count($actualParts) < count($expectedParts)) {
87 | return false;
88 | }
89 |
90 | foreach ($expectedParts as $key => $value) {
91 | if ($value !== $actualParts[$key]) {
92 | return false;
93 | }
94 | }
95 |
96 | return true;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Tests/Service/Storage/MemcacheTest.php:
--------------------------------------------------------------------------------
1 | markTestSkipped('MemCached extension not installed');
14 | }
15 | }
16 |
17 | public function testGetRateInfo()
18 | {
19 | $client = @$this->getMockBuilder('\\Memcached')
20 | ->setMethods(array('get'))
21 | ->getMock();
22 | $client->expects($this->once())
23 | ->method('get')
24 | ->with('foo')
25 | ->will($this->returnValue(array('limit' => 100, 'calls' => 50, 'reset' => 1234)));
26 |
27 | $storage = new Memcache($client);
28 | $rli = $storage->getRateInfo('foo');
29 | $this->assertInstanceOf('Noxlogic\\RateLimitBundle\\Service\\RateLimitInfo', $rli);
30 | $this->assertEquals(100, $rli->getLimit());
31 | $this->assertEquals(50, $rli->getCalls());
32 | $this->assertEquals(1234, $rli->getResetTimestamp());
33 | }
34 |
35 | public function testCreateRate()
36 | {
37 | $client = @$this->getMockBuilder('\\Memcached')
38 | ->setMethods(array('set', 'get'))
39 | ->getMock();
40 | $client->expects($this->exactly(1))
41 | ->method('set');
42 |
43 | $storage = new Memcache($client);
44 | $storage->createRate('foo', 100, 123);
45 | }
46 |
47 |
48 | public function testLimitRateNoKey()
49 | {
50 | $client = @$this->getMockBuilder('\\Memcached')
51 | ->setMethods(array('get','getResultCode'))
52 | ->getMock();
53 | $client->expects($this->any())
54 | ->method('getResultCode')
55 | ->willReturn(\Memcached::RES_SUCCESS);
56 | $client->expects($this->atLeastOnce())
57 | ->method('get')
58 | ->with('foo')
59 | ->will($this->returnValue(array('limit' => 100, 'calls' => 1, 'reset' => 1234)));
60 |
61 | $storage = new Memcache($client);
62 | $storage->limitRate('foo');
63 | }
64 |
65 | public function testLimitRateWithKey()
66 | {
67 | $client = @$this->getMockBuilder('\\Memcached')
68 | ->setMethods(array('get','cas','getResultCode'))
69 | ->getMock();
70 | $client->expects($this->any())
71 | ->method('getResultCode')
72 | ->willReturn(\Memcached::RES_SUCCESS);
73 | $client->expects($this->exactly(1))
74 | ->method('get')
75 | ->with('foo')
76 | ->willReturn(false);
77 |
78 | $storage = new Memcache($client);
79 | $storage->limitRate('foo');
80 | }
81 |
82 | public function testResetRate()
83 | {
84 | $client = @$this->getMockBuilder('\\Memcached')
85 | ->setMethods(array('delete'))
86 | ->getMock();
87 | $client->expects($this->once())
88 | ->method('delete')
89 | ->with('foo');
90 |
91 | $storage = new Memcache($client);
92 | $this->assertTrue($storage->resetRate('foo'));
93 | }
94 |
95 | }
96 |
--------------------------------------------------------------------------------
/Tests/Service/Storage/PhpRedisTest.php:
--------------------------------------------------------------------------------
1 | markTestSkipped('Php Redis client not installed');
14 | }
15 | }
16 |
17 | protected function getRedisMock() {
18 | return $this->getMockBuilder('\Redis');
19 | }
20 |
21 | protected function getStorage($client) {
22 | return new PhpRedis($client);
23 | }
24 |
25 | public function testgetRateInfo()
26 | {
27 | $client = $this->getRedisMock()
28 | ->setMethods(array('hgetall'))
29 | ->getMock();
30 | $client->expects($this->once())
31 | ->method('hgetall')
32 | ->with('foo')
33 | ->will($this->returnValue(array('limit' => 100, 'calls' => 50, 'reset' => 1234)));
34 |
35 | $storage = $this->getStorage($client);
36 | $rli = $storage->getRateInfo('foo');
37 | $this->assertInstanceOf('Noxlogic\\RateLimitBundle\\Service\\RateLimitInfo', $rli);
38 | $this->assertEquals(100, $rli->getLimit());
39 | $this->assertEquals(50, $rli->getCalls());
40 | $this->assertEquals(1234, $rli->getResetTimestamp());
41 | }
42 |
43 | public function testcreateRate()
44 | {
45 | $client = $this->getRedisMock()
46 | ->setMethods(array('hset', 'expire', 'hgetall'))
47 | ->getMock();
48 | $client->expects($this->once())
49 | ->method('expire')
50 | ->with('foo', 123);
51 | $client->expects($this->exactly(3))
52 | ->method('hset')
53 | ->withConsecutive(
54 | array('foo', 'limit', 100),
55 | array('foo', 'calls', 1),
56 | array('foo', 'reset')
57 | );
58 |
59 | $storage = $this->getStorage($client);
60 | $storage->createRate('foo', 100, 123);
61 | }
62 |
63 |
64 | public function testLimitRateNoKey()
65 | {
66 | $client = $this->getRedisMock()
67 | ->setMethods(array('hgetall'))
68 | ->getMock();
69 | $client->expects($this->once())
70 | ->method('hgetall')
71 | ->with('foo')
72 | ->will($this->returnValue([]));
73 |
74 | $storage = $this->getStorage($client);
75 | $this->assertFalse($storage->limitRate('foo'));
76 | }
77 |
78 | public function testLimitRateWithKey()
79 | {
80 | $client = $this->getRedisMock()
81 | ->setMethods(array('hincrby', 'hgetall'))
82 | ->getMock();
83 | $client->expects($this->once())
84 | ->method('hgetall')
85 | ->with('foo')
86 | ->will($this->returnValue([
87 | 'limit' => 1,
88 | 'calls' => 1,
89 | 'reset' => 1,
90 | ]));
91 | $client->expects($this->once())
92 | ->method('hincrby')
93 | ->with('foo', 'calls', 1)
94 | ->will($this->returnValue(2));
95 |
96 | $storage = $this->getStorage($client);
97 | $storage->limitRate('foo');
98 | }
99 |
100 |
101 |
102 | public function testresetRate()
103 | {
104 | $client = $this->getRedisMock()
105 | ->setMethods(array('del'))
106 | ->getMock();
107 | $client->expects($this->once())
108 | ->method('del')
109 | ->with('foo');
110 |
111 | $storage = $this->getStorage($client);
112 | $this->assertTrue($storage->resetRate('foo'));
113 | }
114 |
115 | }
116 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 1.17.0
2 | 2021-04-06 jaytaph
3 | - PHP8 compatibility added.
4 |
5 | ## 1.16.2
6 | 2021-03-13 jaytaph
7 | - Issue #115: Reserved characters in redis are sanitized (fixes IPv6 issues)
8 |
9 | ## 1.16.1
10 | 2021-03-12 jaytaph
11 | - Added support for framework extra bundle 6.0 in the requirements
12 |
13 | ## 1.16.0
14 | 2021-02-01 jaytaph
15 | - Moved from PSR-0 to PSR-4 to keep composer2 compatibility (Maxime Horcholle)
16 | - Added redis cluster functionality (DemigodCode)
17 |
18 | ## 1.15.1
19 | 2020-02-11 jaytaph
20 | - Fixed Symfony5 compatibility issues
21 |
22 | ## 1.15.0
23 | 2020-02-10 jaytaph
24 | - Added support for Symfony 5.x and removed support for < 3.4 and < 4.3 (kai)
25 | - Dropped support for PHP 7.2 or lower
26 |
27 | ## 1.14.0
28 | 2019-02-19 goetas
29 | - Added Payload to RateLimit to allow better exceptions
30 |
31 | ## 1.13.0
32 | 2019-01-04 axi
33 | - Added CheckedRateLimit event that allows RateLimit to be changed
34 |
35 | ## 1.12.0
36 | 2019-01-04
37 | - Something has gone wrong with the versions, just going to make a new one and
38 | move on
39 |
40 | ## 1.11.1
41 | 2018-07-02 mcfedr
42 | - Accept null rate response exception configuration
43 |
44 | ## 1.10.3
45 | 2019-01-04 DemigodCode
46 | - Deprecations in Symfony 4.2
47 |
48 | ## 1.10.2
49 | 2018-09-28 pierniq
50 | - Fixed setting calls in Redis/PhpRedis storage
51 |
52 | ## 1.10.1
53 | 2018-06-29 mcfedr
54 | - Change cache keys to be valid PSR-6 keys
55 |
56 | ## 1.10.0
57 | 2018-06-27 mcfedr
58 | - Add Psr-6 Cache storage engine
59 |
60 | ## 1.9.1
61 | 2018-06-27 mcfedr
62 | - Optimisation for Memcached storage engine
63 | - Reduce likely hood of infinite loop in memcached storage
64 | - Memcached storage will silently fail in the same way other storages fail
65 |
66 | ## 1.9.0
67 | 2018-06-27 mcfedr
68 | - Add Psr-16 Simple Cache storage engine
69 | - Optimisation for Doctrine Cache storage engine
70 |
71 | ## 1.8.2
72 | 2018-06-04 goetas
73 | - Fix Symfony 4 support by allowing newer versions of `framework-extra-bundle`
74 | - Fix travis tests as some just seem to fail
75 |
76 | ## 1.8.1
77 | 2018-05-24 mcfedr
78 | - Support for Symfony 4
79 |
80 | 2017-10-19 merk
81 | - Force $methods to be an array
82 |
83 | 2017-08-17 odoucet
84 | - Fix and improve Travis builds
85 |
86 | 2017-08-11 mcfedr
87 | - More efficient use of Redis
88 | - Option to disable fos listener
89 | - Easy to use RateLimitBundle without extra bundles
90 | - Add support for using a php redis client
91 |
92 | ## 1.7.0
93 | 2016-03-25 Joshua Thijsen
94 | Fixed issue where manual reset did not correctly reset in redis
95 |
96 | 2016-03-18 Scott Brown
97 | Implement reset of rate limit
98 |
99 | ## 1.6.0
100 |
101 | 2015-24-12 Roland Ekström
102 | Initial support for Symfony 3
103 |
104 | ## 1.5.0
105 |
106 | 2015-04-10 Sam Van der Borght
107 | [Security] Prevent ratelimit bypassing by encoding url paths
108 |
109 | 2015-01-05 Jonathan McLean
110 | Fix rate_response_exception
111 |
112 | ## 1.4
113 |
114 | 2014-12-10 Koen Vlaswinkel
115 | Add Doctrine Cache storage engine
116 |
117 | ## 1.3
118 |
119 | 2014-11-17 Joshua Thijssen
120 | Ratelimit can trigger exceptions
121 |
122 | ## 1.2
123 | 2014-07-15 Dan Spencer
124 | Added global rate limits
125 |
126 | ## 1.1
127 | 2014-07-08 Joshua Thijssen
128 | Added changelog to reflect changes in the different releases
129 |
130 | 2014-07-07 Tobias Berchtold
131 | removed dependency to constant used in Symfony > 2.3
132 |
133 | 2014-07-05 Alberto Fernández
134 | Improved README, added enabled configuration
135 |
136 | ## 1.0.1
137 | 2014-06-23 Joshua Thijssen
138 | Fixed installation documentation for 1.x
139 |
140 | ## 1.0 - Initial release
141 |
--------------------------------------------------------------------------------
/Tests/Service/Storage/RedisTest.php:
--------------------------------------------------------------------------------
1 | getMockBuilder('Predis\\Client')
13 | ->setMethods(array('hgetall'))
14 | ->getMock();
15 | $client->expects($this->once())
16 | ->method('hgetall')
17 | ->with('foo')
18 | ->will($this->returnValue(array('limit' => 100, 'calls' => 50, 'reset' => 1234)));
19 |
20 | $storage = new Redis($client);
21 | $rli = $storage->getRateInfo('foo');
22 | $this->assertInstanceOf('Noxlogic\\RateLimitBundle\\Service\\RateLimitInfo', $rli);
23 | $this->assertEquals(100, $rli->getLimit());
24 | $this->assertEquals(50, $rli->getCalls());
25 | $this->assertEquals(1234, $rli->getResetTimestamp());
26 | }
27 |
28 | public function testcreateRate()
29 | {
30 | $client = $this->getMockBuilder('Predis\\Client')
31 | ->setMethods(array('hset', 'expire', 'hgetall'))
32 | ->getMock();
33 | $client->expects($this->once())
34 | ->method('expire')
35 | ->with('foo', 123);
36 | $client->expects($this->exactly(3))
37 | ->method('hset')
38 | ->withConsecutive(
39 | array('foo', 'limit', 100),
40 | array('foo', 'calls', 1),
41 | array('foo', 'reset')
42 | );
43 |
44 | $storage = new Redis($client);
45 | $storage->createRate('foo', 100, 123);
46 | }
47 |
48 |
49 | public function testLimitRateNoKey()
50 | {
51 | $client = $this->getMockBuilder('Predis\\Client')
52 | ->setMethods(array('hgetall'))
53 | ->getMock();
54 | $client->expects($this->once())
55 | ->method('hgetall')
56 | ->with('foo')
57 | ->will($this->returnValue([]));
58 |
59 | $storage = new Redis($client);
60 | $this->assertFalse($storage->limitRate('foo'));
61 | }
62 |
63 | public function testLimitRateWithKey()
64 | {
65 | $client = $this->getMockBuilder('Predis\\Client')
66 | ->setMethods(array('hexists', 'hincrby', 'hgetall'))
67 | ->getMock();
68 | $client->expects($this->once())
69 | ->method('hgetall')
70 | ->with('foo')
71 | ->will($this->returnValue([
72 | 'limit' => 1,
73 | 'calls' => 1,
74 | 'reset' => 1,
75 | ]));
76 | $client->expects($this->once())
77 | ->method('hincrby')
78 | ->with('foo', 'calls', 1)
79 | ->will($this->returnValue(2));
80 |
81 | $storage = new Redis($client);
82 | $storage->limitRate('foo');
83 | }
84 |
85 |
86 |
87 | public function testresetRate()
88 | {
89 | $client = $this->getMockBuilder('Predis\\Client')
90 | ->setMethods(array('del'))
91 | ->getMock();
92 | $client->expects($this->once())
93 | ->method('del')
94 | ->with('foo');
95 |
96 | $storage = new Redis($client);
97 | $this->assertTrue($storage->resetRate('foo'));
98 | }
99 |
100 | public function testSanitizeKey()
101 | {
102 | $client = $this->getMockBuilder('Predis\\Client')
103 | ->setMethods(array('del'))
104 | ->getMock();
105 | $client->expects($this->once())
106 | ->method('del')
107 | ->with('PUT.POST.api_foo.2800_xxx_yyyy_zzzz_d1__41_zz_zz_x_xx_yyyy');
108 |
109 | $storage = new Redis($client);
110 | $this->assertTrue($storage->resetRate('PUT.POST.api_foo.2800:xxx:yyyy:zzzz:d1@@41:zz{zz:x}xx:yyyy'));
111 | }
112 |
113 | }
114 |
--------------------------------------------------------------------------------
/Tests/DependencyInjection/NoxlogicRateLimitExtensionTest.php:
--------------------------------------------------------------------------------
1 | load(array(), $containerBuilder);
24 |
25 | $this->assertEquals($containerBuilder->getParameter('noxlogic_rate_limit.enabled'), true);
26 | $this->assertEquals($containerBuilder->getParameter('noxlogic_rate_limit.rate_response_code'), 429);
27 | $this->assertEquals($containerBuilder->getParameter('noxlogic_rate_limit.display_headers'), true);
28 | $this->assertEquals($containerBuilder->getParameter('noxlogic_rate_limit.headers.reset.name'), 'X-RateLimit-Reset');
29 | }
30 |
31 | public function testStorageEngineParameterProvider()
32 | {
33 | $extension = new NoxlogicRateLimitExtension();
34 | $containerBuilder = new ContainerBuilder(new ParameterBag());
35 | $extension->load(array(
36 | 'noxlogic_rate_limit' => array(
37 | 'storage_engine' => 'doctrine',
38 | 'doctrine_provider' => 'redis_cache',
39 | )
40 | ), $containerBuilder);
41 |
42 | $this->assertEquals('Noxlogic\RateLimitBundle\Service\Storage\DoctrineCache', $containerBuilder->getParameter('noxlogic_rate_limit.storage.class'));
43 |
44 | $storageDef = $containerBuilder->getDefinition('noxlogic_rate_limit.storage');
45 | $this->assertEquals('doctrine_cache.providers.redis_cache', (string)($storageDef->getArgument(0)));
46 | }
47 |
48 | public function testStorageEngineParameterService()
49 | {
50 | $extension = new NoxlogicRateLimitExtension();
51 | $containerBuilder = new ContainerBuilder(new ParameterBag());
52 | $extension->load(array(
53 | 'noxlogic_rate_limit' => array(
54 | 'storage_engine' => 'doctrine',
55 | 'doctrine_service' => 'my.redis_cache',
56 | )
57 | ), $containerBuilder);
58 |
59 | $this->assertEquals('Noxlogic\RateLimitBundle\Service\Storage\DoctrineCache', $containerBuilder->getParameter('noxlogic_rate_limit.storage.class'));
60 |
61 | $storageDef = $containerBuilder->getDefinition('noxlogic_rate_limit.storage');
62 | $this->assertEquals('my.redis_cache', (string)($storageDef->getArgument(0)));
63 | }
64 |
65 | public function testParametersWhenDisabled()
66 | {
67 | $extension = new NoxlogicRateLimitExtension();
68 | $containerBuilder = new ContainerBuilder(new ParameterBag());
69 | $extension->load(array('enabled' => false), $containerBuilder);
70 |
71 | $this->assertEquals(429, $containerBuilder->getParameter('noxlogic_rate_limit.rate_response_code'));
72 | }
73 |
74 | public function testPathLimitsParameter()
75 | {
76 | $pathLimits = array(
77 | 'api' => array(
78 | 'path' => 'api/',
79 | 'methods' => array('GET'),
80 | 'limit' => 100,
81 | 'period' => 60
82 | )
83 | );
84 |
85 | $extension = new NoxlogicRateLimitExtension();
86 | $containerBuilder = new ContainerBuilder(new ParameterBag());
87 | $extension->load(array(array('path_limits' => $pathLimits)), $containerBuilder);
88 |
89 | $this->assertEquals($containerBuilder->getParameter('noxlogic_rate_limit.path_limits'), $pathLimits);
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/Resources/config/services.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 | Noxlogic\RateLimitBundle\EventListener\HeaderModificationListener
9 | Noxlogic\RateLimitBundle\EventListener\RateLimitAnnotationListener
10 | Noxlogic\RateLimitBundle\Service\RateLimitService
11 | Noxlogic\RateLimitBundle\EventListener\OauthKeyGenerateListener
12 | Noxlogic\RateLimitBundle\Util\PathLimitProcessor
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | %noxlogic_rate_limit.path_limits%
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | enabled
39 | %noxlogic_rate_limit.enabled%
40 |
41 |
42 | rate_response_code
43 | %noxlogic_rate_limit.rate_response_code%
44 |
45 |
46 | rate_response_message
47 | %noxlogic_rate_limit.rate_response_message%
48 |
49 |
50 | rate_response_exception
51 | %noxlogic_rate_limit.rate_response_exception%
52 |
53 |
54 |
55 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/Tests/Service/Storage/PsrCacheTest.php:
--------------------------------------------------------------------------------
1 | getMockBuilder('Psr\\Cache\\CacheItemInterface')
13 | ->getMock();
14 | $item->expects($this->once())
15 | ->method('isHit')
16 | ->willReturn(true);
17 | $item->expects($this->once())
18 | ->method('get')
19 | ->willReturn(array('limit' => 100, 'calls' => 50, 'reset' => 1234));
20 |
21 | $client = $this->getMockBuilder('Psr\\Cache\\CacheItemPoolInterface')
22 | ->getMock();
23 | $client->expects($this->once())
24 | ->method('getItem')
25 | ->with('foo')
26 | ->will($this->returnValue($item));
27 |
28 | $storage = new PsrCache($client);
29 | $rli = $storage->getRateInfo('foo');
30 | $this->assertInstanceOf('Noxlogic\\RateLimitBundle\\Service\\RateLimitInfo', $rli);
31 | $this->assertEquals(100, $rli->getLimit());
32 | $this->assertEquals(50, $rli->getCalls());
33 | $this->assertEquals(1234, $rli->getResetTimestamp());
34 | }
35 |
36 | public function testCreateRate()
37 | {
38 | $item = $this->getMockBuilder('Psr\\Cache\\CacheItemInterface')
39 | ->getMock();
40 |
41 | /**
42 | * psr/cache 3.0 changed the return type of set() and expiresAfter() to return self.
43 | * @TODO NEXT_MAJOR: Remove this check and the first conditional block when psr/cache <3 support is dropped.
44 | */
45 | $psrCacheVersion = \Composer\InstalledVersions::getVersion('psr/cache');
46 | if (version_compare($psrCacheVersion, '3.0', '<')) {
47 | $item->expects($this->once())
48 | ->method('set')
49 | ->willReturn(true);
50 | $item->expects($this->once())
51 | ->method('expiresAfter')
52 | ->willReturn(true);
53 | } else {
54 | $item->expects($this->once())
55 | ->method('set')
56 | ->willReturnSelf();
57 | $item->expects($this->once())
58 | ->method('expiresAfter')
59 | ->willReturnSelf();
60 | }
61 |
62 | $client = $this->getMockBuilder('Psr\\Cache\\CacheItemPoolInterface')
63 | ->getMock();
64 | $client->expects($this->once())
65 | ->method('getItem')
66 | ->with('foo')
67 | ->will($this->returnValue($item));
68 | $client->expects($this->once())
69 | ->method('save')
70 | ->with($item)
71 | ->willReturn(true);
72 |
73 | $storage = new PsrCache($client);
74 | $storage->createRate('foo', 100, 123);
75 | }
76 |
77 |
78 | public function testLimitRateNoKey()
79 | {
80 | $item = $this->getMockBuilder('Psr\\Cache\\CacheItemInterface')
81 | ->getMock();
82 | $item->expects($this->once())
83 | ->method('isHit')
84 | ->willReturn(false);
85 |
86 | $client = $this->getMockBuilder('Psr\\Cache\\CacheItemPoolInterface')
87 | ->getMock();
88 | $client->expects($this->once())
89 | ->method('getItem')
90 | ->with('foo')
91 | ->will($this->returnValue($item));
92 |
93 | $storage = new PsrCache($client);
94 | $this->assertFalse($storage->limitRate('foo'));
95 | }
96 |
97 | public function testLimitRateWithKey()
98 | {
99 | $item = $this->getMockBuilder('Psr\\Cache\\CacheItemInterface')
100 | ->getMock();
101 | $item->expects($this->once())
102 | ->method('isHit')
103 | ->willReturn(true);
104 | $item->expects($this->once())
105 | ->method('get')
106 | ->willReturn(array('limit' => 100, 'calls' => 50, 'reset' => 1234));
107 | $item->expects($this->once())
108 | ->method('set');
109 | $item->expects($this->once())
110 | ->method('expiresAfter');
111 |
112 | $client = $this->getMockBuilder('Psr\\Cache\\CacheItemPoolInterface')
113 | ->getMock();
114 | $client->expects($this->once())
115 | ->method('getItem')
116 | ->with('foo')
117 | ->will($this->returnValue($item));
118 | $client->expects($this->once())
119 | ->method('save')
120 | ->with($item)
121 | ->willReturn(true);
122 |
123 | $storage = new PsrCache($client);
124 | $storage->limitRate('foo');
125 | }
126 |
127 | public function testResetRate()
128 | {
129 | $client = $this->getMockBuilder('Psr\\Cache\\CacheItemPoolInterface')
130 | ->getMock();
131 | $client->expects($this->once())
132 | ->method('deleteItem')
133 | ->with('foo')
134 | ->willReturn(true);
135 |
136 | $storage = new PsrCache($client);
137 | $this->assertTrue($storage->resetRate('foo'));
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/Tests/DependencyInjection/ConfigurationTest.php:
--------------------------------------------------------------------------------
1 | processor = new Processor();
17 | }
18 |
19 | private function getConfigs(array $configArray): array
20 | {
21 | $configuration = new Configuration();
22 |
23 | return $this->processor->processConfiguration($configuration, array($configArray));
24 | }
25 |
26 | public function testUnconfiguredConfiguration(): void
27 | {
28 | $configuration = $this->getConfigs(array());
29 |
30 | $this->assertSame(array(
31 | 'enabled' => true,
32 | 'storage_engine' => 'redis',
33 | 'redis_client' => 'default_client',
34 | 'redis_service' => null,
35 | 'php_redis_service' => null,
36 | 'memcache_client' => 'default',
37 | 'memcache_service' => null,
38 | 'doctrine_provider' => null,
39 | 'doctrine_service' => null,
40 | 'simple_cache_service' => null,
41 | 'cache_service' => null,
42 | 'rate_response_code' => 429,
43 | 'rate_response_exception' => null,
44 | 'rate_response_message' => 'You exceeded the rate limit',
45 | 'display_headers' => true,
46 | 'headers' => array(
47 | 'limit' => 'X-RateLimit-Limit',
48 | 'remaining' => 'X-RateLimit-Remaining',
49 | 'reset' => 'X-RateLimit-Reset',
50 | ),
51 | 'path_limits' => array(),
52 | 'fos_oauth_key_listener' => true
53 | ), $configuration);
54 | }
55 |
56 | public function testDisabledConfiguration(): void
57 | {
58 | $configuration = $this->getConfigs(array('enabled' => false));
59 |
60 | $this->assertArrayHasKey('enabled', $configuration);
61 | $this->assertFalse($configuration['enabled']);
62 | }
63 |
64 | public function testPathLimitConfiguration(): void
65 | {
66 | $pathLimits = array(
67 | 'api' => array(
68 | 'path' => 'api/',
69 | 'methods' => array('GET'),
70 | 'limit' => 100,
71 | 'period' => 60
72 | )
73 | );
74 |
75 | $configuration = $this->getConfigs(array(
76 | 'path_limits' => $pathLimits
77 | ));
78 |
79 | $this->assertArrayHasKey('path_limits', $configuration);
80 | $this->assertEquals($pathLimits, $configuration['path_limits']);
81 | }
82 |
83 | public function testMultiplePathLimitConfiguration(): void
84 | {
85 | $pathLimits = array(
86 | 'api' => array(
87 | 'path' => 'api/',
88 | 'methods' => array('GET', 'POST'),
89 | 'limit' => 200,
90 | 'period' => 10
91 | ),
92 | 'api2' => array(
93 | 'path' => 'api2/',
94 | 'methods' => array('*'),
95 | 'limit' => 1000,
96 | 'period' => 15
97 | )
98 | );
99 |
100 | $configuration = $this->getConfigs(array(
101 | 'path_limits' => $pathLimits
102 | ));
103 |
104 | $this->assertArrayHasKey('path_limits', $configuration);
105 | $this->assertEquals($pathLimits, $configuration['path_limits']);
106 | }
107 |
108 | public function testDefaultPathLimitMethods(): void
109 | {
110 | $pathLimits = array(
111 | 'api' => array(
112 | 'path' => 'api/',
113 | 'methods' => array('GET', 'POST'),
114 | 'limit' => 200,
115 | 'period' => 10
116 | ),
117 | 'api2' => array(
118 | 'path' => 'api2/',
119 | 'limit' => 1000,
120 | 'period' => 15
121 | )
122 | );
123 |
124 | $configuration = $this->getConfigs(array(
125 | 'path_limits' => $pathLimits
126 | ));
127 |
128 | $pathLimits['api2']['methods'] = array('*');
129 |
130 | $this->assertArrayHasKey('path_limits', $configuration);
131 | $this->assertEquals($pathLimits, $configuration['path_limits']);
132 | }
133 |
134 | public function testMustBeBasedOnExceptionClass(): void
135 | {
136 | $this->expectException(InvalidConfigurationException::class);
137 | $this->getConfigs(array('rate_response_exception' => '\StdClass'));
138 | }
139 |
140 | /**
141 | * @testWith [""]
142 | * [null]
143 | */
144 | public function testEmptyPathIsNotAllowed(mixed $path): void
145 | {
146 | $pathLimits = [
147 | 'api' => [
148 | 'path' => $path,
149 | 'methods' => ['GET'],
150 | 'limit' => 200,
151 | 'period' => 10
152 | ],
153 | ];
154 |
155 | $this->expectException(InvalidConfigurationException::class);
156 |
157 | $this->getConfigs([
158 | 'path_limits' => $pathLimits
159 | ]);
160 | }
161 |
162 | /**
163 | *
164 | */
165 | public function testMustBeBasedOnExceptionClass2(): void
166 | {
167 | $this->getConfigs(array('rate_response_exception' => '\InvalidArgumentException'));
168 |
169 | # no exception triggered is ok.
170 | $this->expectNotToPerformAssertions();
171 | }
172 |
173 | public function testMustBeBasedOnExceptionOrNull(): void
174 | {
175 | $this->getConfigs(array('rate_response_exception' => null));
176 |
177 | # no exception triggered is ok.
178 | $this->expectNotToPerformAssertions();
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/DependencyInjection/NoxlogicRateLimitExtension.php:
--------------------------------------------------------------------------------
1 | processConfiguration($configuration, $configs);
26 | $this->loadServices($container, $config);
27 |
28 | }
29 |
30 | private function loadServices(ContainerBuilder $container, array $config): void
31 | {
32 | $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
33 | $loader->load('services.xml');
34 |
35 | $container->setParameter('noxlogic_rate_limit.enabled', $config['enabled']);
36 |
37 | $container->setParameter('noxlogic_rate_limit.rate_response_exception', $config['rate_response_exception']);
38 | $container->setParameter('noxlogic_rate_limit.rate_response_code', $config['rate_response_code']);
39 | $container->setParameter('noxlogic_rate_limit.rate_response_message', $config['rate_response_message']);
40 |
41 | $container->setParameter('noxlogic_rate_limit.display_headers', $config['display_headers']);
42 | $container->setParameter('noxlogic_rate_limit.headers.limit.name', $config['headers']['limit']);
43 | $container->setParameter('noxlogic_rate_limit.headers.remaining.name', $config['headers']['remaining']);
44 | $container->setParameter('noxlogic_rate_limit.headers.reset.name', $config['headers']['reset']);
45 |
46 | $container->setParameter('noxlogic_rate_limit.path_limits', $config['path_limits']);
47 |
48 | switch ($config['storage_engine']) {
49 | case 'memcache':
50 | $container->setParameter('noxlogic_rate_limit.storage.class', 'Noxlogic\RateLimitBundle\Service\Storage\Memcache');
51 | if (isset($config['memcache_client'])) {
52 | $service = 'memcache.' . $config['memcache_client'];
53 | } else {
54 | $service = $config['memcache_service'];
55 | }
56 | $container->getDefinition('noxlogic_rate_limit.storage')->replaceArgument(
57 | 0,
58 | new Reference($service)
59 | );
60 | break;
61 | case 'redis':
62 | $container->setParameter('noxlogic_rate_limit.storage.class', 'Noxlogic\RateLimitBundle\Service\Storage\Redis');
63 | if (isset($config['redis_service'])) {
64 | $service = $config['redis_service'];
65 | } else {
66 | $service = 'snc_redis.' . $config['redis_client'];
67 | }
68 | $container->getDefinition('noxlogic_rate_limit.storage')->replaceArgument(
69 | 0,
70 | new Reference($service)
71 | );
72 | break;
73 | case 'doctrine':
74 | $container->setParameter('noxlogic_rate_limit.storage.class', 'Noxlogic\RateLimitBundle\Service\Storage\DoctrineCache');
75 | if (isset($config['doctrine_provider'])) {
76 | $service = 'doctrine_cache.providers.' . $config['doctrine_provider'];
77 | } else {
78 | $service = $config['doctrine_service'];
79 | }
80 | $container->getDefinition('noxlogic_rate_limit.storage')->replaceArgument(
81 | 0,
82 | new Reference($service)
83 | );
84 | break;
85 | case 'php_redis':
86 | $container->setParameter('noxlogic_rate_limit.storage.class', 'Noxlogic\RateLimitBundle\Service\Storage\PhpRedis');
87 | $container->getDefinition('noxlogic_rate_limit.storage')->replaceArgument(
88 | 0,
89 | new Reference($config['php_redis_service'])
90 | );
91 | break;
92 | case 'php_redis_cluster':
93 | $container->setParameter('noxlogic_rate_limit.storage.class', 'Noxlogic\RateLimitBundle\Service\Storage\PhpRedisCluster');
94 | $container->getDefinition('noxlogic_rate_limit.storage')->replaceArgument(
95 | 0,
96 | new Reference($config['php_redis_service'])
97 | );
98 | break;
99 | case 'simple_cache':
100 | $container->setParameter('noxlogic_rate_limit.storage.class', 'Noxlogic\RateLimitBundle\Service\Storage\SimpleCache');
101 | $container->getDefinition('noxlogic_rate_limit.storage')->replaceArgument(
102 | 0,
103 | new Reference($config['simple_cache_service'])
104 | );
105 | break;
106 | case 'cache':
107 | $container->setParameter('noxlogic_rate_limit.storage.class', 'Noxlogic\RateLimitBundle\Service\Storage\PsrCache');
108 | $container->getDefinition('noxlogic_rate_limit.storage')->replaceArgument(
109 | 0,
110 | new Reference($config['cache_service'])
111 | );
112 | break;
113 | }
114 |
115 | if ($config['fos_oauth_key_listener']) {
116 | $tokenStorageReference = new Reference('security.token_storage');
117 | $container->getDefinition('noxlogic_rate_limit.oauth_key_generate_listener')->replaceArgument(0, $tokenStorageReference);
118 | } else {
119 | $container->removeDefinition('noxlogic_rate_limit.oauth_key_generate_listener');
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/Tests/EventListener/HeaderModificationListenerTest.php:
--------------------------------------------------------------------------------
1 | createEvent();
20 |
21 | $listener = new HeaderModificationListener();
22 | $listener->setParameter('display_headers', true);
23 | $listener->setParameter('header_limit_name', 'X-RateLimit-Limit');
24 | $listener->setParameter('header_remaining_name', 'X-RateLimit-Remaining');
25 | $listener->setParameter('header_reset_name', 'X-RateLimit-Reset');
26 | $listener->onKernelResponse($event);
27 |
28 | $this->assertFalse($event->getResponse()->headers->has('X-RateLimit-Limit'));
29 | $this->assertFalse($event->getResponse()->headers->has('X-RateLimit-Reset'));
30 | $this->assertFalse($event->getResponse()->headers->has('X-RateLimit-Remaining'));
31 | }
32 |
33 |
34 | public function testListenerWithInfo()
35 | {
36 | $rateLimitInfo = new RateLimitInfo();
37 | $rateLimitInfo->setCalls(5);
38 | $rateLimitInfo->setLimit(10);
39 | $rateLimitInfo->setResetTimestamp(1520000);
40 |
41 | $event = $this->createEvent();
42 | $event->getRequest()->attributes->set('rate_limit_info', $rateLimitInfo);
43 |
44 | $listener = new HeaderModificationListener();
45 | $listener->setParameter('display_headers', true);
46 | $listener->setParameter('header_limit_name', 'X-RateLimit-Limit');
47 | $listener->setParameter('header_remaining_name', 'X-RateLimit-Remaining');
48 | $listener->setParameter('header_reset_name', 'X-RateLimit-Reset');
49 | $listener->onKernelResponse($event);
50 |
51 | $this->assertEquals(10, $event->getResponse()->headers->has('X-RateLimit-Limit'));
52 | $this->assertEquals(5, $event->getResponse()->headers->has('X-RateLimit-Remaining'));
53 | $this->assertEquals(1520000, $event->getResponse()->headers->has('X-RateLimit-Reset'));
54 | }
55 |
56 | public function testListenerWithDisplayHeaderFalse()
57 | {
58 | $rateLimitInfo = new RateLimitInfo();
59 | $rateLimitInfo->setCalls(5);
60 | $rateLimitInfo->setLimit(10);
61 | $rateLimitInfo->setResetTimestamp(1520000);
62 |
63 | $event = $this->createEvent();
64 | $event->getRequest()->attributes->set('rate_limit_info', $rateLimitInfo);
65 |
66 | $listener = new HeaderModificationListener();
67 | $listener->setParameter('display_headers', false);
68 | $listener->setParameter('header_limit_name', 'X-RateLimit-Limit');
69 | $listener->setParameter('header_remaining_name', 'X-RateLimit-Remaining');
70 | $listener->setParameter('header_reset_name', 'X-RateLimit-Reset');
71 | $listener->onKernelResponse($event);
72 |
73 | $this->assertFalse($event->getResponse()->headers->has('X-RateLimit-Limit'));
74 | $this->assertFalse($event->getResponse()->headers->has('X-RateLimit-Reset'));
75 | $this->assertFalse($event->getResponse()->headers->has('X-RateLimit-Remaining'));
76 | }
77 |
78 | public function testListenerWithCustomHeaders()
79 | {
80 | $rateLimitInfo = new RateLimitInfo();
81 | $rateLimitInfo->setCalls(5);
82 | $rateLimitInfo->setLimit(10);
83 | $rateLimitInfo->setResetTimestamp(1520000);
84 |
85 | $event = $this->createEvent();
86 | $event->getRequest()->attributes->set('rate_limit_info', $rateLimitInfo);
87 |
88 | $listener = new HeaderModificationListener();
89 | $listener->setParameter('display_headers', true);
90 | $listener->setParameter('header_limit_name', 'foo');
91 | $listener->setParameter('header_remaining_name', 'bar');
92 | $listener->setParameter('header_reset_name', 'baz');
93 | $listener->onKernelResponse($event);
94 |
95 | $this->assertTrue($event->getResponse()->headers->has('foo'));
96 | $this->assertTrue($event->getResponse()->headers->has('bar'));
97 | $this->assertTrue($event->getResponse()->headers->has('baz'));
98 | }
99 |
100 | public function testListenerRemainingCannotBeNegative()
101 | {
102 | $rateLimitInfo = new RateLimitInfo();
103 | $rateLimitInfo->setCalls(500);
104 | $rateLimitInfo->setLimit(10);
105 | $rateLimitInfo->setResetTimestamp(1520000);
106 |
107 | $event = $this->createEvent();
108 | $event->getRequest()->attributes->set('rate_limit_info', $rateLimitInfo);
109 |
110 | $listener = new HeaderModificationListener();
111 | $listener->setParameter('display_headers', true);
112 | $listener->setParameter('header_limit_name', 'X-RateLimit-Limit');
113 | $listener->setParameter('header_remaining_name', 'X-RateLimit-Remaining');
114 | $listener->setParameter('header_reset_name', 'X-RateLimit-Reset');
115 | $listener->onKernelResponse($event);
116 |
117 | $this->assertEquals(0, $event->getResponse()->headers->get('X-RateLimit-Remaining'));
118 | }
119 |
120 | public function testListenerWithoutRateInfo()
121 | {
122 | $event = $this->createEvent();
123 |
124 | $listener = new HeaderModificationListener();
125 | $listener->setParameter('display_headers', true);
126 | $listener->setParameter('header_limit_name', 'X-RateLimit-Limit');
127 | $listener->setParameter('header_remaining_name', 'X-RateLimit-Remaining');
128 | $listener->setParameter('header_reset_name', 'X-RateLimit-Reset');
129 | $listener->onKernelResponse($event);
130 |
131 | $this->assertFalse($event->getResponse()->headers->has('X-RateLimit-Limit'));
132 | $this->assertFalse($event->getResponse()->headers->has('X-RateLimit-Reset'));
133 | $this->assertFalse($event->getResponse()->headers->has('X-RateLimit-Remaining'));
134 | }
135 |
136 | /**
137 | * @return ResponseEvent
138 | */
139 | protected function createEvent()
140 | {
141 | $kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock();
142 | $request = new Request();
143 | $response = new Response();
144 |
145 | $event = new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response);
146 |
147 | return $event;
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/DependencyInjection/Configuration.php:
--------------------------------------------------------------------------------
1 | getRootNode();
24 |
25 | $rootNode // @phpstan-ignore method.notFound
26 | ->canBeDisabled()
27 | ->children()
28 | ->enumNode('storage_engine')
29 | ->values(['redis','memcache','doctrine', 'php_redis', 'php_redis_cluster', 'simple_cache', 'cache'])
30 | ->defaultValue('redis')
31 | ->info('The storage engine where all the rates will be stored')
32 | ->end()
33 | ->scalarNode('redis_client')
34 | ->defaultValue('default_client')
35 | ->info('The redis client to use for the redis storage engine')
36 | ->end()
37 | ->scalarNode('redis_service')
38 | ->defaultNull()
39 | ->info('The Redis service to use for the redis storage engine, should be instance of \\Predis\\Client')
40 | ->example('project.predis')
41 | ->end()
42 | ->scalarNode('php_redis_service')
43 | ->defaultNull()
44 | ->info('Service id of a php redis, should be an instance of \\Redis')
45 | ->example('project.redis')
46 | ->end()
47 | ->scalarNode('memcache_client')
48 | ->defaultValue('default')
49 | ->info('The memcache client to use for the memcache storage engine')
50 | ->end()
51 | ->scalarNode('memcache_service')
52 | ->defaultNull()
53 | ->info('The Memcached service to use for the memcache storage engine, should be instance of \\Memcached')
54 | ->example('project.memcached')
55 | ->end()
56 | ->scalarNode('doctrine_provider')
57 | ->defaultNull()
58 | ->info('The Doctrine Cache provider to use for the doctrine storage engine')
59 | ->example('my_apc_cache')
60 | ->end()
61 | ->scalarNode('doctrine_service')
62 | ->defaultNull()
63 | ->info('The Doctrine Cache service to use for the doctrine storage engine')
64 | ->example('project.my_apc_cache')
65 | ->end()
66 | ->scalarNode('simple_cache_service')
67 | ->defaultNull()
68 | ->info('Service id of a simple cache, should be an instance of \\Psr\\SimpleCache\\CacheInterface')
69 | ->example('project.cache')
70 | ->end()
71 | ->scalarNode('cache_service')
72 | ->defaultNull()
73 | ->info('Service id of a cache, should be an instance of \\Psr\\Cache\\CacheItemPoolInterface')
74 | ->example('project.cache')
75 | ->end()
76 | ->integerNode('rate_response_code')
77 | ->min(400)
78 | ->max(499)
79 | ->defaultValue(static::HTTP_TOO_MANY_REQUESTS)
80 | ->info('The HTTP status code to return when a client hits the rate limit')
81 | ->end()
82 | ->scalarNode('rate_response_exception')
83 | ->defaultNull()
84 | ->info('Optional exception class that will be returned when a client hits the rate limit')
85 | ->validate()
86 | ->always(function ($item) {
87 | if ($item && !is_subclass_of($item, '\Exception')) {
88 | throw new InvalidConfigurationException(sprintf("'%s' must inherit the \\Exception class", $item));
89 | }
90 | return $item;
91 | })
92 | ->end()
93 | ->end()
94 | ->scalarNode('rate_response_message')
95 | ->defaultValue('You exceeded the rate limit')
96 | ->info('The HTTP message to return when a client hits the rate limit')
97 | ->end()
98 | ->booleanNode('display_headers')
99 | ->defaultTrue()
100 | ->info('Should the ratelimit headers be automatically added to the response?')
101 | ->end()
102 | ->arrayNode('headers')
103 | ->addDefaultsIfNotSet()
104 | ->info('What are the different header names to add')
105 | ->children()
106 | ->scalarNode('limit')->defaultValue('X-RateLimit-Limit')->end()
107 | ->scalarNode('remaining')->defaultValue('X-RateLimit-Remaining')->end()
108 | ->scalarNode('reset')->defaultValue('X-RateLimit-Reset')->end()
109 | ->end()
110 | ->end()
111 | ->arrayNode('path_limits')
112 | ->defaultValue([])
113 | ->info('Rate limits for paths')
114 | ->prototype('array')
115 | ->children()
116 | ->scalarNode('path')
117 | ->isRequired()
118 | ->cannotBeEmpty()
119 | ->end()
120 | ->arrayNode('methods')
121 | ->prototype('enum')
122 | ->values(['*', 'GET', 'POST', 'PUT', 'DELETE', 'PATCH'])
123 | ->end()
124 | ->requiresAtLeastOneElement()
125 | ->defaultValue(['*'])
126 | ->end()
127 | ->integerNode('limit')
128 | ->isRequired()
129 | ->min(0)
130 | ->end()
131 | ->integerNode('period')
132 | ->isRequired()
133 | ->min(0)
134 | ->end()
135 | ->end()
136 | ->end()
137 | ->end()
138 | ->booleanNode('fos_oauth_key_listener')
139 | ->defaultTrue()
140 | ->info('Enabled the FOS OAuthServerBundle listener')
141 | ->end()
142 | ->end()
143 | ;
144 |
145 | return $treeBuilder;
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/EventListener/RateLimitAnnotationListener.php:
--------------------------------------------------------------------------------
1 | eventDispatcher = $eventDispatcher;
32 | $this->rateLimitService = $rateLimitService;
33 | $this->pathLimitProcessor = $pathLimitProcessor;
34 | }
35 |
36 | public function onKernelController(ControllerEvent $event): void
37 | {
38 | // Skip if the bundle isn't enabled (for instance in test environment)
39 | if( ! $this->getParameter('enabled', true)) {
40 | return;
41 | }
42 |
43 | // Skip if we aren't the main request
44 | if ($event->getRequestType() !== HttpKernelInterface::MAIN_REQUEST) {
45 | return;
46 | }
47 |
48 | // RateLimits used to be set by sensio/framework-extra-bundle by reading annotations
49 | // Tests also use that mechanism, we should probably keep it for retrocompatibility
50 | $request = $event->getRequest();
51 | if ($request->attributes->has('_x-rate-limit')) {
52 | /** @var RateLimit[] $rateLimits */
53 | $rateLimits = $request->attributes->get('_x-rate-limit', []);
54 | } else {
55 | $rateLimits = $this->getRateLimitsFromAttributes($event->getController());
56 | }
57 | $rateLimit = $this->findBestMethodMatch($request, $rateLimits);
58 |
59 | // Another treatment before applying RateLimit ?
60 | $checkedRateLimitEvent = new CheckedRateLimitEvent($request, $rateLimit);
61 | $this->eventDispatcher->dispatch($checkedRateLimitEvent, RateLimitEvents::CHECKED_RATE_LIMIT);
62 | $rateLimit = $checkedRateLimitEvent->getRateLimit();
63 |
64 | // No matching RateLimit found
65 | if (! $rateLimit) {
66 | return;
67 | }
68 |
69 | $key = $this->getKey($event, $rateLimit, $rateLimits);
70 |
71 | // Ratelimit the call
72 | $rateLimitInfo = $this->rateLimitService->limitRate($key);
73 | if (! $rateLimitInfo) {
74 | // Create new rate limit entry for this call
75 | $rateLimitInfo = $this->rateLimitService->createRate($key, $rateLimit->getLimit(), $rateLimit->getPeriod());
76 | if (! $rateLimitInfo) {
77 | // @codeCoverageIgnoreStart
78 | return;
79 | // @codeCoverageIgnoreEnd
80 | }
81 | }
82 |
83 |
84 | // Store the current rating info in the request attributes
85 | $request->attributes->set('rate_limit_info', $rateLimitInfo);
86 |
87 | // Reset the rate limits
88 | if(time() >= $rateLimitInfo->getResetTimestamp()) {
89 | $this->rateLimitService->resetRate($key);
90 | $rateLimitInfo = $this->rateLimitService->createRate($key, $rateLimit->getLimit(), $rateLimit->getPeriod());
91 | if (! $rateLimitInfo) {
92 | // @codeCoverageIgnoreStart
93 | return;
94 | // @codeCoverageIgnoreEnd
95 | }
96 | }
97 |
98 | // When we exceeded our limit, return a custom error response
99 | if ($rateLimitInfo->getCalls() > $rateLimitInfo->getLimit()) {
100 |
101 | // Throw an exception if configured.
102 | if ($this->getParameter('rate_response_exception')) {
103 | $class = $this->getParameter('rate_response_exception');
104 |
105 | $e = new $class($this->getParameter('rate_response_message'), $this->getParameter('rate_response_code'));
106 |
107 | if ($e instanceof RateLimitExceptionInterface) {
108 | $e->setPayload($rateLimit->getPayload());
109 | }
110 |
111 | throw $e;
112 | }
113 |
114 | $message = $this->getParameter('rate_response_message');
115 | $code = $this->getParameter('rate_response_code');
116 | $event->setController(function () use ($message, $code) {
117 | // @codeCoverageIgnoreStart
118 | return new Response($message, $code);
119 | // @codeCoverageIgnoreEnd
120 | });
121 | $event->stopPropagation();
122 | }
123 |
124 | }
125 |
126 |
127 | /**
128 | * @param RateLimit[] $rateLimits
129 | */
130 | protected function findBestMethodMatch(Request $request, array $rateLimits): ?RateLimit
131 | {
132 | // Empty array, check the path limits
133 | if (count($rateLimits) === 0) {
134 | return $this->pathLimitProcessor->getRateLimit($request);
135 | }
136 |
137 | $best_match = null;
138 | foreach ($rateLimits as $rateLimit) {
139 | if (in_array($request->getMethod(), $rateLimit->getMethods(), true)) {
140 | $best_match = $rateLimit;
141 | }
142 |
143 | // Only match "default" annotation when we don't have a best match
144 | if ($best_match === null && count($rateLimit->methods) === 0) {
145 | $best_match = $rateLimit;
146 | }
147 | }
148 |
149 | return $best_match;
150 | }
151 |
152 | /** @param RateLimit[] $rateLimits */
153 | private function getKey(ControllerEvent $event, RateLimit $rateLimit, array $rateLimits): string
154 | {
155 | // Let listeners manipulate the key
156 | $request = $event->getRequest();
157 | $keyEvent = new GenerateKeyEvent($request, '', $rateLimit->getPayload());
158 |
159 | $rateLimitMethods = implode('.', $rateLimit->getMethods());
160 | $keyEvent->addToKey($rateLimitMethods);
161 |
162 | $rateLimitAlias = count($rateLimits) === 0
163 | ? str_replace('/', '.', $this->pathLimitProcessor->getMatchedPath($request))
164 | : $this->getAliasForRequest($event);
165 | $keyEvent->addToKey($rateLimitAlias);
166 | $this->eventDispatcher->dispatch($keyEvent, RateLimitEvents::GENERATE_KEY);
167 |
168 | return $keyEvent->getKey();
169 | }
170 |
171 | private function getAliasForRequest(ControllerEvent $event): string
172 | {
173 | $route = $event->getRequest()->attributes->get('_route');
174 | if ($route) {
175 | return $route;
176 | }
177 |
178 | $controller = $event->getController();
179 |
180 | if (is_string($controller) && str_contains($controller, '::')) {
181 | $controller = explode('::', $controller);
182 | }
183 |
184 | if (is_array($controller)) {
185 | return str_replace('\\', '.', is_string($controller[0]) ? $controller[0] : get_class($controller[0])) . '.' . $controller[1];
186 | }
187 |
188 | if ($controller instanceof \Closure) {
189 | return 'closure';
190 | }
191 |
192 | if (is_object($controller)) {
193 | return str_replace('\\', '.', get_class($controller[0]));
194 | }
195 |
196 | return 'other';
197 | }
198 |
199 | /**
200 | * @return RateLimit[]
201 | */
202 | private function getRateLimitsFromAttributes(string|array|object $controller): array
203 | {
204 | $rClass = $rMethod = null;
205 | if (\is_array($controller) && method_exists(...$controller)) {
206 | $rClass = new \ReflectionClass($controller[0]);
207 | $rMethod = new \ReflectionMethod($controller[0], $controller[1]);
208 | } elseif (\is_string($controller) && false !== $i = strpos($controller, '::')) {
209 | $rClass = new \ReflectionClass(substr($controller, 0, $i));
210 | } elseif (\is_object($controller) && \is_callable([$controller, '__invoke'])) {
211 | $rMethod = new \ReflectionMethod($controller, '__invoke');
212 | } else {
213 | $rMethod = new \ReflectionFunction($controller);
214 | }
215 |
216 | $attributes = [];
217 | foreach (array_merge($rClass?->getAttributes() ?? [], $rMethod?->getAttributes() ?? []) as $attribute) {
218 | if (RateLimit::class === $attribute->getName()) {
219 | $attributes[] = $attribute->newInstance();
220 | }
221 | }
222 |
223 | return $attributes;
224 | }
225 | }
226 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | NoxlogicRateLimitBundle
2 | ========================
3 |
4 | [](https://travis-ci.org/jaytaph/RateLimitBundle)
5 | [](https://scrutinizer-ci.com/g/jaytaph/RateLimitBundle/?branch=master)
6 | [](https://scrutinizer-ci.com/g/jaytaph/RateLimitBundle/?branch=master)
7 |
8 | [](https://packagist.org/packages/noxlogic/ratelimit-bundle) [](https://packagist.org/packages/noxlogic/ratelimit-bundle) [](https://packagist.org/packages/noxlogic/ratelimit-bundle) [](https://packagist.org/packages/noxlogic/ratelimit-bundle)
9 |
10 | This bundle provides enables the `#[RateLimit()]` attribute which allows you to limit the number of connections to actions.
11 | This is mostly useful in APIs.
12 |
13 | The bundle is prepared to work by default in cooperation with the `FOSOAuthServerBundle`. It contains a listener that adds the OAuth token to the cache-key. However, you can create your own key generator to allow custom rate limiting based on the request. See *Create a custom key generator* below.
14 |
15 | This bundle is partially inspired by a GitHub gist from [Ruud Kamphuis](https://github.com/ruudk).
16 |
17 | ## Features
18 |
19 | * Simple usage through attributes
20 | * Customize rates per controller, action and even per HTTP method
21 | * Multiple storage backends: Redis, Memcached and Doctrine cache
22 |
23 | ## Installation
24 |
25 | Installation takes just few easy steps:
26 |
27 | ### Step 1: Install the bundle using composer
28 |
29 | If you're not yet familiar with Composer see http://getcomposer.org.
30 | Tell composer to download the bundle by running the command:
31 |
32 | ``` bash
33 | composer require noxlogic/ratelimit-bundle
34 | ```
35 |
36 | ### Step 2: Enable the bundle
37 |
38 | If you are using `symfony/flex` you can skip this step, the bundle will be enabled automatically,
39 | otherwise you need to enable the bundle by adding it to the `bundles.php` file of your project.
40 |
41 | ``` php
42 | ['all' => true],
47 | // ..
48 | ];
49 | ```
50 |
51 | ### Step 3: Install a storage engine
52 |
53 | #### Redis
54 |
55 | If you want to use Redis as your storage engine, you might want to install `SncRedisBundle`:
56 |
57 | * https://github.com/snc/SncRedisBundle
58 |
59 | #### Memcache
60 |
61 | If you want to use Memcache, you might want to install `LswMemcacheBundle`
62 |
63 | * https://github.com/LeaseWeb/LswMemcacheBundle
64 |
65 | #### Doctrine cache
66 |
67 | If you want to use Doctrine cache as your storage engine, you might want to install `DoctrineCacheBundle`:
68 |
69 | * https://github.com/doctrine/DoctrineCacheBundle
70 |
71 | Referer to their documentations for more details. You can change your storage engine with the `storage_engine` configuration parameter. See *Configuration reference*.
72 |
73 | ## Configuration
74 |
75 | ### Enable bundle only in production
76 |
77 | If you wish to enable the bundle only in production environment (so you can test without worrying about limit in your development environments), you can use the `enabled` configuration setting to enable/disable the bundle completely. It's enabled by default:
78 |
79 | ```yaml
80 | # config_dev.yml
81 | noxlogic_rate_limit:
82 | enabled: false
83 | ```
84 |
85 | ### Configuration reference
86 |
87 | This is the default bundle configuration:
88 |
89 | ```yaml
90 | noxlogic_rate_limit:
91 | enabled: true
92 |
93 | # The storage engine where all the rates will be stored
94 | storage_engine: ~ # One of "redis"; "memcache"; "doctrine"; "php_redis"; "php_redis_cluster"
95 |
96 | # The redis client to use for the redis storage engine
97 | redis_client: default_client
98 |
99 | # The Redis service, use this if you dont use SncRedisBundle and want to specify a service to use
100 | # Should be instance of \Predis\Client
101 | redis_service: null # Example: project.predis
102 |
103 | # The Redis client to use for the php_redis storage engine
104 | # Depending on storage_engine an instance of \Redis or \RedisCluster
105 | php_redis_service: null # Example: project.redis
106 |
107 | # The memcache client to use for the memcache storage engine
108 | memcache_client: default
109 |
110 | # The Memcached service, use this if you dont use LswMemcacheBundle and want to specify a service to use
111 | # Should be instance of \Memcached
112 | memcache_service: null # Example: project.memcached
113 |
114 | # The Doctrine Cache provider to use for the doctrine storage engine
115 | doctrine_provider: null # Example: my_apc_cache
116 |
117 | # The Doctrine Cache service, use this if you dont use DoctrineCacheBundle and want to specify a service to use
118 | # Should be an instance of \Doctrine\Common\Cache\Cache
119 | doctrine_service: null # Example: project.my_apc_cache
120 |
121 | # The HTTP status code to return when a client hits the rate limit
122 | rate_response_code: 429
123 |
124 | # Optional exception class that will be returned when a client hits the rate limit
125 | rate_response_exception: null
126 |
127 | # The HTTP message to return when a client hits the rate limit
128 | rate_response_message: 'You exceeded the rate limit'
129 |
130 | # Should the ratelimit headers be automatically added to the response?
131 | display_headers: true
132 |
133 | # What are the different header names to add
134 | headers:
135 | limit: X-RateLimit-Limit
136 | remaining: X-RateLimit-Remaining
137 | reset: X-RateLimit-Reset
138 |
139 | # Rate limits for paths
140 | path_limits:
141 | path: ~ # Required
142 | methods:
143 |
144 | # Default:
145 | - *
146 | limit: ~ # Required
147 | period: ~ # Required
148 |
149 | # - { path: /api, limit: 1000, period: 3600 }
150 | # - { path: /dashboard, limit: 100, period: 3600, methods: ['GET', 'POST']}
151 |
152 | # Should the FOS OAuthServerBundle listener be enabled
153 | fos_oauth_key_listener: true
154 | ```
155 |
156 |
157 | ## Usage
158 |
159 | ### Simple rate limiting
160 |
161 | To enable rate limiting, you only need to add the attribute to the specified action
162 |
163 | ```php
164 | generateKey();
247 |
248 | $event->addToKey($key);
249 | // $event->setKey($key); // to overwrite key completely
250 | }
251 | }
252 | ```
253 |
254 | Make sure to generate a key based on what is rate limited in your controllers.
255 |
256 | And example of a IP-based key generator can be:
257 |
258 | ```php
259 | getRequest();
270 | $event->addToKey($request->getClientIp());
271 | }
272 | }
273 | ```
274 |
275 |
276 | ## Throwing exceptions
277 |
278 | Instead of returning a Response object when a rate limit has exceeded, it's also possible to throw an exception. This
279 | allows you to easily handle the rate limit on another level, for instance by capturing the ``kernel.exception`` event.
280 |
281 |
282 | ## Running tests
283 |
284 | If you want to run the tests use:
285 |
286 | ```
287 | ./vendor/bin/simple-phpunit
288 | ```
289 |
--------------------------------------------------------------------------------
/Tests/Util/PathLimitProcessorTest.php:
--------------------------------------------------------------------------------
1 | getRateLimit(new Request());
21 |
22 | $this->assertNull($result);
23 | }
24 |
25 | /** @test */
26 | public function itReturnARateLimitIfItMatchesPathAndMethod(): void
27 | {
28 | $plp = new PathLimitProcessor(array(
29 | 'api' => array(
30 | 'path' => 'api/',
31 | 'methods' => array('GET'),
32 | 'limit' => 100,
33 | 'period' => 60
34 | )
35 | ));
36 |
37 | $result = $plp->getRateLimit(
38 | Request::create('/api/', 'GET')
39 | );
40 |
41 | $this->assertInstanceOf(
42 | 'Noxlogic\RateLimitBundle\Attribute\RateLimit',
43 | $result
44 | );
45 |
46 | $this->assertEquals(100, $result->getLimit());
47 | $this->assertEquals(60, $result->getPeriod());
48 | $this->assertEquals(array('GET'), $result->getMethods());
49 | }
50 |
51 | /** @test */
52 | public function itReturnARateLimitIfItMatchesSubPathWithUrlEncodedString()
53 | {
54 | $plp = new PathLimitProcessor(array(
55 | 'api' => array(
56 | 'path' => 'api',
57 | 'methods' => array('GET'),
58 | 'limit' => 100,
59 | 'period' => 60
60 | )
61 | ));
62 |
63 | $result = $plp->getRateLimit(
64 | Request::create('%2Fapi%2Fusers', 'GET')
65 | );
66 |
67 | $this->assertInstanceOf(
68 | 'Noxlogic\RateLimitBundle\Attribute\RateLimit',
69 | $result
70 | );
71 |
72 | $this->assertEquals(100, $result->getLimit());
73 | $this->assertEquals(60, $result->getPeriod());
74 | $this->assertEquals(array('GET'), $result->getMethods());
75 | }
76 |
77 | /** @test */
78 | public function itWorksWhenMultipleMethodsAreSpecified(): void
79 | {
80 | $plp = new PathLimitProcessor(array(
81 | 'api' => array(
82 | 'path' => 'api/',
83 | 'methods' => array('GET', 'POST'),
84 | 'limit' => 1000,
85 | 'period' => 600
86 | )
87 | ));
88 |
89 | $result = $plp->getRateLimit(
90 | Request::create('/api/', 'POST')
91 | );
92 |
93 | $this->assertEquals(1000, $result->getLimit());
94 | $this->assertEquals(600, $result->getPeriod());
95 | $this->assertEquals(array('GET', 'POST'), $result->getMethods());
96 | }
97 |
98 | /** @test */
99 | public function itReturnsTheCorrectRateLimitWithMultiplePathLimits(): void
100 | {
101 | $plp = new PathLimitProcessor(array(
102 | 'api' => array(
103 | 'path' => 'api/',
104 | 'methods' => array('GET', 'POST'),
105 | 'limit' => 1000,
106 | 'period' => 600
107 | ),
108 | 'api2' => array(
109 | 'path' => 'api2/',
110 | 'methods' => array('POST'),
111 | 'limit' => 20,
112 | 'period' => 15
113 | )
114 | ));
115 |
116 | $result = $plp->getRateLimit(
117 | Request::create('/api2/', 'POST')
118 | );
119 |
120 | $this->assertEquals(20, $result->getLimit());
121 | $this->assertEquals(15, $result->getPeriod());
122 | $this->assertEquals(array('POST'), $result->getMethods());
123 | }
124 |
125 | /** @test */
126 | public function itWorksWithLimitsOnSamePathButDifferentMethods(): void
127 | {
128 | $plp = new PathLimitProcessor(array(
129 | 'api_get' => array(
130 | 'path' => 'api/',
131 | 'methods' => array('GET'),
132 | 'limit' => 1000,
133 | 'period' => 600
134 | ),
135 | 'api_post' => array(
136 | 'path' => 'api/',
137 | 'methods' => array('POST'),
138 | 'limit' => 200,
139 | 'period' => 150
140 | )
141 | ));
142 |
143 | $result = $plp->getRateLimit(
144 | Request::create('/api/', 'POST')
145 | );
146 |
147 | $this->assertEquals(200, $result->getLimit());
148 | $this->assertEquals(150, $result->getPeriod());
149 | $this->assertEquals(array('POST'), $result->getMethods());
150 | }
151 |
152 | /** @test */
153 | public function itMatchesAstrixAsAnyMethod(): void
154 | {
155 | $plp = new PathLimitProcessor(array(
156 | 'api' => array(
157 | 'path' => 'api/',
158 | 'methods' => array('*'),
159 | 'limit' => 100,
160 | 'period' => 60
161 | )
162 | ));
163 |
164 | $result = $plp->getRateLimit(
165 | Request::create('/api/users/emails', 'GET')
166 | );
167 |
168 | $this->assertEquals(100, $result->getLimit());
169 | $this->assertEquals(60, $result->getPeriod());
170 | $this->assertEquals(array('*'), $result->getMethods());
171 |
172 | $result = $plp->getRateLimit(
173 | Request::create('/api/users/emails', 'PUT')
174 | );
175 |
176 | $this->assertEquals(100, $result->getLimit());
177 | $this->assertEquals(60, $result->getPeriod());
178 | $this->assertEquals(array('*'), $result->getMethods());
179 |
180 | $result = $plp->getRateLimit(
181 | Request::create('/api/users/emails', 'POST')
182 | );
183 |
184 | $this->assertEquals(100, $result->getLimit());
185 | $this->assertEquals(60, $result->getPeriod());
186 | $this->assertEquals(array('*'), $result->getMethods());
187 | }
188 |
189 | /** @test */
190 | function itMatchesAstrixAsAnyPath()
191 | {
192 | $plp = new PathLimitProcessor(array(
193 | 'api' => array(
194 | 'path' => '*',
195 | 'methods' => array('GET'),
196 | 'limit' => 100,
197 | 'period' => 60
198 | )
199 | ));
200 |
201 | $result = $plp->getRateLimit(Request::create('/api'));
202 |
203 | $this->assertEquals(100, $result->getLimit());
204 | $this->assertEquals(60, $result->getPeriod());
205 | $this->assertEquals(array('GET'), $result->getMethods());
206 |
207 | $result = $plp->getRateLimit(Request::create('/api/users'));
208 |
209 | $this->assertEquals(100, $result->getLimit());
210 | $this->assertEquals(60, $result->getPeriod());
211 | $this->assertEquals(array('GET'), $result->getMethods());
212 |
213 | $result = $plp->getRateLimit(Request::create('/api/users/emails'));
214 |
215 | $this->assertEquals(100, $result->getLimit());
216 | $this->assertEquals(60, $result->getPeriod());
217 | $this->assertEquals(array('GET'), $result->getMethods());
218 | }
219 |
220 | /** @test */
221 | public function itMatchesWhenAccessSubPaths(): void
222 | {
223 | $plp = new PathLimitProcessor(array(
224 | 'api' => array(
225 | 'path' => 'api/',
226 | 'methods' => array('GET'),
227 | 'limit' => 100,
228 | 'period' => 60
229 | )
230 | ));
231 |
232 | $result = $plp->getRateLimit(
233 | Request::create('/api/users/emails', 'GET')
234 | );
235 |
236 | $this->assertEquals(100, $result->getLimit());
237 | $this->assertEquals(60, $result->getPeriod());
238 | $this->assertEquals(array('GET'), $result->getMethods());
239 | }
240 |
241 | /** @test */
242 | public function itReturnsNullIfThereIsNoMatchingPath(): void
243 | {
244 | $plp = new PathLimitProcessor(array(
245 | 'api' => array(
246 | 'path' => 'api/users/emails',
247 | 'methods' => array('GET'),
248 | 'limit' => 100,
249 | 'period' => 60
250 | )
251 | ));
252 |
253 | $result = $plp->getRateLimit(
254 | Request::create('/api', 'GET')
255 | );
256 |
257 | $this->assertNull($result);
258 | }
259 |
260 | /** @test */
261 | public function itMatchesTheMostSpecificPathFirst(): void
262 | {
263 | $plp = new PathLimitProcessor(array(
264 | 'api' => array(
265 | 'path' => 'api',
266 | 'methods' => array('GET'),
267 | 'limit' => 5,
268 | 'period' => 1
269 | ),
270 | 'api_emails' => array(
271 | 'path' => 'api/users/emails',
272 | 'methods' => array('GET'),
273 | 'limit' => 100,
274 | 'period' => 60
275 | )
276 | ));
277 |
278 | $result = $plp->getRateLimit(
279 | Request::create('/api/users/emails', 'GET')
280 | );
281 |
282 | $this->assertEquals(100, $result->getLimit());
283 | $this->assertEquals(60, $result->getPeriod());
284 | $this->assertEquals(array('GET'), $result->getMethods());
285 | }
286 |
287 | /** @test */
288 | public function itReturnsTheMatchedPath(): void
289 | {
290 | $plp = new PathLimitProcessor(array(
291 | 'api' => array(
292 | 'path' => 'api/',
293 | 'methods' => array('GET', 'POST'),
294 | 'limit' => 1000,
295 | 'period' => 600
296 | )
297 | ));
298 |
299 | $path = $plp->getMatchedPath(
300 | Request::create('/api/', 'POST')
301 | );
302 |
303 | $this->assertEquals('api', $path);
304 | }
305 |
306 | /** @test */
307 | public function itReturnsTheCorrectPathForADifferentSetup(): void
308 | {
309 | $plp = new PathLimitProcessor(array(
310 | 'api' => array(
311 | 'path' => 'api',
312 | 'methods' => array('GET'),
313 | 'limit' => 5,
314 | 'period' => 1
315 | ),
316 | 'api_emails' => array(
317 | 'path' => 'api/users/emails',
318 | 'methods' => array('GET'),
319 | 'limit' => 100,
320 | 'period' => 60
321 | )
322 | ));
323 |
324 | $path = $plp->getMatchedPath(
325 | Request::create('/api/users/emails', 'GET')
326 | );
327 |
328 | $this->assertEquals('api/users/emails', $path);
329 | }
330 |
331 | /** @test */
332 | public function itReturnsTheCorrectMatchedPathForSubPaths(): void
333 | {
334 | $plp = new PathLimitProcessor(array(
335 | 'api' => array(
336 | 'path' => 'api/',
337 | 'methods' => array('GET'),
338 | 'limit' => 100,
339 | 'period' => 60
340 | )
341 | ));
342 |
343 | $path = $plp->getMatchedPath(
344 | Request::create('/api/users/emails', 'GET')
345 | );
346 |
347 | $this->assertEquals('api', $path);
348 | }
349 | }
350 |
--------------------------------------------------------------------------------
/Tests/EventListener/RateLimitAnnotationListenerTest.php:
--------------------------------------------------------------------------------
1 | mockStorage = new MockStorage();
30 | $this->mockPathLimitProcessor = $this->getMockBuilder('Noxlogic\RateLimitBundle\Util\PathLimitProcessor')
31 | ->disableOriginalConstructor()
32 | ->getMock();
33 | }
34 |
35 | protected function getMockStorage()
36 | {
37 | return $this->mockStorage;
38 | }
39 |
40 |
41 | public function testReturnedWhenNotEnabled(): void
42 | {
43 | $listener = $this->createListener($this->never());
44 | $listener->setParameter('enabled', false);
45 |
46 | $event = $this->createEvent();
47 | $listener->onKernelController($event);
48 | }
49 |
50 |
51 | public function testReturnedWhenNotAMasterRequest(): void
52 | {
53 | $listener = $this->createListener($this->never());
54 |
55 | $event = $this->createEvent(HttpKernelInterface::SUB_REQUEST);
56 | $listener->onKernelController($event);
57 | }
58 |
59 |
60 | public function testReturnedWhenNoControllerFound(): void
61 | {
62 | $listener = $this->createListener($this->once());
63 |
64 | $kernel = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\HttpKernelInterface')->getMock();
65 | $request = new Request();
66 |
67 | $event = new ControllerEvent($kernel, static function() {}, $request, HttpKernelInterface::MAIN_REQUEST);
68 |
69 | $listener->onKernelController($event);
70 | }
71 |
72 |
73 | public function testReturnedWhenNoAttributesFound(): void
74 | {
75 | $listener = $this->createListener($this->once());
76 |
77 | $event = $this->createEvent();
78 | $listener->onKernelController($event);
79 | }
80 |
81 | public function testDelegatesToPathLimitProcessorWhenNoAttributesFound(): void
82 | {
83 | $request = new Request();
84 | $event = $this->createEvent(HttpKernelInterface::MAIN_REQUEST, $request);
85 |
86 | $listener = $this->createListener($this->once());
87 |
88 | $this->mockPathLimitProcessor->expects($this->once())
89 | ->method('getRateLimit')
90 | ->with($request);
91 |
92 | $listener->onKernelController($event);
93 | }
94 |
95 | public function testDispatchIsCalled(): void
96 | {
97 | $listener = $this->createListener($this->exactly(2));
98 |
99 | $event = $this->createEvent();
100 | $event->getRequest()->attributes->set('_x-rate-limit', array(
101 | new RateLimit([], 100, 3600),
102 | ));
103 |
104 | $listener->onKernelController($event);
105 | }
106 |
107 | public function testDispatchIsCalledWithAttributes(): void
108 | {
109 | $listener = $this->createListener($this->exactly(2));
110 |
111 | $event = $this->createEvent(
112 | HttpKernelInterface::MAIN_REQUEST,
113 | null,
114 | new MockControllerWithAttributes()
115 | );
116 |
117 | $listener->onKernelController($event);
118 | }
119 |
120 | public function testDispatchIsCalledIfThePathLimitProcessorReturnsARateLimit(): void
121 | {
122 | $event = $this->createEvent(HttpKernelInterface::MAIN_REQUEST);
123 |
124 | $listener = $this->createListener($this->exactly(2));
125 | $rateLimit = new RateLimit(
126 | [],
127 | 100,
128 | 200
129 | );
130 |
131 | $this->mockPathLimitProcessor
132 | ->expects($this->any())
133 | ->method('getRateLimit')
134 | ->willReturn($rateLimit);
135 |
136 | $listener->onKernelController($event);
137 | }
138 |
139 | public function testIsRateLimitSetInRequest(): void
140 | {
141 | $listener = $this->createListener($this->any());
142 |
143 | $event = $this->createEvent();
144 | $event->getRequest()->attributes->set('_x-rate-limit', array(
145 | new RateLimit([], 5, 10),
146 | ));
147 |
148 |
149 | $this->assertNull($event->getRequest()->attributes->get('rate_limit_info'));
150 |
151 | // Create initial ratelimit in storage
152 | $listener->onKernelController($event);
153 | $this->assertArrayHasKey('rate_limit_info', $event->getRequest()->attributes->all());
154 |
155 | // Add second ratelimit in storage
156 | $listener->onKernelController($event);
157 | $this->assertArrayHasKey('rate_limit_info', $event->getRequest()->attributes->all());
158 | }
159 |
160 | public function testRateLimit(): void
161 | {
162 | $listener = $this->createListener($this->any());
163 |
164 | $event = $this->createEvent();
165 | $event->getRequest()->attributes->set('_x-rate-limit', array(
166 | new RateLimit([], 5, 5),
167 | ));
168 |
169 | $listener->onKernelController($event);
170 | self::assertIsArray($event->getController());
171 | $listener->onKernelController($event);
172 | self::assertIsArray( $event->getController());
173 | $listener->onKernelController($event);
174 | self::assertIsArray($event->getController());
175 | $listener->onKernelController($event);
176 | self::assertIsArray($event->getController());
177 | $listener->onKernelController($event);
178 | self::assertIsArray($event->getController());
179 | $listener->onKernelController($event);
180 | self::assertIsNotArray($event->getController());
181 | $listener->onKernelController($event);
182 | self::assertIsNotArray($event->getController());
183 | $listener->onKernelController($event);
184 | self::assertIsNotArray($event->getController());
185 | }
186 |
187 | public function testRateLimitThrottling(): void
188 | {
189 | $listener = $this->createListener($this->any());
190 |
191 | $event = $this->createEvent();
192 | $event->getRequest()->attributes->set('_x-rate-limit', array(
193 | new RateLimit([], 5, 3),
194 | ));
195 |
196 | // Throttled
197 | $storage = $this->getMockStorage();
198 | $storage->createMockRate('Noxlogic.RateLimitBundle.Tests.EventListener.MockController.mockAction', 5, 10, 6);
199 | $listener->onKernelController($event);
200 | self::assertIsNotArray($event->getController());
201 | }
202 |
203 | public function testRateLimitExpiring(): void
204 | {
205 | $listener = $this->createListener($this->any());
206 |
207 | $event = $this->createEvent();
208 | $event->getRequest()->attributes->set('_x-rate-limit', array(
209 | new RateLimit([], 5, 3),
210 | ));
211 |
212 | // Expired
213 | $storage = $this->getMockStorage();
214 | $storage->createMockRate('Noxlogic.RateLimitBundle.Tests.EventListener.MockController.mockAction', 5, -10, 12);
215 | $listener->onKernelController($event);
216 | self::assertIsArray($event->getController());
217 | }
218 |
219 | public function testBestMethodMatch(): void
220 | {
221 | $listener = $this->createListener($this->any());
222 | $method = new ReflectionMethod(get_class($listener), 'findBestMethodMatch');
223 | $method->setAccessible(true);
224 |
225 | $request = new Request();
226 |
227 | $annotations = array(
228 | new RateLimit([], 100, 3600),
229 | new RateLimit('GET', 100, 3600),
230 | new RateLimit(['POST', 'PUT'], 100, 3600),
231 | );
232 |
233 | // Find the method that matches the string
234 | $request->setMethod('GET');
235 | $this->assertEquals(
236 | $annotations[1],
237 | $method->invoke($listener, $request, $annotations)
238 | );
239 |
240 | // Method not found, use the default one
241 | $request->setMethod('DELETE');
242 | $this->assertEquals(
243 | $annotations[0],
244 | $method->invoke($listener, $request, $annotations)
245 | );
246 |
247 | // Find best match based in methods in array
248 | $request->setMethod('PUT');
249 | $this->assertEquals(
250 | $annotations[2],
251 | $method->invoke($listener, $request, $annotations)
252 | );
253 | }
254 |
255 |
256 | public function testFindNoAttributes(): void
257 | {
258 | $listener = $this->createListener($this->any());
259 | $method = new ReflectionMethod(get_class($listener), 'findBestMethodMatch');
260 | $method->setAccessible(true);
261 |
262 | $request = new Request();
263 |
264 | $annotations = array();
265 |
266 | $request->setMethod('PUT');
267 | $this->assertNull($method->invoke($listener, $request, $annotations));
268 |
269 | $request->setMethod('GET');
270 | $this->assertNull($method->invoke($listener, $request, $annotations));
271 | }
272 |
273 |
274 | public function testFindBestMethodMatchNotMatchingAnnotations(): void
275 | {
276 | $listener = $this->createListener($this->any());
277 | $method = new ReflectionMethod(get_class($listener), 'findBestMethodMatch');
278 | $method->setAccessible(true);
279 |
280 | $request = new Request();
281 |
282 | $annotations = array(
283 | new RateLimit('GET', 100, 3600),
284 | );
285 |
286 | $request->setMethod('PUT');
287 | $this->assertNull($method->invoke($listener, $request, $annotations));
288 |
289 | $request->setMethod('GET');
290 | $this->assertEquals(
291 | $annotations[0],
292 | $method->invoke($listener, $request, $annotations)
293 | );
294 | }
295 |
296 |
297 | public function testFindBestMethodMatchMatchingMultipleAnnotations(): void
298 | {
299 | $listener = $this->createListener($this->any());
300 | $method = new ReflectionMethod(get_class($listener), 'findBestMethodMatch');
301 | $method->setAccessible(true);
302 |
303 | $request = new Request();
304 |
305 | $annotations = array(
306 | new RateLimit('GET', 100, 3600),
307 | new RateLimit(['GET','PUT'], 200, 7200),
308 | );
309 |
310 | $request->setMethod('PUT');
311 | $this->assertEquals($annotations[1], $method->invoke($listener, $request, $annotations));
312 |
313 | $request->setMethod('GET');
314 | $this->assertEquals($annotations[1], $method->invoke($listener, $request, $annotations));
315 | }
316 |
317 | protected function createEvent(
318 | int $requestType = HttpKernelInterface::MAIN_REQUEST,
319 | ?Request $request = null,
320 | ?MockController $controller = null,
321 | ): ControllerEvent
322 | {
323 | $kernel = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\HttpKernelInterface')->getMock();
324 |
325 | $controller = $controller ?? new MockController();
326 | $action = 'mockAction';
327 |
328 | $request = $request ?? new Request();
329 |
330 | return new ControllerEvent($kernel, array($controller, $action), $request, $requestType);
331 | }
332 |
333 |
334 | protected function createListener($expects): RateLimitAnnotationListener
335 | {
336 | $mockDispatcher = $this->getMockBuilder('Symfony\\Contracts\\EventDispatcher\\EventDispatcherInterface')->getMock();
337 | $mockDispatcher
338 | ->expects($expects)
339 | ->method('dispatch');
340 |
341 | $rateLimitService = new RateLimitService();
342 | $rateLimitService->setStorage($this->getMockStorage());
343 |
344 | return new RateLimitAnnotationListener(
345 | $mockDispatcher,
346 | $rateLimitService,
347 | $this->mockPathLimitProcessor
348 | );
349 | }
350 |
351 | public function testRateLimitKeyGenerationEventHasPayload(): void
352 | {
353 | $event = $this->createEvent();
354 | $request = $event->getRequest();
355 | $request->attributes->set('_x-rate-limit', array(
356 | new RateLimit([], 5, 3, ['foo']),
357 | ));
358 |
359 | $generated = false;
360 | $mockDispatcher = $this->getMockBuilder('Symfony\\Contracts\\EventDispatcher\\EventDispatcherInterface')->getMock();
361 | $generatedCallback = function ($name, $event) use ($request, &$generated) {
362 | if ($name !== RateLimitEvents::GENERATE_KEY) {
363 | return;
364 | }
365 | $generated = true;
366 | $this->assertSame(RateLimitEvents::GENERATE_KEY, $name);
367 | $this->assertSame($request, $event->getRequest());
368 | $this->assertSame(['foo'], $event->getPayload());
369 | $this->assertSame('Noxlogic.RateLimitBundle.Tests.EventListener.MockController.mockAction', $event->getKey());
370 | };
371 | $mockDispatcher
372 | ->expects($this->any())
373 | ->method('dispatch')
374 | ->willReturnCallback(function ($arg1, $arg2) use ($generatedCallback) {
375 | if ($arg1 instanceof Event) {
376 | $generatedCallback($arg2, $arg1);
377 | return $arg1;
378 | } else {
379 | $generatedCallback($arg1, $arg2);
380 | return $arg2;
381 | }
382 | });
383 |
384 | $storage = $this->getMockStorage();
385 | $storage->createMockRate('test-key', 5, 10, 1);
386 |
387 | $rateLimitService = $this->getMockBuilder('Noxlogic\RateLimitBundle\Service\RateLimitService')
388 | ->getMock();
389 |
390 | $listener = new RateLimitAnnotationListener($mockDispatcher, $rateLimitService, $this->mockPathLimitProcessor);
391 | $listener->onKernelController($event);
392 |
393 | $this->assertTrue($generated, 'Generate key event not dispatched');
394 | }
395 |
396 | public function testRateLimitThrottlingWithExceptionAndPayload(): void
397 | {
398 | $listener = $this->createListener($this->any());
399 | $listener->setParameter('rate_response_exception', 'Noxlogic\RateLimitBundle\Tests\Exception\TestException');
400 | $listener->setParameter('rate_response_code', 123);
401 | $listener->setParameter('rate_response_message', 'a message');
402 |
403 | $event = $this->createEvent();
404 | $event->getRequest()->attributes->set('_x-rate-limit', array(
405 | new RateLimit([], 5, 3, ['foo']),
406 | ));
407 |
408 | // Throttled
409 | $storage = $this->getMockStorage();
410 | $storage->createMockRate('Noxlogic.RateLimitBundle.Tests.EventListener.MockController.mockAction', 5, 10, 6);
411 |
412 | try {
413 | $listener->onKernelController($event);
414 |
415 | $this->assertFalse(true, 'Exception not being thrown');
416 | } catch (\Exception $e) {
417 | $this->assertInstanceOf('Noxlogic\RateLimitBundle\Tests\Exception\TestException', $e);
418 | $this->assertSame(123, $e->getCode());
419 | $this->assertSame('a message', $e->getMessage());
420 | $this->assertSame(['foo'], $e->payload);
421 | }
422 | }
423 |
424 | public function testRateLimitThrottlingWithException(): void
425 | {
426 | $this->expectException(\BadFunctionCallException::class);
427 | $this->expectExceptionCode(123);
428 | $this->expectExceptionMessage('a message');
429 | $listener = $this->createListener($this->any());
430 | $listener->setParameter('rate_response_exception', '\BadFunctionCallException');
431 | $listener->setParameter('rate_response_code', 123);
432 | $listener->setParameter('rate_response_message', 'a message');
433 |
434 | $event = $this->createEvent();
435 | $event->getRequest()->attributes->set('_x-rate-limit', array(
436 | new RateLimit([], 5, 3),
437 | ));
438 |
439 | // Throttled
440 | $storage = $this->getMockStorage();
441 | $storage->createMockRate('Noxlogic.RateLimitBundle.Tests.EventListener.MockController.mockAction', 5, 10, 6);
442 | $listener->onKernelController($event);
443 | }
444 |
445 | public function testRateLimitThrottlingWithMessages(): void
446 | {
447 | $listener = $this->createListener($this->any());
448 | $listener->setParameter('rate_response_code', 123);
449 | $listener->setParameter('rate_response_message', 'a message');
450 |
451 | $event = $this->createEvent();
452 | $event->getRequest()->attributes->set('_x-rate-limit', array(
453 | new RateLimit([], 5, 3),
454 | ));
455 |
456 | // Throttled
457 | $storage = $this->getMockStorage();
458 | $storage->createMockRate('Noxlogic.RateLimitBundle.Tests.EventListener.MockController.mockAction', 5, 10, 6);
459 |
460 | /** @var Response $response */
461 | $listener->onKernelController($event);
462 |
463 | // Call the controller, it will return a response object
464 | $a = $event->getController();
465 | $response = $a();
466 |
467 | $this->assertEquals($response->getStatusCode(), 123);
468 | $this->assertEquals($response->getContent(), "a message");
469 | }
470 | }
471 |
--------------------------------------------------------------------------------
/phpstan-baseline.neon:
--------------------------------------------------------------------------------
1 | parameters:
2 | ignoreErrors:
3 | -
4 | message: '#^Method Noxlogic\\RateLimitBundle\\Attribute\\RateLimit\:\:__construct\(\) has parameter \$methods with no type specified\.$#'
5 | identifier: missingType.parameter
6 | count: 1
7 | path: Attribute/RateLimit.php
8 |
9 | -
10 | message: '#^Method Noxlogic\\RateLimitBundle\\Attribute\\RateLimit\:\:getMethods\(\) return type has no value type specified in iterable type array\.$#'
11 | identifier: missingType.iterableValue
12 | count: 1
13 | path: Attribute/RateLimit.php
14 |
15 | -
16 | message: '#^Method Noxlogic\\RateLimitBundle\\Attribute\\RateLimit\:\:setMethods\(\) has parameter \$methods with no type specified\.$#'
17 | identifier: missingType.parameter
18 | count: 1
19 | path: Attribute/RateLimit.php
20 |
21 | -
22 | message: '#^Property Noxlogic\\RateLimitBundle\\Attribute\\RateLimit\:\:\$methods type has no value type specified in iterable type array\.$#'
23 | identifier: missingType.iterableValue
24 | count: 1
25 | path: Attribute/RateLimit.php
26 |
27 | -
28 | message: '#^Method Noxlogic\\RateLimitBundle\\DependencyInjection\\NoxlogicRateLimitExtension\:\:loadServices\(\) has parameter \$config with no value type specified in iterable type array\.$#'
29 | identifier: missingType.iterableValue
30 | count: 1
31 | path: DependencyInjection/NoxlogicRateLimitExtension.php
32 |
33 | -
34 | message: '#^Method Noxlogic\\RateLimitBundle\\EventListener\\BaseListener\:\:getParameter\(\) has parameter \$name with no type specified\.$#'
35 | identifier: missingType.parameter
36 | count: 1
37 | path: EventListener/BaseListener.php
38 |
39 | -
40 | message: '#^Method Noxlogic\\RateLimitBundle\\EventListener\\BaseListener\:\:setParameter\(\) has no return type specified\.$#'
41 | identifier: missingType.return
42 | count: 1
43 | path: EventListener/BaseListener.php
44 |
45 | -
46 | message: '#^Method Noxlogic\\RateLimitBundle\\EventListener\\BaseListener\:\:setParameter\(\) has parameter \$name with no type specified\.$#'
47 | identifier: missingType.parameter
48 | count: 1
49 | path: EventListener/BaseListener.php
50 |
51 | -
52 | message: '#^Method Noxlogic\\RateLimitBundle\\EventListener\\BaseListener\:\:setParameter\(\) has parameter \$value with no type specified\.$#'
53 | identifier: missingType.parameter
54 | count: 1
55 | path: EventListener/BaseListener.php
56 |
57 | -
58 | message: '#^Property Noxlogic\\RateLimitBundle\\EventListener\\BaseListener\:\:\$parameters type has no value type specified in iterable type array\.$#'
59 | identifier: missingType.iterableValue
60 | count: 1
61 | path: EventListener/BaseListener.php
62 |
63 | -
64 | message: '#^Method Noxlogic\\RateLimitBundle\\EventListener\\HeaderModificationListener\:\:__construct\(\) has parameter \$defaultParameters with no value type specified in iterable type array\.$#'
65 | identifier: missingType.iterableValue
66 | count: 1
67 | path: EventListener/HeaderModificationListener.php
68 |
69 | -
70 | message: '#^Method Noxlogic\\RateLimitBundle\\EventListener\\HeaderModificationListener\:\:onKernelResponse\(\) has no return type specified\.$#'
71 | identifier: missingType.return
72 | count: 1
73 | path: EventListener/HeaderModificationListener.php
74 |
75 | -
76 | message: '#^Method Noxlogic\\RateLimitBundle\\EventListener\\RateLimitAnnotationListener\:\:getRateLimitsFromAttributes\(\) has parameter \$controller with no value type specified in iterable type array\.$#'
77 | identifier: missingType.iterableValue
78 | count: 1
79 | path: EventListener/RateLimitAnnotationListener.php
80 |
81 | -
82 | message: '#^Method Noxlogic\\RateLimitBundle\\Events\\GenerateKeyEvent\:\:__construct\(\) has parameter \$key with no type specified\.$#'
83 | identifier: missingType.parameter
84 | count: 1
85 | path: Events/GenerateKeyEvent.php
86 |
87 | -
88 | message: '#^Method Noxlogic\\RateLimitBundle\\Events\\GenerateKeyEvent\:\:__construct\(\) has parameter \$payload with no type specified\.$#'
89 | identifier: missingType.parameter
90 | count: 1
91 | path: Events/GenerateKeyEvent.php
92 |
93 | -
94 | message: '#^Method Noxlogic\\RateLimitBundle\\Events\\GenerateKeyEvent\:\:addToKey\(\) has no return type specified\.$#'
95 | identifier: missingType.return
96 | count: 1
97 | path: Events/GenerateKeyEvent.php
98 |
99 | -
100 | message: '#^Method Noxlogic\\RateLimitBundle\\Events\\GenerateKeyEvent\:\:addToKey\(\) has parameter \$part with no type specified\.$#'
101 | identifier: missingType.parameter
102 | count: 1
103 | path: Events/GenerateKeyEvent.php
104 |
105 | -
106 | message: '#^Method Noxlogic\\RateLimitBundle\\Events\\GenerateKeyEvent\:\:setKey\(\) has no return type specified\.$#'
107 | identifier: missingType.return
108 | count: 1
109 | path: Events/GenerateKeyEvent.php
110 |
111 | -
112 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\RateLimitInfo\:\:setCalls\(\) has no return type specified\.$#'
113 | identifier: missingType.return
114 | count: 1
115 | path: Service/RateLimitInfo.php
116 |
117 | -
118 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\RateLimitInfo\:\:setLimit\(\) has no return type specified\.$#'
119 | identifier: missingType.return
120 | count: 1
121 | path: Service/RateLimitInfo.php
122 |
123 | -
124 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\RateLimitInfo\:\:setResetTimestamp\(\) has no return type specified\.$#'
125 | identifier: missingType.return
126 | count: 1
127 | path: Service/RateLimitInfo.php
128 |
129 | -
130 | message: '#^Property Noxlogic\\RateLimitBundle\\Service\\RateLimitInfo\:\:\$calls has no type specified\.$#'
131 | identifier: missingType.property
132 | count: 1
133 | path: Service/RateLimitInfo.php
134 |
135 | -
136 | message: '#^Property Noxlogic\\RateLimitBundle\\Service\\RateLimitInfo\:\:\$limit has no type specified\.$#'
137 | identifier: missingType.property
138 | count: 1
139 | path: Service/RateLimitInfo.php
140 |
141 | -
142 | message: '#^Property Noxlogic\\RateLimitBundle\\Service\\RateLimitInfo\:\:\$resetTimestamp has no type specified\.$#'
143 | identifier: missingType.property
144 | count: 1
145 | path: Service/RateLimitInfo.php
146 |
147 | -
148 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\RateLimitService\:\:createRate\(\) has no return type specified\.$#'
149 | identifier: missingType.return
150 | count: 1
151 | path: Service/RateLimitService.php
152 |
153 | -
154 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\RateLimitService\:\:createRate\(\) has parameter \$key with no type specified\.$#'
155 | identifier: missingType.parameter
156 | count: 1
157 | path: Service/RateLimitService.php
158 |
159 | -
160 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\RateLimitService\:\:createRate\(\) has parameter \$limit with no type specified\.$#'
161 | identifier: missingType.parameter
162 | count: 1
163 | path: Service/RateLimitService.php
164 |
165 | -
166 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\RateLimitService\:\:createRate\(\) has parameter \$period with no type specified\.$#'
167 | identifier: missingType.parameter
168 | count: 1
169 | path: Service/RateLimitService.php
170 |
171 | -
172 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\RateLimitService\:\:limitRate\(\) has no return type specified\.$#'
173 | identifier: missingType.return
174 | count: 1
175 | path: Service/RateLimitService.php
176 |
177 | -
178 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\RateLimitService\:\:limitRate\(\) has parameter \$key with no type specified\.$#'
179 | identifier: missingType.parameter
180 | count: 1
181 | path: Service/RateLimitService.php
182 |
183 | -
184 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\RateLimitService\:\:resetRate\(\) has no return type specified\.$#'
185 | identifier: missingType.return
186 | count: 1
187 | path: Service/RateLimitService.php
188 |
189 | -
190 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\RateLimitService\:\:resetRate\(\) has parameter \$key with no type specified\.$#'
191 | identifier: missingType.parameter
192 | count: 1
193 | path: Service/RateLimitService.php
194 |
195 | -
196 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\RateLimitService\:\:setStorage\(\) has no return type specified\.$#'
197 | identifier: missingType.return
198 | count: 1
199 | path: Service/RateLimitService.php
200 |
201 | -
202 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\DoctrineCache\:\:createRate\(\) has no return type specified\.$#'
203 | identifier: missingType.return
204 | count: 1
205 | path: Service/Storage/DoctrineCache.php
206 |
207 | -
208 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\DoctrineCache\:\:createRateInfo\(\) has no return type specified\.$#'
209 | identifier: missingType.return
210 | count: 1
211 | path: Service/Storage/DoctrineCache.php
212 |
213 | -
214 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\DoctrineCache\:\:createRateInfo\(\) has parameter \$info with no value type specified in iterable type array\.$#'
215 | identifier: missingType.iterableValue
216 | count: 1
217 | path: Service/Storage/DoctrineCache.php
218 |
219 | -
220 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\DoctrineCache\:\:resetRate\(\) has no return type specified\.$#'
221 | identifier: missingType.return
222 | count: 1
223 | path: Service/Storage/DoctrineCache.php
224 |
225 | -
226 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\DoctrineCache\:\:resetRate\(\) has parameter \$key with no type specified\.$#'
227 | identifier: missingType.parameter
228 | count: 1
229 | path: Service/Storage/DoctrineCache.php
230 |
231 | -
232 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\Memcache\:\:createRate\(\) has no return type specified\.$#'
233 | identifier: missingType.return
234 | count: 1
235 | path: Service/Storage/Memcache.php
236 |
237 | -
238 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\Memcache\:\:createRateInfo\(\) has no return type specified\.$#'
239 | identifier: missingType.return
240 | count: 1
241 | path: Service/Storage/Memcache.php
242 |
243 | -
244 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\Memcache\:\:createRateInfo\(\) has parameter \$info with no value type specified in iterable type array\.$#'
245 | identifier: missingType.iterableValue
246 | count: 1
247 | path: Service/Storage/Memcache.php
248 |
249 | -
250 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\Memcache\:\:resetRate\(\) has no return type specified\.$#'
251 | identifier: missingType.return
252 | count: 1
253 | path: Service/Storage/Memcache.php
254 |
255 | -
256 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\Memcache\:\:resetRate\(\) has parameter \$key with no type specified\.$#'
257 | identifier: missingType.parameter
258 | count: 1
259 | path: Service/Storage/Memcache.php
260 |
261 | -
262 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\PhpRedis\:\:createRate\(\) has no return type specified\.$#'
263 | identifier: missingType.return
264 | count: 1
265 | path: Service/Storage/PhpRedis.php
266 |
267 | -
268 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\PhpRedis\:\:resetRate\(\) has no return type specified\.$#'
269 | identifier: missingType.return
270 | count: 1
271 | path: Service/Storage/PhpRedis.php
272 |
273 | -
274 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\PhpRedis\:\:resetRate\(\) has parameter \$key with no type specified\.$#'
275 | identifier: missingType.parameter
276 | count: 1
277 | path: Service/Storage/PhpRedis.php
278 |
279 | -
280 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\PhpRedis\:\:sanitizeRedisKey\(\) has parameter \$key with no type specified\.$#'
281 | identifier: missingType.parameter
282 | count: 1
283 | path: Service/Storage/PhpRedis.php
284 |
285 | -
286 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\PsrCache\:\:createRate\(\) has no return type specified\.$#'
287 | identifier: missingType.return
288 | count: 1
289 | path: Service/Storage/PsrCache.php
290 |
291 | -
292 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\PsrCache\:\:createRateInfo\(\) has no return type specified\.$#'
293 | identifier: missingType.return
294 | count: 1
295 | path: Service/Storage/PsrCache.php
296 |
297 | -
298 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\PsrCache\:\:createRateInfo\(\) has parameter \$info with no value type specified in iterable type array\.$#'
299 | identifier: missingType.iterableValue
300 | count: 1
301 | path: Service/Storage/PsrCache.php
302 |
303 | -
304 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\PsrCache\:\:resetRate\(\) has no return type specified\.$#'
305 | identifier: missingType.return
306 | count: 1
307 | path: Service/Storage/PsrCache.php
308 |
309 | -
310 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\PsrCache\:\:resetRate\(\) has parameter \$key with no type specified\.$#'
311 | identifier: missingType.parameter
312 | count: 1
313 | path: Service/Storage/PsrCache.php
314 |
315 | -
316 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\Redis\:\:createRate\(\) has no return type specified\.$#'
317 | identifier: missingType.return
318 | count: 1
319 | path: Service/Storage/Redis.php
320 |
321 | -
322 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\Redis\:\:resetRate\(\) has no return type specified\.$#'
323 | identifier: missingType.return
324 | count: 1
325 | path: Service/Storage/Redis.php
326 |
327 | -
328 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\Redis\:\:resetRate\(\) has parameter \$key with no type specified\.$#'
329 | identifier: missingType.parameter
330 | count: 1
331 | path: Service/Storage/Redis.php
332 |
333 | -
334 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\Redis\:\:sanitizeRedisKey\(\) has parameter \$key with no type specified\.$#'
335 | identifier: missingType.parameter
336 | count: 1
337 | path: Service/Storage/Redis.php
338 |
339 | -
340 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\SimpleCache\:\:createRate\(\) has no return type specified\.$#'
341 | identifier: missingType.return
342 | count: 1
343 | path: Service/Storage/SimpleCache.php
344 |
345 | -
346 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\SimpleCache\:\:createRateInfo\(\) has no return type specified\.$#'
347 | identifier: missingType.return
348 | count: 1
349 | path: Service/Storage/SimpleCache.php
350 |
351 | -
352 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\SimpleCache\:\:createRateInfo\(\) has parameter \$info with no value type specified in iterable type array\.$#'
353 | identifier: missingType.iterableValue
354 | count: 1
355 | path: Service/Storage/SimpleCache.php
356 |
357 | -
358 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\SimpleCache\:\:resetRate\(\) has no return type specified\.$#'
359 | identifier: missingType.return
360 | count: 1
361 | path: Service/Storage/SimpleCache.php
362 |
363 | -
364 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\SimpleCache\:\:resetRate\(\) has parameter \$key with no type specified\.$#'
365 | identifier: missingType.parameter
366 | count: 1
367 | path: Service/Storage/SimpleCache.php
368 |
369 | -
370 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\StorageInterface\:\:createRate\(\) has no return type specified\.$#'
371 | identifier: missingType.return
372 | count: 1
373 | path: Service/Storage/StorageInterface.php
374 |
375 | -
376 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\StorageInterface\:\:resetRate\(\) has no return type specified\.$#'
377 | identifier: missingType.return
378 | count: 1
379 | path: Service/Storage/StorageInterface.php
380 |
381 | -
382 | message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\StorageInterface\:\:resetRate\(\) has parameter \$key with no type specified\.$#'
383 | identifier: missingType.parameter
384 | count: 1
385 | path: Service/Storage/StorageInterface.php
386 |
387 | -
388 | message: '#^Method Noxlogic\\RateLimitBundle\\Util\\PathLimitProcessor\:\:methodMatched\(\) has parameter \$expectedMethods with no value type specified in iterable type array\.$#'
389 | identifier: missingType.iterableValue
390 | count: 1
391 | path: Util/PathLimitProcessor.php
392 |
393 | -
394 | message: '#^Method Noxlogic\\RateLimitBundle\\Util\\PathLimitProcessor\:\:methodMatched\(\) has parameter \$method with no type specified\.$#'
395 | identifier: missingType.parameter
396 | count: 1
397 | path: Util/PathLimitProcessor.php
398 |
399 | -
400 | message: '#^Method Noxlogic\\RateLimitBundle\\Util\\PathLimitProcessor\:\:pathMatched\(\) has parameter \$expectedPath with no type specified\.$#'
401 | identifier: missingType.parameter
402 | count: 1
403 | path: Util/PathLimitProcessor.php
404 |
405 | -
406 | message: '#^Method Noxlogic\\RateLimitBundle\\Util\\PathLimitProcessor\:\:pathMatched\(\) has parameter \$path with no type specified\.$#'
407 | identifier: missingType.parameter
408 | count: 1
409 | path: Util/PathLimitProcessor.php
410 |
411 | -
412 | message: '#^Method Noxlogic\\RateLimitBundle\\Util\\PathLimitProcessor\:\:requestMatched\(\) has parameter \$method with no type specified\.$#'
413 | identifier: missingType.parameter
414 | count: 1
415 | path: Util/PathLimitProcessor.php
416 |
417 | -
418 | message: '#^Method Noxlogic\\RateLimitBundle\\Util\\PathLimitProcessor\:\:requestMatched\(\) has parameter \$path with no type specified\.$#'
419 | identifier: missingType.parameter
420 | count: 1
421 | path: Util/PathLimitProcessor.php
422 |
423 | -
424 | message: '#^Method Noxlogic\\RateLimitBundle\\Util\\PathLimitProcessor\:\:requestMatched\(\) has parameter \$pathLimit with no type specified\.$#'
425 | identifier: missingType.parameter
426 | count: 1
427 | path: Util/PathLimitProcessor.php
428 |
--------------------------------------------------------------------------------