From: Aaron Schulz Date: Fri, 12 Nov 2010 01:33:46 +0000 (+0000) Subject: * Changes in IP.php: X-Git-Tag: 1.31.0-rc.0~33920 X-Git-Url: https://git.cyclocoop.org/%242?a=commitdiff_plain;h=ff84f68f6a394b08384e2c78618fe755872224fa;p=lhc%2Fweb%2Fwiklou.git * Changes in IP.php: ** Fixed hexToOctet()/toOctet() padding (pad left not right) ** Made hexToQuad() left-pad input (e.g. C -> 0000000C) ** Added isPublic6() function (checked as needed by isPublic()) ** Rewrote isValidBlock() to not do flaky and roundabout isArray() check. Works for v6 now. ** Removed toArray(), unused outside IP.php and broken for v6 ** Removed toOctet() duplication ** Added new private IPv6ToRawHex() function. Used to make toHex() faster. ** Made some functions private ** Reverted r20435, pointless ** Updated credits * Changes in IPTests: ** Added a bunch of tests (mostly v6) ** Removed weird array test (especially with toArray() gone) after r76514 ** Padding *no* longer needed for hexToX functions - assertion flipped * CheckUser: removed parseRange6() reference (now private) --- diff --git a/includes/IP.php b/includes/IP.php index 9ac6f67a38..b2f4874eba 100644 --- a/includes/IP.php +++ b/includes/IP.php @@ -18,7 +18,7 @@ * http://www.gnu.org/copyleft/gpl.html * * @file - * @author Ashar Voultoiz + * @author Ashar Voultoiz , Aaron Schulz */ // Some regex definition to "play" with IP address and IP address blocks @@ -69,9 +69,6 @@ class IP { * @return bool */ public static function isIPAddress( $ip ) { - if ( !$ip ) { - return false; - } return (bool)preg_match( '/^' . IP_ADDRESS_STRING . '$/', $ip ); } @@ -82,9 +79,6 @@ class IP { * @return bool */ public static function isIPv6( $ip ) { - if ( !$ip ) { - return false; - } return (bool)preg_match( '/^' . RE_IPV6_ADD . '(\/' . RE_IPV6_PREFIX . '|)$/', $ip ); } @@ -95,9 +89,6 @@ class IP { * @return bool */ public static function isIPv4( $ip ) { - if ( !$ip ) { - return false; - } return (bool)preg_match( '/^' . RE_IP_ADD . '(\/' . RE_IP_PREFIX . '|)$/', $ip ); } @@ -116,7 +107,7 @@ class IP { if ( self::isIPv6( $ip ) ) { return $ip; } - // IPv4 CIDRs + // IPv4 address with CIDR if ( strpos( $ip, '/' ) !== false ) { $parts = explode( '/', $ip, 2 ); if ( count( $parts ) != 2 ) { @@ -134,27 +125,17 @@ class IP { return self::toOctet( self::toUnsigned( $ip ) ); } - /** - * Given an IPv6 address in octet notation, returns an unsigned integer. - * @param string $ip octet ipv6 IP address. - * @return string - */ - public static function toUnsigned6( $ip ) { - if ( !$ip ) { - return null; - } - $ip = explode( ':', self::sanitizeIP( $ip ) ); - $r_ip = ''; - foreach ( $ip as $v ) { - $r_ip .= str_pad( $v, 4, 0, STR_PAD_LEFT ); + private static function toUnsigned6( $ip ) { + if ( self::isIPv6( $ip ) ) { + return wfBaseConvert( self::IPv6ToRawHex( $ip ), 16, 10 ); } - $r_ip = wfBaseConvert( $r_ip, 16, 10 ); - return $r_ip; + return false; } /** - * Given an IPv6 address in octet notation, returns the expanded octet. - * IPv4 IPs will be trimmed, thats it... + * Convert an IP into a nice standard form. + * IPv6 addresses in octet notation are expanded to 8 octets. + * IPv4 addresses are just trimmed. * @param string $ip IP address in quad or octet form (CIDR or not). * @return string */ @@ -205,20 +186,12 @@ class IP { /** * Given an unsigned integer, returns an IPv6 address in octet notation - * @param int $ip_int IP address. + * @param string $ip_int IP address. * @return string */ public static function toOctet( $ip_int ) { - // Convert to padded uppercase hex - $ip_hex = wfBaseConvert( $ip_int, 10, 16, 32, false ); - // Separate into 8 octets - $ip_oct = substr( $ip_hex, 0, 4 ); - for ( $n = 1; $n < 8; $n++ ) { - $ip_oct .= ':' . substr( $ip_hex, 4 * $n, 4 ); - } - // NO leading zeroes - $ip_oct = preg_replace( '/(^|:)0+' . RE_IPV6_WORD . '/', '$1$2', $ip_oct ); - return $ip_oct; + $ip_hex = wfBaseConvert( $ip_int, 10, 16, 32, false ); // uppercase hex + return self::hexToOctet( $ip_hex ); } /** @@ -236,12 +209,12 @@ class IP { /** * Converts a hexadecimal number to an IPv6 address in octet notation - * @param string $ip_hex hex IP + * @param string $ip_hex pure hex (no v6- prefix) * @return string (of format a:b:c:d:e:f:g:h) */ public static function hexToOctet( $ip_hex ) { // Convert to padded uppercase hex - $ip_hex = str_pad( strtoupper( $ip_hex ), 32, '0' ); + $ip_hex = str_pad( strtoupper( $ip_hex ), 32, '0', STR_PAD_LEFT ); // Separate into 8 octets $ip_oct = substr( $ip_hex, 0, 4 ); for ( $n = 1; $n < 8; $n++ ) { @@ -254,16 +227,19 @@ class IP { /** * Converts a hexadecimal number to an IPv4 address in quad-dotted notation - * @param string $ip Hex IP + * @param string $ip_hex pure hex * @return string (of format a.b.c.d) */ - public static function hexToQuad( $ip ) { + public static function hexToQuad( $ip_hex ) { + // Convert to padded uppercase hex + $ip_hex = str_pad( strtoupper( $ip_hex ), 8, '0', STR_PAD_LEFT ); + // Separate into four quads $s = ''; for ( $i = 0; $i < 4; $i++ ) { if ( $s !== '' ) { $s .= '.'; } - $s .= base_convert( substr( $ip, $i * 2, 2 ), 16, 10 ); + $s .= base_convert( substr( $ip_hex, $i * 2, 2 ), 16, 10 ); } return $s; } @@ -273,21 +249,21 @@ class IP { * integer network and a number of bits * @return array(string, int) */ - public static function parseCIDR6( $range ) { + private static function parseCIDR6( $range ) { # Explode into $parts = explode( '/', IP::sanitizeIP( $range ), 2 ); if ( count( $parts ) != 2 ) { return array( false, false ); } list( $network, $bits ) = $parts; - $network = self::toUnsigned6( $network ); + $network = self::IPv6ToRawHex( $network ); if ( $network !== false && is_numeric( $bits ) && $bits >= 0 && $bits <= 128 ) { if ( $bits == 0 ) { $network = "0"; } else { # Native 32 bit functions WONT work here!!! # Convert to a padded binary number - $network = wfBaseConvert( $network, 10, 2, 128 ); + $network = wfBaseConvert( $network, 16, 2, 128 ); # Truncate the last (128-$bits) bits and replace them with zeros $network = str_pad( substr( $network, 0, $bits ), 128, 0, STR_PAD_RIGHT ); # Convert back to an integer @@ -301,8 +277,8 @@ class IP { } /** - * Given a string range in a number of formats, return the start and end of - * the range in hexadecimal. For IPv6. + * Given a string range in a number of formats, return the + * start and end of the range in hexadecimal. For IPv6. * * Formats are: * 2001:0db8:85a3::7344/96 CIDR @@ -310,7 +286,7 @@ class IP { * 2001:0db8:85a3::7344/96 Single IP * @return array(string, int) */ - public static function parseRange6( $range ) { + private static function parseRange6( $range ) { # Expand any IPv6 IP $range = IP::sanitizeIP( $range ); // CIDR notation... @@ -356,21 +332,23 @@ class IP { } /** - * Validate an IP address. + * Validate an IP address. Ranges are NOT considered valid. * @param string $ip * @return boolean True if it is valid. */ public static function isValid( $ip ) { - return ( preg_match( '/^' . RE_IP_ADD . '$/', $ip ) || preg_match( '/^' . RE_IPV6_ADD . '$/', $ip ) ); + return ( preg_match( '/^' . RE_IP_ADD . '$/', $ip ) + || preg_match( '/^' . RE_IPV6_ADD . '$/', $ip ) ); } /** - * Validate an IP Block. + * Validate an IP Block (valid address WITH a valid prefix). * @param string $ipblock * @return boolean True if it is valid. */ public static function isValidBlock( $ipblock ) { - return ( count( self::toArray( $ipblock ) ) == 1 + 5 ); + return ( preg_match( '/^' . RE_IPV6_BLOCK . '$/', $ipblock ) + || preg_match( '/^' . RE_IPV4_BLOCK . '$/', $ipblock ) ); } /** @@ -381,6 +359,9 @@ class IP { * @return bool */ public static function isPublic( $ip ) { + if ( self::isIPv6( $ip ) ) { + return self::isPublic6( $ip ); + } $n = self::toUnsigned( $ip ); if ( !$n ) { return false; @@ -414,25 +395,32 @@ class IP { } /** - * Split out an IP block as an array of 4 bytes and a mask, - * return false if it can't be determined - * - * @param string $ipblock A quad dotted/octet IP address - * @return array + * Determine if an IPv6 address really is an IP address, and if it is public, + * i.e. not RFC 4193 or similar + * @param string $ip + * @return bool */ - public static function toArray( $ipblock ) { - $matches = array(); - if( preg_match( '/^' . RE_IP_ADD . '(?:\/(?:' . RE_IP_PREFIX . '))?' . '$/', $ipblock, $matches ) ) { - return $matches; - } elseif ( preg_match( '/^' . RE_IPV6_ADD . '(?:\/(?:' . RE_IPV6_PREFIX . '))?' . '$/', $ipblock, $matches ) ) { - return $matches; - } else { - return false; + private static function isPublic6( $ip ) { + static $privateRanges = false; + if ( !$privateRanges ) { + $privateRanges = array( + array( 'fc::', 'fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' ), # RFC 4193 (local) + array( '0:0:0:0:0:0:0:1', '0:0:0:0:0:0:0:1' ), # loopback + ); + } + $n = self::toHex( $ip ); + foreach ( $privateRanges as $r ) { + $start = self::toHex( $r[0] ); + $end = self::toHex( $r[1] ); + if ( $n >= $start && $n <= $end ) { + return false; + } } + return true; } /** - * Return a zero-padded hexadecimal representation of an IP address. + * Return a zero-padded upper case hexadecimal representation of an IP address. * * Hexadecimal addresses are used because they can easily be extended to * IPv6 support. To separate the ranges, the return value from this @@ -443,11 +431,13 @@ class IP { * @return string */ public static function toHex( $ip ) { - $n = self::toUnsigned( $ip ); - if ( $n !== false ) { - $n = self::isIPv6( $ip ) - ? 'v6-' . wfBaseConvert( $n, 10, 16, 32, false ) - : wfBaseConvert( $n, 10, 16, 8, false ); + if ( self::isIPv6( $ip ) ) { + $n = 'v6-' . self::IPv6ToRawHex( $ip ); + } else { + $n = self::toUnsigned( $ip ); + if ( $n !== false ) { + $n = wfBaseConvert( $n, 10, 16, 8, false ); + } } return $n; } @@ -457,27 +447,44 @@ class IP { * Like ip2long() except that it actually works and has a consistent error return value. * Comes from ProxyTools.php * @param string $ip Quad dotted IP address. - * @return integer + * @return mixed (string/int/false) */ public static function toUnsigned( $ip ) { - // Use IPv6 functions if needed if ( self::isIPv6( $ip ) ) { - return self::toUnsigned6( $ip ); - } - if ( $ip == '255.255.255.255' ) { - $n = -1; + $n = wfBaseConvert( self::IPv6ToRawHex( $ip ), 16, 10 ); } else { - $n = ip2long( $ip ); - if ( $n == -1 || $n === false ) { # Return value on error depends on PHP version - $n = false; + if ( $ip == '255.255.255.255' ) { + $n = -1; + } else { + $n = ip2long( $ip ); + if ( $n == -1 || $n === false ) { # Return value on error depends on PHP version + $n = false; + } + } + if ( $n < 0 ) { + $n += pow( 2, 32 ); } - } - if ( $n < 0 ) { - $n += pow( 2, 32 ); } return $n; } + /** + * Given an IPv6 address in octet notation, returns a pure hex string. + * @param string $ip octet ipv6 IP address. + * @return string hex (uppercase) + */ + private static function IPv6ToRawHex( $ip ) { + $ip = self::sanitizeIP( $ip ); + if ( !$ip ) { + return null; + } + $r_ip = ''; + foreach ( explode( ':', $ip ) as $v ) { + $r_ip .= str_pad( $v, 4, 0, STR_PAD_LEFT ); + } + return $r_ip; + } + /** * Convert a dotted-quad IP to a signed integer * @param string $ip @@ -586,7 +593,6 @@ class IP { * @return bool Whether or not the given address is in the given range. */ public static function isInRange( $addr, $range ) { - // Convert to IPv6 if needed $hexIP = self::toHex( $addr ); list( $start, $end ) = self::parseRange( $range ); return ( strcmp( $hexIP, $start ) >= 0 && diff --git a/maintenance/tests/phpunit/includes/IPTest.php b/maintenance/tests/phpunit/includes/IPTest.php index 2336086f3c..d6913a6238 100644 --- a/maintenance/tests/phpunit/includes/IPTest.php +++ b/maintenance/tests/phpunit/includes/IPTest.php @@ -7,22 +7,70 @@ class IPTest extends PHPUnit_Framework_TestCase { // not sure it should be tested with boolean false. hashar 20100924 public function testisIPAddress() { $this->assertFalse( IP::isIPAddress( false ) ); + $this->assertFalse( IP::isIPAddress( "" ) ); + $this->assertFalse( IP::isIPAddress( 'abc' ) ); + $this->assertFalse( IP::isIPAddress( ':' ) ); $this->assertFalse( IP::isIPAddress( '2001:0DB8::A:1::1'), 'IPv6 with a double :: occurence' ); $this->assertFalse( IP::isIPAddress( '2001:0DB8::A:1::'), 'IPv6 with a double :: occurence, last at end' ); $this->assertFalse( IP::isIPAddress( '::2001:0DB8::5:1'), 'IPv6 with a double :: occurence, firt at beginning' ); - } + $this->assertFalse( IP::isIPAddress( '124.24.52' ), 'IPv4 not enough quads' ); + $this->assertFalse( IP::isIPAddress( '24.324.52.13' ), 'IPv4 out of range' ); + $this->assertFalse( IP::isIPAddress( '.24.52.13' ), 'IPv4 starts with period' ); - /** - * @expectedException MWException - */ - public function testArrayIsNotIPAddress() { - IP::isIPAddress( array('') ); + $this->assertTrue( IP::isIPAddress( 'fc:100::' ) ); + $this->assertTrue( IP::isIPAddress( 'fc:100:a:d:1:e:ac::' ) ); + $this->assertTrue( IP::isIPAddress( '::' ), 'IPv6 zero address' ); + $this->assertTrue( IP::isIPAddress( '::fc' ) ); + $this->assertTrue( IP::isIPAddress( '::fc:100:a:d:1:e:ac' ) ); + $this->assertTrue( IP::isIPAddress( 'fc::100' ) ); + $this->assertTrue( IP::isIPAddress( 'fc::100:a:d:1:e:ac' ) ); + $this->assertTrue( IP::isIPAddress( 'fc::100:a:d:1:e:ac/96', 'IPv6 range with "::"' ) ); + $this->assertTrue( IP::isIPAddress( 'fc:100:a:d:1:e:ac:0' ) ); + $this->assertTrue( IP::isIPAddress( 'fc:100:a:d:1:e:ac:0/24', 'IPv6 range' ) ); + $this->assertTrue( IP::isIPAddress( '124.24.52.13' ) ); + $this->assertTrue( IP::isIPAddress( '1.24.52.13' ) ); + $this->assertTrue( IP::isIPAddress( '74.24.52.13/20', 'IPv4 range' ) ); } - /** - * @expectedException MWException - */ - public function testArrayIsNotIPv6() { - IP::isIPv6( array('') ); + + public function testisIPv6() { + $this->assertFalse( IP::isIPv6( ':fc:100::' ), 'IPv6 starting with lone ":"' ); + $this->assertFalse( IP::isIPv6( 'fc:100:::' ), 'IPv6 ending with a ":::"' ); + $this->assertTrue( IP::isIPv6( 'fc:100::' ) ); + $this->assertTrue( IP::isIPv6( 'fc:100:a::' ) ); + $this->assertTrue( IP::isIPv6( 'fc:100:a:d::' ) ); + $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1::' ) ); + $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1:e::' ) ); + $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1:e:ac::' ) ); + $this->assertFalse( IP::isIPv6( 'fc:100:a:d:1:e:ac:0::' ), 'IPv6 ending it "::" with 8 words' ); + $this->assertFalse( IP::isIPv6( 'fc:100:a:d:1:e:ac:0:1::' ), 'IPv6 with 9 words' ); + + $this->assertFalse( IP::isIPv6( ':::' ) ); + $this->assertFalse( IP::isIPv6( '::0:' ), 'IPv6 ending in a lone ":"' ); + $this->assertTrue( IP::isIPv6( '::' ), 'IPv6 zero address' ); + $this->assertTrue( IP::isIPv6( '::0' ) ); + $this->assertTrue( IP::isIPv6( '::fc' ) ); + $this->assertTrue( IP::isIPv6( '::fc:100' ) ); + $this->assertTrue( IP::isIPv6( '::fc:100:a' ) ); + $this->assertTrue( IP::isIPv6( '::fc:100:a:d' ) ); + $this->assertTrue( IP::isIPv6( '::fc:100:a:d:1' ) ); + $this->assertTrue( IP::isIPv6( '::fc:100:a:d:1:e' ) ); + $this->assertTrue( IP::isIPv6( '::fc:100:a:d:1:e:ac' ) ); + $this->assertFalse( IP::isIPv6( '::fc:100:a:d:1:e:ac:0' ), 'IPv6 with "::" and 8 octets' ); + $this->assertFalse( IP::isIPv6( '::fc:100:a:d:1:e:ac:0:1' ), 'IPv6 with 9 octets' ); + + $this->assertFalse( IP::isIPv6( ':fc::100' ), 'IPv6 starting with lone ":"' ); + $this->assertFalse( IP::isIPv6( 'fc::100:' ), 'IPv6 ending in lone ":"' ); + $this->assertFalse( IP::isIPv6( 'fc:::100' ), 'IPv6 with ":::" in the middle' ); + $this->assertTrue( IP::isIPv6( 'fc::100' ) ); + $this->assertTrue( IP::isIPv6( 'fc::100:a' ) ); + $this->assertTrue( IP::isIPv6( 'fc::100:a:d' ) ); + $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1' ) ); + $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1:e' ) ); + $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1:e:ac' ) ); + $this->assertFalse( IP::isIPv6( 'fc::100:a:d:1:e:ac:0' ), 'IPv6 with "::" and 8 octets' ); + $this->assertFalse( IP::isIPv6( 'fc::100:a:d:1:e:ac:0:1' ), 'IPv6 with 9 octets' ); + + $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1:e:ac:0' ) ); } public function testValidIPs() { @@ -35,6 +83,15 @@ class IPTest extends PHPUnit_Framework_TestCase { $this->assertTrue( IP::isValid( $ip ) , "$ip is a valid IPv4 address" ); } } + foreach ( range( 0, 65535 ) as $i ) { + $a = sprintf( "%04x", $i ); + $b = sprintf( "%03x", $i ); + $c = sprintf( "%02x", $i ); + foreach ( array_unique( array( $a, $b, $c ) ) as $f ) { + $ip = "$f:$f:$f:$f:$f:$f:$f:$f"; + $this->assertTrue( IP::isValid( $ip ) , "$ip is a valid IPv6 address" ); + } + } } public function testInvalidIPs() { @@ -47,6 +104,15 @@ class IPTest extends PHPUnit_Framework_TestCase { $this->assertFalse( IP::isValid( $ip ), "$ip is not a valid IPv4 address" ); } } + foreach ( range( 'g', 'z' ) as $i ) { + $a = sprintf( "%04", $i ); + $b = sprintf( "%03", $i ); + $c = sprintf( "%02", $i ); + foreach ( array_unique( array( $a, $b, $c ) ) as $f ) { + $ip = "$f:$f:$f:$f:$f:$f:$f:$f"; + $this->assertFalse( IP::isValid( $ip ) , "$ip is not a valid IPv6 address" ); + } + } } public function testBogusIPs() { @@ -92,17 +158,36 @@ class IPTest extends PHPUnit_Framework_TestCase { $this->assertEquals( $expected, long2ip( $parse[0] ), "network shifting $CIDR" ); } - public function testHexToQuad() { - $this->assertEquals( '0.0.0.0' , IP::hexToQuad( '0' ) ); $this->assertEquals( '0.0.0.1' , IP::hexToQuad( '00000001' ) ); $this->assertEquals( '255.0.0.0' , IP::hexToQuad( 'FF000000' ) ); $this->assertEquals( '255.255.255.255', IP::hexToQuad( 'FFFFFFFF' ) ); $this->assertEquals( '10.188.222.255' , IP::hexToQuad( '0ABCDEFF' ) ); - - $this->assertNotEquals( '0.0.0.1' , IP::hexToQuad( '1' ) ); - $this->assertNotEquals( '0.0.0.255' , IP::hexToQuad( 'FF' ) ); - $this->assertNotEquals( '0.0.255.0' , IP::hexToQuad( 'FF00' ) ); + // hex not left-padded... + $this->assertEquals( '0.0.0.0' , IP::hexToQuad( '0' ) ); + $this->assertEquals( '0.0.0.1' , IP::hexToQuad( '1' ) ); + $this->assertEquals( '0.0.0.255' , IP::hexToQuad( 'FF' ) ); + $this->assertEquals( '0.0.255.0' , IP::hexToQuad( 'FF00' ) ); + } + + public function testHexToOctet() { + $this->assertEquals( '0:0:0:0:0:0:0:1', + IP::hexToOctet( '00000000000000000000000000000001' ) ); + $this->assertEquals( '0:0:0:0:0:0:FF:3', + IP::hexToOctet( '00000000000000000000000000FF0003' ) ); + $this->assertEquals( '0:0:0:0:0:0:FF00:6', + IP::hexToOctet( '000000000000000000000000FF000006' ) ); + $this->assertEquals( '0:0:0:0:0:0:FCCF:FAFF', + IP::hexToOctet( '000000000000000000000000FCCFFAFF' ) ); + $this->assertEquals( 'FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF', + IP::hexToOctet( 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' ) ); + // hex not left-padded... + $this->assertEquals( '0:0:0:0:0:0:0:0' , IP::hexToOctet( '0' ) ); + $this->assertEquals( '0:0:0:0:0:0:0:1' , IP::hexToOctet( '1' ) ); + $this->assertEquals( '0:0:0:0:0:0:0:FF' , IP::hexToOctet( 'FF' ) ); + $this->assertEquals( '0:0:0:0:0:0:0:FFD0' , IP::hexToOctet( 'FFD0' ) ); + $this->assertEquals( '0:0:0:0:0:0:FA00:0' , IP::hexToOctet( 'FA000000' ) ); + $this->assertEquals( '0:0:0:0:0:0:FCCF:FAFF', IP::hexToOctet( 'FCCFFAFF' ) ); } /*