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