* Add IPv6 functions, fix explicit ranges
[lhc/web/wiklou.git] / includes / IP.php
1 <?php
2 /*
3 * Collection of public static functions to play with IP address
4 * and IP blocks.
5 *
6 * @Author "Ashar Voultoiz" <hashar@altern.org>
7 * @License GPL v2 or later
8 */
9
10 // Some regex definition to "play" with IP address and IP address blocks
11
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}' );
24
25 class IP {
26
27 /**
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.
32 * @return string
33 */
34 public function IPv4toIPv6( $ip ) {
35 if ( !$ip ) return null;
36 // Convert only if needed
37 if ( strpos($ip,':') !==false ) return $ip;
38 // IPv4 CIDRs
39 if ( strpos( $ip, '/' ) !== false ) {
40 $parts = explode( '/', $ip, 2 );
41 if ( count( $parts ) != 2 ) {
42 return false;
43 }
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";
48 } else {
49 return false;
50 }
51 }
52 return IP::toOctet( IP::toUnsigned( $ip ) );
53 }
54
55 /**
56 * Given an IPv6 address in octet notation, returns an unsigned integer.
57 * @param $ip octet ipv6 IP address.
58 * @return string
59 */
60 public function toUnsigned6( $ip ) {
61 if ( !$ip ) return null;
62 $ip = explode(':', IP::expandIPv6( $ip ) );
63 $r_ip = '';
64 foreach ($ip as $v) {
65 $r_ip .= wfBaseConvert( $v, 16, 2, 16);
66 }
67 return wfBaseConvert($r_ip, 2, 10);
68 }
69
70 /**
71 * Given an IPv6 address in octet notation, returns the expanded octet.
72 * @param $ip octet ipv6 IP address.
73 * @return string
74 */
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);
80 }
81 return $ip;
82 }
83
84 /**
85 * Given an unsigned integer, returns an IPv6 address in octet notation
86 * @param $ip integer ipv6 IP address.
87 * @return string
88 */
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 );
97 }
98 return $ip_oct;
99 }
100
101 /**
102 * Convert a network specification in IPv6 CIDR notation to an integer network and a number of bits
103 * @return array(string, int)
104 */
105 public static function parseCIDR6( $range ) {
106 $parts = explode( '/', $range, 2 );
107 if ( count( $parts ) != 2 ) {
108 return array( false, false );
109 }
110 $network = IP::toUnsigned6( $parts[0] );
111 if ( $network !== false && is_numeric( $parts[1] ) && $parts[1] >= 0 && $parts[1] <= 128 ) {
112 $bits = $parts[1];
113 if ( $bits == 0 ) {
114 $network = 0;
115 } else {
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 );
123 }
124 } else {
125 $network = false;
126 $bits = false;
127 }
128
129 return array( $network, $bits );
130 }
131
132 /**
133 * Given a string range in a number of formats, return the start and end of
134 * the range in hexadecimal. For IPv6.
135 *
136 * Formats are:
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)
141 */
142 public static function parseRange6( $range ) {
143 if ( strpos( $range, '/' ) !== false ) {
144 # CIDR
145 list( $network, $bits ) = IP::parseCIDR6( $range );
146 if ( $network === false ) {
147 $start = $end = false;
148 } else {
149 $start = sprintf( '%08X', $network );
150 $end = sprintf( '%08X', $network + pow( 2, (128 - $bits) ) - 1 );
151 }
152 } elseif ( strpos( $range, '-' ) !== false ) {
153 # Explicit range
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;
158 } else {
159 $start = sprintf( '%08X', $start );
160 $end = sprintf( '%08X', $end );
161 }
162 } else {
163 # Single IP
164 $start = $end = IP::toHex6( $range );
165 }
166 if ( $start === false || $end === false ) {
167 return array( false, false );
168 } else {
169 return array( $start, $end );
170 }
171 }
172
173 /**
174 * Validate an IP address.
175 * @return boolean True if it is valid.
176 */
177 public static function isValid( $ip ) {
178 return preg_match( '/^' . RE_IP_ADD . '$/', $ip);
179 }
180
181 /**
182 * Validate an IP Block.
183 * @return boolean True if it is valid.
184 */
185 public static function isValidBlock( $ipblock ) {
186 return ( count(self::toArray($ipblock)) == 1 + 5 );
187 }
188
189 /**
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
193 */
194 public static function isPublic( $ip ) {
195 $n = IP::toUnsigned( $ip );
196 if ( !$n ) {
197 return false;
198 }
199
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 ) ) {
203 return false;
204 }
205
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
214 );
215 }
216
217 foreach ( $privateRanges as $r ) {
218 $start = IP::toUnsigned( $r[0] );
219 $end = IP::toUnsigned( $r[1] );
220 if ( $n >= $start && $n <= $end ) {
221 return false;
222 }
223 }
224 return true;
225 }
226
227 /**
228 * Split out an IP block as an array of 4 bytes and a mask,
229 * return false if it can't be determined
230 *
231 * @parameter $ip string A quad dotted IP address
232 * @return array
233 */
234 public static function toArray( $ipblock ) {
235 $matches = array();
236 if(! preg_match( '/^' . RE_IP_ADD . '(?:\/(?:'.RE_IP_PREFIX.'))?' . '$/', $ipblock, $matches ) ) {
237 return false;
238 } else {
239 return $matches;
240 }
241 }
242
243 /**
244 * Return a zero-padded hexadecimal representation of an IP address.
245 *
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.
250 *
251 * @param $ip Quad dotted IP address.
252 * @return hexidecimal
253 */
254 public static function toHex( $ip ) {
255 $n = self::toUnsigned( $ip );
256 if ( $n !== false ) {
257 $n = sprintf( '%08X', $n );
258 }
259 return $n;
260 }
261
262 // For IPv6
263 public static function toHex6( $ip ) {
264 $n = self::toUnsigned6( $ip );
265 if ( $n !== false ) {
266 $n = sprintf( '%08X', $n );
267 }
268 return $n;
269 }
270
271 /**
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.
276 * @return integer
277 */
278 public static function toUnsigned( $ip ) {
279 if ( $ip == '255.255.255.255' ) {
280 $n = -1;
281 } else {
282 $n = ip2long( $ip );
283 if ( $n == -1 || $n === false ) { # Return value on error depends on PHP version
284 $n = false;
285 }
286 }
287 if ( $n < 0 ) {
288 $n += pow( 2, 32 );
289 }
290 return $n;
291 }
292
293 /**
294 * Convert a dotted-quad IP to a signed integer
295 * Returns false on failure
296 */
297 public static function toSigned( $ip ) {
298 if ( $ip == '255.255.255.255' ) {
299 $n = -1;
300 } else {
301 $n = ip2long( $ip );
302 if ( $n == -1 ) {
303 $n = false;
304 }
305 }
306 return $n;
307 }
308
309 /**
310 * Convert a network specification in CIDR notation to an integer network and a number of bits
311 * @return array(string, int)
312 */
313 public static function parseCIDR( $range ) {
314 $parts = explode( '/', $range, 2 );
315 if ( count( $parts ) != 2 ) {
316 return array( false, false );
317 }
318 $network = IP::toSigned( $parts[0] );
319 if ( $network !== false && is_numeric( $parts[1] ) && $parts[1] >= 0 && $parts[1] <= 32 ) {
320 $bits = $parts[1];
321 if ( $bits == 0 ) {
322 $network = 0;
323 } else {
324 $network &= ~((1 << (32 - $bits)) - 1);
325 }
326 # Convert to unsigned
327 if ( $network < 0 ) {
328 $network += pow( 2, 32 );
329 }
330 } else {
331 $network = false;
332 $bits = false;
333 }
334 return array( $network, $bits );
335 }
336
337 /**
338 * Given a string range in a number of formats, return the start and end of
339 * the range in hexadecimal. For IPv4.
340 *
341 * Formats are:
342 * 1.2.3.4/24 CIDR
343 * 1.2.3.4 - 1.2.3.5 Explicit range
344 * 1.2.3.4 Single IP
345 * @return array(string, int)
346 */
347 public static function parseRange( $range ) {
348 if ( strpos( $range, '/' ) !== false ) {
349 # CIDR
350 list( $network, $bits ) = IP::parseCIDR( $range );
351 if ( $network === false ) {
352 $start = $end = false;
353 } else {
354 $start = sprintf( '%08X', $network );
355 $end = sprintf( '%08X', $network + pow( 2, (32 - $bits) ) - 1 );
356 }
357 } elseif ( strpos( $range, '-' ) !== false ) {
358 # Explicit range
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;
363 } else {
364 $start = sprintf( '%08X', $start );
365 $end = sprintf( '%08X', $end );
366 }
367 } else {
368 # Single IP
369 $start = $end = IP::toHex( $range );
370 }
371 if ( $start === false || $end === false ) {
372 return array( false, false );
373 } else {
374 return array( $start, $end );
375 }
376 }
377
378 /**
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.
383 */
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));
389 }
390
391 /**
392 * Convert some unusual representations of IPv4 addresses to their
393 * canonical dotted quad representation.
394 *
395 * This currently only checks a few IPV4-to-IPv6 related cases. More
396 * unusual representations may be added later.
397 *
398 * @param $addr something that might be an IP address
399 * @return valid dotted quad IPv4 address or null
400 */
401 public static function canonicalize( $addr ) {
402 if ( IP::isValid( $addr ) )
403 return $addr;
404
405 // IPv6 loopback address
406 $m = array();
407 if ( preg_match( '/^0*' . RE_IPV6_GAP . '1$/', $addr, $m ) )
408 return '127.0.0.1';
409
410 // IPv4-mapped and IPv4-compatible IPv6 addresses
411 if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . '(' . RE_IP_ADD . ')$/i', $addr, $m ) )
412 return $m[1];
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] ) );
415
416 return null; // give up
417 }
418 }
419
420 ?>