Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]
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 } elseif ($node > $nodeCount) { 215 // Record is a data pointer 216 return [$node, $i]; 217 } 218 throw new InvalidDatabaseException( 219 'Invalid or corrupt database. Maximum search depth reached without finding a leaf node' 220 ); 221 } 222 223 private function ipV4StartNode(): int 224 { 225 // If we have an IPv4 database, the start node is the first node 226 if ($this->metadata->ipVersion === 4) { 227 return 0; 228 } 229 230 $node = 0; 231 232 for ($i = 0; $i < 96 && $node < $this->metadata->nodeCount; ++$i) { 233 $node = $this->readNode($node, 0); 234 } 235 236 return $node; 237 } 238 239 private function readNode(int $nodeNumber, int $index): int 240 { 241 $baseOffset = $nodeNumber * $this->metadata->nodeByteSize; 242 243 switch ($this->metadata->recordSize) { 244 case 24: 245 $bytes = Util::read($this->fileHandle, $baseOffset + $index * 3, 3); 246 [, $node] = unpack('N', "\x00" . $bytes); 247 248 return $node; 249 case 28: 250 $bytes = Util::read($this->fileHandle, $baseOffset + 3 * $index, 4); 251 if ($index === 0) { 252 $middle = (0xF0 & \ord($bytes[3])) >> 4; 253 } else { 254 $middle = 0x0F & \ord($bytes[0]); 255 } 256 [, $node] = unpack('N', \chr($middle) . substr($bytes, $index, 3)); 257 258 return $node; 259 case 32: 260 $bytes = Util::read($this->fileHandle, $baseOffset + $index * 4, 4); 261 [, $node] = unpack('N', $bytes); 262 263 return $node; 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 { 277 $resolved = $pointer - $this->metadata->nodeCount 278 + $this->metadata->searchTreeSize; 279 if ($resolved >= $this->fileSize) { 280 throw new InvalidDatabaseException( 281 "The MaxMind DB file's search tree is corrupt" 282 ); 283 } 284 285 [$data] = $this->decoder->decode($resolved); 286 287 return $data; 288 } 289 290 /* 291 * This is an extremely naive but reasonably readable implementation. There 292 * are much faster algorithms (e.g., Boyer-Moore) for this if speed is ever 293 * an issue, but I suspect it won't be. 294 */ 295 private function findMetadataStart(string $filename): int 296 { 297 $handle = $this->fileHandle; 298 $fstat = fstat($handle); 299 $fileSize = $fstat['size']; 300 $marker = self::$METADATA_START_MARKER; 301 $markerLength = self::$METADATA_START_MARKER_LENGTH; 302 303 $minStart = $fileSize - min(self::$METADATA_MAX_SIZE, $fileSize); 304 305 for ($offset = $fileSize - $markerLength; $offset >= $minStart; --$offset) { 306 if (fseek($handle, $offset) !== 0) { 307 break; 308 } 309 310 $value = fread($handle, $markerLength); 311 if ($value === $marker) { 312 return $offset + $markerLength; 313 } 314 } 315 throw new InvalidDatabaseException( 316 "Error opening database file ($filename). " . 317 'Is this a valid MaxMind DB file?' 318 ); 319 } 320 321 /** 322 * @throws InvalidArgumentException if arguments are passed to the method 323 * @throws BadMethodCallException if the database has been closed 324 * 325 * @return Metadata object for the database 326 */ 327 public function metadata(): Metadata 328 { 329 if (\func_num_args()) { 330 throw new ArgumentCountError( 331 sprintf('%s() expects exactly 0 parameters, %d given', __METHOD__, \func_num_args()) 332 ); 333 } 334 335 // Not technically required, but this makes it consistent with 336 // C extension and it allows us to change our implementation later. 337 if (!\is_resource($this->fileHandle)) { 338 throw new BadMethodCallException( 339 'Attempt to read from a closed MaxMind DB.' 340 ); 341 } 342 343 return clone $this->metadata; 344 } 345 346 /** 347 * Closes the MaxMind DB and returns resources to the system. 348 * 349 * @throws Exception 350 * if an I/O error occurs 351 */ 352 public function close(): void 353 { 354 if (\func_num_args()) { 355 throw new ArgumentCountError( 356 sprintf('%s() expects exactly 0 parameters, %d given', __METHOD__, \func_num_args()) 357 ); 358 } 359 360 if (!\is_resource($this->fileHandle)) { 361 throw new BadMethodCallException( 362 'Attempt to close a closed MaxMind DB.' 363 ); 364 } 365 fclose($this->fileHandle); 366 } 367 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body