Fixes to ratelimiter and new features

This commit is contained in:
Daniel Hensby 2017-09-27 14:44:38 +01:00
parent 04b1bb816e
commit 51ac297c59
No known key found for this signature in database
GPG Key ID: B00D1E9767F0B06E
6 changed files with 110 additions and 14 deletions

View File

@ -17,12 +17,18 @@ SilverStripe\Core\Injector\Injector:
SilverStripe\Control\Middleware\TrustedProxyMiddleware:
properties:
TrustedProxyIPs: '`SS_TRUSTED_PROXY_IPS`'
SecurityRateLimitMiddleware:
class: SilverStripe\Control\Middleware\RateLimitMiddleware
properties:
ExtraKey: 'Security'
MaxAttempts: 10
Decay: 1
RateLimitedSecurityController:
class: SilverStripe\Control\Middleware\RequestHandlerMiddlewareAdapter
properties:
RequestHandler: '%$SilverStripe\Security\Security'
Middlewares:
- '%$SilverStripe\Control\Middleware\RateLimitMiddleware'
- '%$SecurityRateLimitMiddleware'
---
Name: errorrequestprocessors
After:

View File

@ -11,10 +11,18 @@ After:
---
SilverStripe\Control\Director:
rules:
'Security//$Action/$ID/$OtherID': '%$RateLimitedSecurityController'
'Security//$Action/$ID/$OtherID': SilverStripe\Security\Security
'CMSSecurity//$Action/$ID/$OtherID': SilverStripe\Security\CMSSecurity
'dev': SilverStripe\Dev\DevelopmentAdmin
'interactive': SilverStripe\Dev\SapphireREPL
'InstallerTest//$Action/$ID/$OtherID': SilverStripe\Dev\InstallerTest
'SapphireInfo//$Action/$ID/$OtherID': SilverStripe\Dev\SapphireInfo
'SapphireREPL//$Action/$ID/$OtherID': SilverStripe\Dev\SapphireREPL
---
Name: security-limited
Except:
environment: dev
---
SilverStripe\Control\Director:
rules:
'Security//$Action/$ID/$OtherID': '%$RateLimitedSecurityController'

View File

@ -774,6 +774,14 @@ class HTTPRequest implements ArrayAccess
return sizeof($this->dirParts) <= $this->unshiftedButParsedParts;
}
/**
* @return string Return the host from the request
*/
public function getHost()
{
return $this->getHeader('host');
}
/**
* Returns the client IP address which originated this request.
*

View File

@ -5,13 +5,27 @@ namespace SilverStripe\Control\Middleware;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\Cache\RateLimiter;
use SilverStripe\ErrorPage\ErrorPage;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\Security\Security;
class RateLimitMiddleware implements HTTPMiddleware
{
/**
* @var string Optional extra data to add to request key generation
*/
private $extraKey;
/**
* @var int Maximum number of attempts within the decay period
*/
private $maxAttempts = 10;
/**
* @var int The decay period (in minutes)
*/
private $decay = 1;
/**
* @param HTTPRequest $request
* @param callable $delegate
@ -19,7 +33,11 @@ class RateLimitMiddleware implements HTTPMiddleware
*/
public function process(HTTPRequest $request, callable $delegate)
{
$limiter = new RateLimiter($this->getKeyFromRequest($request), 10, 1);
$limiter = RateLimiter::create(
$this->getKeyFromRequest($request),
$this->getMaxAttempts(),
$this->getDecay()
);
if ($limiter->canAccess()) {
$limiter->hit();
$response = $delegate($request);
@ -36,11 +54,14 @@ class RateLimitMiddleware implements HTTPMiddleware
*/
protected function getKeyFromRequest($request)
{
$domain = parse_url($request->getURL(), PHP_URL_HOST);
$key = $this->getExtraKey() ? $this->getExtraKey() . '-' : '';
$key .= $request->getHost() . '-';
if ($currentUser = Security::getCurrentUser()) {
return md5($domain . '-' . $currentUser->ID);
$key .= $currentUser->ID;
} else {
$key .= $request->getIP();
}
return md5($domain . '-' . $request->getIP());
return md5($key);
}
/**
@ -48,11 +69,7 @@ class RateLimitMiddleware implements HTTPMiddleware
*/
protected function getErrorHTTPResponse()
{
$response = null;
if (class_exists(ErrorPage::class)) {
$response = ErrorPage::response_for(429);
}
return $response ?: new HTTPResponse('<h1>429 - Too many requests</h1>', 429);
return HTTPResponse::create('<h1>429 - Too many requests</h1>', 429);
}
/**
@ -69,4 +86,58 @@ class RateLimitMiddleware implements HTTPMiddleware
$response->addHeader('Retry-After', $ttl);
}
}
/**
* @param string $key
* @return $this
*/
public function setExtraKey($key)
{
$this->extraKey = $key;
return $this;
}
/**
* @return string
*/
public function getExtraKey()
{
return $this->extraKey;
}
/**
* @param int $maxAttempts
* @return $this
*/
public function setMaxAttempts($maxAttempts)
{
$this->maxAttempts = $maxAttempts;
return $this;
}
/**
* @return int
*/
public function getMaxAttempts()
{
return $this->maxAttempts;
}
/**
* @param int $decay Time in minutes
* @return $this
*/
public function setDecay($decay)
{
$this->decay = $decay;
return $this;
}
/**
* @return int
*/
public function getDecay()
{
return $this->decay;
}
}

View File

@ -3,11 +3,14 @@
namespace SilverStripe\Core\Cache;
use Psr\SimpleCache\CacheInterface;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\ORM\FieldType\DBDatetime;
class RateLimiter
{
use Injectable;
/**
* @var CacheInterface
*/
@ -24,7 +27,7 @@ class RateLimiter
private $maxAttempts;
/**
* @var int How long the rate limit lasts for
* @var int How long the rate limit lasts for in minutes
*/
private $decay;

View File

@ -1090,7 +1090,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase implements TestOnly
*/
public static function getExtraControllers()
{
return array_merge([Security::class], static::$extra_controllers);
return static::$extra_controllers;
}
/**