Issues (1482)

src/service/Ip/IpIpReader.php (1 issue)

Labels
Severity
1
<?php
2
3
// +----------------------------------------------------------------------
4
// | ThinkLibrary 6.0 for ThinkPhP 6.0
5
// +----------------------------------------------------------------------
6
// | 版权所有 2017~2020 [ https://www.dtapp.net ]
7
// +----------------------------------------------------------------------
8
// | 官方网站: https://gitee.com/liguangchun/ThinkLibrary
9
// +----------------------------------------------------------------------
10
// | 开源协议 ( https://mit-license.org )
11
// +----------------------------------------------------------------------
12
// | gitee 仓库地址 :https://gitee.com/liguangchun/ThinkLibrary
13
// | github 仓库地址 :https://github.com/GC0202/ThinkLibrary
14
// | Packagist 地址 :https://packagist.org/packages/liguangchun/think-library
15
// +----------------------------------------------------------------------
16
17
namespace DtApp\ThinkLibrary\service\Ip;
18
19
use BadMethodCallException;
20
use Exception;
21
use InvalidArgumentException;
22
23
class IpIpReader
24
{
25
    public const IPV4 = 1;
26
    public const IPV6 = 2;
27
28
    private $file;
29
    private $fileSize = 0;
30
    private $nodeCount = 0;
31
    private $nodeOffset = 0;
32
33
    private $meta = [];
34
35
    private $database;
36
37
    /**
38
     * Reader constructor.
39
     * @param $database
40
     * @throws Exception
41
     */
42
    public function __construct($database)
43
    {
44
        $this->database = $database;
45
46
        $this->init();
47
    }
48
49
    /**
50
     * @throws Exception
51
     */
52
    private function init(): void
53
    {
54
        if (is_readable($this->database) === FALSE) {
55
            throw new InvalidArgumentException("The IP Database file \"{$this->database}\" does not exist or is not readable.");
56
        }
57
        $this->file = @fopen($this->database, 'rb');
58
        if ($this->file === FALSE) {
59
            throw new InvalidArgumentException("IP Database File opening \"{$this->database}\".");
60
        }
61
        $this->fileSize = @filesize($this->database);
62
        if ($this->fileSize === FALSE) {
63
            throw new \UnexpectedValueException("Error determining the size of \"{$this->database}\".");
64
        }
65
66
        $metaLength = unpack('N', fread($this->file, 4))[1];
67
        $text = fread($this->file, $metaLength);
68
69
        $this->meta = json_decode($text, 1);
70
71
        if (isset($this->meta['fields']) === FALSE || isset($this->meta['languages']) === FALSE) {
72
            throw new Exception('IP Database metadata error.');
73
        }
74
75
        $fileSize = 4 + $metaLength + $this->meta['total_size'];
76
        if ($fileSize !== $this->fileSize) {
77
            throw  new Exception('IP Database size error.');
78
        }
79
80
        $this->nodeCount = $this->meta['node_count'];
81
        $this->nodeOffset = 4 + $metaLength;
82
    }
83
84
    /**
85
     * @param $ip
86
     * @param string $language
87
     * @return array|NULL
88
     */
89
    public function find($ip, $language): ?array
90
    {
91
        if (is_resource($this->file) === FALSE) {
92
            throw new BadMethodCallException('IPIP DB closed.');
93
        }
94
95
        if (isset($this->meta['languages'][$language]) === FALSE) {
96
            throw new InvalidArgumentException("language : {$language} not support.");
97
        }
98
99
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6) === FALSE) {
100
            throw new InvalidArgumentException("The value \"$ip\" is not a valid IP address.");
101
        }
102
103
        if (strpos($ip, '.') !== FALSE && !$this->supportV4()) {
104
            throw new InvalidArgumentException("The Database not support IPv4 address.");
105
        }
106
107
        if (strpos($ip, ':') !== FALSE && !$this->supportV6()) {
108
            throw new InvalidArgumentException("The Database not support IPv6 address.");
109
        }
110
111
        try {
112
            $node = $this->findNode($ip);
113
114
            if ($node > 0) {
115
                $data = $this->resolve($node);
116
117
                $values = explode("\t", $data);
0 ignored issues
show
It seems like $data can also be of type null; however, parameter $string of explode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

117
                $values = explode("\t", /** @scrutinizer ignore-type */ $data);
Loading history...
118
119
                return array_slice($values, $this->meta['languages'][$language], count($this->meta['fields']));
120
            }
121
        } catch (Exception $e) {
122
            return NULL;
123
        }
124
125
        return NULL;
126
    }
127
128
    /**
129
     * @param $ip
130
     * @param $language
131
     * @return array|false|null
132
     */
133
    public function findMap($ip, $language)
134
    {
135
        $array = $this->find($ip, $language);
136
        if (NULL === $array) {
137
            return NULL;
138
        }
139
140
        return array_combine($this->meta['fields'], $array);
141
    }
142
143
    /**
144
     * @var int
145
     */
146
    private $v4offset = 0;
147
148
    /**
149
     * @var array
150
     */
151
    private $v6offsetCache = [];
152
153
    /**
154
     * @param $ip
155
     * @return int
156
     * @throws Exception
157
     */
158
    private function findNode($ip): int
159
    {
160
        $binary = inet_pton($ip);
161
        $bitCount = strlen($binary) * 8; // 32 | 128
162
        $key = substr($binary, 0, 2);
163
        $node = 0;
164
        $index = 0;
165
        if ($bitCount === 32) {
166
            if ($this->v4offset === 0) {
167
                for ($i = 0; $i < 96 && $node < $this->nodeCount; $i++) {
168
                    if ($i >= 80) {
169
                        $idx = 1;
170
                    } else {
171
                        $idx = 0;
172
                    }
173
                    $node = $this->readNode($node, $idx);
174
                    if ($node > $this->nodeCount) {
175
                        return 0;
176
                    }
177
                }
178
                $this->v4offset = $node;
179
            } else {
180
                $node = $this->v4offset;
181
            }
182
        } else {
183
            if (isset($this->v6offsetCache[$key])) {
184
                $index = 16;
185
                $node = $this->v6offsetCache[$key];
186
            }
187
        }
188
189
        for ($i = $index; $i < $bitCount; $i++) {
190
            if ($node >= $this->nodeCount) {
191
                break;
192
            }
193
194
            $node = $this->readNode($node, 1 & ((0xFF & ord($binary[$i >> 3])) >> 7 - ($i % 8)));
195
196
            if ($i == 15) {
197
                $this->v6offsetCache[$key] = $node;
198
            }
199
        }
200
201
        if ($node === $this->nodeCount) {
202
            return 0;
203
        }
204
205
        if ($node > $this->nodeCount) {
206
            return $node;
207
        }
208
209
        throw new Exception("find node failed.");
210
    }
211
212
    /**
213
     * @param $node
214
     * @param $index
215
     * @return mixed
216
     * @throws Exception
217
     */
218
    private function readNode($node, $index)
219
    {
220
        return unpack('N', $this->read($this->file, $node * 8 + $index * 4, 4))[1];
221
    }
222
223
    /**
224
     * @param $node
225
     * @return mixed
226
     * @throws Exception
227
     */
228
    private function resolve($node)
229
    {
230
        $resolved = $node - $this->nodeCount + $this->nodeCount * 8;
231
        if ($resolved >= $this->fileSize) {
232
            return NULL;
233
        }
234
235
        $bytes = $this->read($this->file, $resolved, 2);
236
        $size = unpack('N', str_pad($bytes, 4, "\x00", STR_PAD_LEFT))[1];
237
238
        $resolved += 2;
239
240
        return $this->read($this->file, $resolved, $size);
241
    }
242
243
    /**
244
     *
245
     */
246
    public function close(): void
247
    {
248
        if (is_resource($this->file) === TRUE) {
249
            fclose($this->file);
250
        }
251
    }
252
253
    /**
254
     * @param $stream
255
     * @param $offset
256
     * @param $length
257
     * @return bool|string
258
     * @throws Exception
259
     */
260
    private function read($stream, $offset, $length)
261
    {
262
        if ($length > 0) {
263
            if (fseek($stream, $offset + $this->nodeOffset) === 0) {
264
                $value = fread($stream, $length);
265
                if (strlen($value) === $length) {
266
                    return $value;
267
                }
268
            }
269
270
            throw new Exception("The Database file read bad data.");
271
        }
272
273
        return '';
274
    }
275
276
    /**
277
     * @return bool
278
     */
279
    public function supportV6(): bool
280
    {
281
        return ($this->meta['ip_version'] & self::IPV6) === self::IPV6;
282
    }
283
284
    /**
285
     * @return bool
286
     */
287
    public function supportV4(): bool
288
    {
289
        return ($this->meta['ip_version'] & self::IPV4) === self::IPV4;
290
    }
291
292
    /**
293
     * @return array
294
     */
295
    public function getMeta()
296
    {
297
        return $this->meta;
298
    }
299
300
    /**
301
     * @return int  UTC Timestamp
302
     */
303
    public function getBuildTime()
304
    {
305
        return $this->meta['build'];
306
    }
307
}
308