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 12 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 = 15;
19
20 private $decoder;
21 private $fileHandle;
22 private $fileSize;
23 private $ipV4Start;
24 private $metadata;
25
26 27 28 29 30 31 32 33 34 35 36
37 public function __construct($database)
38 {
39 if (func_num_args() != 1) {
40 throw new \InvalidArgumentException(
41 'The constructor takes exactly one argument.'
42 );
43 }
44
45 if (!is_readable($database)) {
46 throw new \InvalidArgumentException(
47 "The file \"$database\" does not exist or is not readable."
48 );
49 }
50 $this->fileHandle = @fopen($database, 'rb');
51 if ($this->fileHandle === false) {
52 throw new \InvalidArgumentException(
53 "Error opening \"$database\"."
54 );
55 }
56 $this->fileSize = @filesize($database);
57 if ($this->fileSize === false) {
58 throw new \UnexpectedValueException(
59 "Error determining the size of \"$database\"."
60 );
61 }
62
63 $start = $this->findMetadataStart($database);
64 $metadataDecoder = new Decoder($this->fileHandle, $start);
65 list($metadataArray) = $metadataDecoder->decode($start);
66 $this->metadata = new Metadata($metadataArray);
67 $this->decoder = new Decoder(
68 $this->fileHandle,
69 $this->metadata->searchTreeSize + self::$DATA_SECTION_SEPARATOR_SIZE
70 );
71 }
72
73 74 75 76 77 78 79 80 81 82 83 84
85 public function get($ipAddress)
86 {
87 if (func_num_args() != 1) {
88 throw new \InvalidArgumentException(
89 'Method takes exactly one argument.'
90 );
91 }
92
93 if (!is_resource($this->fileHandle)) {
94 throw new \BadMethodCallException(
95 'Attempt to read from a closed MaxMind DB.'
96 );
97 }
98
99 if (!filter_var($ipAddress, FILTER_VALIDATE_IP)) {
100 throw new \InvalidArgumentException(
101 "The value \"$ipAddress\" is not a valid IP address."
102 );
103 }
104
105 if ($this->metadata->ipVersion == 4 && strrpos($ipAddress, ':')) {
106 throw new \InvalidArgumentException(
107 "Error looking up $ipAddress. You attempted to look up an"
108 . " IPv6 address in an IPv4-only database."
109 );
110 }
111 $pointer = $this->findAddressInTree($ipAddress);
112 if ($pointer == 0) {
113 return null;
114 }
115 return $this->resolveDataPointer($pointer);
116 }
117
118 private function findAddressInTree($ipAddress)
119 {
120
121 $rawAddress = array_merge(unpack('C*', inet_pton($ipAddress)));
122
123 $bitCount = count($rawAddress) * 8;
124
125
126
127 $node = $this->startNode($bitCount);
128
129 for ($i = 0; $i < $bitCount; $i++) {
130 if ($node >= $this->metadata->nodeCount) {
131 break;
132 }
133 $tempBit = 0xFF & $rawAddress[$i >> 3];
134 $bit = 1 & ($tempBit >> 7 - ($i % 8));
135
136 $node = $this->readNode($node, $bit);
137 }
138 if ($node == $this->metadata->nodeCount) {
139
140 return 0;
141 } elseif ($node > $this->metadata->nodeCount) {
142
143 return $node;
144 }
145 throw new InvalidDatabaseException("Something bad happened");
146 }
147
148
149 private function startNode($length)
150 {
151
152
153 if ($this->metadata->ipVersion == 6 && $length == 32) {
154 return $this->ipV4StartNode();
155 }
156
157
158 return 0;
159 }
160
161 private function ipV4StartNode()
162 {
163
164
165 if ($this->metadata->ipVersion == 4) {
166 return 0;
167 }
168
169 if ($this->ipV4Start != 0) {
170 return $this->ipV4Start;
171 }
172 $node = 0;
173
174 for ($i = 0; $i < 96 && $node < $this->metadata->nodeCount; $i++) {
175 $node = $this->readNode($node, 0);
176 }
177 $this->ipV4Start = $node;
178 return $node;
179 }
180
181 private function readNode($nodeNumber, $index)
182 {
183 $baseOffset = $nodeNumber * $this->metadata->nodeByteSize;
184
185
186 switch ($this->metadata->recordSize) {
187 case 24:
188 $bytes = Util::read($this->fileHandle, $baseOffset + $index * 3, 3);
189 list(, $node) = unpack('N', "\x00" . $bytes);
190 return $node;
191 case 28:
192 $middleByte = Util::read($this->fileHandle, $baseOffset + 3, 1);
193 list(, $middle) = unpack('C', $middleByte);
194 if ($index == 0) {
195 $middle = (0xF0 & $middle) >> 4;
196 } else {
197 $middle = 0x0F & $middle;
198 }
199 $bytes = Util::read($this->fileHandle, $baseOffset + $index * 4, 3);
200 list(, $node) = unpack('N', chr($middle) . $bytes);
201 return $node;
202 case 32:
203 $bytes = Util::read($this->fileHandle, $baseOffset + $index * 4, 4);
204 list(, $node) = unpack('N', $bytes);
205 return $node;
206 default:
207 throw new InvalidDatabaseException(
208 'Unknown record size: '
209 . $this->metadata->recordSize
210 );
211 }
212 }
213
214 private function resolveDataPointer($pointer)
215 {
216 $resolved = $pointer - $this->metadata->nodeCount
217 + $this->metadata->searchTreeSize;
218 if ($resolved > $this->fileSize) {
219 throw new InvalidDatabaseException(
220 "The MaxMind DB file's search tree is corrupt"
221 );
222 }
223
224 list($data) = $this->decoder->decode($resolved);
225 return $data;
226 }
227
228 229 230 231 232
233 private function findMetadataStart($filename)
234 {
235 $handle = $this->fileHandle;
236 $fstat = fstat($handle);
237 $fileSize = $fstat['size'];
238 $marker = self::$METADATA_START_MARKER;
239 $markerLength = self::$METADATA_START_MARKER_LENGTH;
240
241 for ($i = 0; $i < $fileSize - $markerLength; $i++) {
242 for ($j = 0; $j < $markerLength; $j++) {
243 fseek($handle, $fileSize - $i - $j - 1);
244 $matchBit = fgetc($handle);
245 if ($matchBit != $marker[$markerLength - $j - 1]) {
246 continue 2;
247 }
248 }
249 return $fileSize - $i;
250 }
251 throw new InvalidDatabaseException(
252 "Error opening database file ($filename). " .
253 'Is this a valid MaxMind DB file?'
254 );
255 }
256
257 258 259 260 261
262 public function metadata()
263 {
264 if (func_num_args()) {
265 throw new \InvalidArgumentException(
266 'Method takes no arguments.'
267 );
268 }
269
270
271
272 if (!is_resource($this->fileHandle)) {
273 throw new \BadMethodCallException(
274 'Attempt to read from a closed MaxMind DB.'
275 );
276 }
277
278 return $this->metadata;
279 }
280
281 282 283 284 285 286
287 public function close()
288 {
289 if (!is_resource($this->fileHandle)) {
290 throw new \BadMethodCallException(
291 'Attempt to close a closed MaxMind DB.'
292 );
293 }
294 fclose($this->fileHandle);
295 }
296 }
297