1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Noxlogic\RateLimitBundle\EventListener; |
4
|
|
|
|
5
|
|
|
use Noxlogic\RateLimitBundle\Annotation\RateLimit; |
6
|
|
|
use Noxlogic\RateLimitBundle\Events\ProxyFilterControllerEvent; |
7
|
|
|
use Noxlogic\RateLimitBundle\Events\CheckedRateLimitEvent; |
8
|
|
|
use Noxlogic\RateLimitBundle\Events\GenerateKeyEvent; |
9
|
|
|
use Noxlogic\RateLimitBundle\Events\RateLimitEvents; |
10
|
|
|
use Noxlogic\RateLimitBundle\Exception\RateLimitExceptionInterface; |
11
|
|
|
use Noxlogic\RateLimitBundle\Service\RateLimitService; |
12
|
|
|
use Noxlogic\RateLimitBundle\Util\PathLimitProcessor; |
13
|
|
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface as LegacyEventDispatcherInterface; |
14
|
|
|
use Symfony\Component\HttpFoundation\Request; |
15
|
|
|
use Symfony\Component\HttpFoundation\Response; |
16
|
|
|
use Symfony\Component\HttpKernel\HttpKernelInterface; |
17
|
|
|
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; |
18
|
|
|
|
19
|
|
|
class RateLimitAnnotationListener extends BaseListener |
20
|
|
|
{ |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* @var EventDispatcherInterface | LegacyEventDispatcherInterface |
24
|
|
|
*/ |
25
|
|
|
protected $eventDispatcher; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* @var \Noxlogic\RateLimitBundle\Service\RateLimitService |
29
|
|
|
*/ |
30
|
|
|
protected $rateLimitService; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* @var \Noxlogic\RateLimitBundle\Util\PathLimitProcessor |
34
|
|
|
*/ |
35
|
|
|
protected $pathLimitProcessor; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* @param RateLimitService $rateLimitService |
39
|
|
|
*/ |
40
|
19 |
|
public function __construct( |
41
|
|
|
$eventDispatcher, |
42
|
|
|
RateLimitService $rateLimitService, |
43
|
|
|
PathLimitProcessor $pathLimitProcessor |
44
|
|
|
) { |
45
|
19 |
|
$this->eventDispatcher = $eventDispatcher; |
46
|
19 |
|
$this->rateLimitService = $rateLimitService; |
47
|
19 |
|
$this->pathLimitProcessor = $pathLimitProcessor; |
48
|
19 |
|
} |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* @param ProxyFilterControllerEvent $event |
52
|
|
|
*/ |
53
|
15 |
|
public function onKernelController(ProxyFilterControllerEvent $event) |
54
|
|
|
{ |
55
|
|
|
// Skip if the bundle isn't enabled (for instance in test environment) |
56
|
15 |
|
if( ! $this->getParameter('enabled', true)) { |
57
|
1 |
|
return; |
58
|
|
|
} |
59
|
|
|
|
60
|
|
|
// Skip if we aren't the main request |
61
|
14 |
|
if ($event->getRequestType() != HttpKernelInterface::MASTER_REQUEST) { |
62
|
1 |
|
return; |
63
|
|
|
} |
64
|
|
|
|
65
|
|
|
// Find the best match |
66
|
13 |
|
$annotations = $event->getRequest()->attributes->get('_x-rate-limit', array()); |
67
|
13 |
|
$rateLimit = $this->findBestMethodMatch($event->getRequest(), $annotations); |
68
|
|
|
|
69
|
|
|
// Another treatment before applying RateLimit ? |
70
|
13 |
|
$checkedRateLimitEvent = new CheckedRateLimitEvent($event->getRequest(), $rateLimit); |
71
|
13 |
|
$this->dispatch(RateLimitEvents::CHECKED_RATE_LIMIT, $checkedRateLimitEvent); |
72
|
13 |
|
$rateLimit = $checkedRateLimitEvent->getRateLimit(); |
73
|
|
|
|
74
|
|
|
// No matching annotation found |
75
|
13 |
|
if (! $rateLimit) { |
76
|
3 |
|
return; |
77
|
|
|
} |
78
|
|
|
|
79
|
10 |
|
$key = $this->getKey($event, $rateLimit, $annotations); |
80
|
|
|
|
81
|
|
|
// Ratelimit the call |
82
|
10 |
|
$rateLimitInfo = $this->rateLimitService->limitRate($key); |
83
|
10 |
|
if (! $rateLimitInfo) { |
84
|
|
|
// Create new rate limit entry for this call |
85
|
5 |
|
$rateLimitInfo = $this->rateLimitService->createRate($key, $rateLimit->getLimit(), $rateLimit->getPeriod()); |
86
|
5 |
|
if (! $rateLimitInfo) { |
87
|
|
|
// @codeCoverageIgnoreStart |
88
|
|
|
return; |
89
|
|
|
// @codeCoverageIgnoreEnd |
90
|
|
|
} |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
|
94
|
|
|
// Store the current rating info in the request attributes |
95
|
9 |
|
$request = $event->getRequest(); |
96
|
9 |
|
$request->attributes->set('rate_limit_info', $rateLimitInfo); |
97
|
|
|
|
98
|
|
|
// Reset the rate limits |
99
|
9 |
|
if(time() >= $rateLimitInfo->getResetTimestamp()) { |
100
|
1 |
|
$this->rateLimitService->resetRate($key); |
101
|
1 |
|
$rateLimitInfo = $this->rateLimitService->createRate($key, $rateLimit->getLimit(), $rateLimit->getPeriod()); |
102
|
1 |
|
if (! $rateLimitInfo) { |
103
|
|
|
// @codeCoverageIgnoreStart |
104
|
|
|
return; |
105
|
|
|
// @codeCoverageIgnoreEnd |
106
|
|
|
} |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
// When we exceeded our limit, return a custom error response |
110
|
9 |
|
if ($rateLimitInfo->getCalls() > $rateLimitInfo->getLimit()) { |
111
|
|
|
|
112
|
|
|
// Throw an exception if configured. |
113
|
5 |
|
if ($this->getParameter('rate_response_exception')) { |
114
|
2 |
|
$class = $this->getParameter('rate_response_exception'); |
115
|
|
|
|
116
|
2 |
|
$e = new $class($this->getParameter('rate_response_message'), $this->getParameter('rate_response_code')); |
117
|
|
|
|
118
|
2 |
|
if ($e instanceof RateLimitExceptionInterface) { |
119
|
1 |
|
$e->setPayload($rateLimit->getPayload()); |
120
|
|
|
} |
121
|
|
|
|
122
|
2 |
|
throw $e; |
123
|
|
|
} |
124
|
|
|
|
125
|
3 |
|
$message = $this->getParameter('rate_response_message'); |
126
|
3 |
|
$code = $this->getParameter('rate_response_code'); |
127
|
3 |
|
$event->setController(function () use ($message, $code) { |
128
|
|
|
// @codeCoverageIgnoreStart |
129
|
|
|
return new Response($message, $code); |
130
|
|
|
// @codeCoverageIgnoreEnd |
131
|
3 |
|
}); |
132
|
3 |
|
$event->stopPropagation(); |
|
|
|
|
133
|
|
|
} |
134
|
|
|
|
135
|
7 |
|
} |
136
|
|
|
|
137
|
|
|
|
138
|
|
|
/** |
139
|
|
|
* @param RateLimit[] $annotations |
140
|
|
|
*/ |
141
|
17 |
|
protected function findBestMethodMatch(Request $request, array $annotations) |
142
|
|
|
{ |
143
|
|
|
// Empty array, check the path limits |
144
|
17 |
|
if (count($annotations) == 0) { |
145
|
5 |
|
return $this->pathLimitProcessor->getRateLimit($request); |
146
|
|
|
} |
147
|
|
|
|
148
|
12 |
|
$best_match = null; |
149
|
12 |
|
foreach ($annotations as $annotation) { |
150
|
|
|
// cast methods to array, even method holds a string |
151
|
12 |
|
$methods = is_array($annotation->getMethods()) ? $annotation->getMethods() : array($annotation->getMethods()); |
152
|
|
|
|
153
|
12 |
|
if (in_array($request->getMethod(), $methods)) { |
154
|
3 |
|
$best_match = $annotation; |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
// Only match "default" annotation when we don't have a best match |
158
|
12 |
|
if (count($annotation->getMethods()) == 0 && $best_match == null) { |
159
|
12 |
|
$best_match = $annotation; |
160
|
|
|
} |
161
|
|
|
} |
162
|
|
|
|
163
|
12 |
|
return $best_match; |
164
|
|
|
} |
165
|
|
|
|
166
|
10 |
|
private function getKey(ProxyFilterControllerEvent $event, RateLimit $rateLimit, array $annotations) |
167
|
|
|
{ |
168
|
|
|
// Let listeners manipulate the key |
169
|
10 |
|
$keyEvent = new GenerateKeyEvent($event->getRequest(), '', $rateLimit->getPayload()); |
170
|
|
|
|
171
|
10 |
|
$rateLimitMethods = implode('.', $rateLimit->getMethods()); |
172
|
10 |
|
$keyEvent->addToKey($rateLimitMethods); |
173
|
|
|
|
174
|
10 |
|
$rateLimitAlias = count($annotations) === 0 |
175
|
1 |
|
? str_replace('/', '.', $this->pathLimitProcessor->getMatchedPath($event->getRequest())) |
176
|
10 |
|
: $this->getAliasForRequest($event); |
177
|
10 |
|
$keyEvent->addToKey($rateLimitAlias); |
178
|
|
|
|
179
|
10 |
|
$this->dispatch(RateLimitEvents::GENERATE_KEY, $keyEvent); |
180
|
|
|
|
181
|
10 |
|
return $keyEvent->getKey(); |
182
|
|
|
} |
183
|
|
|
|
184
|
9 |
|
private function getAliasForRequest(ProxyFilterControllerEvent $event) |
185
|
|
|
{ |
186
|
9 |
|
if (($route = $event->getRequest()->attributes->get('_route'))) { |
187
|
|
|
return $route; |
188
|
|
|
} |
189
|
|
|
|
190
|
9 |
|
$controller = $event->getController(); |
191
|
|
|
|
192
|
9 |
|
if (is_string($controller) && false !== strpos($controller, '::')) { |
193
|
|
|
$controller = explode('::', $controller); |
194
|
|
|
} |
195
|
|
|
|
196
|
9 |
|
if (is_array($controller)) { |
197
|
9 |
|
return str_replace('\\', '.', is_string($controller[0]) ? $controller[0] : get_class($controller[0])) . '.' . $controller[1]; |
198
|
|
|
} |
199
|
|
|
|
200
|
1 |
|
if ($controller instanceof \Closure) { |
201
|
1 |
|
return 'closure'; |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
if (is_object($controller)) { |
205
|
|
|
return str_replace('\\', '.', get_class($controller[0])); |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
return 'other'; |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
private function dispatch($eventName, $event) |
212
|
|
|
{ |
213
|
|
|
if ($this->eventDispatcher instanceof EventDispatcherInterface) { |
214
|
|
|
// Symfony >= 4.3 |
215
|
|
|
$this->eventDispatcher->dispatch($event, $eventName); |
216
|
|
|
} else { |
217
|
|
|
// Symfony 3.4 |
218
|
|
|
$this->eventDispatcher->dispatch($eventName, $event); |
219
|
|
|
} |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
} |
223
|
|
|
|
This method has been deprecated. The supplier of the class has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.