├── dbs └── .gitkeep ├── .npmrc ├── .prettierrc ├── .gitattributes ├── scripts ├── test └── postinstall.js ├── renovate.json ├── test └── index.js ├── .github └── workflows │ ├── build.yml │ └── release.yml ├── index.js ├── .gitignore ├── LICENSE ├── package.json ├── README.md └── utils.js /dbs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.mmdb filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mocha \ 4 | --check-leaks \ 5 | --bail \ 6 | --grep ${npm_config_grep:-''} \ 7 | --recursive \ 8 | --timeout 1s \ 9 | --inline-diffs \ 10 | test 11 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base", ":disableDependencyDashboard"], 3 | "schedule": "on the first day of the month", 4 | "packageRules": [ 5 | { 6 | "updateTypes": ["minor", "patch", "pin", "digest"], 7 | "automerge": true 8 | }, 9 | { 10 | "depTypeList": ["devDependencies"], 11 | "automerge": true 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const fs = require('fs'); 3 | const geolite2 = require('../'); 4 | 5 | describe('geolite2', function () { 6 | const keys = [ 7 | 'GeoLite2-ASN', 8 | 'GeoLite2-City', 9 | 'GeoLite2-Country', 10 | // Legacy aliases: 11 | 'city', 12 | 'country', 13 | 'asn', 14 | ]; 15 | 16 | keys.forEach((key) => 17 | it(`should return a database path for ${key}`, () => { 18 | var stat = fs.statSync(geolite2.paths[key]); 19 | assert(stat.size > 1e6); 20 | assert(stat.ctime); 21 | }), 22 | ); 23 | }); 24 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: ['*'] 8 | # types: [opened, synchronize] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [18.x, 20.x, 22.x] 17 | 18 | steps: 19 | - uses: actions/checkout@v5 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v6 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - run: npm i 25 | env: 26 | MAXMIND_LICENSE_KEY: ${{ secrets.MAXMIND_LICENSE_KEY }} 27 | - run: npm run format:check 28 | - run: npm test 29 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const { getSelectedDbs } = require('./utils'); 4 | const selected = getSelectedDbs(); 5 | 6 | const makePath = (edition) => path.resolve(__dirname, `dbs/${edition}.mmdb`); 7 | 8 | const paths = selected.reduce((a, c) => { 9 | const aliases = { 10 | 'GeoLite2-ASN': 'asn', 11 | 'GeoLite2-City': 'city', 12 | 'GeoLite2-Country': 'country', 13 | }; 14 | // The keys are the database names. 15 | a[c] = makePath(c); 16 | // For backward compatibility, we also populate the 'city', 'asn', and 17 | // 'country' keys for GeoLite databases. 18 | if (c in aliases) { 19 | a[aliases[c]] = makePath(c); 20 | } 21 | return a; 22 | }, {}); 23 | 24 | module.exports = { 25 | paths, 26 | }; 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | release: 8 | name: Release 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v5 13 | with: 14 | fetch-depth: 0 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v6 17 | with: 18 | node-version: 24 19 | - name: Install dependencies 20 | run: npm i 21 | env: 22 | MAXMIND_LICENSE_KEY: ${{ secrets.MAXMIND_LICENSE_KEY }} 23 | - name: Release 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 27 | run: npx semantic-release 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | package-lock.json 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | dbs/* 40 | 41 | .DS_Store 42 | .vscode 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2025 Dmitry Shirokov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "geolite2", 3 | "version": "0.0.0-development", 4 | "description": "Maxmind's GeoLite2 Free Databases", 5 | "main": "index.js", 6 | "keywords": [ 7 | "maxmind", 8 | "mmdb", 9 | "geo", 10 | "geoip", 11 | "geoip2", 12 | "geobase", 13 | "geo lookup" 14 | ], 15 | "scripts": { 16 | "postinstall": "node scripts/postinstall.js", 17 | "test": "scripts/test", 18 | "semantic-release": "semantic-release", 19 | "format": "prettier --write .", 20 | "format:check": "prettier --write ." 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/runk/node-geolite2.git" 25 | }, 26 | "author": "Dmitry Shirokov ", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/runk/node-geolite2/issues" 30 | }, 31 | "homepage": "https://github.com/runk/node-geolite2#readme", 32 | "dependencies": { 33 | "node-fetch": "^2.7.0", 34 | "tar": "^7.0.0" 35 | }, 36 | "devDependencies": { 37 | "mocha": "^11.0.0", 38 | "prettier": "^3.0.0", 39 | "semantic-release": "^25.0.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-geolite2 2 | 3 | Maxmind's GeoLite2 Free Databases download helper. Also supports Maxmind's paid GeoIP2 databases. 4 | 5 | ## Configuration 6 | 7 | ### Access Key 8 | 9 | **IMPORTANT** You must set up `MAXMIND_ACCOUNT_ID` and `MAXMIND_LICENSE_KEY` environment variables to be able to download databases. To do so, go to the https://www.maxmind.com/en/geolite2/signup, create a free account and generate new license key. 10 | 11 | If you don't have access to the environment variables during installation, you can provide config via `package.json`: 12 | 13 | ```jsonc 14 | { 15 | ... 16 | "geolite2": { 17 | // specify the account id 18 | "account-id": "", 19 | // specify the key 20 | "license-key": "", 21 | // ... or specify the file where key is located: 22 | "license-file": "maxmind-license.key" 23 | } 24 | ... 25 | } 26 | ``` 27 | 28 | Beware of security risks of adding keys and secrets to your repository! 29 | 30 | **Note:** For backwards compatibility, the account ID is currently optional. When not provided we fall back to using legacy Maxmind download URLs with only the license key. However, this behavior may become unsupported in the future so adding an account ID is recommended. 31 | 32 | ### Selecting databases to download 33 | 34 | You can select the dbs you want downloaded by adding a `selected-dbs` property on `geolite2` via `package.json`. 35 | 36 | `selected-dbs` must be an array of edition IDs. For example, `GeoIP2-Enterprise`, `GeoIP2-City`, `GeoLite2-City`, etc (downloading paid databases requires an active subscription). 37 | 38 | If `selected-dbs` is unset, or is set but empty, all the free GeoLite dbs will be downloaded. 39 | 40 | ```jsonc 41 | { 42 | ... 43 | "geolite2": { 44 | "selected-dbs": ["GeoLite2-City", "GeoLite2-Country", "GeoLite2-ASN"] 45 | } 46 | ... 47 | } 48 | ``` 49 | 50 | ## Usage 51 | 52 | ```javascript 53 | var geolite2 = require('geolite2'); 54 | var maxmind = require('maxmind'); 55 | 56 | // The database paths are available under geolite2.paths using the full edition 57 | // ID, e.g. geolite2.paths['GeoLite2-ASN'] 58 | var lookup = maxmind.openSync(geolite2.paths['GeoLite2-City']); 59 | var city = lookup.get('66.6.44.4'); 60 | ``` 61 | 62 | ## Alternatives 63 | 64 | [geolite2-redist](https://github.com/GitSquared/node-geolite2-redist) provides redistribution which does not require personal license key. Make sure you understand legal terms and what they mean for your use-case. 65 | 66 | ## License 67 | 68 | Creative Commons Attribution-ShareAlike 4.0 International License 69 | -------------------------------------------------------------------------------- /scripts/postinstall.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const zlib = require('zlib'); 3 | const tar = require('tar'); 4 | const path = require('path'); 5 | const fetch = require('node-fetch'); 6 | 7 | const { getAccountId, getLicense, getSelectedDbs } = require('../utils'); 8 | 9 | let licenseKey; 10 | try { 11 | licenseKey = getLicense(); 12 | } catch (e) { 13 | console.error('geolite2: Error retrieving Maxmind License Key'); 14 | console.error(e.message); 15 | } 16 | 17 | let accountId; 18 | try { 19 | accountId = getAccountId(); 20 | } catch (e) { 21 | console.error('geolite2: Error retrieving Maxmind Account ID'); 22 | console.error(e.message); 23 | } 24 | 25 | if (!licenseKey) { 26 | console.error(`Error: License Key is not configured.\n 27 | You need to signup for a _free_ Maxmind account to get a license key. 28 | Go to https://www.maxmind.com/en/geolite2/signup, obtain your account ID and 29 | license key and put them in the MAXMIND_ACCOUNT_ID and MAXMIND_LICENSE_KEY 30 | environment variables. 31 | 32 | If you do not have access to env vars, put this config in your package.json 33 | file (at the root level) like this: 34 | 35 | "geolite2": { 36 | // specify the account id 37 | "account-id": "", 38 | // specify the key 39 | "license-key": "", 40 | // ... or specify the file where key is located: 41 | "license-file": "maxmind-licence.key" 42 | } 43 | `); 44 | process.exit(1); 45 | } 46 | 47 | // If an account ID is set, use the new URL path with Basic auth. 48 | // Otherwise, fall back to the legacy URL path with license key as query parameter. 49 | const link = (edition) => 50 | accountId 51 | ? `https://download.maxmind.com/geoip/databases/${edition}/download?suffix=tar.gz` 52 | : `https://download.maxmind.com/app/geoip_download?edition_id=${edition}&license_key=${licenseKey}&suffix=tar.gz`; 53 | 54 | const editionIds = getSelectedDbs(); 55 | 56 | const downloadPath = path.join(__dirname, '..', 'dbs'); 57 | 58 | if (!fs.existsSync(downloadPath)) fs.mkdirSync(downloadPath); 59 | 60 | const request = async (url, options) => { 61 | const response = await fetch(url, { 62 | headers: accountId 63 | ? { 64 | Authorization: `Basic ${Buffer.from( 65 | `${accountId}:${licenseKey}` 66 | ).toString('base64')}`, 67 | } 68 | : undefined, 69 | redirect: 'follow', 70 | ...options, 71 | }); 72 | 73 | if (!response.ok) { 74 | throw new Error( 75 | `Failed to fetch ${url}: ${response.status} ${response.statusText}` 76 | ); 77 | } 78 | 79 | return response; 80 | }; 81 | 82 | // https://dev.maxmind.com/geoip/updating-databases?lang=en#checking-for-the-latest-release-date 83 | const isOutdated = async (dbPath, url) => { 84 | if (!fs.existsSync(dbPath)) return true; 85 | 86 | const response = await request(url, { method: 'HEAD' }); 87 | const remoteLastModified = Date.parse(response.headers['last-modified']); 88 | const localLastModified = fs.statSync(dbPath).mtimeMs; 89 | 90 | return localLastModified < remoteLastModified; 91 | }; 92 | 93 | const main = async () => { 94 | console.log('Downloading maxmind databases...'); 95 | for (const editionId of editionIds) { 96 | const dbPath = path.join(downloadPath, `${editionId}.mmdb`); 97 | const isOutdatedStatus = await isOutdated(dbPath, link(editionId)); 98 | 99 | if (!isOutdatedStatus) { 100 | console.log(' > %s: Is up to date, skipping download', editionId); 101 | continue; 102 | } 103 | console.log(' > %s: Is either missing or outdated, downloading', editionId); 104 | 105 | const response = await request(link(editionId)); 106 | const entryPromises = []; 107 | await new Promise((resolve, reject) => 108 | response.body 109 | .pipe(zlib.createGunzip()) 110 | .pipe(tar.t()) 111 | .on('entry', (entry) => { 112 | if (entry.path.endsWith('.mmdb')) { 113 | const dstFilename = path.join( 114 | downloadPath, 115 | path.basename(entry.path) 116 | ); 117 | console.log(`writing ${dstFilename} ...`); 118 | entryPromises.push( 119 | new Promise((resolve, reject) => { 120 | entry 121 | .pipe(fs.createWriteStream(dstFilename)) 122 | .on('finish', resolve) 123 | .on('error', reject); 124 | }) 125 | ); 126 | } 127 | }) 128 | .on('end', resolve) 129 | .on('error', reject) 130 | ); 131 | await Promise.all(entryPromises); 132 | } 133 | }; 134 | 135 | main() 136 | .then(() => { 137 | // success 138 | process.exit(0); 139 | }) 140 | .catch((err) => { 141 | console.error(err); 142 | process.exit(1); 143 | }); 144 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | 4 | const getConfigWithDir = () => { 5 | const cwd = process.env['INIT_CWD'] || process.cwd(); 6 | let dir = cwd; 7 | 8 | // Find a package.json with geolite2 configuration key at or above the level 9 | // of this directory. 10 | while (fs.existsSync(dir)) { 11 | const packageJSON = path.join(dir, 'package.json'); 12 | if (fs.existsSync(packageJSON)) { 13 | const contents = require(packageJSON); 14 | const config = contents['geolite2']; 15 | if (config) return { config, dir }; 16 | } 17 | 18 | const parentDir = path.resolve(dir, '..'); 19 | if (parentDir === dir) break; 20 | dir = parentDir; 21 | } 22 | 23 | console.log( 24 | "INFO: geolite2 cannot find configuration in package.json file, using defaults.\n" + 25 | "INFO: geolite2 expects to have 'MAXMIND_ACCOUNT_ID' and 'MAXMIND_LICENSE_KEY' to be present in environment variables when package.json is unavailable.", 26 | ); 27 | console.log( 28 | 'INFO: geolite2 expected package.json to be present at a parent of:\n%s', 29 | cwd 30 | ); 31 | }; 32 | 33 | const getConfig = () => { 34 | const configWithDir = getConfigWithDir(); 35 | if (!configWithDir) return; 36 | return configWithDir.config; 37 | }; 38 | 39 | const getAccountId = () => { 40 | const envId = process.env.MAXMIND_ACCOUNT_ID; 41 | if (envId) return envId; 42 | 43 | const config = getConfig(); 44 | if (!config) return; 45 | 46 | return config['account-id']; 47 | } 48 | 49 | const getLicense = () => { 50 | const envKey = process.env.MAXMIND_LICENSE_KEY; 51 | if (envKey) return envKey; 52 | 53 | const configWithDir = getConfigWithDir(); 54 | if (!configWithDir) return; 55 | 56 | const { config, dir } = configWithDir; 57 | 58 | const licenseKey = config['license-key']; 59 | if (licenseKey) return licenseKey; 60 | 61 | const configFile = config['license-file']; 62 | if (!configFile) return; 63 | 64 | const configFilePath = path.join(dir, configFile); 65 | return fs.existsSync(configFilePath) 66 | ? fs.readFileSync(configFilePath, 'utf8').trim() 67 | : undefined; 68 | }; 69 | 70 | const getSelectedDbs = () => { 71 | const aliases = ['ASN', 'City', 'Country']; 72 | const defaultEditions = ['GeoLite2-ASN', 'GeoLite2-City', 'GeoLite2-Country']; 73 | const validEditions = [ 74 | 'GeoIP-Anonymous-Plus', 75 | 'GeoIP-Network-Optimization-City', 76 | 'GeoIP2-Anonymous-IP', 77 | 'GeoIP2-City', 78 | 'GeoIP2-City-Africa', 79 | 'GeoIP2-City-Asia-Pacific', 80 | 'GeoIP2-City-Europe', 81 | 'GeoIP2-City-North-America', 82 | 'GeoIP2-City-Shield', 83 | 'GeoIP2-City-South-America', 84 | 'GeoIP2-Connection-Type', 85 | 'GeoIP2-Country', 86 | 'GeoIP2-Country-Shield', 87 | 'GeoIP2-DensityIncome', 88 | 'GeoIP2-Domain', 89 | 'GeoIP2-Enterprise', 90 | 'GeoIP2-Enterprise-Shield', 91 | 'GeoIP2-IP-Risk', 92 | 'GeoIP2-ISP', 93 | 'GeoIP2-Precision-Enterprise', 94 | 'GeoIP2-Precision-Enterprise-Shield', 95 | 'GeoIP2-Static-IP-Score', 96 | 'GeoIP2-User-Connection-Type', 97 | 'GeoIP2-User-Count', 98 | 'GeoLite2-ASN', 99 | 'GeoLite2-City', 100 | 'GeoLite2-Country', 101 | ]; 102 | 103 | const config = getConfig(); 104 | const selectedWithPossibleAliases = 105 | config != null && config['selected-dbs'] != null 106 | ? config['selected-dbs'] 107 | : defaultEditions; 108 | 109 | if (!Array.isArray(selectedWithPossibleAliases)) { 110 | console.error('selected-dbs property must be an array.'); 111 | process.exit(1); 112 | } 113 | 114 | if (selectedWithPossibleAliases.length === 0) return defaultEditions; 115 | 116 | const selectedEditions = selectedWithPossibleAliases.map((element) => { 117 | const index = aliases.indexOf(element); 118 | 119 | if (index > -1) { 120 | return `GeoLite2-${element}`; 121 | } 122 | 123 | return element; 124 | }); 125 | 126 | const validValuesText = validEditions.join(', '); 127 | if (selectedEditions.length > validEditions.length) { 128 | console.error( 129 | 'Property selected-dbs has too many values, there are only %d valid values: %s', 130 | validEditions.length, 131 | validValuesText, 132 | ); 133 | process.exit(1); 134 | } 135 | 136 | for (const value of selectedEditions) { 137 | const index = validEditions.indexOf(value); 138 | if (index === -1) { 139 | console.error( 140 | 'Invalid value in selected-dbs: %s The only valid values are: %s', 141 | value, 142 | validValuesText, 143 | ); 144 | process.exit(1); 145 | } 146 | } 147 | 148 | return selectedEditions; 149 | }; 150 | 151 | module.exports = { 152 | getConfig, 153 | getAccountId, 154 | getLicense, 155 | getSelectedDbs, 156 | }; 157 | --------------------------------------------------------------------------------