-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #22 from FreeElephants/v0.0.14
V0.0.14
- Loading branch information
Showing
16 changed files
with
377 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ FROM php:8-cli | |
|
||
RUN apt-get update \ | ||
&& apt-get install -y \ | ||
git \ | ||
libzip-dev \ | ||
unzip \ | ||
libicu-dev | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
65 changes: 65 additions & 0 deletions
65
src/FreeElephants/JsonApiToolkit/Middleware/RateLimitMiddleware.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
<?php | ||
|
||
namespace FreeElephants\JsonApiToolkit\Middleware; | ||
|
||
use FreeElephants\JsonApiToolkit\Psr\JsonApiResponseFactory; | ||
use FreeElephants\JsonApiToolkit\RateLimiter\RateConfig; | ||
use FreeElephants\JsonApiToolkit\RateLimiter\RateLimiterInterface; | ||
use FreeElephants\JsonApiToolkit\RateLimiter\SkipRateLimitingSolver; | ||
use FreeElephants\JsonApiToolkit\RequestIdentityResolver\RequestIdentityResolverInterface; | ||
use Psr\Http\Message\ResponseInterface; | ||
use Psr\Http\Message\ServerRequestInterface; | ||
use Psr\Http\Server\MiddlewareInterface; | ||
use Psr\Http\Server\RequestHandlerInterface; | ||
|
||
class RateLimitMiddleware implements MiddlewareInterface | ||
{ | ||
public const HEADER_LIMIT = 'X-Rate-Limit'; | ||
public const HEADER_REMAINING = 'X-Rate-Remaining'; | ||
public const HEADER_TTL = 'X-Rate-Ttl'; | ||
|
||
private JsonApiResponseFactory $responseFactory; | ||
private RequestIdentityResolverInterface $identityResolver; | ||
private RateConfig $rateConfig; | ||
private RateLimiterInterface $rateLimiter; | ||
private SkipRateLimitingSolver $skipRateLimitingSolver; | ||
|
||
public function __construct( | ||
JsonApiResponseFactory $jsonApiResponseFactory, | ||
RequestIdentityResolverInterface $identityResolver, | ||
RateConfig $rateConfig, | ||
RateLimiterInterface $rateLimiter, | ||
SkipRateLimitingSolver $skipRateLimitingSolver | ||
) { | ||
$this->responseFactory = $jsonApiResponseFactory; | ||
$this->identityResolver = $identityResolver; | ||
$this->rateConfig = $rateConfig; | ||
$this->rateLimiter = $rateLimiter; | ||
$this->skipRateLimitingSolver = $skipRateLimitingSolver; | ||
} | ||
|
||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface | ||
{ | ||
if (!$this->skipRateLimitingSolver->isShouldSkip($request)) { | ||
$remaining = $this->rateLimiter->limit( | ||
$this->identityResolver->resolve($request), | ||
$this->rateConfig | ||
); | ||
} else { | ||
$remaining = 1; | ||
} | ||
|
||
if ($remaining <= 0) { | ||
return $this->responseFactory->createSingleErrorResponse('Rate Limit Exceed', 429, $request); | ||
} | ||
|
||
return $this->withHeaders($handler->handle($request), $remaining); | ||
} | ||
|
||
private function withHeaders(ResponseInterface $response, int $remaining): ResponseInterface | ||
{ | ||
return $response->withHeader(self::HEADER_LIMIT, $this->rateConfig->getLimitCount()) | ||
->withHeader(self::HEADER_TTL, $this->rateConfig->getTtl()) | ||
->withHeader(self::HEADER_REMAINING, $remaining); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
20 changes: 20 additions & 0 deletions
20
src/FreeElephants/JsonApiToolkit/RateLimiter/OnPositiveAttributeSkipRateLimitingSolver.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
<?php | ||
|
||
namespace FreeElephants\JsonApiToolkit\RateLimiter; | ||
|
||
use Psr\Http\Message\ServerRequestInterface; | ||
|
||
class OnPositiveAttributeSkipRateLimitingSolver implements SkipRateLimitingSolver | ||
{ | ||
private string $attributeName; | ||
|
||
public function __construct(string $attributeName) | ||
{ | ||
$this->attributeName = $attributeName; | ||
} | ||
|
||
public function isShouldSkip(ServerRequestInterface $request): bool | ||
{ | ||
return $request->getAttribute($this->attributeName, false); | ||
} | ||
} |
25 changes: 25 additions & 0 deletions
25
src/FreeElephants/JsonApiToolkit/RateLimiter/RateConfig.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
<?php | ||
|
||
namespace FreeElephants\JsonApiToolkit\RateLimiter; | ||
|
||
class RateConfig | ||
{ | ||
private int $limitCount; | ||
private int $Ttl; | ||
|
||
public function __construct(int $limitCount, int $Ttl) | ||
{ | ||
$this->limitCount = $limitCount; | ||
$this->Ttl = $Ttl; | ||
} | ||
|
||
public function getLimitCount(): int | ||
{ | ||
return $this->limitCount; | ||
} | ||
|
||
public function getTtl(): int | ||
{ | ||
return $this->Ttl; | ||
} | ||
} |
11 changes: 11 additions & 0 deletions
11
src/FreeElephants/JsonApiToolkit/RateLimiter/RateLimiterInterface.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
<?php | ||
|
||
namespace FreeElephants\JsonApiToolkit\RateLimiter; | ||
|
||
interface RateLimiterInterface | ||
{ | ||
/** | ||
* @return int remaining requests count | ||
*/ | ||
public function limit(string $identity, RateConfig $rateConfig): int; | ||
} |
52 changes: 52 additions & 0 deletions
52
src/FreeElephants/JsonApiToolkit/RateLimiter/RedisRateLimiter.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
<?php | ||
|
||
namespace FreeElephants\JsonApiToolkit\RateLimiter; | ||
|
||
class RedisRateLimiter implements RateLimiterInterface | ||
{ | ||
private \Redis $redis; | ||
|
||
private const KEY = 'rate_limit:'; | ||
|
||
public function __construct(\Redis $redis) | ||
{ | ||
$this->redis = $redis; | ||
} | ||
|
||
public function limit(string $identity, RateConfig $rateConfig): int | ||
{ | ||
$this->redis->set($this->genKey($identity, true), 0, ['nx', 'ex' => $rateConfig->getTtl()]); | ||
|
||
return $rateConfig->getLimitCount() - $this->getValue($identity); | ||
} | ||
|
||
private function genKey(string $identity, $addTimestamp = false) | ||
{ | ||
$result = self::KEY . $identity; | ||
|
||
if ($addTimestamp) { | ||
$result .= ':' . microtime(true); | ||
} else { | ||
$result .= ':*'; | ||
} | ||
|
||
return $result; | ||
} | ||
|
||
private function getValue(string $identity): float | ||
{ | ||
$count = 0; | ||
$iterator = null; | ||
|
||
while ( | ||
($keys = $this->redis->scan( | ||
$iterator, | ||
$this->genKey($identity) | ||
)) !== false | ||
) { | ||
$count += count($keys); | ||
} | ||
|
||
return $count; | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
src/FreeElephants/JsonApiToolkit/RateLimiter/SkipRateLimitingSolver.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
<?php | ||
|
||
namespace FreeElephants\JsonApiToolkit\RateLimiter; | ||
|
||
use Psr\Http\Message\ServerRequestInterface; | ||
|
||
interface SkipRateLimitingSolver | ||
{ | ||
public function isShouldSkip(ServerRequestInterface $request): bool; | ||
} |
27 changes: 27 additions & 0 deletions
27
...FreeElephants/JsonApiToolkit/RequestIdentityResolver/CompositeRequestIdentityResolver.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
<?php | ||
|
||
namespace FreeElephants\JsonApiToolkit\RequestIdentityResolver; | ||
|
||
use Psr\Http\Message\ServerRequestInterface; | ||
|
||
class CompositeRequestIdentityResolver implements RequestIdentityResolverInterface | ||
{ | ||
private array $resolvers; | ||
|
||
public function __construct(RequestIdentityResolverInterface ...$resolvers) | ||
{ | ||
$this->resolvers = $resolvers; | ||
} | ||
|
||
public function resolve(ServerRequestInterface $request): string | ||
{ | ||
foreach ($this->resolvers as $resolver) { | ||
try { | ||
return $resolver->resolve($request); | ||
} catch (UnresolvableRequestIdentityException $exception) { | ||
continue; | ||
} | ||
} | ||
throw new UnresolvableRequestIdentityException(); | ||
} | ||
} |
23 changes: 23 additions & 0 deletions
23
src/FreeElephants/JsonApiToolkit/RequestIdentityResolver/IpRequestIdentityResolver.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
<?php | ||
|
||
namespace FreeElephants\JsonApiToolkit\RequestIdentityResolver; | ||
|
||
use Psr\Http\Message\ServerRequestInterface; | ||
|
||
class IpRequestIdentityResolver implements RequestIdentityResolverInterface | ||
{ | ||
public function resolve(ServerRequestInterface $request): string | ||
{ | ||
$serverParams = $request->getServerParams(); | ||
|
||
if (array_key_exists('HTTP_CLIENT_IP', $serverParams)) { | ||
return $serverParams['HTTP_CLIENT_IP']; | ||
} | ||
|
||
if (array_key_exists('HTTP_X_FORWARDED_FOR', $serverParams)) { | ||
return $serverParams['HTTP_X_FORWARDED_FOR']; | ||
} | ||
|
||
return $serverParams['REMOTE_ADDR'] ?? '127.0.0.1'; | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
...FreeElephants/JsonApiToolkit/RequestIdentityResolver/RequestIdentityResolverInterface.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
<?php | ||
|
||
namespace FreeElephants\JsonApiToolkit\RequestIdentityResolver; | ||
|
||
use Psr\Http\Message\ServerRequestInterface; | ||
|
||
interface RequestIdentityResolverInterface | ||
{ | ||
/** | ||
* @return string Identity | ||
* | ||
* @throws UnresolvableRequestIdentityException | ||
*/ | ||
public function resolve(ServerRequestInterface $request): string; | ||
} |
7 changes: 7 additions & 0 deletions
7
...Elephants/JsonApiToolkit/RequestIdentityResolver/UnresolvableRequestIdentityException.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
<?php | ||
|
||
namespace FreeElephants\JsonApiToolkit\RequestIdentityResolver; | ||
|
||
class UnresolvableRequestIdentityException extends \InvalidArgumentException | ||
{ | ||
} |
33 changes: 33 additions & 0 deletions
33
src/FreeElephants/JsonApiToolkit/Routing/FastRoute/CacheableDispatcherFactoryProxy.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
<?php | ||
|
||
namespace FreeElephants\JsonApiToolkit\Routing\FastRoute; | ||
|
||
use FastRoute\Dispatcher; | ||
use Psr\Cache\CacheItemPoolInterface; | ||
|
||
class CacheableDispatcherFactoryProxy implements DispatcherFactoryInterface | ||
{ | ||
private DispatcherFactoryInterface $dispatcherFactory; | ||
private CacheItemPoolInterface $cacheItemPool; | ||
|
||
public function __construct(DispatcherFactoryInterface $dispatcherFactory, CacheItemPoolInterface $cacheItemPool) | ||
{ | ||
$this->dispatcherFactory = $dispatcherFactory; | ||
$this->cacheItemPool = $cacheItemPool; | ||
} | ||
|
||
public function buildDispatcher(string $openApiDocumentSource): Dispatcher | ||
{ | ||
$key = md5_file($openApiDocumentSource); | ||
$cacheItem = $this->cacheItemPool->getItem($key); | ||
if ($cacheItem->isHit()) { | ||
$dispatcher = $cacheItem->get(); | ||
} else { | ||
$dispatcher = $this->dispatcherFactory->buildDispatcher($openApiDocumentSource); | ||
$cacheItem->set($dispatcher); | ||
$this->cacheItemPool->save($cacheItem); | ||
} | ||
|
||
return $dispatcher; | ||
} | ||
} |
Oops, something went wrong.