Passed
Pull Request — master (#42)
by
unknown
04:10
created

RateLimitFilter::getContent()   C

Complexity

Conditions 7
Paths 6

Size

Total Lines 36
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 36
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 18
nc 6
nop 2
1
<?php
2
3
namespace SilverStripe\VersionFeed\Filters;
4
5
6
7
8
use SilverStripe\Core\Config\Config;
9
use SilverStripe\Control\Controller;
10
use SilverStripe\Control\HTTPResponse_Exception;
11
12
13
14
15
/**
16
 * Provides rate limiting of execution of a callback
17
 */
18
class RateLimitFilter extends ContentFilter {
19
	
20
	/**
21
	 * Time duration (in second) to allow for generation of cached results. Requests to 
22
	 * pages that within this time period that do not hit the cache (and would otherwise trigger
23
	 * a version query) will be presented with a 429 (rate limit) HTTP error
24
	 *
25
	 * @config
26
	 * @var int
27
	 */
28
	private static $lock_timeout = 5;
0 ignored issues
show
introduced by
The private property $lock_timeout is not used, and could be removed.
Loading history...
29
	
30
	/**
31
	 * Determine if the cache generation should be locked on a per-page basis. If true, concurrent page versions
32
	 * may be generated without rate interference.
33
	 *
34
	 * @config
35
	 * @var bool
36
	 */
37
	private static $lock_bypage = false;
0 ignored issues
show
introduced by
The private property $lock_bypage is not used, and could be removed.
Loading history...
38
	
39
	/**
40
	 * Determine if rate limiting should be applied independently to each IP address. This method is not
41
	 * reliable, as most DDoS attacks use multiple IP addresses.
42
	 *
43
	 * @config
44
	 * @var bool
45
	 */
46
	private static $lock_byuserip = false;
0 ignored issues
show
introduced by
The private property $lock_byuserip is not used, and could be removed.
Loading history...
47
48
	/**
49
	 * Time duration (in sections) to deny further search requests after a successful search.
50
	 * Search requests within this time period while another query is in progress will be
51
	 * presented with a 429 (rate limit)
52
	 *
53
	 * @config
54
	 * @var int
55
	 */
56
	private static $lock_cooldown = 2;
0 ignored issues
show
introduced by
The private property $lock_cooldown is not used, and could be removed.
Loading history...
57
	
58
	/**
59
	 * Cache key prefix
60
	 */
61
	const CACHE_PREFIX = 'RateLimitBegin';
62
	
63
	/**
64
	 * Determines the key to use for saving the current rate
65
	 * 
66
	 * @param string $itemkey Input key
67
	 * @return string Result key
68
	 */
69
	protected function getCacheKey($itemkey) {
70
		$key = self::CACHE_PREFIX;
71
		
72
		// Add global identifier
73
		if($this->config()->get('lock_bypage'))  {
74
			$key .= '_' . md5($itemkey);
75
		}
76
		
77
		// Add user-specific identifier
78
		if($this->config()->get('lock_byuserip') && Controller::has_curr()) {
79
			$ip = Controller::curr()->getRequest()->getIP();
80
			$key .= '_' . md5($ip);
81
		}
82
		
83
		return $key;
84
	}
85
86
87
	public function getContent($key, $callback) {
88
		// Bypass rate limiting if flushing, or timeout isn't set
89
		$timeout = $this->config()->get('lock_timeout');
90
		if(isset($_GET['flush']) || !$timeout) {
91
			return parent::getContent($key, $callback);
92
		}
93
		
94
		// Generate result with rate limiting enabled
95
		$limitKey = $this->getCacheKey($key);
96
		$cache = $this->getCache();
97
		if($lockedUntil = $cache->get($limitKey)) {
98
			if(time() < $lockedUntil) {
99
				// Politely inform visitor of limit
100
				$response = new HTTPResponse_Exception('Too Many Requests.', 429);
101
				$response->getResponse()->addHeader('Retry-After', 1 + $lockedUntil - time());
102
				throw $response;
103
			}
104
		}
105
106
		$lifetime = Config::inst()->get(ContentFilter::class, 'cache_lifetime') ?: null;
107
		
108
		// Apply rate limit
109
		$cache->set($limitKey, time() + $timeout, $lifetime);
110
		
111
		// Generate results
112
		$result = parent::getContent($key, $callback);
113
114
		// Reset rate limit with optional cooldown
115
		if($cooldown = $this->config()->get('lock_cooldown')) {
116
			// Set cooldown on successful query execution
117
			$cache->set($limitKey, time() + $cooldown, $lifetime);
118
		} else {
119
			// Without cooldown simply disable lock
120
			$cache->delete($limitKey);
121
		}
122
		return $result;
123
	}
124
}
125