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            ['193.53.125.7', 4, 0x00000000, 0xC1357D07],
23        ];
24
25        return $tests;
26    }
27
28    /**
29     * Test ipToNumber().
30     *
31     * @dataProvider ip_to_number_provider
32     *
33     * @param string $ip The IP address to convert.
34     * @param int    $version The IP version, either 4 or 6.
35     * @param int    $upper   The upper 64 bits of the IP.
36     * @param int    $lower   The lower 64 bits of the IP.
37     *
38     * @return void
39     */
40    public function test_ip_to_number(string $ip, int $version, int $upper, int $lower): void
41    {
42        $result = Ip::ipToNumber($ip);
43
44        $this->assertSame($version, $result['version']);
45        $this->assertSame($upper, $result['upper']);
46        $this->assertSame($lower, $result['lower']);
47    }
48
49    /**
50     * The data provider for test_ip_in_range().
51     *
52     * @return mixed[][] Returns an array of test cases.
53     */
54    public function ip_in_range_provider(): array
55    {
56        $tests = [
57            ['192.168.11.2', '192.168.0.0/16', true],
58            ['192.168.11.2', '192.168.64.1/16', true],
59            ['192.168.11.2', '192.168.64.1/18', false],
60            ['192.168.11.2', '192.168.11.0/20', true],
61            ['127.0.0.1', '127.0.0.0/7', true],
62            ['127.0.0.1', '127.0.0.0/8', true],
63            ['127.0.0.1', '127.200.0.0/8', true],
64            ['127.0.0.1', '127.200.0.0/9', false],
65            ['127.0.0.1', '127.0.0.0/31', true],
66            ['127.0.0.1', '127.0.0.0/32', false],
67            ['127.0.0.1', '127.0.0.1/32', true],
68            ['1111:2222:3333:4444:5555:6666:7777:8888', '1110::/12', true],
69            ['1110:2222:3333:4444:5555:6666:7777:8888', '1110::/12', true],
70            ['1100:2222:3333:4444:5555:6666:7777:8888', '1110::/12', false],
71            ['1111:2222:3333:4444:5555:6666:7777:8888', '1111:2222:3300::/40', true],
72            ['1111:2222:3333:4444:5555:6666:7777:8888', '1111:2222:3200::/40', false],
73            ['1111:2222:3333:4444:5555:6666:7777:8888', '1111:2222:3333:4444:5555:6666:7777:8889/127', true],
74            ['1111:2222:3333:4444:5555:6666:7777:8888', '1111:2222:3333:4444:5555:6666:7777:8889/128', false],
75            ['1111:2222:3333:4444:5555:6666:7777:8889', '1111:2222:3333:4444:5555:6666:7777:8889/128', true],
76            ['abcd:ef0a:bcde:f0ab:cdef:0abc:def0:abcd', 'abcd:ef0a:bcde:f0ab:cdef:0abc:def0:abcd/128', true],
77            ['abcd:ef0a:bcde:f0ab:cdef:0abc:def0:abce', 'abcd:ef0a:bcde:f0ab:cdef:0abc:def0:abcd/128', false],
78        ];
79
80        return $tests;
81    }
82
83    /**
84     * Test ipInRange().
85     *
86     * @dataProvider ip_in_range_provider
87     *
88     * @param string $ip The IP to test.
89     * @param string $range The IP range to test against.
90     * @param bool $expected The expected result from ipInRange().
91     *
92     * @return void
93     */
94    public function test_ip_in_range(string $ip, string $range, bool $expected): void
95    {
96        $result = Ip::ipInRange($ip, $range);
97
98        $this->assertSame($expected, $result);
99    }
100
101    /**
102     * Data provider for test_ip_matches().
103     *
104     * @return mixed[][] Returns an array of test cases.
105     */
106    public function ip_matches_provider(): array
107    {
108        // Tests for a CIDR range.
109        $rangeTests = $this->ip_in_range_provider();
110
111        // Tests for an exact IP match.
112        $exactTests = [
113            ['127.0.0.1', '127.0.0.1', true],
114            ['127.0.0.1', '127.0.0.0', false],
115            ['aaaa:bbbb:cccc:dddd:eeee::', 'aaaa:bbbb:cccc:dddd:eeee:0000:0000:0000', true],
116            ['aaaa:bbbb:cccc:dddd:eeee:0000:0000:0000', 'aaaa:bbbb:cccc:dddd:eeee::', true],
117            ['aaaa:bbbb:0000:0000:0000:0000:0000:0001', 'aaaa:bbbb::1', true],
118            ['aaaa:bbbb::0001', 'aaaa:bbbb::1', true],
119            ['aaaa:bbbb::0001', 'aaaa:bbbb::', false],
120            ['::ffff:127.0.0.1', '127.0.0.1', false],
121            ['::ffff:127.0.0.1', '::0:ffff:127.0.0.1', true],
122        ];
123
124
125        return array_merge($rangeTests, $exactTests);
126    }
127
128    /**
129     * Test ipMatches().
130     *
131     * @dataProvider ip_matches_provider
132     *
133     * @param string $ip        The IP to test.
134     * @param string $ipOrRange The IP or IP range to test against.
135     * @param bool   $expected  The expeced result from ipMatches().
136     *
137     * @return void
138     */
139    public function test_ip_matches(string $ip, string $ipOrRange, bool $expected): void
140    {
141        $result = Ip::ipMatches($ip, $ipOrRange);
142
143        $this->assertSame($expected, $result);
144    }
145
146    /**
147     * Data provider for proxyIsTrusted().
148     *
149     * @return mixed[][] Returns an array of test cases.
150     */
151    public function proxy_is_trusted_provider(): array
152    {
153        // The new default configuration value.
154        $default = ['::1', 'fe80::/10', '127.0.0.0/8', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'];
155
156        // Adding some custom trusted proxies.
157        $custom = array_merge($default, ['1.2.3.4', '1122::', '3.0.0.1/8', '1111:2222::/32']);
158
159        $tests = [
160            // Empty configuration.
161            ['', '127.0.0.1', false],
162
163            // Configuration with an array of  IPs/CIDRs.
164            [$default, '127.0.0.1', true],
165            [$default, '127.1.2.3', true],
166            [$default, '10.1.2.3', true],
167            [$default, '11.1.2.3', false],
168            [$default, '172.16.0.1', true],
169            [$default, '172.160.0.1', false],
170            [$default, '172.31.255.255', true],
171            [$default, '172.32.0.0', false],
172            [$default, '172.200.0.0', false],
173            [$default, '192.168.2.3', true],
174            [$default, '192.169.1.2', false],
175            [$default, '::1', true],
176            [$default, '0000:0000:0000:0000:0000:0000:0000:0001', true],
177
178            // With custom proxies set.
179            [$custom, '127.0.0.1', true],
180            [$custom, '1.2.3.4', true],
181            [$custom, '3.0.1.2', true],
182            [$custom, '1122::', true],
183            [$custom, '1122:0000:0000:0000:0000:0000:0000:0000', true],
184            [$custom, '1111:2223::', false],
185            [$custom, '1111:2222::', true],
186            [$custom, '1111:2222:3333::', true],
187            [$custom, '1111:2222:3333::1', true],
188        ];
189
190        return $tests;
191    }
192
193    /**
194     * Test proxyIsTrusted().
195     *
196     * @dataProvider proxy_is_trusted_provider
197     *
198     * @param string|string[] $config   The value for $conf[trustedproxies].
199     * @param string          $ip       The proxy IP to test.
200     * @param bool            $expected The expected result from proxyIsTrusted().
201     */
202    public function test_proxy_is_trusted($config, string $ip, bool $expected): void
203    {
204        global $conf;
205        $conf['trustedproxies'] = $config;
206
207        $result = Ip::proxyIsTrusted($ip);
208
209        $this->assertSame($expected, $result);
210    }
211
212    /**
213     * Data provider for test_forwarded_for().
214     *
215     * @return mixed[][] Returns an array of test cases.
216     */
217    public function forwarded_for_provider(): array
218    {
219        // The new default configuration value.
220        $default = ['::1', 'fe80::/10', '127.0.0.0/8', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'];
221
222        // Adding some custom trusted proxies.
223        $custom = array_merge($default, ['1.2.3.4', '1122::', '3.0.0.1/8', '1111:2222::/32']);
224
225        $tests = [
226            // Empty config value should always return empty array.
227            [[], '', '127.0.0.1', []],
228            [[], '127.0.0.1', '127.0.0.1', []],
229
230            // The new default configuration.
231            [$default, '', '127.0.0.1', []],
232            [$default, '1.2.3.4', '127.0.0.1', ['1.2.3.4', '127.0.0.1']],
233            [$default, '1.2.3.4', '192.168.1.1', ['1.2.3.4', '192.168.1.1']],
234            [$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']],
235            [$default, '1.2.3.4,172.16.0.1', '::1', ['1.2.3.4', '172.16.0.1', '::1']],
236            [$default, '1.2.3.4,172.16.0.1', '::0001', ['1.2.3.4', '172.16.0.1', '::0001']],
237
238            // Directly from an untrusted proxy.
239            [$default, '', '127.0.0.1', []],
240            [$default, '1.2.3.4', '11.22.33.44', []],
241            [$default, '::1', '11.22.33.44', []],
242            [$default, '::1', '::2', []],
243
244            // From a trusted proxy, but via an untrusted proxy.
245            [$default, '1.2.3.4,11.22.33.44,172.16.0.1', '192.168.1.1', []],
246            [$default, '1.2.3.4,::2,172.16.0.1', '::1', []],
247
248            // A custom configuration.
249            [$custom, '', '127.0.0.1', []],
250            [$custom, '1.2.3.4', '127.0.0.1', ['1.2.3.4', '127.0.0.1']],
251            [$custom, '1.2.3.4', '192.168.1.1', ['1.2.3.4', '192.168.1.1']],
252            [$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']],
253            [$custom, '1.2.3.4,172.16.0.1', '::1', ['1.2.3.4', '172.16.0.1', '::1']],
254            [$custom, '1.2.3.4,172.16.0.1', '::0001', ['1.2.3.4', '172.16.0.1', '::0001']],
255
256            // Directly from an untrusted proxy.
257            [$custom, '', '127.0.0.1', []],
258            [$custom, '1.2.3.4', '11.22.33.44', []],
259            [$custom, '::1', '11.22.33.44', []],
260            [$custom, '::1', '::2', []],
261
262            // From a trusted proxy, but via an untrusted proxy.
263            [$custom, '1.2.3.4,11.22.33.44,172.16.0.1', '192.168.1.1', []],
264            [$custom, '1.2.3.4,::2,172.16.0.1', '::1', []],
265
266            // Via a custom proxy.
267            [$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']],
268            [$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']],
269            [$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::']],
270        ];
271
272        return $tests;
273    }
274
275    /**
276     * Test forwardedFor().
277     *
278     * @dataProvider forwarded_for_provider
279     *
280     * @param string|string[] $config     The trustedproxies config value.
281     * @param string          $header     The X-Forwarded-For header value.
282     * @param string          $remoteAddr The TCP/IP peer address.
283     * @param array           $expected   The expected result from forwardedFor().
284     *
285     * @return void
286     */
287    public function test_forwarded_for($config, string $header, string $remoteAddr, array $expected): void
288    {
289        /* @var Input $INPUT */
290        global $INPUT, $conf;
291
292        $conf['trustedproxies'] = $config;
293        $INPUT->server->set('HTTP_X_FORWARDED_FOR', $header);
294        $INPUT->server->set('REMOTE_ADDR', $remoteAddr);
295
296        $result = Ip::forwardedFor();
297
298        $this->assertSame($expected, $result);
299    }
300
301    /**
302     * Data provider for test_is_ssl().
303     *
304     * @return mixed[][] Returns an array of test cases.
305     */
306    public function is_ssl_provider(): array
307    {
308        // The new default configuration value.
309        $default = ['::1', 'fe80::/10', '127.0.0.0/8', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'];
310
311        $tests = [
312            // Running behind an SSL proxy, HTTP between server and proxy
313            // Proxy (REMOTE_ADDR) is matched by trustedproxies config
314            // HTTPS not set, HTTP_X_FORWARDED_PROTO set to https
315            [$default, '127.0.0.1', '', 'https', true],
316
317            // Running behind an SSL proxy, HTTP between server and proxy
318            // Proxy (REMOTE_ADDR) is not matched by trustedproxies config
319            // HTTPS not set, HTTP_X_FORWARDED_PROTO set to https
320            [[], '8.8.8.8', '', 'https', false],
321
322            // Running behind a plain HTTP proxy, HTTP between server and proxy
323            // HTTPS not set, HTTP_X_FORWARDED_PROTO set to http
324            [$default, '127.0.0.1', '', 'http', false],
325
326            // Running behind an SSL proxy, HTTP between server and proxy
327            // HTTPS set to off, HTTP_X_FORWARDED_PROTO set to https
328            [$default, '127.0.0.1', 'off', 'https', true],
329
330            // Not running behind a proxy, HTTPS server
331            // HTTPS set to on, HTTP_X_FORWARDED_PROTO not set
332            [[], '8.8.8.8', 'on', '', true],
333
334            // Not running behind a proxy, plain HTTP server
335            // HTTPS not set, HTTP_X_FORWARDED_PROTO not set
336            [[], '8.8.8.8', '', '', false],
337
338            // Not running behind a proxy, plain HTTP server
339            // HTTPS set to off, HTTP_X_FORWARDED_PROTO not set
340            [[], '8.8.8.8', 'off', '', false],
341
342            // Running behind an SSL proxy, SSL between proxy and HTTP server
343            // HTTPS set to on, HTTP_X_FORWARDED_PROTO set to https
344            [$default, '127.0.0.1', 'on', 'https', true],
345        ];
346
347        return $tests;
348    }
349
350    /**
351     * Test isSsl().
352     *
353     * @dataProvider is_ssl_provider
354     *
355     * @param string|string[] $config           The trustedproxies config value.
356     * @param string          $remoteAddr       The REMOTE_ADDR value.
357     * @param string          $https            The HTTPS value.
358     * @param string          $forwardedProto   The HTTP_X_FORWARDED_PROTO value.
359     * @param bool            $expected         The expected result from isSsl().
360     *
361     * @return void
362     */
363    public function test_is_ssl($config, string $remoteAddr, string $https, string $forwardedProto, bool $expected): void
364    {
365        /* @var Input $INPUT */
366        global $INPUT, $conf;
367
368        $conf['trustedproxies'] = $config;
369        $INPUT->server->set('REMOTE_ADDR', $remoteAddr);
370        $INPUT->server->set('HTTPS', $https);
371        $INPUT->server->set('HTTP_X_FORWARDED_PROTO', $forwardedProto);
372
373        $result = Ip::isSsl();
374
375        $this->assertSame($expected, $result);
376    }
377
378    /**
379     * Data provider for test_host_name().
380     *
381     * @return mixed[][] Returns an array of test cases.
382     */
383    public function host_name_provider(): array
384    {
385        // The new default configuration value.
386        $default = ['::1', 'fe80::/10', '127.0.0.0/8', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'];
387
388        $tests = [
389            // X-Forwarded-Host with trusted proxy
390            [$default, '127.0.0.1', 'proxy.example.com', 'www.example.com', 'server.local', 'proxy.example.com'],
391
392            // X-Forwarded-Host with untrusted proxy (should fall back to HTTP_HOST)
393            [[], '8.8.8.8', 'proxy.example.com', 'www.example.com', 'server.local', 'www.example.com'],
394
395            // No X-Forwarded-Host, use HTTP_HOST
396            [$default, '127.0.0.1', '', 'www.example.com', 'server.local', 'www.example.com'],
397
398            // No X-Forwarded-Host or HTTP_HOST, use SERVER_NAME
399            [$default, '127.0.0.1', '', '', 'server.local', 'server.local'],
400
401            // No headers set, should fall back to system hostname
402            [$default, '127.0.0.1', '', '', '', php_uname('n')],
403        ];
404
405        return $tests;
406    }
407
408    /**
409     * Test hostName().
410     *
411     * @dataProvider host_name_provider
412     *
413     * @param string|string[] $config           The trustedproxies config value.
414     * @param string          $remoteAddr       The REMOTE_ADDR value.
415     * @param string          $forwardedHost    The HTTP_X_FORWARDED_HOST value.
416     * @param string          $httpHost         The HTTP_HOST value.
417     * @param string          $serverName       The SERVER_NAME value.
418     * @param string          $expected         The expected result from hostName().
419     *
420     * @return void
421     */
422    public function test_host_name($config, string $remoteAddr, string $forwardedHost, string $httpHost, string $serverName, string $expected): void
423    {
424        /* @var Input $INPUT */
425        global $INPUT, $conf;
426
427        $conf['trustedproxies'] = $config;
428        $INPUT->server->set('REMOTE_ADDR', $remoteAddr);
429        $INPUT->server->set('HTTP_X_FORWARDED_HOST', $forwardedHost);
430        $INPUT->server->set('HTTP_HOST', $httpHost);
431        $INPUT->server->set('SERVER_NAME', $serverName);
432
433        $result = Ip::hostName();
434
435        $this->assertSame($expected, $result);
436    }
437}
438