RateLimiter.php 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. <?php
  2. namespace Illuminate\Cache;
  3. use Closure;
  4. use Illuminate\Contracts\Cache\Repository as Cache;
  5. use Illuminate\Support\InteractsWithTime;
  6. class RateLimiter
  7. {
  8. use InteractsWithTime;
  9. /**
  10. * The cache store implementation.
  11. *
  12. * @var \Illuminate\Contracts\Cache\Repository
  13. */
  14. protected $cache;
  15. /**
  16. * The configured limit object resolvers.
  17. *
  18. * @var array
  19. */
  20. protected $limiters = [];
  21. /**
  22. * Create a new rate limiter instance.
  23. *
  24. * @param \Illuminate\Contracts\Cache\Repository $cache
  25. * @return void
  26. */
  27. public function __construct(Cache $cache)
  28. {
  29. $this->cache = $cache;
  30. }
  31. /**
  32. * Register a named limiter configuration.
  33. *
  34. * @param string $name
  35. * @param \Closure $callback
  36. * @return $this
  37. */
  38. public function for(string $name, Closure $callback)
  39. {
  40. $this->limiters[$name] = $callback;
  41. return $this;
  42. }
  43. /**
  44. * Get the given named rate limiter.
  45. *
  46. * @param string $name
  47. * @return \Closure|null
  48. */
  49. public function limiter(string $name)
  50. {
  51. return $this->limiters[$name] ?? null;
  52. }
  53. /**
  54. * Attempts to execute a callback if it's not limited.
  55. *
  56. * @param string $key
  57. * @param int $maxAttempts
  58. * @param \Closure $callback
  59. * @param int $decaySeconds
  60. * @return mixed
  61. */
  62. public function attempt($key, $maxAttempts, Closure $callback, $decaySeconds = 60)
  63. {
  64. if ($this->tooManyAttempts($key, $maxAttempts)) {
  65. return false;
  66. }
  67. if (is_null($result = $callback())) {
  68. $result = true;
  69. }
  70. return tap($result, function () use ($key, $decaySeconds) {
  71. $this->hit($key, $decaySeconds);
  72. });
  73. }
  74. /**
  75. * Determine if the given key has been "accessed" too many times.
  76. *
  77. * @param string $key
  78. * @param int $maxAttempts
  79. * @return bool
  80. */
  81. public function tooManyAttempts($key, $maxAttempts)
  82. {
  83. if ($this->attempts($key) >= $maxAttempts) {
  84. if ($this->cache->has($this->cleanRateLimiterKey($key).':timer')) {
  85. return true;
  86. }
  87. $this->resetAttempts($key);
  88. }
  89. return false;
  90. }
  91. /**
  92. * Increment (by 1) the counter for a given key for a given decay time.
  93. *
  94. * @param string $key
  95. * @param int $decaySeconds
  96. * @return int
  97. */
  98. public function hit($key, $decaySeconds = 60)
  99. {
  100. return $this->increment($key, $decaySeconds);
  101. }
  102. /**
  103. * Increment the counter for a given key for a given decay time by a given amount.
  104. *
  105. * @param string $key
  106. * @param int $decaySeconds
  107. * @param int $amount
  108. * @return int
  109. */
  110. public function increment($key, $decaySeconds = 60, $amount = 1)
  111. {
  112. $key = $this->cleanRateLimiterKey($key);
  113. $this->cache->add(
  114. $key.':timer', $this->availableAt($decaySeconds), $decaySeconds
  115. );
  116. $added = $this->cache->add($key, 0, $decaySeconds);
  117. $hits = (int) $this->cache->increment($key, $amount);
  118. if (! $added && $hits == 1) {
  119. $this->cache->put($key, 1, $decaySeconds);
  120. }
  121. return $hits;
  122. }
  123. /**
  124. * Get the number of attempts for the given key.
  125. *
  126. * @param string $key
  127. * @return mixed
  128. */
  129. public function attempts($key)
  130. {
  131. $key = $this->cleanRateLimiterKey($key);
  132. return $this->cache->get($key, 0);
  133. }
  134. /**
  135. * Reset the number of attempts for the given key.
  136. *
  137. * @param string $key
  138. * @return mixed
  139. */
  140. public function resetAttempts($key)
  141. {
  142. $key = $this->cleanRateLimiterKey($key);
  143. return $this->cache->forget($key);
  144. }
  145. /**
  146. * Get the number of retries left for the given key.
  147. *
  148. * @param string $key
  149. * @param int $maxAttempts
  150. * @return int
  151. */
  152. public function remaining($key, $maxAttempts)
  153. {
  154. $key = $this->cleanRateLimiterKey($key);
  155. $attempts = $this->attempts($key);
  156. return $maxAttempts - $attempts;
  157. }
  158. /**
  159. * Get the number of retries left for the given key.
  160. *
  161. * @param string $key
  162. * @param int $maxAttempts
  163. * @return int
  164. */
  165. public function retriesLeft($key, $maxAttempts)
  166. {
  167. return $this->remaining($key, $maxAttempts);
  168. }
  169. /**
  170. * Clear the hits and lockout timer for the given key.
  171. *
  172. * @param string $key
  173. * @return void
  174. */
  175. public function clear($key)
  176. {
  177. $key = $this->cleanRateLimiterKey($key);
  178. $this->resetAttempts($key);
  179. $this->cache->forget($key.':timer');
  180. }
  181. /**
  182. * Get the number of seconds until the "key" is accessible again.
  183. *
  184. * @param string $key
  185. * @return int
  186. */
  187. public function availableIn($key)
  188. {
  189. $key = $this->cleanRateLimiterKey($key);
  190. return max(0, $this->cache->get($key.':timer') - $this->currentTime());
  191. }
  192. /**
  193. * Clean the rate limiter key from unicode characters.
  194. *
  195. * @param string $key
  196. * @return string
  197. */
  198. public function cleanRateLimiterKey($key)
  199. {
  200. return preg_replace('/&([a-z])[a-z]+;/i', '$1', htmlentities($key));
  201. }
  202. }