155 lines
4.3 KiB
JavaScript
155 lines
4.3 KiB
JavaScript
const axios = require('axios');
|
|
const net = require('net');
|
|
const dns = require('dns').promises;
|
|
const db = require('./db');
|
|
|
|
/**
|
|
* Geo Location Service
|
|
* Resolves IP addresses to geographical coordinates and country info.
|
|
* Caches results in the database to minimize API calls.
|
|
*/
|
|
|
|
const ipInfoToken = process.env.IPINFO_TOKEN;
|
|
const enableExternalGeoLookup = process.env.ENABLE_EXTERNAL_GEO_LOOKUP === 'true';
|
|
|
|
/**
|
|
* Normalizes geo data for consistent display
|
|
*/
|
|
function normalizeGeo(geo) {
|
|
if (!geo) return geo;
|
|
|
|
// Custom normalization for TW, HK, MO to "China, {CODE}"
|
|
const specialRegions = ['TW'];
|
|
if (specialRegions.includes(geo.country?.toUpperCase())) {
|
|
return {
|
|
...geo,
|
|
city: `China, ${geo.country.toUpperCase()}`,
|
|
country_name: 'China'
|
|
};
|
|
}
|
|
return geo;
|
|
}
|
|
|
|
async function getLocation(target) {
|
|
// Normalize target (strip port if present, handle IPv6 brackets)
|
|
let cleanTarget = target;
|
|
if (cleanTarget.startsWith('[')) {
|
|
const closingBracket = cleanTarget.indexOf(']');
|
|
if (closingBracket !== -1) {
|
|
cleanTarget = cleanTarget.substring(1, closingBracket);
|
|
}
|
|
} else {
|
|
const parts = cleanTarget.split(':');
|
|
if (parts.length === 2) {
|
|
cleanTarget = parts[0];
|
|
}
|
|
}
|
|
|
|
// 1. Check if we already have this IP/Domain in DB (FASTEST)
|
|
try {
|
|
const [rows] = await db.query('SELECT * FROM server_locations WHERE ip = ?', [cleanTarget]);
|
|
if (rows.length > 0) {
|
|
const data = rows[0];
|
|
const age = Date.now() - new Date(data.last_updated).getTime();
|
|
if (age < 30 * 24 * 60 * 60 * 1000) {
|
|
return normalizeGeo(data);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
// console.error(`[Geo Service] DB check failed for ${cleanTarget}`);
|
|
}
|
|
|
|
// 2. Resolve domain to IP if needed
|
|
let cleanIp = cleanTarget;
|
|
if (net.isIP(cleanTarget) === 0) {
|
|
try {
|
|
const lookup = await dns.lookup(cleanTarget);
|
|
cleanIp = lookup.address;
|
|
|
|
// Secondary DB check with resolved IP
|
|
const [rows] = await db.query('SELECT * FROM server_locations WHERE ip = ?', [cleanIp]);
|
|
if (rows.length > 0) {
|
|
return normalizeGeo(rows[0]);
|
|
}
|
|
} catch (err) {
|
|
// Quiet DNS failure for tokens (legacy bug mitigation)
|
|
if (!/^[0-9a-f]{16}$/i.test(cleanTarget)) {
|
|
console.error(`[Geo Service] DNS resolution failed for ${cleanTarget}:`, err.message);
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// 3. Skip local/reserved IPs
|
|
if (isLocalIp(cleanIp)) {
|
|
return null;
|
|
}
|
|
|
|
// 4. Resolve via ipinfo.io (LAST RESORT)
|
|
if (!enableExternalGeoLookup) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
console.log(`[Geo Service] API lookup (ipinfo.io) for: ${cleanIp}`);
|
|
const url = `https://ipinfo.io/${cleanIp}/json${ipInfoToken ? `?token=${ipInfoToken}` : ''}`;
|
|
const response = await axios.get(url, { timeout: 5000 });
|
|
const geo = normalizeGeo(response.data);
|
|
|
|
if (geo && geo.loc) {
|
|
const [lat, lon] = geo.loc.split(',').map(Number);
|
|
const locationData = {
|
|
ip: cleanIp,
|
|
country: geo.country,
|
|
country_name: geo.country_name || geo.country, // ipinfo might not have country_name in basic response
|
|
region: geo.region,
|
|
city: geo.city,
|
|
latitude: lat,
|
|
longitude: lon
|
|
};
|
|
|
|
// Save to DB
|
|
await db.query(`
|
|
INSERT INTO server_locations (ip, country, country_name, region, city, latitude, longitude)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE
|
|
country = VALUES(country),
|
|
country_name = VALUES(country_name),
|
|
region = VALUES(region),
|
|
city = VALUES(city),
|
|
latitude = VALUES(latitude),
|
|
longitude = VALUES(longitude)
|
|
`, [
|
|
locationData.ip,
|
|
locationData.country,
|
|
locationData.country_name,
|
|
locationData.region,
|
|
locationData.city,
|
|
locationData.latitude,
|
|
locationData.longitude
|
|
]);
|
|
|
|
return locationData;
|
|
}
|
|
} catch (err) {
|
|
console.error(`[Geo Service] Error resolving IP ${cleanIp}:`, err.message);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function isLocalIp(ip) {
|
|
if (ip === 'localhost' || ip === '127.0.0.1' || ip === '::1') return true;
|
|
|
|
// RFC1918 private addresses
|
|
const p1 = /^10\./;
|
|
const p2 = /^172\.(1[6-9]|2[0-9]|3[0-1])\./;
|
|
const p3 = /^192\.168\./;
|
|
|
|
return p1.test(ip) || p2.test(ip) || p3.test(ip);
|
|
}
|
|
|
|
module.exports = {
|
|
getLocation
|
|
};
|