UNPKG

ip2ldb-reader

Version:
938 lines (929 loc) 29.6 kB
// src/db-reader.ts import fs from "fs"; // src/ip-utils.ts import net from "net"; var FROM_6TO4 = BigInt("42545680458834377588178886921629466624"); var TO_6TO4 = BigInt("42550872755692912415807417417958686719"); var FROM_TEREDO = BigInt("42540488161975842760550356425300246528"); var TO_TEREDO = BigInt("42540488241204005274814694018844196863"); var LAST_32BITS = BigInt("4294967295"); function parseIp(ip) { let ipVersion = net.isIP(ip); let ipNum = BigInt(0); if (ipVersion === 6) { if (/^[:0]+:F{4}:(\d+\.){3}\d+$/i.test(ip)) { ip = ip.replace(/^[:0]+:F{4}:/i, ""); ipVersion = net.isIP(ip); } else if (/^[:0]+F{4}(:[\dA-Z]{4}){2}$/i.test(ip)) { const tmp = ip.replace(/^[:0]+F{4}:/i, "").replace(/:/, ""); ip = (tmp.match(/../g) ?? []).map((b) => parseInt("0x" + b)).join("."); ipVersion = net.isIP(ip); } } if (ipVersion) { ({ ipNum, ipVersion } = getIpNum(ip, ipVersion)); } return { ip, ipVersion, ipNum }; } function getIpNum(ip, ipVersion) { let ipNum = BigInt(0); if (ipVersion === 4) { const d = ip.split("."); ipNum = BigInt(((+d[0] * 256 + +d[1]) * 256 + +d[2]) * 256 + +d[3]); } else if (ipVersion === 6) { const maxsections = 8; const sectionbits = 16; const m = ip.split("::"); let total = BigInt(0); if (m.length === 2) { const arrLeft = m[0] !== "" ? m[0].split(":") : []; const arrRight = m[1] !== "" ? m[1].split(":") : []; for (let x = 0; x < arrLeft.length; x++) { total += BigInt(parseInt("0x" + arrLeft[x])) << BigInt((maxsections - (x + 1)) * sectionbits); } for (let x = 0; x < arrRight.length; x++) { total += BigInt(parseInt("0x" + arrRight[x])) << BigInt((arrRight.length - (x + 1)) * sectionbits); } } else if (m.length === 1) { const arr = m[0].split(":"); for (let x = 0; x < arr.length; x++) { total += BigInt(parseInt("0x" + arr[x])) << BigInt((maxsections - (x + 1)) * sectionbits); } } ipNum = total; if (ipNum >= FROM_6TO4 && ipNum <= TO_6TO4) { ipNum = ipNum >> BigInt(80) & LAST_32BITS; ipVersion = 4; } else if (ipNum >= FROM_TEREDO && ipNum <= TO_TEREDO) { ipNum = ~ipNum & LAST_32BITS; ipVersion = 4; } } return { ipNum, ipVersion }; } // src/db-reader.ts var Position = { country: [0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2], region: [0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3], city: [0, 0, 0, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4], isp: [0, 0, 3, 0, 5, 0, 7, 5, 7, 0, 8, 0, 9, 0, 9, 0, 9, 0, 9, 7, 9, 0, 9, 7, 9, 9, 9], latitude: [0, 0, 0, 0, 0, 5, 5, 0, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5], longitude: [0, 0, 0, 0, 0, 6, 6, 0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6], domain: [0, 0, 0, 0, 0, 0, 0, 6, 8, 0, 9, 0, 10, 0, 10, 0, 10, 0, 10, 8, 10, 0, 10, 8, 10, 10, 10], zipcode: [0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7, 7, 7, 0, 7, 7, 7, 0, 7, 0, 7, 7, 7, 0, 7, 7, 7], timezone: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 7, 8, 8, 8, 7, 8, 0, 8, 8, 8, 0, 8, 8, 8], netspeed: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 11, 0, 11, 8, 11, 0, 11, 0, 11, 0, 11, 11, 11], iddcode: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 12, 0, 12, 0, 12, 9, 12, 0, 12, 12, 12], areacode: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 13, 0, 13, 0, 13, 10, 13, 0, 13, 13, 13], weatherstationcode: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 14, 0, 14, 0, 14, 0, 14, 14, 14], weatherstationname: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 15, 0, 15, 0, 15, 0, 15, 15, 15], mcc: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 16, 0, 16, 9, 16, 16, 16], mnc: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 17, 0, 17, 10, 17, 17, 17], mobilebrand: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 18, 0, 18, 11, 18, 18, 18], elevation: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 19, 0, 19, 19, 19], usagetype: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 20, 20, 20], addresstype: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 21], category: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 22], district: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23], asn: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24], as: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 25] }; var MAX_SIZE = 65536; var MAX_IPV4_RANGE = BigInt("4294967295"); var MAX_IPV6_RANGE = BigInt("340282366920938463463374607431768211455"); var DbReader = class { readerStatus_; dbPath_; cacheInMemory_; fd_; dbCache_; fsWatcher_; indiciesIPv4_; indiciesIPv6_; offset_; enabled_; dbStats_; constructor() { this.readerStatus_ = 0 /* NotInitialized */; this.dbPath_ = null; this.cacheInMemory_ = false; this.fd_ = null; this.dbCache_ = null; this.fsWatcher_ = null; this.indiciesIPv4_ = []; this.indiciesIPv6_ = []; this.offset_ = {}; this.enabled_ = {}; this.dbStats_ = { DBType: 0, DBColumn: 0, DBYear: 0, DBMonth: 0, DBDay: 0, DBCount: 0, DBCountIPv6: 0, BaseAddr: 0, BaseAddrIPv6: 0, IndexBaseAddr: 0, IndexBaseAddrIPv6: 0, ColumnSize: 0, ColumnSizeIPv6: 0, ProductCode: 0, ProductType: 0, FileSize: 0, Indexed: false, IndexedIPv6: false, OldBIN: false }; } /** * Read data from database into a buffer * @param readbytes Number of bytes to read * @param pos Offset from beginning of database */ readToBuffer(readbytes, pos) { if (this.dbCache_) { const buff2 = this.dbCache_.subarray(pos, pos + readbytes); return buff2.length === readbytes ? buff2 : void 0; } if (!this.fd_) { throw new Error("Missing file descriptor, cannot read data"); } const buff = Buffer.alloc(readbytes); const totalread = fs.readSync(this.fd_, buff, 0, readbytes, pos); return totalread === readbytes ? buff : void 0; } /** * Read 8-bit integer from the database * @param pos Offset from beginning of database */ readInt8(pos) { const buff = this.readToBuffer(1, pos - 1); return buff ? buff.readUInt8(0) : void 0; } /** * Read 32-bit integer from the database * @param pos Offset from beginning of database */ readInt32(pos) { const buff = this.readToBuffer(4, pos - 1); return buff ? buff.readUInt32LE(0) : void 0; } /** * Read 32-bit integer from the database as a BigInt * @param pos Offset from beginning of database */ readInt32Big(pos) { const buff = this.readToBuffer(4, pos - 1); return buff ? BigInt(buff.readUInt32LE(0)) : void 0; } /** * Read 32-bit float from the database * @param pos Offset from beginning of database */ readFloat(pos) { const buff = this.readToBuffer(4, pos - 1); return buff ? buff.readFloatLE(0) : void 0; } /** * Read 128-bit integer from the database as a BigInt * @param pos Offset from beginning of database */ readInt128Big(pos) { const buff = this.readToBuffer(16, pos - 1); if (!buff) { return; } let ret = BigInt(0); for (let x = 0; x < 16; x++) { ret += BigInt(buff.readUInt8(x)) << BigInt(8 * x); } return ret; } /** * Read string from the database * @param pos Offset from beginning of database */ readString(pos) { const strBytes = this.readInt8(pos + 1); if (!strBytes) { return; } const buff = this.readToBuffer(strBytes, pos + 1); const str = buff ? buff.toString("utf8") : void 0; return str === "-" ? "" : str; } /** * Read 32-bit integer from a Buffer * @param pos Offset from beginning of buffer * @param buff Buffer */ readBufferInt32(pos, buff) { return buff.readUInt32LE(pos); } /** * Read 32-bit float from a buffer * @param pos Offset from beginning of buffer * @param buff Buffer */ readBufferFloat(pos, buff) { return buff.readFloatLE(pos); } /** * (Re)load database */ loadDatabase() { if (!this.dbPath_) { throw new Error("Path to database not available"); } this.readerStatus_ = 1 /* Initializing */; if (this.fd_ !== null) { try { fs.closeSync(this.fd_); } catch { } } if (this.cacheInMemory_) { this.fd_ = null; this.dbCache_ = fs.readFileSync(this.dbPath_); } else { this.fd_ = fs.openSync(this.dbPath_, "r"); this.dbCache_ = null; } this.dbStats_.DBType = this.readInt8(1) ?? 0; this.dbStats_.DBColumn = this.readInt8(2) ?? 0; this.dbStats_.DBYear = this.readInt8(3) ?? 0; this.dbStats_.DBMonth = this.readInt8(4) ?? 0; this.dbStats_.DBDay = this.readInt8(5) ?? 0; this.dbStats_.DBCount = this.readInt32(6) ?? 0; this.dbStats_.BaseAddr = this.readInt32(10) ?? 0; this.dbStats_.DBCountIPv6 = this.readInt32(14) ?? 0; this.dbStats_.BaseAddrIPv6 = this.readInt32(18) ?? 0; this.dbStats_.IndexBaseAddr = this.readInt32(22) ?? 0; this.dbStats_.IndexBaseAddrIPv6 = this.readInt32(26) ?? 0; this.dbStats_.ProductCode = this.readInt8(30) ?? 0; this.dbStats_.ProductType = this.readInt8(31) ?? 0; this.dbStats_.FileSize = this.readInt32(32) ?? 0; if (this.dbStats_.ProductCode !== 1 && this.dbStats_.DBYear >= 21) { throw new Error( "Incorrect IP2Location BIN file format. Please make sure that you are using the latest IP2Location BIN file." ); } if (this.dbStats_.DBType == "P".charCodeAt(0) && this.dbStats_.DBColumn === "K".charCodeAt(0)) { throw new Error("Incorrect IP2Location BIN file format. Please uncompress zip file."); } this.dbStats_.Indexed = this.dbStats_.IndexBaseAddr > 0; this.dbStats_.OldBIN = !this.dbStats_.DBCountIPv6; this.dbStats_.IndexedIPv6 = !this.dbStats_.OldBIN && this.dbStats_.IndexBaseAddrIPv6 > 0; this.dbStats_.ColumnSize = this.dbStats_.DBColumn << 2; this.dbStats_.ColumnSizeIPv6 = 16 + (this.dbStats_.DBColumn - 1 << 2); const dbType = this.dbStats_.DBType; for (const key of Object.keys(Position)) { this.offset_[key] = Position[key][dbType] ? Position[key][dbType] - 2 << 2 : 0; this.enabled_[key] = Boolean(Position[key][dbType]); } this.indiciesIPv4_ = new Array(MAX_SIZE); this.indiciesIPv6_ = new Array(MAX_SIZE); if (this.dbStats_.Indexed) { let pointer = this.dbStats_.IndexBaseAddr; for (let x = 0; x < MAX_SIZE; x++) { this.indiciesIPv4_[x] = [this.readInt32(pointer) ?? 0, this.readInt32(pointer + 4) ?? 0]; pointer += 8; } if (this.dbStats_.IndexedIPv6) { for (let x = 0; x < MAX_SIZE; x++) { this.indiciesIPv6_[x] = [this.readInt32(pointer) ?? 0, this.readInt32(pointer + 4) ?? 0]; pointer += 8; } } } this.readerStatus_ = 2 /* Ready */; } /** * Get reader status */ get readerStatus() { return this.readerStatus_; } /** * Close database and uninitialize reader */ close() { this.readerStatus_ = 0 /* NotInitialized */; if (this.fd_ !== null) { try { fs.closeSync(this.fd_); } catch { } } if (this.fsWatcher_ !== null) { this.fsWatcher_.close(); } this.dbPath_ = null; this.cacheInMemory_ = false; this.fd_ = null; this.dbCache_ = null; this.fsWatcher_ = null; this.indiciesIPv4_ = []; this.indiciesIPv6_ = []; this.offset_ = {}; this.enabled_ = {}; this.dbStats_ = { DBType: 0, DBColumn: 0, DBYear: 0, DBMonth: 0, DBDay: 0, DBCount: 0, DBCountIPv6: 0, BaseAddr: 0, BaseAddrIPv6: 0, IndexBaseAddr: 0, IndexBaseAddrIPv6: 0, ColumnSize: 0, ColumnSizeIPv6: 0, ProductCode: 0, ProductType: 0, FileSize: 0, Indexed: false, IndexedIPv6: false, OldBIN: false }; } /** * Initialize IP2Location database reader * @param dbPath IP2Location BIN database * @param options Options for database reader */ init(dbPath, options) { if (!dbPath) { throw new Error("Must specify path to database"); } this.dbPath_ = dbPath; this.cacheInMemory_ = options?.cacheDatabaseInMemory ?? false; this.loadDatabase(); if (options?.reloadOnDbUpdate) { this.watchDbFile(); } } /** * Watch database file for changes and re-init if a change is detected */ watchDbFile() { let timeout = null; const originalState = this.readerStatus_; const dbChangeHandler = (filename) => { if (filename && this.dbPath_ && fs.existsSync(this.dbPath_)) { this.fsWatcher_?.close(); this.loadDatabase(); this.fsWatcher_ = getFsWatch(); } else { this.readerStatus_ = originalState; } }; const getFsWatch = () => { if (!this.dbPath_) { throw new Error("Path to database not available"); } return fs.watch(this.dbPath_, (eventType, filename) => { if (!filename) { return; } if (this.fd_) { this.readerStatus_ = 1 /* Initializing */; } if (timeout !== null) { clearTimeout(timeout); } timeout = setTimeout(() => { timeout = null; dbChangeHandler(filename); }, 500); }); }; this.fsWatcher_ = getFsWatch(); } /** * Populate data object with database query results for an IP address * @param ipNum IP number * @param ipVersion IP version * @param data Output data object */ query(ipNum, ipVersion, data) { let low = 0; let high; let maxIpRange; let baseAddr; let columnSize; if (ipVersion === 6) { maxIpRange = MAX_IPV6_RANGE; high = this.dbStats_.DBCountIPv6; baseAddr = this.dbStats_.BaseAddrIPv6; columnSize = this.dbStats_.ColumnSizeIPv6; if (this.dbStats_.IndexedIPv6) { const indexaddr = Number(ipNum >> BigInt(112)); low = this.indiciesIPv6_[indexaddr][0]; high = this.indiciesIPv6_[indexaddr][1]; } } else { maxIpRange = MAX_IPV4_RANGE; high = this.dbStats_.DBCount; baseAddr = this.dbStats_.BaseAddr; columnSize = this.dbStats_.ColumnSize; if (this.dbStats_.Indexed) { const indexaddr = Number(ipNum) >>> 16; low = this.indiciesIPv4_[indexaddr][0]; high = this.indiciesIPv4_[indexaddr][1]; } } if (ipNum >= maxIpRange) { ipNum = maxIpRange - BigInt(1); } while (low <= high) { const mid = Math.floor((low + high) / 2); const rowoffset = baseAddr + mid * columnSize; const rowoffset2 = rowoffset + columnSize; const ipfrom = ipVersion === 6 ? this.readInt128Big(rowoffset) : this.readInt32Big(rowoffset); const ipto = ipVersion === 6 ? this.readInt128Big(rowoffset2) : this.readInt32Big(rowoffset2); if (ipfrom === void 0 || ipto === void 0) { break; } if (ipfrom <= ipNum && ipto > ipNum) { const firstcol = ipVersion === 6 ? 16 : 4; const buff = this.readToBuffer(columnSize - firstcol, rowoffset + firstcol - 1); if (!buff) { break; } const enabledKeys = Object.keys(this.enabled_).filter( (key) => this.enabled_[key] ); for (const key of enabledKeys) { if (key === "country") { const countrypos = this.readBufferInt32(this.offset_[key], buff); data.country_short = this.readString(countrypos) ?? ""; data.country_long = this.readString(countrypos + 3) ?? ""; } else if (key === "longitude" || key === "latitude") { const num = this.readBufferFloat(this.offset_[key], buff); data[key] = num !== 0 ? Math.round(num * 1e6) / 1e6 : null; } else { data[key] = this.readString(this.readBufferInt32(this.offset_[key], buff)) ?? ""; } } data.status = "OK"; return; } else { if (ipfrom > ipNum) { high = mid - 1; } else { low = mid + 1; } } } data.status = "IP_ADDRESS_NOT_FOUND"; } /** * Query IP2Location database with an IP and get location information * @param ip IP address */ get(ip) { const data = { ip, ip_no: "", status: "" }; if (this.readerStatus_ === 0 /* NotInitialized */) { data.status = "NOT_INITIALIZED"; } else if (this.readerStatus_ === 1 /* Initializing */) { data.status = "INITIALIZING"; } else if (!this.dbPath_ || !this.dbCache_ && !fs.existsSync(this.dbPath_)) { data.status = "DATABASE_NOT_FOUND"; } else if (!this.dbStats_.DBType) { data.status = "NOT_INITIALIZED"; } if (data.status) { return data; } let ipVersion; let ipNum; ({ ip, ipVersion, ipNum } = parseIp(ip)); if (!ipVersion) { data.status = "INVALID_IP_ADDRESS"; } else if (ipVersion === 6 && this.dbStats_.OldBIN) { data.status = "IPV6_NOT_SUPPORTED"; } if (data.status) { return data; } data.ip_no = ipNum.toString(); this.query(ipNum, ipVersion, data); return data; } }; // src/csv-reader.ts import fs2 from "fs"; import csvParser from "csv-parser"; var CsvReader = class { readerStatus_; fsWatcher_; reloadPromise_; requiredCsvHeaders_; constructor() { this.readerStatus_ = 0 /* NotInitialized */; this.fsWatcher_ = null; this.reloadPromise_ = Promise.resolve(); this.requiredCsvHeaders_ = []; } /** * Load IP2Location CSV databas * @param csvPath Filesystem path to IP2Location CSV database */ async loadCsv(csvPath) { const parser = fs2.createReadStream(csvPath).pipe(csvParser()); let firstRecord = true; for await (const record of parser) { const inputData = record; if (firstRecord && Object.keys(inputData).filter((key) => this.requiredCsvHeaders_.includes(key)).length !== this.requiredCsvHeaders_.length) { throw new Error("CSV database does not have expected headings"); } firstRecord = false; this.processRecord(inputData); } } /** * Get reader status */ get readerStatus() { return this.readerStatus_; } /** * Get DB reload promise */ get reloadPromise() { return this.reloadPromise_; } /** * Close database and uninitialize reader */ close() { this.readerStatus_ = 0 /* NotInitialized */; if (this.fsWatcher_ !== null) { this.fsWatcher_.close(); } this.fsWatcher_ = null; } /** * Initialize reader * @param dbPath IP2Location CSV database * @param reloadOnDbUpdate Options for database reader */ async init(dbPath, reloadOnDbUpdate) { if (!dbPath) { throw new Error("Must specify path to CSV database"); } this.readerStatus_ = 1 /* Initializing */; await this.loadCsv(dbPath); if (reloadOnDbUpdate) { this.watchDbFile(dbPath); } this.readerStatus_ = 2 /* Ready */; } /** * Watch database file for changes and re-init if a change is detected * @param dbPath Path to watch */ watchDbFile(dbPath) { let timeout = null; const dbChangeHandler = (filename) => { if (filename && fs2.existsSync(dbPath)) { if (this.fsWatcher_ !== null) { this.fsWatcher_.close(); this.fsWatcher_ = null; } this.reloadPromise_ = this.init(dbPath, true).catch(() => void 0); } }; this.fsWatcher_ = fs2.watch(dbPath, (eventType, filename) => { if (!filename) { return; } if (timeout !== null) { clearTimeout(timeout); } timeout = setTimeout(() => { timeout = null; dbChangeHandler(filename); }, 500); }); } }; // src/subdiv-reader.ts var SubdivReader = class extends CsvReader { subdivisionMap_; constructor() { super(); this.subdivisionMap_ = {}; this.requiredCsvHeaders_ = ["country_code", "subdivision_name", "code"]; } /** * Process line from IP2Location subdivision database * @param record Individual row from CSV database, broken into key/value pairs based on CSV headers */ processRecord(record) { const { country_code, subdivision_name, code } = record; const subdivisionCode = code?.length > 3 ? code.substring(3) : void 0; if (!subdivisionCode) { return; } const countryMap = this.subdivisionMap_[country_code] ?? {}; countryMap[subdivision_name] = subdivisionCode; this.subdivisionMap_[country_code] = countryMap; } /** * Close database and uninitialize reader */ close() { super.close(); this.subdivisionMap_ = {}; } /** * Get the ISO 3166-2 subdivision part from a country code and region * @param country ISO 3166-1 country code from IP2Location database * @param region Region from from IP2Location database */ get(country, region) { if (this.readerStatus !== 2 /* Ready */) { return null; } if (!country || !region) { return null; } return this.subdivisionMap_[country]?.[region] ?? null; } }; // src/geonameid-reader.ts var GeoNameIdReader = class extends CsvReader { geoNameIdMap_; constructor() { super(); this.geoNameIdMap_ = {}; this.requiredCsvHeaders_ = ["country_code", "region_name", "city_name", "geonameid"]; } /** * Process line from IP2Location GeoName ID database * @param record Individual row from CSV database, broken into key/value pairs based on CSV headers */ processRecord(record) { const { country_code, region_name, city_name, geonameid } = record; if (!/^\d+$/.test(geonameid)) { return; } const countryMap = this.geoNameIdMap_[country_code] ?? {}; const regionMap = countryMap[region_name] ?? {}; regionMap[city_name] = parseInt(geonameid); countryMap[region_name] = regionMap; this.geoNameIdMap_[country_code] = countryMap; } /** * Close database and uninitialize reader */ close() { super.close(); this.geoNameIdMap_ = {}; } /** * Get the GeoName ID from a country code, region, and city * @param country ISO 3166-1 country code from IP2Location database * @param region Region from from IP2Location database * @param city City from IP2Location database */ get(country, region, city) { if (this.readerStatus !== 2 /* Ready */) { return null; } if (!country || !region || !city) { return null; } return this.geoNameIdMap_[country]?.[region]?.[city] ?? null; } }; // src/country-info-reader.ts var CountryInfoReader = class extends CsvReader { countryInfoMap_; constructor() { super(); this.countryInfoMap_ = {}; this.requiredCsvHeaders_ = ["country_code", "capital", "total_area"]; } /** * Process line from IP2Location country info database * @param record Individual row from CSV database, broken into key/value pairs based on CSV headers */ processRecord(record) { const normalizeStr = (val) => { return val.replace(/^-$/, ""); }; const normalizeNum = (val) => { const numStr = normalizeStr(val).trim(); const num = numStr ? Number(numStr) : null; return num != null && !Number.isNaN(num) ? num : null; }; const countryInfoData = { country_code: "", capital: "", total_area: null }; for (const key in record) { countryInfoData[key] = [ "country_numeric_code", "total_area", "population", "idd_code" ].includes(key) ? normalizeNum(record[key]) : normalizeStr(record[key]); } if (countryInfoData.country_code) { this.countryInfoMap_[countryInfoData.country_code] = countryInfoData; } } /** * Close database and uninitialize reader */ close() { super.close(); this.countryInfoMap_ = {}; } /** * Get country info from a country code * @param country ISO 3166-1 country code from IP2Location database */ get(country) { if (this.readerStatus !== 2 /* Ready */) { return null; } if (!country) { return null; } return this.countryInfoMap_[country] ?? null; } }; // src/iata-icao-reader.ts var IataIcaoReader = class extends CsvReader { iataIcaoMap_; constructor() { super(); this.iataIcaoMap_ = {}; this.requiredCsvHeaders_ = [ "country_code", "region_name", "iata", "icao", "latitude", "longitude" ]; } /** * Process line from IP2Location IATA/ICAO airport database * @param record Individual row from CSV database, broken into key/value pairs based on CSV headers */ processRecord(record) { const normalizeStr = (val) => { return val.replace(/^-$/, ""); }; const normalizeNum = (val) => { const numStr = normalizeStr(val).trim(); const num = numStr ? Number(numStr) : null; return num != null && !Number.isNaN(num) ? num : null; }; const airportOutputData = { iata: "", icao: "", airport: "", latitude: null, longitude: null }; const { country_code, region_name } = record; for (const key in record) { if (["country_code", "region_name"].includes(key)) { continue; } airportOutputData[key] = ["latitude", "longitude"].includes(key) ? normalizeNum(record[key]) : normalizeStr(record[key]); } const countryMap = this.iataIcaoMap_[country_code] ?? {}; const regionArray = countryMap[region_name] ?? []; regionArray.push(airportOutputData); countryMap[region_name] = regionArray; this.iataIcaoMap_[country_code] = countryMap; } /** * Close database and uninitialize reader */ close() { super.close(); this.iataIcaoMap_ = {}; } /** * Get IATA/ICAO airport info from country code and region name * @param country ISO 3166-1 country code from IP2Location database * @param region Region from from IP2Location database */ get(country, region) { if (this.readerStatus !== 2 /* Ready */) { return []; } if (!country || !region) { return []; } return this.iataIcaoMap_[country]?.[region] ?? []; } }; // src/index.ts var Ip2lReader = class { dbReader_; subdivReader_; geoNameIdReader_; countryInfoReader_; iataIcaoReader_; constructor() { this.dbReader_ = new DbReader(); } /** * Initialize IP2Location database reader(s) * @param dbPath IP2Location BIN database * @param options Options for database reader */ async init(dbPath, options) { this.dbReader_.init(dbPath, options); if (!options) { return; } if (options.subdivisionCsvPath) { this.subdivReader_ = new SubdivReader(); await this.subdivReader_.init(options.subdivisionCsvPath, options.reloadOnDbUpdate); } if (options.geoNameIdCsvPath) { this.geoNameIdReader_ = new GeoNameIdReader(); await this.geoNameIdReader_.init(options.geoNameIdCsvPath, options.reloadOnDbUpdate); } if (options.countryInfoCsvPath) { this.countryInfoReader_ = new CountryInfoReader(); await this.countryInfoReader_.init(options.countryInfoCsvPath, options.reloadOnDbUpdate); } if (options.iataIcaoCsvPath) { this.iataIcaoReader_ = new IataIcaoReader(); await this.iataIcaoReader_.init(options.iataIcaoCsvPath, options.reloadOnDbUpdate); } } /** * Query IP2Location database with an IP and get location information * @param ip IP address */ get(ip) { const ip2lData = this.dbReader_.get(ip); if (this.subdivReader_) { if (typeof ip2lData.country_short === "string" && typeof ip2lData.region === "string") { const subdivision = this.subdivReader_.get(ip2lData.country_short, ip2lData.region); if (subdivision !== null) { ip2lData.subdivision = subdivision; } } } if (this.geoNameIdReader_) { if (typeof ip2lData.country_short === "string" && typeof ip2lData.region === "string" && typeof ip2lData.city === "string") { const geoNameId = this.geoNameIdReader_.get( ip2lData.country_short, ip2lData.region, ip2lData.city ); if (geoNameId !== null) { ip2lData.geoname_id = geoNameId; } } } if (this.countryInfoReader_) { if (typeof ip2lData.country_short === "string") { const countryInfo = this.countryInfoReader_.get(ip2lData.country_short); ip2lData.country_info = countryInfo; } } if (this.iataIcaoReader_) { if (typeof ip2lData.country_short === "string" && typeof ip2lData.region === "string") { const airports = this.iataIcaoReader_.get(ip2lData.country_short, ip2lData.region); ip2lData.airports = airports; } } return ip2lData; } /** * Close IP2Location database(s) and uninitialize reader(s) */ close() { this.dbReader_.close(); this.subdivReader_?.close(); this.geoNameIdReader_?.close(); this.countryInfoReader_?.close(); this.iataIcaoReader_?.close(); } }; export { Ip2lReader, Ip2lReader as default };