├── .eslintrc.js ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .travis.yml ├── AUTHORS ├── LICENSE ├── README.md ├── data ├── city.checksum ├── country.checksum ├── geoip-city-names.dat ├── geoip-city.dat ├── geoip-city6.dat ├── geoip-country.dat └── geoip-country6.dat ├── lib ├── fsWatcher.js ├── geoip.js └── utils.js ├── package.json ├── scripts └── updatedb.js └── test ├── geo-lookup.js ├── memory_usage.js └── tests.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "node": true, 4 | "es6": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "ecmaVersion": 2018 9 | }, 10 | "rules": { 11 | "indent": [ 12 | "error", 13 | "tab" 14 | ], 15 | "linebreak-style": [ 16 | "error", 17 | "unix" 18 | ], 19 | "no-console": "off", 20 | "no-constant-condition": "off", 21 | "no-control-regex": "off", 22 | "quotes": "off", 23 | "semi": [ 24 | "error", 25 | "always" 26 | ] 27 | } 28 | }; -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Get Node.js 13 | uses: actions/setup-node@v4 14 | with: 15 | node-version: '>=20.x' 16 | check-latest: true 17 | - run: npm install 18 | - run: npm run updatedb license_key=${{ secrets.MAXMIND_LICENSE_KEY }} 19 | - run: npm test 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /tmp 3 | npm-debug.log 4 | 5 | # JetBrains IDE project files 6 | /.idea/ 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '8' 4 | sudo: false 5 | branches: 6 | except: 7 | - gh-pages 8 | install: 9 | - npm install 10 | script: 11 | - npm test 12 | - npm run-script updatedb 13 | - npm run-script updatedb force 14 | env: 15 | global: 16 | secure: HyJER2vbLVd8rvfFdZ9XUlIlYPmAtXM2ADXzc1Z6cdzDFXqmZs3Rgh66O7PXAVFkImD8aNWKs6Oc6ZOZzhSQqU8tWvOcq3I9JAKpsKAHZh9wYEhVbiuOFUJlViFzkQDUQY/XzL3lOs3OGpqE/ZiX5xJFOQ2PZoumtXfQ7CiDWmfCTaaWIafgw5/+xgAwLVuGtpQZyPxMz67RAsblS4FN6ISRdE64H1L8fOOSDHW+pv44IuyVftwJhqx1dxyu2AzB5NMA8izqOyTQ2SRv0J9YJea55LUp2H+tMjxr/npsMAh8w1HWGNDoZdI8wNTX0CMICe0HN+LN0OZh3YBOmQABRvuyQNqiorWgdH4w1wS8ij83PaKHipsKwL1+Ez2CcVQkHvXJUNodqCNH2uW9fAeDPrQKj0qLpiVeAXlkvUCpGWW4jSr8e1R4KKndtnsSmBmq91bdKUxWfk//pAmr/klcoi8QtMFqk9ZTpivhfJX8ci4rM00CqdHb/nZarl3gDE08uyNoEuBTmrjH6GLW6pHuUdUqGBLjnq6Jh23OmJDrj3plr3EzvreNVNknu3I7Hzhjdo2FcXh8Apoz48L2TsSxYQCd99f34twZnOfaE54xKAloGHfdAEGpsRsTcvW0IkQ540WKWDWbDm5PdoS7uonIvwctuBfLtkfnU3g/uVsN+44= 17 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Philip Tellis (http://bluesmoon.info/) 2 | Arturs Sosins <@ar2rsawseen> 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | There are two licenses, one for the software library, and one for the data. 2 | 3 | This product includes GeoLite data created by MaxMind, available from http://maxmind.com/ 4 | 5 | SOFTWARE LICENSE (Node JS library) 6 | 7 | The node-geoip JavaScript library is licensed under the Apache License, Version 2.0: 8 | 9 | Copyright 2011 Philip Tellis 10 | 11 | Licensed under the Apache License, Version 2.0 (the "License"); 12 | you may not use this file except in compliance with the License. 13 | You may obtain a copy of the License at 14 | 15 | http://www.apache.org/licenses/LICENSE-2.0 16 | 17 | Unless required by applicable law or agreed to in writing, software 18 | distributed under the License is distributed on an "AS IS" BASIS, 19 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20 | See the License for the specific language governing permissions and 21 | limitations under the License. 22 | 23 | DATABASES LICENSE (GeoLite2 databases) 24 | 25 | Copyright (c) 2012-2018 MaxMind, Inc. All Rights Reserved. 26 | 27 | The GeoLite2 databases are distributed under the 28 | Creative Commons Attribution-ShareAlike 4.0 International License (the "License"); 29 | you may not use this file except in compliance with the License. 30 | You may obtain a copy of the License at 31 | 32 | https://creativecommons.org/licenses/by-sa/4.0/legalcode 33 | 34 | The attribution requirement may be met by including the following in all 35 | advertising and documentation mentioning features of or use of this 36 | database: 37 | 38 | This product includes GeoLite2 data created by MaxMind, available from 39 | http://www.maxmind.com. 40 | 41 | THIS DATABASE IS PROVIDED BY MAXMIND, INC ``AS IS'' AND ANY 42 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 43 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 44 | DISCLAIMED. IN NO EVENT SHALL MAXMIND BE LIABLE FOR ANY 45 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 46 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 47 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 48 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 49 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 50 | DATABASE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GeoIP-lite 2 | ========== 3 | 4 | A native NodeJS API for the GeoLite data from MaxMind. 5 | 6 | This product includes GeoLite data created by MaxMind, available from http://maxmind.com/ 7 | 8 | **NOTE** You MUST update the data files after installation. The MaxMind license does not allow us to distribute 9 | the latest version of the data files with this package. Follow the instructions under [update the datafiles](#2-update-the-datafiles-optional) 10 | for details. 11 | 12 | introduction 13 | ------------ 14 | 15 | MaxMind provides a set of data files for IP to Geo mapping along with opensource libraries to parse and lookup these data files. 16 | One would typically write a wrapper around their C API to get access to this data in other languages (like JavaScript). 17 | 18 | GeoIP-lite instead attempts to be a fully native JavaScript library. A converter script converts the CSV files from MaxMind into 19 | an internal binary format (note that this is different from the binary data format provided by MaxMind). The geoip module uses this 20 | binary file to lookup IP addresses and return the country, region and city that it maps to. 21 | 22 | Both IPv4 and IPv6 addresses are supported, however since the GeoLite IPv6 database does not currently contain any city or region 23 | information, city, region and postal code lookups are only supported for IPv4. 24 | 25 | philosophy 26 | ---------- 27 | 28 | I was really aiming for a fast JavaScript native implementation for geomapping of IPs. My prime motivator was the fact that it was 29 | really hard to get libgeoip built for Mac OSX without using the library from MacPorts. 30 | 31 | why geoip-lite 32 | -------------- 33 | 34 | `geoip-lite` is a fully JavaScript implementation of the MaxMind geoip API. It is not as fully featured as bindings that use `libgeoip`. 35 | By reducing scope, this package is about 40% faster at doing lookups. On average, an IP to Location lookup should take 20 microseconds on 36 | a Macbook Pro. IPv4 addresses take about 6 microseconds, while IPv6 addresses take about 30 microseconds. 37 | 38 | synopsis 39 | -------- 40 | 41 | ```javascript 42 | var geoip = require('geoip-lite'); 43 | 44 | var ip = "207.97.227.239"; 45 | var geo = geoip.lookup(ip); 46 | 47 | console.log(geo); 48 | { range: [ 3479298048, 3479300095 ], 49 | country: 'US', 50 | region: 'TX', 51 | eu: '0', 52 | timezone: 'America/Chicago', 53 | city: 'San Antonio', 54 | ll: [ 29.4969, -98.4032 ], 55 | metro: 641, 56 | area: 1000 } 57 | 58 | ``` 59 | 60 | installation 61 | ------------ 62 | ### 1. get the library 63 | 64 | $ npm install geoip-lite 65 | 66 | ### 2. update the datafiles (optional) 67 | 68 | Run `cd node_modules/geoip-lite && npm run-script updatedb license_key=YOUR_LICENSE_KEY` to update the data files. (Replace `YOUR_LICENSE_KEY` with your license key obtained from [maxmind.com](https://support.maxmind.com/hc/en-us/articles/4407111582235-Generate-a-License-Key)) 69 | 70 | You can create a maxmind account [here](https://www.maxmind.com/en/geolite2/signup) 71 | 72 | **NOTE** that this requires a lot of RAM. It is known to fail on on a Digital Ocean or AWS micro instance. 73 | There are no plans to change this. `geoip-lite` stores all data in RAM in order to be fast. 74 | 75 | API 76 | --- 77 | 78 | geoip-lite is completely synchronous. There are no callbacks involved. All blocking file IO is done at startup time, so all runtime 79 | calls are executed in-memory and are fast. Startup may take up to 200ms while it reads into memory and indexes data files. 80 | 81 | ### Looking up an IP address ### 82 | 83 | If you have an IP address in dotted quad notation, IPv6 colon notation, or a 32 bit unsigned integer (treated 84 | as an IPv4 address), pass it to the `lookup` method. Note that you should remove any `[` and `]` around an 85 | IPv6 address before passing it to this method. 86 | 87 | ```javascript 88 | var geo = geoip.lookup(ip); 89 | ``` 90 | 91 | If the IP address was found, the `lookup` method returns an object with the following structure: 92 | 93 | ```javascript 94 | { 95 | range: [ , ], 96 | country: 'XX', // 2 letter ISO-3166-1 country code 97 | region: 'RR', // Up to 3 alphanumeric variable length characters as ISO 3166-2 code 98 | // For US states this is the 2 letter state 99 | // For the United Kingdom this could be ENG as a country like “England 100 | // FIPS 10-4 subcountry code 101 | eu: '0', // 1 if the country is a member state of the European Union, 0 otherwise. 102 | timezone: 'Country/Zone', // Timezone from IANA Time Zone Database 103 | city: "City Name", // This is the full city name 104 | ll: [, ], // The latitude and longitude of the city 105 | metro: , // Metro code 106 | area: // The approximate accuracy radius (km), around the latitude and longitude 107 | } 108 | ``` 109 | 110 | The actual values for the `range` array depend on whether the IP is IPv4 or IPv6 and should be 111 | considered internal to `geoip-lite`. To get a human readable format, pass them to `geoip.pretty()` 112 | 113 | If the IP address was not found, the `lookup` returns `null` 114 | 115 | ### Pretty printing an IP address ### 116 | 117 | If you have a 32 bit unsigned integer, or a number returned as part of the `range` array from the `lookup` method, 118 | the `pretty` method can be used to turn it into a human readable string. 119 | 120 | ```javascript 121 | console.log("The IP is %s", geoip.pretty(ip)); 122 | ``` 123 | 124 | This method returns a string if the input was in a format that `geoip-lite` can recognise, else it returns the 125 | input itself. 126 | 127 | Built-in Updater 128 | ---------------- 129 | 130 | This package contains an update script that can pull the files from MaxMind and handle the conversion from CSV. 131 | A npm script alias has been setup to make this process easy. Please keep in mind this requires internet and MaxMind 132 | rate limits that amount of downloads on their servers. 133 | 134 | You will need, at minimum, a free license key obtained from [maxmind.com](https://support.maxmind.com/hc/en-us/articles/4407111582235-Generate-a-License-Key) to run the update script. 135 | 136 | Package stores checksums of MaxMind data and by default only downloads them if checksums have changed. 137 | 138 | ### Ways to update data ### 139 | 140 | ```shell 141 | #update data if new data is available 142 | npm run-script updatedb license_key=YOUR_LICENSE_KEY 143 | 144 | #force udpate data even if checkums have not changed 145 | npm run-script updatedb-force license_key=YOUR_LICENSE_KEY 146 | ``` 147 | 148 | You can also run it by doing: 149 | 150 | ```bash 151 | node ./node_modules/geoip-lite/scripts/updatedb.js license_key=YOUR_LICENSE_KEY 152 | ``` 153 | 154 | ### Ways to reload data in your app when update finished ### 155 | 156 | If you have a server running `geoip-lite`, and you want to reload its geo data, after you finished update, without a restart. 157 | 158 | #### Programmatically #### 159 | 160 | You can do it programmatically, calling after scheduled data updates 161 | 162 | ```javascript 163 | //Synchronously 164 | geoip.reloadDataSync(); 165 | 166 | //Asynchronously 167 | geoip.reloadData(function(){ 168 | console.log("Done"); 169 | }); 170 | ``` 171 | 172 | #### Automatic Start and stop watching for data updates #### 173 | 174 | You can enable the data watcher to automatically refresh in-memory geo data when a file changes in the data directory. 175 | 176 | ```javascript 177 | geoip.startWatchingDataUpdate(); 178 | ``` 179 | 180 | This tool can be used with `npm run-script updatedb` to periodically update geo data on a running server. 181 | 182 | #### Environment variables 183 | 184 | The following environment variables can be set. 185 | 186 | ```bash 187 | # Override the default node_modules/geoip-lite/data dir 188 | GEOTMPDIR=/some/path 189 | 190 | # Override the default node_modules/geoip-lite/tmp dir 191 | GEODATADIR=/some/path 192 | ``` 193 | 194 | Caveats 195 | ------- 196 | 197 | This package includes the GeoLite database from MaxMind. This database is not the most accurate database available, 198 | however it is the best available for free. You can use the commercial GeoIP database from MaxMind with better 199 | accuracy by buying a license from MaxMind, and then using the conversion utility to convert it to a format that 200 | geoip-lite understands. You will need to use the `.csv` files from MaxMind for conversion. 201 | 202 | Also note that on occassion, the library may take up to 5 seconds to load into memory. This is largely dependent on 203 | how busy your disk is at that time. It can take as little as 200ms on a lightly loaded disk. This is a one time 204 | cost though, and you make it up at run time with very fast lookups. 205 | 206 | ### Memory usage ### 207 | 208 | Quick test on memory consumption shows that library uses around 100Mb per process 209 | 210 | ```javascript 211 | var geoip = require('geoip-lite'); 212 | console.log(process.memoryUsage()); 213 | /** 214 | * Outputs: 215 | * { 216 | * rss: 126365696, 217 | * heapTotal: 10305536, 218 | * heapUsed: 5168944, 219 | * external: 104347120 220 | * } 221 | **/ 222 | ``` 223 | 224 | Alternatives 225 | ---------- 226 | If your use-case requires doing less than 100 queries through the lifetime of your application or if you need really fast latency on start-up, you might want to look into [fast-geoip](https://github.com/onramper/fast-geoip) a package with a compatible API that is optimized for serverless environments and provides faster boot times and lower memory consumption at the expense of longer lookup times. 227 | 228 | References 229 | ---------- 230 | - Documentation from MaxMind 231 | - ISO 3166 (1 & 2) codes 232 | - FIPS region codes 233 | 234 | Copyright 235 | --------- 236 | 237 | `geoip-lite` is Copyright Philip Tellis and other contributors, and the latest version of the code is 238 | available at https://github.com/bluesmoon/node-geoip 239 | 240 | License 241 | ------- 242 | 243 | There are two licenses for the code and data. See the [LICENSE](https://github.com/bluesmoon/node-geoip/blob/master/LICENSE) file for details. 244 | -------------------------------------------------------------------------------- /data/city.checksum: -------------------------------------------------------------------------------- 1 | 738794f02b34c28847582790e570b533 -------------------------------------------------------------------------------- /data/country.checksum: -------------------------------------------------------------------------------- 1 | 480b2d18f8423ae1f9370722fb6d12d8 -------------------------------------------------------------------------------- /data/geoip-city-names.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geoip-lite/node-geoip/c788342dfce6d97244842622da6e4fa259f79742/data/geoip-city-names.dat -------------------------------------------------------------------------------- /data/geoip-city.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geoip-lite/node-geoip/c788342dfce6d97244842622da6e4fa259f79742/data/geoip-city.dat -------------------------------------------------------------------------------- /data/geoip-city6.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geoip-lite/node-geoip/c788342dfce6d97244842622da6e4fa259f79742/data/geoip-city6.dat -------------------------------------------------------------------------------- /data/geoip-country.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geoip-lite/node-geoip/c788342dfce6d97244842622da6e4fa259f79742/data/geoip-country.dat -------------------------------------------------------------------------------- /data/geoip-country6.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geoip-lite/node-geoip/c788342dfce6d97244842622da6e4fa259f79742/data/geoip-country6.dat -------------------------------------------------------------------------------- /lib/fsWatcher.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | path = require('path'), 3 | FSWatcher = {}; 4 | 5 | /** 6 | * Takes a directory/file and watch for change. Upon change, call the 7 | * callback. 8 | * 9 | * @param {String} name: name of this watcher 10 | * @param {String} directory: path to the directory to watch 11 | * @param {String} [filename]: (optional) specific filename to watch for, 12 | * watches for all files in the directory if unspecified 13 | * @param {Integer} cooldownDelay: delay to wait before triggering the callback 14 | * @param {Function} callback: function () : called when changes are detected 15 | **/ 16 | function makeFsWatchFilter(name, directory, filename, cooldownDelay, callback) { 17 | var cooldownId = null; 18 | 19 | //Delete the cooldownId and callback the outer function 20 | function timeoutCallback() { 21 | cooldownId = null; 22 | callback(); 23 | } 24 | 25 | //This function is called when there is a change in the data directory 26 | //It sets a timer to wait for the change to be completed 27 | function onWatchEvent(event, changedFile) { 28 | // check to make sure changedFile is not null 29 | if (!changedFile) { 30 | return; 31 | } 32 | 33 | var filePath = path.join(directory, changedFile); 34 | 35 | if (!filename || filename === changedFile) { 36 | fs.exists(filePath, function onExists(exists) { 37 | if (!exists) { 38 | // if the changed file no longer exists, it was a deletion. 39 | // we ignore deleted files 40 | return; 41 | } 42 | 43 | //At this point, a new file system activity has been detected, 44 | //We have to wait for file transfert to be finished before moving on. 45 | 46 | //If a cooldownId already exists, we delete it 47 | if (cooldownId !== null) { 48 | clearTimeout(cooldownId); 49 | cooldownId = null; 50 | } 51 | 52 | //Once the cooldownDelay has passed, the timeoutCallback function will be called 53 | cooldownId = setTimeout(timeoutCallback, cooldownDelay); 54 | }); 55 | } 56 | } 57 | 58 | //Manage the case where filename is missing (because it's optionnal) 59 | if (typeof cooldownDelay === 'function') { 60 | callback = cooldownDelay; 61 | cooldownDelay = filename; 62 | filename = null; 63 | } 64 | 65 | if (FSWatcher[name]) { 66 | stopWatching(name); 67 | } 68 | 69 | FSWatcher[name] = fs.watch(directory, onWatchEvent); 70 | } 71 | 72 | /** 73 | * Take a FSWatcher object and close it. 74 | * 75 | * @param {string} name: name of the watcher to close 76 | * 77 | **/ 78 | function stopWatching(name) { 79 | FSWatcher[name].close(); 80 | } 81 | 82 | module.exports.makeFsWatchFilter = makeFsWatchFilter; 83 | module.exports.stopWatching = stopWatching; 84 | -------------------------------------------------------------------------------- /lib/geoip.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var net = require('net'); 3 | var path = require('path'); 4 | 5 | fs.existsSync = fs.existsSync || path.existsSync; 6 | 7 | var utils = require('./utils'); 8 | var fsWatcher = require('./fsWatcher'); 9 | var async = require('async'); 10 | 11 | var watcherName = 'dataWatcher'; 12 | 13 | var geodatadir = path.resolve( 14 | __dirname, 15 | global.geodatadir || process.env.GEODATADIR || '../data/' 16 | ); 17 | 18 | var dataFiles = { 19 | city: path.join(geodatadir, 'geoip-city.dat'), 20 | city6: path.join(geodatadir, 'geoip-city6.dat'), 21 | cityNames: path.join(geodatadir, 'geoip-city-names.dat'), 22 | country: path.join(geodatadir, 'geoip-country.dat'), 23 | country6: path.join(geodatadir, 'geoip-country6.dat') 24 | }; 25 | 26 | var privateRange4 = [ 27 | [utils.aton4('10.0.0.0'), utils.aton4('10.255.255.255')], 28 | [utils.aton4('172.16.0.0'), utils.aton4('172.31.255.255')], 29 | [utils.aton4('192.168.0.0'), utils.aton4('192.168.255.255')] 30 | ]; 31 | 32 | var conf4 = { 33 | firstIP: null, 34 | lastIP: null, 35 | lastLine: 0, 36 | locationBuffer: null, 37 | locationRecordSize: 88, 38 | mainBuffer: null, 39 | recordSize: 24 40 | }; 41 | 42 | var conf6 = { 43 | firstIP: null, 44 | lastIP: null, 45 | lastLine: 0, 46 | mainBuffer: null, 47 | recordSize: 48 48 | }; 49 | 50 | //copy original configs 51 | var cache4 = JSON.parse(JSON.stringify(conf4)); 52 | var cache6 = JSON.parse(JSON.stringify(conf6)); 53 | 54 | var RECORD_SIZE = 10; 55 | var RECORD_SIZE6 = 34; 56 | 57 | function lookup4(ip) { 58 | var fline = 0; 59 | var floor = cache4.lastIP; 60 | var cline = cache4.lastLine; 61 | var ceil = cache4.firstIP; 62 | var line; 63 | var locId; 64 | 65 | var buffer = cache4.mainBuffer; 66 | var locBuffer = cache4.locationBuffer; 67 | var privateRange = privateRange4; 68 | var recordSize = cache4.recordSize; 69 | var locRecordSize = cache4.locationRecordSize; 70 | 71 | var i; 72 | 73 | var geodata = { 74 | range: '', 75 | country: '', 76 | region: '', 77 | eu:'', 78 | timezone:'', 79 | city: '', 80 | ll: [null, null] 81 | }; 82 | 83 | // outside IPv4 range 84 | if (ip > cache4.lastIP || ip < cache4.firstIP) { 85 | return null; 86 | } 87 | 88 | // private IP 89 | for (i = 0; i < privateRange.length; i++) { 90 | if (ip >= privateRange[i][0] && ip <= privateRange[i][1]) { 91 | return null; 92 | } 93 | } 94 | 95 | do { 96 | line = Math.round((cline - fline) / 2) + fline; 97 | floor = buffer.readUInt32BE(line * recordSize); 98 | ceil = buffer.readUInt32BE((line * recordSize) + 4); 99 | 100 | if (floor <= ip && ceil >= ip) { 101 | geodata.range = [floor, ceil]; 102 | 103 | if (recordSize === RECORD_SIZE) { 104 | geodata.country = buffer.toString('utf8', (line * recordSize) + 8, (line * recordSize) + 10); 105 | } else { 106 | locId = buffer.readUInt32BE((line * recordSize) + 8); 107 | 108 | // -1>>>0 is a marker for "No Location Info" 109 | if(-1>>>0 > locId) { 110 | geodata.country = locBuffer.toString('utf8', (locId * locRecordSize) + 0, (locId * locRecordSize) + 2).replace(/\u0000.*/, ''); 111 | geodata.region = locBuffer.toString('utf8', (locId * locRecordSize) + 2, (locId * locRecordSize) + 5).replace(/\u0000.*/, ''); 112 | geodata.metro = locBuffer.readInt32BE((locId * locRecordSize) + 5); 113 | geodata.ll[0] = buffer.readInt32BE((line * recordSize) + 12)/10000;//latitude 114 | geodata.ll[1] = buffer.readInt32BE((line * recordSize) + 16)/10000; //longitude 115 | geodata.area = buffer.readUInt32BE((line * recordSize) + 20); //longitude 116 | geodata.eu = locBuffer.toString('utf8', (locId * locRecordSize) + 9, (locId * locRecordSize) + 10).replace(/\u0000.*/, ''); 117 | geodata.timezone = locBuffer.toString('utf8', (locId * locRecordSize) + 10, (locId * locRecordSize) + 42).replace(/\u0000.*/, ''); 118 | geodata.city = locBuffer.toString('utf8', (locId * locRecordSize) + 42, (locId * locRecordSize) + locRecordSize).replace(/\u0000.*/, ''); 119 | } 120 | } 121 | 122 | return geodata; 123 | } else if (fline === cline) { 124 | return null; 125 | } else if (fline === (cline - 1)) { 126 | if (line === fline) { 127 | fline = cline; 128 | } else { 129 | cline = fline; 130 | } 131 | } else if (floor > ip) { 132 | cline = line; 133 | } else if (ceil < ip) { 134 | fline = line; 135 | } 136 | } while(1); 137 | } 138 | 139 | function lookup6(ip) { 140 | var buffer = cache6.mainBuffer; 141 | var recordSize = cache6.recordSize; 142 | var locBuffer = cache4.locationBuffer; 143 | var locRecordSize = cache4.locationRecordSize; 144 | 145 | var geodata = { 146 | range: '', 147 | country: '', 148 | region: '', 149 | city: '', 150 | ll: [0, 0] 151 | }; 152 | function readip(line, offset) { 153 | var ii = 0; 154 | var ip = []; 155 | 156 | for (ii = 0; ii < 2; ii++) { 157 | ip.push(buffer.readUInt32BE((line * recordSize) + (offset * 16) + (ii * 4))); 158 | } 159 | 160 | return ip; 161 | } 162 | 163 | cache6.lastIP = readip(cache6.lastLine, 1); 164 | cache6.firstIP = readip(0, 0); 165 | 166 | var fline = 0; 167 | var floor = cache6.lastIP; 168 | var cline = cache6.lastLine; 169 | var ceil = cache6.firstIP; 170 | var line; 171 | var locId; 172 | 173 | if (utils.cmp6(ip, cache6.lastIP) > 0 || utils.cmp6(ip, cache6.firstIP) < 0) { 174 | return null; 175 | } 176 | 177 | do { 178 | line = Math.round((cline - fline) / 2) + fline; 179 | floor = readip(line, 0); 180 | ceil = readip(line, 1); 181 | 182 | if (utils.cmp6(floor, ip) <= 0 && utils.cmp6(ceil, ip) >= 0) { 183 | if (recordSize === RECORD_SIZE6) { 184 | geodata.country = buffer.toString('utf8', (line * recordSize) + 32, (line * recordSize) + 34).replace(/\u0000.*/, ''); 185 | } else { 186 | locId = buffer.readUInt32BE((line * recordSize) + 32); 187 | 188 | // -1>>>0 is a marker for "No Location Info" 189 | if(-1>>>0 > locId) { 190 | geodata.country = locBuffer.toString('utf8', (locId * locRecordSize) + 0, (locId * locRecordSize) + 2).replace(/\u0000.*/, ''); 191 | geodata.region = locBuffer.toString('utf8', (locId * locRecordSize) + 2, (locId * locRecordSize) + 5).replace(/\u0000.*/, ''); 192 | geodata.metro = locBuffer.readInt32BE((locId * locRecordSize) + 5); 193 | geodata.ll[0] = buffer.readInt32BE((line * recordSize) + 36)/10000;//latitude 194 | geodata.ll[1] = buffer.readInt32BE((line * recordSize) + 40)/10000; //longitude 195 | geodata.area = buffer.readUInt32BE((line * recordSize) + 44); //area 196 | geodata.eu = locBuffer.toString('utf8', (locId * locRecordSize) + 9, (locId * locRecordSize) + 10).replace(/\u0000.*/, ''); 197 | geodata.timezone = locBuffer.toString('utf8', (locId * locRecordSize) + 10, (locId * locRecordSize) + 42).replace(/\u0000.*/, ''); 198 | geodata.city = locBuffer.toString('utf8', (locId * locRecordSize) + 42, (locId * locRecordSize) + locRecordSize).replace(/\u0000.*/, ''); 199 | } 200 | } 201 | // We do not currently have detailed region/city info for IPv6, but finally have coords 202 | return geodata; 203 | } else if (fline === cline) { 204 | return null; 205 | } else if (fline === (cline - 1)) { 206 | if (line === fline) { 207 | fline = cline; 208 | } else { 209 | cline = fline; 210 | } 211 | } else if (utils.cmp6(floor, ip) > 0) { 212 | cline = line; 213 | } else if (utils.cmp6(ceil, ip) < 0) { 214 | fline = line; 215 | } 216 | } while(1); 217 | } 218 | 219 | function get4mapped(ip) { 220 | var ipv6 = ip.toUpperCase(); 221 | var v6prefixes = ['0:0:0:0:0:FFFF:', '::FFFF:']; 222 | for (var i = 0; i < v6prefixes.length; i++) { 223 | var v6prefix = v6prefixes[i]; 224 | if (ipv6.indexOf(v6prefix) == 0) { 225 | return ipv6.substring(v6prefix.length); 226 | } 227 | } 228 | return null; 229 | } 230 | 231 | function preload(callback) { 232 | var datFile; 233 | var datSize; 234 | var asyncCache = JSON.parse(JSON.stringify(conf4)); 235 | 236 | //when the preload function receives a callback, do the task asynchronously 237 | if (typeof arguments[0] === 'function') { 238 | async.series([ 239 | function (cb) { 240 | async.series([ 241 | function (cb2) { 242 | fs.open(dataFiles.cityNames, 'r', function (err, file) { 243 | datFile = file; 244 | cb2(err); 245 | }); 246 | }, 247 | function (cb2) { 248 | fs.fstat(datFile, function (err, stats) { 249 | datSize = stats.size; 250 | asyncCache.locationBuffer = Buffer.alloc(datSize); 251 | cb2(err); 252 | }); 253 | }, 254 | function (cb2) { 255 | fs.read(datFile, asyncCache.locationBuffer, 0, datSize, 0, cb2); 256 | }, 257 | function (cb2) { 258 | fs.close(datFile, cb2); 259 | }, 260 | function (cb2) { 261 | fs.open(dataFiles.city, 'r', function (err, file) { 262 | datFile = file; 263 | cb2(err); 264 | }); 265 | }, 266 | function (cb2) { 267 | fs.fstat(datFile, function (err, stats) { 268 | datSize = stats.size; 269 | cb2(err); 270 | }); 271 | } 272 | ], function (err) { 273 | if (err) { 274 | if (err.code !== 'ENOENT' && err.code !== 'EBADF') { 275 | throw err; 276 | } 277 | 278 | fs.open(dataFiles.country, 'r', function (err, file) { 279 | if (err) { 280 | cb(err); 281 | } else { 282 | datFile = file; 283 | fs.fstat(datFile, function (err, stats) { 284 | datSize = stats.size; 285 | asyncCache.recordSize = RECORD_SIZE; 286 | 287 | cb(); 288 | }); 289 | } 290 | }); 291 | 292 | } else { 293 | cb(); 294 | } 295 | }); 296 | }, 297 | function () { 298 | asyncCache.mainBuffer = Buffer.alloc(datSize); 299 | 300 | async.series([ 301 | function (cb2) { 302 | fs.read(datFile, asyncCache.mainBuffer, 0, datSize, 0, cb2); 303 | }, 304 | function (cb2) { 305 | fs.close(datFile, cb2); 306 | } 307 | ], function (err) { 308 | if (err) { 309 | //keep old cache 310 | } else { 311 | asyncCache.lastLine = (datSize / asyncCache.recordSize) - 1; 312 | asyncCache.lastIP = asyncCache.mainBuffer.readUInt32BE((asyncCache.lastLine * asyncCache.recordSize) + 4); 313 | asyncCache.firstIP = asyncCache.mainBuffer.readUInt32BE(0); 314 | cache4 = asyncCache; 315 | } 316 | callback(err); 317 | }); 318 | } 319 | ]); 320 | } else { 321 | try { 322 | datFile = fs.openSync(dataFiles.cityNames, 'r'); 323 | datSize = fs.fstatSync(datFile).size; 324 | 325 | if (datSize === 0) { 326 | throw { 327 | code: 'EMPTY_FILE' 328 | }; 329 | } 330 | 331 | cache4.locationBuffer = Buffer.alloc(datSize); 332 | fs.readSync(datFile, cache4.locationBuffer, 0, datSize, 0); 333 | fs.closeSync(datFile); 334 | 335 | datFile = fs.openSync(dataFiles.city, 'r'); 336 | datSize = fs.fstatSync(datFile).size; 337 | } catch(err) { 338 | if (err.code !== 'ENOENT' && err.code !== 'EBADF' && err.code !== 'EMPTY_FILE') { 339 | throw err; 340 | } 341 | 342 | datFile = fs.openSync(dataFiles.country, 'r'); 343 | datSize = fs.fstatSync(datFile).size; 344 | cache4.recordSize = RECORD_SIZE; 345 | } 346 | 347 | cache4.mainBuffer = Buffer.alloc(datSize); 348 | fs.readSync(datFile, cache4.mainBuffer, 0, datSize, 0); 349 | 350 | fs.closeSync(datFile); 351 | 352 | cache4.lastLine = (datSize / cache4.recordSize) - 1; 353 | cache4.lastIP = cache4.mainBuffer.readUInt32BE((cache4.lastLine * cache4.recordSize) + 4); 354 | cache4.firstIP = cache4.mainBuffer.readUInt32BE(0); 355 | } 356 | } 357 | 358 | function preload6(callback) { 359 | var datFile; 360 | var datSize; 361 | var asyncCache6 = JSON.parse(JSON.stringify(conf6)); 362 | 363 | //when the preload function receives a callback, do the task asynchronously 364 | if (typeof arguments[0] === 'function') { 365 | async.series([ 366 | function (cb) { 367 | async.series([ 368 | function (cb2) { 369 | fs.open(dataFiles.city6, 'r', function (err, file) { 370 | datFile = file; 371 | cb2(err); 372 | }); 373 | }, 374 | function (cb2) { 375 | fs.fstat(datFile, function (err, stats) { 376 | datSize = stats.size; 377 | cb2(err); 378 | }); 379 | } 380 | ], function (err) { 381 | if (err) { 382 | if (err.code !== 'ENOENT' && err.code !== 'EBADF') { 383 | throw err; 384 | } 385 | 386 | fs.open(dataFiles.country6, 'r', function (err, file) { 387 | if (err) { 388 | cb(err); 389 | } else { 390 | datFile = file; 391 | fs.fstat(datFile, function (err, stats) { 392 | datSize = stats.size; 393 | asyncCache6.recordSize = RECORD_SIZE6; 394 | 395 | cb(); 396 | }); 397 | } 398 | }); 399 | } else { 400 | cb(); 401 | } 402 | }); 403 | }, 404 | function () { 405 | asyncCache6.mainBuffer = Buffer.alloc(datSize); 406 | 407 | async.series([ 408 | function (cb2) { 409 | fs.read(datFile, asyncCache6.mainBuffer, 0, datSize, 0, cb2); 410 | }, 411 | function (cb2) { 412 | fs.close(datFile, cb2); 413 | } 414 | ], function (err) { 415 | if (err) { 416 | //keep old cache 417 | } else { 418 | asyncCache6.lastLine = (datSize / asyncCache6.recordSize) - 1; 419 | cache6 = asyncCache6; 420 | } 421 | callback(err); 422 | }); 423 | } 424 | ]); 425 | } else { 426 | try { 427 | datFile = fs.openSync(dataFiles.city6, 'r'); 428 | datSize = fs.fstatSync(datFile).size; 429 | 430 | if (datSize === 0) { 431 | throw { 432 | code: 'EMPTY_FILE' 433 | }; 434 | } 435 | } catch(err) { 436 | if (err.code !== 'ENOENT' && err.code !== 'EBADF' && err.code !== 'EMPTY_FILE') { 437 | throw err; 438 | } 439 | 440 | datFile = fs.openSync(dataFiles.country6, 'r'); 441 | datSize = fs.fstatSync(datFile).size; 442 | cache6.recordSize = RECORD_SIZE6; 443 | } 444 | 445 | cache6.mainBuffer = Buffer.alloc(datSize); 446 | fs.readSync(datFile, cache6.mainBuffer, 0, datSize, 0); 447 | 448 | fs.closeSync(datFile); 449 | 450 | cache6.lastLine = (datSize / cache6.recordSize) - 1; 451 | } 452 | } 453 | 454 | module.exports = { 455 | cmp: utils.cmp, 456 | 457 | lookup: function(ip) { 458 | if (!ip) { 459 | return null; 460 | } else if (typeof ip === 'number') { 461 | return lookup4(ip); 462 | } else if (net.isIP(ip) === 4) { 463 | return lookup4(utils.aton4(ip)); 464 | } else if (net.isIP(ip) === 6) { 465 | var ipv4 = get4mapped(ip); 466 | if (ipv4) { 467 | return lookup4(utils.aton4(ipv4)); 468 | } else { 469 | return lookup6(utils.aton6(ip)); 470 | } 471 | } 472 | 473 | return null; 474 | }, 475 | 476 | pretty: function(n) { 477 | if (typeof n === 'string') { 478 | return n; 479 | } else if (typeof n === 'number') { 480 | return utils.ntoa4(n); 481 | } else if (n instanceof Array) { 482 | return utils.ntoa6(n); 483 | } 484 | 485 | return n; 486 | }, 487 | 488 | // Start watching for data updates. The watcher waits one minute for file transfer to 489 | // completete before triggering the callback. 490 | startWatchingDataUpdate: function (callback) { 491 | fsWatcher.makeFsWatchFilter(watcherName, geodatadir, 60*1000, function () { 492 | //Reload data 493 | async.series([ 494 | function (cb) { 495 | preload(cb); 496 | }, 497 | function (cb) { 498 | preload6(cb); 499 | } 500 | ], callback); 501 | }); 502 | }, 503 | 504 | // Stop watching for data updates. 505 | stopWatchingDataUpdate: function () { 506 | fsWatcher.stopWatching(watcherName); 507 | }, 508 | 509 | //clear data 510 | clear: function () { 511 | cache4 = JSON.parse(JSON.stringify(conf4)); 512 | cache6 = JSON.parse(JSON.stringify(conf6)); 513 | }, 514 | 515 | // Reload data synchronously 516 | reloadDataSync: function () { 517 | preload(); 518 | preload6(); 519 | }, 520 | 521 | // Reload data asynchronously 522 | reloadData: function (callback) { 523 | //Reload data 524 | async.series([ 525 | function (cb) { 526 | preload(cb); 527 | }, 528 | function (cb) { 529 | preload6(cb); 530 | } 531 | ], callback); 532 | }, 533 | }; 534 | 535 | preload(); 536 | preload6(); 537 | 538 | //lookup4 = gen_lookup('geoip-country.dat', 4); 539 | //lookup6 = gen_lookup('geoip-country6.dat', 16); 540 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | var utils = module.exports = {}; 2 | 3 | utils.aton4 = function(a) { 4 | a = a.split(/\./); 5 | return ((parseInt(a[0], 10)<<24)>>>0) + ((parseInt(a[1], 10)<<16)>>>0) + ((parseInt(a[2], 10)<<8)>>>0) + (parseInt(a[3], 10)>>>0); 6 | }; 7 | 8 | utils.aton6 = function(a) { 9 | a = a.replace(/"/g, '').split(/:/); 10 | 11 | var l = a.length - 1; 12 | var i; 13 | 14 | if (a[l] === '') { 15 | a[l] = 0; 16 | } 17 | 18 | if (l < 7) { 19 | a.length = 8; 20 | 21 | for (i = l; i >= 0 && a[i] !== ''; i--) { 22 | a[7-l+i] = a[i]; 23 | } 24 | } 25 | 26 | for (i = 0; i < 8; i++) { 27 | if (!a[i]) { 28 | a[i]=0; 29 | } else { 30 | a[i] = parseInt(a[i], 16); 31 | } 32 | } 33 | 34 | var r = []; 35 | for (i = 0; i<4; i++) { 36 | r.push(((a[2*i]<<16) + a[2*i+1])>>>0); 37 | } 38 | 39 | return r; 40 | }; 41 | 42 | 43 | utils.cmp = function(a, b) { 44 | if (typeof a === 'number' && typeof b === 'number') { 45 | return (a < b ? -1 : (a > b ? 1 : 0)); 46 | } 47 | 48 | if (a instanceof Array && b instanceof Array) { 49 | return this.cmp6(a, b); 50 | } 51 | 52 | return null; 53 | }; 54 | 55 | utils.cmp6 = function(a, b) { 56 | for (var ii = 0; ii < 2; ii++) { 57 | if (a[ii] < b[ii]) { 58 | return -1; 59 | } 60 | 61 | if (a[ii] > b[ii]) { 62 | return 1; 63 | } 64 | } 65 | 66 | return 0; 67 | }; 68 | 69 | utils.isPrivateIP = function(addr) { 70 | addr = addr.toString(); 71 | 72 | return addr.match(/^10\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/) != null || 73 | addr.match(/^192\.168\.([0-9]{1,3})\.([0-9]{1,3})/) != null || 74 | addr.match(/^172\.16\.([0-9]{1,3})\.([0-9]{1,3})/) != null || 75 | addr.match(/^127\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/) != null || 76 | addr.match(/^169\.254\.([0-9]{1,3})\.([0-9]{1,3})/) != null || 77 | addr.match(/^fc00:/) != null || addr.match(/^fe80:/) != null; 78 | }; 79 | 80 | utils.ntoa4 = function(n) { 81 | n = n.toString(); 82 | n = '' + (n>>>24&0xff) + '.' + (n>>>16&0xff) + '.' + (n>>>8&0xff) + '.' + (n&0xff); 83 | 84 | return n; 85 | }; 86 | 87 | utils.ntoa6 = function(n) { 88 | var a = "["; 89 | 90 | for (var i = 0; i>>16).toString(16) + ':'; 92 | a += (n[i]&0xffff).toString(16) + ':'; 93 | } 94 | 95 | a = a.replace(/:$/, ']').replace(/:0+/g, ':').replace(/::+/, '::'); 96 | 97 | return a; 98 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "geoip-lite", 3 | "version" : "1.4.10", 4 | "description" : "A light weight native JavaScript implementation of GeoIP API from MaxMind", 5 | "keywords" : ["geo", "geoip", "ip", "ipv4", "ipv6", "geolookup", "maxmind", "geolite"], 6 | "homepage" : "https://github.com/geoip-lite/node-geoip", 7 | "author" : "Philip Tellis (http://bluesmoon.info/)", 8 | "files" : ["lib/", "data/", "test/","scripts/"], 9 | "main" : "lib/geoip.js", 10 | "repository" : { "type": "git", "url": "git://github.com/geoip-lite/node-geoip.git" }, 11 | "engines" : { "node": ">=10.3.0" }, 12 | "scripts": { 13 | "pretest": "eslint .", 14 | "test": "nodeunit --reporter=minimal test/tests.js", 15 | "updatedb": "node scripts/updatedb.js", 16 | "updatedb-debug": "node scripts/updatedb.js debug", 17 | "updatedb-force": "node scripts/updatedb.js force" 18 | }, 19 | "dependencies": { 20 | "async": "2.1 - 2.6.4", 21 | "chalk": "4.1 - 4.1.2", 22 | "iconv-lite": "0.4.13 - 0.6.3", 23 | "ip-address": "5.8.9 - 5.9.4", 24 | "lazy": "1.0.11", 25 | "rimraf": "2.5.2 - 2.7.1", 26 | "yauzl": "2.9.2 - 2.10.0" 27 | }, 28 | "config": { 29 | "update": true 30 | }, 31 | "devDependencies": { 32 | "eslint": "^5.12.1", 33 | "nodeunit": "^0.11.2" 34 | }, 35 | "license": "Apache-2.0" 36 | } 37 | -------------------------------------------------------------------------------- /scripts/updatedb.js: -------------------------------------------------------------------------------- 1 | // fetches and converts maxmind lite databases 2 | 3 | 'use strict'; 4 | 5 | var user_agent = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.36 Safari/537.36'; 6 | 7 | var fs = require('fs'); 8 | var http = require('http'); 9 | var https = require('https'); 10 | var path = require('path'); 11 | var url = require('url'); 12 | var zlib = require('zlib'); 13 | var readline = require('readline'); 14 | 15 | fs.existsSync = fs.existsSync || path.existsSync; 16 | 17 | var async = require('async'); 18 | var chalk = require('chalk'); 19 | var iconv = require('iconv-lite'); 20 | var lazy = require('lazy'); 21 | var rimraf = require('rimraf').sync; 22 | var yauzl = require('yauzl'); 23 | var utils = require('../lib/utils'); 24 | var Address6 = require('ip-address').Address6; 25 | var Address4 = require('ip-address').Address4; 26 | 27 | var args = process.argv.slice(2); 28 | var license_key = args.find(function(arg) { 29 | return arg.match(/^license_key=[a-zA-Z0-9]+/) !== null; 30 | }); 31 | if (typeof license_key === 'undefined' && typeof process.env.LICENSE_KEY !== 'undefined') { 32 | license_key = 'license_key='+process.env.LICENSE_KEY; 33 | } 34 | var geodatadir = args.find(function(arg) { 35 | return arg.match(/^geodatadir=[\w./]+/) !== null; 36 | }); 37 | if (typeof geodatadir === 'undefined' && typeof process.env.GEODATADIR !== 'undefined') { 38 | geodatadir = 'geodatadir='+process.env.GEODATADIR; 39 | } 40 | var dataPath = path.resolve(__dirname, '..', 'data'); 41 | if (typeof geodatadir !== 'undefined') { 42 | dataPath = path.resolve(process.cwd(), geodatadir.split('=')[1]); 43 | if (!fs.existsSync(dataPath)) { 44 | console.log(chalk.red('ERROR') + ': Directory does\'t exist: ' + dataPath); 45 | process.exit(1); 46 | } 47 | } 48 | var tmpPath = process.env.GEOTMPDIR ? process.env.GEOTMPDIR : path.resolve(__dirname, '..', 'tmp'); 49 | var countryLookup = {}; 50 | var cityLookup = {NaN: -1}; 51 | var databases = [ 52 | { 53 | type: 'country', 54 | url: 'https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country-CSV&suffix=zip&'+license_key, 55 | checksum: 'https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country-CSV&suffix=zip.sha256&'+license_key, 56 | fileName: 'GeoLite2-Country-CSV.zip', 57 | src: [ 58 | 'GeoLite2-Country-Locations-en.csv', 59 | 'GeoLite2-Country-Blocks-IPv4.csv', 60 | 'GeoLite2-Country-Blocks-IPv6.csv' 61 | ], 62 | dest: [ 63 | '', 64 | 'geoip-country.dat', 65 | 'geoip-country6.dat' 66 | ] 67 | }, 68 | { 69 | type: 'city', 70 | url: 'https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City-CSV&suffix=zip&'+license_key, 71 | checksum: 'https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City-CSV&suffix=zip.sha256&'+license_key, 72 | fileName: 'GeoLite2-City-CSV.zip', 73 | src: [ 74 | 'GeoLite2-City-Locations-en.csv', 75 | 'GeoLite2-City-Blocks-IPv4.csv', 76 | 'GeoLite2-City-Blocks-IPv6.csv' 77 | ], 78 | dest: [ 79 | 'geoip-city-names.dat', 80 | 'geoip-city.dat', 81 | 'geoip-city6.dat' 82 | ] 83 | } 84 | ]; 85 | 86 | function mkdir(name) { 87 | var dir = path.dirname(name); 88 | if (!fs.existsSync(dir)) { 89 | fs.mkdirSync(dir); 90 | } 91 | } 92 | 93 | // Ref: http://stackoverflow.com/questions/8493195/how-can-i-parse-a-csv-string-with-javascript 94 | // Return array of string values, or NULL if CSV string not well formed. 95 | // Return array of string values, or NULL if CSV string not well formed. 96 | 97 | function try_fixing_line(line) { 98 | var pos1 = 0; 99 | var pos2 = -1; 100 | // escape quotes 101 | line = line.replace(/""/,'\\"').replace(/'/g,"\\'"); 102 | 103 | while(pos1 < line.length && pos2 < line.length) { 104 | pos1 = pos2; 105 | pos2 = line.indexOf(',', pos1 + 1); 106 | if(pos2 < 0) pos2 = line.length; 107 | if(line.indexOf("'", (pos1 || 0)) > -1 && line.indexOf("'", pos1) < pos2 && line[pos1 + 1] != '"' && line[pos2 - 1] != '"') { 108 | line = line.substr(0, pos1 + 1) + '"' + line.substr(pos1 + 1, pos2 - pos1 - 1) + '"' + line.substr(pos2, line.length - pos2); 109 | pos2 = line.indexOf(',', pos2 + 1); 110 | if(pos2 < 0) pos2 = line.length; 111 | } 112 | } 113 | return line; 114 | } 115 | 116 | function CSVtoArray(text) { 117 | var re_valid = /^\s*(?:'[^'\\]*(?:\\[\S\s][^'\\]*)*'|"[^"\\]*(?:\\[\S\s][^"\\]*)*"|[^,'"\s\\]*(?:\s+[^,'"\s\\]+)*)\s*(?:,\s*(?:'[^'\\]*(?:\\[\S\s][^'\\]*)*'|"[^"\\]*(?:\\[\S\s][^"\\]*)*"|[^,'"\s\\]*(?:\s+[^,'"\s\\]+)*)\s*)*$/; 118 | var re_value = /(?!\s*$)\s*(?:'([^'\\]*(?:\\[\S\s][^'\\]*)*)'|"([^"\\]*(?:\\[\S\s][^"\\]*)*)"|([^,'"\s\\]*(?:\s+[^,'"\s\\]+)*))\s*(?:,|$)/g; 119 | // Return NULL if input string is not well formed CSV string. 120 | if (!re_valid.test(text)){ 121 | text = try_fixing_line(text); 122 | if(!re_valid.test(text)) 123 | return null; 124 | } 125 | var a = []; // Initialize array to receive values. 126 | text.replace(re_value, // "Walk" the string using replace with callback. 127 | function(m0, m1, m2, m3) { 128 | // Remove backslash from \' in single quoted values. 129 | if (m1 !== undefined) a.push(m1.replace(/\\'/g, "'")); 130 | // Remove backslash from \" in double quoted values. 131 | else if (m2 !== undefined) a.push(m2.replace(/\\"/g, '"').replace(/\\'/g, "'")); 132 | else if (m3 !== undefined) a.push(m3); 133 | return ''; // Return empty string. 134 | }); 135 | // Handle special case of empty last value. 136 | if (/,\s*$/.test(text)) a.push(''); 137 | return a; 138 | } 139 | 140 | function getHTTPOptions(downloadUrl) { 141 | var options = url.parse(downloadUrl); 142 | options.headers = { 143 | 'User-Agent': user_agent 144 | }; 145 | 146 | if (process.env.http_proxy || process.env.https_proxy) { 147 | try { 148 | var HttpsProxyAgent = require('https-proxy-agent'); 149 | options.agent = new HttpsProxyAgent(process.env.http_proxy || process.env.https_proxy); 150 | } 151 | catch (e) { 152 | console.error("Install https-proxy-agent to use an HTTP/HTTPS proxy"); 153 | process.exit(-1); 154 | } 155 | } 156 | 157 | return options; 158 | } 159 | 160 | function check(database, cb) { 161 | if (args.indexOf("force") !== -1) { 162 | //we are forcing database upgrade, 163 | //so not even using checksums 164 | return cb(null, database); 165 | } 166 | 167 | var checksumUrl = database.checksum; 168 | 169 | if (typeof checksumUrl === "undefined") { 170 | //no checksum url to check, skipping 171 | return cb(null, database); 172 | } 173 | 174 | //read existing checksum file 175 | fs.readFile(path.join(dataPath, database.type+".checksum"), {encoding: 'utf8'}, function(err, data) { 176 | if (!err && data && data.length) { 177 | database.checkValue = data; 178 | } 179 | 180 | console.log('Checking ', database.fileName); 181 | 182 | function onResponse(response) { 183 | var status = response.statusCode; 184 | 185 | if(status === 301 || status === 302 || status === 303 || status === 307 || status === 308) { 186 | return https.get(getHTTPOptions(response.headers.location), onResponse); 187 | } else if (status !== 200) { 188 | console.log(chalk.red('ERROR') + ': HTTP Request Failed [%d %s]', status, http.STATUS_CODES[status]); 189 | client.abort(); 190 | process.exit(1); 191 | } 192 | 193 | var str = ""; 194 | response.on("data", function (chunk) { 195 | str += chunk; 196 | }); 197 | 198 | response.on("end", function () { 199 | if (str && str.length) { 200 | if (str == database.checkValue) { 201 | console.log(chalk.green('Database "' + database.type + '" is up to date')); 202 | database.skip = true; 203 | } 204 | else { 205 | console.log(chalk.green('Database ' + database.type + ' has new data')); 206 | database.checkValue = str; 207 | } 208 | } 209 | else { 210 | console.log(chalk.red('ERROR') + ': Could not retrieve checksum for', database.type, chalk.red('Aborting')); 211 | console.log('Run with "force" to update without checksum'); 212 | client.abort(); 213 | process.exit(1); 214 | } 215 | cb(null, database); 216 | }); 217 | } 218 | 219 | var client = https.get(getHTTPOptions(checksumUrl), onResponse); 220 | }); 221 | } 222 | 223 | function fetch(database, cb) { 224 | 225 | if (database.skip) { 226 | return cb(null, null, null, database); 227 | } 228 | 229 | var downloadUrl = database.url; 230 | var fileName = database.fileName; 231 | var gzip = path.extname(fileName) === '.gz'; 232 | 233 | if (gzip) { 234 | fileName = fileName.replace('.gz', ''); 235 | } 236 | 237 | var tmpFile = path.join(tmpPath, fileName); 238 | 239 | if (fs.existsSync(tmpFile)) { 240 | return cb(null, tmpFile, fileName, database); 241 | } 242 | 243 | console.log('Fetching ', fileName); 244 | 245 | function onResponse(response) { 246 | var status = response.statusCode; 247 | 248 | if(status === 301 || status === 302 || status === 303 || status === 307 || status === 308) { 249 | return https.get(getHTTPOptions(response.headers.location), onResponse); 250 | } else if (status !== 200) { 251 | console.log(chalk.red('ERROR') + ': HTTP Request Failed [%d %s]', status, http.STATUS_CODES[status]); 252 | client.abort(); 253 | process.exit(1); 254 | } 255 | 256 | var tmpFilePipe; 257 | var tmpFileStream = fs.createWriteStream(tmpFile); 258 | 259 | if (gzip) { 260 | tmpFilePipe = response.pipe(zlib.createGunzip()).pipe(tmpFileStream); 261 | } else { 262 | tmpFilePipe = response.pipe(tmpFileStream); 263 | } 264 | 265 | tmpFilePipe.on('close', function() { 266 | console.log(chalk.green(' DONE')); 267 | cb(null, tmpFile, fileName, database); 268 | }); 269 | } 270 | 271 | mkdir(tmpFile); 272 | 273 | var client = https.get(getHTTPOptions(downloadUrl), onResponse); 274 | 275 | process.stdout.write('Retrieving ' + fileName + ' ...'); 276 | } 277 | 278 | function extract(tmpFile, tmpFileName, database, cb) { 279 | if (database.skip) { 280 | return cb(null, database); 281 | } 282 | 283 | if (path.extname(tmpFileName) !== '.zip') { 284 | cb(null, database); 285 | } else { 286 | process.stdout.write('Extracting ' + tmpFileName + ' ...'); 287 | yauzl.open(tmpFile, {autoClose: true, lazyEntries: true}, function(err, zipfile) { 288 | if (err) { 289 | throw err; 290 | } 291 | zipfile.readEntry(); 292 | zipfile.on("entry", function(entry) { 293 | if (/\/$/.test(entry.fileName)) { 294 | // Directory file names end with '/'. 295 | // Note that entries for directories themselves are optional. 296 | // An entry's fileName implicitly requires its parent directories to exist. 297 | zipfile.readEntry(); 298 | } else { 299 | // file entry 300 | zipfile.openReadStream(entry, function(err, readStream) { 301 | if (err) { 302 | throw err; 303 | } 304 | readStream.on("end", function() { 305 | zipfile.readEntry(); 306 | }); 307 | var filePath = entry.fileName.split("/"); 308 | // filePath will always have length >= 1, as split() always returns an array of at least one string 309 | var fileName = filePath[filePath.length - 1]; 310 | readStream.pipe(fs.createWriteStream(path.join(tmpPath, fileName))); 311 | }); 312 | } 313 | }); 314 | zipfile.once("end", function() { 315 | console.log(chalk.green(' DONE')); 316 | cb(null, database); 317 | }); 318 | }); 319 | } 320 | } 321 | 322 | function processLookupCountry(src, cb){ 323 | function processLine(line) { 324 | var fields = CSVtoArray(line); 325 | if (!fields || fields.length < 6) { 326 | console.log("weird line: %s::", line); 327 | return; 328 | } 329 | countryLookup[fields[0]] = fields[4]; 330 | } 331 | var tmpDataFile = path.join(tmpPath, src); 332 | 333 | process.stdout.write('Processing Lookup Data (may take a moment) ...'); 334 | 335 | lazy(fs.createReadStream(tmpDataFile)) 336 | .lines 337 | .map(function(byteArray) { 338 | return iconv.decode(byteArray, 'latin1'); 339 | }) 340 | .skip(1) 341 | .map(processLine) 342 | .on('pipe', function() { 343 | console.log(chalk.green(' DONE')); 344 | cb(); 345 | }); 346 | } 347 | 348 | async function processCountryData(src, dest) { 349 | var lines=0; 350 | async function processLine(line) { 351 | var fields = CSVtoArray(line); 352 | 353 | if (!fields || fields.length < 6) { 354 | console.log("weird line: %s::", line); 355 | return; 356 | } 357 | lines++; 358 | 359 | var sip; 360 | var eip; 361 | var rngip; 362 | var cc = countryLookup[fields[1]]; 363 | var b; 364 | var bsz; 365 | var i; 366 | if(cc){ 367 | if (fields[0].match(/:/)) { 368 | // IPv6 369 | bsz = 34; 370 | rngip = new Address6(fields[0]); 371 | sip = utils.aton6(rngip.startAddress().correctForm()); 372 | eip = utils.aton6(rngip.endAddress().correctForm()); 373 | 374 | b = Buffer.alloc(bsz); 375 | for (i = 0; i < sip.length; i++) { 376 | b.writeUInt32BE(sip[i], i * 4); 377 | } 378 | 379 | for (i = 0; i < eip.length; i++) { 380 | b.writeUInt32BE(eip[i], 16 + (i * 4)); 381 | } 382 | } else { 383 | // IPv4 384 | bsz = 10; 385 | 386 | rngip = new Address4(fields[0]); 387 | sip = parseInt(rngip.startAddress().bigInteger(),10); 388 | eip = parseInt(rngip.endAddress().bigInteger(),10); 389 | 390 | b = Buffer.alloc(bsz); 391 | b.fill(0); 392 | b.writeUInt32BE(sip, 0); 393 | b.writeUInt32BE(eip, 4); 394 | } 395 | 396 | b.write(cc, bsz - 2); 397 | if(Date.now() - tstart > 5000) { 398 | tstart = Date.now(); 399 | process.stdout.write('\nStill working (' + lines + ') ...'); 400 | } 401 | 402 | if(datFile._writableState.needDrain) { 403 | return new Promise((resolve) => { 404 | datFile.write(b, resolve); 405 | }); 406 | } else { 407 | return datFile.write(b); 408 | } 409 | } 410 | } 411 | 412 | var dataFile = path.join(dataPath, dest); 413 | var tmpDataFile = path.join(tmpPath, src); 414 | 415 | rimraf(dataFile); 416 | mkdir(dataFile); 417 | 418 | process.stdout.write('Processing Data (may take a moment) ...'); 419 | var tstart = Date.now(); 420 | var datFile = fs.createWriteStream(dataFile); 421 | 422 | var rl = readline.createInterface({ 423 | input: fs.createReadStream(tmpDataFile), 424 | crlfDelay: Infinity 425 | }); 426 | var i = 0; 427 | for await (var line of rl) { 428 | i++; 429 | if(i == 1) continue; 430 | await processLine(line); 431 | } 432 | datFile.close(); 433 | console.log(chalk.green(' DONE')); 434 | } 435 | 436 | async function processCityData(src, dest) { 437 | var lines = 0; 438 | async function processLine(line) { 439 | if (line.match(/^Copyright/) || !line.match(/\d/)) { 440 | return; 441 | } 442 | 443 | var fields = CSVtoArray(line); 444 | if (!fields) { 445 | console.log("weird line: %s::", line); 446 | return; 447 | } 448 | var sip; 449 | var eip; 450 | var rngip; 451 | var locId; 452 | var b; 453 | var bsz; 454 | var lat; 455 | var lon; 456 | var area; 457 | 458 | var i; 459 | 460 | lines++; 461 | 462 | if (fields[0].match(/:/)) { 463 | // IPv6 464 | var offset = 0; 465 | bsz = 48; 466 | rngip = new Address6(fields[0]); 467 | sip = utils.aton6(rngip.startAddress().correctForm()); 468 | eip = utils.aton6(rngip.endAddress().correctForm()); 469 | locId = parseInt(fields[1], 10); 470 | locId = cityLookup[locId]; 471 | 472 | b = Buffer.alloc(bsz); 473 | b.fill(0); 474 | 475 | for (i = 0; i < sip.length; i++) { 476 | b.writeUInt32BE(sip[i], offset); 477 | offset += 4; 478 | } 479 | 480 | for (i = 0; i < eip.length; i++) { 481 | b.writeUInt32BE(eip[i], offset); 482 | offset += 4; 483 | } 484 | b.writeUInt32BE(locId>>>0, 32); 485 | 486 | lat = Math.round(parseFloat(fields[7]) * 10000); 487 | lon = Math.round(parseFloat(fields[8]) * 10000); 488 | area = parseInt(fields[9], 10); 489 | b.writeInt32BE(lat,36); 490 | b.writeInt32BE(lon,40); 491 | b.writeInt32BE(area,44); 492 | } else { 493 | // IPv4 494 | bsz = 24; 495 | 496 | rngip = new Address4(fields[0]); 497 | sip = parseInt(rngip.startAddress().bigInteger(),10); 498 | eip = parseInt(rngip.endAddress().bigInteger(),10); 499 | locId = parseInt(fields[1], 10); 500 | locId = cityLookup[locId]; 501 | b = Buffer.alloc(bsz); 502 | b.fill(0); 503 | b.writeUInt32BE(sip>>>0, 0); 504 | b.writeUInt32BE(eip>>>0, 4); 505 | b.writeUInt32BE(locId>>>0, 8); 506 | 507 | lat = Math.round(parseFloat(fields[7]) * 10000); 508 | lon = Math.round(parseFloat(fields[8]) * 10000); 509 | area = parseInt(fields[9], 10); 510 | b.writeInt32BE(lat,12); 511 | b.writeInt32BE(lon,16); 512 | b.writeInt32BE(area,20); 513 | } 514 | 515 | if(Date.now() - tstart > 5000) { 516 | tstart = Date.now(); 517 | process.stdout.write('\nStill working (' + lines + ') ...'); 518 | } 519 | 520 | if(datFile._writableState.needDrain) { 521 | return new Promise((resolve) => { 522 | datFile.write(b, resolve); 523 | }); 524 | } else { 525 | return datFile.write(b); 526 | } 527 | } 528 | 529 | var dataFile = path.join(dataPath, dest); 530 | var tmpDataFile = path.join(tmpPath, src); 531 | 532 | rimraf(dataFile); 533 | 534 | process.stdout.write('Processing Data (may take a moment) ...'); 535 | var tstart = Date.now(); 536 | var datFile = fs.createWriteStream(dataFile); 537 | 538 | var rl = readline.createInterface({ 539 | input: fs.createReadStream(tmpDataFile), 540 | crlfDelay: Infinity 541 | }); 542 | var i = 0; 543 | for await (var line of rl) { 544 | i++; 545 | if(i == 1) continue; 546 | await processLine(line); 547 | } 548 | datFile.close(); 549 | } 550 | 551 | function processCityDataNames(src, dest, cb) { 552 | var locId = null; 553 | var linesCount = 0; 554 | function processLine(line) { 555 | if (line.match(/^Copyright/) || !line.match(/\d/)) { 556 | return; 557 | } 558 | 559 | var b; 560 | var sz = 88; 561 | var fields = CSVtoArray(line); 562 | if (!fields) { 563 | //lots of cities contain ` or ' in the name and can't be parsed correctly with current method 564 | console.log("weird line: %s::", line); 565 | return; 566 | } 567 | 568 | locId = parseInt(fields[0]); 569 | 570 | cityLookup[locId] = linesCount; 571 | var cc = fields[4]; 572 | var rg = fields[6]; 573 | var city = fields[10]; 574 | var metro = parseInt(fields[11]); 575 | //other possible fields to include 576 | var tz = fields[12]; 577 | var eu = fields[13]; 578 | 579 | b = Buffer.alloc(sz); 580 | b.fill(0); 581 | b.write(cc, 0);//country code 582 | b.write(rg, 2);//region 583 | 584 | if(metro) { 585 | b.writeInt32BE(metro, 5); 586 | } 587 | b.write(eu,9);//is in eu 588 | b.write(tz,10);//timezone 589 | b.write(city, 42);//cityname 590 | 591 | fs.writeSync(datFile, b, 0, b.length, null); 592 | linesCount++; 593 | } 594 | 595 | var dataFile = path.join(dataPath, dest); 596 | var tmpDataFile = path.join(tmpPath, src); 597 | 598 | rimraf(dataFile); 599 | 600 | var datFile = fs.openSync(dataFile, "w"); 601 | 602 | lazy(fs.createReadStream(tmpDataFile)) 603 | .lines 604 | .map(function(byteArray) { 605 | return iconv.decode(byteArray, 'utf-8'); 606 | }) 607 | .skip(1) 608 | .map(processLine) 609 | .on('pipe', cb); 610 | } 611 | 612 | function processData(database, cb) { 613 | if (database.skip) { 614 | return cb(null, database); 615 | } 616 | 617 | var type = database.type; 618 | var src = database.src; 619 | var dest = database.dest; 620 | 621 | if (type === 'country') { 622 | if(Array.isArray(src)){ 623 | processLookupCountry(src[0], function() { 624 | processCountryData(src[1], dest[1]).then(() => { 625 | return processCountryData(src[2], dest[2]); 626 | }).then(() => { 627 | cb(null, database); 628 | }); 629 | }); 630 | } 631 | else{ 632 | processCountryData(src, dest, function() { 633 | cb(null, database); 634 | }); 635 | } 636 | } else if (type === 'city') { 637 | processCityDataNames(src[0], dest[0], function() { 638 | processCityData(src[1], dest[1]).then(() => { 639 | console.log("city data processed"); 640 | return processCityData(src[2], dest[2]); 641 | }).then(() => { 642 | console.log(chalk.green(' DONE')); 643 | cb(null, database); 644 | }); 645 | }); 646 | } 647 | } 648 | 649 | function updateChecksum(database, cb) { 650 | if (database.skip || !database.checkValue) { 651 | //don't need to update checksums cause it was not fetched or did not change 652 | return cb(); 653 | } 654 | fs.writeFile(path.join(dataPath, database.type+".checksum"), database.checkValue, 'utf8', function(err){ 655 | if (err) console.log(chalk.red('Failed to Update checksums.'), "Database:", database.type); 656 | cb(); 657 | }); 658 | } 659 | 660 | if (!license_key) { 661 | console.log(chalk.red('ERROR') + ': Missing license_key'); 662 | process.exit(1); 663 | } 664 | 665 | rimraf(tmpPath); 666 | mkdir(tmpPath); 667 | 668 | async.eachSeries(databases, function(database, nextDatabase) { 669 | 670 | async.seq(check, fetch, extract, processData, updateChecksum)(database, nextDatabase); 671 | 672 | }, function(err) { 673 | if (err) { 674 | console.log(chalk.red('Failed to Update Databases from MaxMind.'), err); 675 | process.exit(1); 676 | } else { 677 | console.log(chalk.green('Successfully Updated Databases from MaxMind.')); 678 | if (args.indexOf("debug") !== -1) { 679 | console.log(chalk.yellow.bold('Notice: temporary files are not deleted for debug purposes.')); 680 | } else { 681 | rimraf(tmpPath); 682 | } 683 | process.exit(0); 684 | } 685 | }); 686 | -------------------------------------------------------------------------------- /test/geo-lookup.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var t1 =+ new Date(); 3 | var geoip = require('../lib/geoip'); 4 | var t2 =+ new Date(); 5 | 6 | if (process.argv.length > 2) { 7 | console.dir(geoip.lookup(process.argv[2])); 8 | var t3 =+ new Date(); 9 | console.log('Startup: %dms, exec: %dms', t2 - t1, t3 - t2); 10 | process.exit(); 11 | } 12 | 13 | var f = []; 14 | var ip; 15 | var n = 30000; 16 | var nf = []; 17 | var r; 18 | var ts =+ new Date(); 19 | 20 | for (var i = 0; i < n; i++) { 21 | if ((i % 2) === 0) { 22 | ip = Math.round((Math.random() * 0xff000000)+ 0xffffff); 23 | } else { 24 | ip = '2001:' + 25 | Math.round(Math.random()*0xffff).toString(16) + ':' + 26 | Math.round(Math.random()*0xffff).toString(16) + ':' + 27 | Math.round(Math.random()*0xffff).toString(16) + ':' + 28 | Math.round(Math.random()*0xffff).toString(16) + ':' + 29 | Math.round(Math.random()*0xffff).toString(16) + ':' + 30 | Math.round(Math.random()*0xffff).toString(16) + ':' + 31 | Math.round(Math.random()*0xffff).toString(16) + ''; 32 | } 33 | 34 | r = geoip.lookup(ip); 35 | 36 | if (r === null) { 37 | nf.push(ip); 38 | continue; 39 | } 40 | 41 | f.push([ip, r]); 42 | 43 | assert.ok(geoip.cmp(ip, r.range[0]) >= 0 , 'Problem with ' + geoip.pretty(ip) + ' < ' + geoip.pretty(r.range[0])); 44 | assert.ok(geoip.cmp(ip, r.range[1]) <= 0 , 'Problem with ' + geoip.pretty(ip) + ' > ' + geoip.pretty(r.range[1])); 45 | } 46 | 47 | var te =+ new Date(); 48 | 49 | /* 50 | f.forEach(function(ip) { 51 | console.log("%s bw %s & %s is %s", geoip.pretty(ip[0]), geoip.pretty(ip[1].range[0]), geoip.pretty(ip[1].range[1]), ip[1].country); 52 | }); 53 | */ 54 | 55 | console.log("Found %d (%d/%d) ips in %dms (%s ip/s) (%sμs/ip)", n, f.length, nf.length, te-ts, (n*1000 / (te-ts)).toFixed(3), ((te-ts) * 1000 / n).toFixed(0)); 56 | console.log("Took %d ms to startup", t2 - t1); -------------------------------------------------------------------------------- /test/memory_usage.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | var geoip = require('../lib/geoip'); 3 | console.log(process.memoryUsage()); -------------------------------------------------------------------------------- /test/tests.js: -------------------------------------------------------------------------------- 1 | var geoip = require('../lib/geoip'); 2 | 3 | module.exports = { 4 | testLookup: function(test) { 5 | test.expect(2); 6 | 7 | var ip = '8.8.4.4'; 8 | var ipv6 = '2001:4860:b002::68'; 9 | 10 | var actual = geoip.lookup(ip); 11 | 12 | test.ok(actual, 'should return data about IPv4.'); 13 | 14 | actual = geoip.lookup(ipv6); 15 | 16 | test.ok(actual, 'should return data about IPv6.'); 17 | 18 | test.done(); 19 | }, 20 | 21 | testDataIP4: function(test) { 22 | test.expect(9); 23 | 24 | var ip = '72.229.28.185'; 25 | 26 | var actual = geoip.lookup(ip); 27 | 28 | test.notStrictEqual(actual.range, undefined, 'should contain IPv4 range'); 29 | 30 | test.strictEqual(actual.country, 'US', "should match country"); 31 | 32 | test.strictEqual(actual.region, 'NY', "should match region"); 33 | 34 | test.strictEqual(actual.eu, '0', "should match eu"); 35 | 36 | test.strictEqual(actual.timezone, 'America/New_York', "should match timezone"); 37 | 38 | test.strictEqual(actual.city, 'New York', "should match city"); 39 | 40 | test.ok(actual.ll, 'should contain coordinates'); 41 | 42 | test.strictEqual(actual.metro, 501, "should match metro"); 43 | 44 | test.strictEqual(actual.area, 5, "should match area"); 45 | 46 | test.done(); 47 | }, 48 | 49 | testDataIP6: function(test) { 50 | test.expect(9); 51 | 52 | var ipv6 = '2001:1c04:400::1'; 53 | 54 | var actual = geoip.lookup(ipv6); 55 | 56 | test.notStrictEqual(actual.range, undefined, 'should contain IPv6 range'); 57 | 58 | test.strictEqual(actual.country, 'NL', "should match country"); 59 | 60 | test.strictEqual(actual.region, 'NH', "should match region"); 61 | 62 | test.strictEqual(actual.eu, '1', "should match eu"); 63 | 64 | test.strictEqual(actual.timezone, 'Europe/Amsterdam', "should match timezone"); 65 | 66 | test.strictEqual(actual.city, 'Zandvoort', "should match city"); 67 | 68 | test.ok(actual.ll, 'should contain coordinates'); 69 | 70 | test.strictEqual(actual.metro, 0, "should match metro"); 71 | 72 | test.strictEqual(actual.area, 5, "should match area"); 73 | 74 | test.done(); 75 | }, 76 | 77 | testUTF8: function(test) { 78 | test.expect(2); 79 | 80 | var ip = "2.139.175.1"; 81 | var expected = "Madrid"; 82 | var actual = geoip.lookup(ip); 83 | 84 | test.ok(actual, "Should return a non-null value for " + ip); 85 | test.equal(actual.city, expected, "UTF8 city name does not match"); 86 | 87 | test.done(); 88 | }, 89 | 90 | testMetro: function(test) { 91 | test.expect(2); 92 | 93 | var actual = geoip.lookup("23.240.63.68"); 94 | 95 | test.equal(actual.city, "Riverside"); //keeps changing with each update from one city to other (close to each other geographically) 96 | test.equal(actual.metro, 803); 97 | 98 | test.done(); 99 | }, 100 | 101 | testIPv4MappedIPv6: function (test) { 102 | test.expect(2); 103 | 104 | var actual = geoip.lookup("195.16.170.74"); 105 | 106 | test.equal(actual.city, ""); 107 | test.equal(actual.metro, 0); 108 | 109 | test.done(); 110 | }, 111 | 112 | testSyncReload: function (test) { 113 | test.expect(6); 114 | 115 | //get original data 116 | var before4 = geoip.lookup("75.82.117.180"); 117 | test.notEqual(before4, null); 118 | 119 | var before6 = geoip.lookup("::ffff:173.185.182.82"); 120 | test.notEqual(before6, null); 121 | 122 | //clear data; 123 | geoip.clear(); 124 | 125 | //make sure data is cleared 126 | var none4 = geoip.lookup("75.82.117.180"); 127 | test.equal(none4, null); 128 | var none6 = geoip.lookup("::ffff:173.185.182.82"); 129 | test.equal(none6, null); 130 | 131 | //reload data synchronized 132 | geoip.reloadDataSync(); 133 | 134 | //make sure we have value from before 135 | var after4 = geoip.lookup("75.82.117.180"); 136 | test.deepEqual(before4, after4); 137 | var after6 = geoip.lookup("::ffff:173.185.182.82"); 138 | test.deepEqual(before6, after6); 139 | 140 | test.done(); 141 | }, 142 | 143 | testAsyncReload: function (test) { 144 | test.expect(6); 145 | 146 | //get original data 147 | var before4 = geoip.lookup("75.82.117.180"); 148 | test.notEqual(before4, null); 149 | var before6 = geoip.lookup("::ffff:173.185.182.82"); 150 | test.notEqual(before6, null); 151 | 152 | //clear data; 153 | geoip.clear(); 154 | 155 | //make sure data is cleared 156 | var none4 = geoip.lookup("75.82.117.180"); 157 | test.equal(none4, null); 158 | var none6 = geoip.lookup("::ffff:173.185.182.82"); 159 | test.equal(none6, null); 160 | 161 | //reload data asynchronously 162 | geoip.reloadData(function(){ 163 | //make sure we have value from before 164 | var after4 = geoip.lookup("75.82.117.180"); 165 | test.deepEqual(before4, after4); 166 | var after6 = geoip.lookup("::ffff:173.185.182.82"); 167 | test.deepEqual(before6, after6); 168 | 169 | test.done(); 170 | }); 171 | }, 172 | 173 | testUnassigned: function (test) { 174 | test.expect(8); 175 | 176 | var ip = '1.1.1.1'; 177 | 178 | var actual = geoip.lookup(ip); 179 | 180 | test.notStrictEqual(actual.range, undefined, 'should contain IPv4 range'); 181 | 182 | test.strictEqual(actual.country, '', "should match empty country"); 183 | 184 | test.strictEqual(actual.region, '', "should match empty region"); 185 | 186 | test.strictEqual(actual.eu, '', "should match empty eu"); 187 | 188 | test.strictEqual(actual.timezone, '', "should match empty timezone"); 189 | 190 | test.strictEqual(actual.city, '', "should match empty city"); 191 | 192 | test.strictEqual(actual.ll[0], null, 'should contain empty coordinates'); 193 | test.strictEqual(actual.ll[1], null, 'should contain empty coordinates'); 194 | 195 | test.done(); 196 | } 197 | }; 198 | --------------------------------------------------------------------------------