55 lines
1.6 KiB
TypeScript
55 lines
1.6 KiB
TypeScript
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' };
|
|
}
|