Skip to content

Commit f646dcf

Browse files
committed
Added ip resolver.
1 parent aa7e432 commit f646dcf

File tree

8 files changed

+420
-13
lines changed

8 files changed

+420
-13
lines changed

.version.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"strategy": "semver",
33
"major": 0,
4-
"minor": 6,
5-
"patch": 10,
4+
"minor": 7,
5+
"patch": 0,
66
"build": 0
77
}

VERSIONLOG.md

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
## 0.6.11
2-
* Added comprehensive rate limiting system with multiple storage backends
3-
* Added RateLimitFilter for flexible request throttling
4-
* Added Redis, File, and Memory storage implementations for rate limiting
5-
* Added configurable rate limiting with environment variable support (flat structure)
6-
* Added whitelisting and blacklisting support for rate limiting
7-
* Added automatic rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset)
8-
* Added HTTP 429 response formatting with JSON/HTML content negotiation
9-
* Added comprehensive tests for rate limiting functionality
1+
* Added comprehensive rate limiting system with multiple storage backends.
2+
* Added RateLimitFilter for request throttling.
3+
* Added Redis, File, and Memory storage implementations for rate limiting.
4+
* Added configurable rate limiting with environment variable support (flat structure).
5+
* Added whitelisting and blacklisting support for rate limiting.
6+
* Added automatic rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset).
7+
* Added HTTP 429 response formatting with JSON/HTML content negotiation.
8+
* Added comprehensive tests for rate limiting functionality.
9+
* Added IPResolver.
1010

1111
## 0.6.10
12-
1312
## 0.6.9 2025-05-21
1413

1514
## 0.6.8 2025-02-18

src/Routing/DefaultIpResolver.php

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
namespace Neuron\Routing;
4+
5+
/**
6+
* Default IP resolver implementation that checks common proxy headers.
7+
*
8+
* This resolver checks standard proxy headers in order of preference,
9+
* validating and extracting the client IP address. It handles:
10+
* - Cloudflare (CF-Connecting-IP)
11+
* - Standard proxies (X-Forwarded-For)
12+
* - Nginx (X-Real-IP)
13+
* - Other proxies (Client-IP)
14+
*
15+
* Falls back to REMOTE_ADDR if no valid IP is found in headers.
16+
*
17+
* @package Neuron\Routing
18+
*/
19+
class DefaultIpResolver implements IIpResolver
20+
{
21+
/**
22+
* Resolve the client IP address by checking common proxy headers.
23+
*
24+
* @param array $server The $_SERVER array
25+
* @return string The resolved IP address
26+
*/
27+
public function resolve( array $server ): string
28+
{
29+
// Check common proxy headers in order of preference
30+
$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
35+
];
36+
37+
foreach( $headers as $header )
38+
{
39+
if( !empty( $server[ $header ] ) )
40+
{
41+
// Handle comma-separated list (proxy chain) - take first IP
42+
$ip = $this->extractFirstIp( $server[ $header ] );
43+
44+
// Validate IP format
45+
if( filter_var( $ip, FILTER_VALIDATE_IP ) )
46+
{
47+
return $ip;
48+
}
49+
}
50+
}
51+
52+
// Fallback to direct connection
53+
return $server[ 'REMOTE_ADDR' ] ?? '0.0.0.0';
54+
}
55+
56+
/**
57+
* Extract the first IP from a potentially comma-separated list.
58+
*
59+
* @param string $ipString The IP string (may contain multiple IPs)
60+
* @return string The first IP address
61+
*/
62+
protected function extractFirstIp( string $ipString ): string
63+
{
64+
if( strpos( $ipString, ',' ) !== false )
65+
{
66+
$ips = explode( ',', $ipString );
67+
return trim( $ips[0] );
68+
}
69+
70+
return trim( $ipString );
71+
}
72+
}

src/Routing/IIpResolver.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace Neuron\Routing;
4+
5+
/**
6+
* Interface for IP address resolution strategies.
7+
*
8+
* Implementations of this interface define how to extract
9+
* the client's IP address from server variables, handling
10+
* various proxy and load balancer configurations.
11+
*
12+
* @package Neuron\Routing
13+
*/
14+
interface IIpResolver
15+
{
16+
/**
17+
* Resolve the client IP address from server variables.
18+
*
19+
* @param array $server The $_SERVER array containing request information
20+
* @return string The resolved IP address (returns '0.0.0.0' if unable to resolve)
21+
*/
22+
public function resolve( array $server ): string;
23+
}

src/Routing/Request.php

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,23 @@ class Request
1717
private $_Get;
1818
private $_Post;
1919
private $_Server;
20+
private ?IIpResolver $_ipResolver;
21+
private ?string $_sourceIp = null;
2022

2123
/**
2224
* Request constructor.
2325
* @param RouteMap $Route
2426
* @param $Method
27+
* @param IIpResolver|null $ipResolver Optional IP resolver
2528
*/
26-
public function __construct( RouteMap $Route, $Method )
29+
public function __construct( RouteMap $Route, $Method, ?IIpResolver $ipResolver = null )
2730
{
2831
$this->_Get = new Get();
2932
$this->_Post = new Post();
3033
$this->_Server = new Server();
3134
$this->_Route = $Route;
3235
$this->_RequestMethod = $Method;
36+
$this->_ipResolver = $ipResolver;
3337
}
3438

3539
/**
@@ -96,4 +100,22 @@ public function getRequest( $Name ): mixed
96100
public function getRouteParam( $Name )
97101
{
98102
}
103+
104+
/**
105+
* Get the source IP address of the request.
106+
*
107+
* Uses the configured IP resolver to determine the client's IP address,
108+
* falling back to DefaultIpResolver if none is configured.
109+
*
110+
* @return string The client IP address
111+
*/
112+
public function getSourceIp(): string
113+
{
114+
if( $this->_sourceIp === null )
115+
{
116+
$resolver = $this->_ipResolver ?? new DefaultIpResolver();
117+
$this->_sourceIp = $resolver->resolve( $_SERVER );
118+
}
119+
return $this->_sourceIp;
120+
}
99121
}

src/Routing/Router.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,18 @@ class Router extends Memory implements IRunnable
5151
private array $_Filter = [];
5252

5353
private array $_FilterRegistry = [];
54+
private ?IIpResolver $_ipResolver = null;
55+
56+
/**
57+
* Set the IP resolver for all requests handled by this router.
58+
*
59+
* @param IIpResolver $resolver The IP resolver to use
60+
* @return void
61+
*/
62+
public function setIpResolver( IIpResolver $resolver ): void
63+
{
64+
$this->_ipResolver = $resolver;
65+
}
5466

5567
/**
5668
* @param string $Name
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
<?php
2+
3+
namespace Tests\Unit;
4+
5+
use Neuron\Routing\DefaultIpResolver;
6+
use PHPUnit\Framework\TestCase;
7+
8+
class DefaultIpResolverTest extends TestCase
9+
{
10+
private DefaultIpResolver $resolver;
11+
12+
protected function setUp(): void
13+
{
14+
parent::setUp();
15+
$this->resolver = new DefaultIpResolver();
16+
}
17+
18+
public function testResolveDirectConnection(): void
19+
{
20+
$server = [
21+
'REMOTE_ADDR' => '192.168.1.100'
22+
];
23+
24+
$ip = $this->resolver->resolve( $server );
25+
26+
$this->assertEquals( '192.168.1.100', $ip );
27+
}
28+
29+
public function testResolveCloudflareHeader(): void
30+
{
31+
$server = [
32+
'HTTP_CF_CONNECTING_IP' => '203.0.113.42',
33+
'HTTP_X_FORWARDED_FOR' => '10.0.0.1',
34+
'REMOTE_ADDR' => '172.71.255.1' // Cloudflare IP
35+
];
36+
37+
$ip = $this->resolver->resolve( $server );
38+
39+
$this->assertEquals( '203.0.113.42', $ip );
40+
}
41+
42+
public function testResolveXForwardedFor(): void
43+
{
44+
$server = [
45+
'HTTP_X_FORWARDED_FOR' => '203.0.113.42',
46+
'REMOTE_ADDR' => '10.0.0.1'
47+
];
48+
49+
$ip = $this->resolver->resolve( $server );
50+
51+
$this->assertEquals( '203.0.113.42', $ip );
52+
}
53+
54+
public function testResolveXForwardedForWithProxyChain(): void
55+
{
56+
$server = [
57+
'HTTP_X_FORWARDED_FOR' => '203.0.113.42, 10.0.0.1, 172.16.0.1',
58+
'REMOTE_ADDR' => '192.168.1.1'
59+
];
60+
61+
$ip = $this->resolver->resolve( $server );
62+
63+
// Should return the first IP (original client)
64+
$this->assertEquals( '203.0.113.42', $ip );
65+
}
66+
67+
public function testResolveXRealIp(): void
68+
{
69+
$server = [
70+
'HTTP_X_REAL_IP' => '203.0.113.42',
71+
'REMOTE_ADDR' => '10.0.0.1'
72+
];
73+
74+
$ip = $this->resolver->resolve( $server );
75+
76+
$this->assertEquals( '203.0.113.42', $ip );
77+
}
78+
79+
public function testResolveClientIp(): void
80+
{
81+
$server = [
82+
'HTTP_CLIENT_IP' => '203.0.113.42',
83+
'REMOTE_ADDR' => '10.0.0.1'
84+
];
85+
86+
$ip = $this->resolver->resolve( $server );
87+
88+
$this->assertEquals( '203.0.113.42', $ip );
89+
}
90+
91+
public function testResolveInvalidIpFallsBackToRemoteAddr(): void
92+
{
93+
$server = [
94+
'HTTP_X_FORWARDED_FOR' => 'not-an-ip',
95+
'HTTP_X_REAL_IP' => 'invalid',
96+
'REMOTE_ADDR' => '192.168.1.100'
97+
];
98+
99+
$ip = $this->resolver->resolve( $server );
100+
101+
$this->assertEquals( '192.168.1.100', $ip );
102+
}
103+
104+
public function testResolveMissingRemoteAddr(): void
105+
{
106+
$server = [
107+
'HTTP_X_FORWARDED_FOR' => 'invalid'
108+
];
109+
110+
$ip = $this->resolver->resolve( $server );
111+
112+
$this->assertEquals( '0.0.0.0', $ip );
113+
}
114+
115+
public function testResolveEmptyServer(): void
116+
{
117+
$server = [];
118+
119+
$ip = $this->resolver->resolve( $server );
120+
121+
$this->assertEquals( '0.0.0.0', $ip );
122+
}
123+
124+
public function testResolveHeaderPriority(): void
125+
{
126+
// All headers present - should use CF-Connecting-IP first
127+
$server = [
128+
'HTTP_CF_CONNECTING_IP' => '1.1.1.1',
129+
'HTTP_X_FORWARDED_FOR' => '2.2.2.2',
130+
'HTTP_X_REAL_IP' => '3.3.3.3',
131+
'HTTP_CLIENT_IP' => '4.4.4.4',
132+
'REMOTE_ADDR' => '5.5.5.5'
133+
];
134+
135+
$ip = $this->resolver->resolve( $server );
136+
137+
$this->assertEquals( '1.1.1.1', $ip );
138+
}
139+
140+
public function testResolveIpv6(): void
141+
{
142+
$server = [
143+
'HTTP_X_FORWARDED_FOR' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334',
144+
'REMOTE_ADDR' => '192.168.1.1'
145+
];
146+
147+
$ip = $this->resolver->resolve( $server );
148+
149+
$this->assertEquals( '2001:0db8:85a3:0000:0000:8a2e:0370:7334', $ip );
150+
}
151+
152+
public function testResolveIpv6Compressed(): void
153+
{
154+
$server = [
155+
'HTTP_X_FORWARDED_FOR' => '2001:db8::8a2e:370:7334',
156+
'REMOTE_ADDR' => '192.168.1.1'
157+
];
158+
159+
$ip = $this->resolver->resolve( $server );
160+
161+
$this->assertEquals( '2001:db8::8a2e:370:7334', $ip );
162+
}
163+
164+
public function testResolveTrimmedIp(): void
165+
{
166+
$server = [
167+
'HTTP_X_FORWARDED_FOR' => ' 203.0.113.42 ',
168+
'REMOTE_ADDR' => '192.168.1.1'
169+
];
170+
171+
$ip = $this->resolver->resolve( $server );
172+
173+
$this->assertEquals( '203.0.113.42', $ip );
174+
}
175+
}

0 commit comments

Comments
 (0)