import { isIP } from 'node:net'; export async function getGeoInfo(ip: string, headers: Headers) { // 1. Try platform-specific headers first (Vercel, Cloudflare, etc.) const headerCountry = headers.get('x-vercel-ip-country') || headers.get('cf-ipcountry') || headers.get('x-country-code'); if (headerCountry && headerCountry !== 'XX') { return { country: headerCountry, city: headers.get('x-vercel-ip-city') || 'Unknown' }; } const cleanIp = ip.trim(); // 2. Handle localhost if (cleanIp === '127.0.0.1' || cleanIp === '::1' || cleanIp.includes('127.0.0.1')) { return { country: 'Local / Dev', city: 'Localhost' }; } // Validate IPv4/IPv6 syntax to prevent SSRF / path traversal via spoofed X-Forwarded-For if (isIP(cleanIp) === 0) { return { country: 'Unknown', city: 'Unknown' }; } // 3. Fallback to external API try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 2000); // 2s timeout const response = await fetch(`http://ip-api.com/json/${encodeURIComponent(cleanIp)}`, { signal: controller.signal, headers: { 'Accept-Encoding': 'gzip, deflate' }, }); clearTimeout(timeoutId); if (response.ok) { const data = await response.json(); if (data.status === 'success') { return { country: data.country || 'Unknown', city: data.city || 'Unknown' }; } } } catch (error) { console.error('GeoIP lookup error:', error); } return { country: 'Unknown', city: 'Unknown' }; }