Skip to content

Commit 12b93aa

Browse files
authored
[WIP] - Initial commit from internal component (#1)
1 parent 2b88f8d commit 12b93aa

33 files changed

Lines changed: 1717 additions & 0 deletions

.editorconfig

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
root = true
2+
3+
[*]
4+
indent_style = space
5+
indent_size = 4
6+
end_of_line = lf
7+
charset = utf-8
8+
trim_trailing_whitespace = true
9+
insert_final_newline = true
10+
11+
[Makefile]
12+
indent_style = tab
13+
14+
[*.md]
15+
trim_trailing_whitespace = false

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/vendor/
2+
/bin/
3+
composer.lock
4+
.phpunit.result.cache
5+
.php_cs.cache

.php_cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
$config = new M6Web\CS\Config\Php74();
4+
5+
$config->getFinder()
6+
->in([
7+
__DIR__.'/src',
8+
__DIR__.'/tests'
9+
]);
10+
11+
return $config;

Makefile

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
SHELL=bash -o pipefail
2+
SOURCE_DIR = $(shell pwd)
3+
BIN_DIR = ${SOURCE_DIR}/bin
4+
COMPOSER = composer
5+
6+
define printSection
7+
@printf "\033[36m\n==================================================\n\033[0m"
8+
@printf "\033[36m $1 \033[0m"
9+
@printf "\033[36m\n==================================================\n\033[0m"
10+
endef
11+
12+
.PHONY: all
13+
all: install quality test
14+
15+
.PHONY: ci
16+
ci: quality test
17+
18+
.PHONY: install
19+
install: clean-vendor composer-install
20+
21+
.PHONY: quality
22+
quality: cs-ci phpstan
23+
24+
.PHONY: test
25+
test: phpunit
26+
27+
### DEPENDENCIES ###
28+
29+
.PHONY: clean-vendor
30+
clean-vendor:
31+
$(call printSection,DEPENDENCIES clean)
32+
rm -rf ${SOURCE_DIR}/vendor
33+
34+
.PHONY: composer-install
35+
composer-install: ${SOURCE_DIR}/vendor/composer/installed.json
36+
37+
${SOURCE_DIR}/vendor/composer/installed.json:
38+
$(call printSection,DEPENDENCIES install)
39+
$(COMPOSER) --no-interaction install --ansi --no-progress --prefer-dist
40+
41+
### TEST ###
42+
43+
.PHONY: phpunit
44+
phpunit:
45+
$(call printSection,TEST phpunit)
46+
${BIN_DIR}/phpunit
47+
48+
### QUALITY ###
49+
50+
.PHONY: phpstan
51+
phpstan:
52+
$(call printSection,QUALITY phpstan)
53+
${BIN_DIR}/phpstan analyse --memory-limit=1G
54+
55+
.PHONY: cs-ci
56+
cs-ci:
57+
$(call printSection,QUALITY php-cs-fixer check)
58+
${BIN_DIR}/php-cs-fixer fix --ansi --dry-run --using-cache=no --verbose
59+
60+
.PHONY: cs-fix
61+
cs-fix:
62+
$(call printSection,QUALITY php-cs-fixer fix)
63+
${BIN_DIR}/php-cs-fixer fix
64+

README.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# RateLimitBundle
2+
This bundle provides an easy way to protect your project by limiting access to your controllers.
3+
4+
## Install the bundle
5+
```bash
6+
composer require bedrock/rate-limit-bundle
7+
```
8+
9+
Update your _config/bundles.php_ file to add the bundle for all env
10+
```php
11+
<?php
12+
13+
return [
14+
...
15+
Bedrock\Bundle\RateLimitBundle\RateLimitBundle::class => ['all' => true],
16+
...
17+
];
18+
```
19+
20+
### Configure the bundle
21+
Add the _config/packages/bedrock_rate_limit.yaml_ file with the following data.
22+
```yaml
23+
bedrock_rate_limit:
24+
limit: 25 # 1000 requests by default
25+
period: 600 # 60 seconds by default
26+
limit_by_route: true|false # false by default
27+
display_headers: true|false # false by default
28+
```
29+
By default, the limitation is common to all routes annotated `@RateLimit()`.
30+
For example, if you keep the default configuration and you configure the `@RateLimit()` annotation in 2 routes. Limit will shared between this 2 routes, if user consume all authorized calls on the first route, the second route couldn't be called.
31+
If you swicth `limit_by_route` to true, users will be allowed to reach the limit on each route annotated.
32+
33+
If you switch `display_headers` to true, 3 headers will be added `x-rate-limit`, `x-rate-limit-hits`, `x-rate-limit-untils` to your responses. This can be usefull to debug your limitations.
34+
`display_headers` is used to display a verbose return if limit is reached.
35+
36+
### Configure your storage
37+
You must tell Symfony which storage implementation you want to use.
38+
39+
Update your _config/services.yml_ like this:
40+
41+
```yaml
42+
...
43+
Bedrock\Bundle\RateLimitBundle\Storage\RateLimitStorageInterface: '@Bedrock\Bundle\RateLimitBundle\Storage\RateLimitInMemoryStorage'
44+
...
45+
```
46+
47+
By default, only `RateLimitInMemory` is provided. But feel free to create your own by implementing `RateLimitStorageInterface` or `ManuallyResetableRateLimitStorageInterface`.
48+
If your database has a TTL system (like Redis), you can implement only `RateLimitStorageInterface`. Otherwhise you must implement also `ManuallyResetableRateLimitStorageInterface` to manually delete rate limit in your database.
49+
50+
### Configure your modifiers
51+
Modifiers are a way to customize the rate limit.
52+
53+
This bundle provides 2 modifiers:
54+
* `HttpMethodRateLimitModifier` limits the requests by `http_method`.
55+
* `RequestAttributeRateLimitModifier` limits the requests by attributes value (taken from the `$request->attributes` Symfony's bag).
56+
57+
Update your _config/services.yml_ like this:
58+
59+
```yaml
60+
...
61+
Bedrock\Bundle\RateLimitBundle\RateLimitModifier\HttpMethodRateLimitModifier:
62+
tags: [ 'rate_limit.modifiers' ]
63+
64+
Bedrock\Bundle\RateLimitBundle\RateLimitModifier\RequestAttributeRateLimitModifier:
65+
arguments:
66+
$attributeName: 'myRequestAttribute'
67+
tags: [ 'rate_limit.modifiers' ]
68+
69+
...
70+
```
71+
72+
You can also create your own rate limit modifier by implementing `RateLimitModifierInterface` and tagging your service accordingly.
73+
74+
### Configure your routes
75+
Add the `@RateLimit()` annotation to your controller methods (by default, the limit will be 1000 requests per minute).
76+
This annotation accepts parameters to customize the rate limit. The following example shows how to limit requests on a route at the rate of 10 requests max every 2 minutes.
77+
:warning: This customization only works if the `limit_by_route` parameter is `true`
78+
79+
```php
80+
/**
81+
* @RateLimit(
82+
* limit=10,
83+
* period=120
84+
* )
85+
*/
86+
```

composer.json

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"name": "bedrock/rate-limit-bundle",
3+
"type": "symfony-bundle",
4+
"license": "MIT",
5+
"authors": [
6+
{
7+
"name": "Bedrock",
8+
"email": "opensource@bedrockstreaming.com",
9+
"homepage": "https://tech.bedrockstreaming.com/"
10+
}
11+
],
12+
"config": {
13+
"bin-dir": "bin",
14+
"vendor-dir": "vendor",
15+
"sort-packages": true
16+
},
17+
"require": {
18+
"php": "7.4.*",
19+
"ext-json": "*",
20+
"doctrine/annotations": "^1.10.0",
21+
"symfony/dependency-injection": "4.4.*",
22+
"symfony/event-dispatcher": "4.4.*",
23+
"symfony/http-foundation": "4.4.*",
24+
"symfony/http-kernel": "4.4.*",
25+
"symfony/config": "4.4.*"
26+
},
27+
"require-dev": {
28+
"phpunit/phpunit": "9.4.*",
29+
"m6web/php-cs-fixer-config": "1.3.*",
30+
"phpstan/phpstan": "0.12.*",
31+
"phpstan/phpstan-phpunit": "0.12.*",
32+
"symfony/var-dumper": "4.4.*"
33+
},
34+
"autoload": {
35+
"psr-4": {
36+
"Bedrock\\Bundle\\RateLimitBundle\\": "src/"
37+
}
38+
},
39+
"autoload-dev": {
40+
"psr-4": {
41+
"Bedrock\\Bundle\\RateLimitBundle\\Tests\\": "tests/"
42+
}
43+
}
44+
}

phpstan.neon.dist

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
includes:
2+
- 'vendor/phpstan/phpstan-phpunit/extension.neon'
3+
- 'vendor/phpstan/phpstan-phpunit/rules.neon'
4+
5+
parameters:
6+
paths:
7+
- 'src'
8+
- 'tests'
9+
level: 'max'
10+
checkGenericClassInNonGenericObjectType: false
11+
ignoreErrors:
12+
- '#Cannot call method integerNode\(\) on Symfony\\Component\\Config\\Definition\\Builder\\NodeParentInterface\|null\.#'

phpunit.xml.dist

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
3+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" backupGlobals="false" colors="true" bootstrap="vendor/autoload.php">
4+
<coverage>
5+
<include>
6+
<directory>src</directory>
7+
</include>
8+
</coverage>
9+
<php>
10+
<ini name="error_reporting" value="-1"/>
11+
<server name="SHELL_VERBOSITY" value="-1"/>
12+
<env name="APP_ENV" value="test" />
13+
</php>
14+
<testsuites>
15+
<testsuite name="Project Test Suite">
16+
<directory>tests</directory>
17+
</testsuite>
18+
</testsuites>
19+
</phpunit>

src/Annotation/RateLimit.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Bedrock\Bundle\RateLimitBundle\Annotation;
6+
7+
/**
8+
* @Annotation
9+
* @Target({"METHOD"})
10+
*/
11+
final class RateLimit
12+
{
13+
private ?int $limit;
14+
private ?int $period;
15+
16+
/**
17+
* @param array<string, int> $args
18+
*/
19+
public function __construct(array $args = [])
20+
{
21+
$this->limit = $args['limit'] ?? null;
22+
$this->period = $args['period'] ?? null;
23+
}
24+
25+
public function getLimit(): ?int
26+
{
27+
return $this->limit;
28+
}
29+
30+
public function setLimit(int $limit): RateLimit
31+
{
32+
$this->limit = $limit;
33+
34+
return $this;
35+
}
36+
37+
public function getPeriod(): ?int
38+
{
39+
return $this->period;
40+
}
41+
42+
public function setPeriod(int $period): RateLimit
43+
{
44+
$this->period = $period;
45+
46+
return $this;
47+
}
48+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Bedrock\Bundle\RateLimitBundle\DependencyInjection;
6+
7+
use Symfony\Component\Config\FileLocator;
8+
use Symfony\Component\DependencyInjection\ContainerBuilder;
9+
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
10+
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
11+
12+
class BedrockRateLimitExtension extends Extension
13+
{
14+
/**
15+
* Override conf key for this bundle
16+
*/
17+
public function getAlias(): string
18+
{
19+
return 'bedrock_rate_limit';
20+
}
21+
22+
/**
23+
* @param array<mixed> $configs
24+
*/
25+
public function load(array $configs, ContainerBuilder $container): void
26+
{
27+
$configuration = new Configuration();
28+
$config = $this->processConfiguration($configuration, $configs);
29+
30+
$container->setParameter('bedrock_rate_limit.limit', $config['limit']);
31+
$container->setParameter('bedrock_rate_limit.period', $config['period']);
32+
$container->setParameter('bedrock_rate_limit.limit_by_route', $config['limit_by_route']);
33+
$container->setParameter('bedrock_rate_limit.display_headers', $config['display_headers']);
34+
35+
$loader = new YamlFileLoader(
36+
$container,
37+
new FileLocator(__DIR__.'/../Resources/config')
38+
);
39+
$loader->load('services.yml');
40+
}
41+
}

0 commit comments

Comments
 (0)