RateLimitFilter   A
last analyzed

Complexity

Total Complexity 11

Size/Duplication

Total Lines 108
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 11
eloc 31
dl 0
loc 108
rs 10
c 0
b 0
f 0

2 Methods

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