1 | <?php |
||
2 | /** |
||
3 | * @link https://www.yiiframework.com/ |
||
4 | * @copyright Copyright (c) 2008 Yii Software LLC |
||
5 | * @license https://www.yiiframework.com/license/ |
||
6 | */ |
||
7 | |||
8 | namespace yii\filters; |
||
9 | |||
10 | use Closure; |
||
11 | use Yii; |
||
12 | use yii\base\ActionFilter; |
||
13 | use yii\web\Request; |
||
14 | use yii\web\Response; |
||
15 | use yii\web\TooManyRequestsHttpException; |
||
16 | |||
17 | /** |
||
18 | * RateLimiter implements a rate limiting algorithm based on the [leaky bucket algorithm](https://en.wikipedia.org/wiki/Leaky_bucket). |
||
19 | * |
||
20 | * You may use RateLimiter by attaching it as a behavior to a controller or module, like the following, |
||
21 | * |
||
22 | * ```php |
||
23 | * public function behaviors() |
||
24 | * { |
||
25 | * return [ |
||
26 | * 'rateLimiter' => [ |
||
27 | * 'class' => \yii\filters\RateLimiter::class, |
||
28 | * ], |
||
29 | * ]; |
||
30 | * } |
||
31 | * ``` |
||
32 | * |
||
33 | * When the user has exceeded his rate limit, RateLimiter will throw a [[TooManyRequestsHttpException]] exception. |
||
34 | * |
||
35 | * Note that RateLimiter requires [[user]] to implement the [[RateLimitInterface]]. RateLimiter will |
||
36 | * do nothing if [[user]] is not set or does not implement [[RateLimitInterface]]. |
||
37 | * |
||
38 | * @author Qiang Xue <[email protected]> |
||
39 | * @since 2.0 |
||
40 | */ |
||
41 | class RateLimiter extends ActionFilter |
||
42 | { |
||
43 | /** |
||
44 | * @var bool whether to include rate limit headers in the response |
||
45 | */ |
||
46 | public $enableRateLimitHeaders = true; |
||
47 | /** |
||
48 | * @var string the message to be displayed when rate limit exceeds |
||
49 | */ |
||
50 | public $errorMessage = 'Rate limit exceeded.'; |
||
51 | /** |
||
52 | * @var RateLimitInterface|Closure|null the user object that implements the RateLimitInterface. If not set, it will take the value of `Yii::$app->user->getIdentity(false)`. |
||
53 | * {@since 2.0.38} It's possible to provide a closure function in order to assign the user identity on runtime. Using a closure to assign the user identity is recommend |
||
54 | * when you are **not** using the standard `Yii::$app->user` component. See the example below: |
||
55 | * ```php |
||
56 | * 'user' => function() { |
||
57 | * return Yii::$app->apiUser->identity; |
||
58 | * } |
||
59 | * ``` |
||
60 | */ |
||
61 | public $user; |
||
62 | /** |
||
63 | * @var Request|null the current request. If not set, the `request` application component will be used. |
||
64 | */ |
||
65 | public $request; |
||
66 | /** |
||
67 | * @var Response|null the response to be sent. If not set, the `response` application component will be used. |
||
68 | */ |
||
69 | public $response; |
||
70 | |||
71 | |||
72 | /** |
||
73 | * {@inheritdoc} |
||
74 | */ |
||
75 | 14 | public function init() |
|
76 | { |
||
77 | 14 | if ($this->request === null) { |
|
78 | 13 | $this->request = Yii::$app->getRequest(); |
|
79 | } |
||
80 | 14 | if ($this->response === null) { |
|
81 | 13 | $this->response = Yii::$app->getResponse(); |
|
82 | } |
||
83 | 14 | } |
|
84 | |||
85 | /** |
||
86 | * {@inheritdoc} |
||
87 | */ |
||
88 | 5 | public function beforeAction($action) |
|
89 | { |
||
90 | 5 | if ($this->user === null && Yii::$app->getUser()) { |
|
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||
91 | 2 | $this->user = Yii::$app->getUser()->getIdentity(false); |
|
92 | } |
||
93 | |||
94 | 5 | if ($this->user instanceof Closure) { |
|
95 | 1 | $this->user = call_user_func($this->user, $action); |
|
96 | } |
||
97 | |||
98 | 5 | if ($this->user instanceof RateLimitInterface) { |
|
99 | 1 | Yii::debug('Check rate limit', __METHOD__); |
|
100 | 1 | $this->checkRateLimit($this->user, $this->request, $this->response, $action); |
|
101 | 4 | } elseif ($this->user) { |
|
102 | 2 | Yii::info('Rate limit skipped: "user" does not implement RateLimitInterface.', __METHOD__); |
|
103 | } else { |
||
104 | 2 | Yii::info('Rate limit skipped: user not logged in.', __METHOD__); |
|
105 | } |
||
106 | |||
107 | 5 | return true; |
|
108 | } |
||
109 | |||
110 | /** |
||
111 | * Checks whether the rate limit exceeds. |
||
112 | * @param RateLimitInterface $user the current user |
||
113 | * @param Request $request |
||
114 | * @param Response $response |
||
115 | * @param \yii\base\Action $action the action to be executed |
||
116 | * @throws TooManyRequestsHttpException if rate limit exceeds |
||
117 | */ |
||
118 | 3 | public function checkRateLimit($user, $request, $response, $action) |
|
119 | { |
||
120 | 3 | list($limit, $window) = $user->getRateLimit($request, $action); |
|
121 | 3 | list($allowance, $timestamp) = $user->loadAllowance($request, $action); |
|
122 | |||
123 | 3 | $current = time(); |
|
124 | |||
125 | 3 | $allowance += (int) (($current - $timestamp) * $limit / $window); |
|
126 | 3 | if ($allowance > $limit) { |
|
127 | $allowance = $limit; |
||
128 | } |
||
129 | |||
130 | 3 | if ($allowance < 1) { |
|
131 | 1 | $user->saveAllowance($request, $action, 0, $current); |
|
132 | 1 | $this->addRateLimitHeaders($response, $limit, 0, $window); |
|
133 | 1 | throw new TooManyRequestsHttpException($this->errorMessage); |
|
134 | } |
||
135 | |||
136 | 2 | $user->saveAllowance($request, $action, $allowance - 1, $current); |
|
137 | 2 | $this->addRateLimitHeaders($response, $limit, $allowance - 1, (int) (($limit - $allowance + 1) * $window / $limit)); |
|
138 | 2 | } |
|
139 | |||
140 | /** |
||
141 | * Adds the rate limit headers to the response. |
||
142 | * @param Response $response |
||
143 | * @param int $limit the maximum number of allowed requests during a period |
||
144 | * @param int $remaining the remaining number of allowed requests within the current period |
||
145 | * @param int $reset the number of seconds to wait before having maximum number of allowed requests again |
||
146 | */ |
||
147 | 5 | public function addRateLimitHeaders($response, $limit, $remaining, $reset) |
|
148 | { |
||
149 | 5 | if ($this->enableRateLimitHeaders) { |
|
150 | 4 | $response->getHeaders() |
|
151 | 4 | ->set('X-Rate-Limit-Limit', $limit) |
|
152 | 4 | ->set('X-Rate-Limit-Remaining', $remaining) |
|
153 | 4 | ->set('X-Rate-Limit-Reset', $reset); |
|
154 | } |
||
155 | 5 | } |
|
156 | } |
||
157 |