Passed
Push — master ( e6ce91...429907 )
by MusikAnimal
04:46
created

RateLimitSubscriber   A

Complexity

Total Complexity 19

Size/Duplication

Total Lines 133
Duplicated Lines 0 %

Test Coverage

Coverage 62.5%

Importance

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