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

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace MaxMind\Db;
  4. use MaxMind\Db\Reader\Decoder;
  5. use MaxMind\Db\Reader\InvalidDatabaseException;
  6. use MaxMind\Db\Reader\Metadata;
  7. use MaxMind\Db\Reader\Util;
  8. /**
  9.  * Instances of this class provide a reader for the MaxMind DB format. IP
  10.  * addresses can be looked up using the get method.
  11.  */
  12. class Reader
  13. {
  14.     /**
  15.      * @var int
  16.      */
  17.     private static $DATA_SECTION_SEPARATOR_SIZE 16;
  18.     /**
  19.      * @var string
  20.      */
  21.     private static $METADATA_START_MARKER "\xAB\xCD\xEFMaxMind.com";
  22.     /**
  23.      * @var int<0, max>
  24.      */
  25.     private static $METADATA_START_MARKER_LENGTH 14;
  26.     /**
  27.      * @var int
  28.      */
  29.     private static $METADATA_MAX_SIZE 131072// 128 * 1024 = 128KiB
  30.     /**
  31.      * @var Decoder
  32.      */
  33.     private $decoder;
  34.     /**
  35.      * @var resource
  36.      */
  37.     private $fileHandle;
  38.     /**
  39.      * @var int
  40.      */
  41.     private $fileSize;
  42.     /**
  43.      * @var int
  44.      */
  45.     private $ipV4Start;
  46.     /**
  47.      * @var Metadata
  48.      */
  49.     private $metadata;
  50.     /**
  51.      * Constructs a Reader for the MaxMind DB format. The file passed to it must
  52.      * be a valid MaxMind DB file such as a GeoIp2 database file.
  53.      *
  54.      * @param string $database
  55.      *                         the MaxMind DB file to use
  56.      *
  57.      * @throws \InvalidArgumentException for invalid database path or unknown arguments
  58.      * @throws InvalidDatabaseException
  59.      *                                   if the database is invalid or there is an error reading
  60.      *                                   from it
  61.      */
  62.     public function __construct(string $database)
  63.     {
  64.         if (\func_num_args() !== 1) {
  65.             throw new \ArgumentCountError(
  66.                 sprintf('%s() expects exactly 1 parameter, %d given'__METHOD__\func_num_args())
  67.             );
  68.         }
  69.         $fileHandle = @fopen($database'rb');
  70.         if ($fileHandle === false) {
  71.             throw new \InvalidArgumentException(
  72.                 "The file \"$database\" does not exist or is not readable."
  73.             );
  74.         }
  75.         $this->fileHandle $fileHandle;
  76.         $fileSize = @filesize($database);
  77.         if ($fileSize === false) {
  78.             throw new \UnexpectedValueException(
  79.                 "Error determining the size of \"$database\"."
  80.             );
  81.         }
  82.         $this->fileSize $fileSize;
  83.         $start $this->findMetadataStart($database);
  84.         $metadataDecoder = new Decoder($this->fileHandle$start);
  85.         [$metadataArray] = $metadataDecoder->decode($start);
  86.         $this->metadata = new Metadata($metadataArray);
  87.         $this->decoder = new Decoder(
  88.             $this->fileHandle,
  89.             $this->metadata->searchTreeSize self::$DATA_SECTION_SEPARATOR_SIZE
  90.         );
  91.         $this->ipV4Start $this->ipV4StartNode();
  92.     }
  93.     /**
  94.      * Retrieves the record for the IP address.
  95.      *
  96.      * @param string $ipAddress
  97.      *                          the IP address to look up
  98.      *
  99.      * @throws \BadMethodCallException   if this method is called on a closed database
  100.      * @throws \InvalidArgumentException if something other than a single IP address is passed to the method
  101.      * @throws InvalidDatabaseException
  102.      *                                   if the database is invalid or there is an error reading
  103.      *                                   from it
  104.      *
  105.      * @return mixed the record for the IP address
  106.      */
  107.     public function get(string $ipAddress)
  108.     {
  109.         if (\func_num_args() !== 1) {
  110.             throw new \ArgumentCountError(
  111.                 sprintf('%s() expects exactly 1 parameter, %d given'__METHOD__\func_num_args())
  112.             );
  113.         }
  114.         [$record] = $this->getWithPrefixLen($ipAddress);
  115.         return $record;
  116.     }
  117.     /**
  118.      * Retrieves the record for the IP address and its associated network prefix length.
  119.      *
  120.      * @param string $ipAddress
  121.      *                          the IP address to look up
  122.      *
  123.      * @throws \BadMethodCallException   if this method is called on a closed database
  124.      * @throws \InvalidArgumentException if something other than a single IP address is passed to the method
  125.      * @throws InvalidDatabaseException
  126.      *                                   if the database is invalid or there is an error reading
  127.      *                                   from it
  128.      *
  129.      * @return array an array where the first element is the record and the
  130.      *               second the network prefix length for the record
  131.      */
  132.     public function getWithPrefixLen(string $ipAddress): array
  133.     {
  134.         if (\func_num_args() !== 1) {
  135.             throw new \ArgumentCountError(
  136.                 sprintf('%s() expects exactly 1 parameter, %d given'__METHOD__\func_num_args())
  137.             );
  138.         }
  139.         if (!\is_resource($this->fileHandle)) {
  140.             throw new \BadMethodCallException(
  141.                 'Attempt to read from a closed MaxMind DB.'
  142.             );
  143.         }
  144.         [$pointer$prefixLen] = $this->findAddressInTree($ipAddress);
  145.         if ($pointer === 0) {
  146.             return [null$prefixLen];
  147.         }
  148.         return [$this->resolveDataPointer($pointer), $prefixLen];
  149.     }
  150.     private function findAddressInTree(string $ipAddress): array
  151.     {
  152.         $packedAddr = @inet_pton($ipAddress);
  153.         if ($packedAddr === false) {
  154.             throw new \InvalidArgumentException(
  155.                 "The value \"$ipAddress\" is not a valid IP address."
  156.             );
  157.         }
  158.         $rawAddress unpack('C*'$packedAddr);
  159.         if ($rawAddress === false) {
  160.             throw new InvalidDatabaseException(
  161.                 'Could not unpack the unsigned char of the packed in_addr representation.'
  162.             );
  163.         }
  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.                 $rc unpack('N'"\x00" $bytes);
  218.                 if ($rc === false) {
  219.                     throw new InvalidDatabaseException(
  220.                         'Could not unpack the unsigned long of the node.'
  221.                     );
  222.                 }
  223.                 [, $node] = $rc;
  224.                 return $node;
  225.             case 28:
  226.                 $bytes Util::read($this->fileHandle$baseOffset $index4);
  227.                 if ($index === 0) {
  228.                     $middle = (0xF0 \ord($bytes[3])) >> 4;
  229.                 } else {
  230.                     $middle 0x0F \ord($bytes[0]);
  231.                 }
  232.                 $rc unpack('N'\chr($middle) . substr($bytes$index3));
  233.                 if ($rc === false) {
  234.                     throw new InvalidDatabaseException(
  235.                         'Could not unpack the unsigned long of the node.'
  236.                     );
  237.                 }
  238.                 [, $node] = $rc;
  239.                 return $node;
  240.             case 32:
  241.                 $bytes Util::read($this->fileHandle$baseOffset $index 44);
  242.                 $rc unpack('N'$bytes);
  243.                 if ($rc === false) {
  244.                     throw new InvalidDatabaseException(
  245.                         'Could not unpack the unsigned long of the node.'
  246.                     );
  247.                 }
  248.                 [, $node] = $rc;
  249.                 return $node;
  250.             default:
  251.                 throw new InvalidDatabaseException(
  252.                     'Unknown record size: '
  253.                     $this->metadata->recordSize
  254.                 );
  255.         }
  256.     }
  257.     /**
  258.      * @return mixed
  259.      */
  260.     private function resolveDataPointer(int $pointer)
  261.     {
  262.         $resolved $pointer $this->metadata->nodeCount
  263.             $this->metadata->searchTreeSize;
  264.         if ($resolved >= $this->fileSize) {
  265.             throw new InvalidDatabaseException(
  266.                 "The MaxMind DB file's search tree is corrupt"
  267.             );
  268.         }
  269.         [$data] = $this->decoder->decode($resolved);
  270.         return $data;
  271.     }
  272.     /*
  273.      * This is an extremely naive but reasonably readable implementation. There
  274.      * are much faster algorithms (e.g., Boyer-Moore) for this if speed is ever
  275.      * an issue, but I suspect it won't be.
  276.      */
  277.     private function findMetadataStart(string $filename): int
  278.     {
  279.         $handle $this->fileHandle;
  280.         $fstat fstat($handle);
  281.         if ($fstat === false) {
  282.             throw new InvalidDatabaseException(
  283.                 "Error getting file information ($filename)."
  284.             );
  285.         }
  286.         $fileSize $fstat['size'];
  287.         $marker self::$METADATA_START_MARKER;
  288.         $markerLength self::$METADATA_START_MARKER_LENGTH;
  289.         $minStart $fileSize min(self::$METADATA_MAX_SIZE$fileSize);
  290.         for ($offset $fileSize $markerLength$offset >= $minStart; --$offset) {
  291.             if (fseek($handle$offset) !== 0) {
  292.                 break;
  293.             }
  294.             $value fread($handle$markerLength);
  295.             if ($value === $marker) {
  296.                 return $offset $markerLength;
  297.             }
  298.         }
  299.         throw new InvalidDatabaseException(
  300.             "Error opening database file ($filename). " .
  301.             'Is this a valid MaxMind DB file?'
  302.         );
  303.     }
  304.     /**
  305.      * @throws \InvalidArgumentException if arguments are passed to the method
  306.      * @throws \BadMethodCallException   if the database has been closed
  307.      *
  308.      * @return Metadata object for the database
  309.      */
  310.     public function metadata(): Metadata
  311.     {
  312.         if (\func_num_args()) {
  313.             throw new \ArgumentCountError(
  314.                 sprintf('%s() expects exactly 0 parameters, %d given'__METHOD__\func_num_args())
  315.             );
  316.         }
  317.         // Not technically required, but this makes it consistent with
  318.         // C extension and it allows us to change our implementation later.
  319.         if (!\is_resource($this->fileHandle)) {
  320.             throw new \BadMethodCallException(
  321.                 'Attempt to read from a closed MaxMind DB.'
  322.             );
  323.         }
  324.         return clone $this->metadata;
  325.     }
  326.     /**
  327.      * Closes the MaxMind DB and returns resources to the system.
  328.      *
  329.      * @throws \Exception
  330.      *                    if an I/O error occurs
  331.      */
  332.     public function close(): void
  333.     {
  334.         if (\func_num_args()) {
  335.             throw new \ArgumentCountError(
  336.                 sprintf('%s() expects exactly 0 parameters, %d given'__METHOD__\func_num_args())
  337.             );
  338.         }
  339.         if (!\is_resource($this->fileHandle)) {
  340.             throw new \BadMethodCallException(
  341.                 'Attempt to close a closed MaxMind DB.'
  342.             );
  343.         }
  344.         fclose($this->fileHandle);
  345.     }
  346. }