Created
March 21, 2020 16:12
-
-
Save lukaszmtw/0310c9e24b4799f610cfedea0b7df920 to your computer and use it in GitHub Desktop.
While doin
This file contains hidden or 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
| <?php | |
| namespace App\Http\Middleware; | |
| use Closure; | |
| use Illuminate\Cache\RateLimiter; | |
| use Illuminate\Http\Exceptions\ThrottleRequestsException; | |
| use Illuminate\Support\InteractsWithTime; | |
| use Illuminate\Support\Str; | |
| use RuntimeException; | |
| use Symfony\Component\HttpFoundation\Response; | |
| class ThrottleRequestsPerRoute | |
| { | |
| use InteractsWithTime; | |
| /** | |
| * The rate limiter instance. | |
| * | |
| * @var \Illuminate\Cache\RateLimiter | |
| */ | |
| protected $limiter; | |
| /** | |
| * Create a new request throttler. | |
| * | |
| * @param \Illuminate\Cache\RateLimiter $limiter | |
| * @return void | |
| */ | |
| public function __construct(RateLimiter $limiter) | |
| { | |
| $this->limiter = $limiter; | |
| } | |
| /** | |
| * Handle an incoming request. | |
| * | |
| * @param \Illuminate\Http\Request $request | |
| * @param \Closure $next | |
| * @param int|string $maxAttempts | |
| * @param float|int $decayMinutes | |
| * @param string $prefix | |
| * @return \Symfony\Component\HttpFoundation\Response | |
| * | |
| * @throws \Illuminate\Http\Exceptions\ThrottleRequestsException | |
| */ | |
| public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1, $prefix = '') | |
| { | |
| $key = $prefix.$this->resolveRequestSignature($request); | |
| $maxAttempts = $this->resolveMaxAttempts($request, $maxAttempts); | |
| if ($this->limiter->tooManyAttempts($key, $maxAttempts)) { | |
| throw $this->buildException($key, $maxAttempts); | |
| } | |
| $this->limiter->hit($key, $decayMinutes * 60); | |
| $response = $next($request); | |
| return $this->addHeaders( | |
| $response, $maxAttempts, | |
| $this->calculateRemainingAttempts($key, $maxAttempts) | |
| ); | |
| } | |
| /** | |
| * Resolve the number of attempts if the user is authenticated or not. | |
| * | |
| * @param \Illuminate\Http\Request $request | |
| * @param int|string $maxAttempts | |
| * @return int | |
| */ | |
| protected function resolveMaxAttempts($request, $maxAttempts) | |
| { | |
| if (Str::contains($maxAttempts, '|')) { | |
| $maxAttempts = explode('|', $maxAttempts, 2)[$request->user() ? 1 : 0]; | |
| } | |
| if (! is_numeric($maxAttempts) && $request->user()) { | |
| $maxAttempts = $request->user()->{$maxAttempts}; | |
| } | |
| return (int) $maxAttempts; | |
| } | |
| /** | |
| * Resolve request signature. | |
| * | |
| * @param \Illuminate\Http\Request $request | |
| * @return string | |
| * | |
| * @throws \RuntimeException | |
| */ | |
| protected function resolveRequestSignature($request) | |
| { | |
| $routeSignature = $request->method().$request->route()->uri; | |
| if ($user = $request->user()) { | |
| return sha1($routeSignature.'|'.$user->getAuthIdentifier()); | |
| } | |
| if ($route = $request->route()) { | |
| return sha1($routeSignature.'|'.$route->getDomain().'|'.$request->ip()); | |
| } | |
| throw new RuntimeException('Unable to generate the request signature. Route unavailable.'); | |
| } | |
| /** | |
| * Create a 'too many attempts' exception. | |
| * | |
| * @param string $key | |
| * @param int $maxAttempts | |
| * @return \Illuminate\Http\Exceptions\ThrottleRequestsException | |
| */ | |
| protected function buildException($key, $maxAttempts) | |
| { | |
| $retryAfter = $this->getTimeUntilNextRetry($key); | |
| $headers = $this->getHeaders( | |
| $maxAttempts, | |
| $this->calculateRemainingAttempts($key, $maxAttempts, $retryAfter), | |
| $retryAfter | |
| ); | |
| return new ThrottleRequestsException( | |
| 'Too Many Attempts.', null, $headers | |
| ); | |
| } | |
| /** | |
| * Get the number of seconds until the next retry. | |
| * | |
| * @param string $key | |
| * @return int | |
| */ | |
| protected function getTimeUntilNextRetry($key) | |
| { | |
| return $this->limiter->availableIn($key); | |
| } | |
| /** | |
| * Add the limit header information to the given response. | |
| * | |
| * @param \Symfony\Component\HttpFoundation\Response $response | |
| * @param int $maxAttempts | |
| * @param int $remainingAttempts | |
| * @param int|null $retryAfter | |
| * @return \Symfony\Component\HttpFoundation\Response | |
| */ | |
| protected function addHeaders(Response $response, $maxAttempts, $remainingAttempts, $retryAfter = null) | |
| { | |
| $response->headers->add( | |
| $this->getHeaders($maxAttempts, $remainingAttempts, $retryAfter) | |
| ); | |
| return $response; | |
| } | |
| /** | |
| * Get the limit headers information. | |
| * | |
| * @param int $maxAttempts | |
| * @param int $remainingAttempts | |
| * @param int|null $retryAfter | |
| * @return array | |
| */ | |
| protected function getHeaders($maxAttempts, $remainingAttempts, $retryAfter = null) | |
| { | |
| $headers = [ | |
| 'X-RateLimit-Limit' => $maxAttempts, | |
| 'X-RateLimit-Remaining' => $remainingAttempts, | |
| ]; | |
| if (! is_null($retryAfter)) { | |
| $headers['Retry-After'] = $retryAfter; | |
| $headers['X-RateLimit-Reset'] = $this->availableAt($retryAfter); | |
| } | |
| return $headers; | |
| } | |
| /** | |
| * Calculate the number of remaining attempts. | |
| * | |
| * @param string $key | |
| * @param int $maxAttempts | |
| * @param int|null $retryAfter | |
| * @return int | |
| */ | |
| protected function calculateRemainingAttempts($key, $maxAttempts, $retryAfter = null) | |
| { | |
| if (is_null($retryAfter)) { | |
| return $this->limiter->retriesLeft($key, $maxAttempts); | |
| } | |
| return 0; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment