]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/IP.php
MediaWiki 1.15.5
[autoinstallsdev/mediawiki.git] / includes / IP.php
1 <?php
2 /*
3  * @Author "Ashar Voultoiz" <hashar@altern.org>
4  * @License GPL v2 or later
5  */
6
7 // Some regex definition to "play" with IP address and IP address blocks
8
9 // An IP is made of 4 bytes from x00 to xFF which is d0 to d255
10 define( 'RE_IP_BYTE', '(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|0?[0-9]?[0-9])');
11 define( 'RE_IP_ADD' , RE_IP_BYTE . '\.' . RE_IP_BYTE . '\.' . RE_IP_BYTE . '\.' . RE_IP_BYTE );
12 // An IPv4 block is an IP address and a prefix (d1 to d32)
13 define( 'RE_IP_PREFIX', '(3[0-2]|[12]?\d)');
14 define( 'RE_IP_BLOCK', RE_IP_ADD . '\/' . RE_IP_PREFIX);
15 // For IPv6 canonicalization (NOT for strict validation; these are quite lax!)
16 define( 'RE_IPV6_WORD', '([0-9A-Fa-f]{1,4})' );
17 define( 'RE_IPV6_GAP', ':(?:0+:)*(?::(?:0+:)*)?' );
18 define( 'RE_IPV6_V4_PREFIX', '0*' . RE_IPV6_GAP . '(?:ffff:)?' );
19 // An IPv6 block is an IP address and a prefix (d1 to d128)
20 define( 'RE_IPV6_PREFIX', '(12[0-8]|1[01][0-9]|[1-9]?\d)');
21 // An IPv6 IP is made up of 8 octets. However abbreviations like "::" can be used. This is lax!
22 define( 'RE_IPV6_ADD', '(:(:' . RE_IPV6_WORD . '){1,7}|' . RE_IPV6_WORD . '(:{1,2}' . RE_IPV6_WORD . '|::$){1,7})' );
23 define( 'RE_IPV6_BLOCK', RE_IPV6_ADD . '\/' . RE_IPV6_PREFIX );
24 // This might be useful for regexps used elsewhere, matches any IPv6 or IPv6 address or network
25 define( 'IP_ADDRESS_STRING',
26         '(?:' .
27                 RE_IP_ADD . '(\/' . RE_IP_PREFIX . '|)' .
28         '|' .
29                 RE_IPV6_ADD . '(\/' . RE_IPV6_PREFIX . '|)' .
30         ')' );
31
32 /**
33  * A collection of public static functions to play with IP address
34  * and IP blocks.
35  */
36 class IP {
37         /**
38          * Given a string, determine if it as valid IP
39          * Unlike isValid(), this looks for networks too
40          * @param $ip IP address.
41          * @return string
42          */
43         public static function isIPAddress( $ip ) {
44                 if ( !$ip ) return false;
45                 if ( is_array( $ip ) ) {
46                   throw new MWException( "invalid value passed to " . __METHOD__ );
47                 }
48                 // IPv6 IPs with two "::" strings are ambiguous and thus invalid
49                 return preg_match( '/^' . IP_ADDRESS_STRING . '$/', $ip) && ( substr_count($ip, '::') < 2 );
50         }
51
52         public static function isIPv6( $ip ) {
53                 if ( !$ip ) return false;
54                 if( is_array( $ip ) ) {
55                   throw new MWException( "invalid value passed to " . __METHOD__ );
56                 }
57                 // IPv6 IPs with two "::" strings are ambiguous and thus invalid
58                 return preg_match( '/^' . RE_IPV6_ADD . '(\/' . RE_IPV6_PREFIX . '|)$/', $ip) && ( substr_count($ip, '::') < 2);
59         }
60
61         public static function isIPv4( $ip ) {
62                 if ( !$ip ) return false;
63                 return preg_match( '/^' . RE_IP_ADD . '(\/' . RE_IP_PREFIX . '|)$/', $ip);
64         }
65
66         /**
67          * Given an IP address in dotted-quad notation, returns an IPv6 octet.
68          * See http://www.answers.com/topic/ipv4-compatible-address
69          * IPs with the first 92 bits as zeros are reserved from IPv6
70          * @param $ip quad-dotted IP address.
71          * @return string
72          */
73         public static function IPv4toIPv6( $ip ) {
74                 if ( !$ip ) return null;
75                 // Convert only if needed
76                 if ( self::isIPv6( $ip ) ) return $ip;
77                 // IPv4 CIDRs
78                 if ( strpos( $ip, '/' ) !== false ) {
79                         $parts = explode( '/', $ip, 2 );
80                         if ( count( $parts ) != 2 ) {
81                                 return false;
82                         }
83                         $network = self::toUnsigned( $parts[0] );
84                         if ( $network !== false && is_numeric( $parts[1] ) && $parts[1] >= 0 && $parts[1] <= 32 ) {
85                                 $bits = $parts[1] + 96;
86                                 return self::toOctet( $network ) . "/$bits";
87                         } else {
88                                 return false;
89                         }
90                 }
91                 return self::toOctet( self::toUnsigned( $ip ) );
92         }
93
94         /**
95          * Given an IPv6 address in octet notation, returns an unsigned integer.
96          * @param $ip octet ipv6 IP address.
97          * @return string
98          */
99         public static function toUnsigned6( $ip ) {
100                 if ( !$ip ) return null;
101         $ip = explode(':', self::sanitizeIP( $ip ) );
102         $r_ip = '';
103         foreach ($ip as $v) {
104                 $r_ip .= str_pad( $v, 4, 0, STR_PAD_LEFT );
105         }
106         $r_ip = wfBaseConvert( $r_ip, 16, 10 );
107         return $r_ip;
108         }
109
110         /**
111          * Given an IPv6 address in octet notation, returns the expanded octet.
112          * IPv4 IPs will be trimmed, thats it...
113          * @param $ip octet ipv6 IP address.
114          * @return string
115          */
116         public static function sanitizeIP( $ip ) {
117                 $ip = trim( $ip );
118                 if ( $ip === '' ) return null;
119                 // Trim and return IPv4 addresses
120                 if ( self::isIPv4($ip) ) return $ip;
121                 // Only IPv6 addresses can be expanded
122                 if ( !self::isIPv6($ip) ) return $ip;
123                 // Remove any whitespaces, convert to upper case
124                 $ip = strtoupper( $ip );
125                 // Expand zero abbreviations
126                 if ( strpos( $ip, '::' ) !== false ) {
127                 $ip = str_replace('::', str_repeat(':0', 8 - substr_count($ip, ':')) . ':', $ip);
128         }
129         // For IPs that start with "::", correct the final IP so that it starts with '0' and not ':'
130         if ( $ip[0] == ':' ) $ip = "0$ip";
131         // Remove leading zereos from each bloc as needed
132         $ip = preg_replace( '/(^|:)0+' . RE_IPV6_WORD . '/', '$1$2', $ip );
133         return $ip;
134         }
135
136         /**
137          * Given an unsigned integer, returns an IPv6 address in octet notation
138          * @param $ip integer IP address.
139          * @return string
140          */
141         public static function toOctet( $ip_int ) {
142                 // Convert to padded uppercase hex
143                 $ip_hex = wfBaseConvert($ip_int, 10, 16, 32, false);
144                 // Separate into 8 octets
145                 $ip_oct = substr( $ip_hex, 0, 4 );
146                 for ($n=1; $n < 8; $n++) {
147                         $ip_oct .= ':' . substr($ip_hex, 4*$n, 4);
148                 }
149                 // NO leading zeroes
150                 $ip_oct = preg_replace( '/(^|:)0+' . RE_IPV6_WORD . '/', '$1$2', $ip_oct );
151         return $ip_oct;
152         }
153         
154         /**
155          * Given a hexadecimal number, returns to an IPv6 address in octet notation
156          * @param $ip string hex IP
157          * @return string
158          */
159         public static function HextoOctet( $ip_hex ) {
160                 // Convert to padded uppercase hex
161                 $ip_hex = str_pad( strtoupper($ip_hex), 32, '0');
162                 // Separate into 8 octets
163                 $ip_oct = substr( $ip_hex, 0, 4 );
164                 for ($n=1; $n < 8; $n++) {
165                         $ip_oct .= ':' . substr($ip_hex, 4*$n, 4);
166                 }
167                 // NO leading zeroes
168                 $ip_oct = preg_replace( '/(^|:)0+' . RE_IPV6_WORD . '/', '$1$2', $ip_oct );
169         return $ip_oct;
170         }
171         
172         /**
173          * Converts a hexadecimal number to an IPv4 address in octet notation
174          * @param $ip string Hex IP
175          * @return string
176          */ 
177         public static function hexToQuad( $ip ) {
178                 // Converts a hexadecimal IP to nnn.nnn.nnn.nnn format
179                 $dec = wfBaseConvert( $ip, 16, 10 );
180                 $parts[3] = $dec % 256;
181                 $dec /= 256;
182                 $parts[2] = $dec % 256;
183                 $dec /= 256;
184                 $parts[1] = $dec % 256;
185                 $parts[0] = $dec / 256;
186                 return implode( '.', array_reverse( $parts ) );
187         }
188
189         /**
190          * Convert a network specification in IPv6 CIDR notation to an integer network and a number of bits
191          * @return array(string, int)
192          */
193         public static function parseCIDR6( $range ) {
194                 # Expand any IPv6 IP
195                 $parts = explode( '/', IP::sanitizeIP( $range ), 2 );
196                 if ( count( $parts ) != 2 ) {
197                         return array( false, false );
198                 }
199                 $network = self::toUnsigned6( $parts[0] );
200                 if ( $network !== false && is_numeric( $parts[1] ) && $parts[1] >= 0 && $parts[1] <= 128 ) {
201                         $bits = $parts[1];
202                         if ( $bits == 0 ) {
203                                 $network = 0;
204                         } else {
205                         # Native 32 bit functions WONT work here!!!
206                         # Convert to a padded binary number
207                                 $network = wfBaseConvert( $network, 10, 2, 128 );
208                         # Truncate the last (128-$bits) bits and replace them with zeros
209                                 $network = str_pad( substr( $network, 0, $bits ), 128, 0, STR_PAD_RIGHT );
210                         # Convert back to an integer
211                                 $network = wfBaseConvert( $network, 2, 10 );
212                         }
213                 } else {
214                         $network = false;
215                         $bits = false;
216                 }
217                 return array( $network, $bits );
218         }
219
220         /**
221          * Given a string range in a number of formats, return the start and end of
222          * the range in hexadecimal. For IPv6.
223          *
224          * Formats are:
225          *     2001:0db8:85a3::7344/96                                   CIDR
226          *     2001:0db8:85a3::7344 - 2001:0db8:85a3::7344   Explicit range
227          *     2001:0db8:85a3::7344/96                                   Single IP
228          * @return array(string, int)
229          */
230         public static function parseRange6( $range ) {
231                 # Expand any IPv6 IP
232                 $range = IP::sanitizeIP( $range );
233                 if ( strpos( $range, '/' ) !== false ) {
234                         # CIDR
235                         list( $network, $bits ) = self::parseCIDR6( $range );
236                         if ( $network === false ) {
237                                 $start = $end = false;
238                         } else {
239                                 $start = wfBaseConvert( $network, 10, 16, 32, false );
240                                 # Turn network to binary (again)
241                                 $end = wfBaseConvert( $network, 10, 2, 128 );
242                                 # Truncate the last (128-$bits) bits and replace them with ones
243                                 $end = str_pad( substr( $end, 0, $bits ), 128, 1, STR_PAD_RIGHT );
244                                 # Convert to hex
245                                 $end = wfBaseConvert( $end, 2, 16, 32, false );
246                                 # see toHex() comment
247                                 $start = "v6-$start"; $end = "v6-$end";
248                         }
249                 } elseif ( strpos( $range, '-' ) !== false ) {
250                         # Explicit range
251                         list( $start, $end ) = array_map( 'trim', explode( '-', $range, 2 ) );
252                         $start = self::toUnsigned6( $start ); $end = self::toUnsigned6( $end );
253                         if ( $start > $end ) {
254                                 $start = $end = false;
255                         } else {
256                                 $start = wfBaseConvert( $start, 10, 16, 32, false );
257                                 $end = wfBaseConvert( $end, 10, 16, 32, false );
258                         }
259                         # see toHex() comment
260                         $start = "v6-$start"; $end = "v6-$end";
261                 } else {
262                         # Single IP
263                         $start = $end = self::toHex( $range );
264                 }
265                 if ( $start === false || $end === false ) {
266                         return array( false, false );
267                 } else {
268                         return array( $start, $end );
269                 }
270     }
271
272         /**
273          * Validate an IP address.
274          * @return boolean True if it is valid.
275          */
276         public static function isValid( $ip ) {
277                 return ( preg_match( '/^' . RE_IP_ADD . '$/', $ip) || preg_match( '/^' . RE_IPV6_ADD . '$/', $ip) );
278         }
279
280         /**
281          * Validate an IP Block.
282          * @return boolean True if it is valid.
283          */
284         public static function isValidBlock( $ipblock ) {
285                 return ( count(self::toArray($ipblock)) == 1 + 5 );
286         }
287
288         /**
289          * Determine if an IP address really is an IP address, and if it is public,
290          * i.e. not RFC 1918 or similar
291          * Comes from ProxyTools.php
292          */
293         public static function isPublic( $ip ) {
294                 $n = self::toUnsigned( $ip );
295                 if ( !$n ) {
296                         return false;
297                 }
298
299                 // ip2long accepts incomplete addresses, as well as some addresses
300                 // followed by garbage characters. Check that it's really valid.
301                 if( $ip != long2ip( $n ) ) {
302                         return false;
303                 }
304
305                 static $privateRanges = false;
306                 if ( !$privateRanges ) {
307                         $privateRanges = array(
308                                 array( '10.0.0.0',    '10.255.255.255' ),   # RFC 1918 (private)
309                                 array( '172.16.0.0',  '172.31.255.255' ),   #     "
310                                 array( '192.168.0.0', '192.168.255.255' ),  #     "
311                                 array( '0.0.0.0',     '0.255.255.255' ),    # this network
312                                 array( '127.0.0.0',   '127.255.255.255' ),  # loopback
313                         );
314                 }
315
316                 foreach ( $privateRanges as $r ) {
317                         $start = self::toUnsigned( $r[0] );
318                         $end = self::toUnsigned( $r[1] );
319                         if ( $n >= $start && $n <= $end ) {
320                                 return false;
321                         }
322                 }
323                 return true;
324         }
325
326         /**
327          * Split out an IP block as an array of 4 bytes and a mask,
328          * return false if it can't be determined
329          *
330          * @param $ip string A quad dotted/octet IP address
331          * @return array
332          */
333         public static function toArray( $ipblock ) {
334                 $matches = array();
335                 if( preg_match( '/^' . RE_IP_ADD . '(?:\/(?:'.RE_IP_PREFIX.'))?' . '$/', $ipblock, $matches ) ) {
336                         return $matches;
337                 } else if ( preg_match( '/^' . RE_IPV6_ADD . '(?:\/(?:'.RE_IPV6_PREFIX.'))?' . '$/', $ipblock, $matches ) ) {
338                         return $matches;
339                 } else {
340                         return false;
341                 }
342         }
343
344         /**
345          * Return a zero-padded hexadecimal representation of an IP address.
346          *
347          * Hexadecimal addresses are used because they can easily be extended to
348          * IPv6 support. To separate the ranges, the return value from this
349          * function for an IPv6 address will be prefixed with "v6-", a non-
350          * hexadecimal string which sorts after the IPv4 addresses.
351          *
352          * @param $ip Quad dotted/octet IP address.
353          * @return hexidecimal
354          */
355         public static function toHex( $ip ) {
356                 $n = self::toUnsigned( $ip );
357                 if ( $n !== false ) {
358                         $n = self::isIPv6($ip) ? "v6-" . wfBaseConvert( $n, 10, 16, 32, false ) : wfBaseConvert( $n, 10, 16, 8, false );
359                 }
360                 return $n;
361         }
362
363         /**
364          * Given an IP address in dotted-quad/octet notation, returns an unsigned integer.
365          * Like ip2long() except that it actually works and has a consistent error return value.
366          * Comes from ProxyTools.php
367          * @param $ip Quad dotted IP address.
368          * @return integer
369          */
370         public static function toUnsigned( $ip ) {
371                 // Use IPv6 functions if needed
372                 if ( self::isIPv6( $ip ) ) {
373                         return self::toUnsigned6( $ip );
374                 }
375                 if ( $ip == '255.255.255.255' ) {
376                         $n = -1;
377                 } else {
378                         $n = ip2long( $ip );
379                         if ( $n == -1 || $n === false ) { # Return value on error depends on PHP version
380                                 $n = false;
381                         }
382                 }
383                 if ( $n < 0 ) {
384                         $n += pow( 2, 32 );
385                 }
386                 return $n;
387         }
388
389         /**
390          * Convert a dotted-quad IP to a signed integer
391          * Returns false on failure
392          */
393         public static function toSigned( $ip ) {
394                 if ( $ip == '255.255.255.255' ) {
395                         $n = -1;
396                 } else {
397                         $n = ip2long( $ip );
398                         if ( $n == -1 ) {
399                                 $n = false;
400                         }
401                 }
402                 return $n;
403         }
404
405         /**
406          * Convert a network specification in CIDR notation to an integer network and a number of bits
407          * @return array(string, int)
408          */
409         public static function parseCIDR( $range ) {
410                 $parts = explode( '/', $range, 2 );
411                 if ( count( $parts ) != 2 ) {
412                         return array( false, false );
413                 }
414                 $network = self::toSigned( $parts[0] );
415                 if ( $network !== false && is_numeric( $parts[1] ) && $parts[1] >= 0 && $parts[1] <= 32 ) {
416                         $bits = $parts[1];
417                         if ( $bits == 0 ) {
418                                 $network = 0;
419                         } else {
420                                 $network &= ~((1 << (32 - $bits)) - 1);
421                         }
422                         # Convert to unsigned
423                         if ( $network < 0 ) {
424                                 $network += pow( 2, 32 );
425                         }
426                 } else {
427                         $network = false;
428                         $bits = false;
429                 }
430                 return array( $network, $bits );
431         }
432
433         /**
434          * Given a string range in a number of formats, return the start and end of
435          * the range in hexadecimal.
436          *
437          * Formats are:
438          *     1.2.3.4/24          CIDR
439          *     1.2.3.4 - 1.2.3.5   Explicit range
440          *     1.2.3.4             Single IP
441          *
442          *     2001:0db8:85a3::7344/96                                   CIDR
443          *     2001:0db8:85a3::7344 - 2001:0db8:85a3::7344   Explicit range
444          *     2001:0db8:85a3::7344                                      Single IP
445          * @return array(string, int)
446          */
447         public static function parseRange( $range ) {
448                 // Use IPv6 functions if needed
449                 if ( self::isIPv6( $range ) ) {
450                         return self::parseRange6( $range );
451                 }
452                 if ( strpos( $range, '/' ) !== false ) {
453                         # CIDR
454                         list( $network, $bits ) = self::parseCIDR( $range );
455                         if ( $network === false ) {
456                                 $start = $end = false;
457                         } else {
458                                 $start = sprintf( '%08X', $network );
459                                 $end = sprintf( '%08X', $network + pow( 2, (32 - $bits) ) - 1 );
460                         }
461                 } elseif ( strpos( $range, '-' ) !== false ) {
462                         # Explicit range
463                         list( $start, $end ) = array_map( 'trim', explode( '-', $range, 2 ) );
464                         if( self::isIPAddress( $start ) && self::isIPAddress( $end ) ) {
465                                 $start = self::toUnsigned( $start ); $end = self::toUnsigned( $end );
466                                 if ( $start > $end ) {
467                                         $start = $end = false;
468                                 } else {
469                                         $start = sprintf( '%08X', $start );
470                                         $end = sprintf( '%08X', $end );
471                                 }
472                         } else {
473                                 $start = $end = false;
474                         }
475                 } else {
476                         # Single IP
477                         $start = $end = self::toHex( $range );
478                 }
479                 if ( $start === false || $end === false ) {
480                         return array( false, false );
481                 } else {
482                         return array( $start, $end );
483                 }
484     }
485
486     /**
487      * Determine if a given IPv4/IPv6 address is in a given CIDR network
488      * @param $addr The address to check against the given range.
489      * @param $range The range to check the given address against.
490      * @return bool Whether or not the given address is in the given range.
491      */
492     public static function isInRange( $addr, $range ) {
493     // Convert to IPv6 if needed
494         $unsignedIP = self::toHex( $addr );
495         list( $start, $end ) = self::parseRange( $range );
496         return (($unsignedIP >= $start) && ($unsignedIP <= $end));
497     }
498
499     /**
500      * Convert some unusual representations of IPv4 addresses to their
501      * canonical dotted quad representation.
502      *
503      * This currently only checks a few IPV4-to-IPv6 related cases.  More
504      * unusual representations may be added later.
505      *
506      * @param $addr something that might be an IP address
507      * @return valid dotted quad IPv4 address or null
508      */
509     public static function canonicalize( $addr ) {
510                 if ( self::isValid( $addr ) )
511                         return $addr;
512
513                 // Annoying IPv6 representations like ::ffff:1.2.3.4
514                 if ( strpos($addr,':') !==false && strpos($addr,'.') !==false ) {
515                         $addr = str_replace( '.', ':', $addr );
516                         if( IP::isIPv6( $addr ) )
517                                 return $addr;
518                 }
519
520                 // IPv6 loopback address
521                 $m = array();
522                 if ( preg_match( '/^0*' . RE_IPV6_GAP . '1$/', $addr, $m ) )
523                         return '127.0.0.1';
524
525                 // IPv4-mapped and IPv4-compatible IPv6 addresses
526                 if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . '(' . RE_IP_ADD . ')$/i', $addr, $m ) )
527                     return $m[1];
528                 if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . RE_IPV6_WORD . ':' . RE_IPV6_WORD . '$/i', $addr, $m ) )
529                     return long2ip( ( hexdec( $m[1] ) << 16 ) + hexdec( $m[2] ) );
530
531                 return null;  // give up
532     }
533 }