API Added lock cool down to rate limiting

This commit is contained in:
Damian Mooyman 2014-04-30 15:40:51 +12:00
parent 241f0604b0
commit 47991567b5
2 changed files with 31 additions and 10 deletions

View File

@ -35,6 +35,16 @@ class RateLimitFilter extends ContentFilter {
*/ */
private static $lock_byuserip = false; private static $lock_byuserip = false;
/**
* Time duration (in sections) to deny further search requests after a successful search.
* Search requests within this time period while another query is in progress will be
* presented with a 429 (rate limit)
*
* @config
* @var int
*/
private static $lock_cooldown = 2;
/** /**
* Cache key prefix * Cache key prefix
*/ */
@ -74,19 +84,29 @@ class RateLimitFilter extends ContentFilter {
// Generate result with rate limiting enabled // Generate result with rate limiting enabled
$limitKey = $this->getCacheKey($key); $limitKey = $this->getCacheKey($key);
$cache = $this->getCache(); $cache = $this->getCache();
if($cacheBegin = $cache->load($limitKey)) { if($lockedUntil = $cache->load($limitKey)) {
if(time() - $cacheBegin < $timeout) { if(time() < $lockedUntil) {
// Politely inform visitor of limit // Politely inform visitor of limit
$response = new \SS_HTTPResponse_Exception('Too Many Requests.', 429); $response = new \SS_HTTPResponse_Exception('Too Many Requests.', 429);
$response->getResponse()->addHeader('Retry-After', 1 + time() - $cacheBegin); $response->getResponse()->addHeader('Retry-After', 1 + $lockedUntil - time());
throw $response; throw $response;
} }
} }
// Generate result with rate limit locked // Apply rate limit
$cache->save(time(), $limitKey); $cache->save(time() + $timeout, $limitKey);
// Generate results
$result = parent::getContent($key, $callback); $result = parent::getContent($key, $callback);
// Reset rate limit with optional cooldown
if($cooldown = \Config::inst()->get(get_class(), 'lock_cooldown')) {
// Set cooldown on successful query execution
$cache->save(time() + $cooldown, $limitKey);
} else {
// Without cooldown simply disable lock
$cache->remove($limitKey); $cache->remove($limitKey);
}
return $result; return $result;
} }
} }

View File

@ -20,6 +20,7 @@ class VersionFeedFunctionalTest extends FunctionalTest {
Config::inst()->update('VersionFeed\Filters\RateLimitFilter', 'lock_timeout', 20); Config::inst()->update('VersionFeed\Filters\RateLimitFilter', 'lock_timeout', 20);
Config::inst()->update('VersionFeed\Filters\RateLimitFilter', 'lock_bypage', false); Config::inst()->update('VersionFeed\Filters\RateLimitFilter', 'lock_bypage', false);
Config::inst()->update('VersionFeed\Filters\RateLimitFilter', 'lock_byuserip', false); Config::inst()->update('VersionFeed\Filters\RateLimitFilter', 'lock_byuserip', false);
Config::inst()->update('VersionFeed\Filters\RateLimitFilter', 'lock_cooldown', false);
} }
public function tearDown() { public function tearDown() {
@ -63,7 +64,7 @@ class VersionFeedFunctionalTest extends FunctionalTest {
Config::inst()->update('VersionFeed\Filters\RateLimitFilter', 'lock_byuserip', false); Config::inst()->update('VersionFeed\Filters\RateLimitFilter', 'lock_byuserip', false);
$cache = SS_Cache::factory('VersionFeed_Controller'); $cache = SS_Cache::factory('VersionFeed_Controller');
$cache->setOption('automatic_serialization', true); $cache->setOption('automatic_serialization', true);
$cache->save(time() - 2, \VersionFeed\Filters\RateLimitFilter::CACHE_PREFIX); $cache->save(time() + 10, \VersionFeed\Filters\RateLimitFilter::CACHE_PREFIX);
// Test normal hit // Test normal hit
$response = $this->get($page1->RelativeLink('changes')); $response = $this->get($page1->RelativeLink('changes'));
@ -81,7 +82,7 @@ class VersionFeedFunctionalTest extends FunctionalTest {
Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $page1->ID, false) Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $page1->ID, false)
)); ));
$key = \VersionFeed\Filters\RateLimitFilter::CACHE_PREFIX . '_' . md5($key); $key = \VersionFeed\Filters\RateLimitFilter::CACHE_PREFIX . '_' . md5($key);
$cache->save(time() - 2, $key); $cache->save(time() + 10, $key);
$response = $this->get($page1->RelativeLink('changes')); $response = $this->get($page1->RelativeLink('changes'));
$this->assertEquals(429, $response->getStatusCode()); $this->assertEquals(429, $response->getStatusCode());
$this->assertGreaterThan(0, $response->getHeader('Retry-After')); $this->assertGreaterThan(0, $response->getHeader('Retry-After'));
@ -92,14 +93,14 @@ class VersionFeedFunctionalTest extends FunctionalTest {
// Test rate limit hit by IP // Test rate limit hit by IP
Config::inst()->update('VersionFeed\Filters\RateLimitFilter', 'lock_byuserip', true); Config::inst()->update('VersionFeed\Filters\RateLimitFilter', 'lock_byuserip', true);
$_SERVER['HTTP_CLIENT_IP'] = '127.0.0.1'; $_SERVER['HTTP_CLIENT_IP'] = '127.0.0.1';
$cache->save(time() - 2, \VersionFeed\Filters\RateLimitFilter::CACHE_PREFIX . '_' . md5('127.0.0.1')); $cache->save(time() + 10, \VersionFeed\Filters\RateLimitFilter::CACHE_PREFIX . '_' . md5('127.0.0.1'));
$response = $this->get($page1->RelativeLink('changes')); $response = $this->get($page1->RelativeLink('changes'));
$this->assertEquals(429, $response->getStatusCode()); $this->assertEquals(429, $response->getStatusCode());
$this->assertGreaterThan(0, $response->getHeader('Retry-After')); $this->assertGreaterThan(0, $response->getHeader('Retry-After'));
// Test rate limit doesn't hit other IP // Test rate limit doesn't hit other IP
$_SERVER['HTTP_CLIENT_IP'] = '127.0.0.20'; $_SERVER['HTTP_CLIENT_IP'] = '127.0.0.20';
$cache->save(time() - 2, \VersionFeed\Filters\RateLimitFilter::CACHE_PREFIX . '_' . md5('127.0.0.1')); $cache->save(time() + 10, \VersionFeed\Filters\RateLimitFilter::CACHE_PREFIX . '_' . md5('127.0.0.1'));
$response = $this->get($page1->RelativeLink('changes')); $response = $this->get($page1->RelativeLink('changes'));
$this->assertEquals(200, $response->getStatusCode()); $this->assertEquals(200, $response->getStatusCode());