Skip to content

Commit

Permalink
Initial working version
Browse files Browse the repository at this point in the history
  • Loading branch information
queicherius committed Jun 14, 2020
0 parents commit 01f7c94
Show file tree
Hide file tree
Showing 13 changed files with 4,674 additions and 0 deletions.
25 changes: 25 additions & 0 deletions .all-contributorsrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"imageSize": 75,
"projectName": "ip-geolocation",
"projectOwner": "devoxa",
"repoType": "github",
"repoHost": "https://github.com",
"skipCi": true,
"contributors": [
{
"login": "queicherius",
"name": "David Reeß",
"avatar_url": "https://avatars3.githubusercontent.com/u/4615516?v=4",
"profile": "https://www.david-reess.de",
"contributions": [
"code",
"doc",
"test"
]
}
],
"files": [
"README.md"
],
"contributorsPerLine": 7
}
25 changes: 25 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
version: 2.1

orbs:
node: circleci/[email protected]

jobs:
build-and-test:
executor:
name: node/default
steps:
- checkout
- node/with-cache:
steps:
- run: yarn install
- run: node scripts/postinstall.js src
- run: yarn test --coverage
- run: bash <(curl -s https://codecov.io/bash)
- run: yarn format:check
- run: yarn lint
- run: yarn build

workflows:
build-and-test:
jobs:
- build-and-test
17 changes: 17 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Downloaded Binaries
src/dbip-city-lite.mmdb
src/version.json

# Compiled Output
dist/

# Tests
coverage/

# Dependencies
node_modules/
yarn-debug.log*
yarn-error.log*

# Development Environments
.history/
14 changes: 14 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Downloaded Binaries
src/dbip-city-lite.mmdb
src/version.json

# Tests
coverage/

# Dependencies
node_modules/
yarn-debug.log*
yarn-error.log*

# Development Environments
.history/
111 changes: 111 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<!-- Title -->
<h1 align="center">
ip-geolocation
</h1>

<!-- Description -->
<h4 align="center">
Resolve an IP address into a geolocation (continent, country, subdivision, city & lat/long)
</h4>

<!-- Badges -->
<p align="center">
<a href="https://www.npmjs.com/package/@devoxa/ip-geolocation">
<img
src="https://img.shields.io/npm/v/@devoxa/ip-geolocation?style=flat-square"
alt="Package Version"
/>
</a>

<a href="https://app.circleci.com/pipelines/github/devoxa/ip-geolocation?branch=master">
<img
src="https://img.shields.io/circleci/build/github/devoxa/ip-geolocation/master?style=flat-square"
alt="Build Status"
/>
</a>

<a href="https://codecov.io/github/devoxa/ip-geolocation">
<img
src="https://img.shields.io/codecov/c/github/devoxa/ip-geolocation/master?style=flat-square"
alt="Code Coverage"
/>
</a>
</p>

<!-- Quicklinks -->
<p align="center">
<a href="#installation">Installation</a> •
<a href="#usage">Usage</a> •
<a href="#contributors">Contributors</a> •
<a href="#license">License :warning:</a>
</p>

<br>

## Installation

:warning: **Before installing this make sure you understand the [License](#license)!**

```bash
yarn add @devoxa/ip-geolocation
```

**This module will automatically download a ~85MB IP geolocation database from
[db-ip.com](https://db-ip.com/db/download/ip-to-city-lite) in a postinstall step.**

## Usage

```ts
import { loadDatabase, geolocateIp } from '@devoxa/ip-geolocation'

// Preload the geolocation database
// Recommended, but not required. Will reduce the first call to `geolocateIp` by ~100ms
await loadDatabase()

// Lookup an IP address
const result = await geolocateIp('69.10.63.243')
// {
// continent: { code: 'NA', name: 'North America' },
// country: { code: 'US', name: 'United States', isInEuropeanUnion: false },
// subdivision: { name: 'New Jersey' },
// city: { name: 'Secaucus' },
// location: { latitude: 40.7861, longitude: -74.0743 },
// }
```

## Contributors

Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):

<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://www.david-reess.de"><img src="https://avatars3.githubusercontent.com/u/4615516?v=4" width="75px;" alt=""/><br /><sub><b>David Reeß</b></sub></a><br /><a href="https://github.com/devoxa/ip-geolocation/commits?author=queicherius" title="Code">💻</a> <a href="https://github.com/devoxa/ip-geolocation/commits?author=queicherius" title="Documentation">📖</a> <a href="https://github.com/devoxa/ip-geolocation/commits?author=queicherius" title="Tests">⚠️</a></td>
</tr>
</table>

<!-- markdownlint-enable -->
<!-- prettier-ignore-end -->

<!-- ALL-CONTRIBUTORS-LIST:END -->

This project follows the [all-contributors](https://github.com/all-contributors/all-contributors)
specification. Contributions of any kind welcome!

## License

MIT

:warning: The module will also automatically download and use a ~85MB IP geolocation database from
[db-ip.com](https://db-ip.com/db/download/ip-to-city-lite). This database is licensed under a
**Creative Commons Attribution 4.0 International License**. You are free to use this database
in your application, provided you give attribution to DB-IP.com for the data.

In the case of a web application, you must include a link back to DB-IP.com on pages that display or
use results from the database:

```html
<a href="https://db-ip.com">IP Geolocation by DB-IP</a>
```
5 changes: 5 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testPathIgnorePatterns: ['dist'],
}
39 changes: 39 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "@devoxa/ip-geolocation",
"description": "Resolve an IP address into a geolocation (continent, country, subdivision, city & lat/long)",
"version": "0.1.0",
"main": "dist/src/index.js",
"license": "MIT",
"repository": {
"url": "https://github.com/devoxa/ip-geolocation"
},
"scripts": {
"test": "jest",
"format": "prettier --ignore-path='.gitignore' --list-different --write .",
"format:check": "prettier --ignore-path='.gitignore' --check .",
"lint": "eslint --ignore-path='.gitignore' '{src,tests}/**/*.ts'",
"build": "rm -rf dist/ && tsc",
"preversion": "yarn build"
},
"eslintConfig": {
"extends": "@devoxa"
},
"prettier": "@devoxa/prettier-config",
"dependencies": {
"maxmind": "^4.1.3"
},
"devDependencies": {
"@devoxa/eslint-config": "^1.0.0",
"@devoxa/prettier-config": "^1.0.0",
"@types/jest": "^25.2.2",
"@types/node": "^14.0.1",
"eslint": "^7.0.0",
"jest": "^26.0.1",
"prettier": "^2.0.5",
"ts-jest": "^26.0.0",
"typescript": "^3.9.2"
},
"publishConfig": {
"access": "public"
}
}
87 changes: 87 additions & 0 deletions scripts/postinstall.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
const fs = require('fs')
const https = require('https')
const path = require('path')
const zlib = require('zlib')

const folder = process.argv[2] || 'dist/src'

const DOWNLOAD_LINK = 'https://download.db-ip.com/free/dbip-city-lite-YYYY-MM.mmdb.gz'
const DOWNLOAD_PATH = path.join(__dirname, '..', folder)
const DOWNLOAD_PATH_VERSION = path.join(DOWNLOAD_PATH, 'version.json')
const DOWNLOAD_PATH_MMDB = path.join(DOWNLOAD_PATH, 'dbip-city-lite.mmdb')

run()

async function run() {
const date = new Date()

// Try to download the latest version of the last 3 months
for (let i = 0; i !== 3; i++) {
date.setMonth(date.getMonth() - i)

try {
await downloadForDate(date)
} catch (err) {
console.warn(err)
}
}

console.error('Failed downloading DBIP City Lite database')
process.exit(1)
}

async function downloadForDate(date) {
const version = buildVersionStringForDate(date)

if (versionExists(version)) {
console.log(`Skipped download because database for ${version} already exists`)
process.exit(0)
}

console.log(`Downloading DBIP City Lite database for ${version}...`)
const downloadUrl = buildDownloadUrl(version)
await downloadToFile(downloadUrl, DOWNLOAD_PATH_MMDB)

console.log(`Writing version file for ${version}...`)
fs.writeFileSync(DOWNLOAD_PATH_VERSION, JSON.stringify({ version }), 'utf-8')

process.exit(0)
}

function buildVersionStringForDate(date) {
return `${date.getFullYear()}-${(date.getMonth() + 1 + '').padStart(2, '0')}`
}

function versionExists(version) {
if (!fs.existsSync(DOWNLOAD_PATH_MMDB)) {
return false
}

try {
const versionJson = JSON.parse(fs.readFileSync(DOWNLOAD_PATH_VERSION, 'utf-8'))
return versionJson.version === version
} catch (err) {
return false
}
}

function buildDownloadUrl(version) {
return DOWNLOAD_LINK.replace('YYYY-MM', version)
}

function downloadToFile(url, path) {
return new Promise((resolve, reject) => {
https.get(url, (response) => {
if (response.statusCode >= 400) {
return reject(`${url} responded with ${response.statusCode}`)
}

// Pipe the response into `gunzip` to inflate the gzip compression, and then
// into the file write stream, and resolve once we are done.
const writeStream = fs.createWriteStream(path)
writeStream.on('finish', () => resolve())

response.pipe(zlib.createGunzip()).pipe(writeStream)
})
})
}
63 changes: 63 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import mmdbReader, { Reader } from 'maxmind'
import fs from 'fs'
import path from 'path'

export interface GeolocateIpResult {
continent: { code: string; name: string }
country: { code: string; name: string; isInEuropeanUnion: boolean }
subdivision: { name: string }
city: { name: string }
location: { latitude: number; longitude: number }
}

let reader: Reader<any>
const DB_FILE_PATH = path.join(__dirname, 'dbip-city-lite.mmdb')

export async function loadDatabase(dbFilePath: string = DB_FILE_PATH) {
if (reader) {
return
}

const dbFileExists = await fileExists(dbFilePath)
if (!dbFileExists) {
throw new Error(`Database file at ${dbFilePath} does not exist`)
}

reader = await mmdbReader.open(dbFilePath)
}

function fileExists(path: string) {
return new Promise((resolve) => fs.access(path, fs.constants.F_OK, (err) => resolve(!err)))
}

export async function geolocateIp(ip: string): Promise<GeolocateIpResult | null> {
await loadDatabase()

const result = reader.get(ip)

if (!result) {
return null
}

return {
continent: {
code: result.continent.code,
name: result.continent.names.en,
},
country: {
code: result.country.iso_code,
name: result.country.names.en,
isInEuropeanUnion: result.country.is_in_european_union,
},
subdivision: {
name: result.subdivisions[0].names.en,
},
city: {
name: result.city.names.en,
},
location: {
latitude: result.location.latitude,
longitude: result.location.longitude,
},
}
}
Loading

0 comments on commit 01f7c94

Please sign in to comment.