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