1 <?php
2 /**
3 * @package FrameworkOnFramework
4 * @subpackage utils
5 * @copyright Copyright (C) 2010-2016 Nicholas K. Dionysopoulos / Akeeba Ltd. All rights reserved.
6 * @license GNU General Public License version 2 or later; see LICENSE.txt
7 */
8
9 defined('FOF_INCLUDED') or die;
10
11 /**
12 * IP address utilities
13 */
14 abstract class FOFUtilsIp
15 {
16 /** @var string The IP address of the current visitor */
17 protected static $ip = null;
18
19 /**
20 * Should I allow IP overrides through X-Forwarded-For or Client-Ip HTTP headers?
21 *
22 * @var bool
23 */
24 protected static $allowIpOverrides = true;
25
26 /**
27 * Get the current visitor's IP address
28 *
29 * @return string
30 */
31 public static function getIp()
32 {
33 if (is_null(static::$ip))
34 {
35 $ip = self::detectAndCleanIP();
36
37 if (!empty($ip) && ($ip != '0.0.0.0') && function_exists('inet_pton') && function_exists('inet_ntop'))
38 {
39 $myIP = @inet_pton($ip);
40
41 if ($myIP !== false)
42 {
43 $ip = inet_ntop($myIP);
44 }
45 }
46
47 static::setIp($ip);
48 }
49
50 return static::$ip;
51 }
52
53 /**
54 * Set the IP address of the current visitor
55 *
56 * @param string $ip
57 *
58 * @return void
59 */
60 public static function setIp($ip)
61 {
62 static::$ip = $ip;
63 }
64
65 /**
66 * Is it an IPv6 IP address?
67 *
68 * @param string $ip An IPv4 or IPv6 address
69 *
70 * @return boolean True if it's IPv6
71 */
72 public static function isIPv6($ip)
73 {
74 if (strstr($ip, ':'))
75 {
76 return true;
77 }
78
79 return false;
80 }
81
82 /**
83 * Checks if an IP is contained in a list of IPs or IP expressions
84 *
85 * @param string $ip The IPv4/IPv6 address to check
86 * @param array|string $ipTable An IP expression (or a comma-separated or array list of IP expressions) to check against
87 *
88 * @return null|boolean True if it's in the list
89 */
90 public static function IPinList($ip, $ipTable = '')
91 {
92 // No point proceeding with an empty IP list
93 if (empty($ipTable))
94 {
95 return false;
96 }
97
98 // If the IP list is not an array, convert it to an array
99 if (!is_array($ipTable))
100 {
101 if (strpos($ipTable, ',') !== false)
102 {
103 $ipTable = explode(',', $ipTable);
104 $ipTable = array_map(function($x) { return trim($x); }, $ipTable);
105 }
106 else
107 {
108 $ipTable = trim($ipTable);
109 $ipTable = array($ipTable);
110 }
111 }
112
113 // If no IP address is found, return false
114 if ($ip == '0.0.0.0')
115 {
116 return false;
117 }
118
119 // If no IP is given, return false
120 if (empty($ip))
121 {
122 return false;
123 }
124
125 // Sanity check
126 if (!function_exists('inet_pton'))
127 {
128 return false;
129 }
130
131 // Get the IP's in_adds representation
132 $myIP = @inet_pton($ip);
133
134 // If the IP is in an unrecognisable format, quite
135 if ($myIP === false)
136 {
137 return false;
138 }
139
140 $ipv6 = self::isIPv6($ip);
141
142 foreach ($ipTable as $ipExpression)
143 {
144 $ipExpression = trim($ipExpression);
145
146 // Inclusive IP range, i.e. 123.123.123.123-124.125.126.127
147 if (strstr($ipExpression, '-'))
148 {
149 list($from, $to) = explode('-', $ipExpression, 2);
150
151 if ($ipv6 && (!self::isIPv6($from) || !self::isIPv6($to)))
152 {
153 // Do not apply IPv4 filtering on an IPv6 address
154 continue;
155 }
156 elseif (!$ipv6 && (self::isIPv6($from) || self::isIPv6($to)))
157 {
158 // Do not apply IPv6 filtering on an IPv4 address
159 continue;
160 }
161
162 $from = @inet_pton(trim($from));
163 $to = @inet_pton(trim($to));
164
165 // Sanity check
166 if (($from === false) || ($to === false))
167 {
168 continue;
169 }
170
171 // Swap from/to if they're in the wrong order
172 if ($from > $to)
173 {
174 list($from, $to) = array($to, $from);
175 }
176
177 if (($myIP >= $from) && ($myIP <= $to))
178 {
179 return true;
180 }
181 }
182 // Netmask or CIDR provided
183 elseif (strstr($ipExpression, '/'))
184 {
185 $binaryip = self::inet_to_bits($myIP);
186
187 list($net, $maskbits) = explode('/', $ipExpression, 2);
188 if ($ipv6 && !self::isIPv6($net))
189 {
190 // Do not apply IPv4 filtering on an IPv6 address
191 continue;
192 }
193 elseif (!$ipv6 && self::isIPv6($net))
194 {
195 // Do not apply IPv6 filtering on an IPv4 address
196 continue;
197 }
198 elseif ($ipv6 && strstr($maskbits, ':'))
199 {
200 // Perform an IPv6 CIDR check
201 if (self::checkIPv6CIDR($myIP, $ipExpression))
202 {
203 return true;
204 }
205
206 // If we didn't match it proceed to the next expression
207 continue;
208 }
209 elseif (!$ipv6 && strstr($maskbits, '.'))
210 {
211 // Convert IPv4 netmask to CIDR
212 $long = ip2long($maskbits);
213 $base = ip2long('255.255.255.255');
214 $maskbits = 32 - log(($long ^ $base) + 1, 2);
215 }
216
217 // Convert network IP to in_addr representation
218 $net = @inet_pton($net);
219
220 // Sanity check
221 if ($net === false)
222 {
223 continue;
224 }
225
226 // Get the network's binary representation
227 $binarynet = self::inet_to_bits($net);
228 $expectedNumberOfBits = $ipv6 ? 128 : 24;
229 $binarynet = str_pad($binarynet, $expectedNumberOfBits, '0', STR_PAD_RIGHT);
230
231 // Check the corresponding bits of the IP and the network
232 $ip_net_bits = substr($binaryip, 0, $maskbits);
233 $net_bits = substr($binarynet, 0, $maskbits);
234
235 if ($ip_net_bits == $net_bits)
236 {
237 return true;
238 }
239 }
240 else
241 {
242 // IPv6: Only single IPs are supported
243 if ($ipv6)
244 {
245 $ipExpression = trim($ipExpression);
246
247 if (!self::isIPv6($ipExpression))
248 {
249 continue;
250 }
251
252 $ipCheck = @inet_pton($ipExpression);
253 if ($ipCheck === false)
254 {
255 continue;
256 }
257
258 if ($ipCheck == $myIP)
259 {
260 return true;
261 }
262 }
263 else
264 {
265 // Standard IPv4 address, i.e. 123.123.123.123 or partial IP address, i.e. 123.[123.][123.][123]
266 $dots = 0;
267 if (substr($ipExpression, -1) == '.')
268 {
269 // Partial IP address. Convert to CIDR and re-match
270 foreach (count_chars($ipExpression, 1) as $i => $val)
271 {
272 if ($i == 46)
273 {
274 $dots = $val;
275 }
276 }
277 switch ($dots)
278 {
279 case 1:
280 $netmask = '255.0.0.0';
281 $ipExpression .= '0.0.0';
282 break;
283
284 case 2:
285 $netmask = '255.255.0.0';
286 $ipExpression .= '0.0';
287 break;
288
289 case 3:
290 $netmask = '255.255.255.0';
291 $ipExpression .= '0';
292 break;
293
294 default:
295 $dots = 0;
296 }
297
298 if ($dots)
299 {
300 $binaryip = self::inet_to_bits($myIP);
301
302 // Convert netmask to CIDR
303 $long = ip2long($netmask);
304 $base = ip2long('255.255.255.255');
305 $maskbits = 32 - log(($long ^ $base) + 1, 2);
306
307 $net = @inet_pton($ipExpression);
308
309 // Sanity check
310 if ($net === false)
311 {
312 continue;
313 }
314
315 // Get the network's binary representation
316 $binarynet = self::inet_to_bits($net);
317 $expectedNumberOfBits = $ipv6 ? 128 : 24;
318 $binarynet = str_pad($binarynet, $expectedNumberOfBits, '0', STR_PAD_RIGHT);
319
320 // Check the corresponding bits of the IP and the network
321 $ip_net_bits = substr($binaryip, 0, $maskbits);
322 $net_bits = substr($binarynet, 0, $maskbits);
323
324 if ($ip_net_bits == $net_bits)
325 {
326 return true;
327 }
328 }
329 }
330 if (!$dots)
331 {
332 $ip = @inet_pton(trim($ipExpression));
333 if ($ip == $myIP)
334 {
335 return true;
336 }
337 }
338 }
339 }
340 }
341
342 return false;
343 }
344
345 /**
346 * Works around the REMOTE_ADDR not containing the user's IP
347 */
348 public static function workaroundIPIssues()
349 {
350 $ip = self::getIp();
351
352 if ($_SERVER['REMOTE_ADDR'] == $ip)
353 {
354 return;
355 }
356
357 if (array_key_exists('REMOTE_ADDR', $_SERVER))
358 {
359 $_SERVER['FOF_REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'];
360 }
361 elseif (function_exists('getenv'))
362 {
363 if (getenv('REMOTE_ADDR'))
364 {
365 $_SERVER['FOF_REMOTE_ADDR'] = getenv('REMOTE_ADDR');
366 }
367 }
368
369 $_SERVER['REMOTE_ADDR'] = $ip;
370 }
371
372 /**
373 * Should I allow the remote client's IP to be overridden by an X-Forwarded-For or Client-Ip HTTP header?
374 *
375 * @param bool $newState True to allow the override
376 *
377 * @return void
378 */
379 public static function setAllowIpOverrides($newState)
380 {
381 self::$allowIpOverrides = $newState ? true : false;
382 }
383
384 /**
385 * Gets the visitor's IP address. Automatically handles reverse proxies
386 * reporting the IPs of intermediate devices, like load balancers. Examples:
387 * https://www.akeebabackup.com/support/admin-tools/13743-double-ip-adresses-in-security-exception-log-warnings.html
388 * http://stackoverflow.com/questions/2422395/why-is-request-envremote-addr-returning-two-ips
389 * The solution used is assuming that the last IP address is the external one.
390 *
391 * @return string
392 */
393 protected static function detectAndCleanIP()
394 {
395 $ip = self::detectIP();
396
397 if ((strstr($ip, ',') !== false) || (strstr($ip, ' ') !== false))
398 {
399 $ip = str_replace(' ', ',', $ip);
400 $ip = str_replace(',,', ',', $ip);
401 $ips = explode(',', $ip);
402 $ip = '';
403 while (empty($ip) && !empty($ips))
404 {
405 $ip = array_pop($ips);
406 $ip = trim($ip);
407 }
408 }
409 else
410 {
411 $ip = trim($ip);
412 }
413
414 return $ip;
415 }
416
417 /**
418 * Gets the visitor's IP address
419 *
420 * @return string
421 */
422 protected static function detectIP()
423 {
424 // Normally the $_SERVER superglobal is set
425 if (isset($_SERVER))
426 {
427 // Do we have an x-forwarded-for HTTP header (e.g. NginX)?
428 if (self::$allowIpOverrides && array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER))
429 {
430 return $_SERVER['HTTP_X_FORWARDED_FOR'];
431 }
432
433 // Do we have a client-ip header (e.g. non-transparent proxy)?
434 if (self::$allowIpOverrides && array_key_exists('HTTP_CLIENT_IP', $_SERVER))
435 {
436 return $_SERVER['HTTP_CLIENT_IP'];
437 }
438
439 // Normal, non-proxied server or server behind a transparent proxy
440 return $_SERVER['REMOTE_ADDR'];
441 }
442
443 // This part is executed on PHP running as CGI, or on SAPIs which do
444 // not set the $_SERVER superglobal
445 // If getenv() is disabled, you're screwed
446 if (!function_exists('getenv'))
447 {
448 return '';
449 }
450
451 // Do we have an x-forwarded-for HTTP header?
452 if (self::$allowIpOverrides && getenv('HTTP_X_FORWARDED_FOR'))
453 {
454 return getenv('HTTP_X_FORWARDED_FOR');
455 }
456
457 // Do we have a client-ip header?
458 if (self::$allowIpOverrides && getenv('HTTP_CLIENT_IP'))
459 {
460 return getenv('HTTP_CLIENT_IP');
461 }
462
463 // Normal, non-proxied server or server behind a transparent proxy
464 if (getenv('REMOTE_ADDR'))
465 {
466 return getenv('REMOTE_ADDR');
467 }
468
469 // Catch-all case for broken servers, apparently
470 return '';
471 }
472
473 /**
474 * Converts inet_pton output to bits string
475 *
476 * @param string $inet The in_addr representation of an IPv4 or IPv6 address
477 *
478 * @return string
479 */
480 protected static function inet_to_bits($inet)
481 {
482 if (strlen($inet) == 4)
483 {
484 $unpacked = unpack('A4', $inet);
485 }
486 else
487 {
488 $unpacked = unpack('A16', $inet);
489 }
490 $unpacked = str_split($unpacked[1]);
491 $binaryip = '';
492
493 foreach ($unpacked as $char)
494 {
495 $binaryip .= str_pad(decbin(ord($char)), 8, '0', STR_PAD_LEFT);
496 }
497
498 return $binaryip;
499 }
500
501 /**
502 * Checks if an IPv6 address $ip is part of the IPv6 CIDR block $cidrnet
503 *
504 * @param string $ip The IPv6 address to check, e.g. 21DA:00D3:0000:2F3B:02AC:00FF:FE28:9C5A
505 * @param string $cidrnet The IPv6 CIDR block, e.g. 21DA:00D3:0000:2F3B::/64
506 *
507 * @return bool
508 */
509 protected static function checkIPv6CIDR($ip, $cidrnet)
510 {
511 $ip = inet_pton($ip);
512 $binaryip=self::inet_to_bits($ip);
513
514 list($net,$maskbits)=explode('/',$cidrnet);
515 $net=inet_pton($net);
516 $binarynet=self::inet_to_bits($net);
517
518 $ip_net_bits=substr($binaryip,0,$maskbits);
519 $net_bits =substr($binarynet,0,$maskbits);
520
521 return $ip_net_bits === $net_bits;
522 }
523 }