Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 21 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

Maxmind's GeoLite2 Free Databases download helper. Also supports Maxmind's paid GeoIP2 databases.

Requires Node 20+ and ships with TypeScript definitions.

## Configuration

### Access Key
Expand All @@ -12,22 +14,22 @@ If you don't have access to the environment variables during installation, you c

```jsonc
{
...
// ...
"geolite2": {
// specify the account id
"account-id": "<your account id>",
// specify the key
"license-key": "<your license key>",
// ... or specify the file where key is located:
"license-file": "maxmind-license.key"
}
...
"license-file": "maxmind-license.key",
},
// ...
}
```

Beware of security risks of adding keys and secrets to your repository!

**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.
**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 behaviour may become unsupported in the future so adding an account ID is recommended.

### Selecting databases to download

Expand All @@ -39,24 +41,30 @@ If `selected-dbs` is unset, or is set but empty, all the free GeoLite dbs will b

```jsonc
{
...
// ...
"geolite2": {
"selected-dbs": ["GeoLite2-City", "GeoLite2-Country", "GeoLite2-ASN"]
}
...
"selected-dbs": ["GeoLite2-City", "GeoLite2-Country", "GeoLite2-ASN"],
},
// ...
}
```

## Usage

```javascript
var geolite2 = require('geolite2');
var maxmind = require('maxmind');
import geolite2 from 'geolite2';
import maxmind from 'maxmind';

// The database paths are available under geolite2.paths using the full edition
// ID, e.g. geolite2.paths['GeoLite2-ASN']
var lookup = maxmind.openSync(geolite2.paths['GeoLite2-City']);
var city = lookup.get('66.6.44.4');
const lookup = maxmind.openSync(geolite2.paths['GeoLite2-City']);
const city = lookup.get('66.6.44.4');
```

Named import is also supported:

```javascript
import { paths } from 'geolite2';
```

## Alternatives
Expand Down
17 changes: 17 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export interface Geolite2Paths {
'GeoLite2-ASN'?: string;
'GeoLite2-City'?: string;
'GeoLite2-Country'?: string;
asn?: string;
city?: string;
country?: string;
[editionId: string]: string | undefined;
}

export declare const paths: Geolite2Paths;

declare const geolite2: {
paths: Geolite2Paths;
};

export default geolite2;
27 changes: 1 addition & 26 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,26 +1 @@
const path = require('path');

const { getSelectedDbs } = require('./utils');
const selected = getSelectedDbs();

const makePath = (edition) => path.resolve(__dirname, `dbs/${edition}.mmdb`);

const paths = selected.reduce((a, c) => {
const aliases = {
'GeoLite2-ASN': 'asn',
'GeoLite2-City': 'city',
'GeoLite2-Country': 'country',
};
// The keys are the database names.
a[c] = makePath(c);
// For backward compatibility, we also populate the 'city', 'asn', and
// 'country' keys for GeoLite databases.
if (c in aliases) {
a[aliases[c]] = makePath(c);
}
return a;
}, {});

module.exports = {
paths,
};
export { default, paths } from './src/index.js';
13 changes: 11 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@
"name": "geolite2",
"version": "0.0.0-development",
"description": "Maxmind's GeoLite2 Free Databases",
"main": "index.js",
"type": "module",
"types": "./index.d.ts",
"exports": {
".": {
"types": "./index.d.ts",
"default": "./index.js"
}
},
"engines": {
"node": ">=20"
},
"keywords": [
"maxmind",
"mmdb",
Expand Down Expand Up @@ -30,7 +40,6 @@
},
"homepage": "https://github.com/runk/node-geolite2#readme",
"dependencies": {
"node-fetch": "^2.7.0",
"tar": "^7.0.0"
},
"devDependencies": {
Expand Down
59 changes: 37 additions & 22 deletions scripts/postinstall.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,50 @@
const fs = require('fs');
const zlib = require('zlib');
const tar = require('tar');
const path = require('path');
const fetch = require('node-fetch');
import fs from 'node:fs';
import path from 'node:path';
import { Readable } from 'node:stream';
import { fileURLToPath } from 'node:url';
import zlib from 'node:zlib';

import * as tar from 'tar';

import {
getSelectedDbs,
} from '../src/databases.js';
import {
getAccountId,
getLicense,
maskLicenseKey,
} from '../src/config.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const { getAccountId, getLicense, getSelectedDbs } = require('../utils');

let licenseKey;
let accountId;
try {
licenseKey = getLicense();
accountId = getAccountId();
console.log('geolite2: Using Maxmind Account ID: %s', accountId);
} catch (e) {
console.error('geolite2: Error retrieving Maxmind License Key');
console.error('geolite2: Error retrieving Maxmind Account ID');
console.error(e.message);
}

let accountId;
let licenseKey;
try {
accountId = getAccountId();
licenseKey = getLicense();
console.log('geolite2: Using Maxmind License Key: %s', maskLicenseKey(licenseKey));
} catch (e) {
console.error('geolite2: Error retrieving Maxmind Account ID');
console.error('geolite2: Error retrieving Maxmind License Key');
console.error(e.message);
}


if (!licenseKey) {
console.error(`Error: License Key is not configured.\n
You need to signup for a _free_ Maxmind account to get a license key.
Go to https://www.maxmind.com/en/geolite2/signup, obtain your account ID and
license key and put them in the MAXMIND_ACCOUNT_ID and MAXMIND_LICENSE_KEY
environment variables.

If you do not have access to env vars, put this config in your package.json
If you do not have access to env variables, put this config in your package.json
file (at the root level) like this:

"geolite2": {
Expand Down Expand Up @@ -62,7 +77,7 @@ const request = async (url, options) => {
headers: accountId
? {
Authorization: `Basic ${Buffer.from(
`${accountId}:${licenseKey}`
`${accountId}:${licenseKey}`,
).toString('base64')}`,
}
: undefined,
Expand All @@ -72,7 +87,7 @@ const request = async (url, options) => {

if (!response.ok) {
throw new Error(
`Failed to fetch ${url}: ${response.status} ${response.statusText}`
`Failed to fetch ${url}: ${response.status} ${response.statusText}`,
);
}

Expand All @@ -84,9 +99,10 @@ const isOutdated = async (dbPath, url) => {
if (!fs.existsSync(dbPath)) return true;

const response = await request(url, { method: 'HEAD' });
const remoteLastModified = Date.parse(response.headers['last-modified']);
const remoteLastModified = Date.parse(response.headers.get('last-modified'));
const localLastModified = fs.statSync(dbPath).mtimeMs;

if (Number.isNaN(remoteLastModified)) return true;
return localLastModified < remoteLastModified;
};

Expand All @@ -105,14 +121,14 @@ const main = async () => {
const response = await request(link(editionId));
const entryPromises = [];
await new Promise((resolve, reject) =>
response.body
Readable.fromWeb(response.body)
.pipe(zlib.createGunzip())
.pipe(tar.t())
.on('entry', (entry) => {
if (entry.path.endsWith('.mmdb')) {
const dstFilename = path.join(
downloadPath,
path.basename(entry.path)
path.basename(entry.path),
);
console.log(`writing ${dstFilename} ...`);
entryPromises.push(
Expand All @@ -121,20 +137,19 @@ const main = async () => {
.pipe(fs.createWriteStream(dstFilename))
.on('finish', resolve)
.on('error', reject);
})
}),
);
}
})
.on('end', resolve)
.on('error', reject)
.on('error', reject),
);
await Promise.all(entryPromises);
}
};

main()
.then(() => {
// success
process.exit(0);
})
.catch((err) => {
Expand Down
90 changes: 90 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import fs from 'node:fs';
import path from 'node:path';

let cachedConfigWithDir;

const findConfigWithDir = () => {
const cwd = process.env.INIT_CWD || process.cwd();
let dir = cwd;

// Find a package.json with geolite2 configuration key at or above this directory.
while (fs.existsSync(dir)) {
const packageJSON = path.join(dir, 'package.json');
if (fs.existsSync(packageJSON)) {
const contents = JSON.parse(fs.readFileSync(packageJSON, 'utf8'));
const config = contents.geolite2;
if (config) return { config, dir };
}

const parentDir = path.resolve(dir, '..');
if (parentDir === dir) break;
dir = parentDir;
}

return;
};

const getConfigWithDir = () => {
if (cachedConfigWithDir !== undefined) {
return cachedConfigWithDir;
}

cachedConfigWithDir = findConfigWithDir();
return cachedConfigWithDir;
};

const getConfig = () => {
const configWithDir = getConfigWithDir();
if (!configWithDir) return;
return configWithDir.config;
};

const getAccountId = () => {
const envId = process.env.MAXMIND_ACCOUNT_ID;
if (envId) return envId;

const config = getConfig();
if (!config) return;

return config['account-id'];
};

const getLicense = () => {
const envKey = process.env.MAXMIND_LICENSE_KEY;
if (envKey) return envKey;

const configWithDir = getConfigWithDir();
if (!configWithDir) return;

const { config, dir } = configWithDir;

const licenseKey = config['license-key'];
if (licenseKey) return licenseKey;

const configFile = config['license-file'];
if (!configFile) return;

const configFilePath = path.join(dir, configFile);
return fs.existsSync(configFilePath)
? fs.readFileSync(configFilePath, 'utf8').trim()
: undefined;
};

const maskLicenseKey = (licenseKey) => {
if (!licenseKey) return 'NOT SET';
if (licenseKey.length <= 4) return '****';
const visiblePart = licenseKey.slice(-4);
return `****${visiblePart}`;
};

const resetConfigCache = () => {
cachedConfigWithDir = undefined;
};

export {
getConfig,
getAccountId,
getLicense,
maskLicenseKey,
resetConfigCache,
};
Loading