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
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\filters;
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](http://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::className(),
* ],
* ];
* }
* ```
*
* 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 <qiang.xue@gmail.com>
* @since 2.0
*/
class RateLimiter extends ActionFilter
{
/**
* @var boolean 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 the user object that implements the RateLimitInterface.
* If not set, it will take the value of `Yii::$app->user->getIdentity(false)`.
*/
public $user;
/**
* @var Request the current request. If not set, the `request` application component will be used.
*/
public $request;
/**
* @var Response the response to be sent. If not set, the `response` application component will be used.
*/
public $response;
/**
* @inheritdoc
*/
public function beforeAction($action)
{
$user = $this->user ? : Yii::$app->getUser()->getIdentity(false);
if ($user instanceof RateLimitInterface) {
Yii::trace('Check rate limit', __METHOD__);
$this->checkRateLimit(
$user,
$this->request ? : Yii::$app->getRequest(),
$this->response ? : Yii::$app->getResponse(),
$action
);
} elseif ($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)
{
$current = time();
list ($limit, $window) = $user->getRateLimit($request, $action);
list ($allowance, $timestamp) = $user->loadAllowance($request, $action);
$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);
} else {
$user->saveAllowance($request, $action, $allowance - 1, $current);
$this->addRateLimitHeaders($response, $limit, $allowance - 1, (int) (($limit - $allowance) * $window / $limit));
}
}
/**
* Adds the rate limit headers to the response
* @param Response $response
* @param integer $limit the maximum number of allowed requests during a period
* @param integer $remaining the remaining number of allowed requests within the current period
* @param integer $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);
}
}
}