├── CHANGELOG.md ├── LICENSE ├── README.md ├── autoload.php ├── composer.json ├── ext ├── config.m4 ├── config.w32 ├── maxminddb.c ├── php_maxminddb.h └── tests │ ├── 001-load.phpt │ ├── 002-final.phpt │ └── 003-open-basedir.phpt ├── package.xml └── src └── MaxMind └── Db ├── Reader.php └── Reader ├── Decoder.php ├── InvalidDatabaseException.php ├── Metadata.php └── Util.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | 1.13.0 5 | ------------------- 6 | 7 | * A redundant `filesize()` call in the reader's constructor was removed. 8 | Pull request by Pavel Djundik. GitHub #189. 9 | 10 | 1.12.1 (2025-05-05) 11 | ------------------- 12 | 13 | * The C extension now checks that the database metadata lookup was 14 | successful. 15 | 16 | 1.12.0 (2024-11-14) 17 | ------------------- 18 | 19 | * Improve the error handling when the user tries to open a directory 20 | with the pure PHP reader. 21 | * Improve the typehints on arrays in the PHPDocs. 22 | 23 | 1.11.1 (2023-12-01) 24 | ------------------- 25 | 26 | * Resolve warnings when compiling the C extension. 27 | * Fix various type issues detected by PHPStan level. Pull request by 28 | LauraTaylorUK. GitHub #160. 29 | 30 | 1.11.0 (2021-10-18) 31 | ------------------- 32 | 33 | * Replace runtime define of a constant to facilitate opcache preloading. 34 | Reported by vedadkajtaz. GitHub #134. 35 | * Resolve minor issue found by the Clang static analyzer in the C 36 | extension. 37 | 38 | 1.10.1 (2021-04-14) 39 | ------------------- 40 | 41 | * Fix a `TypeError` exception in the pure PHP reader when using large 42 | databases on 32-bit PHP builds with the `bcmath` extension. Reported 43 | by dodo1708. GitHub #124. 44 | 45 | 1.10.0 (2021-02-09) 46 | ------------------- 47 | 48 | * When using the pure PHP reader, unsigned integers up to PHP_MAX_INT 49 | will now be integers in PHP rather than strings. Previously integers 50 | greater than 2^24 on 32-bit platforms and 2^56 on 64-bit platforms 51 | would be strings due to the use of `gmp` or `bcmath` to decode them. 52 | Reported by Alejandro Celaya. GitHub #119. 53 | 54 | 1.9.0 (2021-01-07) 55 | ------------------ 56 | 57 | * The `maxminddb` extension is now buildable on Windows. Pull request 58 | by Jan Ehrhardt. GitHub #115. 59 | 60 | 1.8.0 (2020-10-01) 61 | ------------------ 62 | 63 | * Fixes for PHP 8.0. Pull Request by Remi Collet. GitHub #108. 64 | 65 | 1.7.0 (2020-08-07) 66 | ------------------ 67 | 68 | * IMPORTANT: PHP 7.2 or greater is now required. 69 | * The extension no longer depends on the pure PHP classes in 70 | `maxmind-db/reader`. You can use it independently. 71 | * Type hints have been added to both the pure PHP implementation 72 | and the extension. 73 | * The `metadata` method on the reader now returns a new copy of the 74 | metadata object rather than the actual object used by the reader. 75 | * Work around PHP `is_readable()` bug. Reported by Ben Roberts. GitHub 76 | #92. 77 | * This is the first release of the extension as a PECL package. 78 | GitHub #34. 79 | 80 | 1.6.0 (2019-12-19) 81 | ------------------ 82 | 83 | * 1.5.0 and 1.5.1 contained a possible memory corruptions when using 84 | `getWithPrefixLen`. This has been fixed. Reported by proton-ab. 85 | GitHub #96. 86 | * The `composer.json` file now conflicts with all versions of the 87 | `maxminddb` C extension less than the Composer version. This is to 88 | reduce the chance of having an older, conflicting version of the 89 | extension installed. You will need to upgrade the extension before 90 | running `composer update`. Pull request by Benoît Burnichon. GitHub 91 | #97. 92 | 93 | 1.5.1 (2019-12-12) 94 | ------------------ 95 | 96 | * Minor performance improvements. 97 | * Make tests pass with older versions of libmaxminddb. PR by Remi 98 | Collet. GitHub #90. 99 | * Test enhancements. PR by Chun-Sheng, Li. GitHub #91. 100 | 101 | 1.5.0 (2019-09-30) 102 | ------------------ 103 | 104 | * PHP 5.6 or greater is now required. 105 | * The C extension now supports PHP 8. Pull request by John Boehr. 106 | GitHub #87. 107 | * A new method, `getWithPrefixLen`, was added to the `Reader` class. 108 | This method returns an array containing the record and the prefix 109 | length for that record. GitHub #89. 110 | 111 | 1.4.1 (2019-01-04) 112 | ------------------ 113 | 114 | * The `maxminddb` extension now returns a string when a `uint32` 115 | value is greater than `LONG_MAX`. Previously, the value would 116 | overflow. This generally only affects 32-bit machines. Reported 117 | by Remi Collet. GitHub #79. 118 | * For `uint64` values, the `maxminddb` extension now returns an 119 | integer rather than a string when the value is less than or equal 120 | to `LONG_MAX`. This more closely matches the behavior of the pure 121 | PHP reader. 122 | 123 | 1.4.0 (2018-11-20) 124 | ------------------ 125 | 126 | * The `maxminddb` extension now has the arginfo when using reflection. 127 | PR by Remi Collet. GitHub #75. 128 | * The `maxminddb` extension now provides `MINFO()` function that 129 | displays the extension version and the libmaxminddb version. PR by 130 | Remi Collet. GitHub #74. 131 | * The `maxminddb` `configure` script now uses `pkg-config` when 132 | available to get libmaxmindb build info. PR by Remi Collet. 133 | GitHub #73. 134 | * The pure PHP reader now correctly decodes integers on 32-bit platforms. 135 | Previously, large integers would overflow. Reported by Remi Collet. 136 | GitHub #77. 137 | * There are small performance improvements for the pure PHP reader. 138 | 139 | 1.3.0 (2018-02-21) 140 | ------------------ 141 | 142 | * IMPORTANT: The `maxminddb` extension now obeys `open_basedir`. If 143 | `open_basedir` is set, you _must_ store the database within the 144 | specified directory. Placing the file outside of this directory 145 | will result in an exception. Please test your integration before 146 | upgrading the extension. This does not affect the pure PHP 147 | implementation, which has always had this restriction. Reported 148 | by Benoît Burnichon. GitHub #61. 149 | * A custom `autoload.php` file is provided for installations without 150 | Composer. GitHub #56. 151 | 152 | 1.2.0 (2017-10-27) 153 | ------------------ 154 | 155 | * PHP 5.4 or greater is now required. 156 | * The `Reader` class for the `maxminddb` extension is no longer final. 157 | This was change to match the behavior of the pure PHP class. 158 | Reported and fixed by venyii. GitHub #52 & #54. 159 | 160 | 1.1.3 (2017-01-19) 161 | ------------------ 162 | 163 | * Fix incorrect version in `ext/php_maxminddb.h`. GitHub #48. 164 | 165 | 1.1.2 (2016-11-22) 166 | ------------------ 167 | 168 | * Searching for database metadata only occurs within the last 128KB 169 | (128 * 1024 bytes) of the file, speeding detection of corrupt 170 | datafiles. Reported by Eric Teubert. GitHub #42. 171 | * Suggest relevant extensions when installing with Composer. GitHub #37. 172 | 173 | 1.1.1 (2016-09-15) 174 | ------------------ 175 | 176 | * Development files were added to the `.gitattributes` as `export-ignore` so 177 | that they are not part of the Composer release. Pull request by Michele 178 | Locati. GitHub #39. 179 | 180 | 1.1.0 (2016-01-04) 181 | ------------------ 182 | 183 | * The MaxMind DB extension now supports PHP 7. Pull request by John Boehr. 184 | GitHub #27. 185 | 186 | 1.0.3 (2015-03-13) 187 | ------------------ 188 | 189 | * All uses of `strlen` were removed. This should prevent issues in situations 190 | where the function is overloaded or otherwise broken. 191 | 192 | 1.0.2 (2015-01-19) 193 | ------------------ 194 | 195 | * Previously the MaxMind DB extension would cause a segfault if the Reader 196 | object's destructor was called without first having called the constructor. 197 | (Reported by Matthias Saou & Juan Peri. GitHub #20.) 198 | 199 | 1.0.1 (2015-01-12) 200 | ------------------ 201 | 202 | * In the last several releases, the version number in the extension was 203 | incorrect. This release is being done to correct it. No other code changes 204 | are included. 205 | 206 | 1.0.0 (2014-09-22) 207 | ------------------ 208 | 209 | * First production release. 210 | * In the pure PHP reader, a string length test after `fread()` was replaced 211 | with the difference between the start pointer and the end pointer. This 212 | provided a 15% speed increase. 213 | 214 | 0.3.3 (2014-09-15) 215 | ------------------ 216 | 217 | * Clarified behavior of 128-bit type in documentation. 218 | * Updated phpunit and fixed some test breakage from the newer version. 219 | 220 | 0.3.2 (2014-09-10) 221 | ------------------ 222 | 223 | * Fixed invalid reference to global class RuntimeException from namespaced 224 | code. Fixed by Steven Don. GitHub issue #15. 225 | * Additional documentation of `Metadata` class as well as misc. documentation 226 | cleanup. 227 | 228 | 0.3.1 (2014-05-01) 229 | ------------------ 230 | 231 | * The API now works when `mbstring.func_overload` is set. 232 | * BCMath is no longer required. If the decoder encounters a big integer, 233 | it will try to use GMP and then BCMath. If both of those fail, it will 234 | throw an exception. No databases released by MaxMind currently use big 235 | integers. 236 | * The API now officially supports HHVM when using the pure PHP reader. 237 | 238 | 0.3.0 (2014-02-19) 239 | ------------------ 240 | 241 | * This API is now licensed under the Apache License, Version 2.0. 242 | * The code for the C extension was cleaned up, fixing several potential 243 | issues. 244 | 245 | 0.2.0 (2013-10-21) 246 | ------------------ 247 | 248 | * Added optional C extension for using libmaxminddb in place of the pure PHP 249 | reader. 250 | * Significantly improved error handling in pure PHP reader. 251 | * Improved performance for IPv4 lookups in an IPv6 database. 252 | 253 | 0.1.0 (2013-07-16) 254 | ------------------ 255 | 256 | * Initial release 257 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MaxMind DB Reader PHP API # 2 | 3 | ## Description ## 4 | 5 | This is the PHP API for reading MaxMind DB files. MaxMind DB is a binary file 6 | format that stores data indexed by IP address subnets (IPv4 or IPv6). 7 | 8 | ## Installation (Composer) ## 9 | 10 | We recommend installing this package with [Composer](https://getcomposer.org/). 11 | 12 | ### Download Composer ### 13 | 14 | To download Composer, run in the root directory of your project: 15 | 16 | ```bash 17 | curl -sS https://getcomposer.org/installer | php 18 | ``` 19 | 20 | You should now have the file `composer.phar` in your project directory. 21 | 22 | ### Install Dependencies ### 23 | 24 | Run in your project root: 25 | 26 | ``` 27 | php composer.phar require maxmind-db/reader:^1.12.1 28 | ``` 29 | 30 | You should now have the files `composer.json` and `composer.lock` as well as 31 | the directory `vendor` in your project directory. If you use a version control 32 | system, `composer.json` should be added to it. 33 | 34 | ### Require Autoloader ### 35 | 36 | After installing the dependencies, you need to require the Composer autoloader 37 | from your code: 38 | 39 | ```php 40 | require 'vendor/autoload.php'; 41 | ``` 42 | 43 | ## Installation (Standalone) ## 44 | 45 | If you don't want to use Composer for some reason, a custom 46 | `autoload.php` is provided for you in the project root. To use the 47 | library, simply include that file, 48 | 49 | ```php 50 | require('/path/to/MaxMind-DB-Reader-php/autoload.php'); 51 | ``` 52 | 53 | and then instantiate the reader class normally: 54 | 55 | ```php 56 | use MaxMind\Db\Reader; 57 | $reader = new Reader('example.mmdb'); 58 | ``` 59 | 60 | ## Installation (RPM) 61 | 62 | RPMs are available in the [official Fedora repository](https://apps.fedoraproject.org/packages/php-maxminddb). 63 | 64 | To install on Fedora, run: 65 | 66 | ```bash 67 | dnf install php-maxminddb 68 | ``` 69 | 70 | To install on CentOS or RHEL 7, first [enable the EPEL repository](https://fedoraproject.org/wiki/EPEL) 71 | and then run: 72 | 73 | ```bash 74 | yum install php-maxminddb 75 | ``` 76 | 77 | Please note that these packages are *not* maintained by MaxMind. 78 | 79 | ## Usage ## 80 | 81 | ## Example ## 82 | 83 | ```php 84 | get($ipAddress)); 96 | 97 | // getWithPrefixLen returns an array containing the record and the 98 | // associated prefix length for that record. 99 | print_r($reader->getWithPrefixLen($ipAddress)); 100 | 101 | $reader->close(); 102 | ``` 103 | 104 | ## Optional PHP C Extension ## 105 | 106 | MaxMind provides an optional C extension that is a drop-in replacement for 107 | `MaxMind\Db\Reader`. In order to use this extension, you must install the 108 | Reader API as described above and install the extension as described below. If 109 | you are using an autoloader, no changes to your code should be necessary. 110 | 111 | ### Installing Extension ### 112 | 113 | First install [libmaxminddb](https://github.com/maxmind/libmaxminddb) as 114 | described in its [README.md 115 | file](https://github.com/maxmind/libmaxminddb/blob/main/README.md#installing-from-a-tarball). 116 | After successfully installing libmaxmindb, you may install the extension 117 | from [pecl](https://pecl.php.net/package/maxminddb): 118 | 119 | ``` 120 | pecl install maxminddb 121 | ``` 122 | 123 | Alternatively, you may install it from the source. To do so, run the following 124 | commands from the top-level directory of this distribution: 125 | 126 | ``` 127 | cd ext 128 | phpize 129 | ./configure 130 | make 131 | make test 132 | sudo make install 133 | ``` 134 | 135 | You then must load your extension. The recommended method is to add the 136 | following to your `php.ini` file: 137 | 138 | ``` 139 | extension=maxminddb.so 140 | ``` 141 | 142 | Note: You may need to install the PHP development package on your OS such as 143 | php5-dev for Debian-based systems or php-devel for RedHat/Fedora-based ones. 144 | 145 | ## 128-bit Integer Support ## 146 | 147 | The MaxMind DB format includes 128-bit unsigned integer as a type. Although 148 | no MaxMind-distributed database currently makes use of this type, both the 149 | pure PHP reader and the C extension support this type. The pure PHP reader 150 | requires gmp or bcmath to read databases with 128-bit unsigned integers. 151 | 152 | The integer is currently returned as a hexadecimal string (prefixed with "0x") 153 | by the C extension and a decimal string (no prefix) by the pure PHP reader. 154 | Any change to make the reader implementations always return either a 155 | hexadecimal or decimal representation of the integer will NOT be considered a 156 | breaking change. 157 | 158 | ## Support ## 159 | 160 | Please report all issues with this code using the [GitHub issue tracker](https://github.com/maxmind/MaxMind-DB-Reader-php/issues). 161 | 162 | If you are having an issue with a MaxMind service that is not specific to the 163 | client API, please see [our support page](https://www.maxmind.com/en/support). 164 | 165 | ## Requirements ## 166 | 167 | This library requires PHP 7.2 or greater. 168 | 169 | The GMP or BCMath extension may be required to read some databases 170 | using the pure PHP API. 171 | 172 | ## Contributing ## 173 | 174 | Patches and pull requests are encouraged. All code should follow the PSR-1 and 175 | PSR-2 style guidelines. Please include unit tests whenever possible. 176 | 177 | ## Versioning ## 178 | 179 | The MaxMind DB Reader PHP API uses [Semantic Versioning](https://semver.org/). 180 | 181 | ## Copyright and License ## 182 | 183 | This software is Copyright (c) 2014-2025 by MaxMind, Inc. 184 | 185 | This is free software, licensed under the Apache License, Version 2.0. 186 | -------------------------------------------------------------------------------- /autoload.php: -------------------------------------------------------------------------------- 1 | class. 15 | * 16 | * @param string $class 17 | * the name of the class to load 18 | */ 19 | function mmdb_autoload($class): void 20 | { 21 | /* 22 | * A project-specific mapping between the namespaces and where 23 | * they're located. By convention, we include the trailing 24 | * slashes. The one-element array here simply makes things easy 25 | * to extend in the future if (for example) the test classes 26 | * begin to use one another. 27 | */ 28 | $namespace_map = ['MaxMind\Db\\' => __DIR__ . '/src/MaxMind/Db/']; 29 | 30 | foreach ($namespace_map as $prefix => $dir) { 31 | // First swap out the namespace prefix with a directory... 32 | $path = str_replace($prefix, $dir, $class); 33 | 34 | // replace the namespace separator with a directory separator... 35 | $path = str_replace('\\', '/', $path); 36 | 37 | // and finally, add the PHP file extension to the result. 38 | $path .= '.php'; 39 | 40 | // $path should now contain the path to a PHP file defining $class 41 | if (file_exists($path)) { 42 | include $path; 43 | } 44 | } 45 | } 46 | 47 | spl_autoload_register('mmdb_autoload'); 48 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "maxmind-db/reader", 3 | "description": "MaxMind DB Reader API", 4 | "keywords": ["database", "geoip", "geoip2", "geolocation", "maxmind"], 5 | "homepage": "https://github.com/maxmind/MaxMind-DB-Reader-php", 6 | "type": "library", 7 | "license": "Apache-2.0", 8 | "authors": [ 9 | { 10 | "name": "Gregory J. Oschwald", 11 | "email": "goschwald@maxmind.com", 12 | "homepage": "https://www.maxmind.com/" 13 | } 14 | ], 15 | "require": { 16 | "php": ">=7.2" 17 | }, 18 | "suggest": { 19 | "ext-bcmath": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder", 20 | "ext-gmp": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder", 21 | "ext-maxminddb": "A C-based database decoder that provides significantly faster lookups" 22 | }, 23 | "conflict": { 24 | "ext-maxminddb": "<1.11.1 || >=2.0.0" 25 | }, 26 | "require-dev": { 27 | "friendsofphp/php-cs-fixer": "3.*", 28 | "phpunit/phpunit": ">=8.0.0,<10.0.0", 29 | "squizlabs/php_codesniffer": "3.*", 30 | "phpstan/phpstan": "*" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "MaxMind\\Db\\": "src/MaxMind/Db" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "MaxMind\\Db\\Test\\Reader\\": "tests/MaxMind/Db/Test/Reader" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ext/config.m4: -------------------------------------------------------------------------------- 1 | PHP_ARG_WITH(maxminddb, 2 | [Whether to enable the MaxMind DB Reader extension], 3 | [ --with-maxminddb Enable MaxMind DB Reader extension support]) 4 | 5 | PHP_ARG_ENABLE(maxminddb-debug, for MaxMind DB debug support, 6 | [ --enable-maxminddb-debug Enable MaxMind DB debug support], no, no) 7 | 8 | if test $PHP_MAXMINDDB != "no"; then 9 | 10 | AC_PATH_PROG(PKG_CONFIG, pkg-config, no) 11 | 12 | AC_MSG_CHECKING(for libmaxminddb) 13 | if test -x "$PKG_CONFIG" && $PKG_CONFIG --exists libmaxminddb; then 14 | dnl retrieve build options from pkg-config 15 | if $PKG_CONFIG libmaxminddb --atleast-version 1.0.0; then 16 | LIBMAXMINDDB_INC=`$PKG_CONFIG libmaxminddb --cflags` 17 | LIBMAXMINDDB_LIB=`$PKG_CONFIG libmaxminddb --libs` 18 | LIBMAXMINDDB_VER=`$PKG_CONFIG libmaxminddb --modversion` 19 | AC_MSG_RESULT(found version $LIBMAXMINDDB_VER) 20 | else 21 | AC_MSG_ERROR(system libmaxminddb must be upgraded to version >= 1.0.0) 22 | fi 23 | PHP_EVAL_LIBLINE($LIBMAXMINDDB_LIB, MAXMINDDB_SHARED_LIBADD) 24 | PHP_EVAL_INCLINE($LIBMAXMINDDB_INC) 25 | else 26 | AC_MSG_RESULT(pkg-config information missing) 27 | AC_MSG_WARN(will use libmaxmxinddb from compiler default path) 28 | 29 | PHP_CHECK_LIBRARY(maxminddb, MMDB_open) 30 | PHP_ADD_LIBRARY(maxminddb, 1, MAXMINDDB_SHARED_LIBADD) 31 | fi 32 | 33 | if test $PHP_MAXMINDDB_DEBUG != "no"; then 34 | CFLAGS="$CFLAGS -Wall -Wextra -Wno-unused-parameter -Wno-missing-field-initializers -Werror" 35 | fi 36 | 37 | PHP_SUBST(MAXMINDDB_SHARED_LIBADD) 38 | 39 | PHP_NEW_EXTENSION(maxminddb, maxminddb.c, $ext_shared) 40 | fi 41 | -------------------------------------------------------------------------------- /ext/config.w32: -------------------------------------------------------------------------------- 1 | ARG_WITH("maxminddb", "Enable MaxMind DB Reader extension support", "no"); 2 | 3 | if (PHP_MAXMINDDB == "yes") { 4 | if (CHECK_HEADER_ADD_INCLUDE("maxminddb.h", "CFLAGS_MAXMINDDB", PHP_MAXMINDDB + ";" + PHP_PHP_BUILD + "\\include\\maxminddb") && 5 | CHECK_LIB("libmaxminddb.lib", "maxminddb", PHP_MAXMINDDB)) { 6 | EXTENSION("maxminddb", "maxminddb.c"); 7 | } else { 8 | WARNING('Could not find maxminddb.h or libmaxminddb.lib; skipping'); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ext/maxminddb.c: -------------------------------------------------------------------------------- 1 | /* MaxMind, Inc., licenses this file to you under the Apache License, Version 2 | * 2.0 (the "License"); you may not use this file except in compliance with 3 | * the License. You may obtain a copy of the License at 4 | * 5 | * http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | * License for the specific language governing permissions and limitations 11 | * under the License. 12 | */ 13 | 14 | #include "php_maxminddb.h" 15 | 16 | #ifdef HAVE_CONFIG_H 17 | #include "config.h" 18 | #endif 19 | 20 | #include 21 | #include 22 | 23 | #include "Zend/zend_exceptions.h" 24 | #include "Zend/zend_types.h" 25 | #include "ext/spl/spl_exceptions.h" 26 | #include "ext/standard/info.h" 27 | #include 28 | 29 | #ifdef ZTS 30 | #include 31 | #endif 32 | 33 | #define __STDC_FORMAT_MACROS 34 | #include 35 | 36 | #define PHP_MAXMINDDB_NS ZEND_NS_NAME("MaxMind", "Db") 37 | #define PHP_MAXMINDDB_READER_NS ZEND_NS_NAME(PHP_MAXMINDDB_NS, "Reader") 38 | #define PHP_MAXMINDDB_METADATA_NS \ 39 | ZEND_NS_NAME(PHP_MAXMINDDB_READER_NS, "Metadata") 40 | #define PHP_MAXMINDDB_READER_EX_NS \ 41 | ZEND_NS_NAME(PHP_MAXMINDDB_READER_NS, "InvalidDatabaseException") 42 | 43 | #define Z_MAXMINDDB_P(zv) php_maxminddb_fetch_object(Z_OBJ_P(zv)) 44 | typedef size_t strsize_t; 45 | typedef zend_object free_obj_t; 46 | 47 | /* For PHP 8 compatibility */ 48 | #if PHP_VERSION_ID < 80000 49 | 50 | #define PROP_OBJ(zv) (zv) 51 | 52 | #else 53 | 54 | #define PROP_OBJ(zv) Z_OBJ_P(zv) 55 | 56 | #define TSRMLS_C 57 | #define TSRMLS_CC 58 | #define TSRMLS_DC 59 | 60 | /* End PHP 8 compatibility */ 61 | #endif 62 | 63 | #ifndef ZEND_ACC_CTOR 64 | #define ZEND_ACC_CTOR 0 65 | #endif 66 | 67 | /* IS_MIXED was added in 2020 */ 68 | #ifndef IS_MIXED 69 | #define IS_MIXED IS_UNDEF 70 | #endif 71 | 72 | /* ZEND_THIS was added in 7.4 */ 73 | #ifndef ZEND_THIS 74 | #define ZEND_THIS (&EX(This)) 75 | #endif 76 | 77 | typedef struct _maxminddb_obj { 78 | MMDB_s *mmdb; 79 | zend_object std; 80 | } maxminddb_obj; 81 | 82 | PHP_FUNCTION(maxminddb); 83 | 84 | static int 85 | get_record(INTERNAL_FUNCTION_PARAMETERS, zval *record, int *prefix_len); 86 | static const MMDB_entry_data_list_s * 87 | handle_entry_data_list(const MMDB_entry_data_list_s *entry_data_list, 88 | zval *z_value TSRMLS_DC); 89 | static const MMDB_entry_data_list_s * 90 | handle_array(const MMDB_entry_data_list_s *entry_data_list, 91 | zval *z_value TSRMLS_DC); 92 | static const MMDB_entry_data_list_s * 93 | handle_map(const MMDB_entry_data_list_s *entry_data_list, 94 | zval *z_value TSRMLS_DC); 95 | static void handle_uint128(const MMDB_entry_data_list_s *entry_data_list, 96 | zval *z_value TSRMLS_DC); 97 | static void handle_uint64(const MMDB_entry_data_list_s *entry_data_list, 98 | zval *z_value TSRMLS_DC); 99 | static void handle_uint32(const MMDB_entry_data_list_s *entry_data_list, 100 | zval *z_value TSRMLS_DC); 101 | 102 | #define CHECK_ALLOCATED(val) \ 103 | if (!val) { \ 104 | zend_error(E_ERROR, "Out of memory"); \ 105 | return; \ 106 | } 107 | 108 | static zend_object_handlers maxminddb_obj_handlers; 109 | static zend_class_entry *maxminddb_ce, *maxminddb_exception_ce, *metadata_ce; 110 | 111 | static inline maxminddb_obj * 112 | php_maxminddb_fetch_object(zend_object *obj TSRMLS_DC) { 113 | return (maxminddb_obj *)((char *)(obj)-XtOffsetOf(maxminddb_obj, std)); 114 | } 115 | 116 | ZEND_BEGIN_ARG_INFO_EX(arginfo_maxminddbreader_construct, 0, 0, 1) 117 | ZEND_ARG_TYPE_INFO(0, db_file, IS_STRING, 0) 118 | ZEND_END_ARG_INFO() 119 | 120 | PHP_METHOD(MaxMind_Db_Reader, __construct) { 121 | char *db_file = NULL; 122 | strsize_t name_len; 123 | zval *_this_zval = NULL; 124 | 125 | if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC, 126 | getThis(), 127 | "Os", 128 | &_this_zval, 129 | maxminddb_ce, 130 | &db_file, 131 | &name_len) == FAILURE) { 132 | return; 133 | } 134 | 135 | if (0 != php_check_open_basedir(db_file TSRMLS_CC) || 136 | 0 != access(db_file, R_OK)) { 137 | zend_throw_exception_ex( 138 | spl_ce_InvalidArgumentException, 139 | 0 TSRMLS_CC, 140 | "The file \"%s\" does not exist or is not readable.", 141 | db_file); 142 | return; 143 | } 144 | 145 | MMDB_s *mmdb = (MMDB_s *)ecalloc(1, sizeof(MMDB_s)); 146 | int const status = MMDB_open(db_file, MMDB_MODE_MMAP, mmdb); 147 | 148 | if (MMDB_SUCCESS != status) { 149 | zend_throw_exception_ex( 150 | maxminddb_exception_ce, 151 | 0 TSRMLS_CC, 152 | "Error opening database file (%s). Is this a valid " 153 | "MaxMind DB file?", 154 | db_file); 155 | efree(mmdb); 156 | return; 157 | } 158 | 159 | maxminddb_obj *mmdb_obj = Z_MAXMINDDB_P(ZEND_THIS); 160 | mmdb_obj->mmdb = mmdb; 161 | } 162 | 163 | ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX( 164 | arginfo_maxminddbreader_get, 0, 1, IS_MIXED, 1) 165 | ZEND_ARG_TYPE_INFO(0, ip_address, IS_STRING, 0) 166 | ZEND_END_ARG_INFO() 167 | 168 | PHP_METHOD(MaxMind_Db_Reader, get) { 169 | int prefix_len = 0; 170 | get_record(INTERNAL_FUNCTION_PARAM_PASSTHRU, return_value, &prefix_len); 171 | } 172 | 173 | ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX( 174 | arginfo_maxminddbreader_getWithPrefixLen, 0, 1, IS_ARRAY, 1) 175 | ZEND_ARG_TYPE_INFO(0, ip_address, IS_STRING, 0) 176 | ZEND_END_ARG_INFO() 177 | 178 | PHP_METHOD(MaxMind_Db_Reader, getWithPrefixLen) { 179 | zval record, z_prefix_len; 180 | 181 | int prefix_len = 0; 182 | if (get_record(INTERNAL_FUNCTION_PARAM_PASSTHRU, &record, &prefix_len) == 183 | FAILURE) { 184 | return; 185 | } 186 | 187 | array_init(return_value); 188 | add_next_index_zval(return_value, &record); 189 | 190 | ZVAL_LONG(&z_prefix_len, prefix_len); 191 | add_next_index_zval(return_value, &z_prefix_len); 192 | } 193 | 194 | static int 195 | get_record(INTERNAL_FUNCTION_PARAMETERS, zval *record, int *prefix_len) { 196 | char *ip_address = NULL; 197 | strsize_t name_len; 198 | zval *this_zval = NULL; 199 | 200 | if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC, 201 | getThis(), 202 | "Os", 203 | &this_zval, 204 | maxminddb_ce, 205 | &ip_address, 206 | &name_len) == FAILURE) { 207 | return FAILURE; 208 | } 209 | 210 | const maxminddb_obj *mmdb_obj = (maxminddb_obj *)Z_MAXMINDDB_P(ZEND_THIS); 211 | 212 | MMDB_s *mmdb = mmdb_obj->mmdb; 213 | 214 | if (NULL == mmdb) { 215 | zend_throw_exception_ex(spl_ce_BadMethodCallException, 216 | 0 TSRMLS_CC, 217 | "Attempt to read from a closed MaxMind DB."); 218 | return FAILURE; 219 | } 220 | 221 | struct addrinfo hints = { 222 | .ai_family = AF_UNSPEC, 223 | .ai_flags = AI_NUMERICHOST, 224 | /* We set ai_socktype so that we only get one result back */ 225 | .ai_socktype = SOCK_STREAM}; 226 | 227 | struct addrinfo *addresses = NULL; 228 | int gai_status = getaddrinfo(ip_address, NULL, &hints, &addresses); 229 | if (gai_status) { 230 | zend_throw_exception_ex(spl_ce_InvalidArgumentException, 231 | 0 TSRMLS_CC, 232 | "The value \"%s\" is not a valid IP address.", 233 | ip_address); 234 | return FAILURE; 235 | } 236 | if (!addresses || !addresses->ai_addr) { 237 | zend_throw_exception_ex( 238 | spl_ce_InvalidArgumentException, 239 | 0 TSRMLS_CC, 240 | "getaddrinfo was successful but failed to set the addrinfo"); 241 | return FAILURE; 242 | } 243 | 244 | int sa_family = addresses->ai_addr->sa_family; 245 | 246 | int mmdb_error = MMDB_SUCCESS; 247 | MMDB_lookup_result_s result = 248 | MMDB_lookup_sockaddr(mmdb, addresses->ai_addr, &mmdb_error); 249 | 250 | freeaddrinfo(addresses); 251 | 252 | if (MMDB_SUCCESS != mmdb_error) { 253 | zend_class_entry *ex; 254 | if (MMDB_IPV6_LOOKUP_IN_IPV4_DATABASE_ERROR == mmdb_error) { 255 | ex = spl_ce_InvalidArgumentException; 256 | } else { 257 | ex = maxminddb_exception_ce; 258 | } 259 | zend_throw_exception_ex(ex, 260 | 0 TSRMLS_CC, 261 | "Error looking up %s. %s", 262 | ip_address, 263 | MMDB_strerror(mmdb_error)); 264 | return FAILURE; 265 | } 266 | 267 | *prefix_len = result.netmask; 268 | 269 | if (sa_family == AF_INET && mmdb->metadata.ip_version == 6) { 270 | /* We return the prefix length given the IPv4 address. If there is 271 | no IPv4 subtree, we return a prefix length of 0. */ 272 | *prefix_len = *prefix_len >= 96 ? *prefix_len - 96 : 0; 273 | } 274 | 275 | if (!result.found_entry) { 276 | ZVAL_NULL(record); 277 | return SUCCESS; 278 | } 279 | 280 | MMDB_entry_data_list_s *entry_data_list = NULL; 281 | int status = MMDB_get_entry_data_list(&result.entry, &entry_data_list); 282 | 283 | if (MMDB_SUCCESS != status) { 284 | zend_throw_exception_ex(maxminddb_exception_ce, 285 | 0 TSRMLS_CC, 286 | "Error while looking up data for %s. %s", 287 | ip_address, 288 | MMDB_strerror(status)); 289 | MMDB_free_entry_data_list(entry_data_list); 290 | return FAILURE; 291 | } else if (NULL == entry_data_list) { 292 | zend_throw_exception_ex( 293 | maxminddb_exception_ce, 294 | 0 TSRMLS_CC, 295 | "Error while looking up data for %s. Your database may " 296 | "be corrupt or you have found a bug in libmaxminddb.", 297 | ip_address); 298 | return FAILURE; 299 | } 300 | 301 | const MMDB_entry_data_list_s *rv = 302 | handle_entry_data_list(entry_data_list, record TSRMLS_CC); 303 | if (rv == NULL) { 304 | /* We should have already thrown the exception in handle_entry_data_list 305 | */ 306 | return FAILURE; 307 | } 308 | MMDB_free_entry_data_list(entry_data_list); 309 | return SUCCESS; 310 | } 311 | 312 | ZEND_BEGIN_ARG_INFO_EX(arginfo_maxminddbreader_void, 0, 0, 0) 313 | ZEND_END_ARG_INFO() 314 | 315 | PHP_METHOD(MaxMind_Db_Reader, metadata) { 316 | zval *this_zval = NULL; 317 | 318 | if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC, 319 | getThis(), 320 | "O", 321 | &this_zval, 322 | maxminddb_ce) == FAILURE) { 323 | return; 324 | } 325 | 326 | const maxminddb_obj *const mmdb_obj = 327 | (maxminddb_obj *)Z_MAXMINDDB_P(this_zval); 328 | 329 | if (NULL == mmdb_obj->mmdb) { 330 | zend_throw_exception_ex(spl_ce_BadMethodCallException, 331 | 0 TSRMLS_CC, 332 | "Attempt to read from a closed MaxMind DB."); 333 | return; 334 | } 335 | 336 | object_init_ex(return_value, metadata_ce); 337 | 338 | MMDB_entry_data_list_s *entry_data_list; 339 | int status = 340 | MMDB_get_metadata_as_entry_data_list(mmdb_obj->mmdb, &entry_data_list); 341 | if (status != MMDB_SUCCESS) { 342 | zend_throw_exception_ex(maxminddb_exception_ce, 343 | 0 TSRMLS_CC, 344 | "Error while decoding metadata. %s", 345 | MMDB_strerror(status)); 346 | return; 347 | } 348 | 349 | zval metadata_array; 350 | const MMDB_entry_data_list_s *rv = 351 | handle_entry_data_list(entry_data_list, &metadata_array TSRMLS_CC); 352 | if (rv == NULL) { 353 | return; 354 | } 355 | MMDB_free_entry_data_list(entry_data_list); 356 | zend_call_method_with_1_params(PROP_OBJ(return_value), 357 | metadata_ce, 358 | &metadata_ce->constructor, 359 | ZEND_CONSTRUCTOR_FUNC_NAME, 360 | NULL, 361 | &metadata_array); 362 | zval_ptr_dtor(&metadata_array); 363 | } 364 | 365 | PHP_METHOD(MaxMind_Db_Reader, close) { 366 | zval *this_zval = NULL; 367 | 368 | if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC, 369 | getThis(), 370 | "O", 371 | &this_zval, 372 | maxminddb_ce) == FAILURE) { 373 | return; 374 | } 375 | 376 | maxminddb_obj *mmdb_obj = (maxminddb_obj *)Z_MAXMINDDB_P(this_zval); 377 | 378 | if (NULL == mmdb_obj->mmdb) { 379 | zend_throw_exception_ex(spl_ce_BadMethodCallException, 380 | 0 TSRMLS_CC, 381 | "Attempt to close a closed MaxMind DB."); 382 | return; 383 | } 384 | MMDB_close(mmdb_obj->mmdb); 385 | efree(mmdb_obj->mmdb); 386 | mmdb_obj->mmdb = NULL; 387 | } 388 | 389 | static const MMDB_entry_data_list_s * 390 | handle_entry_data_list(const MMDB_entry_data_list_s *entry_data_list, 391 | zval *z_value TSRMLS_DC) { 392 | switch (entry_data_list->entry_data.type) { 393 | case MMDB_DATA_TYPE_MAP: 394 | return handle_map(entry_data_list, z_value TSRMLS_CC); 395 | case MMDB_DATA_TYPE_ARRAY: 396 | return handle_array(entry_data_list, z_value TSRMLS_CC); 397 | case MMDB_DATA_TYPE_UTF8_STRING: 398 | ZVAL_STRINGL(z_value, 399 | entry_data_list->entry_data.utf8_string, 400 | entry_data_list->entry_data.data_size); 401 | break; 402 | case MMDB_DATA_TYPE_BYTES: 403 | ZVAL_STRINGL(z_value, 404 | (char const *)entry_data_list->entry_data.bytes, 405 | entry_data_list->entry_data.data_size); 406 | break; 407 | case MMDB_DATA_TYPE_DOUBLE: 408 | ZVAL_DOUBLE(z_value, entry_data_list->entry_data.double_value); 409 | break; 410 | case MMDB_DATA_TYPE_FLOAT: 411 | ZVAL_DOUBLE(z_value, entry_data_list->entry_data.float_value); 412 | break; 413 | case MMDB_DATA_TYPE_UINT16: 414 | ZVAL_LONG(z_value, entry_data_list->entry_data.uint16); 415 | break; 416 | case MMDB_DATA_TYPE_UINT32: 417 | handle_uint32(entry_data_list, z_value TSRMLS_CC); 418 | break; 419 | case MMDB_DATA_TYPE_BOOLEAN: 420 | ZVAL_BOOL(z_value, entry_data_list->entry_data.boolean); 421 | break; 422 | case MMDB_DATA_TYPE_UINT64: 423 | handle_uint64(entry_data_list, z_value TSRMLS_CC); 424 | break; 425 | case MMDB_DATA_TYPE_UINT128: 426 | handle_uint128(entry_data_list, z_value TSRMLS_CC); 427 | break; 428 | case MMDB_DATA_TYPE_INT32: 429 | ZVAL_LONG(z_value, entry_data_list->entry_data.int32); 430 | break; 431 | default: 432 | zend_throw_exception_ex(maxminddb_exception_ce, 433 | 0 TSRMLS_CC, 434 | "Invalid data type arguments: %d", 435 | entry_data_list->entry_data.type); 436 | return NULL; 437 | } 438 | return entry_data_list; 439 | } 440 | 441 | static const MMDB_entry_data_list_s * 442 | handle_map(const MMDB_entry_data_list_s *entry_data_list, 443 | zval *z_value TSRMLS_DC) { 444 | array_init(z_value); 445 | const uint32_t map_size = entry_data_list->entry_data.data_size; 446 | 447 | uint32_t i; 448 | for (i = 0; i < map_size && entry_data_list; i++) { 449 | entry_data_list = entry_data_list->next; 450 | 451 | char *key = estrndup(entry_data_list->entry_data.utf8_string, 452 | entry_data_list->entry_data.data_size); 453 | if (NULL == key) { 454 | zend_throw_exception_ex(maxminddb_exception_ce, 455 | 0 TSRMLS_CC, 456 | "Invalid data type arguments"); 457 | return NULL; 458 | } 459 | 460 | entry_data_list = entry_data_list->next; 461 | zval new_value; 462 | entry_data_list = 463 | handle_entry_data_list(entry_data_list, &new_value TSRMLS_CC); 464 | if (entry_data_list != NULL) { 465 | add_assoc_zval(z_value, key, &new_value); 466 | } 467 | efree(key); 468 | } 469 | return entry_data_list; 470 | } 471 | 472 | static const MMDB_entry_data_list_s * 473 | handle_array(const MMDB_entry_data_list_s *entry_data_list, 474 | zval *z_value TSRMLS_DC) { 475 | const uint32_t size = entry_data_list->entry_data.data_size; 476 | 477 | array_init(z_value); 478 | 479 | uint32_t i; 480 | for (i = 0; i < size && entry_data_list; i++) { 481 | entry_data_list = entry_data_list->next; 482 | zval new_value; 483 | entry_data_list = 484 | handle_entry_data_list(entry_data_list, &new_value TSRMLS_CC); 485 | if (entry_data_list != NULL) { 486 | add_next_index_zval(z_value, &new_value); 487 | } 488 | } 489 | return entry_data_list; 490 | } 491 | 492 | static void handle_uint128(const MMDB_entry_data_list_s *entry_data_list, 493 | zval *z_value TSRMLS_DC) { 494 | uint64_t high = 0; 495 | uint64_t low = 0; 496 | #if MMDB_UINT128_IS_BYTE_ARRAY 497 | int i; 498 | for (i = 0; i < 8; i++) { 499 | high = (high << 8) | entry_data_list->entry_data.uint128[i]; 500 | } 501 | 502 | for (i = 8; i < 16; i++) { 503 | low = (low << 8) | entry_data_list->entry_data.uint128[i]; 504 | } 505 | #else 506 | high = entry_data_list->entry_data.uint128 >> 64; 507 | low = (uint64_t)entry_data_list->entry_data.uint128; 508 | #endif 509 | 510 | char *num_str; 511 | spprintf(&num_str, 0, "0x%016" PRIX64 "%016" PRIX64, high, low); 512 | CHECK_ALLOCATED(num_str); 513 | 514 | ZVAL_STRING(z_value, num_str); 515 | efree(num_str); 516 | } 517 | 518 | static void handle_uint32(const MMDB_entry_data_list_s *entry_data_list, 519 | zval *z_value TSRMLS_DC) { 520 | uint32_t val = entry_data_list->entry_data.uint32; 521 | 522 | #if LONG_MAX >= UINT32_MAX 523 | ZVAL_LONG(z_value, val); 524 | return; 525 | #else 526 | if (val <= LONG_MAX) { 527 | ZVAL_LONG(z_value, val); 528 | return; 529 | } 530 | 531 | char *int_str; 532 | spprintf(&int_str, 0, "%" PRIu32, val); 533 | CHECK_ALLOCATED(int_str); 534 | 535 | ZVAL_STRING(z_value, int_str); 536 | efree(int_str); 537 | #endif 538 | } 539 | 540 | static void handle_uint64(const MMDB_entry_data_list_s *entry_data_list, 541 | zval *z_value TSRMLS_DC) { 542 | uint64_t val = entry_data_list->entry_data.uint64; 543 | 544 | #if LONG_MAX >= UINT64_MAX 545 | ZVAL_LONG(z_value, val); 546 | return; 547 | #else 548 | if (val <= LONG_MAX) { 549 | ZVAL_LONG(z_value, val); 550 | return; 551 | } 552 | 553 | char *int_str; 554 | spprintf(&int_str, 0, "%" PRIu64, val); 555 | CHECK_ALLOCATED(int_str); 556 | 557 | ZVAL_STRING(z_value, int_str); 558 | efree(int_str); 559 | #endif 560 | } 561 | 562 | static void maxminddb_free_storage(free_obj_t *object TSRMLS_DC) { 563 | maxminddb_obj *obj = 564 | php_maxminddb_fetch_object((zend_object *)object TSRMLS_CC); 565 | if (obj->mmdb != NULL) { 566 | MMDB_close(obj->mmdb); 567 | efree(obj->mmdb); 568 | } 569 | 570 | zend_object_std_dtor(&obj->std TSRMLS_CC); 571 | } 572 | 573 | static zend_object *maxminddb_create_handler(zend_class_entry *type TSRMLS_DC) { 574 | maxminddb_obj *obj = (maxminddb_obj *)ecalloc(1, sizeof(maxminddb_obj)); 575 | zend_object_std_init(&obj->std, type TSRMLS_CC); 576 | object_properties_init(&(obj->std), type); 577 | 578 | obj->std.handlers = &maxminddb_obj_handlers; 579 | 580 | return &obj->std; 581 | } 582 | 583 | /* clang-format off */ 584 | static zend_function_entry maxminddb_methods[] = { 585 | PHP_ME(MaxMind_Db_Reader, __construct, arginfo_maxminddbreader_construct, 586 | ZEND_ACC_PUBLIC | ZEND_ACC_CTOR) 587 | PHP_ME(MaxMind_Db_Reader, close, arginfo_maxminddbreader_void, ZEND_ACC_PUBLIC) 588 | PHP_ME(MaxMind_Db_Reader, get, arginfo_maxminddbreader_get, ZEND_ACC_PUBLIC) 589 | PHP_ME(MaxMind_Db_Reader, getWithPrefixLen, arginfo_maxminddbreader_getWithPrefixLen, ZEND_ACC_PUBLIC) 590 | PHP_ME(MaxMind_Db_Reader, metadata, arginfo_maxminddbreader_void, ZEND_ACC_PUBLIC) 591 | { NULL, NULL, NULL } 592 | }; 593 | /* clang-format on */ 594 | 595 | ZEND_BEGIN_ARG_INFO_EX(arginfo_metadata_construct, 0, 0, 1) 596 | ZEND_ARG_TYPE_INFO(0, metadata, IS_ARRAY, 0) 597 | ZEND_END_ARG_INFO() 598 | 599 | PHP_METHOD(MaxMind_Db_Reader_Metadata, __construct) { 600 | zval *object = NULL; 601 | zval *metadata_array = NULL; 602 | zend_long node_count = 0; 603 | zend_long record_size = 0; 604 | 605 | if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC, 606 | getThis(), 607 | "Oa", 608 | &object, 609 | metadata_ce, 610 | &metadata_array) == FAILURE) { 611 | return; 612 | } 613 | 614 | zval *tmp = NULL; 615 | if ((tmp = zend_hash_str_find(HASH_OF(metadata_array), 616 | "binary_format_major_version", 617 | sizeof("binary_format_major_version") - 1))) { 618 | zend_update_property(metadata_ce, 619 | PROP_OBJ(object), 620 | "binaryFormatMajorVersion", 621 | sizeof("binaryFormatMajorVersion") - 1, 622 | tmp); 623 | } 624 | 625 | if ((tmp = zend_hash_str_find(HASH_OF(metadata_array), 626 | "binary_format_minor_version", 627 | sizeof("binary_format_minor_version") - 1))) { 628 | zend_update_property(metadata_ce, 629 | PROP_OBJ(object), 630 | "binaryFormatMinorVersion", 631 | sizeof("binaryFormatMinorVersion") - 1, 632 | tmp); 633 | } 634 | 635 | if ((tmp = zend_hash_str_find(HASH_OF(metadata_array), 636 | "build_epoch", 637 | sizeof("build_epoch") - 1))) { 638 | zend_update_property(metadata_ce, 639 | PROP_OBJ(object), 640 | "buildEpoch", 641 | sizeof("buildEpoch") - 1, 642 | tmp); 643 | } 644 | 645 | if ((tmp = zend_hash_str_find(HASH_OF(metadata_array), 646 | "database_type", 647 | sizeof("database_type") - 1))) { 648 | zend_update_property(metadata_ce, 649 | PROP_OBJ(object), 650 | "databaseType", 651 | sizeof("databaseType") - 1, 652 | tmp); 653 | } 654 | 655 | if ((tmp = zend_hash_str_find(HASH_OF(metadata_array), 656 | "description", 657 | sizeof("description") - 1))) { 658 | zend_update_property(metadata_ce, 659 | PROP_OBJ(object), 660 | "description", 661 | sizeof("description") - 1, 662 | tmp); 663 | } 664 | 665 | if ((tmp = zend_hash_str_find(HASH_OF(metadata_array), 666 | "ip_version", 667 | sizeof("ip_version") - 1))) { 668 | zend_update_property(metadata_ce, 669 | PROP_OBJ(object), 670 | "ipVersion", 671 | sizeof("ipVersion") - 1, 672 | tmp); 673 | } 674 | 675 | if ((tmp = zend_hash_str_find( 676 | HASH_OF(metadata_array), "languages", sizeof("languages") - 1))) { 677 | zend_update_property(metadata_ce, 678 | PROP_OBJ(object), 679 | "languages", 680 | sizeof("languages") - 1, 681 | tmp); 682 | } 683 | 684 | if ((tmp = zend_hash_str_find(HASH_OF(metadata_array), 685 | "record_size", 686 | sizeof("record_size") - 1))) { 687 | zend_update_property(metadata_ce, 688 | PROP_OBJ(object), 689 | "recordSize", 690 | sizeof("recordSize") - 1, 691 | tmp); 692 | if (Z_TYPE_P(tmp) == IS_LONG) { 693 | record_size = Z_LVAL_P(tmp); 694 | } 695 | } 696 | 697 | if (record_size != 0) { 698 | zend_update_property_long(metadata_ce, 699 | PROP_OBJ(object), 700 | "nodeByteSize", 701 | sizeof("nodeByteSize") - 1, 702 | record_size / 4); 703 | } 704 | 705 | if ((tmp = zend_hash_str_find(HASH_OF(metadata_array), 706 | "node_count", 707 | sizeof("node_count") - 1))) { 708 | zend_update_property(metadata_ce, 709 | PROP_OBJ(object), 710 | "nodeCount", 711 | sizeof("nodeCount") - 1, 712 | tmp); 713 | if (Z_TYPE_P(tmp) == IS_LONG) { 714 | node_count = Z_LVAL_P(tmp); 715 | } 716 | } 717 | 718 | if (record_size != 0) { 719 | zend_update_property_long(metadata_ce, 720 | PROP_OBJ(object), 721 | "searchTreeSize", 722 | sizeof("searchTreeSize") - 1, 723 | record_size * node_count / 4); 724 | } 725 | } 726 | 727 | // clang-format off 728 | static zend_function_entry metadata_methods[] = { 729 | PHP_ME(MaxMind_Db_Reader_Metadata, __construct, arginfo_metadata_construct, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR) 730 | {NULL, NULL, NULL} 731 | }; 732 | // clang-format on 733 | 734 | PHP_MINIT_FUNCTION(maxminddb) { 735 | zend_class_entry ce; 736 | 737 | INIT_CLASS_ENTRY(ce, PHP_MAXMINDDB_READER_EX_NS, NULL); 738 | maxminddb_exception_ce = 739 | zend_register_internal_class_ex(&ce, zend_ce_exception); 740 | 741 | INIT_CLASS_ENTRY(ce, PHP_MAXMINDDB_READER_NS, maxminddb_methods); 742 | maxminddb_ce = zend_register_internal_class(&ce TSRMLS_CC); 743 | maxminddb_ce->create_object = maxminddb_create_handler; 744 | 745 | INIT_CLASS_ENTRY(ce, PHP_MAXMINDDB_METADATA_NS, metadata_methods); 746 | metadata_ce = zend_register_internal_class(&ce TSRMLS_CC); 747 | zend_declare_property_null(metadata_ce, 748 | "binaryFormatMajorVersion", 749 | sizeof("binaryFormatMajorVersion") - 1, 750 | ZEND_ACC_PUBLIC); 751 | zend_declare_property_null(metadata_ce, 752 | "binaryFormatMinorVersion", 753 | sizeof("binaryFormatMinorVersion") - 1, 754 | ZEND_ACC_PUBLIC); 755 | zend_declare_property_null( 756 | metadata_ce, "buildEpoch", sizeof("buildEpoch") - 1, ZEND_ACC_PUBLIC); 757 | zend_declare_property_null(metadata_ce, 758 | "databaseType", 759 | sizeof("databaseType") - 1, 760 | ZEND_ACC_PUBLIC); 761 | zend_declare_property_null( 762 | metadata_ce, "description", sizeof("description") - 1, ZEND_ACC_PUBLIC); 763 | zend_declare_property_null( 764 | metadata_ce, "ipVersion", sizeof("ipVersion") - 1, ZEND_ACC_PUBLIC); 765 | zend_declare_property_null( 766 | metadata_ce, "languages", sizeof("languages") - 1, ZEND_ACC_PUBLIC); 767 | zend_declare_property_null(metadata_ce, 768 | "nodeByteSize", 769 | sizeof("nodeByteSize") - 1, 770 | ZEND_ACC_PUBLIC); 771 | zend_declare_property_null( 772 | metadata_ce, "nodeCount", sizeof("nodeCount") - 1, ZEND_ACC_PUBLIC); 773 | zend_declare_property_null( 774 | metadata_ce, "recordSize", sizeof("recordSize") - 1, ZEND_ACC_PUBLIC); 775 | zend_declare_property_null(metadata_ce, 776 | "searchTreeSize", 777 | sizeof("searchTreeSize") - 1, 778 | ZEND_ACC_PUBLIC); 779 | 780 | memcpy(&maxminddb_obj_handlers, 781 | zend_get_std_object_handlers(), 782 | sizeof(zend_object_handlers)); 783 | maxminddb_obj_handlers.clone_obj = NULL; 784 | maxminddb_obj_handlers.offset = XtOffsetOf(maxminddb_obj, std); 785 | maxminddb_obj_handlers.free_obj = maxminddb_free_storage; 786 | zend_declare_class_constant_string(maxminddb_ce, 787 | "MMDB_LIB_VERSION", 788 | sizeof("MMDB_LIB_VERSION") - 1, 789 | MMDB_lib_version() TSRMLS_CC); 790 | 791 | return SUCCESS; 792 | } 793 | 794 | static PHP_MINFO_FUNCTION(maxminddb) { 795 | php_info_print_table_start(); 796 | 797 | php_info_print_table_row(2, "MaxMind DB Reader", "enabled"); 798 | php_info_print_table_row( 799 | 2, "maxminddb extension version", PHP_MAXMINDDB_VERSION); 800 | php_info_print_table_row( 801 | 2, "libmaxminddb library version", MMDB_lib_version()); 802 | 803 | php_info_print_table_end(); 804 | } 805 | 806 | zend_module_entry maxminddb_module_entry = {STANDARD_MODULE_HEADER, 807 | PHP_MAXMINDDB_EXTNAME, 808 | NULL, 809 | PHP_MINIT(maxminddb), 810 | NULL, 811 | NULL, 812 | NULL, 813 | PHP_MINFO(maxminddb), 814 | PHP_MAXMINDDB_VERSION, 815 | STANDARD_MODULE_PROPERTIES}; 816 | 817 | #ifdef COMPILE_DL_MAXMINDDB 818 | ZEND_GET_MODULE(maxminddb) 819 | #endif 820 | -------------------------------------------------------------------------------- /ext/php_maxminddb.h: -------------------------------------------------------------------------------- 1 | /* MaxMind, Inc., licenses this file to you under the Apache License, Version 2 | * 2.0 (the "License"); you may not use this file except in compliance with 3 | * the License. You may obtain a copy of the License at 4 | * 5 | * http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | * License for the specific language governing permissions and limitations 11 | * under the License. 12 | */ 13 | 14 | #include 15 | 16 | #ifndef PHP_MAXMINDDB_H 17 | #define PHP_MAXMINDDB_H 1 18 | #define PHP_MAXMINDDB_VERSION "1.12.1" 19 | #define PHP_MAXMINDDB_EXTNAME "maxminddb" 20 | 21 | extern zend_module_entry maxminddb_module_entry; 22 | #define phpext_maxminddb_ptr &maxminddb_module_entry 23 | 24 | #endif 25 | -------------------------------------------------------------------------------- /ext/tests/001-load.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Check for maxminddb presence 3 | --SKIPIF-- 4 | 7 | --FILE-- 8 | 11 | --EXPECT-- 12 | maxminddb extension is available 13 | -------------------------------------------------------------------------------- /ext/tests/002-final.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Check that Reader class is not final 3 | --SKIPIF-- 4 | 7 | --FILE-- 8 | isFinal()); 11 | ?> 12 | --EXPECT-- 13 | bool(false) 14 | -------------------------------------------------------------------------------- /ext/tests/003-open-basedir.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | openbase_dir is followed 3 | --INI-- 4 | open_basedir=/--dne-- 5 | --FILE-- 6 | 11 | --EXPECTREGEX-- 12 | .*open_basedir restriction in effect.* 13 | -------------------------------------------------------------------------------- /package.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | maxminddb 8 | pecl.php.net 9 | Reader for the MaxMind DB file format 10 | This is the PHP extension for reading MaxMind DB files. MaxMind DB is a binary file format that stores data indexed by IP address subnets (IPv4 or IPv6). 11 | 12 | Greg Oschwald 13 | oschwald 14 | goschwald@maxmind.com 15 | yes 16 | 17 | 2025-05-05 18 | 19 | 1.12.1 20 | 1.12.1 21 | 22 | 23 | stable 24 | stable 25 | 26 | Apache License 2.0 27 | * The C extension now checks that the database metadata lookup was 28 | successful. 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 7.2.0 54 | 55 | 56 | 1.10.0 57 | 58 | 59 | 60 | maxminddb 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/MaxMind/Db/Reader.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | private static $METADATA_START_MARKER_LENGTH = 14; 32 | 33 | /** 34 | * @var int 35 | */ 36 | private static $METADATA_MAX_SIZE = 131072; // 128 * 1024 = 128KiB 37 | 38 | /** 39 | * @var Decoder 40 | */ 41 | private $decoder; 42 | 43 | /** 44 | * @var resource 45 | */ 46 | private $fileHandle; 47 | 48 | /** 49 | * @var int 50 | */ 51 | private $fileSize; 52 | 53 | /** 54 | * @var int 55 | */ 56 | private $ipV4Start; 57 | 58 | /** 59 | * @var Metadata 60 | */ 61 | private $metadata; 62 | 63 | /** 64 | * Constructs a Reader for the MaxMind DB format. The file passed to it must 65 | * be a valid MaxMind DB file such as a GeoIp2 database file. 66 | * 67 | * @param string $database the MaxMind DB file to use 68 | * 69 | * @throws \InvalidArgumentException for invalid database path or unknown arguments 70 | * @throws InvalidDatabaseException 71 | * if the database is invalid or there is an error reading 72 | * from it 73 | */ 74 | public function __construct(string $database) 75 | { 76 | if (\func_num_args() !== 1) { 77 | throw new \ArgumentCountError( 78 | \sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args()) 79 | ); 80 | } 81 | 82 | if (is_dir($database)) { 83 | // This matches the error that the C extension throws. 84 | throw new InvalidDatabaseException( 85 | "Error opening database file ($database). Is this a valid MaxMind DB file?" 86 | ); 87 | } 88 | 89 | $fileHandle = @fopen($database, 'rb'); 90 | if ($fileHandle === false) { 91 | throw new \InvalidArgumentException( 92 | "The file \"$database\" does not exist or is not readable." 93 | ); 94 | } 95 | $this->fileHandle = $fileHandle; 96 | 97 | $fstat = fstat($fileHandle); 98 | if ($fstat === false) { 99 | throw new \UnexpectedValueException( 100 | "Error determining the size of \"$database\"." 101 | ); 102 | } 103 | $this->fileSize = $fstat['size']; 104 | 105 | $start = $this->findMetadataStart($database); 106 | $metadataDecoder = new Decoder($this->fileHandle, $start); 107 | [$metadataArray] = $metadataDecoder->decode($start); 108 | $this->metadata = new Metadata($metadataArray); 109 | $this->decoder = new Decoder( 110 | $this->fileHandle, 111 | $this->metadata->searchTreeSize + self::$DATA_SECTION_SEPARATOR_SIZE 112 | ); 113 | $this->ipV4Start = $this->ipV4StartNode(); 114 | } 115 | 116 | /** 117 | * Retrieves the record for the IP address. 118 | * 119 | * @param string $ipAddress the IP address to look up 120 | * 121 | * @throws \BadMethodCallException if this method is called on a closed database 122 | * @throws \InvalidArgumentException if something other than a single IP address is passed to the method 123 | * @throws InvalidDatabaseException 124 | * if the database is invalid or there is an error reading 125 | * from it 126 | * 127 | * @return mixed the record for the IP address 128 | */ 129 | public function get(string $ipAddress) 130 | { 131 | if (\func_num_args() !== 1) { 132 | throw new \ArgumentCountError( 133 | \sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args()) 134 | ); 135 | } 136 | [$record] = $this->getWithPrefixLen($ipAddress); 137 | 138 | return $record; 139 | } 140 | 141 | /** 142 | * Retrieves the record for the IP address and its associated network prefix length. 143 | * 144 | * @param string $ipAddress the IP address to look up 145 | * 146 | * @throws \BadMethodCallException if this method is called on a closed database 147 | * @throws \InvalidArgumentException if something other than a single IP address is passed to the method 148 | * @throws InvalidDatabaseException 149 | * if the database is invalid or there is an error reading 150 | * from it 151 | * 152 | * @return array{0:mixed, 1:int} an array where the first element is the record and the 153 | * second the network prefix length for the record 154 | */ 155 | public function getWithPrefixLen(string $ipAddress): array 156 | { 157 | if (\func_num_args() !== 1) { 158 | throw new \ArgumentCountError( 159 | \sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args()) 160 | ); 161 | } 162 | 163 | if (!\is_resource($this->fileHandle)) { 164 | throw new \BadMethodCallException( 165 | 'Attempt to read from a closed MaxMind DB.' 166 | ); 167 | } 168 | 169 | [$pointer, $prefixLen] = $this->findAddressInTree($ipAddress); 170 | if ($pointer === 0) { 171 | return [null, $prefixLen]; 172 | } 173 | 174 | return [$this->resolveDataPointer($pointer), $prefixLen]; 175 | } 176 | 177 | /** 178 | * @return array{0:int, 1:int} 179 | */ 180 | private function findAddressInTree(string $ipAddress): array 181 | { 182 | $packedAddr = @inet_pton($ipAddress); 183 | if ($packedAddr === false) { 184 | throw new \InvalidArgumentException( 185 | "The value \"$ipAddress\" is not a valid IP address." 186 | ); 187 | } 188 | 189 | $rawAddress = unpack('C*', $packedAddr); 190 | if ($rawAddress === false) { 191 | throw new InvalidDatabaseException( 192 | 'Could not unpack the unsigned char of the packed in_addr representation.' 193 | ); 194 | } 195 | 196 | $bitCount = \count($rawAddress) * 8; 197 | 198 | // The first node of the tree is always node 0, at the beginning of the 199 | // value 200 | $node = 0; 201 | 202 | $metadata = $this->metadata; 203 | 204 | // Check if we are looking up an IPv4 address in an IPv6 tree. If this 205 | // is the case, we can skip over the first 96 nodes. 206 | if ($metadata->ipVersion === 6) { 207 | if ($bitCount === 32) { 208 | $node = $this->ipV4Start; 209 | } 210 | } elseif ($metadata->ipVersion === 4 && $bitCount === 128) { 211 | throw new \InvalidArgumentException( 212 | "Error looking up $ipAddress. You attempted to look up an" 213 | . ' IPv6 address in an IPv4-only database.' 214 | ); 215 | } 216 | 217 | $nodeCount = $metadata->nodeCount; 218 | 219 | for ($i = 0; $i < $bitCount && $node < $nodeCount; ++$i) { 220 | $tempBit = 0xFF & $rawAddress[($i >> 3) + 1]; 221 | $bit = 1 & ($tempBit >> 7 - ($i % 8)); 222 | 223 | $node = $this->readNode($node, $bit); 224 | } 225 | if ($node === $nodeCount) { 226 | // Record is empty 227 | return [0, $i]; 228 | } 229 | if ($node > $nodeCount) { 230 | // Record is a data pointer 231 | return [$node, $i]; 232 | } 233 | 234 | throw new InvalidDatabaseException( 235 | 'Invalid or corrupt database. Maximum search depth reached without finding a leaf node' 236 | ); 237 | } 238 | 239 | private function ipV4StartNode(): int 240 | { 241 | // If we have an IPv4 database, the start node is the first node 242 | if ($this->metadata->ipVersion === 4) { 243 | return 0; 244 | } 245 | 246 | $node = 0; 247 | 248 | for ($i = 0; $i < 96 && $node < $this->metadata->nodeCount; ++$i) { 249 | $node = $this->readNode($node, 0); 250 | } 251 | 252 | return $node; 253 | } 254 | 255 | private function readNode(int $nodeNumber, int $index): int 256 | { 257 | $baseOffset = $nodeNumber * $this->metadata->nodeByteSize; 258 | 259 | switch ($this->metadata->recordSize) { 260 | case 24: 261 | $bytes = Util::read($this->fileHandle, $baseOffset + $index * 3, 3); 262 | $rc = unpack('N', "\x00" . $bytes); 263 | if ($rc === false) { 264 | throw new InvalidDatabaseException( 265 | 'Could not unpack the unsigned long of the node.' 266 | ); 267 | } 268 | [, $node] = $rc; 269 | 270 | return $node; 271 | 272 | case 28: 273 | $bytes = Util::read($this->fileHandle, $baseOffset + 3 * $index, 4); 274 | if ($index === 0) { 275 | $middle = (0xF0 & \ord($bytes[3])) >> 4; 276 | } else { 277 | $middle = 0x0F & \ord($bytes[0]); 278 | } 279 | $rc = unpack('N', \chr($middle) . substr($bytes, $index, 3)); 280 | if ($rc === false) { 281 | throw new InvalidDatabaseException( 282 | 'Could not unpack the unsigned long of the node.' 283 | ); 284 | } 285 | [, $node] = $rc; 286 | 287 | return $node; 288 | 289 | case 32: 290 | $bytes = Util::read($this->fileHandle, $baseOffset + $index * 4, 4); 291 | $rc = unpack('N', $bytes); 292 | if ($rc === false) { 293 | throw new InvalidDatabaseException( 294 | 'Could not unpack the unsigned long of the node.' 295 | ); 296 | } 297 | [, $node] = $rc; 298 | 299 | return $node; 300 | 301 | default: 302 | throw new InvalidDatabaseException( 303 | 'Unknown record size: ' 304 | . $this->metadata->recordSize 305 | ); 306 | } 307 | } 308 | 309 | /** 310 | * @return mixed 311 | */ 312 | private function resolveDataPointer(int $pointer) 313 | { 314 | $resolved = $pointer - $this->metadata->nodeCount 315 | + $this->metadata->searchTreeSize; 316 | if ($resolved >= $this->fileSize) { 317 | throw new InvalidDatabaseException( 318 | "The MaxMind DB file's search tree is corrupt" 319 | ); 320 | } 321 | 322 | [$data] = $this->decoder->decode($resolved); 323 | 324 | return $data; 325 | } 326 | 327 | /* 328 | * This is an extremely naive but reasonably readable implementation. There 329 | * are much faster algorithms (e.g., Boyer-Moore) for this if speed is ever 330 | * an issue, but I suspect it won't be. 331 | */ 332 | private function findMetadataStart(string $filename): int 333 | { 334 | $handle = $this->fileHandle; 335 | $fileSize = $this->fileSize; 336 | $marker = self::$METADATA_START_MARKER; 337 | $markerLength = self::$METADATA_START_MARKER_LENGTH; 338 | 339 | $minStart = $fileSize - min(self::$METADATA_MAX_SIZE, $fileSize); 340 | 341 | for ($offset = $fileSize - $markerLength; $offset >= $minStart; --$offset) { 342 | if (fseek($handle, $offset) !== 0) { 343 | break; 344 | } 345 | 346 | $value = fread($handle, $markerLength); 347 | if ($value === $marker) { 348 | return $offset + $markerLength; 349 | } 350 | } 351 | 352 | throw new InvalidDatabaseException( 353 | "Error opening database file ($filename). " 354 | . 'Is this a valid MaxMind DB file?' 355 | ); 356 | } 357 | 358 | /** 359 | * @throws \InvalidArgumentException if arguments are passed to the method 360 | * @throws \BadMethodCallException if the database has been closed 361 | * 362 | * @return Metadata object for the database 363 | */ 364 | public function metadata(): Metadata 365 | { 366 | if (\func_num_args()) { 367 | throw new \ArgumentCountError( 368 | \sprintf('%s() expects exactly 0 parameters, %d given', __METHOD__, \func_num_args()) 369 | ); 370 | } 371 | 372 | // Not technically required, but this makes it consistent with 373 | // C extension and it allows us to change our implementation later. 374 | if (!\is_resource($this->fileHandle)) { 375 | throw new \BadMethodCallException( 376 | 'Attempt to read from a closed MaxMind DB.' 377 | ); 378 | } 379 | 380 | return clone $this->metadata; 381 | } 382 | 383 | /** 384 | * Closes the MaxMind DB and returns resources to the system. 385 | * 386 | * @throws \Exception 387 | * if an I/O error occurs 388 | */ 389 | public function close(): void 390 | { 391 | if (\func_num_args()) { 392 | throw new \ArgumentCountError( 393 | \sprintf('%s() expects exactly 0 parameters, %d given', __METHOD__, \func_num_args()) 394 | ); 395 | } 396 | 397 | if (!\is_resource($this->fileHandle)) { 398 | throw new \BadMethodCallException( 399 | 'Attempt to close a closed MaxMind DB.' 400 | ); 401 | } 402 | fclose($this->fileHandle); 403 | } 404 | } 405 | -------------------------------------------------------------------------------- /src/MaxMind/Db/Reader/Decoder.php: -------------------------------------------------------------------------------- 1 | fileStream = $fileStream; 59 | $this->pointerBase = $pointerBase; 60 | 61 | $this->pointerTestHack = $pointerTestHack; 62 | 63 | $this->switchByteOrder = $this->isPlatformLittleEndian(); 64 | } 65 | 66 | /** 67 | * @return array 68 | */ 69 | public function decode(int $offset): array 70 | { 71 | $ctrlByte = \ord(Util::read($this->fileStream, $offset, 1)); 72 | ++$offset; 73 | 74 | $type = $ctrlByte >> 5; 75 | 76 | // Pointers are a special case, we don't read the next $size bytes, we 77 | // use the size to determine the length of the pointer and then follow 78 | // it. 79 | if ($type === self::_POINTER) { 80 | [$pointer, $offset] = $this->decodePointer($ctrlByte, $offset); 81 | 82 | // for unit testing 83 | if ($this->pointerTestHack) { 84 | return [$pointer]; 85 | } 86 | 87 | [$result] = $this->decode($pointer); 88 | 89 | return [$result, $offset]; 90 | } 91 | 92 | if ($type === self::_EXTENDED) { 93 | $nextByte = \ord(Util::read($this->fileStream, $offset, 1)); 94 | 95 | $type = $nextByte + 7; 96 | 97 | if ($type < 8) { 98 | throw new InvalidDatabaseException( 99 | 'Something went horribly wrong in the decoder. An extended type ' 100 | . 'resolved to a type number < 8 (' 101 | . $type 102 | . ')' 103 | ); 104 | } 105 | 106 | ++$offset; 107 | } 108 | 109 | [$size, $offset] = $this->sizeFromCtrlByte($ctrlByte, $offset); 110 | 111 | return $this->decodeByType($type, $offset, $size); 112 | } 113 | 114 | /** 115 | * @param int<0, max> $size 116 | * 117 | * @return array{0:mixed, 1:int} 118 | */ 119 | private function decodeByType(int $type, int $offset, int $size): array 120 | { 121 | switch ($type) { 122 | case self::_MAP: 123 | return $this->decodeMap($size, $offset); 124 | 125 | case self::_ARRAY: 126 | return $this->decodeArray($size, $offset); 127 | 128 | case self::_BOOLEAN: 129 | return [$this->decodeBoolean($size), $offset]; 130 | } 131 | 132 | $newOffset = $offset + $size; 133 | $bytes = Util::read($this->fileStream, $offset, $size); 134 | 135 | switch ($type) { 136 | case self::_BYTES: 137 | case self::_UTF8_STRING: 138 | return [$bytes, $newOffset]; 139 | 140 | case self::_DOUBLE: 141 | $this->verifySize(8, $size); 142 | 143 | return [$this->decodeDouble($bytes), $newOffset]; 144 | 145 | case self::_FLOAT: 146 | $this->verifySize(4, $size); 147 | 148 | return [$this->decodeFloat($bytes), $newOffset]; 149 | 150 | case self::_INT32: 151 | return [$this->decodeInt32($bytes, $size), $newOffset]; 152 | 153 | case self::_UINT16: 154 | case self::_UINT32: 155 | case self::_UINT64: 156 | case self::_UINT128: 157 | return [$this->decodeUint($bytes, $size), $newOffset]; 158 | 159 | default: 160 | throw new InvalidDatabaseException( 161 | 'Unknown or unexpected type: ' . $type 162 | ); 163 | } 164 | } 165 | 166 | private function verifySize(int $expected, int $actual): void 167 | { 168 | if ($expected !== $actual) { 169 | throw new InvalidDatabaseException( 170 | "The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)" 171 | ); 172 | } 173 | } 174 | 175 | /** 176 | * @return array{0:array, 1:int} 177 | */ 178 | private function decodeArray(int $size, int $offset): array 179 | { 180 | $array = []; 181 | 182 | for ($i = 0; $i < $size; ++$i) { 183 | [$value, $offset] = $this->decode($offset); 184 | $array[] = $value; 185 | } 186 | 187 | return [$array, $offset]; 188 | } 189 | 190 | private function decodeBoolean(int $size): bool 191 | { 192 | return $size !== 0; 193 | } 194 | 195 | private function decodeDouble(string $bytes): float 196 | { 197 | // This assumes IEEE 754 doubles, but most (all?) modern platforms 198 | // use them. 199 | $rc = unpack('E', $bytes); 200 | if ($rc === false) { 201 | throw new InvalidDatabaseException( 202 | 'Could not unpack a double value from the given bytes.' 203 | ); 204 | } 205 | [, $double] = $rc; 206 | 207 | return $double; 208 | } 209 | 210 | private function decodeFloat(string $bytes): float 211 | { 212 | // This assumes IEEE 754 floats, but most (all?) modern platforms 213 | // use them. 214 | $rc = unpack('G', $bytes); 215 | if ($rc === false) { 216 | throw new InvalidDatabaseException( 217 | 'Could not unpack a float value from the given bytes.' 218 | ); 219 | } 220 | [, $float] = $rc; 221 | 222 | return $float; 223 | } 224 | 225 | private function decodeInt32(string $bytes, int $size): int 226 | { 227 | switch ($size) { 228 | case 0: 229 | return 0; 230 | 231 | case 1: 232 | case 2: 233 | case 3: 234 | $bytes = str_pad($bytes, 4, "\x00", \STR_PAD_LEFT); 235 | 236 | break; 237 | 238 | case 4: 239 | break; 240 | 241 | default: 242 | throw new InvalidDatabaseException( 243 | "The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)" 244 | ); 245 | } 246 | 247 | $rc = unpack('l', $this->maybeSwitchByteOrder($bytes)); 248 | if ($rc === false) { 249 | throw new InvalidDatabaseException( 250 | 'Could not unpack a 32bit integer value from the given bytes.' 251 | ); 252 | } 253 | [, $int] = $rc; 254 | 255 | return $int; 256 | } 257 | 258 | /** 259 | * @return array{0:array, 1:int} 260 | */ 261 | private function decodeMap(int $size, int $offset): array 262 | { 263 | $map = []; 264 | 265 | for ($i = 0; $i < $size; ++$i) { 266 | [$key, $offset] = $this->decode($offset); 267 | [$value, $offset] = $this->decode($offset); 268 | $map[$key] = $value; 269 | } 270 | 271 | return [$map, $offset]; 272 | } 273 | 274 | /** 275 | * @return array{0:int, 1:int} 276 | */ 277 | private function decodePointer(int $ctrlByte, int $offset): array 278 | { 279 | $pointerSize = (($ctrlByte >> 3) & 0x3) + 1; 280 | 281 | $buffer = Util::read($this->fileStream, $offset, $pointerSize); 282 | $offset += $pointerSize; 283 | 284 | switch ($pointerSize) { 285 | case 1: 286 | $packed = \chr($ctrlByte & 0x7) . $buffer; 287 | $rc = unpack('n', $packed); 288 | if ($rc === false) { 289 | throw new InvalidDatabaseException( 290 | 'Could not unpack an unsigned short value from the given bytes (pointerSize is 1).' 291 | ); 292 | } 293 | [, $pointer] = $rc; 294 | $pointer += $this->pointerBase; 295 | 296 | break; 297 | 298 | case 2: 299 | $packed = "\x00" . \chr($ctrlByte & 0x7) . $buffer; 300 | $rc = unpack('N', $packed); 301 | if ($rc === false) { 302 | throw new InvalidDatabaseException( 303 | 'Could not unpack an unsigned long value from the given bytes (pointerSize is 2).' 304 | ); 305 | } 306 | [, $pointer] = $rc; 307 | $pointer += $this->pointerBase + 2048; 308 | 309 | break; 310 | 311 | case 3: 312 | $packed = \chr($ctrlByte & 0x7) . $buffer; 313 | 314 | // It is safe to use 'N' here, even on 32 bit machines as the 315 | // first bit is 0. 316 | $rc = unpack('N', $packed); 317 | if ($rc === false) { 318 | throw new InvalidDatabaseException( 319 | 'Could not unpack an unsigned long value from the given bytes (pointerSize is 3).' 320 | ); 321 | } 322 | [, $pointer] = $rc; 323 | $pointer += $this->pointerBase + 526336; 324 | 325 | break; 326 | 327 | case 4: 328 | // We cannot use unpack here as we might overflow on 32 bit 329 | // machines 330 | $pointerOffset = $this->decodeUint($buffer, $pointerSize); 331 | 332 | $pointerBase = $this->pointerBase; 333 | 334 | if (\PHP_INT_MAX - $pointerBase >= $pointerOffset) { 335 | $pointer = $pointerOffset + $pointerBase; 336 | } else { 337 | throw new \RuntimeException( 338 | 'The database offset is too large to be represented on your platform.' 339 | ); 340 | } 341 | 342 | break; 343 | 344 | default: 345 | throw new InvalidDatabaseException( 346 | 'Unexpected pointer size ' . $pointerSize 347 | ); 348 | } 349 | 350 | return [$pointer, $offset]; 351 | } 352 | 353 | // @phpstan-ignore-next-line 354 | private function decodeUint(string $bytes, int $byteLength) 355 | { 356 | if ($byteLength === 0) { 357 | return 0; 358 | } 359 | 360 | // PHP integers are signed. PHP_INT_SIZE - 1 is the number of 361 | // complete bytes that can be converted to an integer. However, 362 | // we can convert another byte if the leading bit is zero. 363 | $useRealInts = $byteLength <= \PHP_INT_SIZE - 1 364 | || ($byteLength === \PHP_INT_SIZE && (\ord($bytes[0]) & 0x80) === 0); 365 | 366 | if ($useRealInts) { 367 | $integer = 0; 368 | for ($i = 0; $i < $byteLength; ++$i) { 369 | $part = \ord($bytes[$i]); 370 | $integer = ($integer << 8) + $part; 371 | } 372 | 373 | return $integer; 374 | } 375 | 376 | // We only use gmp or bcmath if the final value is too big 377 | $integerAsString = '0'; 378 | for ($i = 0; $i < $byteLength; ++$i) { 379 | $part = \ord($bytes[$i]); 380 | 381 | if (\extension_loaded('gmp')) { 382 | $integerAsString = gmp_strval(gmp_add(gmp_mul($integerAsString, '256'), $part)); 383 | } elseif (\extension_loaded('bcmath')) { 384 | $integerAsString = bcadd(bcmul($integerAsString, '256'), (string) $part); 385 | } else { 386 | throw new \RuntimeException( 387 | 'The gmp or bcmath extension must be installed to read this database.' 388 | ); 389 | } 390 | } 391 | 392 | return $integerAsString; 393 | } 394 | 395 | /** 396 | * @return array{0:int, 1:int} 397 | */ 398 | private function sizeFromCtrlByte(int $ctrlByte, int $offset): array 399 | { 400 | $size = $ctrlByte & 0x1F; 401 | 402 | if ($size < 29) { 403 | return [$size, $offset]; 404 | } 405 | 406 | $bytesToRead = $size - 28; 407 | $bytes = Util::read($this->fileStream, $offset, $bytesToRead); 408 | 409 | if ($size === 29) { 410 | $size = 29 + \ord($bytes); 411 | } elseif ($size === 30) { 412 | $rc = unpack('n', $bytes); 413 | if ($rc === false) { 414 | throw new InvalidDatabaseException( 415 | 'Could not unpack an unsigned short value from the given bytes.' 416 | ); 417 | } 418 | [, $adjust] = $rc; 419 | $size = 285 + $adjust; 420 | } else { 421 | $rc = unpack('N', "\x00" . $bytes); 422 | if ($rc === false) { 423 | throw new InvalidDatabaseException( 424 | 'Could not unpack an unsigned long value from the given bytes.' 425 | ); 426 | } 427 | [, $adjust] = $rc; 428 | $size = $adjust + 65821; 429 | } 430 | 431 | return [$size, $offset + $bytesToRead]; 432 | } 433 | 434 | private function maybeSwitchByteOrder(string $bytes): string 435 | { 436 | return $this->switchByteOrder ? strrev($bytes) : $bytes; 437 | } 438 | 439 | private function isPlatformLittleEndian(): bool 440 | { 441 | $testint = 0x00FF; 442 | $packed = pack('S', $testint); 443 | $rc = unpack('v', $packed); 444 | if ($rc === false) { 445 | throw new InvalidDatabaseException( 446 | 'Could not unpack an unsigned short value from the given bytes.' 447 | ); 448 | } 449 | 450 | return $testint === current($rc); 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /src/MaxMind/Db/Reader/InvalidDatabaseException.php: -------------------------------------------------------------------------------- 1 | 52 | */ 53 | public $description; 54 | 55 | /** 56 | * This is an unsigned 16-bit integer which is always 4 or 6. It indicates 57 | * whether the database contains IPv4 or IPv6 address data. 58 | * 59 | * @var int 60 | */ 61 | public $ipVersion; 62 | 63 | /** 64 | * An array of strings, each of which is a language code. A given record 65 | * may contain data items that have been localized to some or all of 66 | * these languages. This may be undefined. 67 | * 68 | * @var array 69 | */ 70 | public $languages; 71 | 72 | /** 73 | * @var int 74 | */ 75 | public $nodeByteSize; 76 | 77 | /** 78 | * This is an unsigned 32-bit integer indicating the number of nodes in 79 | * the search tree. 80 | * 81 | * @var int 82 | */ 83 | public $nodeCount; 84 | 85 | /** 86 | * This is an unsigned 16-bit integer. It indicates the number of bits in a 87 | * record in the search tree. Note that each node consists of two records. 88 | * 89 | * @var int 90 | */ 91 | public $recordSize; 92 | 93 | /** 94 | * @var int 95 | */ 96 | public $searchTreeSize; 97 | 98 | /** 99 | * @param array $metadata 100 | */ 101 | public function __construct(array $metadata) 102 | { 103 | if (\func_num_args() !== 1) { 104 | throw new \ArgumentCountError( 105 | \sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args()) 106 | ); 107 | } 108 | 109 | $this->binaryFormatMajorVersion 110 | = $metadata['binary_format_major_version']; 111 | $this->binaryFormatMinorVersion 112 | = $metadata['binary_format_minor_version']; 113 | $this->buildEpoch = $metadata['build_epoch']; 114 | $this->databaseType = $metadata['database_type']; 115 | $this->languages = $metadata['languages']; 116 | $this->description = $metadata['description']; 117 | $this->ipVersion = $metadata['ip_version']; 118 | $this->nodeCount = $metadata['node_count']; 119 | $this->recordSize = $metadata['record_size']; 120 | $this->nodeByteSize = $this->recordSize / 4; 121 | $this->searchTreeSize = $this->nodeCount * $this->nodeByteSize; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/MaxMind/Db/Reader/Util.php: -------------------------------------------------------------------------------- 1 | $numberOfBytes 12 | */ 13 | public static function read($stream, int $offset, int $numberOfBytes): string 14 | { 15 | if ($numberOfBytes === 0) { 16 | return ''; 17 | } 18 | if (fseek($stream, $offset) === 0) { 19 | $value = fread($stream, $numberOfBytes); 20 | 21 | // We check that the number of bytes read is equal to the number 22 | // asked for. We use ftell as getting the length of $value is 23 | // much slower. 24 | if ($value !== false && ftell($stream) - $offset === $numberOfBytes) { 25 | return $value; 26 | } 27 | } 28 | 29 | throw new InvalidDatabaseException( 30 | 'The MaxMind DB file contains bad data' 31 | ); 32 | } 33 | } 34 | --------------------------------------------------------------------------------