PageRenderTime 37ms CodeModel.GetById 14ms RepoModel.GetById 1ms app.codeStats 0ms

/core/IP.php

https://github.com/quarkness/piwik
PHP | 606 lines | 378 code | 63 blank | 165 comment | 83 complexity | ac53244219e26c144dd4ad7d0da2c1c4 MD5 | raw file
  1. <?php
  2. /**
  3. * Piwik - Open source web analytics
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. * @version $Id$
  8. *
  9. * @category Piwik
  10. * @package Piwik
  11. */
  12. if(Piwik_Common::isWindows() || !function_exists('inet_ntop')) {
  13. function _inet_ntop($in_addr) {
  14. return php_compat_inet_ntop($in_addr);
  15. }
  16. } else {
  17. function _inet_ntop($in_addr) {
  18. return inet_ntop($in_addr);
  19. }
  20. }
  21. if(Piwik_Common::isWindows() || !function_exists('inet_pton')) {
  22. function _inet_pton($address) {
  23. return php_compat_inet_pton($address);
  24. }
  25. } else {
  26. function _inet_pton($address) {
  27. return inet_pton($address);
  28. }
  29. }
  30. /**
  31. * Handling IP addresses (both IPv4 and IPv6).
  32. *
  33. * As of Piwik 1.3, IP addresses are stored in the DB has VARBINARY(16),
  34. * and passed around in network address format which has the advantage of
  35. * being in big-endian byte order, allowing for binary-safe string
  36. * comparison of addresses (of the same length), even on Intel x86.
  37. *
  38. * As a matter of naming convention, we use $ip for the network address format
  39. * and $ipString for the presentation format (i.e., human-readable form).
  40. *
  41. * We're not using the network address format (in_addr) for socket functions,
  42. * so we don't have to worry about incompatibility with Windows UNICODE
  43. * and inetPtonW().
  44. *
  45. * @package Piwik
  46. */
  47. class Piwik_IP
  48. {
  49. /**
  50. * Sanitize human-readable IP address.
  51. *
  52. * @param string $ipString IP address
  53. * @return string|false
  54. */
  55. static public function sanitizeIp($ipString)
  56. {
  57. $ipString = trim($ipString);
  58. // CIDR notation, A.B.C.D/E
  59. $posSlash = strrpos($ipString, '/');
  60. if($posSlash !== false)
  61. {
  62. $ipString = substr($ipString, 0, $posSlash);
  63. }
  64. $posColon = strrpos($ipString, ':');
  65. $posDot = strrpos($ipString, '.');
  66. if($posColon !== false)
  67. {
  68. // IPv6 address with port, [A:B:C:D:E:F:G:H]:EEEE
  69. $posRBrac = strrpos($ipString, ']');
  70. if($posRBrac !== false && $ipString[0] == '[')
  71. {
  72. $ipString = substr($ipString, 1, $posRBrac - 1);
  73. }
  74. if($posDot !== false)
  75. {
  76. // IPv4 address with port, A.B.C.D:EEEE
  77. if($posColon > $posDot)
  78. {
  79. $ipString = substr($ipString, 0, $posColon);
  80. }
  81. // else: Dotted quad IPv6 address, A:B:C:D:E:F:G.H.I.J
  82. }
  83. else if(strpos($ipString, ':') === $posColon)
  84. {
  85. $ipString = substr($ipString, 0, $posColon);
  86. }
  87. // else: IPv6 address, A:B:C:D:E:F:G:H
  88. }
  89. // else: IPv4 address, A.B.C.D
  90. return $ipString;
  91. }
  92. /**
  93. * Sanitize human-readable (user-supplied) IP address range.
  94. *
  95. * Accepts the following formats for $ipRange:
  96. * - single IPv4 address, e.g., 127.0.0.1
  97. * - single IPv6 address, e.g., ::1/128
  98. * - IPv4 block using CIDR notation, e.g., 192.168.0.0/22 represents the IPv4 addresses from 192.168.0.0 to 192.168.3.255
  99. * - IPv6 block using CIDR notation, e.g., 2001:DB8::/48 represents the IPv6 addresses from 2001:DB8:0:0:0:0:0:0 to 2001:DB8:0:FFFF:FFFF:FFFF:FFFF:FFFF
  100. * - wildcards, e.g., 192.168.0.*
  101. *
  102. * @param string $ipRangeString IP address range
  103. * @return string|false IP address range in CIDR notation
  104. */
  105. static public function sanitizeIpRange($ipRangeString)
  106. {
  107. $ipRangeString = trim($ipRangeString);
  108. if(empty($ipRangeString))
  109. {
  110. return false;
  111. }
  112. // IPv4 address with wildcards '*'
  113. if(strpos($ipRangeString, '*') !== false)
  114. {
  115. if(preg_match('~(^|\.)\*\.\d+(\.|$)~D', $ipRangeString))
  116. {
  117. return false;
  118. }
  119. $bits = 32 - 8 * substr_count($ipRangeString, '*');
  120. $ipRangeString = str_replace('*', '0', $ipRangeString);
  121. }
  122. // CIDR
  123. if(($pos = strpos($ipRangeString, '/')) !== false)
  124. {
  125. $bits = substr($ipRangeString, $pos + 1);
  126. $ipRangeString = substr($ipRangeString, 0, $pos);
  127. }
  128. // single IP
  129. if(($ip = @_inet_pton($ipRangeString)) === false)
  130. return false;
  131. $maxbits = Piwik_Common::strlen($ip) * 8;
  132. if(!isset($bits))
  133. $bits = $maxbits;
  134. if($bits < 0 || $bits > $maxbits)
  135. {
  136. return false;
  137. }
  138. return "$ipRangeString/$bits";
  139. }
  140. /**
  141. * Convert presentation format IP address to network address format
  142. *
  143. * @param string $ipString IP address, either IPv4 or IPv6, e.g., "127.0.0.1"
  144. * @return string Binary-safe string, e.g., "\x7F\x00\x00\x01"
  145. */
  146. static public function P2N($ipString)
  147. {
  148. // use @inet_pton() because it throws an exception and E_WARNING on invalid input
  149. $ip = @_inet_pton($ipString);
  150. return $ip === false ? "\x00\x00\x00\x00" : $ip;
  151. }
  152. /**
  153. * Convert network address format to presentation format
  154. *
  155. * @see prettyPrint()
  156. *
  157. * @param string $ip IP address in network address format
  158. * @return string IP address in presentation format
  159. */
  160. static public function N2P($ip)
  161. {
  162. // use @inet_ntop() because it throws an exception and E_WARNING on invalid input
  163. $ipStr = @_inet_ntop($ip);
  164. return $ipStr === false ? '0.0.0.0' : $ipStr;
  165. }
  166. /**
  167. * Alias for N2P()
  168. *
  169. * @param string $ip IP address in network address format
  170. * @return string IP address in presentation format
  171. */
  172. static public function prettyPrint($ip)
  173. {
  174. return self::N2P($ip);
  175. }
  176. /**
  177. * Convert IP address (in network address format) to presentation format.
  178. * This is a backward compatibility function for code that only expects
  179. * IPv4 addresses (i.e., doesn't support IPv6).
  180. *
  181. * This function does not support the long (or its string representation)
  182. * returned by the built-in ip2long() function, from Piwik 1.3 and earlier.
  183. *
  184. * @param string $ip IPv4 address in network address format
  185. * @return string IP address in presentation format
  186. */
  187. static public function long2ip($ip)
  188. {
  189. // IPv4
  190. if(Piwik_Common::strlen($ip) == 4)
  191. {
  192. return self::N2P($ip);
  193. }
  194. // IPv6 - transitional address?
  195. if(Piwik_Common::strlen($ip) == 16)
  196. {
  197. if(substr_compare($ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff", 0, 12) === 0
  198. || substr_compare($ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 0, 12) === 0)
  199. {
  200. // remap 128-bit IPv4-mapped and IPv4-compat addresses
  201. return self::N2P(Piwik_Common::substr($ip, 12));
  202. }
  203. }
  204. return '0.0.0.0';
  205. }
  206. /**
  207. * Get low and high IP addresses for a specified range.
  208. *
  209. * @param array $ipRange An IP address range in presentation format
  210. * @return array|false Array ($lowIp, $highIp) in network address format, or false if failure
  211. */
  212. static public function getIpsForRange($ipRange)
  213. {
  214. if(strpos($ipRange, '/') === false)
  215. {
  216. $ipRange = self::sanitizeIpRange($ipRange);
  217. }
  218. $pos = strpos($ipRange, '/');
  219. $bits = substr($ipRange, $pos + 1);
  220. $range = substr($ipRange, 0, $pos);
  221. $high = $low = @_inet_pton($range);
  222. if($low === false)
  223. {
  224. return false;
  225. }
  226. $lowLen = Piwik_Common::strlen($low);
  227. $i = $lowLen - 1;
  228. $bits = $lowLen * 8 - $bits;
  229. for($n = (int)($bits / 8); $n > 0; $n--, $i--)
  230. {
  231. $low[$i] = chr(0);
  232. $high[$i] = chr(255);
  233. }
  234. $n = $bits % 8;
  235. if($n)
  236. {
  237. $low[$i] = chr(ord($low[$i]) & ~((1 << $n) - 1));
  238. $high[$i] = chr(ord($high[$i]) | ((1 << $n) - 1));
  239. }
  240. return array($low, $high);
  241. }
  242. /**
  243. * Determines if an IP address is in a specified IP address range.
  244. *
  245. * An IPv4-mapped address should be range checked with an IPv4-mapped address range.
  246. *
  247. * @param string $ip IP address in network address format
  248. * @param array $ipRanges List of IP address ranges
  249. * @return bool True if in any of the specified IP address ranges; else false.
  250. */
  251. static public function isIpInRange($ip, $ipRanges)
  252. {
  253. $ipLen = Piwik_Common::strlen($ip);
  254. if(empty($ip) || empty($ipRanges) || ($ipLen != 4 && $ipLen != 16))
  255. {
  256. return false;
  257. }
  258. foreach($ipRanges as $range)
  259. {
  260. if(is_array($range))
  261. {
  262. // already split into low/high IP addresses
  263. $range[0] = self::P2N($range[0]);
  264. $range[1] = self::P2N($range[1]);
  265. }
  266. else
  267. {
  268. // expect CIDR format but handle some variations
  269. $range = self::getIpsForRange($range);
  270. }
  271. if($range === false)
  272. {
  273. continue;
  274. }
  275. $low = $range[0];
  276. $high = $range[1];
  277. if(Piwik_Common::strlen($low) != $ipLen)
  278. {
  279. continue;
  280. }
  281. // binary-safe string comparison
  282. if($ip >= $low && $ip <= $high)
  283. {
  284. return true;
  285. }
  286. }
  287. return false;
  288. }
  289. /**
  290. * Returns the best possible IP of the current user, in the format A.B.C.D
  291. * For example, this could be the proxy client's IP address.
  292. *
  293. * @return string IP address in presentation format
  294. */
  295. static public function getIpFromHeader()
  296. {
  297. $clientHeaders = null;
  298. if(!empty($GLOBALS['PIWIK_TRACKER_MODE']))
  299. {
  300. $clientHeaders = @Piwik_Tracker_Config::getInstance()->General['proxy_client_headers'];
  301. }
  302. else
  303. {
  304. $config = Zend_Registry::get('config');
  305. if($config !== false && isset($config->General->proxy_client_headers))
  306. {
  307. $clientHeaders = $config->General->proxy_client_headers->toArray();
  308. }
  309. }
  310. if(!is_array($clientHeaders))
  311. {
  312. $clientHeaders = array();
  313. }
  314. $default = '0.0.0.0';
  315. if(isset($_SERVER['REMOTE_ADDR']))
  316. {
  317. $default = $_SERVER['REMOTE_ADDR'];
  318. }
  319. $ipString = self::getNonProxyIpFromHeader($default, $clientHeaders);
  320. return self::sanitizeIp($ipString);
  321. }
  322. /**
  323. * Returns a non-proxy IP address from header
  324. *
  325. * @param string $default Default value to return if no matching proxy header
  326. * @param array $proxyHeaders List of proxy headers
  327. * @return string
  328. */
  329. static public function getNonProxyIpFromHeader($default, $proxyHeaders)
  330. {
  331. $proxyIps = null;
  332. if(!empty($GLOBALS['PIWIK_TRACKER_MODE']))
  333. {
  334. $proxyIps = @Piwik_Tracker_Config::getInstance()->General['proxy_ips'];
  335. }
  336. else
  337. {
  338. try {
  339. $config = Zend_Registry::get('config');
  340. } catch(Exception $e) {
  341. $config = false;
  342. }
  343. if($config !== false && isset($config->General->proxy_ips))
  344. {
  345. $proxyIps = $config->General->proxy_ips->toArray();
  346. }
  347. }
  348. if(!is_array($proxyIps))
  349. {
  350. $proxyIps = array();
  351. }
  352. $proxyIps[] = $default;
  353. // examine proxy headers
  354. foreach($proxyHeaders as $proxyHeader)
  355. {
  356. if(!empty($_SERVER[$proxyHeader]))
  357. {
  358. $proxyIp = self::getLastIpFromList($_SERVER[$proxyHeader], $proxyIps);
  359. if(strlen($proxyIp) && stripos($proxyIp, 'unknown') === false)
  360. {
  361. return $proxyIp;
  362. }
  363. }
  364. }
  365. return $default;
  366. }
  367. /**
  368. * Returns the last IP address in a comma separated list, subject to an optional exclusion list.
  369. *
  370. * @param string $csv Comma separated list of elements
  371. * @param array $excludedIps Optional list of excluded IP addresses (or IP address ranges)
  372. * @return string Last (non-excluded) IP address in the list
  373. */
  374. static public function getLastIpFromList($csv, $excludedIps = null)
  375. {
  376. $p = strrpos($csv, ',');
  377. if($p !== false)
  378. {
  379. $elements = explode(',', $csv);
  380. for($i = count($elements); $i--; )
  381. {
  382. $element = trim(Piwik_Common::sanitizeInputValue($elements[$i]));
  383. if(empty($excludedIps) || (!in_array($element, $excludedIps) && !self::isIpInRange(self::P2N(self::sanitizeIp($element)), $excludedIps)))
  384. {
  385. return $element;
  386. }
  387. }
  388. }
  389. return trim(Piwik_Common::sanitizeInputValue($csv));
  390. }
  391. /**
  392. * Get hostname for a given IP address
  393. *
  394. * @param string $ipStr Human-readable IP address
  395. * @return string Hostname or unmodified $ipStr if failure
  396. */
  397. static public function getHostByAddr($ipStr)
  398. {
  399. // PHP's reverse lookup supports ipv4 and ipv6
  400. // except on Windows before PHP 5.3
  401. $host = strtolower(@gethostbyaddr($ipStr));
  402. return $host === '' ? $ipStr : $host;
  403. }
  404. }
  405. /**
  406. * Converts a packed internet address to a human readable representation
  407. *
  408. * @link http://php.net/inet_ntop
  409. *
  410. * @param string $in_addr 32-bit IPv4 or 128-bit IPv6 address
  411. * @return string|false string representation of address or false on failure
  412. */
  413. function php_compat_inet_ntop($in_addr)
  414. {
  415. $r = bin2hex($in_addr);
  416. switch (Piwik_Common::strlen($in_addr))
  417. {
  418. case 4:
  419. // IPv4 address
  420. $prefix = '';
  421. break;
  422. case 16:
  423. // IPv4-mapped address
  424. if(substr_compare($r, '00000000000000000000ffff', 0, 24) === 0)
  425. {
  426. $prefix = '::ffff:';
  427. $r = substr($r, 24);
  428. break;
  429. }
  430. // IPv4-compat address
  431. if(substr_compare($r, '000000000000000000000000', 0, 24) === 0 &&
  432. substr_compare($r, '0000', 24, 4) !== 0)
  433. {
  434. $prefix = '::';
  435. $r = substr($r, 24);
  436. break;
  437. }
  438. $r = str_split($r, 4);
  439. $r = implode(':', $r);
  440. // compress leading zeros
  441. $r = preg_replace(
  442. '/(^|:)0{1,3}/',
  443. '$1',
  444. $r
  445. );
  446. // compress longest (and leftmost) consecutive groups of zeros
  447. if(preg_match_all('/(?:^|:)(0(:|$))+/D', $r, $matches))
  448. {
  449. $longestMatch = 0;
  450. foreach($matches[0] as $aMatch)
  451. {
  452. if(strlen($aMatch) > strlen($longestMatch))
  453. {
  454. $longestMatch = $aMatch;
  455. }
  456. }
  457. $r = substr_replace($r, '::', strpos($r, $longestMatch), strlen($longestMatch));
  458. }
  459. return $r;
  460. default:
  461. return false;
  462. }
  463. $r = str_split($r, 2);
  464. $r = array_map('hexdec', $r);
  465. $r = implode('.', $r);
  466. return $prefix . $r;
  467. }
  468. /**
  469. * Converts a human readable IP address to its packed in_addr representation
  470. *
  471. * @link http://php.net/inet_pton
  472. *
  473. * @param string $address a human readable IPv4 or IPv6 address
  474. * @return string in_addr representation or false on failure
  475. */
  476. function php_compat_inet_pton($address)
  477. {
  478. // IPv4 (or IPv4-compat, or IPv4-mapped)
  479. if(preg_match('/(^|:)([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$/iD', $address, $matches))
  480. {
  481. for($i = count($matches); $i-- > 2; )
  482. {
  483. if($matches[$i] > 255 ||
  484. ($matches[$i][0] == '0' && strlen($matches[$i]) > 1))
  485. {
  486. return false;
  487. }
  488. }
  489. if(empty($matches[1]))
  490. {
  491. $r = ip2long($address);
  492. if($r === false)
  493. {
  494. return false;
  495. }
  496. return pack('N', $r);
  497. }
  498. $suffix = sprintf("%02x%02x:%02x%02x", $matches[2], $matches[3], $matches[4], $matches[5]);
  499. $address = substr_replace($address, $matches[1] . $suffix, strrpos($address, $matches[0]));
  500. }
  501. // IPv6
  502. if(strpos($address, ':') === false ||
  503. strspn($address, '01234567890abcdefABCDEF:') !== strlen($address))
  504. {
  505. return false;
  506. }
  507. if(substr($address, 0, 2) == '::')
  508. {
  509. $address = '0'.$address;
  510. }
  511. if(substr($address, -2) == '::')
  512. {
  513. $address .= '0';
  514. }
  515. $r = explode(':', $address);
  516. $count = count($r);
  517. // grouped zeros
  518. if(strpos($address, '::') !== false
  519. && $count < 8)
  520. {
  521. $zeroGroup = array_search('', $r, 1);
  522. // we're replacing this cell, so we splice (8 - $count + 1) cells containing '0'
  523. array_splice($r, $zeroGroup, 1, array_fill(0, 9 - $count, '0'));
  524. }
  525. // guard against excessive ':' or '::'
  526. if($count > 8 ||
  527. array_search('', $r, 1) !== false)
  528. {
  529. return false;
  530. }
  531. // leading zeros
  532. foreach($r as $v)
  533. {
  534. if(strlen(ltrim($v, '0')) > 4)
  535. {
  536. return false;
  537. }
  538. }
  539. $r = array_map('hexdec', $r);
  540. array_unshift($r, 'n*');
  541. $r = call_user_func_array('pack', $r);
  542. return $r;
  543. }