Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 1
<?php
2
 
3
declare(strict_types=1);
4
 
5
namespace MaxMind\Db;
6
 
7
use MaxMind\Db\Reader\Decoder;
8
use MaxMind\Db\Reader\InvalidDatabaseException;
9
use MaxMind\Db\Reader\Metadata;
10
use MaxMind\Db\Reader\Util;
11
 
12
/**
13
 * Instances of this class provide a reader for the MaxMind DB format. IP
14
 * addresses can be looked up using the get method.
15
 */
16
class Reader
17
{
18
    /**
19
     * @var int
20
     */
21
    private static $DATA_SECTION_SEPARATOR_SIZE = 16;
22
 
23
    /**
24
     * @var string
25
     */
26
    private static $METADATA_START_MARKER = "\xAB\xCD\xEFMaxMind.com";
27
 
28
    /**
29
     * @var int<0, max>
30
     */
31
    private static $METADATA_START_MARKER_LENGTH = 14;
32
 
33
    /**
34
     * @var int
35
     */
36
    private static $METADATA_MAX_SIZE = 131072; // 128 * 1024 = 128KiB
37
 
38
    /**
39
     * @var Decoder
40
     */
41
    private $decoder;
42
 
43
    /**
44
     * @var resource
45
     */
46
    private $fileHandle;
47
 
48
    /**
49
     * @var int
50
     */
51
    private $fileSize;
52
 
53
    /**
54
     * @var int
55
     */
56
    private $ipV4Start;
57
 
58
    /**
59
     * @var Metadata
60
     */
61
    private $metadata;
62
 
63
    /**
64
     * Constructs a Reader for the MaxMind DB format. The file passed to it must
65
     * be a valid MaxMind DB file such as a GeoIp2 database file.
66
     *
67
     * @param string $database the MaxMind DB file to use
68
     *
69
     * @throws \InvalidArgumentException for invalid database path or unknown arguments
70
     * @throws InvalidDatabaseException
71
     *                                   if the database is invalid or there is an error reading
72
     *                                   from it
73
     */
74
    public function __construct(string $database)
75
    {
76
        if (\func_num_args() !== 1) {
77
            throw new \ArgumentCountError(
78
                \sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args())
79
            );
80
        }
81
 
82
        if (is_dir($database)) {
83
            // This matches the error that the C extension throws.
84
            throw new InvalidDatabaseException(
85
                "Error opening database file ($database). Is this a valid MaxMind DB file?"
86
            );
87
        }
88
 
89
        $fileHandle = @fopen($database, 'rb');
90
        if ($fileHandle === false) {
91
            throw new \InvalidArgumentException(
92
                "The file \"$database\" does not exist or is not readable."
93
            );
94
        }
95
        $this->fileHandle = $fileHandle;
96
 
97
        $fileSize = @filesize($database);
98
        if ($fileSize === false) {
99
            throw new \UnexpectedValueException(
100
                "Error determining the size of \"$database\"."
101
            );
102
        }
103
        $this->fileSize = $fileSize;
104
 
105
        $start = $this->findMetadataStart($database);
106
        $metadataDecoder = new Decoder($this->fileHandle, $start);
107
        [$metadataArray] = $metadataDecoder->decode($start);
108
        $this->metadata = new Metadata($metadataArray);
109
        $this->decoder = new Decoder(
110
            $this->fileHandle,
111
            $this->metadata->searchTreeSize + self::$DATA_SECTION_SEPARATOR_SIZE
112
        );
113
        $this->ipV4Start = $this->ipV4StartNode();
114
    }
115
 
116
    /**
117
     * Retrieves the record for the IP address.
118
     *
119
     * @param string $ipAddress the IP address to look up
120
     *
121
     * @throws \BadMethodCallException   if this method is called on a closed database
122
     * @throws \InvalidArgumentException if something other than a single IP address is passed to the method
123
     * @throws InvalidDatabaseException
124
     *                                   if the database is invalid or there is an error reading
125
     *                                   from it
126
     *
127
     * @return mixed the record for the IP address
128
     */
129
    public function get(string $ipAddress)
130
    {
131
        if (\func_num_args() !== 1) {
132
            throw new \ArgumentCountError(
133
                \sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args())
134
            );
135
        }
136
        [$record] = $this->getWithPrefixLen($ipAddress);
137
 
138
        return $record;
139
    }
140
 
141
    /**
142
     * Retrieves the record for the IP address and its associated network prefix length.
143
     *
144
     * @param string $ipAddress the IP address to look up
145
     *
146
     * @throws \BadMethodCallException   if this method is called on a closed database
147
     * @throws \InvalidArgumentException if something other than a single IP address is passed to the method
148
     * @throws InvalidDatabaseException
149
     *                                   if the database is invalid or there is an error reading
150
     *                                   from it
151
     *
152
     * @return array{0:mixed, 1:int} an array where the first element is the record and the
153
     *                               second the network prefix length for the record
154
     */
155
    public function getWithPrefixLen(string $ipAddress): array
156
    {
157
        if (\func_num_args() !== 1) {
158
            throw new \ArgumentCountError(
159
                \sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args())
160
            );
161
        }
162
 
163
        if (!\is_resource($this->fileHandle)) {
164
            throw new \BadMethodCallException(
165
                'Attempt to read from a closed MaxMind DB.'
166
            );
167
        }
168
 
169
        [$pointer, $prefixLen] = $this->findAddressInTree($ipAddress);
170
        if ($pointer === 0) {
171
            return [null, $prefixLen];
172
        }
173
 
174
        return [$this->resolveDataPointer($pointer), $prefixLen];
175
    }
176
 
177
    /**
178
     * @return array{0:int, 1:int}
179
     */
180
    private function findAddressInTree(string $ipAddress): array
181
    {
182
        $packedAddr = @inet_pton($ipAddress);
183
        if ($packedAddr === false) {
184
            throw new \InvalidArgumentException(
185
                "The value \"$ipAddress\" is not a valid IP address."
186
            );
187
        }
188
 
189
        $rawAddress = unpack('C*', $packedAddr);
190
        if ($rawAddress === false) {
191
            throw new InvalidDatabaseException(
192
                'Could not unpack the unsigned char of the packed in_addr representation.'
193
            );
194
        }
195
 
196
        $bitCount = \count($rawAddress) * 8;
197
 
198
        // The first node of the tree is always node 0, at the beginning of the
199
        // value
200
        $node = 0;
201
 
202
        $metadata = $this->metadata;
203
 
204
        // Check if we are looking up an IPv4 address in an IPv6 tree. If this
205
        // is the case, we can skip over the first 96 nodes.
206
        if ($metadata->ipVersion === 6) {
207
            if ($bitCount === 32) {
208
                $node = $this->ipV4Start;
209
            }
210
        } elseif ($metadata->ipVersion === 4 && $bitCount === 128) {
211
            throw new \InvalidArgumentException(
212
                "Error looking up $ipAddress. You attempted to look up an"
213
                . ' IPv6 address in an IPv4-only database.'
214
            );
215
        }
216
 
217
        $nodeCount = $metadata->nodeCount;
218
 
219
        for ($i = 0; $i < $bitCount && $node < $nodeCount; ++$i) {
220
            $tempBit = 0xFF & $rawAddress[($i >> 3) + 1];
221
            $bit = 1 & ($tempBit >> 7 - ($i % 8));
222
 
223
            $node = $this->readNode($node, $bit);
224
        }
225
        if ($node === $nodeCount) {
226
            // Record is empty
227
            return [0, $i];
228
        }
229
        if ($node > $nodeCount) {
230
            // Record is a data pointer
231
            return [$node, $i];
232
        }
233
 
234
        throw new InvalidDatabaseException(
235
            'Invalid or corrupt database. Maximum search depth reached without finding a leaf node'
236
        );
237
    }
238
 
239
    private function ipV4StartNode(): int
240
    {
241
        // If we have an IPv4 database, the start node is the first node
242
        if ($this->metadata->ipVersion === 4) {
243
            return 0;
244
        }
245
 
246
        $node = 0;
247
 
248
        for ($i = 0; $i < 96 && $node < $this->metadata->nodeCount; ++$i) {
249
            $node = $this->readNode($node, 0);
250
        }
251
 
252
        return $node;
253
    }
254
 
255
    private function readNode(int $nodeNumber, int $index): int
256
    {
257
        $baseOffset = $nodeNumber * $this->metadata->nodeByteSize;
258
 
259
        switch ($this->metadata->recordSize) {
260
            case 24:
261
                $bytes = Util::read($this->fileHandle, $baseOffset + $index * 3, 3);
262
                $rc = unpack('N', "\x00" . $bytes);
263
                if ($rc === false) {
264
                    throw new InvalidDatabaseException(
265
                        'Could not unpack the unsigned long of the node.'
266
                    );
267
                }
268
                [, $node] = $rc;
269
 
270
                return $node;
271
 
272
            case 28:
273
                $bytes = Util::read($this->fileHandle, $baseOffset + 3 * $index, 4);
274
                if ($index === 0) {
275
                    $middle = (0xF0 & \ord($bytes[3])) >> 4;
276
                } else {
277
                    $middle = 0x0F & \ord($bytes[0]);
278
                }
279
                $rc = unpack('N', \chr($middle) . substr($bytes, $index, 3));
280
                if ($rc === false) {
281
                    throw new InvalidDatabaseException(
282
                        'Could not unpack the unsigned long of the node.'
283
                    );
284
                }
285
                [, $node] = $rc;
286
 
287
                return $node;
288
 
289
            case 32:
290
                $bytes = Util::read($this->fileHandle, $baseOffset + $index * 4, 4);
291
                $rc = unpack('N', $bytes);
292
                if ($rc === false) {
293
                    throw new InvalidDatabaseException(
294
                        'Could not unpack the unsigned long of the node.'
295
                    );
296
                }
297
                [, $node] = $rc;
298
 
299
                return $node;
300
 
301
            default:
302
                throw new InvalidDatabaseException(
303
                    'Unknown record size: '
304
                    . $this->metadata->recordSize
305
                );
306
        }
307
    }
308
 
309
    /**
310
     * @return mixed
311
     */
312
    private function resolveDataPointer(int $pointer)
313
    {
314
        $resolved = $pointer - $this->metadata->nodeCount
315
            + $this->metadata->searchTreeSize;
316
        if ($resolved >= $this->fileSize) {
317
            throw new InvalidDatabaseException(
318
                "The MaxMind DB file's search tree is corrupt"
319
            );
320
        }
321
 
322
        [$data] = $this->decoder->decode($resolved);
323
 
324
        return $data;
325
    }
326
 
327
    /*
328
     * This is an extremely naive but reasonably readable implementation. There
329
     * are much faster algorithms (e.g., Boyer-Moore) for this if speed is ever
330
     * an issue, but I suspect it won't be.
331
     */
332
    private function findMetadataStart(string $filename): int
333
    {
334
        $handle = $this->fileHandle;
335
        $fstat = fstat($handle);
336
        if ($fstat === false) {
337
            throw new InvalidDatabaseException(
338
                "Error getting file information ($filename)."
339
            );
340
        }
341
        $fileSize = $fstat['size'];
342
        $marker = self::$METADATA_START_MARKER;
343
        $markerLength = self::$METADATA_START_MARKER_LENGTH;
344
 
345
        $minStart = $fileSize - min(self::$METADATA_MAX_SIZE, $fileSize);
346
 
347
        for ($offset = $fileSize - $markerLength; $offset >= $minStart; --$offset) {
348
            if (fseek($handle, $offset) !== 0) {
349
                break;
350
            }
351
 
352
            $value = fread($handle, $markerLength);
353
            if ($value === $marker) {
354
                return $offset + $markerLength;
355
            }
356
        }
357
 
358
        throw new InvalidDatabaseException(
359
            "Error opening database file ($filename). " .
360
            'Is this a valid MaxMind DB file?'
361
        );
362
    }
363
 
364
    /**
365
     * @throws \InvalidArgumentException if arguments are passed to the method
366
     * @throws \BadMethodCallException   if the database has been closed
367
     *
368
     * @return Metadata object for the database
369
     */
370
    public function metadata(): Metadata
371
    {
372
        if (\func_num_args()) {
373
            throw new \ArgumentCountError(
374
                \sprintf('%s() expects exactly 0 parameters, %d given', __METHOD__, \func_num_args())
375
            );
376
        }
377
 
378
        // Not technically required, but this makes it consistent with
379
        // C extension and it allows us to change our implementation later.
380
        if (!\is_resource($this->fileHandle)) {
381
            throw new \BadMethodCallException(
382
                'Attempt to read from a closed MaxMind DB.'
383
            );
384
        }
385
 
386
        return clone $this->metadata;
387
    }
388
 
389
    /**
390
     * Closes the MaxMind DB and returns resources to the system.
391
     *
392
     * @throws \Exception
393
     *                    if an I/O error occurs
394
     */
395
    public function close(): void
396
    {
397
        if (\func_num_args()) {
398
            throw new \ArgumentCountError(
399
                \sprintf('%s() expects exactly 0 parameters, %d given', __METHOD__, \func_num_args())
400
            );
401
        }
402
 
403
        if (!\is_resource($this->fileHandle)) {
404
            throw new \BadMethodCallException(
405
                'Attempt to close a closed MaxMind DB.'
406
            );
407
        }
408
        fclose($this->fileHandle);
409
    }
410
}