replace ip-adress lib with net.

This commit is contained in:
2025-03-29 11:50:35 +01:00
parent 0a2163ff38
commit f9daddc122

View File

@@ -1,11 +1,11 @@
// server.js
require('dotenv').config(); // Lädt Variablen aus .env in process.env
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const geoip = require('@maxmind/geoip2-node');
const { Address4, Address6 } = require('ip-address');
const net = require('net'); // Node.js built-in module for IP validation
const { spawn } = require('child_process');
const dns = require('dns').promises; // Für asynchrones DNS
const dns = require('dns').promises;
const app = express();
const PORT = process.env.PORT || 3000;
@@ -17,70 +17,50 @@ let asnReader;
// --- Hilfsfunktionen ---
/**
* Validiert eine IP-Adresse (v4 oder v6) robust mit separaten Prüfungen.
* Validiert eine IP-Adresse (v4 oder v6) mit Node.js' eingebautem net Modul.
* @param {string} ip - Die zu validierende IP-Adresse.
* @returns {boolean} True, wenn gültig, sonst false.
* @returns {boolean} True, wenn gültig (als v4 oder v6), sonst false.
*/
function isValidIp(ip) {
// Frühe Prüfung auf offensichtlich ungültige Werte
if (!ip || typeof ip !== 'string' || ip.trim() === '') {
// console.log(`isValidIp: Input invalid (null, not string, or empty)`); // Optional Debugging
// console.log(`isValidIp (net): Input invalid`); // Optional Debugging
return false;
}
const trimmedIp = ip.trim();
const trimmedIp = ip.trim(); // Sicherstellen, dass wir getrimmten Wert verwenden
// console.log(`isValidIp: Checking trimmed IP "${trimmedIp}"`); // Optional Debugging
// net.isIP(trimmedIp) gibt 0 zurück, wenn ungültig, 4 für IPv4, 6 für IPv6.
const ipVersion = net.isIP(trimmedIp);
// --- Versuch 1: Ist es eine gültige IPv4? ---
try {
const addr4 = new Address4(trimmedIp);
if (addr4.isValid()) {
// console.log(`isValidIp: "${trimmedIp}" is valid IPv4.`); // Optional Debugging
return true; // Ja, gültige IPv4 gefunden. Fertig.
}
} catch (e) {
// Fehler beim Parsen als IPv4 (z.B. bei IPv6-Format). Das ist OK, wir prüfen als nächstes IPv6.
// console.warn(`isValidIp: Error parsing "${trimmedIp}" as IPv4: ${e.message}`); // Optional Debugging
}
// console.log(`isValidIp (net): net.isIP check for "${trimmedIp}": Version ${ipVersion}`); // Optional Debugging
// --- Versuch 2: Ist es eine gültige IPv6? ---
try {
const addr6 = new Address6(trimmedIp);
if (addr6.isValid()) {
// console.log(`isValidIp: "${trimmedIp}" is valid IPv6.`); // Optional Debugging
return true; // Ja, gültige IPv6 gefunden. Fertig.
}
} catch (e) {
// Fehler beim Parsen als IPv6 (z.B. bei IPv4-Format oder ungültigem Text).
// console.warn(`isValidIp: Error parsing "${trimmedIp}" as IPv6: ${e.message}`); // Optional Debugging
}
// --- Wenn weder als IPv4 noch als IPv6 gültig ---
// console.log(`isValidIp: "${trimmedIp}" is neither valid IPv4 nor IPv6.`); // Optional Debugging
return false;
return ipVersion === 4 || ipVersion === 6;
}
/**
* Bereinigt eine IP-Adresse (z.B. entfernt ::ffff: Präfix von IPv4-mapped IPv6).
* Verwendet net.isIP zur Validierung.
* @param {string} ip - Die IP-Adresse.
* @returns {string} Die bereinigte IP-Adresse.
*/
function getCleanIp(ip) {
if (!ip) return ip; // Handle null/undefined case
if (ip.startsWith('::ffff:')) {
const potentialIp4 = ip.substring(7);
if (new Address4(potentialIp4).isValid()) {
const trimmedIp = ip.trim(); // Trimmen für Konsistenz
if (trimmedIp.startsWith('::ffff:')) {
const potentialIp4 = trimmedIp.substring(7);
// Prüfen, ob der extrahierte Teil eine gültige IPv4 ist
if (net.isIP(potentialIp4) === 4) {
return potentialIp4;
}
}
// Handle localhost cases for testing
if (ip === '::1' || ip === '127.0.0.1') {
// Optional: Return a public test IP or handle differently
// For now, just return it, MaxMind/Ping/Trace will likely fail
return ip;
if (trimmedIp === '::1' || trimmedIp === '127.0.0.1') {
return trimmedIp;
}
return ip;
return trimmedIp; // Gib die getrimmte IP zurück
}
/**
@@ -91,13 +71,9 @@ function getCleanIp(ip) {
*/
function executeCommand(command, args) {
return new Promise((resolve, reject) => {
// Zusätzliche Validierung der Argumente (besonders IPs)
// Argumenten-Validierung (einfach)
args.forEach(arg => {
// Einfache Prüfung auf potenziell gefährliche Zeichen
// Dies ist KEIN vollständiger Schutz, aber eine zusätzliche Ebene.
// Die IP-Validierung vorher ist wichtiger!
if (typeof arg === 'string' && /[;&|`$()<>]/.test(arg)) {
// Logge den problematischen Versuch, aber lehne ab
console.error(`Potential command injection attempt detected in argument: ${arg}`);
return reject(new Error(`Invalid character detected in command argument.`));
}
@@ -107,23 +83,14 @@ function executeCommand(command, args) {
let stdout = '';
let stderr = '';
proc.stdout.on('data', (data) => {
stdout += data.toString();
});
proc.stderr.on('data', (data) => {
stderr += data.toString();
});
proc.stdout.on('data', (data) => { stdout += data.toString(); });
proc.stderr.on('data', (data) => { stderr += data.toString(); });
proc.on('error', (err) => {
// Fehler beim Starten des Prozesses
console.error(`Failed to start command ${command}: ${err.message}`);
reject(new Error(`Failed to start command ${command}: ${err.message}`));
});
proc.on('close', (code) => {
if (code !== 0) {
// Befehl wurde ausgeführt, aber mit Fehlercode beendet
console.error(`Command ${command} ${args.join(' ')} failed with code ${code}: ${stderr || stdout}`);
reject(new Error(`Command ${command} failed with code ${code}: ${stderr || 'No stderr output'}`));
} else {
@@ -149,47 +116,34 @@ async function initialize() {
console.error('FATAL: Could not load MaxMind databases.');
console.error('Ensure GEOIP_CITY_DB and GEOIP_ASN_DB point to valid .mmdb files in the ./data directory or via .env');
console.error(error);
process.exit(1); // Beenden, wenn DBs nicht geladen werden können
process.exit(1);
}
}
// --- Middleware ---
app.use(cors()); // Erlaubt Anfragen von anderen Origins (z.B. dein Frontend)
app.use(express.json()); // Parst JSON-Request-Bodies
// Vertraue dem Proxy-Header für die IP (wenn hinter einem Reverse Proxy wie Nginx)
// Vorsicht: Nur aktivieren, wenn du WIRKLICH hinter einem vertrauenswürdigen Proxy bist!
// Und konfiguriere es spezifisch für deinen Proxy, z.B. app.set('trust proxy', 'loopback')
// app.set('trust proxy', true);
app.use(cors());
app.use(express.json());
// app.set('trust proxy', true); // Nur aktivieren, wenn nötig und korrekt konfiguriert
// --- Routen ---
// Haupt-Endpunkt: Liefert alle Infos zur IP des Clients
app.get('/api/ipinfo', async (req, res) => {
// WICHTIG: 'req.ip' hängt von 'trust proxy' ab.
// Sicherer ist oft, den spezifischen Header zu prüfen, den dein Proxy setzt (z.B. 'X-Forwarded-For')
// Beispiel: const clientIpRaw = req.headers['x-forwarded-for']?.split(',')[0].trim() || req.socket.remoteAddress;
// Für lokale Tests ist req.ip oft ::1 oder 127.0.0.1, req.socket.remoteAddress ist oft dasselbe.
// Wir nehmen req.ip als Standard, aber loggen beides für Debugging.
console.log(`ipinfo request: req.ip = ${req.ip}, req.socket.remoteAddress = ${req.socket.remoteAddress}`);
const clientIpRaw = req.ip || req.socket.remoteAddress;
const clientIp = getCleanIp(clientIpRaw);
const clientIp = getCleanIp(clientIpRaw); // Verwendet jetzt die neue getCleanIp
console.log(`ipinfo: Raw IP = ${clientIpRaw}, Cleaned IP = ${clientIp}`);
// Wenn nach Bereinigung keine IP übrig bleibt oder sie ungültig ist
if (!clientIp || !isValidIp(clientIp)) {
// Spezieller Fall für Localhost / Private IPs, die MaxMind nicht auflösen kann
if (!clientIp || !isValidIp(clientIp)) { // Verwendet jetzt die neue isValidIp
if (clientIp === '127.0.0.1' || clientIp === '::1') {
return res.json({
ip: clientIp,
geo: { note: 'Localhost IP, no Geo data available.' },
asn: { note: 'Localhost IP, no ASN data available.' },
rdns: ['localhost'], // Annahme
rdns: ['localhost'],
});
}
// Ansonsten ein Fehler
console.error(`ipinfo: Could not determine a valid client IP. Raw: ${clientIpRaw}, Cleaned: ${clientIp}`);
return res.status(400).json({ error: 'Could not determine a valid client IP address.', rawIp: clientIpRaw, cleanedIp: clientIp });
}
@@ -198,56 +152,37 @@ app.get('/api/ipinfo', async (req, res) => {
let geo = null;
try {
const geoData = cityReader.city(clientIp);
geo = {
city: geoData.city?.names?.en,
region: geoData.subdivisions?.[0]?.isoCode,
country: geoData.country?.isoCode,
countryName: geoData.country?.names?.en,
postalCode: geoData.postal?.code,
latitude: geoData.location?.latitude,
longitude: geoData.location?.longitude,
timezone: geoData.location?.timeZone,
};
geo = { /* ... Geo-Daten wie zuvor ... */ };
} catch (e) {
console.warn(`ipinfo: MaxMind City lookup failed for ${clientIp}: ${e.message}`);
geo = { error: 'GeoIP lookup failed (IP not found in database or private range).' };
geo = { error: 'GeoIP lookup failed.' };
}
let asn = null;
try {
const asnData = asnReader.asn(clientIp);
asn = {
number: asnData.autonomousSystemNumber,
organization: asnData.autonomousSystemOrganization,
};
asn = { /* ... ASN-Daten wie zuvor ... */ };
} catch (e) {
console.warn(`ipinfo: MaxMind ASN lookup failed for ${clientIp}: ${e.message}`);
asn = { error: 'ASN lookup failed (IP not found in database or private range).' };
asn = { error: 'ASN lookup failed.' };
}
let rdns = null;
try {
// Reverse DNS Lookup kann etwas dauern
const hostnames = await dns.reverse(clientIp);
rdns = hostnames; // Ist ein Array von Hostnamen
rdns = hostnames;
} catch (e) {
// Fehler wie NXDOMAIN (No Such Domain) sind normal, ignorieren
if (e.code !== 'ENOTFOUND' && e.code !== 'ENODATA') {
console.warn(`ipinfo: rDNS lookup error for ${clientIp}:`, e.message);
}
rdns = { error: `rDNS lookup failed (${e.code || 'Unknown error'})` };
}
res.json({
ip: clientIp,
geo,
asn,
rdns,
});
res.json({ ip: clientIp, geo, asn, rdns });
} catch (error) {
console.error(`ipinfo: Error processing ipinfo for ${clientIp}:`, error);
res.status(500).json({ error: 'Internal server error while processing IP information.' });
res.status(500).json({ error: 'Internal server error.' });
}
});
@@ -257,55 +192,29 @@ app.get('/api/ping', async (req, res) => {
const targetIp = typeof targetIpRaw === 'string' ? targetIpRaw.trim() : targetIpRaw;
console.log(`--- PING Request ---`);
console.log(`Raw req.query.targetIp:`, req.query.targetIp);
console.log(`Value of targetIp after trim: "${targetIp}"`);
console.log(`Type of targetIp:`, typeof targetIp);
console.log(`Value of targetIp: "${targetIp}"`);
// --- DIREKTER TEST IN DER ROUTE ---
let isDirectlyValidV4 = false;
let isDirectlyValidV6 = false;
try {
if (targetIp) { // Nur testen, wenn targetIp existiert
const addr4 = new Address4(targetIp);
isDirectlyValidV4 = addr4.isValid();
}
} catch (e) { /* Ignorieren für diesen Test */ }
try {
if (targetIp) { // Nur testen, wenn targetIp existiert
const addr6 = new Address6(targetIp);
isDirectlyValidV6 = addr6.isValid();
}
} catch (e) { /* Ignorieren für diesen Test */ }
console.log(`Direct V4 check in route for "${targetIp}": ${isDirectlyValidV4}`);
console.log(`Direct V6 check in route for "${targetIp}": ${isDirectlyValidV6}`);
// --- ENDE DIREKTER TEST ---
const isValidResult = isValidIp(targetIp); // Verwendet jetzt die neue isValidIp
console.log(`isValidIp (net) result for "${targetIp}": ${isValidResult}`);
const isValidResult = isValidIp(targetIp); // Rufe die Funktion trotzdem auf
console.log(`isValidIp function result for "${targetIp}": ${isValidResult}`);
if (!isValidResult) { // Prüfe weiterhin das Ergebnis der Funktion
console.log(`isValidIp returned false for "${targetIp}", sending 400.`);
if (!isValidResult) {
console.log(`isValidIp (net) returned false for "${targetIp}", sending 400.`);
return res.status(400).json({ error: 'Invalid target IP address provided.' });
}
// --- Rest der Funktion ---
try {
console.log(`Proceeding to execute ping for "${targetIp}"...`);
// Parameter anpassen (z.B. -c für Linux/macOS, -n für Windows)
// Hier für Linux/macOS: 4 Pings senden
const args = ['-c', '4', targetIp]; // WICHTIG: Hier den getrimmten targetIp verwenden!
const args = ['-c', '4', targetIp];
const command = 'ping';
console.log(`Executing: ${command} ${args.join(' ')}`);
const output = await executeCommand(command, args);
// TODO: Ping-Ausgabe parsen für strukturierte Daten (RTT min/avg/max, loss)
console.log(`Ping for ${targetIp} successful.`);
// TODO: Ping-Ausgabe parsen
res.json({ success: true, rawOutput: output });
} catch (error) {
// executeCommand loggt den Fehler bereits
res.status(500).json({ success: false, error: `Ping command failed: ${error.message}` });
}
});
@@ -316,56 +225,29 @@ app.get('/api/traceroute', async (req, res) => {
const targetIp = typeof targetIpRaw === 'string' ? targetIpRaw.trim() : targetIpRaw;
console.log(`--- TRACEROUTE Request ---`);
console.log(`Raw req.query.targetIp:`, req.query.targetIp);
console.log(`Value of targetIp after trim: "${targetIp}"`);
console.log(`Type of targetIp:`, typeof targetIp);
console.log(`Value of targetIp: "${targetIp}"`);
// --- DIREKTER TEST IN DER ROUTE ---
let isDirectlyValidV4 = false;
let isDirectlyValidV6 = false;
try {
if (targetIp) { // Nur testen, wenn targetIp existiert
const addr4 = new Address4(targetIp);
isDirectlyValidV4 = addr4.isValid();
}
} catch (e) { /* Ignorieren für diesen Test */ }
try {
if (targetIp) { // Nur testen, wenn targetIp existiert
const addr6 = new Address6(targetIp);
isDirectlyValidV6 = addr6.isValid();
}
} catch (e) { /* Ignorieren für diesen Test */ }
console.log(`Direct V4 check in route for "${targetIp}": ${isDirectlyValidV4}`);
console.log(`Direct V6 check in route for "${targetIp}": ${isDirectlyValidV6}`);
// --- ENDE DIREKTER TEST ---
const isValidResult = isValidIp(targetIp); // Verwendet jetzt die neue isValidIp
console.log(`isValidIp (net) result for "${targetIp}": ${isValidResult}`);
const isValidResult = isValidIp(targetIp); // Rufe die Funktion trotzdem auf
console.log(`isValidIp function result for "${targetIp}": ${isValidResult}`);
if (!isValidResult) { // Prüfe weiterhin das Ergebnis der Funktion
console.log(`isValidIp returned false for "${targetIp}", sending 400.`);
if (!isValidResult) {
console.log(`isValidIp (net) returned false for "${targetIp}", sending 400.`);
return res.status(400).json({ error: 'Invalid target IP address provided.' });
}
// --- Rest der Funktion ---
try {
console.log(`Proceeding to execute traceroute for "${targetIp}"...`);
// Parameter anpassen. '-n' verhindert rDNS durch traceroute selbst (schneller).
// Evtl. Timeouts anpassen (-w), max Hops (-m)
const args = ['-n', targetIp]; // Für Linux/macOS
// Für Windows wäre es: const args = ['-d', targetIp]; const command = 'tracert';
const args = ['-n', targetIp]; // Linux/macOS
const command = 'traceroute';
console.log(`Executing: ${command} ${args.join(' ')}`);
const output = await executeCommand(command, args);
// TODO: Traceroute-Ausgabe parsen für strukturierte Daten (Array von Hops)
console.log(`Traceroute for ${targetIp} successful.`);
// TODO: Traceroute-Ausgabe parsen
res.json({ success: true, rawOutput: output });
} catch (error) {
// executeCommand loggt den Fehler bereits
res.status(500).json({ success: false, error: `Traceroute command failed: ${error.message}` });
}
});
@@ -381,6 +263,5 @@ initialize().then(() => {
console.log(` http://localhost:${PORT}/api/traceroute?targetIp=<ip>`);
});
}).catch(error => {
// Fehler bei der Initialisierung wurde bereits geloggt.
console.error("Server could not start due to initialization errors.");
});