IpRegion.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. <?php
  2. namespace app\extra\tools;
  3. class IpRegion
  4. {
  5. const IPV4_VERSION_NO = 4;
  6. const IPV6_VERSION_NO = 6;
  7. const XDB_HEADER_LENGTH = 256;
  8. const VECTOR_INDEX_ROWS = 256;
  9. const VECTOR_INDEX_COLS = 256;
  10. const VECTOR_INDEX_SIZE = 8;
  11. /**
  12. * @var string
  13. */
  14. private $dbFileV4;
  15. /**
  16. * @var string
  17. */
  18. private $dbFileV6;
  19. /**
  20. * @var string|null
  21. */
  22. private $contentBuffV4 = null;
  23. /**
  24. * @var string|null
  25. */
  26. private $contentBuffV6 = null;
  27. /**
  28. * ipRegion info
  29. */
  30. private $ip = '';
  31. private $ip_address = '';
  32. private $ip_address_list = array();
  33. private $country = '';
  34. private $province = '';
  35. private $city = '';
  36. private $isp = '';
  37. private $isoCode = '';
  38. /**
  39. * @param string|null $ipv4DbFile
  40. * @param string|null $ipv6DbFile
  41. */
  42. public function __construct($ipv4DbFile = null, $ipv6DbFile = null)
  43. {
  44. $baseDir = __DIR__;
  45. $this->dbFileV4 = $ipv4DbFile ?: $baseDir . '/ip2region_v4.xdb';
  46. $this->dbFileV6 = $ipv6DbFile ?: $baseDir . '/ip2region_v6.xdb';
  47. }
  48. /**
  49. * @param string $ip
  50. * @return $this|null
  51. */
  52. public function setIp($ip)
  53. {
  54. $this->resetRegion();
  55. $this->ip = $ip;
  56. $ipBytes = $this->parseIP($ip);
  57. if ($ipBytes === null) {
  58. throw new \InvalidArgumentException("Invalid ip address: {$ip}");
  59. }
  60. $version = $this->getVersionMeta($ipBytes);
  61. $contentBuff = $this->loadContentBuffer($version['version_no'], $version['db_file']);
  62. $region = $this->searchByBytes($version, $contentBuff, $ipBytes);
  63. if ($region === '') {
  64. return null;
  65. }
  66. $this->fillRegion($region);
  67. return $this;
  68. }
  69. /**
  70. * @return string
  71. */
  72. public function getIpAddress()
  73. {
  74. return $this->ip_address;
  75. }
  76. /**
  77. * @return string
  78. */
  79. public function getCountry()
  80. {
  81. return $this->country;
  82. }
  83. /**
  84. * @return string
  85. */
  86. public function getProvince()
  87. {
  88. return $this->province;
  89. }
  90. /**
  91. * @return string
  92. */
  93. public function getCity()
  94. {
  95. return $this->city;
  96. }
  97. /**
  98. * @return string
  99. */
  100. public function getIsp()
  101. {
  102. return $this->isp;
  103. }
  104. /**
  105. * @return string
  106. */
  107. public function getIsoCode()
  108. {
  109. return $this->isoCode;
  110. }
  111. /**
  112. * @return array
  113. */
  114. public function getIpAddressList()
  115. {
  116. return $this->ip_address_list;
  117. }
  118. /**
  119. * @param string $ipString
  120. * @return string|null
  121. */
  122. private function parseIP($ipString)
  123. {
  124. $flag = FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6;
  125. if (!filter_var($ipString, FILTER_VALIDATE_IP, $flag)) {
  126. return null;
  127. }
  128. return inet_pton($ipString);
  129. }
  130. /**
  131. * @param string $ipBytes
  132. * @return array
  133. */
  134. private function getVersionMeta($ipBytes)
  135. {
  136. if (strlen($ipBytes) === 4) {
  137. return array(
  138. 'version_no' => self::IPV4_VERSION_NO,
  139. 'bytes' => 4,
  140. 'segment_index_size' => 14,
  141. 'db_file' => $this->dbFileV4,
  142. );
  143. }
  144. return array(
  145. 'version_no' => self::IPV6_VERSION_NO,
  146. 'bytes' => 16,
  147. 'segment_index_size' => 38,
  148. 'db_file' => $this->dbFileV6,
  149. );
  150. }
  151. /**
  152. * @param int $versionNo
  153. * @param string $dbFile
  154. * @return string
  155. */
  156. private function loadContentBuffer($versionNo, $dbFile)
  157. {
  158. if ($versionNo === self::IPV4_VERSION_NO) {
  159. if ($this->contentBuffV4 === null) {
  160. $this->contentBuffV4 = $this->loadContentFromFile($dbFile);
  161. }
  162. return $this->contentBuffV4;
  163. }
  164. if ($this->contentBuffV6 === null) {
  165. $this->contentBuffV6 = $this->loadContentFromFile($dbFile);
  166. }
  167. return $this->contentBuffV6;
  168. }
  169. /**
  170. * @param string $dbFile
  171. * @return string
  172. */
  173. private function loadContentFromFile($dbFile)
  174. {
  175. $handle = fopen($dbFile, 'rb');
  176. if ($handle === false) {
  177. throw new \RuntimeException("Fail to open xdb file {$dbFile}");
  178. }
  179. $verifyError = $this->verifyHandle($handle);
  180. if ($verifyError !== null) {
  181. fclose($handle);
  182. throw new \RuntimeException("Invalid xdb file {$dbFile}: {$verifyError}");
  183. }
  184. if (fseek($handle, 0, SEEK_END) === -1) {
  185. fclose($handle);
  186. throw new \RuntimeException("Fail to seek xdb file {$dbFile}");
  187. }
  188. $size = ftell($handle);
  189. if ($size === false) {
  190. fclose($handle);
  191. throw new \RuntimeException("Fail to stat xdb file {$dbFile}");
  192. }
  193. if (fseek($handle, 0) === -1) {
  194. fclose($handle);
  195. throw new \RuntimeException("Fail to rewind xdb file {$dbFile}");
  196. }
  197. $contentBuff = fread($handle, $size);
  198. fclose($handle);
  199. if ($contentBuff === false || strlen($contentBuff) != $size) {
  200. throw new \RuntimeException("Fail to load xdb file {$dbFile}");
  201. }
  202. return $contentBuff;
  203. }
  204. /**
  205. * @param resource $handle
  206. * @return string|null
  207. */
  208. private function verifyHandle($handle)
  209. {
  210. $header = $this->loadHeader($handle);
  211. if ($header === null) {
  212. return 'failed to load the header';
  213. }
  214. if ($header['version'] == 2) {
  215. $runtimePtrBytes = 4;
  216. } elseif ($header['version'] == 3) {
  217. $runtimePtrBytes = $header['runtimePtrBytes'];
  218. } else {
  219. return "invalid structure version `{$header['version']}`";
  220. }
  221. $stat = fstat($handle);
  222. if ($stat === false) {
  223. return 'failed to stat the xdb file';
  224. }
  225. $maxFilePtr = (1 << ($runtimePtrBytes * 8)) - 1;
  226. if ($stat['size'] > $maxFilePtr) {
  227. return "xdb file exceeds the maximum supported bytes: {$maxFilePtr}";
  228. }
  229. return null;
  230. }
  231. /**
  232. * @param resource $handle
  233. * @return array|null
  234. */
  235. private function loadHeader($handle)
  236. {
  237. if (fseek($handle, 0) === -1) {
  238. return null;
  239. }
  240. $buff = fread($handle, self::XDB_HEADER_LENGTH);
  241. if ($buff === false || strlen($buff) != self::XDB_HEADER_LENGTH) {
  242. return null;
  243. }
  244. return array(
  245. 'version' => $this->leGetUint16($buff, 0),
  246. 'indexPolicy' => $this->leGetUint16($buff, 2),
  247. 'createdAt' => $this->leGetUint32($buff, 4),
  248. 'startIndexPtr' => $this->leGetUint32($buff, 8),
  249. 'endIndexPtr' => $this->leGetUint32($buff, 12),
  250. 'ipVersion' => $this->leGetUint16($buff, 16),
  251. 'runtimePtrBytes' => $this->leGetUint16($buff, 18),
  252. );
  253. }
  254. /**
  255. * @param array $version
  256. * @param string $contentBuff
  257. * @param string $ipBytes
  258. * @return string
  259. */
  260. private function searchByBytes(array $version, $contentBuff, $ipBytes)
  261. {
  262. if (strlen($ipBytes) != $version['bytes']) {
  263. throw new \InvalidArgumentException('invalid ip address version');
  264. }
  265. $il0 = ord($ipBytes[0]) & 0xFF;
  266. $il1 = ord($ipBytes[1]) & 0xFF;
  267. $idx = $il0 * self::VECTOR_INDEX_COLS * self::VECTOR_INDEX_SIZE + $il1 * self::VECTOR_INDEX_SIZE;
  268. $sPtr = $this->leGetUint32($contentBuff, self::XDB_HEADER_LENGTH + $idx);
  269. $ePtr = $this->leGetUint32($contentBuff, self::XDB_HEADER_LENGTH + $idx + 4);
  270. if ($sPtr == 0 || $ePtr == 0) {
  271. return '';
  272. }
  273. $bytes = $version['bytes'];
  274. $dataOffset = $bytes << 1;
  275. $idxSize = $version['segment_index_size'];
  276. $dataLen = 0;
  277. $dataPtr = 0;
  278. $l = 0;
  279. $h = ($ePtr - $sPtr) / $idxSize;
  280. while ($l <= $h) {
  281. $m = ($l + $h) >> 1;
  282. $p = $sPtr + $m * $idxSize;
  283. $buff = substr($contentBuff, $p, $idxSize);
  284. if ($this->compareIpBytes($version['version_no'], $ipBytes, $buff, 0) < 0) {
  285. $h = $m - 1;
  286. } elseif ($this->compareIpBytes($version['version_no'], $ipBytes, $buff, $bytes) > 0) {
  287. $l = $m + 1;
  288. } else {
  289. $dataLen = $this->leGetUint16($buff, $dataOffset);
  290. $dataPtr = $this->leGetUint32($buff, $dataOffset + 2);
  291. break;
  292. }
  293. }
  294. if ($dataLen == 0) {
  295. return '';
  296. }
  297. return substr($contentBuff, $dataPtr, $dataLen);
  298. }
  299. /**
  300. * @param int $versionNo
  301. * @param string $ipBytes
  302. * @param string $buff
  303. * @param int $offset
  304. * @return int
  305. */
  306. private function compareIpBytes($versionNo, $ipBytes, $buff, $offset)
  307. {
  308. if ($versionNo === self::IPV4_VERSION_NO) {
  309. $len = strlen($ipBytes);
  310. $end = $offset + $len;
  311. for ($i = 0, $j = $end - 1; $i < $len; $i++, $j--) {
  312. $left = ord($ipBytes[$i]) & 0xFF;
  313. $right = ord($buff[$j]) & 0xFF;
  314. if ($left > $right) {
  315. return 1;
  316. }
  317. if ($left < $right) {
  318. return -1;
  319. }
  320. }
  321. return 0;
  322. }
  323. $result = strcmp($ipBytes, substr($buff, $offset, strlen($ipBytes)));
  324. if ($result < 0) {
  325. return -1;
  326. }
  327. if ($result > 0) {
  328. return 1;
  329. }
  330. return 0;
  331. }
  332. /**
  333. * @param string $region
  334. */
  335. private function fillRegion($region)
  336. {
  337. $parts = explode('|', $region);
  338. $this->country = isset($parts[0]) ? $this->normalizeField($parts[0]) : '';
  339. $this->province = isset($parts[1]) ? $this->normalizeField($parts[1]) : '';
  340. $this->city = isset($parts[2]) ? $this->normalizeField($parts[2]) : '';
  341. $this->isp = isset($parts[3]) ? $this->normalizeField($parts[3]) : '';
  342. $this->isoCode = isset($parts[4]) ? $this->normalizeField($parts[4]) : '';
  343. $addressParts = array_filter(
  344. array($this->country, $this->province, $this->city),
  345. function ($value) {
  346. return $value !== '';
  347. }
  348. );
  349. $this->ip_address = implode(' ', $addressParts);
  350. $this->ip_address_list = array(
  351. 'ip_address' => $this->ip_address,
  352. 'country' => $this->country,
  353. 'province' => $this->province,
  354. 'city' => $this->city,
  355. 'isp' => $this->isp,
  356. 'iso_code' => $this->isoCode,
  357. );
  358. }
  359. private function resetRegion()
  360. {
  361. $this->ip_address = '';
  362. $this->ip_address_list = array();
  363. $this->country = '';
  364. $this->province = '';
  365. $this->city = '';
  366. $this->isp = '';
  367. $this->isoCode = '';
  368. }
  369. /**
  370. * @param string $value
  371. * @return string
  372. */
  373. private function normalizeField($value)
  374. {
  375. $value = trim($value);
  376. if ($value === '0') {
  377. return '';
  378. }
  379. return $value;
  380. }
  381. /**
  382. * @param string $buffer
  383. * @param int $offset
  384. * @return int|string
  385. */
  386. private function leGetUint32($buffer, $offset)
  387. {
  388. $value = (ord($buffer[$offset])) | (ord($buffer[$offset + 1]) << 8)
  389. | (ord($buffer[$offset + 2]) << 16) | (ord($buffer[$offset + 3]) << 24);
  390. if ($value < 0 && PHP_INT_SIZE == 4) {
  391. $value = sprintf('%u', $value);
  392. }
  393. return $value;
  394. }
  395. /**
  396. * @param string $buffer
  397. * @param int $offset
  398. * @return int
  399. */
  400. private function leGetUint16($buffer, $offset)
  401. {
  402. return (ord($buffer[$offset])) | (ord($buffer[$offset + 1]) << 8);
  403. }
  404. public function __destruct()
  405. {
  406. $this->contentBuffV4 = null;
  407. $this->contentBuffV6 = null;
  408. }
  409. }