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
|
|
|
} |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* {@inheritdoc} |
87
|
|
|
*/ |
88
|
5 |
|
public function beforeAction($action) |
89
|
|
|
{ |
90
|
5 |
|
if ($this->user === null && Yii::$app->getUser()) { |
|
|
|
|
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
|
|
|
} |
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
|
|
|
} |
156
|
|
|
} |
157
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.