mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Merge pull request #7373 from dhensby/pulls/4/rate-limit-security
NEW RateLimiter for Security controller
This commit is contained in:
commit
da27948777
@ -18,3 +18,7 @@ SilverStripe\Core\Injector\Injector:
|
||||
factory: SilverStripe\Core\Cache\CacheFactory
|
||||
constructor:
|
||||
namespace: "VersionProvider_composerlock"
|
||||
Psr\SimpleCache\CacheInterface.RateLimiter:
|
||||
factory: SilverStripe\Core\Cache\CacheFactory
|
||||
constructor:
|
||||
namespace: 'ratelimiter'
|
||||
|
@ -21,7 +21,8 @@ SilverStripe\Core\Manifest\ModuleManifest:
|
||||
- $project
|
||||
---
|
||||
Name: modules-framework
|
||||
After: modules-other
|
||||
After:
|
||||
- '#modules-other'
|
||||
---
|
||||
SilverStripe\Core\Manifest\ModuleManifest:
|
||||
module_priority:
|
||||
|
@ -17,10 +17,22 @@ 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:
|
||||
- '%$SecurityRateLimitMiddleware'
|
||||
---
|
||||
Name: errorrequestprocessors
|
||||
After:
|
||||
- requestprocessors
|
||||
- '#requestprocessors'
|
||||
---
|
||||
SilverStripe\Core\Injector\Injector:
|
||||
# Note: If Director config changes, take note it will affect this config too
|
||||
|
@ -18,3 +18,13 @@ SilverStripe\Control\Director:
|
||||
'InstallerTest//$Action/$ID/$OtherID': SilverStripe\Dev\InstallerTest
|
||||
'SapphireInfo//$Action/$ID/$OtherID': SilverStripe\Dev\SapphireInfo
|
||||
'SapphireREPL//$Action/$ID/$OtherID': SilverStripe\Dev\SapphireREPL
|
||||
---
|
||||
Name: security-limited
|
||||
After:
|
||||
- '#rootroutes'
|
||||
Except:
|
||||
environment: dev
|
||||
---
|
||||
SilverStripe\Control\Director:
|
||||
rules:
|
||||
'Security//$Action/$ID/$OtherID': '%$RateLimitedSecurityController'
|
||||
|
@ -19,7 +19,7 @@ SilverStripe\Core\Injector\Injector:
|
||||
---
|
||||
Name: coresecurity
|
||||
After:
|
||||
- requestprocessors
|
||||
- '#requestprocessors'
|
||||
---
|
||||
SilverStripe\Core\Injector\Injector:
|
||||
SilverStripe\Control\Director:
|
||||
|
72
docs/en/02_Developer_Guides/09_Security/05_Rate_Limiting.md
Normal file
72
docs/en/02_Developer_Guides/09_Security/05_Rate_Limiting.md
Normal file
@ -0,0 +1,72 @@
|
||||
title: Rate Limiting
|
||||
summary: SilverStripe's in built rate limiting features
|
||||
|
||||
# Rate Limiting
|
||||
|
||||
SilverStripe Framework comes with a [Middleware](developer_guides/controllers/middlewares/) that provides rate limiting
|
||||
for the Security controller. This provides added protection to a potentially vulnerable part of a SilverStripe application
|
||||
where an attacker is free to bombard your login forms or other Security endpoints.
|
||||
|
||||
## Applying rate limiting to controllers
|
||||
|
||||
You can apply rate limiting to other specific controllers or your entire SilverStripe application. When applying rate
|
||||
limiting to other controllers you can define custom limits for each controller.
|
||||
|
||||
First, you need to define your rate limit middleware with the required settings:
|
||||
|
||||
```yml
|
||||
SilverStripe\Core\Injector\Injector:
|
||||
MyRateLimitMiddleware:
|
||||
class: SilverStripe\Control\Middleware\RateLimitMiddleware
|
||||
properties:
|
||||
ExtraKey: 'mylimiter' # this isolates your rate limiter from others
|
||||
MaxAttempts: 10 # how many attempts are allowed in a decay period
|
||||
Decay: 1 # how long the decay period is in minutes
|
||||
```
|
||||
|
||||
Next, you need to define your request handler which will apply the middleware to the controller:
|
||||
|
||||
```yml
|
||||
SilverStripe\Core\Injector\Injector:
|
||||
MyRateLimitedController:
|
||||
class: SilverStripe\Control\Middleware\RequestHandlerMiddlewareAdapter
|
||||
properties:
|
||||
RequestHandler: '%$MyController' # the fully qualified class name of your controller
|
||||
Middlewares:
|
||||
- '%$MyRateLimitMiddleware' # the rate limiter we just defined in the last step
|
||||
```
|
||||
|
||||
Finally, you need to define the custom routing:
|
||||
|
||||
```yml
|
||||
Director:
|
||||
rules:
|
||||
'MyController//$Action/$ID/$OtherID': '%$MyRateLimitedController'
|
||||
```
|
||||
|
||||
## Applying rate limiting across an entire application
|
||||
|
||||
If you'd like to add rate limiting to an entire application (ie: across all routes) then you'll need to define your rate
|
||||
limit middleware much like the first step outlined in the previous section and then you'll have to apply it to the entire
|
||||
site as you would with any other middleware:
|
||||
|
||||
```yml
|
||||
SilverStripe\Core\Injector\Injector:
|
||||
SilverStripe\Control\Director:
|
||||
properties:
|
||||
Middlewares:
|
||||
SiteWideRateLimitMiddleware: '%$SiteWideRateLimitMiddleware'
|
||||
```
|
||||
|
||||
## Disabling the Rate Limiter
|
||||
|
||||
You may already solve the rate limiting problem on a server level and the built in rate limiting may well be redundant.
|
||||
If this is the case you can turn off the rate limiting middleware by redefining the URL rules for the Security controller.
|
||||
|
||||
Add the following to your config.yml:
|
||||
|
||||
```yml
|
||||
SilverStripe\Control\Director:
|
||||
rules:
|
||||
'Security//$Action/$ID/$OtherID': SilverStripe\Security\Security
|
||||
```
|
@ -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.
|
||||
*
|
||||
|
168
src/Control/Middleware/RateLimitMiddleware.php
Normal file
168
src/Control/Middleware/RateLimitMiddleware.php
Normal file
@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Control\Middleware;
|
||||
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
use SilverStripe\Control\HTTPResponse;
|
||||
use SilverStripe\Core\Cache\RateLimiter;
|
||||
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;
|
||||
|
||||
/**
|
||||
* @var RateLimiter|null
|
||||
*/
|
||||
private $rateLimiter;
|
||||
|
||||
/**
|
||||
* @param HTTPRequest $request
|
||||
* @param callable $delegate
|
||||
* @return HTTPResponse
|
||||
*/
|
||||
public function process(HTTPRequest $request, callable $delegate)
|
||||
{
|
||||
if (!$limiter = $this->getRateLimiter()) {
|
||||
$limiter = RateLimiter::create(
|
||||
$this->getKeyFromRequest($request),
|
||||
$this->getMaxAttempts(),
|
||||
$this->getDecay()
|
||||
);
|
||||
}
|
||||
if ($limiter->canAccess()) {
|
||||
$limiter->hit();
|
||||
$response = $delegate($request);
|
||||
} else {
|
||||
$response = $this->getErrorHTTPResponse();
|
||||
}
|
||||
$this->addHeadersToResponse($response, $limiter);
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param HTTPRequest $request
|
||||
* @return string
|
||||
*/
|
||||
protected function getKeyFromRequest($request)
|
||||
{
|
||||
$key = $this->getExtraKey() ? $this->getExtraKey() . '-' : '';
|
||||
$key .= $request->getHost() . '-';
|
||||
if ($currentUser = Security::getCurrentUser()) {
|
||||
$key .= $currentUser->ID;
|
||||
} else {
|
||||
$key .= $request->getIP();
|
||||
}
|
||||
return md5($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HTTPResponse
|
||||
*/
|
||||
protected function getErrorHTTPResponse()
|
||||
{
|
||||
return HTTPResponse::create('<h1>429 - Too many requests</h1>', 429);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param HTTPResponse $response
|
||||
* @param RateLimiter $limiter
|
||||
*/
|
||||
protected function addHeadersToResponse($response, $limiter)
|
||||
{
|
||||
$response->addHeader('X-RateLimit-Limit', $limiter->getMaxAttempts());
|
||||
$response->addHeader('X-RateLimit-Remaining', $remaining = $limiter->getNumAttemptsRemaining());
|
||||
$ttl = $limiter->getTimeToReset();
|
||||
$response->addHeader('X-RateLimit-Reset', DBDatetime::now()->getTimestamp() + $ttl);
|
||||
if ($remaining <= 0) {
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RateLimiter $rateLimiter
|
||||
* @return $this
|
||||
*/
|
||||
public function setRateLimiter($rateLimiter)
|
||||
{
|
||||
$this->rateLimiter = $rateLimiter;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RateLimiter|null
|
||||
*/
|
||||
public function getRateLimiter()
|
||||
{
|
||||
return $this->rateLimiter;
|
||||
}
|
||||
}
|
193
src/Core/Cache/RateLimiter.php
Normal file
193
src/Core/Cache/RateLimiter.php
Normal file
@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
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
|
||||
*/
|
||||
private $cache;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $identifier;
|
||||
|
||||
/**
|
||||
* @var int Maximum number of attempts allowed
|
||||
*/
|
||||
private $maxAttempts;
|
||||
|
||||
/**
|
||||
* @var int How long the rate limit lasts for in minutes
|
||||
*/
|
||||
private $decay;
|
||||
|
||||
/**
|
||||
* RateLimiter constructor.
|
||||
* @param string $identifier
|
||||
* @param int $maxAttempts
|
||||
* @param int $decay
|
||||
*/
|
||||
public function __construct($identifier, $maxAttempts, $decay)
|
||||
{
|
||||
$this->setIdentifier($identifier);
|
||||
$this->setMaxAttempts($maxAttempts);
|
||||
$this->setDecay($decay);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CacheInterface
|
||||
*/
|
||||
public function getCache()
|
||||
{
|
||||
if (!$this->cache) {
|
||||
$this->setCache(Injector::inst()->create(CacheInterface::class . '.RateLimiter'));
|
||||
}
|
||||
return $this->cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CacheInterface $cache
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setCache($cache)
|
||||
{
|
||||
$this->cache = $cache;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getIdentifier()
|
||||
{
|
||||
return $this->identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $identifier
|
||||
* @return $this
|
||||
*/
|
||||
public function setIdentifier($identifier)
|
||||
{
|
||||
$this->identifier = $identifier;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getMaxAttempts()
|
||||
{
|
||||
return $this->maxAttempts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $maxAttempts
|
||||
* @return $this
|
||||
*/
|
||||
public function setMaxAttempts($maxAttempts)
|
||||
{
|
||||
$this->maxAttempts = $maxAttempts;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getDecay()
|
||||
{
|
||||
return $this->decay;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $decay
|
||||
* @return $this
|
||||
*/
|
||||
public function setDecay($decay)
|
||||
{
|
||||
$this->decay = $decay;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getNumAttempts()
|
||||
{
|
||||
return $this->getCache()->get($this->getIdentifier(), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getNumAttemptsRemaining()
|
||||
{
|
||||
return max(0, $this->getMaxAttempts() - $this->getNumAttempts());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getTimeToReset()
|
||||
{
|
||||
if ($expiry = $this->getCache()->get($this->getIdentifier() . '-timer')) {
|
||||
return $expiry - DBDatetime::now()->getTimestamp();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function clearAttempts()
|
||||
{
|
||||
$this->getCache()->delete($this->getIdentifier());
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a hit in the rate limit cache
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function hit()
|
||||
{
|
||||
if (!$this->getCache()->has($this->getIdentifier())) {
|
||||
$ttl = $this->getDecay() * 60;
|
||||
$expiry = DBDatetime::now()->getTimestamp() + $ttl;
|
||||
$this->getCache()->set($this->getIdentifier() . '-timer', $expiry, $ttl);
|
||||
} else {
|
||||
$expiry = $this->getCache()->get($this->getIdentifier() . '-timer');
|
||||
$ttl = $expiry - DBDatetime::now()->getTimestamp();
|
||||
}
|
||||
$this->getCache()->set($this->getIdentifier(), $this->getNumAttempts() + 1, $ttl);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function canAccess()
|
||||
{
|
||||
if ($this->getNumAttempts() >= $this->getMaxAttempts()) {
|
||||
// if the timer cache item still exists then they are locked out
|
||||
if ($this->getCache()->has($this->getIdentifier() . '-timer')) {
|
||||
return false;
|
||||
}
|
||||
// the timer key has expired so we can clear their attempts and start again
|
||||
$this->clearAttempts();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
@ -190,14 +190,15 @@ class DBDatetime extends DBDate implements TemplateGlobalProvider
|
||||
*/
|
||||
public static function set_mock_now($datetime)
|
||||
{
|
||||
if ($datetime instanceof DBDatetime) {
|
||||
self::$mock_now = $datetime;
|
||||
} elseif (is_string($datetime)) {
|
||||
self::$mock_now = DBField::create_field('Datetime', $datetime);
|
||||
} else {
|
||||
throw new InvalidArgumentException('DBDatetime::set_mock_now(): Wrong format: ' . $datetime);
|
||||
if (!$datetime instanceof DBDatetime) {
|
||||
$value = $datetime;
|
||||
$datetime = DBField::create_field('Datetime', $datetime);
|
||||
if ($datetime === false) {
|
||||
throw new InvalidArgumentException('DBDatetime::set_mock_now(): Wrong format: ' . $value);
|
||||
}
|
||||
}
|
||||
self::$mock_now = $datetime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear any mocked date, which causes
|
||||
|
18
tests/php/Control/Middleware/Control/TestController.php
Normal file
18
tests/php/Control/Middleware/Control/TestController.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Control\Tests\Middleware\Control;
|
||||
|
||||
use SilverStripe\Control\Controller;
|
||||
|
||||
class TestController extends Controller
|
||||
{
|
||||
public function index($request)
|
||||
{
|
||||
return "Success";
|
||||
}
|
||||
|
||||
public function Link($action = null)
|
||||
{
|
||||
return Controller::join_links('TestController', $action);
|
||||
}
|
||||
}
|
67
tests/php/Control/Middleware/RateLimitMiddlewareTest.php
Normal file
67
tests/php/Control/Middleware/RateLimitMiddlewareTest.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Control\Tests\Middleware;
|
||||
|
||||
use SilverStripe\Control\Middleware\RateLimitMiddleware;
|
||||
use SilverStripe\Control\Middleware\RequestHandlerMiddlewareAdapter;
|
||||
use SilverStripe\Control\Tests\Middleware\Control\TestController;
|
||||
use SilverStripe\Core\Config\Config;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\Dev\FunctionalTest;
|
||||
use SilverStripe\ORM\FieldType\DBDatetime;
|
||||
|
||||
class RateLimitMiddlewareTest extends FunctionalTest
|
||||
{
|
||||
|
||||
protected static $extra_controllers = [
|
||||
TestController::class,
|
||||
];
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
DBDatetime::set_mock_now('2017-09-27 00:00:00');
|
||||
Config::modify()->set(Injector::class, 'TestRateLimitMiddleware', [
|
||||
'class' => RateLimitMiddleware::class,
|
||||
'properties' => [
|
||||
'ExtraKey' => 'test',
|
||||
'MaxAttempts' => 2,
|
||||
'Decay' => 1,
|
||||
],
|
||||
]);
|
||||
Config::modify()->set(Injector::class, 'RateLimitTestController', [
|
||||
'class' => RequestHandlerMiddlewareAdapter::class,
|
||||
'properties' => [
|
||||
'RequestHandler' => '%$' . TestController::class,
|
||||
'Middlewares' => [
|
||||
'%$TestRateLimitMiddleware'
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getExtraRoutes()
|
||||
{
|
||||
$rules = parent::getExtraRoutes();
|
||||
$rules['TestController//$Action/$ID/$OtherID'] = '%$RateLimitTestController';
|
||||
return $rules;
|
||||
}
|
||||
|
||||
public function testRequest()
|
||||
{
|
||||
$response = $this->get('TestController');
|
||||
$this->assertFalse($response->isError());
|
||||
$this->assertEquals(2, $response->getHeader('X-RateLimit-Limit'));
|
||||
$this->assertEquals(1, $response->getHeader('X-RateLimit-Remaining'));
|
||||
$this->assertEquals(DBDatetime::now()->getTimestamp() + 60, $response->getHeader('X-RateLimit-Reset'));
|
||||
$this->assertEquals('Success', $response->getBody());
|
||||
$response = $this->get('TestController');
|
||||
$this->assertFalse($response->isError());
|
||||
$this->assertEquals(0, $response->getHeader('X-RateLimit-Remaining'));
|
||||
$response = $this->get('TestController');
|
||||
$this->assertTrue($response->isError());
|
||||
$this->assertEquals(429, $response->getStatusCode());
|
||||
$this->assertEquals(60, $response->getHeader('retry-after'));
|
||||
$this->assertNotEquals('Success', $response->getBody());
|
||||
}
|
||||
}
|
127
tests/php/Core/Cache/RateLimiterTest.php
Normal file
127
tests/php/Core/Cache/RateLimiterTest.php
Normal file
@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Core\Tests\Cache;
|
||||
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
use SilverStripe\Core\Cache\RateLimiter;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\ORM\FieldType\DBDatetime;
|
||||
use Symfony\Component\Cache\Simple\ArrayCache;
|
||||
|
||||
class RateLimiterTest extends SapphireTest
|
||||
{
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
DBDatetime::set_mock_now('2017-09-27 00:00:00');
|
||||
}
|
||||
|
||||
public function testConstruct()
|
||||
{
|
||||
$cache = new ArrayCache();
|
||||
$rateLimiter = new RateLimiter(
|
||||
'test',
|
||||
5,
|
||||
1
|
||||
);
|
||||
$rateLimiter->setCache($cache);
|
||||
$this->assertEquals('test', $rateLimiter->getIdentifier());
|
||||
$this->assertEquals(5, $rateLimiter->getMaxAttempts());
|
||||
$this->assertEquals(1, $rateLimiter->getDecay());
|
||||
}
|
||||
|
||||
public function testGetNumberOfAttempts()
|
||||
{
|
||||
$cache = new ArrayCache();
|
||||
$rateLimiter = new RateLimiter(
|
||||
'test',
|
||||
5,
|
||||
1
|
||||
);
|
||||
$rateLimiter->setCache($cache);
|
||||
for ($i = 0; $i < 7; ++$i) {
|
||||
$this->assertEquals($i, $rateLimiter->getNumAttempts());
|
||||
$rateLimiter->hit();
|
||||
}
|
||||
}
|
||||
|
||||
public function testGetNumAttemptsRemaining()
|
||||
{
|
||||
$cache = new ArrayCache();
|
||||
$rateLimiter = new RateLimiter(
|
||||
'test',
|
||||
1,
|
||||
1
|
||||
);
|
||||
$rateLimiter->setCache($cache);
|
||||
$this->assertEquals(1, $rateLimiter->getNumAttemptsRemaining());
|
||||
$rateLimiter->hit();
|
||||
$this->assertEquals(0, $rateLimiter->getNumAttemptsRemaining());
|
||||
$rateLimiter->hit();
|
||||
$this->assertEquals(0, $rateLimiter->getNumAttemptsRemaining());
|
||||
}
|
||||
|
||||
public function testGetTimeToReset()
|
||||
{
|
||||
$cache = new ArrayCache();
|
||||
$rateLimiter = new RateLimiter(
|
||||
'test',
|
||||
1,
|
||||
1
|
||||
);
|
||||
$rateLimiter->setCache($cache);
|
||||
$this->assertEquals(0, $rateLimiter->getTimeToReset());
|
||||
$rateLimiter->hit();
|
||||
$this->assertEquals(60, $rateLimiter->getTimeToReset());
|
||||
DBDatetime::set_mock_now(DBDatetime::now()->getTimestamp() + 30);
|
||||
$this->assertEquals(30, $rateLimiter->getTimeToReset());
|
||||
}
|
||||
|
||||
public function testClearAttempts()
|
||||
{
|
||||
$cache = new ArrayCache();
|
||||
$rateLimiter = new RateLimiter(
|
||||
'test',
|
||||
1,
|
||||
1
|
||||
);
|
||||
$rateLimiter->setCache($cache);
|
||||
for ($i = 0; $i < 5; ++$i) {
|
||||
$rateLimiter->hit();
|
||||
}
|
||||
$this->assertEquals(5, $rateLimiter->getNumAttempts());
|
||||
$rateLimiter->clearAttempts();
|
||||
$this->assertEquals(0, $rateLimiter->getNumAttempts());
|
||||
}
|
||||
|
||||
public function testHit()
|
||||
{
|
||||
$cache = new ArrayCache();
|
||||
$rateLimiter = new RateLimiter(
|
||||
'test',
|
||||
1,
|
||||
1
|
||||
);
|
||||
$rateLimiter->setCache($cache);
|
||||
$this->assertFalse($cache->has('test'));
|
||||
$this->assertFalse($cache->has('test-timer'));
|
||||
$rateLimiter->hit();
|
||||
$this->assertTrue($cache->has('test'));
|
||||
$this->assertTrue($cache->has('test-timer'));
|
||||
}
|
||||
|
||||
public function testCanAccess()
|
||||
{
|
||||
$cache = new ArrayCache();
|
||||
$rateLimiter = new RateLimiter(
|
||||
'test',
|
||||
1,
|
||||
1
|
||||
);
|
||||
$rateLimiter->setCache($cache);
|
||||
$this->assertTrue($rateLimiter->canAccess());
|
||||
$rateLimiter->hit();
|
||||
$this->assertFalse($rateLimiter->canAccess());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user