UNPKG

geoip-lite2

Version:

A light weight native JavaScript implementation of GeoIP API from MaxMind. Improved and faster version by Sefinek.

501 lines (440 loc) 13.9 kB
const { open, fstat, read, close, openSync, fstatSync, readSync, closeSync } = require('fs'); const { join, resolve } = require('path'); const { isIP } = require('net'); const async = require('async'); const { aton4, aton6, cmp6, ntoa4, ntoa6, cmp } = require('./utils.js'); const fsWatcher = require('./fsWatcher.js'); const { version } = require('../package.json'); const watcherName = 'dataWatcher'; const geoDataDir = resolve( __dirname, global.geoDataDir || process.env.GEODATADIR || '../geoip-data/' ); const dataFiles = { city: join(geoDataDir, 'geoip-city.dat'), city6: join(geoDataDir, 'geoip-city6.dat'), cityNames: join(geoDataDir, 'geoip-city-names.dat'), country: join(geoDataDir, 'geoip-country.dat'), country6: join(geoDataDir, 'geoip-country6.dat'), }; const privateRange4 = [ [aton4('10.0.0.0'), aton4('10.255.255.255')], [aton4('172.16.0.0'), aton4('172.31.255.255')], [aton4('192.168.0.0'), aton4('192.168.255.255')], ]; const conf4 = { firstIP: null, lastIP: null, lastLine: 0, locationBuffer: null, locationRecordSize: 88, mainBuffer: null, recordSize: 24, }; const conf6 = { firstIP: null, lastIP: null, lastLine: 0, mainBuffer: null, recordSize: 48, }; // Copy original configs let cache4 = JSON.parse(JSON.stringify(conf4)); let cache6 = JSON.parse(JSON.stringify(conf6)); const RECORD_SIZE = 10; const RECORD_SIZE6 = 34; const lookup4 = ip => { let fline = 0; let cline = cache4.lastLine; let floor = cache4.lastIP; let ceil = cache4.firstIP; let line, locId; const buffer = cache4.mainBuffer; const locBuffer = cache4.locationBuffer; const privateRange = privateRange4; const recordSize = cache4.recordSize; const locRecordSize = cache4.locationRecordSize; const geoData = { range: [null, null], country: '', region: '', eu: '', timezone: '', city: '', ll: [null, null], metro: null, area: null, }; // Outside IPv4 range if (ip > cache4.lastIP || ip < cache4.firstIP) return null; // Private IP for (let i = 0; i < privateRange.length; i++) { if (ip >= privateRange[i][0] && ip <= privateRange[i][1]) return null; } while (true) { line = Math.round((cline - fline) / 2) + fline; floor = buffer.readUInt32BE(line * recordSize); ceil = buffer.readUInt32BE((line * recordSize) + 4); if (floor <= ip && ceil >= ip) { geoData.range = [floor, ceil]; if (recordSize === RECORD_SIZE) { geoData.country = buffer.toString('utf8', (line * recordSize) + 8, (line * recordSize) + 10); } else { locId = buffer.readUInt32BE((line * recordSize) + 8); // -1>>>0 is a marker for "No Location Info" if (-1 >>> 0 > locId) { geoData.country = locBuffer.toString('utf8', (locId * locRecordSize), (locId * locRecordSize) + 2).replace(/\u0000.*/, ''); geoData.region = locBuffer.toString('utf8', (locId * locRecordSize) + 2, (locId * locRecordSize) + 5).replace(/\u0000.*/, ''); geoData.metro = locBuffer.readInt32BE((locId * locRecordSize) + 5); geoData.ll[0] = buffer.readInt32BE((line * recordSize) + 12) / 10000; // latitude geoData.ll[1] = buffer.readInt32BE((line * recordSize) + 16) / 10000; // longitude geoData.area = buffer.readUInt32BE((line * recordSize) + 20); geoData.eu = locBuffer.toString('utf8', (locId * locRecordSize) + 9, (locId * locRecordSize) + 10).replace(/\u0000.*/, ''); geoData.timezone = locBuffer.toString('utf8', (locId * locRecordSize) + 10, (locId * locRecordSize) + 42).replace(/\u0000.*/, ''); geoData.city = locBuffer.toString('utf8', (locId * locRecordSize) + 42, (locId * locRecordSize) + locRecordSize).replace(/\u0000.*/, ''); } } return geoData; } else if (fline === cline) { return null; } else if (fline === (cline - 1)) { if (line === fline) { fline = cline; } else { cline = fline; } } else if (floor > ip) { cline = line; } else if (ceil < ip) { fline = line; } } }; const lookup6 = ip => { const buffer = cache6.mainBuffer; const recordSize = cache6.recordSize; const locBuffer = cache4.locationBuffer; const locRecordSize = cache4.locationRecordSize; const geoData = { range: [null, null], country: '', region: '', city: '', ll: [0, 0], metro: null, area: null, eu: '', timezone: '' }; const readIp = (line, offset) => { const ipArray = []; for (let i = 0; i < 2; i++) { ipArray.push(buffer.readUInt32BE((line * recordSize) + (offset * 16) + (i * 4))); } return ipArray; }; cache6.lastIP = readIp(cache6.lastLine, 1); cache6.firstIP = readIp(0, 0); let fline = 0; let cline = cache6.lastLine; let floor = cache6.lastIP; let ceil = cache6.firstIP; let line, locId; if (cmp6(ip, cache6.lastIP) > 0 || cmp6(ip, cache6.firstIP) < 0) return null; while (true) { line = Math.round((cline - fline) / 2) + fline; floor = readIp(line, 0); ceil = readIp(line, 1); if (cmp6(floor, ip) <= 0 && cmp6(ceil, ip) >= 0) { if (recordSize === RECORD_SIZE6) { geoData.country = buffer.toString('utf8', (line * recordSize) + 32, (line * recordSize) + 34).replace(/\u0000.*/, ''); } else { locId = buffer.readUInt32BE((line * recordSize) + 32); // -1>>>0 is a marker for "No Location Info" if (-1 >>> 0 > locId) { geoData.country = locBuffer.toString('utf8', (locId * locRecordSize), (locId * locRecordSize) + 2).replace(/\u0000.*/, ''); geoData.region = locBuffer.toString('utf8', (locId * locRecordSize) + 2, (locId * locRecordSize) + 5).replace(/\u0000.*/, ''); geoData.metro = locBuffer.readInt32BE((locId * locRecordSize) + 5); geoData.ll[0] = buffer.readInt32BE((line * recordSize) + 36) / 10000; // latitude geoData.ll[1] = buffer.readInt32BE((line * recordSize) + 40) / 10000; // longitude geoData.area = buffer.readUInt32BE((line * recordSize) + 44); // area geoData.eu = locBuffer.toString('utf8', (locId * locRecordSize) + 9, (locId * locRecordSize) + 10).replace(/\u0000.*/, ''); geoData.timezone = locBuffer.toString('utf8', (locId * locRecordSize) + 10, (locId * locRecordSize) + 42).replace(/\u0000.*/, ''); geoData.city = locBuffer.toString('utf8', (locId * locRecordSize) + 42, (locId * locRecordSize) + locRecordSize).replace(/\u0000.*/, ''); } } // We do not currently have detailed region/city info for IPv6, but finally have coords return geoData; } else if (fline === cline) { return null; } else if (fline === (cline - 1)) { if (line === fline) { fline = cline; } else { cline = fline; } } else if (cmp6(floor, ip) > 0) { cline = line; } else if (cmp6(ceil, ip) < 0) { fline = line; } } }; const v6prefixes = ['0:0:0:0:0:FFFF:', '::FFFF:']; const get4mapped = ip => { const ipv6 = ip.toUpperCase(); for (let i = 0; i < v6prefixes.length; i++) { const v6prefix = v6prefixes[i]; if (ipv6.indexOf(v6prefix) === 0) return ipv6.substring(v6prefix.length); } return null; }; function preload(callback) { let datFile; let datSize; const asyncCache = JSON.parse(JSON.stringify(conf4)); // When the preload function receives a callback, do the task asynchronously if (typeof arguments[0] === 'function') { async.series([ cb => { async.series([ cb2 => { open(dataFiles.cityNames, 'r', (err, file) => { datFile = file; cb2(err); }); }, cb2 => { fstat(datFile, (err, stats) => { datSize = stats.size; asyncCache.locationBuffer = Buffer.alloc(datSize); cb2(err); }); }, cb2 => { read(datFile, asyncCache.locationBuffer, 0, datSize, 0, cb2); }, cb2 => { close(datFile, cb2); }, cb2 => { open(dataFiles.city, 'r', (err, file) => { datFile = file; cb2(err); }); }, cb2 => { fstat(datFile, (err, stats) => { datSize = stats.size; cb2(err); }); }, ], err => { if (err) { if (err.code !== 'ENOENT' && err.code !== 'EBADF') { throw err; } open(dataFiles.country, 'r', (err, file) => { if (err) { cb(err); } else { datFile = file; fstat(datFile, (err, stats) => { datSize = stats.size; asyncCache.recordSize = RECORD_SIZE; cb(); }); } }); } else { cb(); } }); }, () => { asyncCache.mainBuffer = Buffer.alloc(datSize); async.series([ cb2 => { read(datFile, asyncCache.mainBuffer, 0, datSize, 0, cb2); }, cb2 => { close(datFile, cb2); }, ], err => { if (!err) { asyncCache.lastLine = (datSize / asyncCache.recordSize) - 1; asyncCache.lastIP = asyncCache.mainBuffer.readUInt32BE((asyncCache.lastLine * asyncCache.recordSize) + 4); asyncCache.firstIP = asyncCache.mainBuffer.readUInt32BE(0); cache4 = asyncCache; } callback(err); }); }, ]); } else { try { datFile = openSync(dataFiles.cityNames, 'r'); datSize = fstatSync(datFile).size; if (datSize === 0) throw { code: 'EMPTY_FILE' }; cache4.locationBuffer = Buffer.alloc(datSize); readSync(datFile, cache4.locationBuffer, 0, datSize, 0); closeSync(datFile); datFile = openSync(dataFiles.city, 'r'); datSize = fstatSync(datFile).size; } catch (err) { if (err.code !== 'ENOENT' && err.code !== 'EBADF' && err.code !== 'EMPTY_FILE') { throw err; } datFile = openSync(dataFiles.country, 'r'); datSize = fstatSync(datFile).size; cache4.recordSize = RECORD_SIZE; } cache4.mainBuffer = Buffer.alloc(datSize); readSync(datFile, cache4.mainBuffer, 0, datSize, 0); closeSync(datFile); cache4.lastLine = (datSize / cache4.recordSize) - 1; cache4.lastIP = cache4.mainBuffer.readUInt32BE((cache4.lastLine * cache4.recordSize) + 4); cache4.firstIP = cache4.mainBuffer.readUInt32BE(0); } } function preload6(callback) { let datFile; let datSize; const asyncCache6 = JSON.parse(JSON.stringify(conf6)); // When the preload function receives a callback, do the task asynchronously if (typeof arguments[0] === 'function') { async.series([ cb => { async.series([ cb2 => { open(dataFiles.city6, 'r', (err, file) => { datFile = file; cb2(err); }); }, cb2 => { fstat(datFile, (err, stats) => { datSize = stats.size; cb2(err); }); }, ], err => { if (err) { if (err.code !== 'ENOENT' && err.code !== 'EBADF') { throw err; } open(dataFiles.country6, 'r', (err, file) => { if (err) { cb(err); } else { datFile = file; fstat(datFile, (err, stats) => { datSize = stats.size; asyncCache6.recordSize = RECORD_SIZE6; cb(); }); } }); } else { cb(); } }); }, () => { asyncCache6.mainBuffer = Buffer.alloc(datSize); async.series([ cb2 => { read(datFile, asyncCache6.mainBuffer, 0, datSize, 0, cb2); }, cb2 => { close(datFile, cb2); }, ], err => { if (!err) { asyncCache6.lastLine = (datSize / asyncCache6.recordSize) - 1; cache6 = asyncCache6; } callback(err); }); }, ]); } else { try { datFile = openSync(dataFiles.city6, 'r'); datSize = fstatSync(datFile).size; if (datSize === 0) throw { code: 'EMPTY_FILE' }; } catch (err) { if (err.code !== 'ENOENT' && err.code !== 'EBADF' && err.code !== 'EMPTY_FILE') { throw err; } datFile = openSync(dataFiles.country6, 'r'); datSize = fstatSync(datFile).size; cache6.recordSize = RECORD_SIZE6; } cache6.mainBuffer = Buffer.alloc(datSize); readSync(datFile, cache6.mainBuffer, 0, datSize, 0); closeSync(datFile); cache6.lastLine = (datSize / cache6.recordSize) - 1; } } module.exports = { cmp, lookup: ip => { if (!ip) { return null; } else if (typeof ip === 'number') { return lookup4(ip); } else if (isIP(ip) === 4) { return lookup4(aton4(ip)); } else if (isIP(ip) === 6) { const ipv4 = get4mapped(ip); if (ipv4) { return lookup4(aton4(ipv4)); } else { return lookup6(aton6(ip)); } } return null; }, pretty: n => { if (typeof n === 'string') { return n; } else if (typeof n === 'number') { return ntoa4(n); } else if (Array.isArray(n)) { return ntoa6(n); } return n; }, // Start watching for data updates. The watcher waits one minute for file transfer to // complete before triggering the callback. startWatchingDataUpdate: callback => { fsWatcher.makeFsWatchFilter(watcherName, geoDataDir, 60 * 1000, async () => { // Reload data await async.series([ cb => { preload(cb); }, cb => { preload6(cb); }, ], callback); }); }, // Stop watching for data updates. stopWatchingDataUpdate: () => fsWatcher.stopWatching(watcherName), // Clear data clear: () => { cache4 = JSON.parse(JSON.stringify(conf4)); cache6 = JSON.parse(JSON.stringify(conf6)); }, // Reload data synchronously reloadDataSync: () => { preload(); preload6(); }, // Reload data asynchronously reloadData: async callback => { // Reload data await async.series([ cb => { preload(cb); }, cb => { preload6(cb); }, ], callback); }, version, }; preload(); preload6(); // lookup4 = gen_lookup('geoip-country.dat', 4); // lookup6 = gen_lookup('geoip-country6.dat', 16);