import { getLogger } from './logging.js'; /** * Service configuration for IP detection */ interface IpService { name: string; v4Url?: string; v6Url?: string; parseResponse?: (text: string) => string; } /** * PublicIp class for detecting public IPv4 and IPv6 addresses * Uses multiple fallback services for reliability */ export class PublicIp { private readonly services: IpService[] = [ { name: 'ipify', v4Url: 'https://api.ipify.org?format=text', v6Url: 'https://api6.ipify.org?format=text', }, { name: 'ident.me', v4Url: 'https://v4.ident.me', v6Url: 'https://v6.ident.me', }, { name: 'seeip', v4Url: 'https://ipv4.seeip.org', v6Url: 'https://ipv6.seeip.org', }, { name: 'icanhazip', v4Url: 'https://ipv4.icanhazip.com', v6Url: 'https://ipv6.icanhazip.com', }, ]; private readonly timeout: number; private readonly logger = getLogger(); constructor(options?: { timeout?: number }) { this.timeout = options?.timeout ?? 2000; } /** * Get public IPv4 address */ public async getPublicIpv4(): Promise { for (const service of this.services) { if (!service.v4Url) continue; try { const ip = await this.fetchIpFromService(service.v4Url, service.parseResponse); if (this.isValidIpv4(ip)) { this.logger.info?.(`Got IPv4 from ${service.name}: ${ip}`); return ip; } } catch (error) { this.logger.debug?.(`Failed to get IPv4 from ${service.name}: ${error.message}`); } } this.logger.warn?.('Could not determine public IPv4 address from any service'); return null; } /** * Get public IPv6 address */ public async getPublicIpv6(): Promise { for (const service of this.services) { if (!service.v6Url) continue; try { const ip = await this.fetchIpFromService(service.v6Url, service.parseResponse); if (this.isValidIpv6(ip)) { this.logger.info?.(`Got IPv6 from ${service.name}: ${ip}`); return ip; } } catch (error) { this.logger.debug?.(`Failed to get IPv6 from ${service.name}: ${error.message}`); } } this.logger.warn?.('Could not determine public IPv6 address from any service'); return null; } /** * Get both IPv4 and IPv6 addresses */ public async getPublicIps(): Promise<{ v4: string | null; v6: string | null }> { const [v4, v6] = await Promise.all([ this.getPublicIpv4(), this.getPublicIpv6(), ]); return { v4, v6 }; } /** * Fetch IP from a service URL */ private async fetchIpFromService( url: string, parseResponse?: (text: string) => string ): Promise { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); try { const response = await fetch(url, { signal: controller.signal, headers: { 'User-Agent': '@push.rocks/smartnetwork', }, }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const text = await response.text(); const ip = parseResponse ? parseResponse(text) : text.trim(); return ip; } finally { clearTimeout(timeoutId); } } /** * Validate IPv4 address format */ private isValidIpv4(ip: string): boolean { if (!ip) return false; const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/; if (!ipv4Regex.test(ip)) return false; const parts = ip.split('.'); return parts.every(part => { const num = parseInt(part, 10); return num >= 0 && num <= 255; }); } /** * Validate IPv6 address format */ private isValidIpv6(ip: string): boolean { if (!ip) return false; // Simplified IPv6 validation - checks for colon-separated hex values const ipv6Regex = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/; const ipv6CompressedRegex = /^::([0-9a-fA-F]{0,4}:){0,6}[0-9a-fA-F]{0,4}$|^([0-9a-fA-F]{0,4}:){1,7}:$/; return ipv6Regex.test(ip) || ipv6CompressedRegex.test(ip); } }