Passed
Pull Request — master (#18)
by Indra
11:25
created

RateLimitHandler::getThrottle()   B

Complexity

Conditions 7
Paths 10

Size

Total Lines 32
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 7

Importance

Changes 0
Metric Value
eloc 19
c 0
b 0
f 0
dl 0
loc 32
ccs 19
cts 19
cp 1
rs 8.8333
cc 7
nc 10
nop 2
crap 7
1
<?php
2
3
/*
4
 * This file is part of the ApiRateLimitBundle
5
 *
6
 * (c) Indra Gunawan <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Indragunawan\ApiRateLimitBundle\Service;
13
14
use Doctrine\Common\Annotations\AnnotationReader;
15
use Indragunawan\ApiRateLimitBundle\Annotation\ApiRateLimit;
16
use Psr\Cache\CacheItemPoolInterface;
17
use ReflectionClass;
18
use Symfony\Component\HttpFoundation\Request;
19
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
20
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
21
22
/**
23
 * @author Indra Gunawan <[email protected]>
24
 */
25
class RateLimitHandler
26
{
27
    /**
28
     * @var CacheItemPoolInterface
29
     */
30
    private $cacheItemPool;
31
32
    /**
33
     * @var TokenStorageInterface
34
     */
35
    private $tokenStorage;
36
37
    /**
38
     * @var AuthorizationCheckerInterface
39
     */
40
    private $authorizationChecker;
41
42
    /**
43
     * @var array
44
     */
45
    private $throttleConfig;
46
47
    /**
48
     * @var int
49
     */
50
    private $limit;
51
52
    /**
53
     * @var int
54
     */
55
    private $remaining;
56
57
    /**
58
     * @var int
59
     */
60
    private $reset;
61
62
    /**
63
     * @var
64
     */
65
    private $enabled = true;
66
67
    /**
68
     * @var bool
69
     */
70
    private $rateLimitExceeded = false;
71
72
    /**
73
     * RateLimitHandler constructor.
74
     */
75 8
    public function __construct(
76
        CacheItemPoolInterface $cacheItemPool,
77
        TokenStorageInterface $tokenStorage,
78
        AuthorizationCheckerInterface $authorizationChecker,
79
        array $throttleConfig
80
    ) {
81 8
        $this->cacheItemPool = $cacheItemPool;
82 8
        $this->tokenStorage = $tokenStorage;
83 8
        $this->authorizationChecker = $authorizationChecker;
84 8
        $this->throttleConfig = $throttleConfig;
85 8
    }
86
87
    /**
88
     * @return bool
89
     */
90 8
    public function isEnabled()
91
    {
92 8
        return $this->enabled;
93
    }
94
95
    /**
96
     * @return bool
97
     */
98 5
    public function isRateLimitExceeded()
99
    {
100 5
        return $this->rateLimitExceeded;
101
    }
102
103 7
    public function getRateLimitInfo(): array
104
    {
105
        return [
106 7
            'limit' => $this->limit,
107 7
            'remaining' => $this->remaining,
108 7
            'reset' => $this->reset,
109
        ];
110
    }
111
112 8
    public static function generateCacheKey(string $ip, string $username = null, string $userRole = null): string
113
    {
114 8
        if (!empty($username) && !empty($userRole)) {
115 2
            return sprintf('_api_rate_limit_metadata$%s', sha1($userRole.$username));
116
        }
117
118 6
        return sprintf('_api_rate_limit_metadata$%s', sha1($ip));
119
    }
120
121
    /**
122
     * @throws \Doctrine\Common\Annotations\AnnotationException
123
     * @throws \Psr\Cache\InvalidArgumentException
124
     * @throws \ReflectionException
125
     */
126 8
     public function handle(Request $request)
127
    {
128 8
        $annotationReader = new AnnotationReader();
129
        /** @var ApiRateLimit $annotation */
130 8
        $annotation = $annotationReader->getClassAnnotation(
131 8
            new ReflectionClass($request->attributes->get('_api_resource_class')),
132 8
            ApiRateLimit::class
133
        );
134
135 8
        if (null !== $annotation) {
136 8
            $this->enabled = $annotation->enabled;
137
            if ((
138 8
                    !\in_array($request->getMethod(), array_map('strtoupper', $annotation->methods), true)
139
                    &&
140 8
                    !\in_array($request->attributes->get('_api_item_operation_name'),array_map('strtolower', $annotation->methods),true)
141
                )
142 8
                && !empty($annotation->methods)
143
            ) {
144
                // The annotation is ignored as the method is not corresponding
145 8
                $annotation = new ApiRateLimit();
146
            }
147
        } else {
148
            $annotation = new ApiRateLimit();
149
        }
150
151 8
        list($key, $limit, $period) = $this->getThrottle($request, $annotation);
152
153 8
        if ($this->enabled) {
154 7
            $this->decreaseRateLimitRemaining($key, $limit, $period);
155
        }
156 8
    }
157
158
    /**
159
     * @throws \Psr\Cache\InvalidArgumentException
160
     */
161 7
    protected function decreaseRateLimitRemaining(string $key, int $limit, int $period)
162
    {
163 7
        $cost = 1;
164 7
        $currentTime = gmdate('U');
165
166 7
        $rateLimitInfo = $this->cacheItemPool->getItem($key);
167 7
        $rateLimit = $rateLimitInfo->get();
168 7
        if ($rateLimitInfo->isHit() && $currentTime <= $rateLimit['reset']) {
169
            // decrease existing rate limit remaining
170 2
            if ($rateLimit['remaining'] - $cost >= 0) {
171 1
                $remaining = $rateLimit['remaining'] - $cost;
172 1
                $reset = $rateLimit['reset'];
173 1
                $ttl = $rateLimit['reset'] - $currentTime;
174
            } else {
175 1
                $this->rateLimitExceeded = true;
176 1
                $this->reset = $rateLimit['reset'];
177 1
                $this->limit = $limit;
178 1
                $this->remaining = 0;
179
180 2
                return;
181
            }
182
        } else {
183
            // add / reset new rate limit remaining
184 5
            $remaining = $limit - $cost;
185 5
            $reset = $currentTime + $period;
186 5
            $ttl = $period;
187
        }
188
189
        $rateLimit = [
190 6
            'limit' => $limit,
191 6
            'remaining' => $remaining,
192 6
            'reset' => $reset,
193
        ];
194
195 6
        $rateLimitInfo->set($rateLimit);
196 6
        $rateLimitInfo->expiresAfter($ttl);
197
198 6
        $this->cacheItemPool->save($rateLimitInfo);
199
200 6
        $this->limit = $limit;
201 6
        $this->remaining = $remaining;
202 6
        $this->reset = $reset;
203 6
    }
204
205
    /**
206
     * @return array
207
     */
208 8
    private function getThrottle(Request $request, ApiRateLimit $annotation)
209
    {
210 8
        if (null !== $token = $this->tokenStorage->getToken()) {
211
            // no anonymous
212 2
            if (\is_object($token->getUser())) {
213 2
                $rolesConfig = $this->throttleConfig['roles'];
214 2
                if (!empty($annotation->throttle['roles'])) {
215 1
                    $rolesConfig = $annotation->throttle['roles'];
216
                }
217
218 2
                foreach ($rolesConfig as $role => $throttle) {
219 2
                    if ($this->authorizationChecker->isGranted($role)) {
220 2
                        $username = $token->getUsername();
221 2
                        $userRole = $role;
222 2
                        $limit = $throttle['limit'];
223 2
                        $period = $throttle['period'];
224
225 2
                        return [self::generateCacheKey($request->getClientIp(), $username, $userRole), $limit, $period];
0 ignored issues
show
Bug introduced by
It seems like $request->getClientIp() can also be of type null; however, parameter $ip of Indragunawan\ApiRateLimi...ler::generateCacheKey() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

225
                        return [self::generateCacheKey(/** @scrutinizer ignore-type */ $request->getClientIp(), $username, $userRole), $limit, $period];
Loading history...
226
                    }
227
                }
228
            }
229
        }
230
231 6
        if (!empty($annotation->throttle['default'])) {
232 1
            $limit = $annotation->throttle['default']['limit'];
233 1
            $period = $annotation->throttle['default']['period'];
234
        } else {
235 5
            $limit = $this->throttleConfig['default']['limit'];
236 5
            $period = $this->throttleConfig['default']['period'];
237
        }
238
239 6
        return [self::generateCacheKey($request->getClientIp()), $limit, $period];
240
    }
241
}
242