MediaWiki
REL1_23
|
00001 <?php 00024 // Some regex definition to "play" with IP address and IP address blocks 00025 00026 // An IPv4 address is made of 4 bytes from x00 to xFF which is d0 to d255 00027 define( 'RE_IP_BYTE', '(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|0?[0-9]?[0-9])' ); 00028 define( 'RE_IP_ADD', RE_IP_BYTE . '\.' . RE_IP_BYTE . '\.' . RE_IP_BYTE . '\.' . RE_IP_BYTE ); 00029 // An IPv4 block is an IP address and a prefix (d1 to d32) 00030 define( 'RE_IP_PREFIX', '(3[0-2]|[12]?\d)' ); 00031 define( 'RE_IP_BLOCK', RE_IP_ADD . '\/' . RE_IP_PREFIX ); 00032 00033 // An IPv6 address is made up of 8 words (each x0000 to xFFFF). 00034 // However, the "::" abbreviation can be used on consecutive x0000 words. 00035 define( 'RE_IPV6_WORD', '([0-9A-Fa-f]{1,4})' ); 00036 define( 'RE_IPV6_PREFIX', '(12[0-8]|1[01][0-9]|[1-9]?\d)' ); 00037 define( 'RE_IPV6_ADD', 00038 '(?:' . // starts with "::" (including "::") 00039 ':(?::|(?::' . RE_IPV6_WORD . '){1,7})' . 00040 '|' . // ends with "::" (except "::") 00041 RE_IPV6_WORD . '(?::' . RE_IPV6_WORD . '){0,6}::' . 00042 '|' . // contains one "::" in the middle (the ^ makes the test fail if none found) 00043 RE_IPV6_WORD . '(?::((?(-1)|:))?' . RE_IPV6_WORD . '){1,6}(?(-2)|^)' . 00044 '|' . // contains no "::" 00045 RE_IPV6_WORD . '(?::' . RE_IPV6_WORD . '){7}' . 00046 ')' 00047 ); 00048 // An IPv6 block is an IP address and a prefix (d1 to d128) 00049 define( 'RE_IPV6_BLOCK', RE_IPV6_ADD . '\/' . RE_IPV6_PREFIX ); 00050 // For IPv6 canonicalization (NOT for strict validation; these are quite lax!) 00051 define( 'RE_IPV6_GAP', ':(?:0+:)*(?::(?:0+:)*)?' ); 00052 define( 'RE_IPV6_V4_PREFIX', '0*' . RE_IPV6_GAP . '(?:ffff:)?' ); 00053 00054 // This might be useful for regexps used elsewhere, matches any IPv6 or IPv6 address or network 00055 define( 'IP_ADDRESS_STRING', 00056 '(?:' . 00057 RE_IP_ADD . '(?:\/' . RE_IP_PREFIX . ')?' . // IPv4 00058 '|' . 00059 RE_IPV6_ADD . '(?:\/' . RE_IPV6_PREFIX . ')?' . // IPv6 00060 ')' 00061 ); 00062 00067 class IP { 00076 public static function isIPAddress( $ip ) { 00077 return (bool)preg_match( '/^' . IP_ADDRESS_STRING . '$/', $ip ); 00078 } 00079 00087 public static function isIPv6( $ip ) { 00088 return (bool)preg_match( '/^' . RE_IPV6_ADD . '(?:\/' . RE_IPV6_PREFIX . ')?$/', $ip ); 00089 } 00090 00098 public static function isIPv4( $ip ) { 00099 return (bool)preg_match( '/^' . RE_IP_ADD . '(?:\/' . RE_IP_PREFIX . ')?$/', $ip ); 00100 } 00101 00110 public static function isValid( $ip ) { 00111 return ( preg_match( '/^' . RE_IP_ADD . '$/', $ip ) 00112 || preg_match( '/^' . RE_IPV6_ADD . '$/', $ip ) ); 00113 } 00114 00123 public static function isValidBlock( $ipblock ) { 00124 return ( preg_match( '/^' . RE_IPV6_BLOCK . '$/', $ipblock ) 00125 || preg_match( '/^' . RE_IP_BLOCK . '$/', $ipblock ) ); 00126 } 00127 00136 public static function sanitizeIP( $ip ) { 00137 $ip = trim( $ip ); 00138 if ( $ip === '' ) { 00139 return null; 00140 } 00141 if ( self::isIPv4( $ip ) || !self::isIPv6( $ip ) ) { 00142 return $ip; // nothing else to do for IPv4 addresses or invalid ones 00143 } 00144 // Remove any whitespaces, convert to upper case 00145 $ip = strtoupper( $ip ); 00146 // Expand zero abbreviations 00147 $abbrevPos = strpos( $ip, '::' ); 00148 if ( $abbrevPos !== false ) { 00149 // We know this is valid IPv6. Find the last index of the 00150 // address before any CIDR number (e.g. "a:b:c::/24"). 00151 $CIDRStart = strpos( $ip, "/" ); 00152 $addressEnd = ( $CIDRStart !== false ) 00153 ? $CIDRStart - 1 00154 : strlen( $ip ) - 1; 00155 // If the '::' is at the beginning... 00156 if ( $abbrevPos == 0 ) { 00157 $repeat = '0:'; 00158 $extra = ( $ip == '::' ) ? '0' : ''; // for the address '::' 00159 $pad = 9; // 7+2 (due to '::') 00160 // If the '::' is at the end... 00161 } elseif ( $abbrevPos == ( $addressEnd - 1 ) ) { 00162 $repeat = ':0'; 00163 $extra = ''; 00164 $pad = 9; // 7+2 (due to '::') 00165 // If the '::' is in the middle... 00166 } else { 00167 $repeat = ':0'; 00168 $extra = ':'; 00169 $pad = 8; // 6+2 (due to '::') 00170 } 00171 $ip = str_replace( '::', 00172 str_repeat( $repeat, $pad - substr_count( $ip, ':' ) ) . $extra, 00173 $ip 00174 ); 00175 } 00176 // Remove leading zeros from each bloc as needed 00177 $ip = preg_replace( '/(^|:)0+(' . RE_IPV6_WORD . ')/', '$1$2', $ip ); 00178 00179 return $ip; 00180 } 00181 00189 public static function prettifyIP( $ip ) { 00190 $ip = self::sanitizeIP( $ip ); // normalize (removes '::') 00191 if ( self::isIPv6( $ip ) ) { 00192 // Split IP into an address and a CIDR 00193 if ( strpos( $ip, '/' ) !== false ) { 00194 list( $ip, $cidr ) = explode( '/', $ip, 2 ); 00195 } else { 00196 list( $ip, $cidr ) = array( $ip, '' ); 00197 } 00198 // Get the largest slice of words with multiple zeros 00199 $offset = 0; 00200 $longest = $longestPos = false; 00201 while ( preg_match( 00202 '!(?:^|:)0(?::0)+(?:$|:)!', $ip, $m, PREG_OFFSET_CAPTURE, $offset 00203 ) ) { 00204 list( $match, $pos ) = $m[0]; // full match 00205 if ( strlen( $match ) > strlen( $longest ) ) { 00206 $longest = $match; 00207 $longestPos = $pos; 00208 } 00209 $offset = ( $pos + strlen( $match ) ); // advance 00210 } 00211 if ( $longest !== false ) { 00212 // Replace this portion of the string with the '::' abbreviation 00213 $ip = substr_replace( $ip, '::', $longestPos, strlen( $longest ) ); 00214 } 00215 // Add any CIDR back on 00216 if ( $cidr !== '' ) { 00217 $ip = "{$ip}/{$cidr}"; 00218 } 00219 // Convert to lower case to make it more readable 00220 $ip = strtolower( $ip ); 00221 } 00222 00223 return $ip; 00224 } 00225 00242 public static function splitHostAndPort( $both ) { 00243 if ( substr( $both, 0, 1 ) === '[' ) { 00244 if ( preg_match( '/^\[(' . RE_IPV6_ADD . ')\](?::(?P<port>\d+))?$/', $both, $m ) ) { 00245 if ( isset( $m['port'] ) ) { 00246 return array( $m[1], intval( $m['port'] ) ); 00247 } else { 00248 return array( $m[1], false ); 00249 } 00250 } else { 00251 // Square bracket found but no IPv6 00252 return false; 00253 } 00254 } 00255 $numColons = substr_count( $both, ':' ); 00256 if ( $numColons >= 2 ) { 00257 // Is it a bare IPv6 address? 00258 if ( preg_match( '/^' . RE_IPV6_ADD . '$/', $both ) ) { 00259 return array( $both, false ); 00260 } else { 00261 // Not valid IPv6, but too many colons for anything else 00262 return false; 00263 } 00264 } 00265 if ( $numColons >= 1 ) { 00266 // Host:port? 00267 $bits = explode( ':', $both ); 00268 if ( preg_match( '/^\d+/', $bits[1] ) ) { 00269 return array( $bits[0], intval( $bits[1] ) ); 00270 } else { 00271 // Not a valid port 00272 return false; 00273 } 00274 } 00275 00276 // Plain hostname 00277 return array( $both, false ); 00278 } 00279 00291 public static function combineHostAndPort( $host, $port, $defaultPort = false ) { 00292 if ( strpos( $host, ':' ) !== false ) { 00293 $host = "[$host]"; 00294 } 00295 if ( $defaultPort !== false && $port == $defaultPort ) { 00296 return $host; 00297 } else { 00298 return "$host:$port"; 00299 } 00300 } 00301 00308 public static function toOctet( $ip_int ) { 00309 return self::hexToOctet( wfBaseConvert( $ip_int, 10, 16, 32, false ) ); 00310 } 00311 00318 public static function formatHex( $hex ) { 00319 if ( substr( $hex, 0, 3 ) == 'v6-' ) { // IPv6 00320 return self::hexToOctet( substr( $hex, 3 ) ); 00321 } else { // IPv4 00322 return self::hexToQuad( $hex ); 00323 } 00324 } 00325 00332 public static function hexToOctet( $ip_hex ) { 00333 // Pad hex to 32 chars (128 bits) 00334 $ip_hex = str_pad( strtoupper( $ip_hex ), 32, '0', STR_PAD_LEFT ); 00335 // Separate into 8 words 00336 $ip_oct = substr( $ip_hex, 0, 4 ); 00337 for ( $n = 1; $n < 8; $n++ ) { 00338 $ip_oct .= ':' . substr( $ip_hex, 4 * $n, 4 ); 00339 } 00340 // NO leading zeroes 00341 $ip_oct = preg_replace( '/(^|:)0+(' . RE_IPV6_WORD . ')/', '$1$2', $ip_oct ); 00342 00343 return $ip_oct; 00344 } 00345 00352 public static function hexToQuad( $ip_hex ) { 00353 // Pad hex to 8 chars (32 bits) 00354 $ip_hex = str_pad( strtoupper( $ip_hex ), 8, '0', STR_PAD_LEFT ); 00355 // Separate into four quads 00356 $s = ''; 00357 for ( $i = 0; $i < 4; $i++ ) { 00358 if ( $s !== '' ) { 00359 $s .= '.'; 00360 } 00361 $s .= base_convert( substr( $ip_hex, $i * 2, 2 ), 16, 10 ); 00362 } 00363 00364 return $s; 00365 } 00366 00374 public static function isPublic( $ip ) { 00375 if ( self::isIPv6( $ip ) ) { 00376 return self::isPublic6( $ip ); 00377 } 00378 $n = self::toUnsigned( $ip ); 00379 if ( !$n ) { 00380 return false; 00381 } 00382 00383 // ip2long accepts incomplete addresses, as well as some addresses 00384 // followed by garbage characters. Check that it's really valid. 00385 if ( $ip != long2ip( $n ) ) { 00386 return false; 00387 } 00388 00389 static $privateRanges = false; 00390 if ( !$privateRanges ) { 00391 $privateRanges = array( 00392 array( '10.0.0.0', '10.255.255.255' ), # RFC 1918 (private) 00393 array( '172.16.0.0', '172.31.255.255' ), # RFC 1918 (private) 00394 array( '192.168.0.0', '192.168.255.255' ), # RFC 1918 (private) 00395 array( '0.0.0.0', '0.255.255.255' ), # this network 00396 array( '127.0.0.0', '127.255.255.255' ), # loopback 00397 ); 00398 } 00399 00400 foreach ( $privateRanges as $r ) { 00401 $start = self::toUnsigned( $r[0] ); 00402 $end = self::toUnsigned( $r[1] ); 00403 if ( $n >= $start && $n <= $end ) { 00404 return false; 00405 } 00406 } 00407 00408 return true; 00409 } 00410 00418 private static function isPublic6( $ip ) { 00419 static $privateRanges = false; 00420 if ( !$privateRanges ) { 00421 $privateRanges = array( 00422 array( 'fc00::', 'fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' ), # RFC 4193 (local) 00423 array( '0:0:0:0:0:0:0:1', '0:0:0:0:0:0:0:1' ), # loopback 00424 ); 00425 } 00426 $n = self::toHex( $ip ); 00427 foreach ( $privateRanges as $r ) { 00428 $start = self::toHex( $r[0] ); 00429 $end = self::toHex( $r[1] ); 00430 if ( $n >= $start && $n <= $end ) { 00431 return false; 00432 } 00433 } 00434 00435 return true; 00436 } 00437 00449 public static function toHex( $ip ) { 00450 if ( self::isIPv6( $ip ) ) { 00451 $n = 'v6-' . self::IPv6ToRawHex( $ip ); 00452 } else { 00453 $n = self::toUnsigned( $ip ); 00454 if ( $n !== false ) { 00455 $n = wfBaseConvert( $n, 10, 16, 8, false ); 00456 } 00457 } 00458 00459 return $n; 00460 } 00461 00468 private static function IPv6ToRawHex( $ip ) { 00469 $ip = self::sanitizeIP( $ip ); 00470 if ( !$ip ) { 00471 return null; 00472 } 00473 $r_ip = ''; 00474 foreach ( explode( ':', $ip ) as $v ) { 00475 $r_ip .= str_pad( $v, 4, 0, STR_PAD_LEFT ); 00476 } 00477 00478 return $r_ip; 00479 } 00480 00488 public static function toUnsigned( $ip ) { 00489 if ( self::isIPv6( $ip ) ) { 00490 $n = self::toUnsigned6( $ip ); 00491 } else { 00492 // Bug 60035: an IP with leading 0's fails in ip2long sometimes (e.g. *.08) 00493 $ip = preg_replace( '/(?<=\.)0+(?=[1-9])/', '', $ip ); 00494 $n = ip2long( $ip ); 00495 if ( $n < 0 ) { 00496 $n += pow( 2, 32 ); 00497 # On 32-bit platforms (and on Windows), 2^32 does not fit into an int, 00498 # so $n becomes a float. We convert it to string instead. 00499 if ( is_float( $n ) ) { 00500 $n = (string)$n; 00501 } 00502 } 00503 } 00504 00505 return $n; 00506 } 00507 00512 private static function toUnsigned6( $ip ) { 00513 return wfBaseConvert( self::IPv6ToRawHex( $ip ), 16, 10 ); 00514 } 00515 00523 public static function parseCIDR( $range ) { 00524 if ( self::isIPv6( $range ) ) { 00525 return self::parseCIDR6( $range ); 00526 } 00527 $parts = explode( '/', $range, 2 ); 00528 if ( count( $parts ) != 2 ) { 00529 return array( false, false ); 00530 } 00531 list( $network, $bits ) = $parts; 00532 $network = ip2long( $network ); 00533 if ( $network !== false && is_numeric( $bits ) && $bits >= 0 && $bits <= 32 ) { 00534 if ( $bits == 0 ) { 00535 $network = 0; 00536 } else { 00537 $network &= ~( ( 1 << ( 32 - $bits ) ) - 1 ); 00538 } 00539 # Convert to unsigned 00540 if ( $network < 0 ) { 00541 $network += pow( 2, 32 ); 00542 } 00543 } else { 00544 $network = false; 00545 $bits = false; 00546 } 00547 00548 return array( $network, $bits ); 00549 } 00550 00566 public static function parseRange( $range ) { 00567 // CIDR notation 00568 if ( strpos( $range, '/' ) !== false ) { 00569 if ( self::isIPv6( $range ) ) { 00570 return self::parseRange6( $range ); 00571 } 00572 list( $network, $bits ) = self::parseCIDR( $range ); 00573 if ( $network === false ) { 00574 $start = $end = false; 00575 } else { 00576 $start = sprintf( '%08X', $network ); 00577 $end = sprintf( '%08X', $network + pow( 2, ( 32 - $bits ) ) - 1 ); 00578 } 00579 // Explicit range 00580 } elseif ( strpos( $range, '-' ) !== false ) { 00581 list( $start, $end ) = array_map( 'trim', explode( '-', $range, 2 ) ); 00582 if ( self::isIPv6( $start ) && self::isIPv6( $end ) ) { 00583 return self::parseRange6( $range ); 00584 } 00585 if ( self::isIPv4( $start ) && self::isIPv4( $end ) ) { 00586 $start = self::toUnsigned( $start ); 00587 $end = self::toUnsigned( $end ); 00588 if ( $start > $end ) { 00589 $start = $end = false; 00590 } else { 00591 $start = sprintf( '%08X', $start ); 00592 $end = sprintf( '%08X', $end ); 00593 } 00594 } else { 00595 $start = $end = false; 00596 } 00597 } else { 00598 # Single IP 00599 $start = $end = self::toHex( $range ); 00600 } 00601 if ( $start === false || $end === false ) { 00602 return array( false, false ); 00603 } else { 00604 return array( $start, $end ); 00605 } 00606 } 00607 00616 private static function parseCIDR6( $range ) { 00617 # Explode into <expanded IP,range> 00618 $parts = explode( '/', IP::sanitizeIP( $range ), 2 ); 00619 if ( count( $parts ) != 2 ) { 00620 return array( false, false ); 00621 } 00622 list( $network, $bits ) = $parts; 00623 $network = self::IPv6ToRawHex( $network ); 00624 if ( $network !== false && is_numeric( $bits ) && $bits >= 0 && $bits <= 128 ) { 00625 if ( $bits == 0 ) { 00626 $network = "0"; 00627 } else { 00628 # Native 32 bit functions WONT work here!!! 00629 # Convert to a padded binary number 00630 $network = wfBaseConvert( $network, 16, 2, 128 ); 00631 # Truncate the last (128-$bits) bits and replace them with zeros 00632 $network = str_pad( substr( $network, 0, $bits ), 128, 0, STR_PAD_RIGHT ); 00633 # Convert back to an integer 00634 $network = wfBaseConvert( $network, 2, 10 ); 00635 } 00636 } else { 00637 $network = false; 00638 $bits = false; 00639 } 00640 00641 return array( $network, (int)$bits ); 00642 } 00643 00657 private static function parseRange6( $range ) { 00658 # Expand any IPv6 IP 00659 $range = IP::sanitizeIP( $range ); 00660 // CIDR notation... 00661 if ( strpos( $range, '/' ) !== false ) { 00662 list( $network, $bits ) = self::parseCIDR6( $range ); 00663 if ( $network === false ) { 00664 $start = $end = false; 00665 } else { 00666 $start = wfBaseConvert( $network, 10, 16, 32, false ); 00667 # Turn network to binary (again) 00668 $end = wfBaseConvert( $network, 10, 2, 128 ); 00669 # Truncate the last (128-$bits) bits and replace them with ones 00670 $end = str_pad( substr( $end, 0, $bits ), 128, 1, STR_PAD_RIGHT ); 00671 # Convert to hex 00672 $end = wfBaseConvert( $end, 2, 16, 32, false ); 00673 # see toHex() comment 00674 $start = "v6-$start"; 00675 $end = "v6-$end"; 00676 } 00677 // Explicit range notation... 00678 } elseif ( strpos( $range, '-' ) !== false ) { 00679 list( $start, $end ) = array_map( 'trim', explode( '-', $range, 2 ) ); 00680 $start = self::toUnsigned6( $start ); 00681 $end = self::toUnsigned6( $end ); 00682 if ( $start > $end ) { 00683 $start = $end = false; 00684 } else { 00685 $start = wfBaseConvert( $start, 10, 16, 32, false ); 00686 $end = wfBaseConvert( $end, 10, 16, 32, false ); 00687 } 00688 # see toHex() comment 00689 $start = "v6-$start"; 00690 $end = "v6-$end"; 00691 } else { 00692 # Single IP 00693 $start = $end = self::toHex( $range ); 00694 } 00695 if ( $start === false || $end === false ) { 00696 return array( false, false ); 00697 } else { 00698 return array( $start, $end ); 00699 } 00700 } 00701 00709 public static function isInRange( $addr, $range ) { 00710 $hexIP = self::toHex( $addr ); 00711 list( $start, $end ) = self::parseRange( $range ); 00712 00713 return ( strcmp( $hexIP, $start ) >= 0 && 00714 strcmp( $hexIP, $end ) <= 0 ); 00715 } 00716 00727 public static function canonicalize( $addr ) { 00728 // remove zone info (bug 35738) 00729 $addr = preg_replace( '/\%.*/', '', $addr ); 00730 00731 if ( self::isValid( $addr ) ) { 00732 return $addr; 00733 } 00734 // Turn mapped addresses from ::ce:ffff:1.2.3.4 to 1.2.3.4 00735 if ( strpos( $addr, ':' ) !== false && strpos( $addr, '.' ) !== false ) { 00736 $addr = substr( $addr, strrpos( $addr, ':' ) + 1 ); 00737 if ( self::isIPv4( $addr ) ) { 00738 return $addr; 00739 } 00740 } 00741 // IPv6 loopback address 00742 $m = array(); 00743 if ( preg_match( '/^0*' . RE_IPV6_GAP . '1$/', $addr, $m ) ) { 00744 return '127.0.0.1'; 00745 } 00746 // IPv4-mapped and IPv4-compatible IPv6 addresses 00747 if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . '(' . RE_IP_ADD . ')$/i', $addr, $m ) ) { 00748 return $m[1]; 00749 } 00750 if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . RE_IPV6_WORD . 00751 ':' . RE_IPV6_WORD . '$/i', $addr, $m ) 00752 ) { 00753 return long2ip( ( hexdec( $m[1] ) << 16 ) + hexdec( $m[2] ) ); 00754 } 00755 00756 return null; // give up 00757 } 00758 00765 public static function sanitizeRange( $range ) { 00766 list( /*...*/, $bits ) = self::parseCIDR( $range ); 00767 list( $start, /*...*/ ) = self::parseRange( $range ); 00768 $start = self::formatHex( $start ); 00769 if ( $bits === false ) { 00770 return $start; // wasn't actually a range 00771 } 00772 00773 return "$start/$bits"; 00774 } 00775 }