xref: /dokuwiki/_test/tests/inc/IpTest.php (revision 884caed926ca0aa0af6ce3f34ae3aa7317a3361a)
1<?php
2
3namespace dokuwiki\test;
4
5use dokuwiki\Input\Input;
6use dokuwiki\Ip;
7
8class IpTest extends \DokuWikiTest {
9
10    /**
11     * The data provider for ipToNumber() tests.
12     *
13     * @return mixed[][] Returns an array of test cases.
14     */
15    public function ip_to_number_provider() : array
16    {
17        $tests = [
18            ['127.0.0.1', 4, 0x00000000, 0x7f000001],
19            ['::127.0.0.1', 6, 0x00000000, 0x7f000001],
20            ['::1', 6, 0x00000000, 0x00000001],
21            ['38AF:3033:AA39:CDE3:1A46:094C:44ED:5300', 6, 0x38AF3033AA39CDE3, 0x1A46094C44ED5300],
22            ['0000:0000:0000:0000:0000:0000:0000:0000', 6, 0, 0],
23            ['193.53.125.7', 4, 0x00000000, 0xC1357D07],
24            // NOTE: wrap around! 0xFFFFFFFFFFFFFFFE seen as -2
25            ['7FFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFE', 6, 0x7FFFFFFFFFFFFFFF, -2],
26        ];
27
28        // float loses percision and give confusing test results!
29        // sprintf("%.0f",0x7FFFFFFFFFFFFFFF) == sprintf("%.0f",0x7FFFFFFFFFFFFF00)
30        // using string of decimal values instead
31        if(PHP_INT_SIZE == 4) {
32            $tests = [
33                ['127.0.0.1', 4, '0', '2130706433'],
34                ['::127.0.0.1', 6, '0','2130706433'],
35                ['::1', 6, '0', '1'],
36                ['0000:0000:0000:0000:0000:0000:0000:0000', 6, '0', '0'],
37                ['193.53.125.7', 4, '0', '3241508103'],
38                ['7FFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFE', 6, '9223372036854775807', '18446744073709551614'],
39                ['38AF:3033:AA39:CDE3:1A46:094C:44ED:5300', 6, '4084536385505709539', '1893210916534440704']
40            ];
41        }
42        return $tests;
43    }
44
45    /**
46     * Test ipToNumber().
47     *
48     * @dataProvider ip_to_number_provider
49     *
50     * @param string $ip The IP address to convert.
51     * @param int    $version The IP version, either 4 or 6.
52     * @param int    $upper   The upper 64 bits of the IP.
53     * @param int    $lower   The lower 64 bits of the IP.
54     *
55     * @return void
56     *
57     * Note: $upper and $lower are likley 'float' instead of 'int' for large values.
58     * The more accurate hint 'int|float' is not supported in php7.4.
59     */
60    public function test_ip_to_number(string $ip, int $version, $upper, $lower): void
61    {
62        $result = Ip::ipToNumber($ip);
63
64        // force output of ipv4 to string for easy compare
65        if(PHP_INT_SIZE == 4 and !is_string($result['upper']) ) {
66            $result['upper'] = sprintf("%.0f", $result['upper']);
67            $result['lower'] = sprintf("%.0f", $result['lower']);
68        }
69
70        $this->assertSame($version, $result['version']);
71        $this->assertSame($upper, $result['upper']);
72        $this->assertSame($lower, $result['lower']);
73    }
74
75    /**
76     * The data provider for test_ip_in_range().
77     *
78     * @return mixed[][] Returns an array of test cases.
79     */
80    public function ip_in_range_provider(): array
81    {
82        $tests = [
83            ['192.168.11.2', '192.168.0.0/16', true],
84            ['192.168.11.2', '192.168.64.1/16', true],
85            ['192.168.11.2', '192.168.64.1/18', false],
86            ['192.168.11.2', '192.168.11.0/20', true],
87            ['127.0.0.1', '127.0.0.0/7', true],
88            ['127.0.0.1', '127.0.0.0/8', true],
89            ['127.0.0.1', '127.200.0.0/8', true],
90            ['127.0.0.1', '127.200.0.0/9', false],
91            ['127.0.0.1', '127.0.0.0/31', true],
92            ['127.0.0.1', '127.0.0.0/32', false],
93            ['127.0.0.1', '127.0.0.1/32', true],
94            ['1111:2222:3333:4444:5555:6666:7777:8888', '1110::/12', true],
95            ['1110:2222:3333:4444:5555:6666:7777:8888', '1110::/12', true],
96            ['1100:2222:3333:4444:5555:6666:7777:8888', '1110::/12', false],
97            ['1111:2222:3333:4444:5555:6666:7777:8888', '1111:2222:3300::/40', true],
98            ['1111:2222:3333:4444:5555:6666:7777:8888', '1111:2222:3200::/40', false],
99            ['1111:2222:3333:4444:5555:6666:7777:8888', '1111:2222:3333:4444:5555:6666:7777:8889/127', true],
100            ['1111:2222:3333:4444:5555:6666:7777:8888', '1111:2222:3333:4444:5555:6666:7777:8889/128', false],
101            ['1111:2222:3333:4444:5555:6666:7777:8889', '1111:2222:3333:4444:5555:6666:7777:8889/128', true],
102            ['abcd:ef0a:bcde:f0ab:cdef:0abc:def0:abcd', 'abcd:ef0a:bcde:f0ab:cdef:0abc:def0:abcd/128', true],
103            ['abcd:ef0a:bcde:f0ab:cdef:0abc:def0:abce', 'abcd:ef0a:bcde:f0ab:cdef:0abc:def0:abcd/128', false],
104        ];
105
106        return $tests;
107    }
108
109    /**
110     * Test ipInRange().
111     *
112     * @dataProvider ip_in_range_provider
113     *
114     * @param string $ip The IP to test.
115     * @param string $range The IP range to test against.
116     * @param bool $expected The expected result from ipInRange().
117     *
118     * @return void
119     */
120    public function test_ip_in_range(string $ip, string $range, bool $expected): void
121    {
122        $result = Ip::ipInRange($ip, $range);
123
124        $this->assertSame($expected, $result);
125    }
126
127    /**
128     * The data provider for test_ip_in_range_invalid_mask().
129     *
130     * @return string[][] Returns an array of [ip, range] with an invalid mask.
131     */
132    public function ip_in_range_invalid_mask_provider(): array
133    {
134        return [
135            // Non-numeric mask: would throw a TypeError on the "+= 96" IPv4 path.
136            ['127.0.0.1', '10.0.0.0/abc'],
137            // Empty mask.
138            ['127.0.0.1', '10.0.0.0/'],
139            // Negative mask: would pass the "> 128" check and match every IPv4.
140            ['1.2.3.4', '10.0.0.0/-1'],
141            ['1.2.3.4', '10.0.0.0/-5'],
142        ];
143    }
144
145    /**
146     * Test that ipInRange() rejects an invalid mask with an Exception rather
147     * than a TypeError or an over-broad match.
148     *
149     * @dataProvider ip_in_range_invalid_mask_provider
150     *
151     * @param string $ip    The IP to test.
152     * @param string $range The IP range with the invalid mask.
153     *
154     * @return void
155     */
156    public function test_ip_in_range_invalid_mask(string $ip, string $range): void
157    {
158        $this->expectException(\Exception::class);
159        Ip::ipInRange($ip, $range);
160    }
161
162    /**
163     * Data provider for test_ip_matches().
164     *
165     * @return mixed[][] Returns an array of test cases.
166     */
167    public function ip_matches_provider(): array
168    {
169        // Tests for a CIDR range.
170        $rangeTests = $this->ip_in_range_provider();
171
172        // Tests for an exact IP match.
173        $exactTests = [
174            ['127.0.0.1', '127.0.0.1', true],
175            ['127.0.0.1', '127.0.0.0', false],
176            ['aaaa:bbbb:cccc:dddd:eeee::', 'aaaa:bbbb:cccc:dddd:eeee:0000:0000:0000', true],
177            ['aaaa:bbbb:cccc:dddd:eeee:0000:0000:0000', 'aaaa:bbbb:cccc:dddd:eeee::', true],
178            ['aaaa:bbbb:0000:0000:0000:0000:0000:0001', 'aaaa:bbbb::1', true],
179            ['aaaa:bbbb::0001', 'aaaa:bbbb::1', true],
180            ['aaaa:bbbb::0001', 'aaaa:bbbb::', false],
181            ['::ffff:127.0.0.1', '127.0.0.1', false],
182            ['::ffff:127.0.0.1', '::0:ffff:127.0.0.1', true],
183        ];
184
185        // Invalid masks must degrade to "no match" instead of a fatal error
186        // (non-numeric mask) or an over-broad match (negative mask).
187        $invalidMaskTests = [
188            ['127.0.0.1', '10.0.0.0/abc', false],
189            ['127.0.0.1', '10.0.0.0/', false],
190            ['1.2.3.4', '10.0.0.0/-1', false],
191            ['1.2.3.4', '10.0.0.0/-5', false],
192        ];
193
194
195        return array_merge($rangeTests, $exactTests, $invalidMaskTests);
196    }
197
198    /**
199     * Test ipMatches().
200     *
201     * @dataProvider ip_matches_provider
202     *
203     * @param string $ip        The IP to test.
204     * @param string $ipOrRange The IP or IP range to test against.
205     * @param bool   $expected  The expeced result from ipMatches().
206     *
207     * @return void
208     */
209    public function test_ip_matches(string $ip, string $ipOrRange, bool $expected): void
210    {
211        $result = Ip::ipMatches($ip, $ipOrRange);
212
213        $this->assertSame($expected, $result);
214    }
215
216    /**
217     * Data provider for proxyIsTrusted().
218     *
219     * @return mixed[][] Returns an array of test cases.
220     */
221    public function proxy_is_trusted_provider(): array
222    {
223        // The new default configuration value.
224        $default = ['::1', 'fe80::/10', '127.0.0.0/8', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'];
225
226        // Adding some custom trusted proxies.
227        $custom = array_merge($default, ['1.2.3.4', '1122::', '3.0.0.1/8', '1111:2222::/32']);
228
229        $tests = [
230            // Empty configuration.
231            ['', '127.0.0.1', false],
232
233            // Configuration with an array of  IPs/CIDRs.
234            [$default, '127.0.0.1', true],
235            [$default, '127.1.2.3', true],
236            [$default, '10.1.2.3', true],
237            [$default, '11.1.2.3', false],
238            [$default, '172.16.0.1', true],
239            [$default, '172.160.0.1', false],
240            [$default, '172.31.255.255', true],
241            [$default, '172.32.0.0', false],
242            [$default, '172.200.0.0', false],
243            [$default, '192.168.2.3', true],
244            [$default, '192.169.1.2', false],
245            [$default, '::1', true],
246            [$default, '0000:0000:0000:0000:0000:0000:0000:0001', true],
247
248            // With custom proxies set.
249            [$custom, '127.0.0.1', true],
250            [$custom, '1.2.3.4', true],
251            [$custom, '3.0.1.2', true],
252            [$custom, '1122::', true],
253            [$custom, '1122:0000:0000:0000:0000:0000:0000:0000', true],
254            [$custom, '1111:2223::', false],
255            [$custom, '1111:2222::', true],
256            [$custom, '1111:2222:3333::', true],
257            [$custom, '1111:2222:3333::1', true],
258        ];
259
260        return $tests;
261    }
262
263    /**
264     * Test proxyIsTrusted().
265     *
266     * @dataProvider proxy_is_trusted_provider
267     *
268     * @param string|string[] $config   The value for $conf[trustedproxies].
269     * @param string          $ip       The proxy IP to test.
270     * @param bool            $expected The expected result from proxyIsTrusted().
271     */
272    public function test_proxy_is_trusted($config, string $ip, bool $expected): void
273    {
274        global $conf;
275        $conf['trustedproxies'] = $config;
276
277        $result = Ip::proxyIsTrusted($ip);
278
279        $this->assertSame($expected, $result);
280    }
281
282    /**
283     * Data provider for test_forwarded_for().
284     *
285     * @return mixed[][] Returns an array of test cases.
286     */
287    public function forwarded_for_provider(): array
288    {
289        // The new default configuration value.
290        $default = ['::1', 'fe80::/10', '127.0.0.0/8', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'];
291
292        // Adding some custom trusted proxies.
293        $custom = array_merge($default, ['1.2.3.4', '1122::', '3.0.0.1/8', '1111:2222::/32']);
294
295        $tests = [
296            // Empty config value should always return empty array.
297            [[], '', '127.0.0.1', []],
298            [[], '127.0.0.1', '127.0.0.1', []],
299
300            // The new default configuration.
301            [$default, '', '127.0.0.1', []],
302            [$default, '1.2.3.4', '127.0.0.1', ['1.2.3.4', '127.0.0.1']],
303            [$default, '1.2.3.4', '192.168.1.1', ['1.2.3.4', '192.168.1.1']],
304            [$default, '1.2.3.4,172.16.0.1', '192.168.1.1', ['1.2.3.4', '172.16.0.1', '192.168.1.1']],
305            [$default, '1.2.3.4,172.16.0.1', '::1', ['1.2.3.4', '172.16.0.1', '::1']],
306            [$default, '1.2.3.4,172.16.0.1', '::0001', ['1.2.3.4', '172.16.0.1', '::0001']],
307
308            // Directly from an untrusted proxy.
309            [$default, '', '127.0.0.1', []],
310            [$default, '1.2.3.4', '11.22.33.44', []],
311            [$default, '::1', '11.22.33.44', []],
312            [$default, '::1', '::2', []],
313
314            // From a trusted proxy, but via an untrusted proxy.
315            [$default, '1.2.3.4,11.22.33.44,172.16.0.1', '192.168.1.1', []],
316            [$default, '1.2.3.4,::2,172.16.0.1', '::1', []],
317
318            // A custom configuration.
319            [$custom, '', '127.0.0.1', []],
320            [$custom, '1.2.3.4', '127.0.0.1', ['1.2.3.4', '127.0.0.1']],
321            [$custom, '1.2.3.4', '192.168.1.1', ['1.2.3.4', '192.168.1.1']],
322            [$custom, '1.2.3.4,172.16.0.1', '192.168.1.1', ['1.2.3.4', '172.16.0.1', '192.168.1.1']],
323            [$custom, '1.2.3.4,172.16.0.1', '::1', ['1.2.3.4', '172.16.0.1', '::1']],
324            [$custom, '1.2.3.4,172.16.0.1', '::0001', ['1.2.3.4', '172.16.0.1', '::0001']],
325
326            // Directly from an untrusted proxy.
327            [$custom, '', '127.0.0.1', []],
328            [$custom, '1.2.3.4', '11.22.33.44', []],
329            [$custom, '::1', '11.22.33.44', []],
330            [$custom, '::1', '::2', []],
331
332            // From a trusted proxy, but via an untrusted proxy.
333            [$custom, '1.2.3.4,11.22.33.44,172.16.0.1', '192.168.1.1', []],
334            [$custom, '1.2.3.4,::2,172.16.0.1', '::1', []],
335
336            // Via a custom proxy.
337            [$custom, '11.2.3.4,3.1.2.3,172.16.0.1', '192.168.1.1', ['11.2.3.4', '3.1.2.3', '172.16.0.1', '192.168.1.1']],
338            [$custom, '11.2.3.4,1122::,172.16.0.1', '3.0.0.1', ['11.2.3.4', '1122::', '172.16.0.1', '3.0.0.1']],
339            [$custom, '11.2.3.4,1122::,172.16.0.1', '1111:2222:3333::', ['11.2.3.4', '1122::', '172.16.0.1', '1111:2222:3333::']],
340        ];
341
342        return $tests;
343    }
344
345    /**
346     * Test forwardedFor().
347     *
348     * @dataProvider forwarded_for_provider
349     *
350     * @param string|string[] $config     The trustedproxies config value.
351     * @param string          $header     The X-Forwarded-For header value.
352     * @param string          $remoteAddr The TCP/IP peer address.
353     * @param array           $expected   The expected result from forwardedFor().
354     *
355     * @return void
356     */
357    public function test_forwarded_for($config, string $header, string $remoteAddr, array $expected): void
358    {
359        /* @var Input $INPUT */
360        global $INPUT, $conf;
361
362        $conf['trustedproxies'] = $config;
363        $INPUT->server->set('HTTP_X_FORWARDED_FOR', $header);
364        $INPUT->server->set('REMOTE_ADDR', $remoteAddr);
365
366        $result = Ip::forwardedFor();
367
368        $this->assertSame($expected, $result);
369    }
370
371    /**
372     * Data provider for test_is_ssl().
373     *
374     * @return mixed[][] Returns an array of test cases.
375     */
376    public function is_ssl_provider(): array
377    {
378        // The new default configuration value.
379        $default = ['::1', 'fe80::/10', '127.0.0.0/8', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'];
380
381        $tests = [
382            // Running behind an SSL proxy, HTTP between server and proxy
383            // Proxy (REMOTE_ADDR) is matched by trustedproxies config
384            // HTTPS not set, HTTP_X_FORWARDED_PROTO set to https
385            [$default, '127.0.0.1', '', 'https', true],
386
387            // Running behind an SSL proxy, HTTP between server and proxy
388            // Proxy (REMOTE_ADDR) is not matched by trustedproxies config
389            // HTTPS not set, HTTP_X_FORWARDED_PROTO set to https
390            [[], '8.8.8.8', '', 'https', false],
391
392            // Running behind a plain HTTP proxy, HTTP between server and proxy
393            // HTTPS not set, HTTP_X_FORWARDED_PROTO set to http
394            [$default, '127.0.0.1', '', 'http', false],
395
396            // Running behind an SSL proxy, HTTP between server and proxy
397            // HTTPS set to off, HTTP_X_FORWARDED_PROTO set to https
398            [$default, '127.0.0.1', 'off', 'https', true],
399
400            // Not running behind a proxy, HTTPS server
401            // HTTPS set to on, HTTP_X_FORWARDED_PROTO not set
402            [[], '8.8.8.8', 'on', '', true],
403
404            // Not running behind a proxy, plain HTTP server
405            // HTTPS not set, HTTP_X_FORWARDED_PROTO not set
406            [[], '8.8.8.8', '', '', false],
407
408            // Not running behind a proxy, plain HTTP server
409            // HTTPS set to off, HTTP_X_FORWARDED_PROTO not set
410            [[], '8.8.8.8', 'off', '', false],
411
412            // Running behind an SSL proxy, SSL between proxy and HTTP server
413            // HTTPS set to on, HTTP_X_FORWARDED_PROTO set to https
414            [$default, '127.0.0.1', 'on', 'https', true],
415        ];
416
417        return $tests;
418    }
419
420    /**
421     * Test isSsl().
422     *
423     * @dataProvider is_ssl_provider
424     *
425     * @param string|string[] $config           The trustedproxies config value.
426     * @param string          $remoteAddr       The REMOTE_ADDR value.
427     * @param string          $https            The HTTPS value.
428     * @param string          $forwardedProto   The HTTP_X_FORWARDED_PROTO value.
429     * @param bool            $expected         The expected result from isSsl().
430     *
431     * @return void
432     */
433    public function test_is_ssl($config, string $remoteAddr, string $https, string $forwardedProto, bool $expected): void
434    {
435        /* @var Input $INPUT */
436        global $INPUT, $conf;
437
438        $conf['trustedproxies'] = $config;
439        $INPUT->server->set('REMOTE_ADDR', $remoteAddr);
440        $INPUT->server->set('HTTPS', $https);
441        $INPUT->server->set('HTTP_X_FORWARDED_PROTO', $forwardedProto);
442
443        $result = Ip::isSsl();
444
445        $this->assertSame($expected, $result);
446    }
447
448    /**
449     * Data provider for test_host_name().
450     *
451     * @return mixed[][] Returns an array of test cases.
452     */
453    public function host_name_provider(): array
454    {
455        // The new default configuration value.
456        $default = ['::1', 'fe80::/10', '127.0.0.0/8', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'];
457
458        $tests = [
459            // X-Forwarded-Host with trusted proxy
460            [$default, '127.0.0.1', 'proxy.example.com', 'www.example.com', 'server.local', 'proxy.example.com'],
461
462            // X-Forwarded-Host with untrusted proxy (should fall back to HTTP_HOST)
463            [[], '8.8.8.8', 'proxy.example.com', 'www.example.com', 'server.local', 'www.example.com'],
464
465            // No X-Forwarded-Host, use HTTP_HOST
466            [$default, '127.0.0.1', '', 'www.example.com', 'server.local', 'www.example.com'],
467
468            // No X-Forwarded-Host or HTTP_HOST, use SERVER_NAME
469            [$default, '127.0.0.1', '', '', 'server.local', 'server.local'],
470
471            // No headers set, should fall back to system hostname
472            [$default, '127.0.0.1', '', '', '', php_uname('n')],
473        ];
474
475        return $tests;
476    }
477
478    /**
479     * Test hostName().
480     *
481     * @dataProvider host_name_provider
482     *
483     * @param string|string[] $config           The trustedproxies config value.
484     * @param string          $remoteAddr       The REMOTE_ADDR value.
485     * @param string          $forwardedHost    The HTTP_X_FORWARDED_HOST value.
486     * @param string          $httpHost         The HTTP_HOST value.
487     * @param string          $serverName       The SERVER_NAME value.
488     * @param string          $expected         The expected result from hostName().
489     *
490     * @return void
491     */
492    public function test_host_name($config, string $remoteAddr, string $forwardedHost, string $httpHost, string $serverName, string $expected): void
493    {
494        /* @var Input $INPUT */
495        global $INPUT, $conf;
496
497        $conf['trustedproxies'] = $config;
498        $INPUT->server->set('REMOTE_ADDR', $remoteAddr);
499        $INPUT->server->set('HTTP_X_FORWARDED_HOST', $forwardedHost);
500        $INPUT->server->set('HTTP_HOST', $httpHost);
501        $INPUT->server->set('SERVER_NAME', $serverName);
502
503        $result = Ip::hostName();
504
505        $this->assertSame($expected, $result);
506    }
507
508    /**
509     * Test custom client IP header configuration.
510     */
511    public function client_ip_provider(): array
512    {
513        return [
514            // client_ip_header disabled, X-Real-IP present -> use REMOTE_ADDR
515            [null, ['HTTP_X_REAL_IP' => '5.6.7.8', 'REMOTE_ADDR' => '1.2.3.4'], '1.2.3.4'],
516
517            // client_ip_header set to X_REAL_IP, X-Real-IP present -> use X-Real-IP
518            ['X_REAL_IP', ['HTTP_X_REAL_IP' => '5.6.7.8', 'REMOTE_ADDR' => '1.2.3.4'], '5.6.7.8'],
519
520            // custom client_ip_header set to CF_CONNECTING_IP -> use CF header
521            ['CF_CONNECTING_IP', ['HTTP_CF_CONNECTING_IP' => '5.6.7.8', 'REMOTE_ADDR' => '1.2.3.4'], '5.6.7.8'],
522
523            // client_ip_header set to X_REAL_IP but only CF header present -> fallback to REMOTE_ADDR
524            ['X_REAL_IP', ['HTTP_CF_CONNECTING_IP' => '5.6.7.8', 'REMOTE_ADDR' => '1.2.3.4'], '1.2.3.4'],
525        ];
526    }
527
528    /**
529     * @dataProvider client_ip_provider
530     */
531    public function test_client_ip($client_ip_header, array $server, string $expected): void
532    {
533        /* @var Input $INPUT */
534        global $INPUT, $conf;
535
536        if ($client_ip_header !== null) {
537            $conf['client_ip_header'] = $client_ip_header;
538        } else {
539            unset($conf['client_ip_header']);
540        }
541
542        // Set provided header variables
543        foreach ($server as $key => $value) {
544            $INPUT->server->set($key, $value);
545        }
546
547        $result = Ip::clientIp();
548
549        $this->assertSame($expected, $result);
550    }
551}
552