PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.2.0
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.2.0
5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / vendor / prefixed / maxmind-db / reader / src / MaxMind / Db / Reader.php
matomo / app / vendor / prefixed / maxmind-db / reader / src / MaxMind / Db Last commit date
Reader 1 year ago Reader.php 1 year ago
Reader.php
305 lines
1 <?php
2
3 declare (strict_types=1);
4 namespace Matomo\Dependencies\MaxMind\Db;
5
6 use Matomo\Dependencies\MaxMind\Db\Reader\Decoder;
7 use Matomo\Dependencies\MaxMind\Db\Reader\InvalidDatabaseException;
8 use Matomo\Dependencies\MaxMind\Db\Reader\Metadata;
9 use Matomo\Dependencies\MaxMind\Db\Reader\Util;
10 /**
11 * Instances of this class provide a reader for the MaxMind DB format. IP
12 * addresses can be looked up using the get method.
13 */
14 class Reader
15 {
16 /**
17 * @var int
18 */
19 private static $DATA_SECTION_SEPARATOR_SIZE = 16;
20 /**
21 * @var string
22 */
23 private static $METADATA_START_MARKER = "\xab\xcd\xefMaxMind.com";
24 /**
25 * @var int<0, max>
26 */
27 private static $METADATA_START_MARKER_LENGTH = 14;
28 /**
29 * @var int
30 */
31 private static $METADATA_MAX_SIZE = 131072;
32 // 128 * 1024 = 128KiB
33 /**
34 * @var Decoder
35 */
36 private $decoder;
37 /**
38 * @var resource
39 */
40 private $fileHandle;
41 /**
42 * @var int
43 */
44 private $fileSize;
45 /**
46 * @var int
47 */
48 private $ipV4Start;
49 /**
50 * @var Metadata
51 */
52 private $metadata;
53 /**
54 * Constructs a Reader for the MaxMind DB format. The file passed to it must
55 * be a valid MaxMind DB file such as a GeoIp2 database file.
56 *
57 * @param string $database the MaxMind DB file to use
58 *
59 * @throws \InvalidArgumentException for invalid database path or unknown arguments
60 * @throws InvalidDatabaseException
61 * if the database is invalid or there is an error reading
62 * from it
63 */
64 public function __construct(string $database)
65 {
66 if (\func_num_args() !== 1) {
67 throw new \ArgumentCountError(\sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args()));
68 }
69 if (is_dir($database)) {
70 // This matches the error that the C extension throws.
71 throw new InvalidDatabaseException("Error opening database file ({$database}). Is this a valid MaxMind DB file?");
72 }
73 $fileHandle = @fopen($database, 'rb');
74 if ($fileHandle === \false) {
75 throw new \InvalidArgumentException("The file \"{$database}\" does not exist or is not readable.");
76 }
77 $this->fileHandle = $fileHandle;
78 $fileSize = @filesize($database);
79 if ($fileSize === \false) {
80 throw new \UnexpectedValueException("Error determining the size of \"{$database}\".");
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($this->fileHandle, $this->metadata->searchTreeSize + self::$DATA_SECTION_SEPARATOR_SIZE);
88 $this->ipV4Start = $this->ipV4StartNode();
89 }
90 /**
91 * Retrieves the record for the IP address.
92 *
93 * @param string $ipAddress the IP address to look up
94 *
95 * @throws \BadMethodCallException if this method is called on a closed database
96 * @throws \InvalidArgumentException if something other than a single IP address is passed to the method
97 * @throws InvalidDatabaseException
98 * if the database is invalid or there is an error reading
99 * from it
100 *
101 * @return mixed the record for the IP address
102 */
103 public function get(string $ipAddress)
104 {
105 if (\func_num_args() !== 1) {
106 throw new \ArgumentCountError(\sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args()));
107 }
108 [$record] = $this->getWithPrefixLen($ipAddress);
109 return $record;
110 }
111 /**
112 * Retrieves the record for the IP address and its associated network prefix length.
113 *
114 * @param string $ipAddress the IP address to look up
115 *
116 * @throws \BadMethodCallException if this method is called on a closed database
117 * @throws \InvalidArgumentException if something other than a single IP address is passed to the method
118 * @throws InvalidDatabaseException
119 * if the database is invalid or there is an error reading
120 * from it
121 *
122 * @return array{0:mixed, 1:int} an array where the first element is the record and the
123 * second the network prefix length for the record
124 */
125 public function getWithPrefixLen(string $ipAddress) : array
126 {
127 if (\func_num_args() !== 1) {
128 throw new \ArgumentCountError(\sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args()));
129 }
130 if (!\is_resource($this->fileHandle)) {
131 throw new \BadMethodCallException('Attempt to read from a closed MaxMind DB.');
132 }
133 [$pointer, $prefixLen] = $this->findAddressInTree($ipAddress);
134 if ($pointer === 0) {
135 return [null, $prefixLen];
136 }
137 return [$this->resolveDataPointer($pointer), $prefixLen];
138 }
139 /**
140 * @return array{0:int, 1:int}
141 */
142 private function findAddressInTree(string $ipAddress) : array
143 {
144 $packedAddr = @inet_pton($ipAddress);
145 if ($packedAddr === \false) {
146 throw new \InvalidArgumentException("The value \"{$ipAddress}\" is not a valid IP address.");
147 }
148 $rawAddress = unpack('C*', $packedAddr);
149 if ($rawAddress === \false) {
150 throw new InvalidDatabaseException('Could not unpack the unsigned char of the packed in_addr representation.');
151 }
152 $bitCount = \count($rawAddress) * 8;
153 // The first node of the tree is always node 0, at the beginning of the
154 // value
155 $node = 0;
156 $metadata = $this->metadata;
157 // Check if we are looking up an IPv4 address in an IPv6 tree. If this
158 // is the case, we can skip over the first 96 nodes.
159 if ($metadata->ipVersion === 6) {
160 if ($bitCount === 32) {
161 $node = $this->ipV4Start;
162 }
163 } elseif ($metadata->ipVersion === 4 && $bitCount === 128) {
164 throw new \InvalidArgumentException("Error looking up {$ipAddress}. You attempted to look up an" . ' IPv6 address in an IPv4-only database.');
165 }
166 $nodeCount = $metadata->nodeCount;
167 for ($i = 0; $i < $bitCount && $node < $nodeCount; ++$i) {
168 $tempBit = 0xff & $rawAddress[($i >> 3) + 1];
169 $bit = 1 & $tempBit >> 7 - $i % 8;
170 $node = $this->readNode($node, $bit);
171 }
172 if ($node === $nodeCount) {
173 // Record is empty
174 return [0, $i];
175 }
176 if ($node > $nodeCount) {
177 // Record is a data pointer
178 return [$node, $i];
179 }
180 throw new InvalidDatabaseException('Invalid or corrupt database. Maximum search depth reached without finding a leaf node');
181 }
182 private function ipV4StartNode() : int
183 {
184 // If we have an IPv4 database, the start node is the first node
185 if ($this->metadata->ipVersion === 4) {
186 return 0;
187 }
188 $node = 0;
189 for ($i = 0; $i < 96 && $node < $this->metadata->nodeCount; ++$i) {
190 $node = $this->readNode($node, 0);
191 }
192 return $node;
193 }
194 private function readNode(int $nodeNumber, int $index) : int
195 {
196 $baseOffset = $nodeNumber * $this->metadata->nodeByteSize;
197 switch ($this->metadata->recordSize) {
198 case 24:
199 $bytes = Util::read($this->fileHandle, $baseOffset + $index * 3, 3);
200 $rc = unpack('N', "\x00" . $bytes);
201 if ($rc === \false) {
202 throw new InvalidDatabaseException('Could not unpack the unsigned long of the node.');
203 }
204 [, $node] = $rc;
205 return $node;
206 case 28:
207 $bytes = Util::read($this->fileHandle, $baseOffset + 3 * $index, 4);
208 if ($index === 0) {
209 $middle = (0xf0 & \ord($bytes[3])) >> 4;
210 } else {
211 $middle = 0xf & \ord($bytes[0]);
212 }
213 $rc = unpack('N', \chr($middle) . substr($bytes, $index, 3));
214 if ($rc === \false) {
215 throw new InvalidDatabaseException('Could not unpack the unsigned long of the node.');
216 }
217 [, $node] = $rc;
218 return $node;
219 case 32:
220 $bytes = Util::read($this->fileHandle, $baseOffset + $index * 4, 4);
221 $rc = unpack('N', $bytes);
222 if ($rc === \false) {
223 throw new InvalidDatabaseException('Could not unpack the unsigned long of the node.');
224 }
225 [, $node] = $rc;
226 return $node;
227 default:
228 throw new InvalidDatabaseException('Unknown record size: ' . $this->metadata->recordSize);
229 }
230 }
231 /**
232 * @return mixed
233 */
234 private function resolveDataPointer(int $pointer)
235 {
236 $resolved = $pointer - $this->metadata->nodeCount + $this->metadata->searchTreeSize;
237 if ($resolved >= $this->fileSize) {
238 throw new InvalidDatabaseException("The MaxMind DB file's search tree is corrupt");
239 }
240 [$data] = $this->decoder->decode($resolved);
241 return $data;
242 }
243 /*
244 * This is an extremely naive but reasonably readable implementation. There
245 * are much faster algorithms (e.g., Boyer-Moore) for this if speed is ever
246 * an issue, but I suspect it won't be.
247 */
248 private function findMetadataStart(string $filename) : int
249 {
250 $handle = $this->fileHandle;
251 $fstat = fstat($handle);
252 if ($fstat === \false) {
253 throw new InvalidDatabaseException("Error getting file information ({$filename}).");
254 }
255 $fileSize = $fstat['size'];
256 $marker = self::$METADATA_START_MARKER;
257 $markerLength = self::$METADATA_START_MARKER_LENGTH;
258 $minStart = $fileSize - min(self::$METADATA_MAX_SIZE, $fileSize);
259 for ($offset = $fileSize - $markerLength; $offset >= $minStart; --$offset) {
260 if (fseek($handle, $offset) !== 0) {
261 break;
262 }
263 $value = fread($handle, $markerLength);
264 if ($value === $marker) {
265 return $offset + $markerLength;
266 }
267 }
268 throw new InvalidDatabaseException("Error opening database file ({$filename}). " . 'Is this a valid MaxMind DB file?');
269 }
270 /**
271 * @throws \InvalidArgumentException if arguments are passed to the method
272 * @throws \BadMethodCallException if the database has been closed
273 *
274 * @return Metadata object for the database
275 */
276 public function metadata() : Metadata
277 {
278 if (\func_num_args()) {
279 throw new \ArgumentCountError(\sprintf('%s() expects exactly 0 parameters, %d given', __METHOD__, \func_num_args()));
280 }
281 // Not technically required, but this makes it consistent with
282 // C extension and it allows us to change our implementation later.
283 if (!\is_resource($this->fileHandle)) {
284 throw new \BadMethodCallException('Attempt to read from a closed MaxMind DB.');
285 }
286 return clone $this->metadata;
287 }
288 /**
289 * Closes the MaxMind DB and returns resources to the system.
290 *
291 * @throws \Exception
292 * if an I/O error occurs
293 */
294 public function close() : void
295 {
296 if (\func_num_args()) {
297 throw new \ArgumentCountError(\sprintf('%s() expects exactly 0 parameters, %d given', __METHOD__, \func_num_args()));
298 }
299 if (!\is_resource($this->fileHandle)) {
300 throw new \BadMethodCallException('Attempt to close a closed MaxMind DB.');
301 }
302 fclose($this->fileHandle);
303 }
304 }
305