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