// server.js require('dotenv').config(); const express = require('express'); const cors = require('cors'); const geoip = require('@maxmind/geoip2-node'); const net = require('net'); // Node.js built-in module for IP validation const { spawn } = require('child_process'); const dns = require('dns').promises; const app = express(); const PORT = process.env.PORT || 3000; // --- Globale Variablen für MaxMind Reader --- let cityReader; let asnReader; // --- Hilfsfunktionen --- /** * 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 (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 (net): Input invalid`); // Optional Debugging return false; } const trimmedIp = ip.trim(); // net.isIP(trimmedIp) gibt 0 zurück, wenn ungültig, 4 für IPv4, 6 für IPv6. const ipVersion = net.isIP(trimmedIp); // console.log(`isValidIp (net): net.isIP check for "${trimmedIp}": Version ${ipVersion}`); // Optional Debugging 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 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 (trimmedIp === '::1' || trimmedIp === '127.0.0.1') { return trimmedIp; } return trimmedIp; // Gib die getrimmte IP zurück } /** * Führt einen Shell-Befehl sicher aus und gibt stdout zurück. * @param {string} command - Der Befehl (z.B. 'ping'). * @param {string[]} args - Die Argumente als Array. * @returns {Promise} Eine Promise, die mit stdout aufgelöst wird. */ function executeCommand(command, args) { return new Promise((resolve, reject) => { // Argumenten-Validierung (einfach) args.forEach(arg => { if (typeof arg === 'string' && /[;&|`$()<>]/.test(arg)) { console.error(`Potential command injection attempt detected in argument: ${arg}`); return reject(new Error(`Invalid character detected in command argument.`)); } }); const proc = spawn(command, args); let stdout = ''; let stderr = ''; proc.stdout.on('data', (data) => { stdout += data.toString(); }); proc.stderr.on('data', (data) => { stderr += data.toString(); }); proc.on('error', (err) => { 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) { 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 { resolve(stdout); } }); }); } // --- Initialisierung (MaxMind DBs laden) --- async function initialize() { try { console.log('Loading MaxMind databases...'); const cityDbPath = process.env.GEOIP_CITY_DB || './data/GeoLite2-City.mmdb'; const asnDbPath = process.env.GEOIP_ASN_DB || './data/GeoLite2-ASN.mmdb'; console.log(`City DB Path: ${cityDbPath}`); console.log(`ASN DB Path: ${asnDbPath}`); cityReader = await geoip.Reader.open(cityDbPath); asnReader = await geoip.Reader.open(asnDbPath); console.log('MaxMind databases loaded successfully.'); } catch (error) { 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); } } // --- Middleware --- 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) => { 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); // Verwendet jetzt die neue getCleanIp console.log(`ipinfo: Raw IP = ${clientIpRaw}, Cleaned IP = ${clientIp}`); 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'], }); } 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 }); } try { let geo = null; try { const geoData = cityReader.city(clientIp); geo = { /* ... Geo-Daten wie zuvor ... */ }; } catch (e) { console.warn(`ipinfo: MaxMind City lookup failed for ${clientIp}: ${e.message}`); geo = { error: 'GeoIP lookup failed.' }; } let asn = null; try { const asnData = asnReader.asn(clientIp); asn = { /* ... ASN-Daten wie zuvor ... */ }; } catch (e) { console.warn(`ipinfo: MaxMind ASN lookup failed for ${clientIp}: ${e.message}`); asn = { error: 'ASN lookup failed.' }; } let rdns = null; try { const hostnames = await dns.reverse(clientIp); rdns = hostnames; } catch (e) { 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 }); } catch (error) { console.error(`ipinfo: Error processing ipinfo for ${clientIp}:`, error); res.status(500).json({ error: 'Internal server error.' }); } }); // Ping Endpunkt app.get('/api/ping', async (req, res) => { const targetIpRaw = req.query.targetIp; const targetIp = typeof targetIpRaw === 'string' ? targetIpRaw.trim() : targetIpRaw; console.log(`--- PING Request ---`); console.log(`Value of targetIp: "${targetIp}"`); const isValidResult = isValidIp(targetIp); // Verwendet jetzt die neue isValidIp console.log(`isValidIp (net) result for "${targetIp}": ${isValidResult}`); if (!isValidResult) { console.log(`isValidIp (net) returned false for "${targetIp}", sending 400.`); return res.status(400).json({ error: 'Invalid target IP address provided.' }); } try { console.log(`Proceeding to execute ping for "${targetIp}"...`); const args = ['-c', '4', targetIp]; const command = 'ping'; console.log(`Executing: ${command} ${args.join(' ')}`); const output = await executeCommand(command, args); console.log(`Ping for ${targetIp} successful.`); // TODO: Ping-Ausgabe parsen res.json({ success: true, rawOutput: output }); } catch (error) { res.status(500).json({ success: false, error: `Ping command failed: ${error.message}` }); } }); // Traceroute Endpunkt app.get('/api/traceroute', async (req, res) => { const targetIpRaw = req.query.targetIp; const targetIp = typeof targetIpRaw === 'string' ? targetIpRaw.trim() : targetIpRaw; console.log(`--- TRACEROUTE Request ---`); console.log(`Value of targetIp: "${targetIp}"`); const isValidResult = isValidIp(targetIp); // Verwendet jetzt die neue isValidIp console.log(`isValidIp (net) result for "${targetIp}": ${isValidResult}`); if (!isValidResult) { console.log(`isValidIp (net) returned false for "${targetIp}", sending 400.`); return res.status(400).json({ error: 'Invalid target IP address provided.' }); } try { console.log(`Proceeding to execute traceroute for "${targetIp}"...`); const args = ['-n', targetIp]; // Linux/macOS const command = 'traceroute'; console.log(`Executing: ${command} ${args.join(' ')}`); const output = await executeCommand(command, args); console.log(`Traceroute for ${targetIp} successful.`); // TODO: Traceroute-Ausgabe parsen res.json({ success: true, rawOutput: output }); } catch (error) { res.status(500).json({ success: false, error: `Traceroute command failed: ${error.message}` }); } }); // --- Server starten --- initialize().then(() => { app.listen(PORT, () => { console.log(`Server listening on port ${PORT}`); console.log(`API endpoints available at:`); console.log(` http://localhost:${PORT}/api/ipinfo`); console.log(` http://localhost:${PORT}/api/ping?targetIp=`); console.log(` http://localhost:${PORT}/api/traceroute?targetIp=`); }); }).catch(error => { console.error("Server could not start due to initialization errors."); });