Passed
Pull Request — master (#204)
by
unknown
01:59
created

RateLimiterMiddleware::withCounterId()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 6
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Web\RateLimiter;
6
7
use Psr\Http\Message\ResponseFactoryInterface;
8
use Psr\Http\Message\ResponseInterface;
9
use Psr\Http\Message\ServerRequestInterface;
10
use Psr\Http\Server\MiddlewareInterface;
11
use Psr\Http\Server\RequestHandlerInterface;
12
use Yiisoft\Http\Status;
13
14
/**
15
 * RateLimiter helps to prevent abuse by limiting the number of requests that could be me made consequentially.
16
 *
17
 * For example, you may want to limit the API usage of each user to be at most 100 API calls within a period of 10 minutes.
18
 * If too many requests are received from a user within the stated period of the time, a response with status code 429
19
 * (meaning "Too Many Requests") should be returned.
20
 */
21
final class RateLimiterMiddleware implements MiddlewareInterface
22
{
23
    private CounterInterface $counter;
24
25
    private ResponseFactoryInterface $responseFactory;
26
27
    private string $counterId;
28
29
    /**
30
     * @var callable
31
     */
32
    private $counterIdCallback;
33
34
    public function __construct(CounterInterface $counter, ResponseFactoryInterface $responseFactory)
35
    {
36
        $this->counter = $counter;
37
        $this->responseFactory = $responseFactory;
38
    }
39
40
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
41
    {
42
        $this->counter->setId($this->generateId($request));
43
        $result = $this->counter->incrementAndGetResult();
44
45
        if ($result->isLimitReached()) {
46
            $response = $this->createErrorResponse();
47
        } else {
48
            $response = $handler->handle($request);
49
        }
50
51
        return $this->addHeaders($response, $result);
52
    }
53
54
    public function withCounterIdCallback(?callable $callback): self
55
    {
56
        $new = clone $this;
57
        $new->counterIdCallback = $callback;
58
59
        return $new;
60
    }
61
62
    public function withCounterId(string $id): self
63
    {
64
        $new = clone $this;
65
        $new->counterId = $id;
66
67
        return $new;
68
    }
69
70
    private function createErrorResponse(): ResponseInterface
71
    {
72
        $response = $this->responseFactory->createResponse(Status::TOO_MANY_REQUESTS);
73
        $response->getBody()->write(Status::TEXTS[Status::TOO_MANY_REQUESTS]);
74
75
        return $response;
76
    }
77
78
    private function generateId(ServerRequestInterface $request): string
79
    {
80
        if ($this->counterIdCallback !== null) {
81
            return \call_user_func($this->counterIdCallback, $request);
82
        }
83
84
        return $this->counterId ?? $this->generateIdFromRequest($request);
85
    }
86
87
    private function generateIdFromRequest(ServerRequestInterface $request): string
88
    {
89
        return strtolower($request->getMethod() . '-' . $request->getUri()->getPath());
90
    }
91
92
    private function addHeaders(ResponseInterface $response, CounterStatistics $result): ResponseInterface
93
    {
94
        return $response
95
            ->withHeader('X-Rate-Limit-Limit', $result->getLimit())
96
            ->withHeader('X-Rate-Limit-Remaining', $result->getRemaining())
97
            ->withHeader('X-Rate-Limit-Reset', $result->getResetTime());
98
    }
99
}
100