Completed
Push — master ( d0572a...56be9e )
by Pásztor
03:28 queued 01:34
created

ThrottleRequests::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
1
<?php
2
3
namespace Autumn\Api\Http\Middleware;
4
5
use Closure;
6
use Carbon\Carbon;
7
use RuntimeException;
8
use Autumn\Api\Classes\RateLimiter;
9
use Symfony\Component\HttpFoundation\Response;
10
11
class ThrottleRequests
12
{
13
    /**
14
     * The rate limiter instance.
15
     *
16
     * @var \Autumn\Api\Classes\RateLimiter
17
     */
18
    protected $limiter;
19
20
    /**
21
     * Create a new request throttler.
22
     *
23
     * @param  \Autumn\Api\Classes\RateLimiter  $limiter
24
     *
25
     * @return void
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
26
     */
27
    public function __construct(RateLimiter $limiter)
28
    {
29
        $this->limiter = $limiter;
30
    }
31
32
    /**
33
     * Handle an incoming request.
34
     *
35
     * @param  \Illuminate\Http\Request  $request
36
     * @param  \Closure  $next
37
     * @param  int  $maxAttempts
38
     * @param  float|int  $decayMinutes
39
     *
40
     * @return mixed
41
     */
42
    public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1)
43
    {
44
        $key = $this->resolveRequestSignature($request);
45
46
        if ($this->limiter->tooManyAttempts($key, $maxAttempts, $decayMinutes)) {
47
            return $this->buildResponse($key, $maxAttempts);
48
        }
49
50
        $this->limiter->hit($key, $decayMinutes);
51
52
        $response = $next($request);
53
54
        return $this->addHeaders(
55
            $response, $maxAttempts,
56
            $this->calculateRemainingAttempts($key, $maxAttempts)
57
        );
58
    }
59
60
    /**
61
     * Resolve request signature.
62
     *
63
     * @param  \Illuminate\Http\Request  $request
64
     *
65
     * @return string
66
     */
67
    protected function resolveRequestSignature($request)
68
    {
69
        if (! $route = $request->route()) {
70
            throw new RuntimeException('Unable to generate fingerprint. Route unavailable.');
71
        }
72
73
        return sha1(implode('|', array_merge(
74
            $route->methods(), [$route->domain(), $route->uri(), $request->ip()]
75
        )));
76
    }
77
78
    /**
79
     * Create a 'too many attempts' response.
80
     *
81
     * @param  string  $key
82
     * @param  int  $maxAttempts
83
     *
84
     * @return \Symfony\Component\HttpFoundation\Response
85
     */
86
    protected function buildResponse($key, $maxAttempts)
87
    {
88
        $response = new Response('Too Many Attempts.', 429);
89
90
        $retryAfter = $this->limiter->availableIn($key);
91
92
        return $this->addHeaders(
93
            $response, $maxAttempts,
94
            $this->calculateRemainingAttempts($key, $maxAttempts, $retryAfter),
95
            $retryAfter
96
        );
97
    }
98
99
    /**
100
     * Add the limit header information to the given response.
101
     *
102
     * @param  \Symfony\Component\HttpFoundation\Response  $response
103
     * @param  int  $maxAttempts
104
     * @param  int  $remainingAttempts
105
     * @param  int|null  $retryAfter
106
     *
107
     * @return \Symfony\Component\HttpFoundation\Response
108
     */
109
    protected function addHeaders(Response $response, $maxAttempts, $remainingAttempts, $retryAfter = null)
110
    {
111
        $headers = [
112
            'X-RateLimit-Limit' => $maxAttempts,
113
            'X-RateLimit-Remaining' => $remainingAttempts,
114
        ];
115
116
        if (! is_null($retryAfter)) {
117
            $headers['Retry-After'] = $retryAfter;
118
            $headers['X-RateLimit-Reset'] = Carbon::now()->getTimestamp() + $retryAfter;
119
        }
120
121
        $response->headers->add($headers);
122
123
        return $response;
124
    }
125
126
    /**
127
     * Calculate the number of remaining attempts.
128
     *
129
     * @param  string  $key
130
     * @param  int  $maxAttempts
131
     * @param  int|null  $retryAfter
132
     *
133
     * @return int
134
     */
135
    protected function calculateRemainingAttempts($key, $maxAttempts, $retryAfter = null)
136
    {
137
        if (is_null($retryAfter)) {
138
            return $this->limiter->retriesLeft($key, $maxAttempts);
139
        }
140
141
        return 0;
142
    }
143
}
144