faf09a0ebe951dbb182944031500a1865f0060d2
3 * Collection of public static functions to play with IP address
6 * @Author "Ashar Voultoiz" <hashar@altern.org>
7 * @License GPL v2 or later
10 // Some regex definition to "play" with IP address and IP address blocks
12 // An IP is made of 4 bytes from x00 to xFF which is d0 to d255
13 define( 'RE_IP_BYTE', '(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|0?[0-9]?[0-9])');
14 define( 'RE_IP_ADD' , RE_IP_BYTE
. '\.' . RE_IP_BYTE
. '\.' . RE_IP_BYTE
. '\.' . RE_IP_BYTE
);
15 // An IP block is an IP address and a prefix (d1 to d32)
16 define( 'RE_IP_PREFIX', '(3[0-2]|[12]?\d)');
17 define( 'RE_IP_BLOCK', RE_IP_ADD
. '\/' . RE_IP_PREFIX
);
18 // For IPv6 canonicalization (NOT for strict validation; these are quite lax!)
19 define( 'RE_IPV6_WORD', '([0-9A-Fa-f]{1,4})' );
20 define( 'RE_IPV6_GAP', ':(?:0+:)*(?::(?:0+:)*)?' );
21 define( 'RE_IPV6_V4_PREFIX', '0*' . RE_IPV6_GAP
. '(?:ffff:)?' );
22 // An IPv6 IP is made up of 8 octeds. However abbreviations like "::" can be used. This is lax!
23 define( 'RE_IPV6_ADD', RE_IPV6_WORD
. '(::$|' . RE_IPV6_GAP
. RE_IPV6_WORD
. '){1,7}' );
28 * Given an IP address in dotted-quad notation, returns an IPv6 octet.
29 * See http://www.answers.com/topic/ipv4-compatible-address
30 * IPs with the first 92 bits as zeros are reserved from IPv6
31 * @param $ip quad-dotted IP address.
34 public function IPv4toIPv6( $ip ) {
35 if ( !$ip ) return null;
36 // Convert only if needed
37 if ( strpos($ip,':') !==false ) return $ip;
39 if ( strpos( $ip, '/' ) !== false ) {
40 $parts = explode( '/', $ip, 2 );
41 if ( count( $parts ) != 2 ) {
44 $network = IP
::toUnsigned( $parts[0] );
45 $bits = $parts[1] +
96;
46 if ( $network !== false && is_numeric( $parts[1] ) && $parts[1] >= 0 && $parts[1] <= 32 ) {
47 return IP
::toOctet( $network ) . "/$bits";
52 return IP
::toOctet( IP
::toUnsigned( $ip ) );
56 * Given an IPv6 address in octet notation, returns an unsigned integer.
57 * @param $ip octet ipv6 IP address.
60 public function toUnsigned6( $ip ) {
61 if ( !$ip ) return null;
62 $ip = explode(':', IP
::expandIPv6( $ip ) );
65 $r_ip .= wfBaseConvert( $v, 16, 2, 16);
67 return wfBaseConvert($r_ip, 2, 10);
71 * Given an IPv6 address in octet notation, returns the expanded octet.
72 * @param $ip octet ipv6 IP address.
75 public function expandIPv6( $ip ) {
76 if ( !$ip ) return null;
77 // Expand zero abbreviations
78 if ( substr_count($ip, '::') ) {
79 $ip = str_replace('::', str_repeat(':0000', 8 - substr_count($ip, ':')) . ':', $ip);
85 * Given an unsigned integer, returns an IPv6 address in octet notation
86 * @param $ip integer ipv6 IP address.
89 public function toOctet( $ip_int ) {
90 // Convert integer to binary
91 $ip_int = wfBaseConvert($ip_int, 10, 2, 128);
92 // Seperate into 8 octets
93 $ip_oct = base_convert( substr( $ip_int, 0, 16 ), 2, 16 );
94 for ($n=1; $n < 8; $n++
) {
95 // Convert to hex, and add ":" marks
96 $ip_oct .= ':' . base_convert( substr($ip_int, 16*$n, 16), 2, 16 );
102 * Convert a network specification in IPv6 CIDR notation to an integer network and a number of bits
103 * @return array(string, int)
105 public static function parseCIDR6( $range ) {
106 $parts = explode( '/', $range, 2 );
107 if ( count( $parts ) != 2 ) {
108 return array( false, false );
110 $network = IP
::toUnsigned6( $parts[0] );
111 if ( $network !== false && is_numeric( $parts[1] ) && $parts[1] >= 0 && $parts[1] <= 128 ) {
116 # Native 32 bit functions WONT work here!!!
117 # Convert to a padded binary number
118 $network = wfBaseConvert( $network, 10, 2, 128 );
119 # Truncate the last (128-$bits) bits and replace them with zeros
120 $network = str_pad( substr( $network, 0, (128 - $bits) ), 128, 0, STR_PAD_RIGHT
);
121 # Convert back to an integer
122 $network = wfBaseConvert( $network, 2, 10 );
129 return array( $network, $bits );
133 * Given a string range in a number of formats, return the start and end of
134 * the range in hexadecimal. For IPv6.
137 * 2001:0db8:85a3::7344/96 CIDR
138 * 2001:0db8:85a3::7344 - 2001:0db8:85a3::7344 Explicit range
139 * 2001:0db8:85a3::7344/96 Single IP
140 * @return array(string, int)
142 public static function parseRange6( $range ) {
143 if ( strpos( $range, '/' ) !== false ) {
145 list( $network, $bits ) = IP
::parseCIDR6( $range );
146 if ( $network === false ) {
147 $start = $end = false;
149 $start = sprintf( '%08X', $network );
150 $end = sprintf( '%08X', $network +
pow( 2, (128 - $bits) ) - 1 );
152 } elseif ( strpos( $range, '-' ) !== false ) {
154 list( $start, $end ) = array_map( 'trim', explode( '-', $range, 2 ) );
155 $start = IP
::toUnsigned6( $start ); $end = IP
::toUnsigned6( $end );
156 if ( $start > $end ) {
157 $start = $end = false;
159 $start = sprintf( '%08X', $start );
160 $end = sprintf( '%08X', $end );
164 $start = $end = IP
::toHex6( $range );
166 if ( $start === false ||
$end === false ) {
167 return array( false, false );
169 return array( $start, $end );
174 * Validate an IP address.
175 * @return boolean True if it is valid.
177 public static function isValid( $ip ) {
178 return preg_match( '/^' . RE_IP_ADD
. '$/', $ip);
182 * Validate an IP Block.
183 * @return boolean True if it is valid.
185 public static function isValidBlock( $ipblock ) {
186 return ( count(self
::toArray($ipblock)) == 1 +
5 );
190 * Determine if an IP address really is an IP address, and if it is public,
191 * i.e. not RFC 1918 or similar
192 * Comes from ProxyTools.php
194 public static function isPublic( $ip ) {
195 $n = IP
::toUnsigned( $ip );
200 // ip2long accepts incomplete addresses, as well as some addresses
201 // followed by garbage characters. Check that it's really valid.
202 if( $ip != long2ip( $n ) ) {
206 static $privateRanges = false;
207 if ( !$privateRanges ) {
208 $privateRanges = array(
209 array( '10.0.0.0', '10.255.255.255' ), # RFC 1918 (private)
210 array( '172.16.0.0', '172.31.255.255' ), # "
211 array( '192.168.0.0', '192.168.255.255' ), # "
212 array( '0.0.0.0', '0.255.255.255' ), # this network
213 array( '127.0.0.0', '127.255.255.255' ), # loopback
217 foreach ( $privateRanges as $r ) {
218 $start = IP
::toUnsigned( $r[0] );
219 $end = IP
::toUnsigned( $r[1] );
220 if ( $n >= $start && $n <= $end ) {
228 * Split out an IP block as an array of 4 bytes and a mask,
229 * return false if it can't be determined
231 * @parameter $ip string A quad dotted IP address
234 public static function toArray( $ipblock ) {
236 if(! preg_match( '/^' . RE_IP_ADD
. '(?:\/(?:'.RE_IP_PREFIX
.'))?' . '$/', $ipblock, $matches ) ) {
244 * Return a zero-padded hexadecimal representation of an IP address.
246 * Hexadecimal addresses are used because they can easily be extended to
247 * IPv6 support. To separate the ranges, the return value from this
248 * function for an IPv6 address will be prefixed with "v6-", a non-
249 * hexadecimal string which sorts after the IPv4 addresses.
251 * @param $ip Quad dotted IP address.
252 * @return hexidecimal
254 public static function toHex( $ip ) {
255 $n = self
::toUnsigned( $ip );
256 if ( $n !== false ) {
257 $n = sprintf( '%08X', $n );
263 public static function toHex6( $ip ) {
264 $n = self
::toUnsigned6( $ip );
265 if ( $n !== false ) {
266 $n = sprintf( '%08X', $n );
272 * Given an IP address in dotted-quad notation, returns an unsigned integer.
273 * Like ip2long() except that it actually works and has a consistent error return value.
274 * Comes from ProxyTools.php
275 * @param $ip Quad dotted IP address.
278 public static function toUnsigned( $ip ) {
279 if ( $ip == '255.255.255.255' ) {
283 if ( $n == -1 ||
$n === false ) { # Return value on error depends on PHP version
294 * Convert a dotted-quad IP to a signed integer
295 * Returns false on failure
297 public static function toSigned( $ip ) {
298 if ( $ip == '255.255.255.255' ) {
310 * Convert a network specification in CIDR notation to an integer network and a number of bits
311 * @return array(string, int)
313 public static function parseCIDR( $range ) {
314 $parts = explode( '/', $range, 2 );
315 if ( count( $parts ) != 2 ) {
316 return array( false, false );
318 $network = IP
::toSigned( $parts[0] );
319 if ( $network !== false && is_numeric( $parts[1] ) && $parts[1] >= 0 && $parts[1] <= 32 ) {
324 $network &= ~
((1 << (32 - $bits)) - 1);
326 # Convert to unsigned
327 if ( $network < 0 ) {
328 $network +
= pow( 2, 32 );
334 return array( $network, $bits );
338 * Given a string range in a number of formats, return the start and end of
339 * the range in hexadecimal. For IPv4.
343 * 1.2.3.4 - 1.2.3.5 Explicit range
345 * @return array(string, int)
347 public static function parseRange( $range ) {
348 if ( strpos( $range, '/' ) !== false ) {
350 list( $network, $bits ) = IP
::parseCIDR( $range );
351 if ( $network === false ) {
352 $start = $end = false;
354 $start = sprintf( '%08X', $network );
355 $end = sprintf( '%08X', $network +
pow( 2, (32 - $bits) ) - 1 );
357 } elseif ( strpos( $range, '-' ) !== false ) {
359 list( $start, $end ) = array_map( 'trim', explode( '-', $range, 2 ) );
360 $start = IP
::toUnsigned( $start ); $end = IP
::toUnsigned( $end );
361 if ( $start > $end ) {
362 $start = $end = false;
364 $start = sprintf( '%08X', $start );
365 $end = sprintf( '%08X', $end );
369 $start = $end = IP
::toHex( $range );
371 if ( $start === false ||
$end === false ) {
372 return array( false, false );
374 return array( $start, $end );
379 * Determine if a given IPv4 address is in a given CIDR network
380 * @param $addr The address to check against the given range.
381 * @param $range The range to check the given address against.
382 * @return bool Whether or not the given address is in the given range.
384 public static function isInRange( $addr, $range ) {
385 // Convert to IPv6 if needed
386 $unsignedIP = IP
::toUnsigned6( IP
::IPv4toIPv6($addr) );
387 list( $start, $end ) = IP
::parseRange6( IP
::IPv4toIPv6($range) );
388 return (($unsignedIP >= $start) && ($unsignedIP <= $end));
392 * Convert some unusual representations of IPv4 addresses to their
393 * canonical dotted quad representation.
395 * This currently only checks a few IPV4-to-IPv6 related cases. More
396 * unusual representations may be added later.
398 * @param $addr something that might be an IP address
399 * @return valid dotted quad IPv4 address or null
401 public static function canonicalize( $addr ) {
402 if ( IP
::isValid( $addr ) )
405 // IPv6 loopback address
407 if ( preg_match( '/^0*' . RE_IPV6_GAP
. '1$/', $addr, $m ) )
410 // IPv4-mapped and IPv4-compatible IPv6 addresses
411 if ( preg_match( '/^' . RE_IPV6_V4_PREFIX
. '(' . RE_IP_ADD
. ')$/i', $addr, $m ) )
413 if ( preg_match( '/^' . RE_IPV6_V4_PREFIX
. RE_IPV6_WORD
. ':' . RE_IPV6_WORD
. '$/i', $addr, $m ) )
414 return long2ip( ( hexdec( $m[1] ) << 16 ) +
hexdec( $m[2] ) );
416 return null; // give up