import { Reader, ReaderModel, City, Asn } from '@maxmind/geoip2-node'; import { isIP } from 'net'; import path from 'path'; import fs from 'fs'; import NodeCache from 'node-cache'; import { GeoIPLocation, SimplifiedGeoIPResponse, DetailedGeoIPResponse, BatchFailedLookup, BatchGeoIPResponse, } from '../types/index'; import config from './config'; import logger from './logger'; class GeoIPService { private cityReader?: ReaderModel; private asnReader?: ReaderModel; private cache: NodeCache; private dbPath: string; private isInitialized: boolean = false; constructor() { this.cache = new NodeCache({ stdTTL: 300, checkperiod: 60 }); // 5 minutes cache this.dbPath = config.maxmindDbPath; this.initializeReader(); } private async initializeReader(): Promise { try { const cityDbPath = path.join(this.dbPath, 'GeoLite2-City.mmdb'); const asnDbPath = path.join(this.dbPath, 'GeoLite2-ASN.mmdb'); if (!fs.existsSync(cityDbPath)) { throw new Error(`GeoIP City database not found at ${cityDbPath}`); } if (!fs.existsSync(asnDbPath)) { throw new Error(`GeoIP ASN database not found at ${asnDbPath}`); } this.cityReader = await Reader.open(cityDbPath); this.asnReader = await Reader.open(asnDbPath); this.isInitialized = true; logger.info('GeoIP databases initialized successfully', { cityDbPath, asnDbPath, }); } catch (error) { logger.error('Failed to initialize GeoIP database', error as Error, { dbPath: this.dbPath, }); throw error; } } private ensureInitialized(): void { if (!this.isInitialized || !this.cityReader || !this.asnReader) { throw new Error('GeoIP service not initialized'); } } async lookupSimple(ip: string): Promise { this.ensureInitialized(); const cacheKey = `simple_${ip}`; const cached = this.cache.get(cacheKey); if (cached) { return cached; } try { const cityResponse: City = this.cityReader!.city(ip); const asnResponse: Asn = this.asnReader!.asn(ip); const result: SimplifiedGeoIPResponse = { ip, country: cityResponse.country?.names?.en || 'Unknown', country_code: cityResponse.country?.isoCode || 'Unknown', is_in_european_union: cityResponse.country?.isInEuropeanUnion || false, region: cityResponse.subdivisions?.[0]?.names?.en || 'Unknown', region_code: cityResponse.subdivisions?.[0]?.isoCode || null, city: cityResponse.city?.names?.en || 'Unknown', latitude: cityResponse.location?.latitude || null, longitude: cityResponse.location?.longitude || null, timezone: cityResponse.location?.timeZone || null, postal_code: cityResponse.postal?.code || null, continent_code: cityResponse.continent?.code || null, continent_name: cityResponse.continent?.names?.en || null, organization: asnResponse.autonomousSystemOrganization || null, asn: asnResponse.autonomousSystemNumber || null, }; this.cache.set(cacheKey, result); logger.debug('GeoIP lookup completed', { ip, result }); return result; } catch (error) { logger.error('GeoIP lookup failed', error as Error, { ip }); throw error; } } async lookupDetailed(ip: string): Promise { this.ensureInitialized(); const cacheKey = `detailed_${ip}`; const cached = this.cache.get(cacheKey); if (cached) { return cached; } try { const response: City = this.cityReader!.city(ip); const asnResponse: Asn = this.asnReader!.asn(ip); const result: DetailedGeoIPResponse = { ip, location: response as GeoIPLocation, asn: asnResponse, }; this.cache.set(cacheKey, result); logger.debug('Detailed GeoIP lookup completed', { ip }); return result; } catch (error) { logger.error('Detailed GeoIP lookup failed', error as Error, { ip }); throw error; } } async lookupBatch(ips: string[]): Promise { const results = await Promise.allSettled( ips.map(async ip => this.lookupSimple(ip)) ); const succeeded: SimplifiedGeoIPResponse[] = []; const failed: Array = []; results.forEach((result, index) => { if (result.status === 'fulfilled') { succeeded.push(result.value); } else { failed.push({ ip: ips[index] || 'unknown', error: result.reason?.message || 'Lookup failed', }); } }); return { succeeded, failed }; } /** * Validates if a string is a valid IP address (IPv4 or IPv6) * @param ip - IP address string to validate * @returns true if valid IP address, false otherwise */ isValidIP(ip: string): boolean { return isIP(ip) !== 0; } isPrivateIP(ip: string): boolean { const privateRanges = [ /^10\./, /^172\.(1[6-9]|2[0-9]|3[0-1])\./, /^192\.168\./, /^127\./, /^169\.254\./, /^::1$/, /^fc00:/, /^fe80:/, ]; return privateRanges.some(range => range.test(ip)); } async healthCheck(): Promise { try { await this.lookupSimple('8.8.8.8'); // Test with Google DNS return true; } catch (error) { logger.error('GeoIP service health check failed', error as Error); return false; } } } export const geoIPService = new GeoIPService(); export default geoIPService;