vendor/maxmind-db/reader/src/MaxMind/Db/Reader.php line 73

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