From 64370f1ad60358e33c2802bf56fff7721271b156 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 3 Mar 2014 22:01:58 -0500 Subject: [PATCH] Implemented rate limiter. --- framework/rest/Controller.php | 25 ++++++-- framework/rest/RateLimitInterface.php | 39 ++++++++++++ framework/rest/RateLimiter.php | 85 +++++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 framework/rest/RateLimitInterface.php create mode 100644 framework/rest/RateLimiter.php diff --git a/framework/rest/Controller.php b/framework/rest/Controller.php index 12c1783262..aec2cfb79a 100644 --- a/framework/rest/Controller.php +++ b/framework/rest/Controller.php @@ -48,6 +48,12 @@ class Controller extends \yii\web\Controller * authentication methods, each represented by an authentication class or configuration. */ public $authMethods = ['yii\rest\HttpBasicAuth', 'yii\rest\HttpBearerAuth', 'yii\rest\QueryParamAuth']; + /** + * @var string|array the rate limiter class or configuration. If this is not set or empty, + * the rate limiting will be disabled. + * @see checkRateLimit() + */ + public $rateLimiter = 'yii\rest\RateLimiter'; /** * @var string the chosen API version number * @see supportedVersions @@ -186,15 +192,26 @@ class Controller extends \yii\web\Controller /** * Ensures the rate limit is not exceeded. - * You may override this method to log the API usage and make sure the rate limit is not exceeded. - * If exceeded, you should throw a [[TooManyRequestsHttpException]], and you may also send some HTTP headers, - * such as `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, - * to explain the rate limit information. + * + * This method will use [[rateLimiter]] to check rate limit. In order to perform rate limiting check, + * the user must be authenticated and the user identity object (`Yii::$app->user->identity`) must + * implement [[RateLimitInterface]]. + * * @param \yii\base\Action $action the action to be executed * @throws TooManyRequestsHttpException if the rate limit is exceeded. */ protected function checkRateLimit($action) { + if (empty($this->rateLimiter)) { + return; + } + + $identity = Yii::$app->getUser()->getIdentity(false); + if ($identity instanceof RateLimitInterface) { + /** @var RateLimiter $rateLimiter */ + $rateLimiter = Yii::createObject($this->rateLimiter); + $rateLimiter->check($identity, Yii::$app->getRequest(), Yii::$app->getResponse(), $action); + } } /** diff --git a/framework/rest/RateLimitInterface.php b/framework/rest/RateLimitInterface.php new file mode 100644 index 0000000000..07f60e0270 --- /dev/null +++ b/framework/rest/RateLimitInterface.php @@ -0,0 +1,39 @@ + + * @since 2.0 + */ +interface RateLimitInterface +{ + /** + * Returns the maximum number of allowed requests and the window size. + * @param array $params the additional parameters associated with the rate limit. + * @return array an array of two elements. The first element is the maximum number of allowed requests, + * and the second element is the size of the window in seconds. + */ + public function getRateLimit($params = []); + /** + * Loads the number of allowed requests and the corresponding timestamp from a persistent storage. + * @param array $params the additional parameters associated with the rate limit. + * @return array an array of two elements. The first element is the number of allowed requests, + * and the second element is the corresponding UNIX timestamp. + */ + public function loadAllowance($params = []); + /** + * Saves the number of allowed requests and the corresponding timestamp to a persistent storage. + * @param integer $allowance the number of allowed requests remaining. + * @param integer $timestamp the current timestamp. + * @param array $params the additional parameters associated with the rate limit. + */ + public function saveAllowance($allowance, $timestamp, $params = []); +} diff --git a/framework/rest/RateLimiter.php b/framework/rest/RateLimiter.php new file mode 100644 index 0000000000..753a0f0c67 --- /dev/null +++ b/framework/rest/RateLimiter.php @@ -0,0 +1,85 @@ + + * @since 2.0 + */ +class RateLimiter extends Component +{ + /** + * @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.'; + + /** + * Checks whether the rate limit exceeds. + * @param RateLimitInterface $user the current user + * @param Request $request + * @param Response $response + * @param Action $action the action to be executed + * @throws TooManyRequestsHttpException if rate limit exceeds + */ + public function check($user, $request, $response, $action) + { + $current = time(); + $params = [ + 'request' => $request, + 'action' => $action, + ]; + + list ($limit, $window) = $user->getRateLimit($params); + list ($allowance, $timestamp) = $user->loadAllowance($params); + + $allowance += (int)(($current - $timestamp) * $limit / $window); + if ($allowance > $limit) { + $allowance = $limit; + } + + if ($allowance < 1) { + $user->saveAllowance(0, $current, $params); + $this->addRateLimitHeaders($response, $limit, 0, $window); + throw new TooManyRequestsHttpException($this->errorMessage); + } else { + $user->saveAllowance($allowance - 1, $current, $params); + $this->addRateLimitHeaders($response, $limit, 0, (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 + */ + protected 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); + } + } +}