PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.6.1
Defender Security – Malware Scanner, Login Security & Firewall v5.6.1
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / src / extra / maxmind-db / reader / src / MaxMind / Db / Reader.php

Reader.php in Defender Security – Malware Scanner, Login Security & Firewall 5.6.1, at src/extra/maxmind-db/reader/src/MaxMind/Db/Reader.php

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