Overview

Namespaces

  • GeoIp2
    • Database
    • Exception
    • Model
    • Record
    • WebService
  • MaxMind
    • Db
      • Reader
  • PHP

Classes

  • GeoIp2\Database\Reader
  • GeoIp2\Model\AnonymousIp
  • GeoIp2\Model\Asn
  • GeoIp2\Model\City
  • GeoIp2\Model\ConnectionType
  • GeoIp2\Model\Country
  • GeoIp2\Model\Domain
  • GeoIp2\Model\Enterprise
  • GeoIp2\Model\Insights
  • GeoIp2\Model\Isp
  • GeoIp2\Record\AbstractPlaceRecord
  • GeoIp2\Record\AbstractRecord
  • GeoIp2\Record\City
  • GeoIp2\Record\Continent
  • GeoIp2\Record\Country
  • GeoIp2\Record\Location
  • GeoIp2\Record\MaxMind
  • GeoIp2\Record\Postal
  • GeoIp2\Record\RepresentedCountry
  • GeoIp2\Record\Subdivision
  • GeoIp2\Record\Traits
  • GeoIp2\WebService\Client
  • MaxMind\Db\Reader
  • MaxMind\Db\Reader\Decoder
  • MaxMind\Db\Reader\Metadata
  • MaxMind\Db\Reader\Util

Interfaces

  • GeoIp2\ProviderInterface
  • JsonSerializable
  • Throwable

Exceptions

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