From: Kunal Mehta Date: Thu, 22 Sep 2016 03:18:08 +0000 (-0700) Subject: Move IP class to libs/ X-Git-Tag: 1.31.0-rc.0~5399^2 X-Git-Url: http://git.cyclocoop.org/%22%20.%20%20%20%24self2%20.%20%20%20%22&var_mode_affiche=boucle?a=commitdiff_plain;h=04dfd9b6bcfbcc23b2126e341155a961ac974d2b;p=lhc%2Fweb%2Fwiklou.git Move IP class to libs/ Also fix some misplaced parenthesis in IPTest. Change-Id: I84d6120c49f733ec45e7e0005259871808b7568b --- diff --git a/autoload.php b/autoload.php index aa0f561a1b..9e5bf04b78 100644 --- a/autoload.php +++ b/autoload.php @@ -586,7 +586,7 @@ $wgAutoloadLocalClasses = [ 'IJobSpecification' => __DIR__ . '/includes/jobqueue/JobSpecification.php', 'ILoadBalancer' => __DIR__ . '/includes/libs/rdbms/loadbalancer/ILoadBalancer.php', 'ILoadMonitor' => __DIR__ . '/includes/libs/rdbms/loadmonitor/ILoadMonitor.php', - 'IP' => __DIR__ . '/includes/utils/IP.php', + 'IP' => __DIR__ . '/includes/libs/IP.php', 'IPSet' => __DIR__ . '/includes/compat/IPSetCompat.php', 'IPTC' => __DIR__ . '/includes/media/IPTC.php', 'IRCColourfulRCFeedFormatter' => __DIR__ . '/includes/rcfeed/IRCColourfulRCFeedFormatter.php', diff --git a/includes/libs/IP.php b/includes/libs/IP.php new file mode 100644 index 0000000000..21203a47ce --- /dev/null +++ b/includes/libs/IP.php @@ -0,0 +1,744 @@ +", Aaron Schulz + */ + +use IPSet\IPSet; + +// Some regex definition to "play" with IP address and IP address blocks + +// An IPv4 address is made of 4 bytes from x00 to xFF which is d0 to d255 +define( 'RE_IP_BYTE', '(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|0?[0-9]?[0-9])' ); +define( 'RE_IP_ADD', RE_IP_BYTE . '\.' . RE_IP_BYTE . '\.' . RE_IP_BYTE . '\.' . RE_IP_BYTE ); +// An IPv4 block is an IP address and a prefix (d1 to d32) +define( 'RE_IP_PREFIX', '(3[0-2]|[12]?\d)' ); +define( 'RE_IP_BLOCK', RE_IP_ADD . '\/' . RE_IP_PREFIX ); + +// An IPv6 address is made up of 8 words (each x0000 to xFFFF). +// However, the "::" abbreviation can be used on consecutive x0000 words. +define( 'RE_IPV6_WORD', '([0-9A-Fa-f]{1,4})' ); +define( 'RE_IPV6_PREFIX', '(12[0-8]|1[01][0-9]|[1-9]?\d)' ); +define( 'RE_IPV6_ADD', + '(?:' . // starts with "::" (including "::") + ':(?::|(?::' . RE_IPV6_WORD . '){1,7})' . + '|' . // ends with "::" (except "::") + RE_IPV6_WORD . '(?::' . RE_IPV6_WORD . '){0,6}::' . + '|' . // contains one "::" in the middle (the ^ makes the test fail if none found) + RE_IPV6_WORD . '(?::((?(-1)|:))?' . RE_IPV6_WORD . '){1,6}(?(-2)|^)' . + '|' . // contains no "::" + RE_IPV6_WORD . '(?::' . RE_IPV6_WORD . '){7}' . + ')' +); +// An IPv6 block is an IP address and a prefix (d1 to d128) +define( 'RE_IPV6_BLOCK', RE_IPV6_ADD . '\/' . RE_IPV6_PREFIX ); +// For IPv6 canonicalization (NOT for strict validation; these are quite lax!) +define( 'RE_IPV6_GAP', ':(?:0+:)*(?::(?:0+:)*)?' ); +define( 'RE_IPV6_V4_PREFIX', '0*' . RE_IPV6_GAP . '(?:ffff:)?' ); + +// This might be useful for regexps used elsewhere, matches any IPv4 or IPv6 address or network +define( 'IP_ADDRESS_STRING', + '(?:' . + RE_IP_ADD . '(?:\/' . RE_IP_PREFIX . ')?' . // IPv4 + '|' . + RE_IPV6_ADD . '(?:\/' . RE_IPV6_PREFIX . ')?' . // IPv6 + ')' +); + +/** + * A collection of public static functions to play with IP address + * and IP blocks. + */ +class IP { + /** @var IPSet */ + private static $proxyIpSet = null; + + /** + * Determine if a string is as valid IP address or network (CIDR prefix). + * SIIT IPv4-translated addresses are rejected. + * @note canonicalize() tries to convert translated addresses to IPv4. + * + * @param string $ip Possible IP address + * @return bool + */ + public static function isIPAddress( $ip ) { + return (bool)preg_match( '/^' . IP_ADDRESS_STRING . '$/', $ip ); + } + + /** + * Given a string, determine if it as valid IP in IPv6 only. + * @note Unlike isValid(), this looks for networks too. + * + * @param string $ip Possible IP address + * @return bool + */ + public static function isIPv6( $ip ) { + return (bool)preg_match( '/^' . RE_IPV6_ADD . '(?:\/' . RE_IPV6_PREFIX . ')?$/', $ip ); + } + + /** + * Given a string, determine if it as valid IP in IPv4 only. + * @note Unlike isValid(), this looks for networks too. + * + * @param string $ip Possible IP address + * @return bool + */ + public static function isIPv4( $ip ) { + return (bool)preg_match( '/^' . RE_IP_ADD . '(?:\/' . RE_IP_PREFIX . ')?$/', $ip ); + } + + /** + * Validate an IP address. Ranges are NOT considered valid. + * SIIT IPv4-translated addresses are rejected. + * @note canonicalize() tries to convert translated addresses to IPv4. + * + * @param string $ip + * @return bool True if it is valid + */ + public static function isValid( $ip ) { + return ( preg_match( '/^' . RE_IP_ADD . '$/', $ip ) + || preg_match( '/^' . RE_IPV6_ADD . '$/', $ip ) ); + } + + /** + * Validate an IP Block (valid address WITH a valid prefix). + * SIIT IPv4-translated addresses are rejected. + * @note canonicalize() tries to convert translated addresses to IPv4. + * + * @param string $ipblock + * @return bool True if it is valid + */ + public static function isValidBlock( $ipblock ) { + return ( preg_match( '/^' . RE_IPV6_BLOCK . '$/', $ipblock ) + || preg_match( '/^' . RE_IP_BLOCK . '$/', $ipblock ) ); + } + + /** + * Convert an IP into a verbose, uppercase, normalized form. + * Both IPv4 and IPv6 addresses are trimmed. Additionally, + * IPv6 addresses in octet notation are expanded to 8 words; + * IPv4 addresses have leading zeros, in each octet, removed. + * + * @param string $ip IP address in quad or octet form (CIDR or not). + * @return string + */ + public static function sanitizeIP( $ip ) { + $ip = trim( $ip ); + if ( $ip === '' ) { + return null; + } + /* If not an IP, just return trimmed value, since sanitizeIP() is called + * in a number of contexts where usernames are supplied as input. + */ + if ( !self::isIPAddress( $ip ) ) { + return $ip; + } + if ( self::isIPv4( $ip ) ) { + // Remove leading 0's from octet representation of IPv4 address + $ip = preg_replace( '/(?:^|(?<=\.))0+(?=[1-9]|0\.|0$)/', '', $ip ); + return $ip; + } + // Remove any whitespaces, convert to upper case + $ip = strtoupper( $ip ); + // Expand zero abbreviations + $abbrevPos = strpos( $ip, '::' ); + if ( $abbrevPos !== false ) { + // We know this is valid IPv6. Find the last index of the + // address before any CIDR number (e.g. "a:b:c::/24"). + $CIDRStart = strpos( $ip, "/" ); + $addressEnd = ( $CIDRStart !== false ) + ? $CIDRStart - 1 + : strlen( $ip ) - 1; + // If the '::' is at the beginning... + if ( $abbrevPos == 0 ) { + $repeat = '0:'; + $extra = ( $ip == '::' ) ? '0' : ''; // for the address '::' + $pad = 9; // 7+2 (due to '::') + // If the '::' is at the end... + } elseif ( $abbrevPos == ( $addressEnd - 1 ) ) { + $repeat = ':0'; + $extra = ''; + $pad = 9; // 7+2 (due to '::') + // If the '::' is in the middle... + } else { + $repeat = ':0'; + $extra = ':'; + $pad = 8; // 6+2 (due to '::') + } + $ip = str_replace( '::', + str_repeat( $repeat, $pad - substr_count( $ip, ':' ) ) . $extra, + $ip + ); + } + // Remove leading zeros from each bloc as needed + $ip = preg_replace( '/(^|:)0+(' . RE_IPV6_WORD . ')/', '$1$2', $ip ); + + return $ip; + } + + /** + * Prettify an IP for display to end users. + * This will make it more compact and lower-case. + * + * @param string $ip + * @return string + */ + public static function prettifyIP( $ip ) { + $ip = self::sanitizeIP( $ip ); // normalize (removes '::') + if ( self::isIPv6( $ip ) ) { + // Split IP into an address and a CIDR + if ( strpos( $ip, '/' ) !== false ) { + list( $ip, $cidr ) = explode( '/', $ip, 2 ); + } else { + list( $ip, $cidr ) = [ $ip, '' ]; + } + // Get the largest slice of words with multiple zeros + $offset = 0; + $longest = $longestPos = false; + while ( preg_match( + '!(?:^|:)0(?::0)+(?:$|:)!', $ip, $m, PREG_OFFSET_CAPTURE, $offset + ) ) { + list( $match, $pos ) = $m[0]; // full match + if ( strlen( $match ) > strlen( $longest ) ) { + $longest = $match; + $longestPos = $pos; + } + $offset = ( $pos + strlen( $match ) ); // advance + } + if ( $longest !== false ) { + // Replace this portion of the string with the '::' abbreviation + $ip = substr_replace( $ip, '::', $longestPos, strlen( $longest ) ); + } + // Add any CIDR back on + if ( $cidr !== '' ) { + $ip = "{$ip}/{$cidr}"; + } + // Convert to lower case to make it more readable + $ip = strtolower( $ip ); + } + + return $ip; + } + + /** + * Given a host/port string, like one might find in the host part of a URL + * per RFC 2732, split the hostname part and the port part and return an + * array with an element for each. If there is no port part, the array will + * have false in place of the port. If the string was invalid in some way, + * false is returned. + * + * This was easy with IPv4 and was generally done in an ad-hoc way, but + * with IPv6 it's somewhat more complicated due to the need to parse the + * square brackets and colons. + * + * A bare IPv6 address is accepted despite the lack of square brackets. + * + * @param string $both The string with the host and port + * @return array|false Array normally, false on certain failures + */ + public static function splitHostAndPort( $both ) { + if ( substr( $both, 0, 1 ) === '[' ) { + if ( preg_match( '/^\[(' . RE_IPV6_ADD . ')\](?::(?P\d+))?$/', $both, $m ) ) { + if ( isset( $m['port'] ) ) { + return [ $m[1], intval( $m['port'] ) ]; + } else { + return [ $m[1], false ]; + } + } else { + // Square bracket found but no IPv6 + return false; + } + } + $numColons = substr_count( $both, ':' ); + if ( $numColons >= 2 ) { + // Is it a bare IPv6 address? + if ( preg_match( '/^' . RE_IPV6_ADD . '$/', $both ) ) { + return [ $both, false ]; + } else { + // Not valid IPv6, but too many colons for anything else + return false; + } + } + if ( $numColons >= 1 ) { + // Host:port? + $bits = explode( ':', $both ); + if ( preg_match( '/^\d+/', $bits[1] ) ) { + return [ $bits[0], intval( $bits[1] ) ]; + } else { + // Not a valid port + return false; + } + } + + // Plain hostname + return [ $both, false ]; + } + + /** + * Given a host name and a port, combine them into host/port string like + * you might find in a URL. If the host contains a colon, wrap it in square + * brackets like in RFC 2732. If the port matches the default port, omit + * the port specification + * + * @param string $host + * @param int $port + * @param bool|int $defaultPort + * @return string + */ + public static function combineHostAndPort( $host, $port, $defaultPort = false ) { + if ( strpos( $host, ':' ) !== false ) { + $host = "[$host]"; + } + if ( $defaultPort !== false && $port == $defaultPort ) { + return $host; + } else { + return "$host:$port"; + } + } + + /** + * Convert an IPv4 or IPv6 hexadecimal representation back to readable format + * + * @param string $hex Number, with "v6-" prefix if it is IPv6 + * @return string Quad-dotted (IPv4) or octet notation (IPv6) + */ + public static function formatHex( $hex ) { + if ( substr( $hex, 0, 3 ) == 'v6-' ) { // IPv6 + return self::hexToOctet( substr( $hex, 3 ) ); + } else { // IPv4 + return self::hexToQuad( $hex ); + } + } + + /** + * Converts a hexadecimal number to an IPv6 address in octet notation + * + * @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 ) { + // Pad hex to 32 chars (128 bits) + $ip_hex = str_pad( strtoupper( $ip_hex ), 32, '0', STR_PAD_LEFT ); + // Separate into 8 words + $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; + } + + /** + * Converts a hexadecimal number to an IPv4 address in quad-dotted notation + * + * @param string $ip_hex Pure hex + * @return string (of format a.b.c.d) + */ + public static function hexToQuad( $ip_hex ) { + // Pad hex to 8 chars (32 bits) + $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_hex, $i * 2, 2 ), 16, 10 ); + } + + return $s; + } + + /** + * Determine if an IP address really is an IP address, and if it is public, + * i.e. not RFC 1918 or similar + * + * @param string $ip + * @return bool + */ + public static function isPublic( $ip ) { + static $privateSet = null; + if ( !$privateSet ) { + $privateSet = new IPSet( [ + '10.0.0.0/8', # RFC 1918 (private) + '172.16.0.0/12', # RFC 1918 (private) + '192.168.0.0/16', # RFC 1918 (private) + '0.0.0.0/8', # this network + '127.0.0.0/8', # loopback + 'fc00::/7', # RFC 4193 (local) + '0:0:0:0:0:0:0:1', # loopback + '169.254.0.0/16', # link-local + 'fe80::/10', # link-local + ] ); + } + return !$privateSet->match( $ip ); + } + + /** + * 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 + * function for an IPv6 address will be prefixed with "v6-", a non- + * hexadecimal string which sorts after the IPv4 addresses. + * + * @param string $ip Quad dotted/octet IP address. + * @return string|bool False on failure + */ + public static function toHex( $ip ) { + if ( self::isIPv6( $ip ) ) { + $n = 'v6-' . self::IPv6ToRawHex( $ip ); + } elseif ( self::isIPv4( $ip ) ) { + // T62035/T97897: An IP with leading 0's fails in ip2long sometimes (e.g. *.08), + // also double/triple 0 needs to be changed to just a single 0 for ip2long. + $ip = self::sanitizeIP( $ip ); + $n = ip2long( $ip ); + if ( $n < 0 ) { + $n += pow( 2, 32 ); + # On 32-bit platforms (and on Windows), 2^32 does not fit into an int, + # so $n becomes a float. We convert it to string instead. + if ( is_float( $n ) ) { + $n = (string)$n; + } + } + if ( $n !== false ) { + # Floating points can handle the conversion; faster than Wikimedia\base_convert() + $n = strtoupper( str_pad( base_convert( $n, 10, 16 ), 8, '0', STR_PAD_LEFT ) ); + } + } else { + $n = false; + } + + return $n; + } + + /** + * Given an IPv6 address in octet notation, returns a pure hex string. + * + * @param string $ip Octet ipv6 IP address. + * @return string|bool Pure hex (uppercase); false on failure + */ + private static function IPv6ToRawHex( $ip ) { + $ip = self::sanitizeIP( $ip ); + if ( !$ip ) { + return false; + } + $r_ip = ''; + foreach ( explode( ':', $ip ) as $v ) { + $r_ip .= str_pad( $v, 4, 0, STR_PAD_LEFT ); + } + + return $r_ip; + } + + /** + * Convert a network specification in CIDR notation + * to an integer network and a number of bits + * + * @param string $range IP with CIDR prefix + * @return array(int or string, int) + */ + public static function parseCIDR( $range ) { + if ( self::isIPv6( $range ) ) { + return self::parseCIDR6( $range ); + } + $parts = explode( '/', $range, 2 ); + if ( count( $parts ) != 2 ) { + return [ false, false ]; + } + list( $network, $bits ) = $parts; + $network = ip2long( $network ); + if ( $network !== false && is_numeric( $bits ) && $bits >= 0 && $bits <= 32 ) { + if ( $bits == 0 ) { + $network = 0; + } else { + $network &= ~( ( 1 << ( 32 - $bits ) ) - 1 ); + } + # Convert to unsigned + if ( $network < 0 ) { + $network += pow( 2, 32 ); + } + } else { + $network = false; + $bits = false; + } + + return [ $network, $bits ]; + } + + /** + * Given a string range in a number of formats, + * return the start and end of the range in hexadecimal. + * + * Formats are: + * 1.2.3.4/24 CIDR + * 1.2.3.4 - 1.2.3.5 Explicit range + * 1.2.3.4 Single IP + * + * 2001:0db8:85a3::7344/96 CIDR + * 2001:0db8:85a3::7344 - 2001:0db8:85a3::7344 Explicit range + * 2001:0db8:85a3::7344 Single IP + * @param string $range IP range + * @return array(string, string) + */ + public static function parseRange( $range ) { + // CIDR notation + if ( strpos( $range, '/' ) !== false ) { + if ( self::isIPv6( $range ) ) { + return self::parseRange6( $range ); + } + list( $network, $bits ) = self::parseCIDR( $range ); + if ( $network === false ) { + $start = $end = false; + } else { + $start = sprintf( '%08X', $network ); + $end = sprintf( '%08X', $network + pow( 2, ( 32 - $bits ) ) - 1 ); + } + // Explicit range + } elseif ( strpos( $range, '-' ) !== false ) { + list( $start, $end ) = array_map( 'trim', explode( '-', $range, 2 ) ); + if ( self::isIPv6( $start ) && self::isIPv6( $end ) ) { + return self::parseRange6( $range ); + } + if ( self::isIPv4( $start ) && self::isIPv4( $end ) ) { + $start = self::toHex( $start ); + $end = self::toHex( $end ); + if ( $start > $end ) { + $start = $end = false; + } + } else { + $start = $end = false; + } + } else { + # Single IP + $start = $end = self::toHex( $range ); + } + if ( $start === false || $end === false ) { + return [ false, false ]; + } else { + return [ $start, $end ]; + } + } + + /** + * Convert a network specification in IPv6 CIDR notation to an + * integer network and a number of bits + * + * @param string $range + * + * @return array(string, int) + */ + private static function parseCIDR6( $range ) { + # Explode into + $parts = explode( '/', IP::sanitizeIP( $range ), 2 ); + if ( count( $parts ) != 2 ) { + return [ false, false ]; + } + list( $network, $bits ) = $parts; + $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 = Wikimedia\base_convert( $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 + $network = Wikimedia\base_convert( $network, 2, 10 ); + } + } else { + $network = false; + $bits = false; + } + + return [ $network, (int)$bits ]; + } + + /** + * 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 + * 2001:0db8:85a3::7344 - 2001:0db8:85a3::7344 Explicit range + * 2001:0db8:85a3::7344/96 Single IP + * + * @param string $range + * + * @return array(string, string) + */ + private static function parseRange6( $range ) { + # Expand any IPv6 IP + $range = IP::sanitizeIP( $range ); + // CIDR notation... + if ( strpos( $range, '/' ) !== false ) { + list( $network, $bits ) = self::parseCIDR6( $range ); + if ( $network === false ) { + $start = $end = false; + } else { + $start = Wikimedia\base_convert( $network, 10, 16, 32, false ); + # Turn network to binary (again) + $end = Wikimedia\base_convert( $network, 10, 2, 128 ); + # Truncate the last (128-$bits) bits and replace them with ones + $end = str_pad( substr( $end, 0, $bits ), 128, 1, STR_PAD_RIGHT ); + # Convert to hex + $end = Wikimedia\base_convert( $end, 2, 16, 32, false ); + # see toHex() comment + $start = "v6-$start"; + $end = "v6-$end"; + } + // Explicit range notation... + } elseif ( strpos( $range, '-' ) !== false ) { + list( $start, $end ) = array_map( 'trim', explode( '-', $range, 2 ) ); + $start = self::toHex( $start ); + $end = self::toHex( $end ); + if ( $start > $end ) { + $start = $end = false; + } + } else { + # Single IP + $start = $end = self::toHex( $range ); + } + if ( $start === false || $end === false ) { + return [ false, false ]; + } else { + return [ $start, $end ]; + } + } + + /** + * Determine if a given IPv4/IPv6 address is in a given CIDR network + * + * @param string $addr The address to check against the given range. + * @param string $range The range to check the given address against. + * @return bool Whether or not the given address is in the given range. + * + * @note This can return unexpected results for invalid arguments! + * Make sure you pass a valid IP address and IP range. + */ + public static function isInRange( $addr, $range ) { + $hexIP = self::toHex( $addr ); + list( $start, $end ) = self::parseRange( $range ); + + return ( strcmp( $hexIP, $start ) >= 0 && + strcmp( $hexIP, $end ) <= 0 ); + } + + /** + * Determines if an IP address is a list of CIDR a.b.c.d/n ranges. + * + * @since 1.25 + * + * @param string $ip the IP to check + * @param array $ranges the IP ranges, each element a range + * + * @return bool true if the specified adress belongs to the specified range; otherwise, false. + */ + public static function isInRanges( $ip, $ranges ) { + foreach ( $ranges as $range ) { + if ( self::isInRange( $ip, $range ) ) { + return true; + } + } + return false; + } + + /** + * Convert some unusual representations of IPv4 addresses to their + * canonical dotted quad representation. + * + * This currently only checks a few IPV4-to-IPv6 related cases. More + * unusual representations may be added later. + * + * @param string $addr Something that might be an IP address + * @return string|null Valid dotted quad IPv4 address or null + */ + public static function canonicalize( $addr ) { + // remove zone info (bug 35738) + $addr = preg_replace( '/\%.*/', '', $addr ); + + if ( self::isValid( $addr ) ) { + return $addr; + } + // Turn mapped addresses from ::ce:ffff:1.2.3.4 to 1.2.3.4 + if ( strpos( $addr, ':' ) !== false && strpos( $addr, '.' ) !== false ) { + $addr = substr( $addr, strrpos( $addr, ':' ) + 1 ); + if ( self::isIPv4( $addr ) ) { + return $addr; + } + } + // IPv6 loopback address + $m = []; + if ( preg_match( '/^0*' . RE_IPV6_GAP . '1$/', $addr, $m ) ) { + return '127.0.0.1'; + } + // IPv4-mapped and IPv4-compatible IPv6 addresses + if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . '(' . RE_IP_ADD . ')$/i', $addr, $m ) ) { + return $m[1]; + } + if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . RE_IPV6_WORD . + ':' . RE_IPV6_WORD . '$/i', $addr, $m ) + ) { + return long2ip( ( hexdec( $m[1] ) << 16 ) + hexdec( $m[2] ) ); + } + + return null; // give up + } + + /** + * Gets rid of unneeded numbers in quad-dotted/octet IP strings + * For example, 127.111.113.151/24 -> 127.111.113.0/24 + * @param string $range IP address to normalize + * @return string + */ + public static function sanitizeRange( $range ) { + list( /*...*/, $bits ) = self::parseCIDR( $range ); + list( $start, /*...*/ ) = self::parseRange( $range ); + $start = self::formatHex( $start ); + if ( $bits === false ) { + return $start; // wasn't actually a range + } + + return "$start/$bits"; + } + + /** + * Returns the subnet of a given IP + * + * @param string $ip + * @return string|false + */ + public static function getSubnet( $ip ) { + $matches = []; + $subnet = false; + if ( IP::isIPv6( $ip ) ) { + $parts = IP::parseRange( "$ip/64" ); + $subnet = $parts[0]; + } elseif ( preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) { + // IPv4 + $subnet = $matches[1]; + } + return $subnet; + } +} diff --git a/includes/utils/IP.php b/includes/utils/IP.php deleted file mode 100644 index 21203a47ce..0000000000 --- a/includes/utils/IP.php +++ /dev/null @@ -1,744 +0,0 @@ -", Aaron Schulz - */ - -use IPSet\IPSet; - -// Some regex definition to "play" with IP address and IP address blocks - -// An IPv4 address is made of 4 bytes from x00 to xFF which is d0 to d255 -define( 'RE_IP_BYTE', '(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|0?[0-9]?[0-9])' ); -define( 'RE_IP_ADD', RE_IP_BYTE . '\.' . RE_IP_BYTE . '\.' . RE_IP_BYTE . '\.' . RE_IP_BYTE ); -// An IPv4 block is an IP address and a prefix (d1 to d32) -define( 'RE_IP_PREFIX', '(3[0-2]|[12]?\d)' ); -define( 'RE_IP_BLOCK', RE_IP_ADD . '\/' . RE_IP_PREFIX ); - -// An IPv6 address is made up of 8 words (each x0000 to xFFFF). -// However, the "::" abbreviation can be used on consecutive x0000 words. -define( 'RE_IPV6_WORD', '([0-9A-Fa-f]{1,4})' ); -define( 'RE_IPV6_PREFIX', '(12[0-8]|1[01][0-9]|[1-9]?\d)' ); -define( 'RE_IPV6_ADD', - '(?:' . // starts with "::" (including "::") - ':(?::|(?::' . RE_IPV6_WORD . '){1,7})' . - '|' . // ends with "::" (except "::") - RE_IPV6_WORD . '(?::' . RE_IPV6_WORD . '){0,6}::' . - '|' . // contains one "::" in the middle (the ^ makes the test fail if none found) - RE_IPV6_WORD . '(?::((?(-1)|:))?' . RE_IPV6_WORD . '){1,6}(?(-2)|^)' . - '|' . // contains no "::" - RE_IPV6_WORD . '(?::' . RE_IPV6_WORD . '){7}' . - ')' -); -// An IPv6 block is an IP address and a prefix (d1 to d128) -define( 'RE_IPV6_BLOCK', RE_IPV6_ADD . '\/' . RE_IPV6_PREFIX ); -// For IPv6 canonicalization (NOT for strict validation; these are quite lax!) -define( 'RE_IPV6_GAP', ':(?:0+:)*(?::(?:0+:)*)?' ); -define( 'RE_IPV6_V4_PREFIX', '0*' . RE_IPV6_GAP . '(?:ffff:)?' ); - -// This might be useful for regexps used elsewhere, matches any IPv4 or IPv6 address or network -define( 'IP_ADDRESS_STRING', - '(?:' . - RE_IP_ADD . '(?:\/' . RE_IP_PREFIX . ')?' . // IPv4 - '|' . - RE_IPV6_ADD . '(?:\/' . RE_IPV6_PREFIX . ')?' . // IPv6 - ')' -); - -/** - * A collection of public static functions to play with IP address - * and IP blocks. - */ -class IP { - /** @var IPSet */ - private static $proxyIpSet = null; - - /** - * Determine if a string is as valid IP address or network (CIDR prefix). - * SIIT IPv4-translated addresses are rejected. - * @note canonicalize() tries to convert translated addresses to IPv4. - * - * @param string $ip Possible IP address - * @return bool - */ - public static function isIPAddress( $ip ) { - return (bool)preg_match( '/^' . IP_ADDRESS_STRING . '$/', $ip ); - } - - /** - * Given a string, determine if it as valid IP in IPv6 only. - * @note Unlike isValid(), this looks for networks too. - * - * @param string $ip Possible IP address - * @return bool - */ - public static function isIPv6( $ip ) { - return (bool)preg_match( '/^' . RE_IPV6_ADD . '(?:\/' . RE_IPV6_PREFIX . ')?$/', $ip ); - } - - /** - * Given a string, determine if it as valid IP in IPv4 only. - * @note Unlike isValid(), this looks for networks too. - * - * @param string $ip Possible IP address - * @return bool - */ - public static function isIPv4( $ip ) { - return (bool)preg_match( '/^' . RE_IP_ADD . '(?:\/' . RE_IP_PREFIX . ')?$/', $ip ); - } - - /** - * Validate an IP address. Ranges are NOT considered valid. - * SIIT IPv4-translated addresses are rejected. - * @note canonicalize() tries to convert translated addresses to IPv4. - * - * @param string $ip - * @return bool True if it is valid - */ - public static function isValid( $ip ) { - return ( preg_match( '/^' . RE_IP_ADD . '$/', $ip ) - || preg_match( '/^' . RE_IPV6_ADD . '$/', $ip ) ); - } - - /** - * Validate an IP Block (valid address WITH a valid prefix). - * SIIT IPv4-translated addresses are rejected. - * @note canonicalize() tries to convert translated addresses to IPv4. - * - * @param string $ipblock - * @return bool True if it is valid - */ - public static function isValidBlock( $ipblock ) { - return ( preg_match( '/^' . RE_IPV6_BLOCK . '$/', $ipblock ) - || preg_match( '/^' . RE_IP_BLOCK . '$/', $ipblock ) ); - } - - /** - * Convert an IP into a verbose, uppercase, normalized form. - * Both IPv4 and IPv6 addresses are trimmed. Additionally, - * IPv6 addresses in octet notation are expanded to 8 words; - * IPv4 addresses have leading zeros, in each octet, removed. - * - * @param string $ip IP address in quad or octet form (CIDR or not). - * @return string - */ - public static function sanitizeIP( $ip ) { - $ip = trim( $ip ); - if ( $ip === '' ) { - return null; - } - /* If not an IP, just return trimmed value, since sanitizeIP() is called - * in a number of contexts where usernames are supplied as input. - */ - if ( !self::isIPAddress( $ip ) ) { - return $ip; - } - if ( self::isIPv4( $ip ) ) { - // Remove leading 0's from octet representation of IPv4 address - $ip = preg_replace( '/(?:^|(?<=\.))0+(?=[1-9]|0\.|0$)/', '', $ip ); - return $ip; - } - // Remove any whitespaces, convert to upper case - $ip = strtoupper( $ip ); - // Expand zero abbreviations - $abbrevPos = strpos( $ip, '::' ); - if ( $abbrevPos !== false ) { - // We know this is valid IPv6. Find the last index of the - // address before any CIDR number (e.g. "a:b:c::/24"). - $CIDRStart = strpos( $ip, "/" ); - $addressEnd = ( $CIDRStart !== false ) - ? $CIDRStart - 1 - : strlen( $ip ) - 1; - // If the '::' is at the beginning... - if ( $abbrevPos == 0 ) { - $repeat = '0:'; - $extra = ( $ip == '::' ) ? '0' : ''; // for the address '::' - $pad = 9; // 7+2 (due to '::') - // If the '::' is at the end... - } elseif ( $abbrevPos == ( $addressEnd - 1 ) ) { - $repeat = ':0'; - $extra = ''; - $pad = 9; // 7+2 (due to '::') - // If the '::' is in the middle... - } else { - $repeat = ':0'; - $extra = ':'; - $pad = 8; // 6+2 (due to '::') - } - $ip = str_replace( '::', - str_repeat( $repeat, $pad - substr_count( $ip, ':' ) ) . $extra, - $ip - ); - } - // Remove leading zeros from each bloc as needed - $ip = preg_replace( '/(^|:)0+(' . RE_IPV6_WORD . ')/', '$1$2', $ip ); - - return $ip; - } - - /** - * Prettify an IP for display to end users. - * This will make it more compact and lower-case. - * - * @param string $ip - * @return string - */ - public static function prettifyIP( $ip ) { - $ip = self::sanitizeIP( $ip ); // normalize (removes '::') - if ( self::isIPv6( $ip ) ) { - // Split IP into an address and a CIDR - if ( strpos( $ip, '/' ) !== false ) { - list( $ip, $cidr ) = explode( '/', $ip, 2 ); - } else { - list( $ip, $cidr ) = [ $ip, '' ]; - } - // Get the largest slice of words with multiple zeros - $offset = 0; - $longest = $longestPos = false; - while ( preg_match( - '!(?:^|:)0(?::0)+(?:$|:)!', $ip, $m, PREG_OFFSET_CAPTURE, $offset - ) ) { - list( $match, $pos ) = $m[0]; // full match - if ( strlen( $match ) > strlen( $longest ) ) { - $longest = $match; - $longestPos = $pos; - } - $offset = ( $pos + strlen( $match ) ); // advance - } - if ( $longest !== false ) { - // Replace this portion of the string with the '::' abbreviation - $ip = substr_replace( $ip, '::', $longestPos, strlen( $longest ) ); - } - // Add any CIDR back on - if ( $cidr !== '' ) { - $ip = "{$ip}/{$cidr}"; - } - // Convert to lower case to make it more readable - $ip = strtolower( $ip ); - } - - return $ip; - } - - /** - * Given a host/port string, like one might find in the host part of a URL - * per RFC 2732, split the hostname part and the port part and return an - * array with an element for each. If there is no port part, the array will - * have false in place of the port. If the string was invalid in some way, - * false is returned. - * - * This was easy with IPv4 and was generally done in an ad-hoc way, but - * with IPv6 it's somewhat more complicated due to the need to parse the - * square brackets and colons. - * - * A bare IPv6 address is accepted despite the lack of square brackets. - * - * @param string $both The string with the host and port - * @return array|false Array normally, false on certain failures - */ - public static function splitHostAndPort( $both ) { - if ( substr( $both, 0, 1 ) === '[' ) { - if ( preg_match( '/^\[(' . RE_IPV6_ADD . ')\](?::(?P\d+))?$/', $both, $m ) ) { - if ( isset( $m['port'] ) ) { - return [ $m[1], intval( $m['port'] ) ]; - } else { - return [ $m[1], false ]; - } - } else { - // Square bracket found but no IPv6 - return false; - } - } - $numColons = substr_count( $both, ':' ); - if ( $numColons >= 2 ) { - // Is it a bare IPv6 address? - if ( preg_match( '/^' . RE_IPV6_ADD . '$/', $both ) ) { - return [ $both, false ]; - } else { - // Not valid IPv6, but too many colons for anything else - return false; - } - } - if ( $numColons >= 1 ) { - // Host:port? - $bits = explode( ':', $both ); - if ( preg_match( '/^\d+/', $bits[1] ) ) { - return [ $bits[0], intval( $bits[1] ) ]; - } else { - // Not a valid port - return false; - } - } - - // Plain hostname - return [ $both, false ]; - } - - /** - * Given a host name and a port, combine them into host/port string like - * you might find in a URL. If the host contains a colon, wrap it in square - * brackets like in RFC 2732. If the port matches the default port, omit - * the port specification - * - * @param string $host - * @param int $port - * @param bool|int $defaultPort - * @return string - */ - public static function combineHostAndPort( $host, $port, $defaultPort = false ) { - if ( strpos( $host, ':' ) !== false ) { - $host = "[$host]"; - } - if ( $defaultPort !== false && $port == $defaultPort ) { - return $host; - } else { - return "$host:$port"; - } - } - - /** - * Convert an IPv4 or IPv6 hexadecimal representation back to readable format - * - * @param string $hex Number, with "v6-" prefix if it is IPv6 - * @return string Quad-dotted (IPv4) or octet notation (IPv6) - */ - public static function formatHex( $hex ) { - if ( substr( $hex, 0, 3 ) == 'v6-' ) { // IPv6 - return self::hexToOctet( substr( $hex, 3 ) ); - } else { // IPv4 - return self::hexToQuad( $hex ); - } - } - - /** - * Converts a hexadecimal number to an IPv6 address in octet notation - * - * @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 ) { - // Pad hex to 32 chars (128 bits) - $ip_hex = str_pad( strtoupper( $ip_hex ), 32, '0', STR_PAD_LEFT ); - // Separate into 8 words - $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; - } - - /** - * Converts a hexadecimal number to an IPv4 address in quad-dotted notation - * - * @param string $ip_hex Pure hex - * @return string (of format a.b.c.d) - */ - public static function hexToQuad( $ip_hex ) { - // Pad hex to 8 chars (32 bits) - $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_hex, $i * 2, 2 ), 16, 10 ); - } - - return $s; - } - - /** - * Determine if an IP address really is an IP address, and if it is public, - * i.e. not RFC 1918 or similar - * - * @param string $ip - * @return bool - */ - public static function isPublic( $ip ) { - static $privateSet = null; - if ( !$privateSet ) { - $privateSet = new IPSet( [ - '10.0.0.0/8', # RFC 1918 (private) - '172.16.0.0/12', # RFC 1918 (private) - '192.168.0.0/16', # RFC 1918 (private) - '0.0.0.0/8', # this network - '127.0.0.0/8', # loopback - 'fc00::/7', # RFC 4193 (local) - '0:0:0:0:0:0:0:1', # loopback - '169.254.0.0/16', # link-local - 'fe80::/10', # link-local - ] ); - } - return !$privateSet->match( $ip ); - } - - /** - * 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 - * function for an IPv6 address will be prefixed with "v6-", a non- - * hexadecimal string which sorts after the IPv4 addresses. - * - * @param string $ip Quad dotted/octet IP address. - * @return string|bool False on failure - */ - public static function toHex( $ip ) { - if ( self::isIPv6( $ip ) ) { - $n = 'v6-' . self::IPv6ToRawHex( $ip ); - } elseif ( self::isIPv4( $ip ) ) { - // T62035/T97897: An IP with leading 0's fails in ip2long sometimes (e.g. *.08), - // also double/triple 0 needs to be changed to just a single 0 for ip2long. - $ip = self::sanitizeIP( $ip ); - $n = ip2long( $ip ); - if ( $n < 0 ) { - $n += pow( 2, 32 ); - # On 32-bit platforms (and on Windows), 2^32 does not fit into an int, - # so $n becomes a float. We convert it to string instead. - if ( is_float( $n ) ) { - $n = (string)$n; - } - } - if ( $n !== false ) { - # Floating points can handle the conversion; faster than Wikimedia\base_convert() - $n = strtoupper( str_pad( base_convert( $n, 10, 16 ), 8, '0', STR_PAD_LEFT ) ); - } - } else { - $n = false; - } - - return $n; - } - - /** - * Given an IPv6 address in octet notation, returns a pure hex string. - * - * @param string $ip Octet ipv6 IP address. - * @return string|bool Pure hex (uppercase); false on failure - */ - private static function IPv6ToRawHex( $ip ) { - $ip = self::sanitizeIP( $ip ); - if ( !$ip ) { - return false; - } - $r_ip = ''; - foreach ( explode( ':', $ip ) as $v ) { - $r_ip .= str_pad( $v, 4, 0, STR_PAD_LEFT ); - } - - return $r_ip; - } - - /** - * Convert a network specification in CIDR notation - * to an integer network and a number of bits - * - * @param string $range IP with CIDR prefix - * @return array(int or string, int) - */ - public static function parseCIDR( $range ) { - if ( self::isIPv6( $range ) ) { - return self::parseCIDR6( $range ); - } - $parts = explode( '/', $range, 2 ); - if ( count( $parts ) != 2 ) { - return [ false, false ]; - } - list( $network, $bits ) = $parts; - $network = ip2long( $network ); - if ( $network !== false && is_numeric( $bits ) && $bits >= 0 && $bits <= 32 ) { - if ( $bits == 0 ) { - $network = 0; - } else { - $network &= ~( ( 1 << ( 32 - $bits ) ) - 1 ); - } - # Convert to unsigned - if ( $network < 0 ) { - $network += pow( 2, 32 ); - } - } else { - $network = false; - $bits = false; - } - - return [ $network, $bits ]; - } - - /** - * Given a string range in a number of formats, - * return the start and end of the range in hexadecimal. - * - * Formats are: - * 1.2.3.4/24 CIDR - * 1.2.3.4 - 1.2.3.5 Explicit range - * 1.2.3.4 Single IP - * - * 2001:0db8:85a3::7344/96 CIDR - * 2001:0db8:85a3::7344 - 2001:0db8:85a3::7344 Explicit range - * 2001:0db8:85a3::7344 Single IP - * @param string $range IP range - * @return array(string, string) - */ - public static function parseRange( $range ) { - // CIDR notation - if ( strpos( $range, '/' ) !== false ) { - if ( self::isIPv6( $range ) ) { - return self::parseRange6( $range ); - } - list( $network, $bits ) = self::parseCIDR( $range ); - if ( $network === false ) { - $start = $end = false; - } else { - $start = sprintf( '%08X', $network ); - $end = sprintf( '%08X', $network + pow( 2, ( 32 - $bits ) ) - 1 ); - } - // Explicit range - } elseif ( strpos( $range, '-' ) !== false ) { - list( $start, $end ) = array_map( 'trim', explode( '-', $range, 2 ) ); - if ( self::isIPv6( $start ) && self::isIPv6( $end ) ) { - return self::parseRange6( $range ); - } - if ( self::isIPv4( $start ) && self::isIPv4( $end ) ) { - $start = self::toHex( $start ); - $end = self::toHex( $end ); - if ( $start > $end ) { - $start = $end = false; - } - } else { - $start = $end = false; - } - } else { - # Single IP - $start = $end = self::toHex( $range ); - } - if ( $start === false || $end === false ) { - return [ false, false ]; - } else { - return [ $start, $end ]; - } - } - - /** - * Convert a network specification in IPv6 CIDR notation to an - * integer network and a number of bits - * - * @param string $range - * - * @return array(string, int) - */ - private static function parseCIDR6( $range ) { - # Explode into - $parts = explode( '/', IP::sanitizeIP( $range ), 2 ); - if ( count( $parts ) != 2 ) { - return [ false, false ]; - } - list( $network, $bits ) = $parts; - $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 = Wikimedia\base_convert( $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 - $network = Wikimedia\base_convert( $network, 2, 10 ); - } - } else { - $network = false; - $bits = false; - } - - return [ $network, (int)$bits ]; - } - - /** - * 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 - * 2001:0db8:85a3::7344 - 2001:0db8:85a3::7344 Explicit range - * 2001:0db8:85a3::7344/96 Single IP - * - * @param string $range - * - * @return array(string, string) - */ - private static function parseRange6( $range ) { - # Expand any IPv6 IP - $range = IP::sanitizeIP( $range ); - // CIDR notation... - if ( strpos( $range, '/' ) !== false ) { - list( $network, $bits ) = self::parseCIDR6( $range ); - if ( $network === false ) { - $start = $end = false; - } else { - $start = Wikimedia\base_convert( $network, 10, 16, 32, false ); - # Turn network to binary (again) - $end = Wikimedia\base_convert( $network, 10, 2, 128 ); - # Truncate the last (128-$bits) bits and replace them with ones - $end = str_pad( substr( $end, 0, $bits ), 128, 1, STR_PAD_RIGHT ); - # Convert to hex - $end = Wikimedia\base_convert( $end, 2, 16, 32, false ); - # see toHex() comment - $start = "v6-$start"; - $end = "v6-$end"; - } - // Explicit range notation... - } elseif ( strpos( $range, '-' ) !== false ) { - list( $start, $end ) = array_map( 'trim', explode( '-', $range, 2 ) ); - $start = self::toHex( $start ); - $end = self::toHex( $end ); - if ( $start > $end ) { - $start = $end = false; - } - } else { - # Single IP - $start = $end = self::toHex( $range ); - } - if ( $start === false || $end === false ) { - return [ false, false ]; - } else { - return [ $start, $end ]; - } - } - - /** - * Determine if a given IPv4/IPv6 address is in a given CIDR network - * - * @param string $addr The address to check against the given range. - * @param string $range The range to check the given address against. - * @return bool Whether or not the given address is in the given range. - * - * @note This can return unexpected results for invalid arguments! - * Make sure you pass a valid IP address and IP range. - */ - public static function isInRange( $addr, $range ) { - $hexIP = self::toHex( $addr ); - list( $start, $end ) = self::parseRange( $range ); - - return ( strcmp( $hexIP, $start ) >= 0 && - strcmp( $hexIP, $end ) <= 0 ); - } - - /** - * Determines if an IP address is a list of CIDR a.b.c.d/n ranges. - * - * @since 1.25 - * - * @param string $ip the IP to check - * @param array $ranges the IP ranges, each element a range - * - * @return bool true if the specified adress belongs to the specified range; otherwise, false. - */ - public static function isInRanges( $ip, $ranges ) { - foreach ( $ranges as $range ) { - if ( self::isInRange( $ip, $range ) ) { - return true; - } - } - return false; - } - - /** - * Convert some unusual representations of IPv4 addresses to their - * canonical dotted quad representation. - * - * This currently only checks a few IPV4-to-IPv6 related cases. More - * unusual representations may be added later. - * - * @param string $addr Something that might be an IP address - * @return string|null Valid dotted quad IPv4 address or null - */ - public static function canonicalize( $addr ) { - // remove zone info (bug 35738) - $addr = preg_replace( '/\%.*/', '', $addr ); - - if ( self::isValid( $addr ) ) { - return $addr; - } - // Turn mapped addresses from ::ce:ffff:1.2.3.4 to 1.2.3.4 - if ( strpos( $addr, ':' ) !== false && strpos( $addr, '.' ) !== false ) { - $addr = substr( $addr, strrpos( $addr, ':' ) + 1 ); - if ( self::isIPv4( $addr ) ) { - return $addr; - } - } - // IPv6 loopback address - $m = []; - if ( preg_match( '/^0*' . RE_IPV6_GAP . '1$/', $addr, $m ) ) { - return '127.0.0.1'; - } - // IPv4-mapped and IPv4-compatible IPv6 addresses - if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . '(' . RE_IP_ADD . ')$/i', $addr, $m ) ) { - return $m[1]; - } - if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . RE_IPV6_WORD . - ':' . RE_IPV6_WORD . '$/i', $addr, $m ) - ) { - return long2ip( ( hexdec( $m[1] ) << 16 ) + hexdec( $m[2] ) ); - } - - return null; // give up - } - - /** - * Gets rid of unneeded numbers in quad-dotted/octet IP strings - * For example, 127.111.113.151/24 -> 127.111.113.0/24 - * @param string $range IP address to normalize - * @return string - */ - public static function sanitizeRange( $range ) { - list( /*...*/, $bits ) = self::parseCIDR( $range ); - list( $start, /*...*/ ) = self::parseRange( $range ); - $start = self::formatHex( $start ); - if ( $bits === false ) { - return $start; // wasn't actually a range - } - - return "$start/$bits"; - } - - /** - * Returns the subnet of a given IP - * - * @param string $ip - * @return string|false - */ - public static function getSubnet( $ip ) { - $matches = []; - $subnet = false; - if ( IP::isIPv6( $ip ) ) { - $parts = IP::parseRange( "$ip/64" ); - $subnet = $parts[0]; - } elseif ( preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) { - // IPv4 - $subnet = $matches[1]; - } - return $subnet; - } -} diff --git a/tests/phpunit/includes/libs/IPTest.php b/tests/phpunit/includes/libs/IPTest.php new file mode 100644 index 0000000000..307652d938 --- /dev/null +++ b/tests/phpunit/includes/libs/IPTest.php @@ -0,0 +1,670 @@ +assertFalse( IP::isIPAddress( $val ), $desc ); + } + + /** + * Provide a list of things that aren't IP addresses + */ + public function provideInvalidIPs() { + return [ + [ false, 'Boolean false is not an IP' ], + [ true, 'Boolean true is not an IP' ], + [ '', 'Empty string is not an IP' ], + [ 'abc', 'Garbage IP string' ], + [ ':', 'Single ":" is not an IP' ], + [ '2001:0DB8::A:1::1', 'IPv6 with a double :: occurrence' ], + [ '2001:0DB8::A:1::', 'IPv6 with a double :: occurrence, last at end' ], + [ '::2001:0DB8::5:1', 'IPv6 with a double :: occurrence, firt at beginning' ], + [ '124.24.52', 'IPv4 not enough quads' ], + [ '24.324.52.13', 'IPv4 out of range' ], + [ '.24.52.13', 'IPv4 starts with period' ], + [ 'fc:100:300', 'IPv6 with only 3 words' ], + ]; + } + + /** + * @covers IP::isIPAddress + */ + public function testisIPAddress() { + $this->assertTrue( IP::isIPAddress( '::' ), 'RFC 4291 IPv6 Unspecified Address' ); + $this->assertTrue( IP::isIPAddress( '::1' ), 'RFC 4291 IPv6 Loopback Address' ); + $this->assertTrue( IP::isIPAddress( '74.24.52.13/20' ), 'IPv4 range' ); + $this->assertTrue( IP::isIPAddress( 'fc:100:a:d:1:e:ac:0/24' ), 'IPv6 range' ); + $this->assertTrue( IP::isIPAddress( 'fc::100:a:d:1:e:ac/96' ), 'IPv6 range with "::"' ); + + $validIPs = [ 'fc:100::', 'fc:100:a:d:1:e:ac::', 'fc::100', '::fc:100:a:d:1:e:ac', + '::fc', 'fc::100:a:d:1:e:ac', 'fc:100:a:d:1:e:ac:0', '124.24.52.13', '1.24.52.13' ]; + foreach ( $validIPs as $ip ) { + $this->assertTrue( IP::isIPAddress( $ip ), "$ip is a valid IP address" ); + } + } + + /** + * @covers IP::isIPv6 + */ + public function testisIPv6() { + $this->assertFalse( IP::isIPv6( ':fc:100::' ), 'IPv6 starting with lone ":"' ); + $this->assertFalse( IP::isIPv6( 'fc:100:::' ), 'IPv6 ending with a ":::"' ); + $this->assertFalse( IP::isIPv6( 'fc:300' ), 'IPv6 with only 2 words' ); + $this->assertFalse( IP::isIPv6( 'fc:100:300' ), 'IPv6 with only 3 words' ); + + $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 8 words ending with "::"' ); + $this->assertFalse( + IP::isIPv6( 'fc:100:a:d:1:e:ac:0:1::' ), + 'IPv6 with 9 words ending with "::"' + ); + + $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 words' ); + $this->assertFalse( IP::isIPv6( '::fc:100:a:d:1:e:ac:0:1' ), 'IPv6 with 9 words' ); + + $this->assertFalse( IP::isIPv6( ':fc::100' ), 'IPv6 starting with lone ":"' ); + $this->assertFalse( IP::isIPv6( 'fc::100:' ), 'IPv6 ending with lone ":"' ); + $this->assertFalse( IP::isIPv6( 'fc:::100' ), 'IPv6 with ":::" in the middle' ); + + $this->assertTrue( IP::isIPv6( 'fc::100' ), 'IPv6 with "::" and 2 words' ); + $this->assertTrue( IP::isIPv6( 'fc::100:a' ), 'IPv6 with "::" and 3 words' ); + $this->assertTrue( IP::isIPv6( 'fc::100:a:d' ), 'IPv6 with "::" and 4 words' ); + $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1' ), 'IPv6 with "::" and 5 words' ); + $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1:e' ), 'IPv6 with "::" and 6 words' ); + $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1:e:ac' ), 'IPv6 with "::" and 7 words' ); + $this->assertTrue( IP::isIPv6( '2001::df' ), 'IPv6 with "::" and 2 words' ); + $this->assertTrue( IP::isIPv6( '2001:5c0:1400:a::df' ), 'IPv6 with "::" and 5 words' ); + $this->assertTrue( IP::isIPv6( '2001:5c0:1400:a::df:2' ), 'IPv6 with "::" and 6 words' ); + + $this->assertFalse( IP::isIPv6( 'fc::100:a:d:1:e:ac:0' ), 'IPv6 with "::" and 8 words' ); + $this->assertFalse( IP::isIPv6( 'fc::100:a:d:1:e:ac:0:1' ), 'IPv6 with 9 words' ); + + $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1:e:ac:0' ) ); + } + + /** + * @covers IP::isIPv4 + * @dataProvider provideInvalidIPv4Addresses + */ + public function testisNotIPv4( $bogusIP, $desc ) { + $this->assertFalse( IP::isIPv4( $bogusIP ), $desc ); + } + + public function provideInvalidIPv4Addresses() { + return [ + [ false, 'Boolean false is not an IP' ], + [ true, 'Boolean true is not an IP' ], + [ '', 'Empty string is not an IP' ], + [ 'abc', 'Letters are not an IP' ], + [ ':', 'A colon is not an IP' ], + [ '124.24.52', 'IPv4 not enough quads' ], + [ '24.324.52.13', 'IPv4 out of range' ], + [ '.24.52.13', 'IPv4 starts with period' ], + ]; + } + + /** + * @covers IP::isIPv4 + * @dataProvider provideValidIPv4Address + */ + public function testIsIPv4( $ip, $desc ) { + $this->assertTrue( IP::isIPv4( $ip ), $desc ); + } + + /** + * Provide some IPv4 addresses and ranges + */ + public function provideValidIPv4Address() { + return [ + [ '124.24.52.13', 'Valid IPv4 address' ], + [ '1.24.52.13', 'Another valid IPv4 address' ], + [ '74.24.52.13/20', 'An IPv4 range' ], + ]; + } + + /** + * @covers IP::isValid + */ + public function testValidIPs() { + foreach ( range( 0, 255 ) as $i ) { + $a = sprintf( "%03d", $i ); + $b = sprintf( "%02d", $i ); + $c = sprintf( "%01d", $i ); + foreach ( array_unique( [ $a, $b, $c ] ) as $f ) { + $ip = "$f.$f.$f.$f"; + $this->assertTrue( IP::isValid( $ip ), "$ip is a valid IPv4 address" ); + } + } + foreach ( range( 0x0, 0xFFFF, 0xF ) as $i ) { + $a = sprintf( "%04x", $i ); + $b = sprintf( "%03x", $i ); + $c = sprintf( "%02x", $i ); + foreach ( array_unique( [ $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" ); + } + } + // test with some abbreviations + $this->assertFalse( IP::isValid( ':fc:100::' ), 'IPv6 starting with lone ":"' ); + $this->assertFalse( IP::isValid( 'fc:100:::' ), 'IPv6 ending with a ":::"' ); + $this->assertFalse( IP::isValid( 'fc:300' ), 'IPv6 with only 2 words' ); + $this->assertFalse( IP::isValid( 'fc:100:300' ), 'IPv6 with only 3 words' ); + + $this->assertTrue( IP::isValid( 'fc:100::' ) ); + $this->assertTrue( IP::isValid( 'fc:100:a:d:1:e::' ) ); + $this->assertTrue( IP::isValid( 'fc:100:a:d:1:e:ac::' ) ); + + $this->assertTrue( IP::isValid( 'fc::100' ), 'IPv6 with "::" and 2 words' ); + $this->assertTrue( IP::isValid( 'fc::100:a' ), 'IPv6 with "::" and 3 words' ); + $this->assertTrue( IP::isValid( '2001::df' ), 'IPv6 with "::" and 2 words' ); + $this->assertTrue( IP::isValid( '2001:5c0:1400:a::df' ), 'IPv6 with "::" and 5 words' ); + $this->assertTrue( IP::isValid( '2001:5c0:1400:a::df:2' ), 'IPv6 with "::" and 6 words' ); + $this->assertTrue( IP::isValid( 'fc::100:a:d:1' ), 'IPv6 with "::" and 5 words' ); + $this->assertTrue( IP::isValid( 'fc::100:a:d:1:e:ac' ), 'IPv6 with "::" and 7 words' ); + + $this->assertFalse( + IP::isValid( 'fc:100:a:d:1:e:ac:0::' ), + 'IPv6 with 8 words ending with "::"' + ); + $this->assertFalse( + IP::isValid( 'fc:100:a:d:1:e:ac:0:1::' ), + 'IPv6 with 9 words ending with "::"' + ); + } + + /** + * @covers IP::isValid + */ + public function testInvalidIPs() { + // Out of range... + foreach ( range( 256, 999 ) as $i ) { + $a = sprintf( "%03d", $i ); + $b = sprintf( "%02d", $i ); + $c = sprintf( "%01d", $i ); + foreach ( array_unique( [ $a, $b, $c ] ) as $f ) { + $ip = "$f.$f.$f.$f"; + $this->assertFalse( IP::isValid( $ip ), "$ip is not a valid IPv4 address" ); + } + } + foreach ( range( 'g', 'z' ) as $i ) { + $a = sprintf( "%04s", $i ); + $b = sprintf( "%03s", $i ); + $c = sprintf( "%02s", $i ); + foreach ( array_unique( [ $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" ); + } + } + // Have CIDR + $ipCIDRs = [ + '212.35.31.121/32', + '212.35.31.121/18', + '212.35.31.121/24', + '::ff:d:321:5/96', + 'ff::d3:321:5/116', + 'c:ff:12:1:ea:d:321:5/120', + ]; + foreach ( $ipCIDRs as $i ) { + $this->assertFalse( IP::isValid( $i ), + "$i is an invalid IP address because it is a block" ); + } + // Incomplete/garbage + $invalid = [ + 'www.xn--var-xla.net', + '216.17.184.G', + '216.17.184.1.', + '216.17.184', + '216.17.184.', + '256.17.184.1' + ]; + foreach ( $invalid as $i ) { + $this->assertFalse( IP::isValid( $i ), "$i is an invalid IP address" ); + } + } + + /** + * Provide some valid IP blocks + */ + public function provideValidBlocks() { + return [ + [ '116.17.184.5/32' ], + [ '0.17.184.5/30' ], + [ '16.17.184.1/24' ], + [ '30.242.52.14/1' ], + [ '10.232.52.13/8' ], + [ '30.242.52.14/0' ], + [ '::e:f:2001/96' ], + [ '::c:f:2001/128' ], + [ '::10:f:2001/70' ], + [ '::fe:f:2001/1' ], + [ '::6d:f:2001/8' ], + [ '::fe:f:2001/0' ], + ]; + } + + /** + * @covers IP::isValidBlock + * @dataProvider provideValidBlocks + */ + public function testValidBlocks( $block ) { + $this->assertTrue( IP::isValidBlock( $block ), "$block is a valid IP block" ); + } + + /** + * @covers IP::isValidBlock + * @dataProvider provideInvalidBlocks + */ + public function testInvalidBlocks( $invalid ) { + $this->assertFalse( IP::isValidBlock( $invalid ), "$invalid is not a valid IP block" ); + } + + public function provideInvalidBlocks() { + return [ + [ '116.17.184.5/33' ], + [ '0.17.184.5/130' ], + [ '16.17.184.1/-1' ], + [ '10.232.52.13/*' ], + [ '7.232.52.13/ab' ], + [ '11.232.52.13/' ], + [ '::e:f:2001/129' ], + [ '::c:f:2001/228' ], + [ '::10:f:2001/-1' ], + [ '::6d:f:2001/*' ], + [ '::86:f:2001/ab' ], + [ '::23:f:2001/' ], + ]; + } + + /** + * @covers IP::sanitizeIP + * @dataProvider provideSanitizeIP + */ + public function testSanitizeIP( $expected, $input ) { + $result = IP::sanitizeIP( $input ); + $this->assertEquals( $expected, $result ); + } + + /** + * Provider for IP::testSanitizeIP() + */ + public static function provideSanitizeIP() { + return [ + [ '0.0.0.0', '0.0.0.0' ], + [ '0.0.0.0', '00.00.00.00' ], + [ '0.0.0.0', '000.000.000.000' ], + [ '141.0.11.253', '141.000.011.253' ], + [ '1.2.4.5', '1.2.4.5' ], + [ '1.2.4.5', '01.02.04.05' ], + [ '1.2.4.5', '001.002.004.005' ], + [ '10.0.0.1', '010.0.000.1' ], + [ '80.72.250.4', '080.072.250.04' ], + [ 'Foo.1000.00', 'Foo.1000.00' ], + [ 'Bar.01', 'Bar.01' ], + [ 'Bar.010', 'Bar.010' ], + [ null, '' ], + [ null, ' ' ] + ]; + } + + /** + * @covers IP::toHex + * @dataProvider provideToHex + */ + public function testToHex( $expected, $input ) { + $result = IP::toHex( $input ); + $this->assertTrue( $result === false || is_string( $result ) ); + $this->assertEquals( $expected, $result ); + } + + /** + * Provider for IP::testToHex() + */ + public static function provideToHex() { + return [ + [ '00000001', '0.0.0.1' ], + [ '01020304', '1.2.3.4' ], + [ '7F000001', '127.0.0.1' ], + [ '80000000', '128.0.0.0' ], + [ 'DEADCAFE', '222.173.202.254' ], + [ 'FFFFFFFF', '255.255.255.255' ], + [ '8D000BFD', '141.000.11.253' ], + [ false, 'IN.VA.LI.D' ], + [ 'v6-00000000000000000000000000000001', '::1' ], + [ 'v6-20010DB885A3000000008A2E03707334', '2001:0db8:85a3:0000:0000:8a2e:0370:7334' ], + [ 'v6-20010DB885A3000000008A2E03707334', '2001:db8:85a3::8a2e:0370:7334' ], + [ false, 'IN:VA::LI:D' ], + [ false, ':::1' ] + ]; + } + + /** + * @covers IP::isPublic + * @dataProvider provideIsPublic + */ + public function testIsPublic( $expected, $input ) { + $result = IP::isPublic( $input ); + $this->assertEquals( $expected, $result ); + } + + /** + * Provider for IP::testIsPublic() + */ + public static function provideIsPublic() { + return [ + [ false, 'fc00::3' ], # RFC 4193 (local) + [ false, 'fc00::ff' ], # RFC 4193 (local) + [ false, '127.1.2.3' ], # loopback + [ false, '::1' ], # loopback + [ false, 'fe80::1' ], # link-local + [ false, '169.254.1.1' ], # link-local + [ false, '10.0.0.1' ], # RFC 1918 (private) + [ false, '172.16.0.1' ], # RFC 1918 (private) + [ false, '192.168.0.1' ], # RFC 1918 (private) + [ true, '2001:5c0:1000:a::133' ], # public + [ true, 'fc::3' ], # public + [ true, '00FC::' ] # public + ]; + } + + // Private wrapper used to test CIDR Parsing. + private function assertFalseCIDR( $CIDR, $msg = '' ) { + $ff = [ false, false ]; + $this->assertEquals( $ff, IP::parseCIDR( $CIDR ), $msg ); + } + + // Private wrapper to test network shifting using only dot notation + private function assertNet( $expected, $CIDR ) { + $parse = IP::parseCIDR( $CIDR ); + $this->assertEquals( $expected, long2ip( $parse[0] ), "network shifting $CIDR" ); + } + + /** + * @covers IP::hexToQuad + * @dataProvider provideIPsAndHexes + */ + public function testHexToQuad( $ip, $hex ) { + $this->assertEquals( $ip, IP::hexToQuad( $hex ) ); + } + + /** + * Provide some IP addresses and their equivalent hex representations + */ + public function provideIPsandHexes() { + return [ + [ '0.0.0.1', '00000001' ], + [ '255.0.0.0', 'FF000000' ], + [ '255.255.255.255', 'FFFFFFFF' ], + [ '10.188.222.255', '0ABCDEFF' ], + // hex not left-padded... + [ '0.0.0.0', '0' ], + [ '0.0.0.1', '1' ], + [ '0.0.0.255', 'FF' ], + [ '0.0.255.0', 'FF00' ], + ]; + } + + /** + * @covers IP::hexToOctet + * @dataProvider provideOctetsAndHexes + */ + public function testHexToOctet( $octet, $hex ) { + $this->assertEquals( $octet, IP::hexToOctet( $hex ) ); + } + + /** + * Provide some hex and octet representations of the same IPs + */ + public function provideOctetsAndHexes() { + return [ + [ '0:0:0:0:0:0:0:1', '00000000000000000000000000000001' ], + [ '0:0:0:0:0:0:FF:3', '00000000000000000000000000FF0003' ], + [ '0:0:0:0:0:0:FF00:6', '000000000000000000000000FF000006' ], + [ '0:0:0:0:0:0:FCCF:FAFF', '000000000000000000000000FCCFFAFF' ], + [ 'FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF', 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' ], + // hex not left-padded... + [ '0:0:0:0:0:0:0:0', '0' ], + [ '0:0:0:0:0:0:0:1', '1' ], + [ '0:0:0:0:0:0:0:FF', 'FF' ], + [ '0:0:0:0:0:0:0:FFD0', 'FFD0' ], + [ '0:0:0:0:0:0:FA00:0', 'FA000000' ], + [ '0:0:0:0:0:0:FCCF:FAFF', 'FCCFFAFF' ], + ]; + } + + /** + * IP::parseCIDR() returns an array containing a signed IP address + * representing the network mask and the bit mask. + * @covers IP::parseCIDR + */ + public function testCIDRParsing() { + $this->assertFalseCIDR( '192.0.2.0', "missing mask" ); + $this->assertFalseCIDR( '192.0.2.0/', "missing bitmask" ); + + // Verify if statement + $this->assertFalseCIDR( '256.0.0.0/32', "invalid net" ); + $this->assertFalseCIDR( '192.0.2.0/AA', "mask not numeric" ); + $this->assertFalseCIDR( '192.0.2.0/-1', "mask < 0" ); + $this->assertFalseCIDR( '192.0.2.0/33', "mask > 32" ); + + // Check internal logic + # 0 mask always result in array(0,0) + $this->assertEquals( [ 0, 0 ], IP::parseCIDR( '192.0.0.2/0' ) ); + $this->assertEquals( [ 0, 0 ], IP::parseCIDR( '0.0.0.0/0' ) ); + $this->assertEquals( [ 0, 0 ], IP::parseCIDR( '255.255.255.255/0' ) ); + + // @todo FIXME: Add more tests. + + # This part test network shifting + $this->assertNet( '192.0.0.0', '192.0.0.2/24' ); + $this->assertNet( '192.168.5.0', '192.168.5.13/24' ); + $this->assertNet( '10.0.0.160', '10.0.0.161/28' ); + $this->assertNet( '10.0.0.0', '10.0.0.3/28' ); + $this->assertNet( '10.0.0.0', '10.0.0.3/30' ); + $this->assertNet( '10.0.0.4', '10.0.0.4/30' ); + $this->assertNet( '172.17.32.0', '172.17.35.48/21' ); + $this->assertNet( '10.128.0.0', '10.135.0.0/9' ); + $this->assertNet( '134.0.0.0', '134.0.5.1/8' ); + } + + /** + * @covers IP::canonicalize + */ + public function testIPCanonicalizeOnValidIp() { + $this->assertEquals( '192.0.2.152', IP::canonicalize( '192.0.2.152' ), + 'Canonicalization of a valid IP returns it unchanged' ); + } + + /** + * @covers IP::canonicalize + */ + public function testIPCanonicalizeMappedAddress() { + $this->assertEquals( + '192.0.2.152', + IP::canonicalize( '::ffff:192.0.2.152' ) + ); + $this->assertEquals( + '192.0.2.152', + IP::canonicalize( '::192.0.2.152' ) + ); + } + + /** + * Issues there are most probably from IP::toHex() or IP::parseRange() + * @covers IP::isInRange + * @dataProvider provideIPsAndRanges + */ + public function testIPIsInRange( $expected, $addr, $range, $message = '' ) { + $this->assertEquals( + $expected, + IP::isInRange( $addr, $range ), + $message + ); + } + + /** Provider for testIPIsInRange() */ + public static function provideIPsAndRanges() { + # Format: (expected boolean, address, range, optional message) + return [ + # IPv4 + [ true, '192.0.2.0', '192.0.2.0/24', 'Network address' ], + [ true, '192.0.2.77', '192.0.2.0/24', 'Simple address' ], + [ true, '192.0.2.255', '192.0.2.0/24', 'Broadcast address' ], + + [ false, '0.0.0.0', '192.0.2.0/24' ], + [ false, '255.255.255', '192.0.2.0/24' ], + + # IPv6 + [ false, '::1', '2001:DB8::/32' ], + [ false, '::', '2001:DB8::/32' ], + [ false, 'FE80::1', '2001:DB8::/32' ], + + [ true, '2001:DB8::', '2001:DB8::/32' ], + [ true, '2001:0DB8::', '2001:DB8::/32' ], + [ true, '2001:DB8::1', '2001:DB8::/32' ], + [ true, '2001:0DB8::1', '2001:DB8::/32' ], + [ true, '2001:0DB8:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF', + '2001:DB8::/32' ], + + [ false, '2001:0DB8:F::', '2001:DB8::/96' ], + ]; + } + + /** + * Test for IP::splitHostAndPort(). + * @dataProvider provideSplitHostAndPort + */ + public function testSplitHostAndPort( $expected, $input, $description ) { + $this->assertEquals( $expected, IP::splitHostAndPort( $input ), $description ); + } + + /** + * Provider for IP::splitHostAndPort() + */ + public static function provideSplitHostAndPort() { + return [ + [ false, '[', 'Unclosed square bracket' ], + [ false, '[::', 'Unclosed square bracket 2' ], + [ [ '::', false ], '::', 'Bare IPv6 0' ], + [ [ '::1', false ], '::1', 'Bare IPv6 1' ], + [ [ '::', false ], '[::]', 'Bracketed IPv6 0' ], + [ [ '::1', false ], '[::1]', 'Bracketed IPv6 1' ], + [ [ '::1', 80 ], '[::1]:80', 'Bracketed IPv6 with port' ], + [ false, '::x', 'Double colon but no IPv6' ], + [ [ 'x', 80 ], 'x:80', 'Hostname and port' ], + [ false, 'x:x', 'Hostname and invalid port' ], + [ [ 'x', false ], 'x', 'Plain hostname' ] + ]; + } + + /** + * Test for IP::combineHostAndPort() + * @dataProvider provideCombineHostAndPort + */ + public function testCombineHostAndPort( $expected, $input, $description ) { + list( $host, $port, $defaultPort ) = $input; + $this->assertEquals( + $expected, + IP::combineHostAndPort( $host, $port, $defaultPort ), + $description ); + } + + /** + * Provider for IP::combineHostAndPort() + */ + public static function provideCombineHostAndPort() { + return [ + [ '[::1]', [ '::1', 2, 2 ], 'IPv6 default port' ], + [ '[::1]:2', [ '::1', 2, 3 ], 'IPv6 non-default port' ], + [ 'x', [ 'x', 2, 2 ], 'Normal default port' ], + [ 'x:2', [ 'x', 2, 3 ], 'Normal non-default port' ], + ]; + } + + /** + * Test for IP::sanitizeRange() + * @dataProvider provideIPCIDRs + */ + public function testSanitizeRange( $input, $expected, $description ) { + $this->assertEquals( $expected, IP::sanitizeRange( $input ), $description ); + } + + /** + * Provider for IP::testSanitizeRange() + */ + public static function provideIPCIDRs() { + return [ + [ '35.56.31.252/16', '35.56.0.0/16', 'IPv4 range' ], + [ '135.16.21.252/24', '135.16.21.0/24', 'IPv4 range' ], + [ '5.36.71.252/32', '5.36.71.252/32', 'IPv4 silly range' ], + [ '5.36.71.252', '5.36.71.252', 'IPv4 non-range' ], + [ '0:1:2:3:4:c5:f6:7/96', '0:1:2:3:4:C5:0:0/96', 'IPv6 range' ], + [ '0:1:2:3:4:5:6:7/120', '0:1:2:3:4:5:6:0/120', 'IPv6 range' ], + [ '0:e1:2:3:4:5:e6:7/128', '0:E1:2:3:4:5:E6:7/128', 'IPv6 silly range' ], + [ '0:c1:A2:3:4:5:c6:7', '0:C1:A2:3:4:5:C6:7', 'IPv6 non range' ], + ]; + } + + /** + * Test for IP::prettifyIP() + * @dataProvider provideIPsToPrettify + */ + public function testPrettifyIP( $ip, $prettified ) { + $this->assertEquals( $prettified, IP::prettifyIP( $ip ), "Prettify of $ip" ); + } + + /** + * Provider for IP::testPrettifyIP() + */ + public static function provideIPsToPrettify() { + return [ + [ '0:0:0:0:0:0:0:0', '::' ], + [ '0:0:0::0:0:0', '::' ], + [ '0:0:0:1:0:0:0:0', '0:0:0:1::' ], + [ '0:0::f', '::f' ], + [ '0::0:0:0:33:fef:b', '::33:fef:b' ], + [ '3f:535:0:0:0:0:e:fbb', '3f:535::e:fbb' ], + [ '0:0:fef:0:0:0:e:fbb', '0:0:fef::e:fbb' ], + [ 'abbc:2004::0:0:0:0', 'abbc:2004::' ], + [ 'cebc:2004:f:0:0:0:0:0', 'cebc:2004:f::' ], + [ '0:0:0:0:0:0:0:0/16', '::/16' ], + [ '0:0:0::0:0:0/64', '::/64' ], + [ '0:0::f/52', '::f/52' ], + [ '::0:0:33:fef:b/52', '::33:fef:b/52' ], + [ '3f:535:0:0:0:0:e:fbb/48', '3f:535::e:fbb/48' ], + [ '0:0:fef:0:0:0:e:fbb/96', '0:0:fef::e:fbb/96' ], + [ 'abbc:2004:0:0::0:0/40', 'abbc:2004::/40' ], + [ 'aebc:2004:f:0:0:0:0:0/80', 'aebc:2004:f::/80' ], + ]; + } +} diff --git a/tests/phpunit/includes/utils/IPTest.php b/tests/phpunit/includes/utils/IPTest.php deleted file mode 100644 index 5e0626b6bb..0000000000 --- a/tests/phpunit/includes/utils/IPTest.php +++ /dev/null @@ -1,670 +0,0 @@ -assertFalse( IP::isIPAddress( $val ), $desc ); - } - - /** - * Provide a list of things that aren't IP addresses - */ - public function provideInvalidIPs() { - return [ - [ false, 'Boolean false is not an IP' ], - [ true, 'Boolean true is not an IP' ], - [ '', 'Empty string is not an IP' ], - [ 'abc', 'Garbage IP string' ], - [ ':', 'Single ":" is not an IP' ], - [ '2001:0DB8::A:1::1', 'IPv6 with a double :: occurrence' ], - [ '2001:0DB8::A:1::', 'IPv6 with a double :: occurrence, last at end' ], - [ '::2001:0DB8::5:1', 'IPv6 with a double :: occurrence, firt at beginning' ], - [ '124.24.52', 'IPv4 not enough quads' ], - [ '24.324.52.13', 'IPv4 out of range' ], - [ '.24.52.13', 'IPv4 starts with period' ], - [ 'fc:100:300', 'IPv6 with only 3 words' ], - ]; - } - - /** - * @covers IP::isIPAddress - */ - public function testisIPAddress() { - $this->assertTrue( IP::isIPAddress( '::' ), 'RFC 4291 IPv6 Unspecified Address' ); - $this->assertTrue( IP::isIPAddress( '::1' ), 'RFC 4291 IPv6 Loopback Address' ); - $this->assertTrue( IP::isIPAddress( '74.24.52.13/20', 'IPv4 range' ) ); - $this->assertTrue( IP::isIPAddress( 'fc:100:a:d:1:e:ac:0/24' ), 'IPv6 range' ); - $this->assertTrue( IP::isIPAddress( 'fc::100:a:d:1:e:ac/96' ), 'IPv6 range with "::"' ); - - $validIPs = [ 'fc:100::', 'fc:100:a:d:1:e:ac::', 'fc::100', '::fc:100:a:d:1:e:ac', - '::fc', 'fc::100:a:d:1:e:ac', 'fc:100:a:d:1:e:ac:0', '124.24.52.13', '1.24.52.13' ]; - foreach ( $validIPs as $ip ) { - $this->assertTrue( IP::isIPAddress( $ip ), "$ip is a valid IP address" ); - } - } - - /** - * @covers IP::isIPv6 - */ - public function testisIPv6() { - $this->assertFalse( IP::isIPv6( ':fc:100::' ), 'IPv6 starting with lone ":"' ); - $this->assertFalse( IP::isIPv6( 'fc:100:::' ), 'IPv6 ending with a ":::"' ); - $this->assertFalse( IP::isIPv6( 'fc:300' ), 'IPv6 with only 2 words' ); - $this->assertFalse( IP::isIPv6( 'fc:100:300' ), 'IPv6 with only 3 words' ); - - $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 8 words ending with "::"' ); - $this->assertFalse( - IP::isIPv6( 'fc:100:a:d:1:e:ac:0:1::' ), - 'IPv6 with 9 words ending with "::"' - ); - - $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 words' ); - $this->assertFalse( IP::isIPv6( '::fc:100:a:d:1:e:ac:0:1' ), 'IPv6 with 9 words' ); - - $this->assertFalse( IP::isIPv6( ':fc::100' ), 'IPv6 starting with lone ":"' ); - $this->assertFalse( IP::isIPv6( 'fc::100:' ), 'IPv6 ending with lone ":"' ); - $this->assertFalse( IP::isIPv6( 'fc:::100' ), 'IPv6 with ":::" in the middle' ); - - $this->assertTrue( IP::isIPv6( 'fc::100' ), 'IPv6 with "::" and 2 words' ); - $this->assertTrue( IP::isIPv6( 'fc::100:a' ), 'IPv6 with "::" and 3 words' ); - $this->assertTrue( IP::isIPv6( 'fc::100:a:d', 'IPv6 with "::" and 4 words' ) ); - $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1' ), 'IPv6 with "::" and 5 words' ); - $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1:e' ), 'IPv6 with "::" and 6 words' ); - $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1:e:ac' ), 'IPv6 with "::" and 7 words' ); - $this->assertTrue( IP::isIPv6( '2001::df' ), 'IPv6 with "::" and 2 words' ); - $this->assertTrue( IP::isIPv6( '2001:5c0:1400:a::df' ), 'IPv6 with "::" and 5 words' ); - $this->assertTrue( IP::isIPv6( '2001:5c0:1400:a::df:2' ), 'IPv6 with "::" and 6 words' ); - - $this->assertFalse( IP::isIPv6( 'fc::100:a:d:1:e:ac:0' ), 'IPv6 with "::" and 8 words' ); - $this->assertFalse( IP::isIPv6( 'fc::100:a:d:1:e:ac:0:1' ), 'IPv6 with 9 words' ); - - $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1:e:ac:0' ) ); - } - - /** - * @covers IP::isIPv4 - * @dataProvider provideInvalidIPv4Addresses - */ - public function testisNotIPv4( $bogusIP, $desc ) { - $this->assertFalse( IP::isIPv4( $bogusIP ), $desc ); - } - - public function provideInvalidIPv4Addresses() { - return [ - [ false, 'Boolean false is not an IP' ], - [ true, 'Boolean true is not an IP' ], - [ '', 'Empty string is not an IP' ], - [ 'abc', 'Letters are not an IP' ], - [ ':', 'A colon is not an IP' ], - [ '124.24.52', 'IPv4 not enough quads' ], - [ '24.324.52.13', 'IPv4 out of range' ], - [ '.24.52.13', 'IPv4 starts with period' ], - ]; - } - - /** - * @covers IP::isIPv4 - * @dataProvider provideValidIPv4Address - */ - public function testIsIPv4( $ip, $desc ) { - $this->assertTrue( IP::isIPv4( $ip ), $desc ); - } - - /** - * Provide some IPv4 addresses and ranges - */ - public function provideValidIPv4Address() { - return [ - [ '124.24.52.13', 'Valid IPv4 address' ], - [ '1.24.52.13', 'Another valid IPv4 address' ], - [ '74.24.52.13/20', 'An IPv4 range' ], - ]; - } - - /** - * @covers IP::isValid - */ - public function testValidIPs() { - foreach ( range( 0, 255 ) as $i ) { - $a = sprintf( "%03d", $i ); - $b = sprintf( "%02d", $i ); - $c = sprintf( "%01d", $i ); - foreach ( array_unique( [ $a, $b, $c ] ) as $f ) { - $ip = "$f.$f.$f.$f"; - $this->assertTrue( IP::isValid( $ip ), "$ip is a valid IPv4 address" ); - } - } - foreach ( range( 0x0, 0xFFFF, 0xF ) as $i ) { - $a = sprintf( "%04x", $i ); - $b = sprintf( "%03x", $i ); - $c = sprintf( "%02x", $i ); - foreach ( array_unique( [ $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" ); - } - } - // test with some abbreviations - $this->assertFalse( IP::isValid( ':fc:100::' ), 'IPv6 starting with lone ":"' ); - $this->assertFalse( IP::isValid( 'fc:100:::' ), 'IPv6 ending with a ":::"' ); - $this->assertFalse( IP::isValid( 'fc:300' ), 'IPv6 with only 2 words' ); - $this->assertFalse( IP::isValid( 'fc:100:300' ), 'IPv6 with only 3 words' ); - - $this->assertTrue( IP::isValid( 'fc:100::' ) ); - $this->assertTrue( IP::isValid( 'fc:100:a:d:1:e::' ) ); - $this->assertTrue( IP::isValid( 'fc:100:a:d:1:e:ac::' ) ); - - $this->assertTrue( IP::isValid( 'fc::100' ), 'IPv6 with "::" and 2 words' ); - $this->assertTrue( IP::isValid( 'fc::100:a' ), 'IPv6 with "::" and 3 words' ); - $this->assertTrue( IP::isValid( '2001::df' ), 'IPv6 with "::" and 2 words' ); - $this->assertTrue( IP::isValid( '2001:5c0:1400:a::df' ), 'IPv6 with "::" and 5 words' ); - $this->assertTrue( IP::isValid( '2001:5c0:1400:a::df:2' ), 'IPv6 with "::" and 6 words' ); - $this->assertTrue( IP::isValid( 'fc::100:a:d:1' ), 'IPv6 with "::" and 5 words' ); - $this->assertTrue( IP::isValid( 'fc::100:a:d:1:e:ac' ), 'IPv6 with "::" and 7 words' ); - - $this->assertFalse( - IP::isValid( 'fc:100:a:d:1:e:ac:0::' ), - 'IPv6 with 8 words ending with "::"' - ); - $this->assertFalse( - IP::isValid( 'fc:100:a:d:1:e:ac:0:1::' ), - 'IPv6 with 9 words ending with "::"' - ); - } - - /** - * @covers IP::isValid - */ - public function testInvalidIPs() { - // Out of range... - foreach ( range( 256, 999 ) as $i ) { - $a = sprintf( "%03d", $i ); - $b = sprintf( "%02d", $i ); - $c = sprintf( "%01d", $i ); - foreach ( array_unique( [ $a, $b, $c ] ) as $f ) { - $ip = "$f.$f.$f.$f"; - $this->assertFalse( IP::isValid( $ip ), "$ip is not a valid IPv4 address" ); - } - } - foreach ( range( 'g', 'z' ) as $i ) { - $a = sprintf( "%04s", $i ); - $b = sprintf( "%03s", $i ); - $c = sprintf( "%02s", $i ); - foreach ( array_unique( [ $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" ); - } - } - // Have CIDR - $ipCIDRs = [ - '212.35.31.121/32', - '212.35.31.121/18', - '212.35.31.121/24', - '::ff:d:321:5/96', - 'ff::d3:321:5/116', - 'c:ff:12:1:ea:d:321:5/120', - ]; - foreach ( $ipCIDRs as $i ) { - $this->assertFalse( IP::isValid( $i ), - "$i is an invalid IP address because it is a block" ); - } - // Incomplete/garbage - $invalid = [ - 'www.xn--var-xla.net', - '216.17.184.G', - '216.17.184.1.', - '216.17.184', - '216.17.184.', - '256.17.184.1' - ]; - foreach ( $invalid as $i ) { - $this->assertFalse( IP::isValid( $i ), "$i is an invalid IP address" ); - } - } - - /** - * Provide some valid IP blocks - */ - public function provideValidBlocks() { - return [ - [ '116.17.184.5/32' ], - [ '0.17.184.5/30' ], - [ '16.17.184.1/24' ], - [ '30.242.52.14/1' ], - [ '10.232.52.13/8' ], - [ '30.242.52.14/0' ], - [ '::e:f:2001/96' ], - [ '::c:f:2001/128' ], - [ '::10:f:2001/70' ], - [ '::fe:f:2001/1' ], - [ '::6d:f:2001/8' ], - [ '::fe:f:2001/0' ], - ]; - } - - /** - * @covers IP::isValidBlock - * @dataProvider provideValidBlocks - */ - public function testValidBlocks( $block ) { - $this->assertTrue( IP::isValidBlock( $block ), "$block is a valid IP block" ); - } - - /** - * @covers IP::isValidBlock - * @dataProvider provideInvalidBlocks - */ - public function testInvalidBlocks( $invalid ) { - $this->assertFalse( IP::isValidBlock( $invalid ), "$invalid is not a valid IP block" ); - } - - public function provideInvalidBlocks() { - return [ - [ '116.17.184.5/33' ], - [ '0.17.184.5/130' ], - [ '16.17.184.1/-1' ], - [ '10.232.52.13/*' ], - [ '7.232.52.13/ab' ], - [ '11.232.52.13/' ], - [ '::e:f:2001/129' ], - [ '::c:f:2001/228' ], - [ '::10:f:2001/-1' ], - [ '::6d:f:2001/*' ], - [ '::86:f:2001/ab' ], - [ '::23:f:2001/' ], - ]; - } - - /** - * @covers IP::sanitizeIP - * @dataProvider provideSanitizeIP - */ - public function testSanitizeIP( $expected, $input ) { - $result = IP::sanitizeIP( $input ); - $this->assertEquals( $expected, $result ); - } - - /** - * Provider for IP::testSanitizeIP() - */ - public static function provideSanitizeIP() { - return [ - [ '0.0.0.0', '0.0.0.0' ], - [ '0.0.0.0', '00.00.00.00' ], - [ '0.0.0.0', '000.000.000.000' ], - [ '141.0.11.253', '141.000.011.253' ], - [ '1.2.4.5', '1.2.4.5' ], - [ '1.2.4.5', '01.02.04.05' ], - [ '1.2.4.5', '001.002.004.005' ], - [ '10.0.0.1', '010.0.000.1' ], - [ '80.72.250.4', '080.072.250.04' ], - [ 'Foo.1000.00', 'Foo.1000.00' ], - [ 'Bar.01', 'Bar.01' ], - [ 'Bar.010', 'Bar.010' ], - [ null, '' ], - [ null, ' ' ] - ]; - } - - /** - * @covers IP::toHex - * @dataProvider provideToHex - */ - public function testToHex( $expected, $input ) { - $result = IP::toHex( $input ); - $this->assertTrue( $result === false || is_string( $result ) ); - $this->assertEquals( $expected, $result ); - } - - /** - * Provider for IP::testToHex() - */ - public static function provideToHex() { - return [ - [ '00000001', '0.0.0.1' ], - [ '01020304', '1.2.3.4' ], - [ '7F000001', '127.0.0.1' ], - [ '80000000', '128.0.0.0' ], - [ 'DEADCAFE', '222.173.202.254' ], - [ 'FFFFFFFF', '255.255.255.255' ], - [ '8D000BFD', '141.000.11.253' ], - [ false, 'IN.VA.LI.D' ], - [ 'v6-00000000000000000000000000000001', '::1' ], - [ 'v6-20010DB885A3000000008A2E03707334', '2001:0db8:85a3:0000:0000:8a2e:0370:7334' ], - [ 'v6-20010DB885A3000000008A2E03707334', '2001:db8:85a3::8a2e:0370:7334' ], - [ false, 'IN:VA::LI:D' ], - [ false, ':::1' ] - ]; - } - - /** - * @covers IP::isPublic - * @dataProvider provideIsPublic - */ - public function testIsPublic( $expected, $input ) { - $result = IP::isPublic( $input ); - $this->assertEquals( $expected, $result ); - } - - /** - * Provider for IP::testIsPublic() - */ - public static function provideIsPublic() { - return [ - [ false, 'fc00::3' ], # RFC 4193 (local) - [ false, 'fc00::ff' ], # RFC 4193 (local) - [ false, '127.1.2.3' ], # loopback - [ false, '::1' ], # loopback - [ false, 'fe80::1' ], # link-local - [ false, '169.254.1.1' ], # link-local - [ false, '10.0.0.1' ], # RFC 1918 (private) - [ false, '172.16.0.1' ], # RFC 1918 (private) - [ false, '192.168.0.1' ], # RFC 1918 (private) - [ true, '2001:5c0:1000:a::133' ], # public - [ true, 'fc::3' ], # public - [ true, '00FC::' ] # public - ]; - } - - // Private wrapper used to test CIDR Parsing. - private function assertFalseCIDR( $CIDR, $msg = '' ) { - $ff = [ false, false ]; - $this->assertEquals( $ff, IP::parseCIDR( $CIDR ), $msg ); - } - - // Private wrapper to test network shifting using only dot notation - private function assertNet( $expected, $CIDR ) { - $parse = IP::parseCIDR( $CIDR ); - $this->assertEquals( $expected, long2ip( $parse[0] ), "network shifting $CIDR" ); - } - - /** - * @covers IP::hexToQuad - * @dataProvider provideIPsAndHexes - */ - public function testHexToQuad( $ip, $hex ) { - $this->assertEquals( $ip, IP::hexToQuad( $hex ) ); - } - - /** - * Provide some IP addresses and their equivalent hex representations - */ - public function provideIPsandHexes() { - return [ - [ '0.0.0.1', '00000001' ], - [ '255.0.0.0', 'FF000000' ], - [ '255.255.255.255', 'FFFFFFFF' ], - [ '10.188.222.255', '0ABCDEFF' ], - // hex not left-padded... - [ '0.0.0.0', '0' ], - [ '0.0.0.1', '1' ], - [ '0.0.0.255', 'FF' ], - [ '0.0.255.0', 'FF00' ], - ]; - } - - /** - * @covers IP::hexToOctet - * @dataProvider provideOctetsAndHexes - */ - public function testHexToOctet( $octet, $hex ) { - $this->assertEquals( $octet, IP::hexToOctet( $hex ) ); - } - - /** - * Provide some hex and octet representations of the same IPs - */ - public function provideOctetsAndHexes() { - return [ - [ '0:0:0:0:0:0:0:1', '00000000000000000000000000000001' ], - [ '0:0:0:0:0:0:FF:3', '00000000000000000000000000FF0003' ], - [ '0:0:0:0:0:0:FF00:6', '000000000000000000000000FF000006' ], - [ '0:0:0:0:0:0:FCCF:FAFF', '000000000000000000000000FCCFFAFF' ], - [ 'FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF', 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' ], - // hex not left-padded... - [ '0:0:0:0:0:0:0:0', '0' ], - [ '0:0:0:0:0:0:0:1', '1' ], - [ '0:0:0:0:0:0:0:FF', 'FF' ], - [ '0:0:0:0:0:0:0:FFD0', 'FFD0' ], - [ '0:0:0:0:0:0:FA00:0', 'FA000000' ], - [ '0:0:0:0:0:0:FCCF:FAFF', 'FCCFFAFF' ], - ]; - } - - /** - * IP::parseCIDR() returns an array containing a signed IP address - * representing the network mask and the bit mask. - * @covers IP::parseCIDR - */ - public function testCIDRParsing() { - $this->assertFalseCIDR( '192.0.2.0', "missing mask" ); - $this->assertFalseCIDR( '192.0.2.0/', "missing bitmask" ); - - // Verify if statement - $this->assertFalseCIDR( '256.0.0.0/32', "invalid net" ); - $this->assertFalseCIDR( '192.0.2.0/AA', "mask not numeric" ); - $this->assertFalseCIDR( '192.0.2.0/-1', "mask < 0" ); - $this->assertFalseCIDR( '192.0.2.0/33', "mask > 32" ); - - // Check internal logic - # 0 mask always result in array(0,0) - $this->assertEquals( [ 0, 0 ], IP::parseCIDR( '192.0.0.2/0' ) ); - $this->assertEquals( [ 0, 0 ], IP::parseCIDR( '0.0.0.0/0' ) ); - $this->assertEquals( [ 0, 0 ], IP::parseCIDR( '255.255.255.255/0' ) ); - - // @todo FIXME: Add more tests. - - # This part test network shifting - $this->assertNet( '192.0.0.0', '192.0.0.2/24' ); - $this->assertNet( '192.168.5.0', '192.168.5.13/24' ); - $this->assertNet( '10.0.0.160', '10.0.0.161/28' ); - $this->assertNet( '10.0.0.0', '10.0.0.3/28' ); - $this->assertNet( '10.0.0.0', '10.0.0.3/30' ); - $this->assertNet( '10.0.0.4', '10.0.0.4/30' ); - $this->assertNet( '172.17.32.0', '172.17.35.48/21' ); - $this->assertNet( '10.128.0.0', '10.135.0.0/9' ); - $this->assertNet( '134.0.0.0', '134.0.5.1/8' ); - } - - /** - * @covers IP::canonicalize - */ - public function testIPCanonicalizeOnValidIp() { - $this->assertEquals( '192.0.2.152', IP::canonicalize( '192.0.2.152' ), - 'Canonicalization of a valid IP returns it unchanged' ); - } - - /** - * @covers IP::canonicalize - */ - public function testIPCanonicalizeMappedAddress() { - $this->assertEquals( - '192.0.2.152', - IP::canonicalize( '::ffff:192.0.2.152' ) - ); - $this->assertEquals( - '192.0.2.152', - IP::canonicalize( '::192.0.2.152' ) - ); - } - - /** - * Issues there are most probably from IP::toHex() or IP::parseRange() - * @covers IP::isInRange - * @dataProvider provideIPsAndRanges - */ - public function testIPIsInRange( $expected, $addr, $range, $message = '' ) { - $this->assertEquals( - $expected, - IP::isInRange( $addr, $range ), - $message - ); - } - - /** Provider for testIPIsInRange() */ - public static function provideIPsAndRanges() { - # Format: (expected boolean, address, range, optional message) - return [ - # IPv4 - [ true, '192.0.2.0', '192.0.2.0/24', 'Network address' ], - [ true, '192.0.2.77', '192.0.2.0/24', 'Simple address' ], - [ true, '192.0.2.255', '192.0.2.0/24', 'Broadcast address' ], - - [ false, '0.0.0.0', '192.0.2.0/24' ], - [ false, '255.255.255', '192.0.2.0/24' ], - - # IPv6 - [ false, '::1', '2001:DB8::/32' ], - [ false, '::', '2001:DB8::/32' ], - [ false, 'FE80::1', '2001:DB8::/32' ], - - [ true, '2001:DB8::', '2001:DB8::/32' ], - [ true, '2001:0DB8::', '2001:DB8::/32' ], - [ true, '2001:DB8::1', '2001:DB8::/32' ], - [ true, '2001:0DB8::1', '2001:DB8::/32' ], - [ true, '2001:0DB8:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF', - '2001:DB8::/32' ], - - [ false, '2001:0DB8:F::', '2001:DB8::/96' ], - ]; - } - - /** - * Test for IP::splitHostAndPort(). - * @dataProvider provideSplitHostAndPort - */ - public function testSplitHostAndPort( $expected, $input, $description ) { - $this->assertEquals( $expected, IP::splitHostAndPort( $input ), $description ); - } - - /** - * Provider for IP::splitHostAndPort() - */ - public static function provideSplitHostAndPort() { - return [ - [ false, '[', 'Unclosed square bracket' ], - [ false, '[::', 'Unclosed square bracket 2' ], - [ [ '::', false ], '::', 'Bare IPv6 0' ], - [ [ '::1', false ], '::1', 'Bare IPv6 1' ], - [ [ '::', false ], '[::]', 'Bracketed IPv6 0' ], - [ [ '::1', false ], '[::1]', 'Bracketed IPv6 1' ], - [ [ '::1', 80 ], '[::1]:80', 'Bracketed IPv6 with port' ], - [ false, '::x', 'Double colon but no IPv6' ], - [ [ 'x', 80 ], 'x:80', 'Hostname and port' ], - [ false, 'x:x', 'Hostname and invalid port' ], - [ [ 'x', false ], 'x', 'Plain hostname' ] - ]; - } - - /** - * Test for IP::combineHostAndPort() - * @dataProvider provideCombineHostAndPort - */ - public function testCombineHostAndPort( $expected, $input, $description ) { - list( $host, $port, $defaultPort ) = $input; - $this->assertEquals( - $expected, - IP::combineHostAndPort( $host, $port, $defaultPort ), - $description ); - } - - /** - * Provider for IP::combineHostAndPort() - */ - public static function provideCombineHostAndPort() { - return [ - [ '[::1]', [ '::1', 2, 2 ], 'IPv6 default port' ], - [ '[::1]:2', [ '::1', 2, 3 ], 'IPv6 non-default port' ], - [ 'x', [ 'x', 2, 2 ], 'Normal default port' ], - [ 'x:2', [ 'x', 2, 3 ], 'Normal non-default port' ], - ]; - } - - /** - * Test for IP::sanitizeRange() - * @dataProvider provideIPCIDRs - */ - public function testSanitizeRange( $input, $expected, $description ) { - $this->assertEquals( $expected, IP::sanitizeRange( $input ), $description ); - } - - /** - * Provider for IP::testSanitizeRange() - */ - public static function provideIPCIDRs() { - return [ - [ '35.56.31.252/16', '35.56.0.0/16', 'IPv4 range' ], - [ '135.16.21.252/24', '135.16.21.0/24', 'IPv4 range' ], - [ '5.36.71.252/32', '5.36.71.252/32', 'IPv4 silly range' ], - [ '5.36.71.252', '5.36.71.252', 'IPv4 non-range' ], - [ '0:1:2:3:4:c5:f6:7/96', '0:1:2:3:4:C5:0:0/96', 'IPv6 range' ], - [ '0:1:2:3:4:5:6:7/120', '0:1:2:3:4:5:6:0/120', 'IPv6 range' ], - [ '0:e1:2:3:4:5:e6:7/128', '0:E1:2:3:4:5:E6:7/128', 'IPv6 silly range' ], - [ '0:c1:A2:3:4:5:c6:7', '0:C1:A2:3:4:5:C6:7', 'IPv6 non range' ], - ]; - } - - /** - * Test for IP::prettifyIP() - * @dataProvider provideIPsToPrettify - */ - public function testPrettifyIP( $ip, $prettified ) { - $this->assertEquals( $prettified, IP::prettifyIP( $ip ), "Prettify of $ip" ); - } - - /** - * Provider for IP::testPrettifyIP() - */ - public static function provideIPsToPrettify() { - return [ - [ '0:0:0:0:0:0:0:0', '::' ], - [ '0:0:0::0:0:0', '::' ], - [ '0:0:0:1:0:0:0:0', '0:0:0:1::' ], - [ '0:0::f', '::f' ], - [ '0::0:0:0:33:fef:b', '::33:fef:b' ], - [ '3f:535:0:0:0:0:e:fbb', '3f:535::e:fbb' ], - [ '0:0:fef:0:0:0:e:fbb', '0:0:fef::e:fbb' ], - [ 'abbc:2004::0:0:0:0', 'abbc:2004::' ], - [ 'cebc:2004:f:0:0:0:0:0', 'cebc:2004:f::' ], - [ '0:0:0:0:0:0:0:0/16', '::/16' ], - [ '0:0:0::0:0:0/64', '::/64' ], - [ '0:0::f/52', '::f/52' ], - [ '::0:0:33:fef:b/52', '::33:fef:b/52' ], - [ '3f:535:0:0:0:0:e:fbb/48', '3f:535::e:fbb/48' ], - [ '0:0:fef:0:0:0:e:fbb/96', '0:0:fef::e:fbb/96' ], - [ 'abbc:2004:0:0::0:0/40', 'abbc:2004::/40' ], - [ 'aebc:2004:f:0:0:0:0:0/80', 'aebc:2004:f::/80' ], - ]; - } -}