| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758 |
- <?php
- declare(strict_types=1);
- /** Proxy-aware caller address resolution. No database or network access. */
- require dirname(__DIR__) . '/vendor/autoload.php';
- \Dotenv\Dotenv::createUnsafeMutable(dirname(__DIR__))->load();
- \support\App::loadAllConfig(['route']);
- use app\miniapi\http\ClientIp;
- $checks = 0;
- function check(bool $ok, string $label): void
- {
- global $checks;
- $checks++;
- if (!$ok) {
- throw new RuntimeException($label);
- }
- }
- $loopback = ['127.0.0.1', '::1'];
- // An untrusted peer keeps its own address and its header is ignored outright.
- check(ClientIp::from('203.0.113.9', '1.2.3.4', $loopback) === '203.0.113.9', 'Untrusted peer ignores forwarded header');
- check(ClientIp::from('203.0.113.9', '', []) === '203.0.113.9', 'Empty allowlist ignores forwarded header');
- // Behind a trusted proxy the caller address comes from the chain.
- check(ClientIp::from('127.0.0.1', '203.0.113.9', $loopback) === '203.0.113.9', 'Trusted loopback proxy resolves caller');
- check(ClientIp::from('::1', '203.0.113.9', $loopback) === '203.0.113.9', 'Trusted IPv6 loopback proxy resolves caller');
- // Multi-hop chains are walked right to left, skipping every trusted hop.
- check(
- ClientIp::from('127.0.0.1', '1.2.3.4, 203.0.113.9, 127.0.0.1', $loopback) === '203.0.113.9',
- 'Rightmost untrusted hop wins'
- );
- check(
- ClientIp::from('127.0.0.1', '1.2.3.4, 203.0.113.9, 10.0.0.1', ['127.0.0.1', '10.0.0.1']) === '203.0.113.9',
- 'Configured proxy hops are skipped'
- );
- // A proxy that sends no usable header falls back to the peer rather than trusting junk.
- check(ClientIp::from('127.0.0.1', '', $loopback) === '127.0.0.1', 'Missing header falls back to peer');
- check(ClientIp::from('127.0.0.1', ' ', $loopback) === '127.0.0.1', 'Blank header falls back to peer');
- check(ClientIp::from('127.0.0.1', 'not-an-ip, bogus', $loopback) === '127.0.0.1', 'Unparsable chain falls back to peer');
- check(ClientIp::from('127.0.0.1', '127.0.0.1, ::1', $loopback) === '127.0.0.1', 'Fully trusted chain falls back to peer');
- // Proxy formats: host:port for IPv4 and the bracketed form for IPv6.
- check(ClientIp::from('127.0.0.1', '203.0.113.9:54321', $loopback) === '203.0.113.9', 'IPv4 port suffix stripped');
- check(ClientIp::from('127.0.0.1', '[2001:db8::1]:443', $loopback) === '2001:db8::1', 'Bracketed IPv6 port stripped');
- check(ClientIp::from('[::1]:8080', '203.0.113.9', ['::1']) === '203.0.113.9', 'Peer given in bracketed form');
- check(ClientIp::from('::1', '2001:db8::1', ['::1']) === '2001:db8::1', 'Bare IPv6 chain entry kept intact');
- // Allowlist entries are canonicalised too, so configuration spelling cannot silently diverge.
- check(ClientIp::from('127.0.0.1', '203.0.113.9', [' 127.0.0.1 ', '::1', '', 'garbage']) === '203.0.113.9', 'Allowlist entries trimmed');
- check(ClientIp::from('', '203.0.113.9', $loopback) === '', 'Empty peer stays empty');
- echo "PASS: $checks client address checks.\n";
|