Skip to content

Commit

Permalink
Merge pull request #22 from FreeElephants/v0.0.14
Browse files Browse the repository at this point in the history
V0.0.14
  • Loading branch information
samizdam authored Feb 2, 2021
2 parents 347fc43 + 4f7695a commit d4e083e
Show file tree
Hide file tree
Showing 16 changed files with 377 additions and 1 deletion.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added
- Data Transfer Object classes generation from swagger spec
- CacheableDispatcherFactoryProxy
- JsonApiResponseFactory::createRelationshipResponse()
- RateLimitMiddleware and related packages

## [0.0.13] - 2021-01-27
### Added
Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ FROM php:8-cli

RUN apt-get update \
&& apt-get install -y \
git \
libzip-dev \
unzip \
libicu-dev
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@
"neomerx/json-api": "^4.0",
"nette/php-generator": "^3.4",
"nikic/fast-route": "^1.3",
"psr/http-message": "^1.0",
"psr/cache": "^1.0",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.0",
"psr/http-server-handler": "^1.0",
"psr/http-server-middleware": "^1.0",
"rakit/validation": "^1.2",
Expand Down
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);
}
}
11 changes: 11 additions & 0 deletions src/FreeElephants/JsonApiToolkit/Psr/JsonApiResponseFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,17 @@ public function createResponse($data, ServerRequestInterface $request = null): R
return $response;
}

public function createRelationshipResponse($data): ResponseInterface
{
$content = $this->encoder->encodeIdentifiers($data);

$response = $this->createPsrResponse();
$response->getBody()->write($content);
$response->getBody()->rewind();

return $response;
}

public function createErrorResponse(ErrorCollection $errors, int $status): ResponseInterface
{
$response = $this->createPsrResponse($status);
Expand Down
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 src/FreeElephants/JsonApiToolkit/RateLimiter/RateConfig.php
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;
}
}
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 src/FreeElephants/JsonApiToolkit/RateLimiter/RedisRateLimiter.php
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;
}
}
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;
}
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();
}
}
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';
}
}
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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace FreeElephants\JsonApiToolkit\RequestIdentityResolver;

class UnresolvableRequestIdentityException extends \InvalidArgumentException
{
}
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;
}
}
Loading

0 comments on commit d4e083e

Please sign in to comment.