Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
namespace Illuminate\Routing\Middleware;
4
 
5
use Closure;
6
use Illuminate\Cache\RateLimiter;
7
use Illuminate\Cache\RateLimiting\Unlimited;
8
use Illuminate\Http\Exceptions\HttpResponseException;
9
use Illuminate\Http\Exceptions\ThrottleRequestsException;
10
use Illuminate\Support\Arr;
11
use Illuminate\Support\InteractsWithTime;
12
use RuntimeException;
13
use Symfony\Component\HttpFoundation\Response;
14
 
15
class ThrottleRequests
16
{
17
    use InteractsWithTime;
18
 
19
    /**
20
     * The rate limiter instance.
21
     *
22
     * @var \Illuminate\Cache\RateLimiter
23
     */
24
    protected $limiter;
25
 
26
    /**
27
     * Create a new request throttler.
28
     *
29
     * @param  \Illuminate\Cache\RateLimiter  $limiter
30
     * @return void
31
     */
32
    public function __construct(RateLimiter $limiter)
33
    {
34
        $this->limiter = $limiter;
35
    }
36
 
37
    /**
38
     * Handle an incoming request.
39
     *
40
     * @param  \Illuminate\Http\Request  $request
41
     * @param  \Closure  $next
42
     * @param  int|string  $maxAttempts
43
     * @param  float|int  $decayMinutes
44
     * @param  string  $prefix
45
     * @return \Symfony\Component\HttpFoundation\Response
46
     *
47
     * @throws \Illuminate\Http\Exceptions\ThrottleRequestsException
48
     */
49
    public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1, $prefix = '')
50
    {
51
        if (is_string($maxAttempts)
52
            && func_num_args() === 3
53
            && ! is_null($limiter = $this->limiter->limiter($maxAttempts))) {
54
            return $this->handleRequestUsingNamedLimiter($request, $next, $maxAttempts, $limiter);
55
        }
56
 
57
        return $this->handleRequest(
58
            $request,
59
            $next,
60
            [
61
                (object) [
62
                    'key' => $prefix.$this->resolveRequestSignature($request),
63
                    'maxAttempts' => $this->resolveMaxAttempts($request, $maxAttempts),
64
                    'decayMinutes' => $decayMinutes,
65
                    'responseCallback' => null,
66
                ],
67
            ]
68
        );
69
    }
70
 
71
    /**
72
     * Handle an incoming request.
73
     *
74
     * @param  \Illuminate\Http\Request  $request
75
     * @param  \Closure  $next
76
     * @param  string  $limiterName
77
     * @param  \Closure  $limiter
78
     * @return \Symfony\Component\HttpFoundation\Response
79
     *
80
     * @throws \Illuminate\Http\Exceptions\ThrottleRequestsException
81
     */
82
    protected function handleRequestUsingNamedLimiter($request, Closure $next, $limiterName, Closure $limiter)
83
    {
84
        $limiterResponse = $limiter($request);
85
 
86
        if ($limiterResponse instanceof Response) {
87
            return $limiterResponse;
88
        } elseif ($limiterResponse instanceof Unlimited) {
89
            return $next($request);
90
        }
91
 
92
        return $this->handleRequest(
93
            $request,
94
            $next,
95
            collect(Arr::wrap($limiterResponse))->map(function ($limit) use ($limiterName) {
96
                return (object) [
97
                    'key' => md5($limiterName.$limit->key),
98
                    'maxAttempts' => $limit->maxAttempts,
99
                    'decayMinutes' => $limit->decayMinutes,
100
                    'responseCallback' => $limit->responseCallback,
101
                ];
102
            })->all()
103
        );
104
    }
105
 
106
    /**
107
     * Handle an incoming request.
108
     *
109
     * @param  \Illuminate\Http\Request  $request
110
     * @param  \Closure  $next
111
     * @param  array  $limits
112
     * @return \Symfony\Component\HttpFoundation\Response
113
     *
114
     * @throws \Illuminate\Http\Exceptions\ThrottleRequestsException
115
     */
116
    protected function handleRequest($request, Closure $next, array $limits)
117
    {
118
        foreach ($limits as $limit) {
119
            if ($this->limiter->tooManyAttempts($limit->key, $limit->maxAttempts)) {
120
                throw $this->buildException($request, $limit->key, $limit->maxAttempts, $limit->responseCallback);
121
            }
122
 
123
            $this->limiter->hit($limit->key, $limit->decayMinutes * 60);
124
        }
125
 
126
        $response = $next($request);
127
 
128
        foreach ($limits as $limit) {
129
            $response = $this->addHeaders(
130
                $response,
131
                $limit->maxAttempts,
132
                $this->calculateRemainingAttempts($limit->key, $limit->maxAttempts)
133
            );
134
        }
135
 
136
        return $response;
137
    }
138
 
139
    /**
140
     * Resolve the number of attempts if the user is authenticated or not.
141
     *
142
     * @param  \Illuminate\Http\Request  $request
143
     * @param  int|string  $maxAttempts
144
     * @return int
145
     */
146
    protected function resolveMaxAttempts($request, $maxAttempts)
147
    {
148
        if (str_contains($maxAttempts, '|')) {
149
            $maxAttempts = explode('|', $maxAttempts, 2)[$request->user() ? 1 : 0];
150
        }
151
 
152
        if (! is_numeric($maxAttempts) && $request->user()) {
153
            $maxAttempts = $request->user()->{$maxAttempts};
154
        }
155
 
156
        return (int) $maxAttempts;
157
    }
158
 
159
    /**
160
     * Resolve request signature.
161
     *
162
     * @param  \Illuminate\Http\Request  $request
163
     * @return string
164
     *
165
     * @throws \RuntimeException
166
     */
167
    protected function resolveRequestSignature($request)
168
    {
169
        if ($user = $request->user()) {
170
            return sha1($user->getAuthIdentifier());
171
        } elseif ($route = $request->route()) {
172
            return sha1($route->getDomain().'|'.$request->ip());
173
        }
174
 
175
        throw new RuntimeException('Unable to generate the request signature. Route unavailable.');
176
    }
177
 
178
    /**
179
     * Create a 'too many attempts' exception.
180
     *
181
     * @param  \Illuminate\Http\Request  $request
182
     * @param  string  $key
183
     * @param  int  $maxAttempts
184
     * @param  callable|null  $responseCallback
185
     * @return \Illuminate\Http\Exceptions\ThrottleRequestsException
186
     */
187
    protected function buildException($request, $key, $maxAttempts, $responseCallback = null)
188
    {
189
        $retryAfter = $this->getTimeUntilNextRetry($key);
190
 
191
        $headers = $this->getHeaders(
192
            $maxAttempts,
193
            $this->calculateRemainingAttempts($key, $maxAttempts, $retryAfter),
194
            $retryAfter
195
        );
196
 
197
        return is_callable($responseCallback)
198
                    ? new HttpResponseException($responseCallback($request, $headers))
199
                    : new ThrottleRequestsException('Too Many Attempts.', null, $headers);
200
    }
201
 
202
    /**
203
     * Get the number of seconds until the next retry.
204
     *
205
     * @param  string  $key
206
     * @return int
207
     */
208
    protected function getTimeUntilNextRetry($key)
209
    {
210
        return $this->limiter->availableIn($key);
211
    }
212
 
213
    /**
214
     * Add the limit header information to the given response.
215
     *
216
     * @param  \Symfony\Component\HttpFoundation\Response  $response
217
     * @param  int  $maxAttempts
218
     * @param  int  $remainingAttempts
219
     * @param  int|null  $retryAfter
220
     * @return \Symfony\Component\HttpFoundation\Response
221
     */
222
    protected function addHeaders(Response $response, $maxAttempts, $remainingAttempts, $retryAfter = null)
223
    {
224
        $response->headers->add(
225
            $this->getHeaders($maxAttempts, $remainingAttempts, $retryAfter, $response)
226
        );
227
 
228
        return $response;
229
    }
230
 
231
    /**
232
     * Get the limit headers information.
233
     *
234
     * @param  int  $maxAttempts
235
     * @param  int  $remainingAttempts
236
     * @param  int|null  $retryAfter
237
     * @param  \Symfony\Component\HttpFoundation\Response|null  $response
238
     * @return array
239
     */
240
    protected function getHeaders($maxAttempts,
241
                                  $remainingAttempts,
242
                                  $retryAfter = null,
243
                                  ?Response $response = null)
244
    {
245
        if ($response &&
246
            ! is_null($response->headers->get('X-RateLimit-Remaining')) &&
247
            (int) $response->headers->get('X-RateLimit-Remaining') <= (int) $remainingAttempts) {
248
            return [];
249
        }
250
 
251
        $headers = [
252
            'X-RateLimit-Limit' => $maxAttempts,
253
            'X-RateLimit-Remaining' => $remainingAttempts,
254
        ];
255
 
256
        if (! is_null($retryAfter)) {
257
            $headers['Retry-After'] = $retryAfter;
258
            $headers['X-RateLimit-Reset'] = $this->availableAt($retryAfter);
259
        }
260
 
261
        return $headers;
262
    }
263
 
264
    /**
265
     * Calculate the number of remaining attempts.
266
     *
267
     * @param  string  $key
268
     * @param  int  $maxAttempts
269
     * @param  int|null  $retryAfter
270
     * @return int
271
     */
272
    protected function calculateRemainingAttempts($key, $maxAttempts, $retryAfter = null)
273
    {
274
        return is_null($retryAfter) ? $this->limiter->retriesLeft($key, $maxAttempts) : 0;
275
    }
276
}