mirror of https://github.com/MISP/MISP
Merge pull request #9497 from JakubOnderka/rate-limit-fix
fix: [internal] Rate limitingpull/9498/head
commit
b4602e74f1
|
@ -830,29 +830,34 @@ class AppController extends Controller
|
|||
|
||||
private function __rateLimitCheck(array $user)
|
||||
{
|
||||
$info = array();
|
||||
$rateLimitCheck = $this->RateLimit->check(
|
||||
$user,
|
||||
$this->request->params['controller'],
|
||||
$this->request->action,
|
||||
$info,
|
||||
$this->response->type()
|
||||
$this->request->params['action'],
|
||||
);
|
||||
if (!empty($info)) {
|
||||
$this->RestResponse->setHeader('X-Rate-Limit-Limit', $info['limit']);
|
||||
$this->RestResponse->setHeader('X-Rate-Limit-Remaining', $info['remaining']);
|
||||
$this->RestResponse->setHeader('X-Rate-Limit-Reset', $info['reset']);
|
||||
|
||||
if ($rateLimitCheck) {
|
||||
$headers = [
|
||||
'X-Rate-Limit-Limit' => $rateLimitCheck['limit'],
|
||||
'X-Rate-Limit-Remaining' => $rateLimitCheck['remaining'],
|
||||
'X-Rate-Limit-Reset' => $rateLimitCheck['reset'],
|
||||
];
|
||||
|
||||
if ($rateLimitCheck['exceeded']) {
|
||||
$response = $this->RestResponse->throwException(
|
||||
429,
|
||||
__('Rate limit exceeded.'),
|
||||
'/' . $this->request->params['controller'] . '/' . $this->request->params['action'],
|
||||
false,
|
||||
false,
|
||||
$headers
|
||||
);
|
||||
$response->send();
|
||||
$this->_stop();
|
||||
} else {
|
||||
$this->RestResponse->headers = array_merge($this->RestResponse->headers, $headers);
|
||||
}
|
||||
}
|
||||
if ($rateLimitCheck !== true) {
|
||||
$this->response->header('X-Rate-Limit-Limit', $info['limit']);
|
||||
$this->response->header('X-Rate-Limit-Remaining', $info['remaining']);
|
||||
$this->response->header('X-Rate-Limit-Reset', $info['reset']);
|
||||
$this->response->body($rateLimitCheck);
|
||||
$this->response->statusCode(429);
|
||||
$this->response->send();
|
||||
$this->_stop();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function afterFilter()
|
||||
|
|
|
@ -12,58 +12,60 @@ class RateLimitComponent extends Component
|
|||
)
|
||||
);
|
||||
|
||||
public $components = array('RestResponse');
|
||||
|
||||
/**
|
||||
* @param array $user
|
||||
* @param string $controller
|
||||
* @param string $action
|
||||
* @param array $info
|
||||
* @param string $responseType
|
||||
* @return bool
|
||||
* @return array|null
|
||||
* @throws RedisException
|
||||
*/
|
||||
public function check(array $user, $controller, $action, &$info = array(), $responseType)
|
||||
public function check(array $user, $controller, $action)
|
||||
{
|
||||
if (!empty($user['Role']['enforce_rate_limit']) && isset(self::LIMITED_FUNCTIONS[$controller][$action])) {
|
||||
if ($user['Role']['rate_limit_count'] == 0) {
|
||||
throw new MethodNotAllowedException(__('API searches are not allowed for this user role.'));
|
||||
}
|
||||
try {
|
||||
$redis = RedisTool::init();
|
||||
} catch (Exception $e) {
|
||||
return true; // redis is not available, allow access
|
||||
}
|
||||
$uuid = Configure::read('MISP.uuid') ?: 'no-uuid';
|
||||
$keyName = 'misp:' . $uuid . ':rate_limit:' . $user['id'];
|
||||
$count = $redis->get($keyName);
|
||||
if ($count !== false && $count >= $user['Role']['rate_limit_count']) {
|
||||
$info = array(
|
||||
'limit' => $user['Role']['rate_limit_count'],
|
||||
'reset' => $redis->ttl($keyName),
|
||||
'remaining' => $user['Role']['rate_limit_count'] - $count,
|
||||
);
|
||||
return $this->RestResponse->throwException(
|
||||
429,
|
||||
__('Rate limit exceeded.'),
|
||||
'/' . $controller . '/' . $action,
|
||||
$responseType
|
||||
);
|
||||
} else {
|
||||
if ($count === false) {
|
||||
$redis->setEx($keyName, 900, 1);
|
||||
} else {
|
||||
$redis->setEx($keyName, $redis->ttl($keyName), intval($count) + 1);
|
||||
}
|
||||
}
|
||||
$count += 1;
|
||||
$info = array(
|
||||
'limit' => $user['Role']['rate_limit_count'],
|
||||
'reset' => $redis->ttl($keyName),
|
||||
'remaining' => $user['Role']['rate_limit_count'] - $count
|
||||
);
|
||||
|
||||
if (!isset(self::LIMITED_FUNCTIONS[$controller][$action])) {
|
||||
return null; // no limit enforced for this controller action
|
||||
}
|
||||
return true;
|
||||
|
||||
if (empty($user['Role']['enforce_rate_limit'])) {
|
||||
return null; // no limit enforced for this role
|
||||
}
|
||||
|
||||
$rateLimit = (int)$user['Role']['rate_limit_count'];
|
||||
if ($rateLimit === 0) {
|
||||
throw new MethodNotAllowedException(__('API searches are not allowed for this user role.'));
|
||||
}
|
||||
|
||||
try {
|
||||
$redis = RedisTool::init();
|
||||
} catch (Exception $e) {
|
||||
return null; // redis is not available, allow access
|
||||
}
|
||||
|
||||
$uuid = Configure::read('MISP.uuid') ?: 'no-uuid';
|
||||
$keyName = 'misp:' . $uuid . ':rate_limit:' . $user['id'];
|
||||
$count = $redis->get($keyName);
|
||||
|
||||
if ($count !== false && $count >= $rateLimit) {
|
||||
return [
|
||||
'exceeded' => true,
|
||||
'limit' => $rateLimit,
|
||||
'reset' => $redis->ttl($keyName),
|
||||
'remaining' => $rateLimit - $count,
|
||||
];
|
||||
}
|
||||
|
||||
$newCount = $redis->incr($keyName);
|
||||
if ($newCount === 1) {
|
||||
$redis->expire($keyName, 900);
|
||||
$reset = 900;
|
||||
} else {
|
||||
$reset = $redis->ttl($keyName);
|
||||
}
|
||||
|
||||
return [
|
||||
'exceeded' => false,
|
||||
'limit' => $rateLimit,
|
||||
'reset' => $reset,
|
||||
'remaining' => $rateLimit - $newCount,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -517,7 +517,7 @@ class RestResponseComponent extends Component
|
|||
if ($id) {
|
||||
$response['id'] = $id;
|
||||
}
|
||||
return $this->__sendResponse($response, 403, $format);
|
||||
return $this->prepareResponse($response, 403, $format);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -562,7 +562,7 @@ class RestResponseComponent extends Component
|
|||
if ($id) {
|
||||
$response['id'] = $id;
|
||||
}
|
||||
return $this->__sendResponse($response, 200, $format);
|
||||
return $this->prepareResponse($response, 200, $format);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -587,7 +587,7 @@ class RestResponseComponent extends Component
|
|||
* @return CakeResponse
|
||||
* @throws Exception
|
||||
*/
|
||||
private function __sendResponse($response, $code, $format = false, $raw = false, $download = false, $headers = array())
|
||||
private function prepareResponse($response, $code, $format = false, $raw = false, $download = false, $headers = array())
|
||||
{
|
||||
App::uses('TmpFileTool', 'Tools');
|
||||
$format = !empty($format) ? strtolower($format) : 'json';
|
||||
|
@ -775,7 +775,7 @@ class RestResponseComponent extends Component
|
|||
if (!empty($errors)) {
|
||||
$data['errors'] = $errors;
|
||||
}
|
||||
return $this->__sendResponse($data, 200, $format, $raw, $download, $headers);
|
||||
return $this->prepareResponse($data, 200, $format, $raw, $download, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -807,7 +807,7 @@ class RestResponseComponent extends Component
|
|||
'message' => $message,
|
||||
'url' => $url
|
||||
);
|
||||
return $this->__sendResponse($message, $code, $format, $raw, false, $headers);
|
||||
return $this->prepareResponse($message, $code, $format, $raw, false, $headers);
|
||||
}
|
||||
|
||||
public function setHeader($header, $value)
|
||||
|
@ -834,7 +834,7 @@ class RestResponseComponent extends Component
|
|||
}
|
||||
}
|
||||
$response['url'] = $this->__generateURL($actionArray, $controller, $params);
|
||||
return $this->__sendResponse($response, 200, $format);
|
||||
return $this->prepareResponse($response, 200, $format);
|
||||
}
|
||||
|
||||
private function __setup()
|
||||
|
|
Loading…
Reference in New Issue