diff --git a/src/service/ip/IpIpDistrictInfo.php b/src/service/ip/IpIpDistrictInfo.php new file mode 100644 index 0000000000000000000000000000000000000000..6ce438333d361e59d695b89525f09ac3511fb142 --- /dev/null +++ b/src/service/ip/IpIpDistrictInfo.php @@ -0,0 +1,42 @@ + $value) { + $this->{$field} = $value; + } + } + + public function __get($name) + { + return $this->{$name}; + } +} diff --git a/src/service/ip/IpIpReader.php b/src/service/ip/IpIpReader.php new file mode 100644 index 0000000000000000000000000000000000000000..2ee943ae44934b3701c052cd1683cbb01f96479c --- /dev/null +++ b/src/service/ip/IpIpReader.php @@ -0,0 +1,276 @@ +database = $database; + + $this->init(); + } + + private function init() + { + if (is_readable($this->database) === FALSE) { + throw new InvalidArgumentException("The IP Database file \"{$this->database}\" does not exist or is not readable."); + } + $this->file = @fopen($this->database, 'rb'); + if ($this->file === FALSE) { + throw new InvalidArgumentException("IP Database File opening \"{$this->database}\"."); + } + $this->fileSize = @filesize($this->database); + if ($this->fileSize === FALSE) { + throw new \UnexpectedValueException("Error determining the size of \"{$this->database}\"."); + } + + $metaLength = unpack('N', fread($this->file, 4))[1]; + $text = fread($this->file, $metaLength); + + $this->meta = json_decode($text, 1); + + if (isset($this->meta['fields']) === FALSE || isset($this->meta['languages']) === FALSE) { + throw new Exception('IP Database metadata error.'); + } + + $fileSize = 4 + $metaLength + $this->meta['total_size']; + if ($fileSize != $this->fileSize) { + throw new Exception('IP Database size error.'); + } + + $this->nodeCount = $this->meta['node_count']; + $this->nodeOffset = 4 + $metaLength; + } + + /** + * @param $ip + * @param string $language + * @return array|NULL + */ + public function find($ip, $language) + { + if (is_resource($this->file) === FALSE) { + throw new BadMethodCallException('IPIP DB closed.'); + } + + if (isset($this->meta['languages'][$language]) === FALSE) { + throw new InvalidArgumentException("language : {$language} not support."); + } + + if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6) === FALSE) { + throw new InvalidArgumentException("The value \"$ip\" is not a valid IP address."); + } + + if (strpos($ip, '.') !== FALSE && !$this->supportV4()) { + throw new InvalidArgumentException("The Database not support IPv4 address."); + } elseif (strpos($ip, ':') !== FALSE && !$this->supportV6()) { + throw new InvalidArgumentException("The Database not support IPv6 address."); + } + + try { + $node = $this->findNode($ip); + + if ($node > 0) { + $data = $this->resolve($node); + + $values = explode("\t", $data); + + return array_slice($values, $this->meta['languages'][$language], count($this->meta['fields'])); + } + } catch (Exception $e) { + return NULL; + } + + return NULL; + } + + public function findMap($ip, $language) + { + $array = $this->find($ip, $language); + if (NULL === $array) { + return NULL; + } + + return array_combine($this->meta['fields'], $array); + } + + private $v4offset = 0; + private $v6offsetCache = []; + + /** + * @param $ip + * @return int + * @throws Exception + */ + private function findNode($ip) + { + $binary = inet_pton($ip); + $bitCount = strlen($binary) * 8; // 32 | 128 + $key = substr($binary, 0, 2); + $node = 0; + $index = 0; + if ($bitCount === 32) { + if ($this->v4offset === 0) { + for ($i = 0; $i < 96 && $node < $this->nodeCount; $i++) { + if ($i >= 80) { + $idx = 1; + } else { + $idx = 0; + } + $node = $this->readNode($node, $idx); + if ($node > $this->nodeCount) { + return 0; + } + } + $this->v4offset = $node; + } else { + $node = $this->v4offset; + } + } else { + if (isset($this->v6offsetCache[$key])) { + $index = 16; + $node = $this->v6offsetCache[$key]; + } + } + + for ($i = $index; $i < $bitCount; $i++) { + if ($node >= $this->nodeCount) { + break; + } + + $node = $this->readNode($node, 1 & ((0xFF & ord($binary[$i >> 3])) >> 7 - ($i % 8))); + + if ($i == 15) { + $this->v6offsetCache[$key] = $node; + } + } + + if ($node === $this->nodeCount) { + return 0; + } elseif ($node > $this->nodeCount) { + return $node; + } + + throw new Exception("find node failed."); + } + + /** + * @param $node + * @param $index + * @return mixed + * @throws Exception + */ + private function readNode($node, $index) + { + return unpack('N', $this->read($this->file, $node * 8 + $index * 4, 4))[1]; + } + + /** + * @param $node + * @return mixed + * @throws Exception + */ + private function resolve($node) + { + $resolved = $node - $this->nodeCount + $this->nodeCount * 8; + if ($resolved >= $this->fileSize) { + return NULL; + } + + $bytes = $this->read($this->file, $resolved, 2); + $size = unpack('N', str_pad($bytes, 4, "\x00", STR_PAD_LEFT))[1]; + + $resolved += 2; + + return $this->read($this->file, $resolved, $size); + } + + public function close() + { + if (is_resource($this->file) === TRUE) { + fclose($this->file); + } + } + + /** + * @param $stream + * @param $offset + * @param $length + * @return bool|string + * @throws Exception + */ + private function read($stream, $offset, $length) + { + if ($length > 0) { + if (fseek($stream, $offset + $this->nodeOffset) === 0) { + $value = fread($stream, $length); + if (strlen($value) === $length) { + return $value; + } + } + + throw new Exception("The Database file read bad data."); + } + + return ''; + } + + public function supportV6() + { + return ($this->meta['ip_version'] & self::IPV6) === self::IPV6; + } + + public function supportV4() + { + return ($this->meta['ip_version'] & self::IPV4) === self::IPV4; + } + + public function getMeta() + { + return $this->meta; + } + + /** + * @return int UTC Timestamp + */ + public function getBuildTime() + { + return $this->meta['build']; + } +} diff --git a/src/service/ip/IpIpService.php b/src/service/ip/IpIpService.php new file mode 100644 index 0000000000000000000000000000000000000000..2e19c65a6558af70fab010b578d9779fb115d951 --- /dev/null +++ b/src/service/ip/IpIpService.php @@ -0,0 +1,74 @@ +reader = new IpIpReader(__DIR__ . '/bin/ipipfree.ipdb'); + } + + /** + * @param $ip + * @param $language + * @return array|NULL + */ + public function getFind(string $ip = '', string $language = 'CN') + { + if (empty($ip)) $ip = get_ip(); + return $this->reader->find($ip, $language); + } + + /** + * @param $ip + * @param $language + * @return array|false|null + */ + public function getFindMap(string $ip = '', string $language = 'CN') + { + if (empty($ip)) $ip = get_ip(); + return $this->reader->findMap($ip, $language); + } + + /** + * @param $ip + * @param $language + * @return IpIpDistrictInfo|null + */ + public function getFindInfo(string $ip = '', string $language = 'CN') + { + if (empty($ip)) $ip = get_ip(); + $map = $this->getFindMap($ip, $language); + if (NULL === $map) return NUll; + return new IpIpDistrictInfo($map); + } +}