MediaWiki
REL1_22
|
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 return $ip; 00179 } 00180 00188 public static function prettifyIP( $ip ) { 00189 $ip = self::sanitizeIP( $ip ); // normalize (removes '::') 00190 if ( self::isIPv6( $ip ) ) { 00191 // Split IP into an address and a CIDR 00192 if ( strpos( $ip, '/' ) !== false ) { 00193 list( $ip, $cidr ) = explode( '/', $ip, 2 ); 00194 } else { 00195 list( $ip, $cidr ) = array( $ip, '' ); 00196 } 00197 // Get the largest slice of words with multiple zeros 00198 $offset = 0; 00199 $longest = $longestPos = false; 00200 while ( preg_match( 00201 '!(?:^|:)0(?::0)+(?:$|:)!', $ip, $m, PREG_OFFSET_CAPTURE, $offset 00202 ) ) { 00203 list( $match, $pos ) = $m[0]; // full match 00204 if ( strlen( $match ) > strlen( $longest ) ) { 00205 $longest = $match; 00206 $longestPos = $pos; 00207 } 00208 $offset = ( $pos + strlen( $match ) ); // advance 00209 } 00210 if ( $longest !== false ) { 00211 // Replace this portion of the string with the '::' abbreviation 00212 $ip = substr_replace( $ip, '::', $longestPos, strlen( $longest ) ); 00213 } 00214 // Add any CIDR back on 00215 if ( $cidr !== '' ) { 00216 $ip = "{$ip}/{$cidr}"; 00217 } 00218 // Convert to lower case to make it more readable 00219 $ip = strtolower( $ip ); 00220 } 00221 return $ip; 00222 } 00223 00240 public static function splitHostAndPort( $both ) { 00241 if ( substr( $both, 0, 1 ) === '[' ) { 00242 if ( preg_match( '/^\[(' . RE_IPV6_ADD . ')\](?::(?P<port>\d+))?$/', $both, $m ) ) { 00243 if ( isset( $m['port'] ) ) { 00244 return array( $m[1], intval( $m['port'] ) ); 00245 } else { 00246 return array( $m[1], false ); 00247 } 00248 } else { 00249 // Square bracket found but no IPv6 00250 return false; 00251 } 00252 } 00253 $numColons = substr_count( $both, ':' ); 00254 if ( $numColons >= 2 ) { 00255 // Is it a bare IPv6 address? 00256 if ( preg_match( '/^' . RE_IPV6_ADD . '$/', $both ) ) { 00257 return array( $both, false ); 00258 } else { 00259 // Not valid IPv6, but too many colons for anything else 00260 return false; 00261 } 00262 } 00263 if ( $numColons >= 1 ) { 00264 // Host:port? 00265 $bits = explode( ':', $both ); 00266 if ( preg_match( '/^\d+/', $bits[1] ) ) { 00267 return array( $bits[0], intval( $bits[1] ) ); 00268 } else { 00269 // Not a valid port 00270 return false; 00271 } 00272 } 00273 // Plain hostname 00274 return array( $both, false ); 00275 } 00276 00288 public static function combineHostAndPort( $host, $port, $defaultPort = false ) { 00289 if ( strpos( $host, ':' ) !== false ) { 00290 $host = "[$host]"; 00291 } 00292 if ( $defaultPort !== false && $port == $defaultPort ) { 00293 return $host; 00294 } else { 00295 return "$host:$port"; 00296 } 00297 } 00298 00305 public static function toOctet( $ip_int ) { 00306 return self::hexToOctet( wfBaseConvert( $ip_int, 10, 16, 32, false ) ); 00307 } 00308 00315 public static function formatHex( $hex ) { 00316 if ( substr( $hex, 0, 3 ) == 'v6-' ) { // IPv6 00317 return self::hexToOctet( substr( $hex, 3 ) ); 00318 } else { // IPv4 00319 return self::hexToQuad( $hex ); 00320 } 00321 } 00322 00329 public static function hexToOctet( $ip_hex ) { 00330 // Pad hex to 32 chars (128 bits) 00331 $ip_hex = str_pad( strtoupper( $ip_hex ), 32, '0', STR_PAD_LEFT ); 00332 // Separate into 8 words 00333 $ip_oct = substr( $ip_hex, 0, 4 ); 00334 for ( $n = 1; $n < 8; $n++ ) { 00335 $ip_oct .= ':' . substr( $ip_hex, 4 * $n, 4 ); 00336 } 00337 // NO leading zeroes 00338 $ip_oct = preg_replace( '/(^|:)0+(' . RE_IPV6_WORD . ')/', '$1$2', $ip_oct ); 00339 return $ip_oct; 00340 } 00341 00348 public static function hexToQuad( $ip_hex ) { 00349 // Pad hex to 8 chars (32 bits) 00350 $ip_hex = str_pad( strtoupper( $ip_hex ), 8, '0', STR_PAD_LEFT ); 00351 // Separate into four quads 00352 $s = ''; 00353 for ( $i = 0; $i < 4; $i++ ) { 00354 if ( $s !== '' ) { 00355 $s .= '.'; 00356 } 00357 $s .= base_convert( substr( $ip_hex, $i * 2, 2 ), 16, 10 ); 00358 } 00359 return $s; 00360 } 00361 00370 public static function isPublic( $ip ) { 00371 if ( self::isIPv6( $ip ) ) { 00372 return self::isPublic6( $ip ); 00373 } 00374 $n = self::toUnsigned( $ip ); 00375 if ( !$n ) { 00376 return false; 00377 } 00378 00379 // ip2long accepts incomplete addresses, as well as some addresses 00380 // followed by garbage characters. Check that it's really valid. 00381 if ( $ip != long2ip( $n ) ) { 00382 return false; 00383 } 00384 00385 static $privateRanges = false; 00386 if ( !$privateRanges ) { 00387 $privateRanges = array( 00388 array( '10.0.0.0', '10.255.255.255' ), # RFC 1918 (private) 00389 array( '172.16.0.0', '172.31.255.255' ), # RFC 1918 (private) 00390 array( '192.168.0.0', '192.168.255.255' ), # RFC 1918 (private) 00391 array( '0.0.0.0', '0.255.255.255' ), # this network 00392 array( '127.0.0.0', '127.255.255.255' ), # loopback 00393 ); 00394 } 00395 00396 foreach ( $privateRanges as $r ) { 00397 $start = self::toUnsigned( $r[0] ); 00398 $end = self::toUnsigned( $r[1] ); 00399 if ( $n >= $start && $n <= $end ) { 00400 return false; 00401 } 00402 } 00403 return true; 00404 } 00405 00413 private static function isPublic6( $ip ) { 00414 static $privateRanges = false; 00415 if ( !$privateRanges ) { 00416 $privateRanges = array( 00417 array( 'fc00::', 'fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' ), # RFC 4193 (local) 00418 array( '0:0:0:0:0:0:0:1', '0:0:0:0:0:0:0:1' ), # loopback 00419 ); 00420 } 00421 $n = self::toHex( $ip ); 00422 foreach ( $privateRanges as $r ) { 00423 $start = self::toHex( $r[0] ); 00424 $end = self::toHex( $r[1] ); 00425 if ( $n >= $start && $n <= $end ) { 00426 return false; 00427 } 00428 } 00429 return true; 00430 } 00431 00443 public static function toHex( $ip ) { 00444 if ( self::isIPv6( $ip ) ) { 00445 $n = 'v6-' . self::IPv6ToRawHex( $ip ); 00446 } else { 00447 $n = self::toUnsigned( $ip ); 00448 if ( $n !== false ) { 00449 $n = wfBaseConvert( $n, 10, 16, 8, false ); 00450 } 00451 } 00452 return $n; 00453 } 00454 00461 private static function IPv6ToRawHex( $ip ) { 00462 $ip = self::sanitizeIP( $ip ); 00463 if ( !$ip ) { 00464 return null; 00465 } 00466 $r_ip = ''; 00467 foreach ( explode( ':', $ip ) as $v ) { 00468 $r_ip .= str_pad( $v, 4, 0, STR_PAD_LEFT ); 00469 } 00470 return $r_ip; 00471 } 00472 00481 public static function toUnsigned( $ip ) { 00482 if ( self::isIPv6( $ip ) ) { 00483 $n = self::toUnsigned6( $ip ); 00484 } else { 00485 $n = ip2long( $ip ); 00486 if ( $n < 0 ) { 00487 $n += pow( 2, 32 ); 00488 # On 32-bit platforms (and on Windows), 2^32 does not fit into an int, 00489 # so $n becomes a float. We convert it to string instead. 00490 if ( is_float( $n ) ) { 00491 $n = (string)$n; 00492 } 00493 } 00494 } 00495 return $n; 00496 } 00497 00502 private static function toUnsigned6( $ip ) { 00503 return wfBaseConvert( self::IPv6ToRawHex( $ip ), 16, 10 ); 00504 } 00505 00513 public static function parseCIDR( $range ) { 00514 if ( self::isIPv6( $range ) ) { 00515 return self::parseCIDR6( $range ); 00516 } 00517 $parts = explode( '/', $range, 2 ); 00518 if ( count( $parts ) != 2 ) { 00519 return array( false, false ); 00520 } 00521 list( $network, $bits ) = $parts; 00522 $network = ip2long( $network ); 00523 if ( $network !== false && is_numeric( $bits ) && $bits >= 0 && $bits <= 32 ) { 00524 if ( $bits == 0 ) { 00525 $network = 0; 00526 } else { 00527 $network &= ~( ( 1 << ( 32 - $bits ) ) - 1 ); 00528 } 00529 # Convert to unsigned 00530 if ( $network < 0 ) { 00531 $network += pow( 2, 32 ); 00532 } 00533 } else { 00534 $network = false; 00535 $bits = false; 00536 } 00537 return array( $network, $bits ); 00538 } 00539 00555 public static function parseRange( $range ) { 00556 // CIDR notation 00557 if ( strpos( $range, '/' ) !== false ) { 00558 if ( self::isIPv6( $range ) ) { 00559 return self::parseRange6( $range ); 00560 } 00561 list( $network, $bits ) = self::parseCIDR( $range ); 00562 if ( $network === false ) { 00563 $start = $end = false; 00564 } else { 00565 $start = sprintf( '%08X', $network ); 00566 $end = sprintf( '%08X', $network + pow( 2, ( 32 - $bits ) ) - 1 ); 00567 } 00568 // Explicit range 00569 } elseif ( strpos( $range, '-' ) !== false ) { 00570 list( $start, $end ) = array_map( 'trim', explode( '-', $range, 2 ) ); 00571 if ( self::isIPv6( $start ) && self::isIPv6( $end ) ) { 00572 return self::parseRange6( $range ); 00573 } 00574 if ( self::isIPv4( $start ) && self::isIPv4( $end ) ) { 00575 $start = self::toUnsigned( $start ); 00576 $end = self::toUnsigned( $end ); 00577 if ( $start > $end ) { 00578 $start = $end = false; 00579 } else { 00580 $start = sprintf( '%08X', $start ); 00581 $end = sprintf( '%08X', $end ); 00582 } 00583 } else { 00584 $start = $end = false; 00585 } 00586 } else { 00587 # Single IP 00588 $start = $end = self::toHex( $range ); 00589 } 00590 if ( $start === false || $end === false ) { 00591 return array( false, false ); 00592 } else { 00593 return array( $start, $end ); 00594 } 00595 } 00596 00605 private static function parseCIDR6( $range ) { 00606 # Explode into <expanded IP,range> 00607 $parts = explode( '/', IP::sanitizeIP( $range ), 2 ); 00608 if ( count( $parts ) != 2 ) { 00609 return array( false, false ); 00610 } 00611 list( $network, $bits ) = $parts; 00612 $network = self::IPv6ToRawHex( $network ); 00613 if ( $network !== false && is_numeric( $bits ) && $bits >= 0 && $bits <= 128 ) { 00614 if ( $bits == 0 ) { 00615 $network = "0"; 00616 } else { 00617 # Native 32 bit functions WONT work here!!! 00618 # Convert to a padded binary number 00619 $network = wfBaseConvert( $network, 16, 2, 128 ); 00620 # Truncate the last (128-$bits) bits and replace them with zeros 00621 $network = str_pad( substr( $network, 0, $bits ), 128, 0, STR_PAD_RIGHT ); 00622 # Convert back to an integer 00623 $network = wfBaseConvert( $network, 2, 10 ); 00624 } 00625 } else { 00626 $network = false; 00627 $bits = false; 00628 } 00629 return array( $network, (int)$bits ); 00630 } 00631 00645 private static function parseRange6( $range ) { 00646 # Expand any IPv6 IP 00647 $range = IP::sanitizeIP( $range ); 00648 // CIDR notation... 00649 if ( strpos( $range, '/' ) !== false ) { 00650 list( $network, $bits ) = self::parseCIDR6( $range ); 00651 if ( $network === false ) { 00652 $start = $end = false; 00653 } else { 00654 $start = wfBaseConvert( $network, 10, 16, 32, false ); 00655 # Turn network to binary (again) 00656 $end = wfBaseConvert( $network, 10, 2, 128 ); 00657 # Truncate the last (128-$bits) bits and replace them with ones 00658 $end = str_pad( substr( $end, 0, $bits ), 128, 1, STR_PAD_RIGHT ); 00659 # Convert to hex 00660 $end = wfBaseConvert( $end, 2, 16, 32, false ); 00661 # see toHex() comment 00662 $start = "v6-$start"; 00663 $end = "v6-$end"; 00664 } 00665 // Explicit range notation... 00666 } elseif ( strpos( $range, '-' ) !== false ) { 00667 list( $start, $end ) = array_map( 'trim', explode( '-', $range, 2 ) ); 00668 $start = self::toUnsigned6( $start ); 00669 $end = self::toUnsigned6( $end ); 00670 if ( $start > $end ) { 00671 $start = $end = false; 00672 } else { 00673 $start = wfBaseConvert( $start, 10, 16, 32, false ); 00674 $end = wfBaseConvert( $end, 10, 16, 32, false ); 00675 } 00676 # see toHex() comment 00677 $start = "v6-$start"; 00678 $end = "v6-$end"; 00679 } else { 00680 # Single IP 00681 $start = $end = self::toHex( $range ); 00682 } 00683 if ( $start === false || $end === false ) { 00684 return array( false, false ); 00685 } else { 00686 return array( $start, $end ); 00687 } 00688 } 00689 00697 public static function isInRange( $addr, $range ) { 00698 $hexIP = self::toHex( $addr ); 00699 list( $start, $end ) = self::parseRange( $range ); 00700 return ( strcmp( $hexIP, $start ) >= 0 && 00701 strcmp( $hexIP, $end ) <= 0 ); 00702 } 00703 00714 public static function canonicalize( $addr ) { 00715 // remove zone info (bug 35738) 00716 $addr = preg_replace( '/\%.*/', '', $addr ); 00717 00718 if ( self::isValid( $addr ) ) { 00719 return $addr; 00720 } 00721 // Turn mapped addresses from ::ce:ffff:1.2.3.4 to 1.2.3.4 00722 if ( strpos( $addr, ':' ) !== false && strpos( $addr, '.' ) !== false ) { 00723 $addr = substr( $addr, strrpos( $addr, ':' ) + 1 ); 00724 if ( self::isIPv4( $addr ) ) { 00725 return $addr; 00726 } 00727 } 00728 // IPv6 loopback address 00729 $m = array(); 00730 if ( preg_match( '/^0*' . RE_IPV6_GAP . '1$/', $addr, $m ) ) { 00731 return '127.0.0.1'; 00732 } 00733 // IPv4-mapped and IPv4-compatible IPv6 addresses 00734 if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . '(' . RE_IP_ADD . ')$/i', $addr, $m ) ) { 00735 return $m[1]; 00736 } 00737 if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . RE_IPV6_WORD . 00738 ':' . RE_IPV6_WORD . '$/i', $addr, $m ) ) 00739 { 00740 return long2ip( ( hexdec( $m[1] ) << 16 ) + hexdec( $m[2] ) ); 00741 } 00742 00743 return null; // give up 00744 } 00745 00752 public static function sanitizeRange( $range ) { 00753 list( /*...*/, $bits ) = self::parseCIDR( $range ); 00754 list( $start, /*...*/ ) = self::parseRange( $range ); 00755 $start = self::formatHex( $start ); 00756 if ( $bits === false ) { 00757 return $start; // wasn't actually a range 00758 } 00759 return "$start/$bits"; 00760 } 00761 }