geoip-lite
Version:
A light weight native JavaScript implementation of GeoIP API from MaxMind
468 lines (397 loc) • 11.9 kB
JavaScript
var fs = require('fs');
var net = require('net');
var path = require('path');
var utils = require('./utils');
var fsWatcher = require('./fsWatcher');
var watcherName = 'dataWatcher';
var geodatadir = path.resolve(
__dirname,
global.geodatadir || process.env.GEODATADIR || '../data/'
);
var dataFiles = {
city: path.join(geodatadir, 'geoip-city.dat'),
city6: path.join(geodatadir, 'geoip-city6.dat'),
cityNames: path.join(geodatadir, 'geoip-city-names.dat'),
country: path.join(geodatadir, 'geoip-country.dat'),
country6: path.join(geodatadir, 'geoip-country6.dat')
};
var privateRange4 = [
[utils.aton4('10.0.0.0'), utils.aton4('10.255.255.255')],
[utils.aton4('172.16.0.0'), utils.aton4('172.31.255.255')],
[utils.aton4('192.168.0.0'), utils.aton4('192.168.255.255')]
];
var conf4 = {
firstIP: null,
lastIP: null,
lastLine: 0,
locationBuffer: null,
locationRecordSize: 88,
mainBuffer: null,
recordSize: 24
};
var conf6 = {
firstIP: null,
lastIP: null,
lastLine: 0,
mainBuffer: null,
recordSize: 48
};
var cache4 = { ...conf4 };
var cache6 = { ...conf6 };
var RECORD_SIZE = 10;
var RECORD_SIZE6 = 34;
var NO_LOCATION = (-1 >>> 0);
function bufstr(buf, start, end) {
var nullPos = buf.indexOf(0, start);
if (nullPos >= start && nullPos < end) {
end = nullPos;
}
return buf.toString('utf8', start, end);
}
function readip6(buffer, recordSize, line, offset) {
var baseOffset = (line * recordSize) + (offset * 16);
return [
buffer.readUInt32BE(baseOffset),
buffer.readUInt32BE(baseOffset + 4)
];
}
function lookup4(ip) {
var fline = 0;
var floor;
var cline = cache4.lastLine;
var ceil;
var line;
var buffer = cache4.mainBuffer;
var locBuffer = cache4.locationBuffer;
var privateRange = privateRange4;
var recordSize = cache4.recordSize;
var locRecordSize = cache4.locationRecordSize;
var i;
// outside IPv4 range
if (ip > cache4.lastIP || ip < cache4.firstIP) {
return null;
}
// private IP
for (i = 0; i < privateRange.length; i++) {
if (ip >= privateRange[i][0] && ip <= privateRange[i][1]) {
return null;
}
}
do {
line = (fline + cline) >>> 1;
let offset = line * recordSize;
floor = buffer.readUInt32BE(offset);
ceil = buffer.readUInt32BE(offset + 4);
if (floor <= ip && ceil >= ip) {
let geodata = {
range: [floor, ceil],
country: '',
region: '',
eu: '',
timezone: '',
city: '',
ll: [null, null]
};
if (recordSize === RECORD_SIZE) {
geodata.country = buffer.toString('utf8', offset + 8, offset + 10);
} else {
let locId = buffer.readUInt32BE(offset + 8);
if (NO_LOCATION > locId) {
let locOffset = locId * locRecordSize;
geodata.country = bufstr(locBuffer, locOffset, locOffset + 2);
geodata.region = bufstr(locBuffer, locOffset + 2, locOffset + 5);
geodata.metro = locBuffer.readInt32BE(locOffset + 5);
geodata.ll[0] = buffer.readInt32BE(offset + 12) / 10000;
geodata.ll[1] = buffer.readInt32BE(offset + 16) / 10000;
geodata.area = buffer.readUInt32BE(offset + 20);
geodata.eu = bufstr(locBuffer, locOffset + 9, locOffset + 10);
geodata.timezone = bufstr(locBuffer, locOffset + 10, locOffset + 42);
geodata.city = bufstr(locBuffer, locOffset + 42, locOffset + locRecordSize);
}
}
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;
}
} while(1);
}
function lookup6(ip) {
var buffer = cache6.mainBuffer;
var recordSize = cache6.recordSize;
var locBuffer = cache4.locationBuffer;
var locRecordSize = cache4.locationRecordSize;
var fline = 0;
var floor;
var cline = cache6.lastLine;
var ceil;
var line;
if (utils.cmp6(ip, cache6.lastIP) > 0 || utils.cmp6(ip, cache6.firstIP) < 0) {
return null;
}
do {
line = (fline + cline) >>> 1;
floor = readip6(buffer, recordSize, line, 0);
ceil = readip6(buffer, recordSize, line, 1);
if (utils.cmp6(floor, ip) <= 0 && utils.cmp6(ceil, ip) >= 0) {
let offset = line * recordSize;
let geodata = {
range: '',
country: '',
region: '',
city: '',
ll: [0, 0]
};
if (recordSize === RECORD_SIZE6) {
geodata.country = bufstr(buffer, offset + 32, offset + 34);
} else {
let locId = buffer.readUInt32BE(offset + 32);
if (NO_LOCATION > locId) {
let locOffset = locId * locRecordSize;
geodata.country = bufstr(locBuffer, locOffset, locOffset + 2);
geodata.region = bufstr(locBuffer, locOffset + 2, locOffset + 5);
geodata.metro = locBuffer.readInt32BE(locOffset + 5);
geodata.ll[0] = buffer.readInt32BE(offset + 36) / 10000;
geodata.ll[1] = buffer.readInt32BE(offset + 40) / 10000;
geodata.area = buffer.readUInt32BE(offset + 44);
geodata.eu = bufstr(locBuffer, locOffset + 9, locOffset + 10);
geodata.timezone = bufstr(locBuffer, locOffset + 10, locOffset + 42);
geodata.city = bufstr(locBuffer, locOffset + 42, locOffset + locRecordSize);
}
}
return geodata;
} else if (fline === cline) {
return null;
} else if (fline === (cline - 1)) {
if (line === fline) {
fline = cline;
} else {
cline = fline;
}
} else if (utils.cmp6(floor, ip) > 0) {
cline = line;
} else if (utils.cmp6(ceil, ip) < 0) {
fline = line;
}
} while(1);
}
function get4mapped(ip) {
var ipv6 = ip.toUpperCase();
var v6prefixes = ['0:0:0:0:0:FFFF:', '::FFFF:'];
for (var i = 0; i < v6prefixes.length; i++) {
var v6prefix = v6prefixes[i];
if (ipv6.indexOf(v6prefix) == 0) {
return ipv6.substring(v6prefix.length);
}
}
return null;
}
async function preloadAsync() {
var asyncCache = { ...conf4 };
var mainFh;
var datSize;
try {
var locFh = await fs.promises.open(dataFiles.cityNames, 'r');
try {
var locStats = await locFh.stat();
if (locStats.size === 0) throw { code: 'EMPTY_FILE' };
asyncCache.locationBuffer = Buffer.alloc(locStats.size);
await locFh.read(asyncCache.locationBuffer, 0, locStats.size, 0);
} finally {
await locFh.close();
}
mainFh = await fs.promises.open(dataFiles.city, 'r');
var cityStats = await mainFh.stat();
datSize = cityStats.size;
} catch (err) {
if (err.code !== 'ENOENT' && err.code !== 'EBADF' && err.code !== 'EMPTY_FILE') {
if (mainFh) try { await mainFh.close(); } catch { /* ignore close error during cleanup */ }
throw err;
}
mainFh = await fs.promises.open(dataFiles.country, 'r');
var countryStats = await mainFh.stat();
datSize = countryStats.size;
asyncCache.recordSize = RECORD_SIZE;
}
try {
asyncCache.mainBuffer = Buffer.alloc(datSize);
await mainFh.read(asyncCache.mainBuffer, 0, datSize, 0);
} finally {
await mainFh.close();
}
asyncCache.lastLine = (datSize / asyncCache.recordSize) - 1;
asyncCache.lastIP = asyncCache.mainBuffer.readUInt32BE((asyncCache.lastLine * asyncCache.recordSize) + 4);
asyncCache.firstIP = asyncCache.mainBuffer.readUInt32BE(0);
cache4 = asyncCache;
}
function preload(callback) {
if (typeof callback === 'function') {
preloadAsync().then(function () { callback(null); }, callback);
return;
}
var datFile;
var datSize;
try {
datFile = fs.openSync(dataFiles.cityNames, 'r');
datSize = fs.fstatSync(datFile).size;
if (datSize === 0) {
throw {
code: 'EMPTY_FILE'
};
}
cache4.locationBuffer = Buffer.alloc(datSize);
fs.readSync(datFile, cache4.locationBuffer, 0, datSize, 0);
fs.closeSync(datFile);
datFile = fs.openSync(dataFiles.city, 'r');
datSize = fs.fstatSync(datFile).size;
} catch(err) {
if (err.code !== 'ENOENT' && err.code !== 'EBADF' && err.code !== 'EMPTY_FILE') {
throw err;
}
datFile = fs.openSync(dataFiles.country, 'r');
datSize = fs.fstatSync(datFile).size;
cache4.recordSize = RECORD_SIZE;
}
cache4.mainBuffer = Buffer.alloc(datSize);
fs.readSync(datFile, cache4.mainBuffer, 0, datSize, 0);
fs.closeSync(datFile);
cache4.lastLine = (datSize / cache4.recordSize) - 1;
cache4.lastIP = cache4.mainBuffer.readUInt32BE((cache4.lastLine * cache4.recordSize) + 4);
cache4.firstIP = cache4.mainBuffer.readUInt32BE(0);
}
async function preload6Async() {
var asyncCache6 = { ...conf6 };
var mainFh;
var datSize;
try {
mainFh = await fs.promises.open(dataFiles.city6, 'r');
var cityStats = await mainFh.stat();
datSize = cityStats.size;
if (datSize === 0) {
await mainFh.close();
mainFh = null;
throw { code: 'EMPTY_FILE' };
}
} catch (err) {
if (err.code !== 'ENOENT' && err.code !== 'EBADF' && err.code !== 'EMPTY_FILE') {
if (mainFh) try { await mainFh.close(); } catch { /* ignore close error during cleanup */ }
throw err;
}
mainFh = await fs.promises.open(dataFiles.country6, 'r');
var countryStats = await mainFh.stat();
datSize = countryStats.size;
asyncCache6.recordSize = RECORD_SIZE6;
}
try {
asyncCache6.mainBuffer = Buffer.alloc(datSize);
await mainFh.read(asyncCache6.mainBuffer, 0, datSize, 0);
} finally {
await mainFh.close();
}
asyncCache6.lastLine = (datSize / asyncCache6.recordSize) - 1;
asyncCache6.lastIP = readip6(asyncCache6.mainBuffer, asyncCache6.recordSize, asyncCache6.lastLine, 1);
asyncCache6.firstIP = readip6(asyncCache6.mainBuffer, asyncCache6.recordSize, 0, 0);
cache6 = asyncCache6;
}
function preload6(callback) {
if (typeof callback === 'function') {
preload6Async().then(function () { callback(null); }, callback);
return;
}
var datFile;
var datSize;
try {
datFile = fs.openSync(dataFiles.city6, 'r');
datSize = fs.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 = fs.openSync(dataFiles.country6, 'r');
datSize = fs.fstatSync(datFile).size;
cache6.recordSize = RECORD_SIZE6;
}
cache6.mainBuffer = Buffer.alloc(datSize);
fs.readSync(datFile, cache6.mainBuffer, 0, datSize, 0);
fs.closeSync(datFile);
cache6.lastLine = (datSize / cache6.recordSize) - 1;
cache6.lastIP = readip6(cache6.mainBuffer, cache6.recordSize, cache6.lastLine, 1);
cache6.firstIP = readip6(cache6.mainBuffer, cache6.recordSize, 0, 0);
}
module.exports = {
cmp: utils.cmp,
lookup: function(ip) {
if (!ip) {
return null;
} else if (typeof ip === 'number') {
return lookup4(ip);
}
var ver = net.isIP(ip);
if (ver === 4) {
return lookup4(utils.aton4(ip));
} else if (ver === 6) {
var ipv4 = get4mapped(ip);
if (ipv4) {
return lookup4(utils.aton4(ipv4));
} else {
return lookup6(utils.aton6(ip));
}
}
return null;
},
pretty: function(n) {
if (typeof n === 'string') {
return n;
} else if (typeof n === 'number') {
return utils.ntoa4(n);
} else if (n instanceof Array) {
return utils.ntoa6(n);
}
return n;
},
// Start watching for data updates. The watcher waits one minute for file transfer to
// completete before triggering the callback.
startWatchingDataUpdate: function (callback) {
fsWatcher.makeFsWatchFilter(watcherName, geodatadir, 60*1000, function () {
preloadAsync()
.then(function () { return preload6Async(); })
.then(function () { callback(null); }, callback);
});
},
// Stop watching for data updates.
stopWatchingDataUpdate: function () {
fsWatcher.stopWatching(watcherName);
},
//clear data
clear: function () {
cache4 = { ...conf4 };
cache6 = { ...conf6 };
},
// Reload data synchronously
reloadDataSync: function () {
preload();
preload6();
},
// Reload data asynchronously
reloadData: function (callback) {
preloadAsync()
.then(function () { return preload6Async(); })
.then(function () { callback(null); }, callback);
},
};
preload();
preload6();