Skip to content

Commit

Permalink
[WIP] - Initial commit from internal component (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
Melfo01 authored Nov 18, 2020
1 parent 2b88f8d commit 12b93aa
Show file tree
Hide file tree
Showing 33 changed files with 1,717 additions and 0 deletions.
15 changes: 15 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
root = true

[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[Makefile]
indent_style = tab

[*.md]
trim_trailing_whitespace = false
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/vendor/
/bin/
composer.lock
.phpunit.result.cache
.php_cs.cache
11 changes: 11 additions & 0 deletions .php_cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

$config = new M6Web\CS\Config\Php74();

$config->getFinder()
->in([
__DIR__.'/src',
__DIR__.'/tests'
]);

return $config;
64 changes: 64 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
SHELL=bash -o pipefail
SOURCE_DIR = $(shell pwd)
BIN_DIR = ${SOURCE_DIR}/bin
COMPOSER = composer

define printSection
@printf "\033[36m\n==================================================\n\033[0m"
@printf "\033[36m $1 \033[0m"
@printf "\033[36m\n==================================================\n\033[0m"
endef

.PHONY: all
all: install quality test

.PHONY: ci
ci: quality test

.PHONY: install
install: clean-vendor composer-install

.PHONY: quality
quality: cs-ci phpstan

.PHONY: test
test: phpunit

### DEPENDENCIES ###

.PHONY: clean-vendor
clean-vendor:
$(call printSection,DEPENDENCIES clean)
rm -rf ${SOURCE_DIR}/vendor

.PHONY: composer-install
composer-install: ${SOURCE_DIR}/vendor/composer/installed.json

${SOURCE_DIR}/vendor/composer/installed.json:
$(call printSection,DEPENDENCIES install)
$(COMPOSER) --no-interaction install --ansi --no-progress --prefer-dist

### TEST ###

.PHONY: phpunit
phpunit:
$(call printSection,TEST phpunit)
${BIN_DIR}/phpunit

### QUALITY ###

.PHONY: phpstan
phpstan:
$(call printSection,QUALITY phpstan)
${BIN_DIR}/phpstan analyse --memory-limit=1G

.PHONY: cs-ci
cs-ci:
$(call printSection,QUALITY php-cs-fixer check)
${BIN_DIR}/php-cs-fixer fix --ansi --dry-run --using-cache=no --verbose

.PHONY: cs-fix
cs-fix:
$(call printSection,QUALITY php-cs-fixer fix)
${BIN_DIR}/php-cs-fixer fix

86 changes: 86 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# RateLimitBundle
This bundle provides an easy way to protect your project by limiting access to your controllers.

## Install the bundle
```bash
composer require bedrock/rate-limit-bundle
```

Update your _config/bundles.php_ file to add the bundle for all env
```php
<?php

return [
...
Bedrock\Bundle\RateLimitBundle\RateLimitBundle::class => ['all' => true],
...
];
```

### Configure the bundle
Add the _config/packages/bedrock_rate_limit.yaml_ file with the following data.
```yaml
bedrock_rate_limit:
limit: 25 # 1000 requests by default
period: 600 # 60 seconds by default
limit_by_route: true|false # false by default
display_headers: true|false # false by default
```
By default, the limitation is common to all routes annotated `@RateLimit()`.
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.
If you swicth `limit_by_route` to true, users will be allowed to reach the limit on each route annotated.

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.
`display_headers` is used to display a verbose return if limit is reached.

### Configure your storage
You must tell Symfony which storage implementation you want to use.

Update your _config/services.yml_ like this:

```yaml
...
Bedrock\Bundle\RateLimitBundle\Storage\RateLimitStorageInterface: '@Bedrock\Bundle\RateLimitBundle\Storage\RateLimitInMemoryStorage'
...
```

By default, only `RateLimitInMemory` is provided. But feel free to create your own by implementing `RateLimitStorageInterface` or `ManuallyResetableRateLimitStorageInterface`.
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.

### Configure your modifiers
Modifiers are a way to customize the rate limit.

This bundle provides 2 modifiers:
* `HttpMethodRateLimitModifier` limits the requests by `http_method`.
* `RequestAttributeRateLimitModifier` limits the requests by attributes value (taken from the `$request->attributes` Symfony's bag).

Update your _config/services.yml_ like this:

```yaml
...
Bedrock\Bundle\RateLimitBundle\RateLimitModifier\HttpMethodRateLimitModifier:
tags: [ 'rate_limit.modifiers' ]
Bedrock\Bundle\RateLimitBundle\RateLimitModifier\RequestAttributeRateLimitModifier:
arguments:
$attributeName: 'myRequestAttribute'
tags: [ 'rate_limit.modifiers' ]
...
```

You can also create your own rate limit modifier by implementing `RateLimitModifierInterface` and tagging your service accordingly.

### Configure your routes
Add the `@RateLimit()` annotation to your controller methods (by default, the limit will be 1000 requests per minute).
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.
:warning: This customization only works if the `limit_by_route` parameter is `true`

```php
/**
* @RateLimit(
* limit=10,
* period=120
* )
*/
```
44 changes: 44 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "bedrock/rate-limit-bundle",
"type": "symfony-bundle",
"license": "MIT",
"authors": [
{
"name": "Bedrock",
"email": "[email protected]",
"homepage": "https://tech.bedrockstreaming.com/"
}
],
"config": {
"bin-dir": "bin",
"vendor-dir": "vendor",
"sort-packages": true
},
"require": {
"php": "7.4.*",
"ext-json": "*",
"doctrine/annotations": "^1.10.0",
"symfony/dependency-injection": "4.4.*",
"symfony/event-dispatcher": "4.4.*",
"symfony/http-foundation": "4.4.*",
"symfony/http-kernel": "4.4.*",
"symfony/config": "4.4.*"
},
"require-dev": {
"phpunit/phpunit": "9.4.*",
"m6web/php-cs-fixer-config": "1.3.*",
"phpstan/phpstan": "0.12.*",
"phpstan/phpstan-phpunit": "0.12.*",
"symfony/var-dumper": "4.4.*"
},
"autoload": {
"psr-4": {
"Bedrock\\Bundle\\RateLimitBundle\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Bedrock\\Bundle\\RateLimitBundle\\Tests\\": "tests/"
}
}
}
12 changes: 12 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
includes:
- 'vendor/phpstan/phpstan-phpunit/extension.neon'
- 'vendor/phpstan/phpstan-phpunit/rules.neon'

parameters:
paths:
- 'src'
- 'tests'
level: 'max'
checkGenericClassInNonGenericObjectType: false
ignoreErrors:
- '#Cannot call method integerNode\(\) on Symfony\\Component\\Config\\Definition\\Builder\\NodeParentInterface\|null\.#'
19 changes: 19 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
<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">
<coverage>
<include>
<directory>src</directory>
</include>
</coverage>
<php>
<ini name="error_reporting" value="-1"/>
<server name="SHELL_VERBOSITY" value="-1"/>
<env name="APP_ENV" value="test" />
</php>
<testsuites>
<testsuite name="Project Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>
48 changes: 48 additions & 0 deletions src/Annotation/RateLimit.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace Bedrock\Bundle\RateLimitBundle\Annotation;

/**
* @Annotation
* @Target({"METHOD"})
*/
final class RateLimit
{
private ?int $limit;
private ?int $period;

/**
* @param array<string, int> $args
*/
public function __construct(array $args = [])
{
$this->limit = $args['limit'] ?? null;
$this->period = $args['period'] ?? null;
}

public function getLimit(): ?int
{
return $this->limit;
}

public function setLimit(int $limit): RateLimit
{
$this->limit = $limit;

return $this;
}

public function getPeriod(): ?int
{
return $this->period;
}

public function setPeriod(int $period): RateLimit
{
$this->period = $period;

return $this;
}
}
41 changes: 41 additions & 0 deletions src/DependencyInjection/BedrockRateLimitExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Bedrock\Bundle\RateLimitBundle\DependencyInjection;

use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;

class BedrockRateLimitExtension extends Extension
{
/**
* Override conf key for this bundle
*/
public function getAlias(): string
{
return 'bedrock_rate_limit';
}

/**
* @param array<mixed> $configs
*/
public function load(array $configs, ContainerBuilder $container): void
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);

$container->setParameter('bedrock_rate_limit.limit', $config['limit']);
$container->setParameter('bedrock_rate_limit.period', $config['period']);
$container->setParameter('bedrock_rate_limit.limit_by_route', $config['limit_by_route']);
$container->setParameter('bedrock_rate_limit.display_headers', $config['display_headers']);

$loader = new YamlFileLoader(
$container,
new FileLocator(__DIR__.'/../Resources/config')
);
$loader->load('services.yml');
}
}
40 changes: 40 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace Bedrock\Bundle\RateLimitBundle\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('bedrock_rate_limit');
/** @var ArrayNodeDefinition $rootNode */
$rootNode = $treeBuilder->getRootNode();

$rootNode
->children()
->booleanNode('limit_by_route')
->defaultValue(false)
->end()
->integerNode('limit')
->defaultValue(1000)
->min(0)
->end()
->integerNode('period')
->defaultValue(60)
->min(0)
->end()
->booleanNode('display_headers')
->defaultValue(false)
->end()
->end()
;

return $treeBuilder;
}
}
Loading

0 comments on commit 12b93aa

Please sign in to comment.