Skip to content

Commit ecc0679

Browse files
committed
Merge branch 'release/0.8.3'
2 parents ea7a940 + 5a80e9b commit ecc0679

File tree

6 files changed

+125
-76
lines changed

6 files changed

+125
-76
lines changed

.version.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
"strategy": "semver",
33
"major": 0,
44
"minor": 8,
5-
"patch": 2,
5+
"patch": 3,
66
"build": 0
77
}

README.md

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,6 @@ $router->get('/api/data', $handler, 'rate_limit');
114114

115115
### Configuration Options
116116

117-
Rate limiting can be configured via array or environment variables:
118-
119117
```php
120118
// Array configuration
121119
$config = new RateLimitConfig([
@@ -127,13 +125,6 @@ $config = new RateLimitConfig([
127125
'redis_port' => 6379,
128126
'file_path' => 'cache/rate_limits'
129127
]);
130-
131-
// From settings/environment variables (flat structure)
132-
// RATE_LIMIT_ENABLED=true
133-
// RATE_LIMIT_STORAGE=redis
134-
// RATE_LIMIT_REQUESTS=100
135-
// RATE_LIMIT_WINDOW=3600
136-
$config = RateLimitConfig::fromSettings($settingsSource);
137128
```
138129

139130
### Storage Backends
@@ -172,30 +163,6 @@ $config = new RateLimitConfig([
172163
]);
173164
```
174165

175-
### Advanced Features
176-
177-
#### Key Strategies
178-
Control how rate limits are applied:
179-
180-
```php
181-
// Limit by IP address (default)
182-
$filter = new RateLimitFilter($config, 'ip');
183-
184-
// Limit by authenticated user
185-
$filter = new RateLimitFilter($config, 'user');
186-
187-
// Limit by IP + route combination
188-
$filter = new RateLimitFilter($config, 'route');
189-
190-
// Custom key generation
191-
class CustomRateLimitFilter extends RateLimitFilter {
192-
protected function getCustomKey(RouteMap $route): string {
193-
// Your custom logic here
194-
return $_SESSION['tenant_id'] ?? $this->getClientIp();
195-
}
196-
}
197-
```
198-
199166
#### Whitelisting and Blacklisting
200167

201168
```php

VERSIONLOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## 0.8.3 2025-11-12
2+
* Updated to use the IPResolver for rate limiting.
3+
14
## 0.8.2 2025-11-11
25

36
## 0.8.1 2025-11-07

src/Routing/DefaultIpResolver.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ public function resolve( array $server ): string
2828
{
2929
// Check common proxy headers in order of preference
3030
$headers = [
31-
'HTTP_CF_CONNECTING_IP', // Cloudflare
32-
'HTTP_X_FORWARDED_FOR', // Standard proxy
33-
'HTTP_X_REAL_IP', // Nginx
34-
'HTTP_CLIENT_IP', // Some proxies
31+
'HTTP_CF_CONNECTING_IP', // Cloudflare
32+
'HTTP_X_FORWARDED_FOR', // Standard proxy
33+
'HTTP_X_REAL_IP', // Nginx
34+
'HTTP_CLIENT_IP', // Some proxies
3535
];
3636

3737
foreach( $headers as $header )
@@ -61,12 +61,12 @@ public function resolve( array $server ): string
6161
*/
6262
protected function extractFirstIp( string $ipString ): string
6363
{
64-
if( strpos( $ipString, ',' ) !== false )
64+
if( str_contains( $ipString, ',' ) )
6565
{
6666
$ips = explode( ',', $ipString );
6767
return trim( $ips[0] );
6868
}
6969

7070
return trim( $ipString );
7171
}
72-
}
72+
}

src/Routing/Filters/RateLimitFilter.php

Lines changed: 21 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
use Neuron\Routing\RateLimit\RateLimitResponse;
99
use Neuron\Routing\RateLimit\Storage\IRateLimitStorage;
1010
use Neuron\Routing\RateLimit\Storage\RateLimitStorageFactory;
11+
use Neuron\Routing\IIpResolver;
12+
use Neuron\Routing\DefaultIpResolver;
1113
use Neuron\Patterns\Registry;
1214
use Neuron\Log\Log;
1315

@@ -26,24 +28,29 @@ class RateLimitFilter extends Filter
2628
private string $_keyStrategy;
2729
private array $_whitelist;
2830
private array $_blacklist;
31+
private IIpResolver $_ipResolver;
2932

3033
/**
3134
* @param RateLimitConfig $config Rate limit configuration
3235
* @param string $keyStrategy Key generation strategy ('ip', 'user', 'route', 'custom')
3336
* @param array $whitelist IP addresses or user IDs to exempt from rate limiting
3437
* @param array $blacklist IP addresses or user IDs to apply stricter limits
38+
* @param IIpResolver|null $ipResolver Optional IP resolver (defaults to DefaultIpResolver)
39+
* @throws \Exception
3540
*/
3641
public function __construct(
3742
RateLimitConfig $config,
3843
string $keyStrategy = 'ip',
3944
array $whitelist = [],
40-
array $blacklist = []
45+
array $blacklist = [],
46+
?IIpResolver $ipResolver = null
4147
)
4248
{
4349
$this->_config = $config;
4450
$this->_keyStrategy = $keyStrategy;
4551
$this->_whitelist = $whitelist;
4652
$this->_blacklist = $blacklist;
53+
$this->_ipResolver = $ipResolver ?? new DefaultIpResolver();
4754

4855
// Get base path from registry if available
4956
$basePath = Registry::getInstance()->get( 'BasePath' ) ?? '';
@@ -59,7 +66,7 @@ function( RouteMap $route ) { $this->checkRateLimit( $route ); },
5966
}
6067

6168
/**
62-
* Check rate limit for the current request.
69+
* Check the rate limit for the current request.
6370
*
6471
* @param RouteMap $route
6572
* @return void
@@ -112,45 +119,23 @@ protected function checkRateLimit( RouteMap $route ): void
112119
*/
113120
protected function generateKey( RouteMap $route ): string
114121
{
115-
switch( $this->_keyStrategy )
122+
return match ( $this->_keyStrategy )
116123
{
117-
case 'ip':
118-
return $this->getClientIp();
119-
120-
case 'user':
121-
return $this->getUserId();
122-
123-
case 'route':
124-
return $this->getClientIp() . ':' . $route->getPath();
125-
126-
case 'custom':
127-
return $this->getCustomKey( $route );
128-
129-
default:
130-
return $this->getClientIp();
131-
}
124+
'user' => $this->getUserId(),
125+
'route' => $this->getClientIp() . ':' . $route->getPath(),
126+
'custom' => $this->getCustomKey( $route ),
127+
default => $this->getClientIp(),
128+
};
132129
}
133130

134131
/**
135-
* Get client IP address.
132+
* Get the client IP address using the configured IP resolver.
136133
*
137134
* @return string
138135
*/
139136
protected function getClientIp(): string
140137
{
141-
// Check for forwarded IP (proxy/load balancer)
142-
if( !empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) )
143-
{
144-
$ips = explode( ',', $_SERVER['HTTP_X_FORWARDED_FOR'] );
145-
return trim( $ips[0] );
146-
}
147-
148-
if( !empty( $_SERVER['HTTP_X_REAL_IP'] ) )
149-
{
150-
return $_SERVER['HTTP_X_REAL_IP'];
151-
}
152-
153-
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
138+
return $this->_ipResolver->resolve( $_SERVER );
154139
}
155140

156141
/**
@@ -173,7 +158,7 @@ protected function getUserId(): string
173158
}
174159

175160
/**
176-
* Get custom key for rate limiting.
161+
* Get a custom key for rate limiting.
177162
*
178163
* @param RouteMap $route
179164
* @return string
@@ -224,7 +209,7 @@ protected function getLimit( string $key ): int
224209
}
225210

226211
/**
227-
* Get time window for a key.
212+
* Get the time window for a key.
228213
*
229214
* @param string $key
230215
* @return int
@@ -236,7 +221,7 @@ protected function getWindow( string $key ): int
236221
}
237222

238223
/**
239-
* Add rate limit headers to response.
224+
* Add the rate limit headers to response.
240225
*
241226
* @param string $key
242227
* @param int $limit
@@ -283,4 +268,4 @@ public function clear(): void
283268
{
284269
$this->_storage->clear();
285270
}
286-
}
271+
}

tests/unit/RateLimit/RateLimitFilterTest.php

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
use Neuron\Routing\RateLimit\RateLimitConfig;
66
use Neuron\Routing\RouteMap;
77
use Neuron\Routing\Router;
8+
use Neuron\Routing\IIpResolver;
9+
use Neuron\Routing\DefaultIpResolver;
810

911
class RateLimitFilterTest extends TestCase
1012
{
@@ -192,11 +194,103 @@ public function testKeyStrategies()
192194
$this->assertEquals('10.0.0.1:/test', $keyRoute);
193195
}
194196

197+
public function testCustomIpResolver()
198+
{
199+
// Create a mock IP resolver that always returns a specific IP
200+
$mockResolver = $this->createMock(IIpResolver::class);
201+
$mockResolver->method('resolve')
202+
->willReturn('203.0.113.42');
203+
204+
$config = new RateLimitConfig([
205+
'enabled' => true,
206+
'storage' => 'memory',
207+
'requests' => 5,
208+
'window' => 60
209+
]);
210+
211+
$filter = new RateLimitFilter(
212+
$config,
213+
'ip',
214+
[],
215+
[],
216+
$mockResolver
217+
);
218+
219+
$route = new RouteMap('/test', function() { return 'test'; });
220+
221+
// Use reflection to verify the IP being used
222+
$reflection = new ReflectionClass($filter);
223+
$method = $reflection->getMethod('generateKey');
224+
$method->setAccessible(true);
225+
226+
$key = $method->invoke($filter, $route);
227+
228+
// Should use the IP from our mock resolver
229+
$this->assertEquals('203.0.113.42', $key);
230+
}
231+
232+
public function testDefaultIpResolver()
233+
{
234+
// Test that DefaultIpResolver is used when no resolver is provided
235+
$_SERVER['REMOTE_ADDR'] = '198.51.100.25';
236+
237+
$config = new RateLimitConfig([
238+
'enabled' => true,
239+
'storage' => 'memory',
240+
'requests' => 5,
241+
'window' => 60
242+
]);
243+
244+
// Create filter without explicit IP resolver
245+
$filter = new RateLimitFilter($config, 'ip');
246+
247+
$route = new RouteMap('/test', function() { return 'test'; });
248+
249+
// Use reflection to verify IP resolution
250+
$reflection = new ReflectionClass($filter);
251+
$method = $reflection->getMethod('generateKey');
252+
$method->setAccessible(true);
253+
254+
$key = $method->invoke($filter, $route);
255+
256+
// Should use the IP from $_SERVER via DefaultIpResolver
257+
$this->assertEquals('198.51.100.25', $key);
258+
}
259+
260+
public function testCloudflareIpResolution()
261+
{
262+
// Test that Cloudflare headers are supported via DefaultIpResolver
263+
$_SERVER['REMOTE_ADDR'] = '192.0.2.1';
264+
$_SERVER['HTTP_CF_CONNECTING_IP'] = '198.51.100.50';
265+
266+
$config = new RateLimitConfig([
267+
'enabled' => true,
268+
'storage' => 'memory',
269+
'requests' => 5,
270+
'window' => 60
271+
]);
272+
273+
$filter = new RateLimitFilter($config, 'ip');
274+
275+
$route = new RouteMap('/test', function() { return 'test'; });
276+
277+
// Use reflection to verify IP resolution
278+
$reflection = new ReflectionClass($filter);
279+
$method = $reflection->getMethod('generateKey');
280+
$method->setAccessible(true);
281+
282+
$key = $method->invoke($filter, $route);
283+
284+
// Should prioritize Cloudflare header
285+
$this->assertEquals('198.51.100.50', $key);
286+
}
287+
195288
protected function tearDown(): void
196289
{
197290
// Clean up server variables
198291
unset($_SERVER['REMOTE_ADDR']);
199292
unset($_SERVER['HTTP_X_FORWARDED_FOR']);
200293
unset($_SERVER['HTTP_X_REAL_IP']);
294+
unset($_SERVER['HTTP_CF_CONNECTING_IP']);
201295
}
202296
}

0 commit comments

Comments
 (0)