Passed
Push — master ( 9b6f1b...510c7f )
by MusikAnimal
04:53
created

RateLimitSubscriber   A

Complexity

Total Complexity 19

Size/Duplication

Total Lines 132
Duplicated Lines 0 %

Test Coverage

Coverage 62.5%

Importance

Changes 0
Metric Value
dl 0
loc 132
ccs 30
cts 48
cp 0.625
rs 10
c 0
b 0
f 0
wmc 19

5 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A getSubscribedEvents() 0 4 1
B checkBlacklist() 0 18 8
C onKernelController() 0 48 7
A denyAccess() 0 11 2
1
<?php
2
/**
3
 * This file contains only the RateLimitSubscriber class.
4
 */
5
6
namespace AppBundle\EventSubscriber;
7
8
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
9
use Symfony\Component\DependencyInjection\Container;
10
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
11
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
12
use Symfony\Component\HttpKernel\KernelEvents;
13
use Psr\Cache\CacheItemPoolInterface;
14
use Psr\Log\LoggerInterface;
15
use DateInterval;
16
17
/**
18
 * A RateLimitSubscriber checks to see if users are exceeding usage limitations.
19
 */
20
class RateLimitSubscriber implements EventSubscriberInterface
21
{
22
23
    /** @var Container The DI container. */
24
    protected $container;
25
26
    /** @var int Number of requests allowed in time period */
27
    protected $rateLimit;
28
29
    /** @var int Number of minutes during which $rateLimit requests are permitted */
30
    protected $rateDuration;
31
32
    /**
33
     * Save the container for later use.
34
     * @param Container $container The DI container.
35
     */
36 14
    public function __construct(Container $container)
37
    {
38 14
        $this->container = $container;
39 14
    }
40
41
    /**
42
     * Register our interest in the kernel.controller event.
43
     * @return string[]
44
     */
45 1
    public static function getSubscribedEvents()
46
    {
47
        return [
48 1
            KernelEvents::CONTROLLER => 'onKernelController',
49
        ];
50
    }
51
52
    /**
53
     * Check if the current user has exceeded the configured usage limitations.
54
     * @param FilterControllerEvent $event The event.
55
     * @throws TooManyRequestsHttpException If rate limits have been exceeded.
56
     */
57 14
    public function onKernelController(FilterControllerEvent $event)
58
    {
59 14
        $this->rateLimit = (int) $this->container->getParameter('app.rate_limit_count');
60 14
        $this->rateDuration = (int) $this->container->getParameter('app.rate_limit_time');
61
62
        // Zero values indicate the rate limiting feature should be disabled.
63 14
        if ($this->rateLimit === 0 || $this->rateDuration === 0) {
64
            return;
65
        }
66
67 14
        $controller = $event->getController();
68 14
        $loggedIn = (bool) $this->container->get('session')->get('logged_in_user');
69
70
        /**
71
         * Rate limiting will not apply to these actions
72
         * @var array
73
         */
74 14
        $actionWhitelist = ['indexAction', 'showAction', 'aboutAction'];
75
76
        // No rate limits on lightweight pages or if they are logged in.
77 14
        if (in_array($controller[1], $actionWhitelist) || $loggedIn) {
78 12
            return;
79
        }
80
81 3
        $request = $event->getRequest();
82
83 3
        $this->checkBlacklist($request);
84
85
        // Build and fetch cache key based on session ID and requested URI,
86
        //   removing any reserved characters from URI.
87 3
        $uri = preg_replace('/[^a-zA-Z0-9,\.]/', '', $request->getRequestUri());
88 3
        $sessionId = $request->getSession()->getId();
89 3
        $cacheKey = 'ratelimit.'.$sessionId.'.'.$uri;
90 3
        $cache = $this->container->get('cache.app');
91 3
        $cacheItem = $cache->getItem($cacheKey);
92
93
        // If increment value already in cache, or start with 1.
94 3
        $count = $cacheItem->isHit() ? (int) $cacheItem->get() + 1 : 1;
95
96
        // Check if limit has been exceeded, and if so, throw an error.
97 3
        if ($count > $this->rateLimit) {
98
            $this->denyAccess($request, 'Exceeded rate limitation');
99
        }
100
101
        // Reset the clock on every request.
102 3
        $cacheItem->set($count)
103 3
            ->expiresAfter(new DateInterval('PT'.$this->rateDuration.'M'));
104 3
        $cache->save($cacheItem);
105 3
    }
106
107
    /**
108
     * Check the request against blacklisted URIs and user agents
109
     * @param \Symfony\Component\HttpFoundation\Request $request
110
     */
111 3
    private function checkBlacklist(\Symfony\Component\HttpFoundation\Request $request)
112
    {
113
        // First check user agent and URI blacklists
114 3
        if ($this->container->hasParameter('request_blacklist')) {
115
            $blacklist = $this->container->getParameter('request_blacklist');
116
            // User agents
117
            if (is_array($blacklist['user_agent'])) {
118
                foreach ($blacklist['user_agent'] as $ua) {
119
                    if (strpos($request->headers->get('User-Agent'), $ua) !== false) {
120
                        $this->denyAccess($request, "Matched blacklisted user agent `$ua`");
121
                    }
122
                }
123
            }
124
            // URIs
125
            if (is_array($blacklist['uri'])) {
126
                foreach ($blacklist['uri'] as $uri) {
127
                    if (strpos($request->getRequestUri(), $uri) !== false) {
128
                        $this->denyAccess($request, "Matched blacklisted URI `$uri`");
129
                    }
130
                }
131
            }
132
        }
133 3
    }
134
135
    /**
136
     * Throw exception for denied access due to spider crawl or hitting usage limits.
137
     * @param \Symfony\Component\HttpFoundation\Request $request
138
     * @param string $logComment Comment to include with the log entry.
139
     * @todo i18n
140
     */
141
    private function denyAccess(\Symfony\Component\HttpFoundation\Request $request, $logComment = '')
142
    {
143
        // Log the denied request
144
        $logger = $this->container->get('monolog.logger.rate_limit');
145
        $logger->info(
146
            "<URI>: " . $request->getRequestUri() .
147
            ($logComment != '' ? "\t<Reason>: $logComment" : '') .
148
            "\t<User agent>: " . $request->headers->get('User-Agent')
149
        );
150
151
        throw new TooManyRequestsHttpException(600, 'error-rate-limit', null, $this->rateDuration);
152
    }
153
}
154