Also fix some misplaced parenthesis in IPTest.
Change-Id: I84d6120c49f733ec45e7e0005259871808b7568b
'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',
--- /dev/null
+<?php
+/**
+ * Functions and constants to play with IP addresses and ranges
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Antoine Musso "<hashar at free dot fr>", 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<port>\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 <expanded IP,range>
+ $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;
+ }
+}
+++ /dev/null
-<?php
-/**
- * Functions and constants to play with IP addresses and ranges
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @author Antoine Musso "<hashar at free dot fr>", 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<port>\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 <expanded IP,range>
- $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;
- }
-}
--- /dev/null
+<?php
+/**
+ * Tests for IP validity functions.
+ *
+ * Ported from /t/inc/IP.t by avar.
+ *
+ * @group IP
+ * @todo Test methods in this call should be split into a method and a
+ * dataprovider.
+ */
+
+class IPTest extends PHPUnit_Framework_TestCase {
+ /**
+ * @covers IP::isIPAddress
+ * @dataProvider provideInvalidIPs
+ */
+ public function isNotIPAddress( $val, $desc ) {
+ $this->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' ],
+ ];
+ }
+}
+++ /dev/null
-<?php
-/**
- * Tests for IP validity functions.
- *
- * Ported from /t/inc/IP.t by avar.
- *
- * @group IP
- * @todo Test methods in this call should be split into a method and a
- * dataprovider.
- */
-
-class IPTest extends PHPUnit_Framework_TestCase {
- /**
- * @covers IP::isIPAddress
- * @dataProvider provideInvalidIPs
- */
- public function isNotIPAddress( $val, $desc ) {
- $this->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' ],
- ];
- }
-}