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