This commit is contained in:
2026-03-23 13:51:53 +01:00
parent 2c3d2a4137
commit 8762d4042c
5 changed files with 395 additions and 28 deletions
+111 -26
View File
@@ -9,6 +9,13 @@ const storage = require('./storage');
const consoleCmd = require('./console');
const { serverConfig, configSave } = require('./server_config');
let geoip = null;
try {
geoip = require('geoip-lite');
} catch (e) {
geoip = null;
}
function parseMarkdown(parsed) {
parsed = parsed.replace(/<\/?[^>]+(>|$)/g, '');
@@ -57,35 +64,100 @@ function authenticateWithXdrd(client, salt, password) {
}
const ipCache = new Map();
const ipInfoInFlight = new Map();
function fetchIpWhoisInfo(ip, timeoutMs = 1500) {
return new Promise((resolve) => {
if (!ip || !net.isIP(ip)) return resolve({});
const url = `https://ipwho.is/${encodeURIComponent(ip)}`;
const req = https.get(url, { timeout: timeoutMs }, (res) => {
if (res.statusCode !== 200) {
res.resume();
return resolve({});
}
let body = '';
res.on('data', (chunk) => (body += chunk));
res.on('end', () => {
try {
const data = JSON.parse(body);
if (!data || data.success === false) return resolve({});
const connection = data.connection || {};
const isp = connection.isp || connection.org;
const asnRaw = connection.asn;
const as =
typeof asnRaw === 'string'
? asnRaw
: typeof asnRaw === 'number'
? `AS${asnRaw}`
: undefined;
resolve({
isp: typeof isp === 'string' && isp.trim() ? isp.trim() : undefined,
as,
});
} catch (e) {
resolve({});
}
});
});
req.on('timeout', () => {
req.destroy();
resolve({});
});
req.on('error', () => resolve({}));
});
}
function handleConnect(clientIp, currentUsers, ws, callback) {
if (ipCache.has(clientIp)) {
// Use cached location info
processConnection(clientIp, ipCache.get(clientIp), currentUsers, ws, callback);
return;
}
http.get(`http://ip-api.com/json/${clientIp}`, (response) => {
let data = "";
if (ipInfoInFlight.has(clientIp)) {
ipInfoInFlight
.get(clientIp)
.then((info) => processConnection(clientIp, info, currentUsers, ws, callback))
.catch(() => processConnection(clientIp, { country: undefined }, currentUsers, ws, callback));
return;
}
response.on("data", (chunk) => {
data += chunk;
let locationInfo = { country: undefined };
if (geoip && clientIp && net.isIP(clientIp)) {
const geo = geoip.lookup(clientIp);
if (geo) {
locationInfo = {
country: geo.country,
countryCode: geo.country,
city: geo.city,
regionName: geo.region,
};
}
} else if (!geoip) {
consoleCmd.logWarn('geoip-lite is not installed; location will be Unknown.');
}
const inFlightPromise = fetchIpWhoisInfo(normalizedClientIp)
.then((whoisInfo) => {
const merged = { ...locationInfo, ...whoisInfo };
ipCache.set(normalizedClientIp, merged);
ipInfoInFlight.delete(normalizedClientIp);
return merged;
})
.catch(() => {
ipCache.set(normalizedClientIp, locationInfo);
ipInfoInFlight.delete(normalizedClientIp);
return locationInfo;
});
response.on("end", () => {
try {
const locationInfo = JSON.parse(data);
ipCache.set(clientIp, locationInfo); // Store in cache
processConnection(clientIp, locationInfo, currentUsers, ws, callback);
} catch (error) {
console.error("Error parsing location data:", error);
callback("User allowed");
}
});
}).on("error", (err) => {
consoleCmd.logError("Error fetching location data:", err.code);
callback("User allowed");
});
ipInfoInFlight.set(normalizedClientIp, inFlightPromise);
inFlightPromise.then((info) => processConnection(clientIp, info, currentUsers, ws, callback));
}
let bannedASCache = { data: null, timestamp: 0 };
@@ -131,31 +203,44 @@ const recentBannedIps = new Map(); // Store clientIp -> timestamp
function processConnection(clientIp, locationInfo, currentUsers, ws, callback) {
const options = { year: "numeric", month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" };
const connectionTime = new Date().toLocaleString([], options);
const normalizedClientIp = clientIp?.replace(/^::ffff:/, '');
fetchBannedAS((bannedAS) => {
if (bannedAS.some((as) => locationInfo.as?.includes(as))) {
const now = Date.now();
const lastSeen = recentBannedIps.get(normalizedClientIp) || 0;
const lastSeen = recentBannedIps.get(clientIp) || 0;
if (now - lastSeen > 300 * 1000) {
consoleCmd.logWarn(`Banned AS list client kicked (${normalizedClientIp})`);
recentBannedIps.set(normalizedClientIp, now);
consoleCmd.logWarn(`Banned AS list client kicked (${clientIp})`);
recentBannedIps.set(clientIp, now);
}
return callback("User banned");
}
const userLocation = locationInfo.country === undefined ? "Unknown" : `${locationInfo.city}, ${locationInfo.regionName}, ${locationInfo.countryCode}`;
const userLocation =
locationInfo && locationInfo.country !== undefined
? [
locationInfo.city,
locationInfo.regionName && /\p{L}/u.test(String(locationInfo.regionName))
? locationInfo.regionName
: undefined,
locationInfo.countryCode,
]
.filter(Boolean)
.join(', ')
: 'Unknown';
const userLocationForLog = locationInfo?.isp ? `${userLocation} (${locationInfo.isp})` : userLocation;
storage.connectedUsers.push({
ip: clientIp,
location: userLocation,
isp: locationInfo?.isp,
as: locationInfo?.as,
time: connectionTime,
instance: ws,
});
consoleCmd.logInfo(`Web client \x1b[32mconnected\x1b[0m (${normalizedClientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${userLocation}`);
consoleCmd.logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${userLocationForLog}`);
callback("User allowed");
});