geoip-lite2
Version:
Fast, native implementation of the GeoIP API from MaxMind in JavaScript. An improved version by Sefinek, continuously maintained.
229 lines (188 loc) • 7 kB
JavaScript
const aton4 = a => {
const parts = a.split('.');
return ((Number.parseInt(parts[0], 10) << 24) >>> 0) + ((Number.parseInt(parts[1], 10) << 16) >>> 0) + ((Number.parseInt(parts[2], 10) << 8) >>> 0) + (Number.parseInt(parts[3], 10) >>> 0);
};
const ntoa4 = n => ((n >>> 24) & 0xff) + '.' + ((n >>> 16) & 0xff) + '.' + ((n >>> 8) & 0xff) + '.' + (n & 0xff);
const aton6 = a => {
let parts;
const omitStart = a.indexOf('::');
if (omitStart >= 0) {
const left = a.slice(0, omitStart);
const right = a.slice(omitStart + 2);
const leftParts = left ? left.split(':') : [];
const rightParts = right ? right.split(':') : [];
const omitted = 8 - leftParts.length - rightParts.length;
parts = leftParts.concat(new Array(Math.max(omitted, 0)).fill('0'), rightParts);
} else {
parts = a.split(':');
}
for (let i = 0; i < 8; i++) {
parts[i] = Number.parseInt(parts[i] || '0', 16);
}
return [
((parts[0] << 16) + parts[1]) >>> 0,
((parts[2] << 16) + parts[3]) >>> 0,
((parts[4] << 16) + parts[5]) >>> 0,
((parts[6] << 16) + parts[7]) >>> 0,
];
};
const ipv4ToUint32Strict = ip => {
const octets = ip.split('.');
if (octets.length !== 4) throw new TypeError(`Invalid IPv4 address: ${ip}`);
for (let i = 0; i < octets.length; i++) {
const octet = Number.parseInt(octets[i], 10);
if (!Number.isInteger(octet) || octet < 0 || octet > 255) throw new TypeError(`Invalid IPv4 address: ${ip}`);
}
return aton4(ip);
};
const ipv4RangeFromCidr = cidr => {
const [ip, prefixText] = cidr.split('/');
const prefix = Number.parseInt(prefixText, 10);
if (!Number.isInteger(prefix) || prefix < 0 || prefix > 32) throw new TypeError(`Invalid IPv4 CIDR: ${cidr}`);
const address = ipv4ToUint32Strict(ip);
const mask = prefix === 0 ? 0 : (0xFFFFFFFF << (32 - prefix)) >>> 0;
const start = (address & mask) >>> 0;
const hostMask = (~mask) >>> 0;
const end = (start | hostMask) >>> 0;
return [start, end];
};
const normalizeIpv6WithEmbeddedIpv4 = ip => {
if (!ip.includes('.')) return ip;
const lastColon = ip.lastIndexOf(':');
if (lastColon === -1) throw new TypeError(`Invalid IPv6 address: ${ip}`);
const ipv4Part = ip.slice(lastColon + 1);
const ipv4Int = ipv4ToUint32Strict(ipv4Part);
const high = ((ipv4Int >>> 16) & 0xFFFF).toString(16);
const low = (ipv4Int & 0xFFFF).toString(16);
return `${ip.slice(0, lastColon)}:${high}:${low}`;
};
const ipv6ToBigInt = ip => {
const normalized = normalizeIpv6WithEmbeddedIpv4(ip).toLowerCase();
const hasCompression = normalized.includes('::');
if (hasCompression && normalized.indexOf('::') !== normalized.lastIndexOf('::')) throw new TypeError(`Invalid IPv6 address: ${ip}`);
let groups;
if (hasCompression) {
const [leftRaw, rightRaw] = normalized.split('::');
const left = leftRaw ? leftRaw.split(':') : [];
const right = rightRaw ? rightRaw.split(':') : [];
const omitted = 8 - left.length - right.length;
if (omitted < 0) throw new TypeError(`Invalid IPv6 address: ${ip}`);
groups = [...left, ...new Array(omitted).fill('0'), ...right];
} else {
groups = normalized.split(':');
}
if (groups.length !== 8) throw new TypeError(`Invalid IPv6 address: ${ip}`);
let value = 0n;
for (let i = 0; i < groups.length; i++) {
const group = groups[i] || '0';
const groupValue = Number.parseInt(group, 16);
if (!Number.isInteger(groupValue) || groupValue < 0 || groupValue > 0xFFFF) throw new TypeError(`Invalid IPv6 address: ${ip}`);
value = (value << 16n) + BigInt(groupValue);
}
return value;
};
const bigIntToIpv6Uint32Array = value => {
return [
Number((value >> 96n) & 0xFFFFFFFFn),
Number((value >> 64n) & 0xFFFFFFFFn),
Number((value >> 32n) & 0xFFFFFFFFn),
Number(value & 0xFFFFFFFFn),
];
};
const ipv6RangeFromCidr = cidr => {
const [ip, prefixText] = cidr.split('/');
const prefix = Number.parseInt(prefixText, 10);
if (!Number.isInteger(prefix) || prefix < 0 || prefix > 128) throw new TypeError(`Invalid IPv6 CIDR: ${cidr}`);
const address = ipv6ToBigInt(ip);
const hostBits = 128n - BigInt(prefix);
const networkMask = prefix === 0 ? 0n : ((1n << BigInt(prefix)) - 1n) << hostBits;
const start = address & networkMask;
const end = start | (hostBits === 0n ? 0n : ((1n << hostBits) - 1n));
return [
bigIntToIpv6Uint32Array(start),
bigIntToIpv6Uint32Array(end),
];
};
const ntoa6 = n => {
let a = '[';
for (let i = 0; i < n.length; i++) {
a += (n[i] >>> 16).toString(16) + ':';
a += (n[i] & 0xffff).toString(16) + ':';
}
a = a.replace(/:$/, ']').replace(/:0+/g, ':').replace(/::+/, '::');
return a;
};
const cmp6 = (a, b) => {
for (let ii = 0; ii < 4; ii++) {
const av = a[ii] ?? 0;
const bv = b[ii] ?? 0;
if (av < bv) return -1;
if (av > bv) return 1;
}
return 0;
};
const cmp = (a, b) => {
if (typeof a === 'number' && typeof b === 'number') return (a < b ? -1 : (a > b ? 1 : 0));
if (Array.isArray(a) && Array.isArray(b)) return cmp6(a, b);
return null;
};
const NO_LOCATION_INFO = -1 >>> 0;
const removeNullTerminator = str => {
const nullIndex = str.indexOf('\0');
return nullIndex === -1 ? str : str.substring(0, nullIndex);
};
const readIp6 = (buffer, line, recordSize, ipIndex) => {
const base = (line * recordSize) + (ipIndex * 16);
return [
buffer.readUInt32BE(base),
buffer.readUInt32BE(base + 4),
buffer.readUInt32BE(base + 8),
buffer.readUInt32BE(base + 12),
];
};
const createGeoData = () => ({
country: '',
region: '',
isEu: false,
timezone: '',
city: '',
ll: [null, null],
metro: null,
area: null,
});
const populateGeoDataFromLocation = ({
geoData,
locationBuffer,
locationRecordSize,
locationId,
coordBuffer,
latitudeOffset,
longitudeOffset,
areaOffset,
}) => {
if (locationId >= NO_LOCATION_INFO) return;
const locOffset = locationId * locationRecordSize;
geoData.country = removeNullTerminator(locationBuffer.toString('utf8', locOffset, locOffset + 2));
geoData.region = removeNullTerminator(locationBuffer.toString('utf8', locOffset + 2, locOffset + 5));
geoData.metro = locationBuffer.readInt32BE(locOffset + 5);
geoData.ll[0] = coordBuffer.readInt32BE(latitudeOffset) / 10000;
geoData.ll[1] = coordBuffer.readInt32BE(longitudeOffset) / 10000;
geoData.area = coordBuffer.readUInt32BE(areaOffset);
geoData.isEu = removeNullTerminator(locationBuffer.toString('utf8', locOffset + 9, locOffset + 10)) === '1';
geoData.timezone = removeNullTerminator(locationBuffer.toString('utf8', locOffset + 10, locOffset + 42));
geoData.city = removeNullTerminator(locationBuffer.toString('utf8', locOffset + 42, locOffset + locationRecordSize));
};
module.exports = {
aton4,
ntoa4,
aton6,
ipv4RangeFromCidr,
ipv6RangeFromCidr,
ntoa6,
cmp,
cmp6,
removeNullTerminator,
readIp6,
createGeoData,
populateGeoDataFromLocation,
};