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 = 14;
19: private static $METADATA_MAX_SIZE = 131072;
20:
21: private $decoder;
22: private $fileHandle;
23: private $fileSize;
24: private $ipV4Start;
25: private $metadata;
26:
27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 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: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 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:
126: $rawAddress = array_merge(unpack('C*', inet_pton($ipAddress)));
127:
128: $bitCount = count($rawAddress) * 8;
129:
130:
131:
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:
145: return 0;
146: } elseif ($node > $this->metadata->nodeCount) {
147:
148: return $node;
149: }
150: throw new InvalidDatabaseException('Something bad happened');
151: }
152:
153: private function startNode($length)
154: {
155:
156:
157: if ($this->metadata->ipVersion === 6 && $length === 32) {
158: return $this->ipV4StartNode();
159: }
160:
161:
162: return 0;
163: }
164:
165: private function ipV4StartNode()
166: {
167:
168:
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:
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: 239: 240: 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: 271: 272: 273: 274:
275: public function metadata()
276: {
277: if (func_num_args()) {
278: throw new \InvalidArgumentException(
279: 'Method takes no arguments.'
280: );
281: }
282:
283:
284:
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: 296: 297: 298: 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: