Skip to content

Commit 5170ac8

Browse files
committed
Implemented rate limiting. Closes dingo#2.
Signed-off-by: Jason Lewis <[email protected]>
1 parent 6e58192 commit 5170ac8

File tree

5 files changed

+381
-0
lines changed

5 files changed

+381
-0
lines changed

src/ApiServiceProvider.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ protected function registerAuthentication()
162162
protected function registerMiddlewares()
163163
{
164164
$this->app->middleware('Dingo\Api\Http\Middleware\Authentication', [$this->app]);
165+
166+
$this->app->middleware('Dingo\Api\Http\Middleware\RateLimit', [$this->app]);
165167
}
166168

167169
}

src/Auth/Shield.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,14 @@ public function setUser($user)
134134
return $this;
135135
}
136136

137+
/**
138+
* Check if a user has authenticated with the API.
139+
*
140+
* @return bool
141+
*/
142+
public function check()
143+
{
144+
return ! is_null($this->user());
145+
}
146+
137147
}

src/Http/Middleware/RateLimit.php

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
<?php namespace Dingo\Api\Http\Middleware;
2+
3+
use Dingo\Api\Http\Response;
4+
use Illuminate\Container\Container;
5+
use Dingo\Api\Http\InternalRequest;
6+
use Symfony\Component\HttpFoundation\Request;
7+
use Symfony\Component\HttpKernel\HttpKernelInterface;
8+
9+
class RateLimit implements HttpKernelInterface {
10+
11+
/**
12+
* The wrapped kernel implementation.
13+
*
14+
* @var \Symfony\Component\HttpKernel\HttpKernelInterface
15+
*/
16+
protected $app;
17+
18+
/**
19+
* Laravel application container.
20+
*
21+
* @var \Illuminate\Container\Container
22+
*/
23+
protected $container;
24+
25+
/**
26+
* Default rate limiting config.
27+
*
28+
* @var array
29+
*/
30+
protected $defaultConfig = [
31+
'authenticated' => [
32+
'limit' => 6000,
33+
'reset' => 3600
34+
],
35+
'unauthenticated' => [
36+
'limit' => 60,
37+
'reset' => 3600
38+
],
39+
'exceeded' => 'API rate limit has been exceeded.'
40+
];
41+
42+
/**
43+
* Array of resolved container bindings.
44+
*
45+
* @var array
46+
*/
47+
protected $bindings = [];
48+
49+
/**
50+
* Create a new rate limit middleware instance.
51+
*
52+
* @param \Symfony\Component\HttpKernel\HttpKernelInterface $app
53+
* @param \Illuminate\Container\Container $container
54+
* @return void
55+
*/
56+
public function __construct(HttpKernelInterface $app, Container $container)
57+
{
58+
$this->app = $app;
59+
$this->container = $container;
60+
}
61+
62+
/**
63+
* Handle a given request and return the response.
64+
*
65+
* @param \Symfony\Component\HttpFoundation\Request $request
66+
* @param int $type
67+
* @param bool $catch
68+
* @return \Symfony\Component\HttpFoundation\Response
69+
* @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException
70+
*/
71+
public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
72+
{
73+
// Our middleware needs to ensure that Laravel is booted before we
74+
// can do anything. This gives us access to all the booted
75+
// service providers and other container bindings.
76+
$this->container->boot();
77+
78+
$this->defaultConfig = array_merge($this->defaultConfig, $this->config->get('api::rate_limiting'));
79+
80+
// Internal requests as well as requests that are not targetting the
81+
// API will not be rate limited. We'll also be sure not to perform
82+
// any rate limiting if it has been disabled.
83+
if ($request instanceof InternalRequest or ! $this->router->requestTargettingApi($request) or $this->rateLimitingDisabled())
84+
{
85+
return $this->app->handle($request, $type, $catch);
86+
}
87+
88+
$cacheKeys = $this->getCacheKeys($request);
89+
90+
$this->cache->add($cacheKeys['limit'], 0, $this->getCacheReset());
91+
$this->cache->add($cacheKeys['reset'], time() + ($this->getCacheReset() * 60), $this->getCacheReset());
92+
93+
$this->cache->increment($cacheKeys['limit']);
94+
95+
// If the total number of requests made exceeds the allowed number of
96+
// requests then we'll create a new API response with a 403 status
97+
// code. This will inform the consumer they have breached their
98+
// allowed limit and must wait until it is reset.
99+
$allowedRequests = $this->getAllowedRequests();
100+
$totalRequests = $this->cache->get($cacheKeys['limit']);
101+
102+
if ($totalRequests > $allowedRequests)
103+
{
104+
$response = new Response($this->defaultConfig['exceeded'], 403);
105+
106+
$response->morph();
107+
}
108+
109+
// Otherwise we'll let the next middleware handle the request and
110+
// use the response returned from that.
111+
else
112+
{
113+
$response = $this->app->handle($request, $type, $catch);
114+
}
115+
116+
$requestsRemaining = $allowedRequests - $totalRequests;
117+
118+
$response->headers->set('X-RateLimit-Limit', $allowedRequests);
119+
$response->headers->set('X-RateLimit-Remaining', $requestsRemaining > 0 ? $requestsRemaining : 0);
120+
$response->headers->set('X-RateLimit-Reset', $this->cache->get($cacheKeys['reset']));
121+
122+
return $response;
123+
}
124+
125+
/**
126+
* Get the allowed number of requests.
127+
*
128+
* @return int
129+
*/
130+
protected function getAllowedRequests()
131+
{
132+
return $this->shield->check() ? $this->defaultConfig['authenticated']['limit'] : $this->defaultConfig['unauthenticated']['limit'];
133+
}
134+
135+
/**
136+
* Determine if rate limiting is disabled.
137+
*
138+
* @return bool
139+
*/
140+
protected function rateLimitingDisabled()
141+
{
142+
return $this->getAllowedRequests() == 0;
143+
}
144+
145+
/**
146+
* Get the cache reset time.
147+
*
148+
* @return int
149+
*/
150+
protected function getCacheReset()
151+
{
152+
153+
return $this->shield->check() ? $this->defaultConfig['authenticated']['reset'] : $this->defaultConfig['unauthenticated']['reset'];
154+
}
155+
156+
/**
157+
* Get the "limit" and "reset" cache keys.
158+
*
159+
* @param \Symfony\Component\HttpFoundation\Request $request
160+
* @return array
161+
*/
162+
protected function getCacheKeys(Request $request)
163+
{
164+
return [
165+
'limit' => sprintf('dingo:api:limit:%s', $request->getClientIp()),
166+
'reset' => sprintf('dingo:api:reset:%s', $request->getClientIp())
167+
];
168+
}
169+
170+
/**
171+
* Dynamically handle binding calls on the container.
172+
*
173+
* @param string $binding
174+
* @return mixed
175+
*/
176+
public function __get($binding)
177+
{
178+
$mappings = ['shield' => 'dingo.api.auth'];
179+
$binding = isset($mappings[$binding]) ? $mappings[$binding] : $binding;
180+
181+
if (isset($this->bindings[$binding])) return $this->bindings[$binding];
182+
183+
return $this->bindings[$binding] = $this->container->make($binding);
184+
}
185+
186+
}

src/config/config.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,36 @@
6565

6666
'auth' => ['basic'],
6767

68+
/*
69+
|--------------------------------------------------------------------------
70+
| Rate Limiting
71+
|--------------------------------------------------------------------------
72+
|
73+
| Consumers of your API can be limited to the amount of requests they can
74+
| make. You can configure the limit based on whether the consumer is
75+
| authenticated or unauthenticated.
76+
|
77+
| The "limit" is the number of requests the consumer can make within a
78+
| certain amount time which is defined by "reset" in minutes.
79+
|
80+
*/
81+
82+
'rate_limiting' => [
83+
84+
'authenticated' => [
85+
'limit' => 6000,
86+
'reset' => 60
87+
],
88+
89+
'unauthenticated' => [
90+
'limit' => 60,
91+
'reset' => 60
92+
],
93+
94+
'exceeded' => 'API rate limit has been exceeded.'
95+
96+
],
97+
6898
/*
6999
|--------------------------------------------------------------------------
70100
| Response Formats

0 commit comments

Comments
 (0)