Skip to content

Commit

Permalink
Feature / Rate Limit (#113)
Browse files Browse the repository at this point in the history
Added rate limits using Redis and filtering by two rules using user ID
and IP.
  • Loading branch information
vokomarov authored May 8, 2023
2 parents 1b72ff4 + acdd8c3 commit 8b6a458
Show file tree
Hide file tree
Showing 51 changed files with 1,224 additions and 411 deletions.
2 changes: 2 additions & 0 deletions .env.actions
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ DB_NAME=cashtrack
DB_USER=cashtrack
DB_PASSWORD=secret

REDIS_CONNECTION=cache:6379

S3_REGION=us-east-1
S3_ENDPOINT=
S3_KEY=
Expand Down
2 changes: 1 addition & 1 deletion .env.build
Original file line number Diff line number Diff line change
@@ -1 +1 @@
RELEASE_VERSION=0.1.25
RELEASE_VERSION=1.2.2-dev
2 changes: 2 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ DB_NAME=cashtrack
DB_USER=cashtrack
DB_PASSWORD=secret

REDIS_CONNECTION=localhost:6379

FIREBASE_DATABASE_URI=
FIREBASE_STORAGE_BUCKET=
FIREBASE_PROJECT_ID=
Expand Down
28 changes: 16 additions & 12 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,23 @@ jobs:
container: shivammathur/node:latest
steps:
- name: Checkout Repository
uses: actions/checkout@v2
uses: actions/checkout@v3

- name: Setup PHP
uses: shivammathur/setup-php@v2
env:
runner: self-hosted
with:
php-version: '8.2'
extensions: zip xsl dom exif intl pcntl bcmath sockets mbstring pdo_mysql mysqli
extensions: zip, xsl, dom, exif, intl, pcntl, bcmath, sockets, mbstring, pdo_mysql, mysqli, redis
tools: composer

- name: Get Composer Cache Directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT

- name: Prepare Cache For Composer
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
Expand All @@ -47,23 +47,23 @@ jobs:
container: shivammathur/node:latest
steps:
- name: Checkout Repository
uses: actions/checkout@v2
uses: actions/checkout@v3

- name: Setup PHP
uses: shivammathur/setup-php@v2
env:
runner: self-hosted
with:
php-version: '8.2'
extensions: zip xsl dom exif intl pcntl bcmath sockets mbstring pdo_mysql mysqli
extensions: zip, xsl, dom, exif, intl, pcntl, bcmath, sockets, mbstring, pdo_mysql, mysqli, redis
tools: composer

- name: Get Composer Cache Directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT

- name: Prepare Cache For Composer
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: "${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}"
Expand All @@ -84,6 +84,10 @@ jobs:
packages: write

services:
cache:
image: cashtrack/redis:latest
ports:
- 6379
database:
image: cashtrack/mysql:latest
env:
Expand All @@ -97,7 +101,7 @@ jobs:

steps:
- name: Checkout Repository
uses: actions/checkout@v2
uses: actions/checkout@v3

- name: Setup PHP
uses: shivammathur/setup-php@v2
Expand All @@ -106,15 +110,15 @@ jobs:
with:
php-version: '8.2'
coverage: pcov
extensions: zip xsl dom exif intl pcntl bcmath sockets mbstring pdo_mysql mysqli
extensions: zip, xsl, dom, exif, intl, pcntl, bcmath, sockets, mbstring, pdo_mysql, mysqli, redis
tools: composer, phpunit

- name: Get Composer Cache Directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT

- name: Prepare Cache For Composer
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
steps:
- name: Checkout repository
if: github.event_name != 'pull_request'
uses: actions/checkout@v2
uses: actions/checkout@v3

# Login against a Docker registry except on PR
# https://github.com/docker/login-action
Expand Down Expand Up @@ -71,7 +71,7 @@ jobs:

steps:
- name: Checkout infra repository
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: ${{ env.INFRA_REPO }}
ref: ${{ env.INFRA_REPO_REF }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ jobs:
runs-on: [self-hosted, Linux, x64]
steps:
- name: Checkout Repository
uses: actions/checkout@v2
uses: actions/checkout@v3

- name: Prepare Cache For Vulnerability Database
uses: actions/cache@v2
uses: actions/cache@v3
id: vulnerability-db-cache
with:
path: ~/.vulnerability-db/cache
Expand Down
9 changes: 5 additions & 4 deletions .rr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,19 @@ server:
http:
address: 0.0.0.0:8080
middleware:
- gzip
- http_metrics
- gzip
pool:
num_workers: 1
supervisor:
max_worker_memory: 100

kv:
local:
driver: memory
redis:
driver: redis
config:
interval: 60
addrs:
- ${REDIS_CONNECTION}

logs:
mode: production
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
FROM php:8.2.4-alpine3.17 as backend

RUN --mount=type=bind,from=mlocati/php-extension-installer:1.5,source=/usr/bin/install-php-extensions,target=/usr/local/bin/install-php-extensions \
install-php-extensions opcache zip xsl dom exif intl pcntl bcmath sockets mbstring pdo_mysql mysqli && \
install-php-extensions opcache zip xsl dom exif intl pcntl bcmath sockets mbstring pdo_mysql mysqli redis && \
apk del --no-cache ${PHPIZE_DEPS} ${BUILD_DEPENDS}

COPY --from=ghcr.io/roadrunner-server/roadrunner:2.12.3 /usr/bin/rr /usr/bin/rr
Expand Down
36 changes: 36 additions & 0 deletions app/config/redis.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types = 1);

return [
/**
* Can be a host, or the path to a unix domain socket.
* Starting from version 5.0.0 it is possible to specify schema.
*/
'connection' => env('REDIS_CONNECTION', 'localhost:6379'),

/**
* Value in seconds (default is 0 meaning it will use default_socket_timeout)
*/
'timeout' => 2.0,

/**
* Value in milliseconds
*/
'retry_interval' => 2,

/**
* Value in seconds (default is 0 meaning it will use default_socket_timeout)
*/
'retry_timeout' => 2.0,

/**
* Prepend to any key on a connection level
*/
'prefix' => 'CT:',

/**
* The number of retries, meaning if you set this option to n, there will be a maximum n+1 attempts overall.
*/
'max_retries' => 5,
];
1 change: 1 addition & 0 deletions app/locale/en/messages.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
'error_nick_name_claimed' => 'Nick name already claimed.',
'error_value_is_not_unique' => 'Value should be unique.',
'error_profile_not_confirmed' => 'You are not allowed to perform this action as your profile is not confirmed.',
'error_rate_limit_reached' => 'Too many requests. Please try again later.',

'email_confirmation_confirm_failure' => 'Unable to confirm your email.',
'email_confirmation_ok' => 'Your email has been confirmed.',
Expand Down
1 change: 1 addition & 0 deletions app/locale/uk/messages.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
'error_nick_name_claimed' => 'Нікнейм вже зайнято.',
'error_value_is_not_unique' => 'Значення має бути унікальним.',
'error_profile_not_confirmed' => 'Вам не дозволено здійснити бажану операцію, так як Ваш профіль не підтверджено.',
'error_rate_limit_reached' => 'Забагато запитів. Будь ласка, спробуйте ще раз пізніше.',

'email_confirmation_confirm_failure' => 'Неможливо підтвердити ваш email.',
'email_confirmation_ok' => 'Ваш email було підтверджено.',
Expand Down
3 changes: 2 additions & 1 deletion app/src/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,12 @@ class App extends Kernel
*/
protected const LOAD = [
// Logging and exceptions handling
RoadRunnerBridge\LoggerBootloader::class,
Bootloader\LoggingBootloader::class,
Monolog\MonologBootloader::class,
Bootloader\ExceptionHandlerBootloader::class,

// RoadRunner
RoadRunnerBridge\LoggerBootloader::class,
RoadRunnerBridge\QueueBootloader::class,
RoadRunnerBridge\HttpBootloader::class,
RoadRunnerBridge\CacheBootloader::class,
Expand Down Expand Up @@ -144,6 +144,7 @@ class App extends Kernel
* Application specific services and extensions.
*/
protected const APP = [
Bootloader\RedisBootloader::class,
Auth\AuthBootloader::class,
Bootloader\RoutesBootloader::class,
Bootloader\UserBootloader::class,
Expand Down
65 changes: 65 additions & 0 deletions app/src/Bootloader/RedisBootloader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

declare(strict_types=1);

namespace App\Bootloader;

use App\Config\RedisConfig;
use Psr\Log\LoggerInterface;
use Redis;
use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Core\Container;

class RedisBootloader extends Bootloader
{
public function __construct(private RedisConfig $config, private LoggerInterface $logger)
{
}

public function boot(Container $container): void
{
$container->bindSingleton(Redis::class, fn (): Redis => $this->resolve());
}

protected function resolve(): Redis
{
$redis = new Redis();

if ($this->config->getHost() === '') {
return $redis;
}

$uri = "{$this->config->getHost()}:{$this->config->getPort()}";

$this->logger->info("Resolving a connection to a Redis instance [{$uri}]");

$status = $redis->pconnect(
$this->config->getHost(),
$this->config->getPort(),
$this->config->getTimeout(),
null,
$this->config->getRetryInterval(),
$this->config->getRetryTimeout(),
);

if (! $status) {
$this->logger->emergency("Connection to a Redis instance failed [{$uri}]: {$redis->getLastError()}");

throw new \RedisException("Unable to connect to Redis");
}

if (! $redis->ping()) {
$this->logger->emergency("PING to a Redis instance is not successful [{$uri}]: {$redis->getLastError()}");

throw new \RedisException("Unable to connect to Redis");
}

$redis->setOption(Redis::OPT_PREFIX, $this->config->getPrefix());
$redis->setOption(Redis::OPT_MAX_RETRIES, $this->config->getMaxRetries());
$redis->setOption(Redis::OPT_SCAN, Redis::SCAN_RETRY);

$this->logger->info("Connection to a Redis instance has been established [{$uri}]");

return $redis;
}
}
15 changes: 13 additions & 2 deletions app/src/Bootloader/RoutesBootloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@

namespace App\Bootloader;

use App\Auth\AuthMiddleware;
use App\Middleware\LocaleSelectorMiddleware;
use App\Middleware\RateLimitMiddleware;
use App\Middleware\UserLocaleSelectorMiddleware;
use App\Request\JsonErrorsRenderer;
use Spiral\Bootloader\Http\RoutesBootloader as BaseRoutesBootloader;
use App\Auth\AuthMiddleware;
use App\Service\RateLimit\RateLimitInterface;
use App\Service\RateLimit\RedisRateLimit;
use Spiral\Auth\Middleware\AuthMiddleware as InitAuthMiddleware;
use Spiral\Bootloader\Http\RoutesBootloader as BaseRoutesBootloader;
use Spiral\Debug\StateCollector\HttpCollector;
use Spiral\Filter\ValidationHandlerMiddleware;
use Spiral\Filters\ErrorsRendererInterface;
Expand All @@ -24,6 +27,10 @@ final class RoutesBootloader extends BaseRoutesBootloader
ErrorsRendererInterface::class => JsonErrorsRenderer::class,
];

protected const BINDINGS = [
RateLimitInterface::class => RedisRateLimit::class,
];

protected const DEPENDENCIES = [
AnnotatedRoutesBootloader::class,
];
Expand All @@ -45,8 +52,12 @@ protected function middlewareGroups(): array
return [
'auth' => [
AuthMiddleware::class,
RateLimitMiddleware::class,
UserLocaleSelectorMiddleware::class,
],
'web' => [
RateLimitMiddleware::class,
],
];
}

Expand Down
20 changes: 3 additions & 17 deletions app/src/Bootloader/S3Bootloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,11 @@
*/
class S3Bootloader extends Bootloader
{
/**
* @var \App\Config\S3Config
*/
private $config;

/**
* FirebaseBootloader constructor.
*
* @param \App\Config\S3Config $config
*/
public function __construct(S3Config $config)
{
$this->config = $config;
public function __construct(
private S3Config $config,
) {
}

/**
* @param \Spiral\Core\Container $container
* @return void
*/
public function boot(Container $container): void
{
$container->bind(S3ClientInterface::class, function (): S3ClientInterface {
Expand Down
Loading

0 comments on commit 8b6a458

Please sign in to comment.