diff --git a/backend/package-lock.json b/backend/package-lock.json index c611fc4..980f426 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -14,7 +14,7 @@ "dotenv": "^16.4.7", "express": "^4.21.2", "express-rate-limit": "^7.5.0", - "oui": "^13.0.5", + "macaddress": "^0.5.3", "pino": "^9.6.0", "pino-pretty": "^13.0.0", "whois-json": "^2.0.4" @@ -740,6 +740,11 @@ "lower-case": "^1.1.2" } }, + "node_modules/macaddress": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/macaddress/-/macaddress-0.5.3.tgz", + "integrity": "sha512-vGBKTA+jwM4KgjGZ+S/8/Mkj9rWzePyGY6jManXPGhiWu63RYwW8dKPyk5koP+8qNVhPhHgFa1y/MJ4wrjsNrg==" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -899,22 +904,6 @@ "wrappy": "1" } }, - "node_modules/oui": { - "version": "13.0.5", - "resolved": "https://registry.npmjs.org/oui/-/oui-13.0.5.tgz", - "integrity": "sha512-bAXoXcLrl3SweuEuh8aWpdTsfHka6B7Gh1SxsTZMXtWi9QSxQk7CupxWlP2QQ4olTDifaWAiQbROMxfarAQZlQ==", - "dependencies": { - "oui-data": "^1.1.76" - }, - "bin": { - "oui": "bin/oui.js" - } - }, - "node_modules/oui-data": { - "version": "1.1.314", - "resolved": "https://registry.npmjs.org/oui-data/-/oui-data-1.1.314.tgz", - "integrity": "sha512-F8r1LwdAucn6Fgq1KECZPMCpMgXPdhCblFOY/eQ6S83x5lfQQK9aRRIpz7dH6iQeAeqoSP1b4I8z4lqUaTwAvg==" - }, "node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", diff --git a/backend/package.json b/backend/package.json index b1c89f2..28dc974 100644 --- a/backend/package.json +++ b/backend/package.json @@ -15,7 +15,7 @@ "dotenv": "^16.4.7", "express": "^4.21.2", "express-rate-limit": "^7.5.0", - "oui": "^13.0.5", + "macaddress": "^0.5.3", "pino": "^9.6.0", "pino-pretty": "^13.0.0", "whois-json": "^2.0.4" diff --git a/backend/server.js b/backend/server.js index e9c3b0c..6978621 100644 --- a/backend/server.js +++ b/backend/server.js @@ -9,7 +9,7 @@ const dns = require('dns').promises; const pino = require('pino'); // Logging library const rateLimit = require('express-rate-limit'); // Rate limiting middleware const whois = require('whois-json'); // Hinzugefügt für WHOIS -const oui = require('oui'); // Ersetzt mac-lookup +const macaddress = require('macaddress'); // Ersetzt oui // --- Logger Initialisierung --- const logger = pino({ @@ -40,7 +40,6 @@ function isValidIp(ip) { } const trimmedIp = ip.trim(); const ipVersion = net.isIP(trimmedIp); // Gibt 0, 4 oder 6 zurück - // logger.debug({ ip: trimmedIp, version: ipVersion }, 'isValidIp check'); // Optional: Debug log return ipVersion === 4 || ipVersion === 6; } @@ -83,8 +82,6 @@ function isValidDomain(domain) { if (!domain || typeof domain !== 'string' || domain.trim().length < 3) { return false; } - // Einfache Regex: Muss mindestens einen Punkt enthalten und keine ungültigen Zeichen. - // Erlaubt IDNs (Internationalized Domain Names) durch \p{L} const domainRegex = /^(?:[a-z0-9\p{L}](?:[a-z0-9\p{L}-]{0,61}[a-z0-9\p{L}])?\.)+[a-z0-9\p{L}][a-z0-9\p{L}-]{0,61}[a-z0-9\p{L}]$/iu; return domainRegex.test(domain.trim()); } @@ -99,7 +96,6 @@ function isValidMac(mac) { return false; } // Erlaubt Formate wie 00:1A:2B:3C:4D:5E, 00-1A-2B-3C-4D-5E, 001A.2B3C.4D5E - // oui() validiert intern, aber eine Vorabprüfung schadet nicht. const macRegex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$|^([0-9A-Fa-f]{4}\.){2}([0-9A-Fa-f]{4})$/; return macRegex.test(mac.trim()); } @@ -107,7 +103,6 @@ function isValidMac(mac) { /** * 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. */ @@ -184,7 +179,7 @@ function parsePingOutput(pingOutput) { const statsLine = lines.find(line => line.includes('packets transmitted')); if (statsLine) { const transmittedMatch = statsLine.match(/(\d+)\s+packets transmitted/); - const receivedMatch = statsLine.match(/(\d+)\s+(?:received|packets received)/); // Anpassung für Varianten + const receivedMatch = statsLine.match(/(\d+)\s+(?:received|packets received)/); const lossMatch = statsLine.match(/([\d.]+)%\s+packet loss/); if (transmittedMatch) packetsTransmitted = parseInt(transmittedMatch[1], 10); if (receivedMatch) packetsReceived = parseInt(receivedMatch[1], 10); @@ -209,7 +204,7 @@ function parsePingOutput(pingOutput) { rtt: rtt.avg !== null ? rtt : null, }; if (packetsTransmitted > 0 && rtt.avg === null && packetsReceived === 0) { - result.error = "Request timed out or host unreachable."; // Spezifischer Fehler bei Totalausfall + result.error = "Request timed out or host unreachable."; } } catch (parseError) { @@ -226,9 +221,8 @@ function parsePingOutput(pingOutput) { */ function parseTracerouteLine(line) { line = line.trim(); - if (!line || line.startsWith('traceroute to')) return null; // Ignoriere Header + if (!line || line.startsWith('traceroute to')) return null; - // Regex angepasst für mehr Robustheit (optionaler Hostname, IP immer da, RTTs oder *) const hopMatch = line.match(/^(\s*\d+)\s+(?:([a-zA-Z0-9\.\-]+)\s+\(([\d\.:a-fA-F]+)\)|([\d\.:a-fA-F]+))\s+(.*)$/); const timeoutMatch = line.match(/^(\s*\d+)\s+(\*\s+\*\s+\*)/); @@ -242,29 +236,25 @@ function parseTracerouteLine(line) { }; } else if (hopMatch) { const hop = parseInt(hopMatch[1].trim(), 10); - const hostname = hopMatch[2]; // Kann undefined sein - const ipInParen = hopMatch[3]; // Kann undefined sein - const ipDirect = hopMatch[4]; // Kann undefined sein + const hostname = hopMatch[2]; + const ipInParen = hopMatch[3]; + const ipDirect = hopMatch[4]; const restOfLine = hopMatch[5].trim(); - const ip = ipInParen || ipDirect; - // Extrahiere RTTs (können * sein oder Zahl mit " ms") const rttParts = restOfLine.split(/\s+/); const rtts = rttParts.map(p => p === '*' ? '*' : p.replace(/\s*ms$/, '')).filter(p => p === '*' || !isNaN(parseFloat(p))).slice(0, 3); - // Fülle fehlende RTTs mit '*' auf, falls weniger als 3 gefunden wurden while (rtts.length < 3) rtts.push('*'); return { hop: hop, - hostname: hostname || null, // Setze null, wenn kein Hostname gefunden + hostname: hostname || null, ip: ip, rtt: rtts, rawLine: line, }; } - // logger.debug({ line }, "Unparsed traceroute line"); // Optional: Log unparsed lines - return null; // Nicht als Hop-Zeile erkannt + return null; } @@ -279,9 +269,8 @@ async function initialize() { asnReader = await geoip.Reader.open(asnDbPath); logger.info('MaxMind databases loaded successfully.'); - // Kein explizites Laden mehr für 'oui' nötig. - // Die Daten werden bei der ersten Verwendung automatisch geladen/aktualisiert. - logger.info('MAC address lookup data (oui) will be loaded on first use.'); + // Kein explizites Laden für 'macaddress' nötig. + logger.info('MAC address lookup data (macaddress) will be loaded on first use.'); } catch (error) { logger.fatal({ error: error.message, stack: error.stack }, 'Could not initialize MaxMind databases. Exiting.'); @@ -290,20 +279,18 @@ async function initialize() { } // --- Middleware --- -app.use(cors()); // Erlaubt Anfragen von anderen Origins -app.use(express.json()); // Parst JSON-Request-Bodies - -// Vertraue Proxy-Headern (vorsichtig verwenden!) -app.set('trust proxy', 2); // Vertraue zwei Proxys (externer Nginx + interner Nginx) +app.use(cors()); +app.use(express.json()); +app.set('trust proxy', 2); // Rate Limiter const generalLimiter = rateLimit({ windowMs: 5 * 60 * 1000, // 5 Minuten - max: process.env.NODE_ENV === 'production' ? 20 : 200, // Mehr Anfragen im Dev erlauben + max: process.env.NODE_ENV === 'production' ? 20 : 200, standardHeaders: true, legacyHeaders: false, message: { error: 'Too many requests from this IP, please try again after 5 minutes' }, - keyGenerator: (req, res) => req.ip || req.socket.remoteAddress, // IP des Clients als Schlüssel + keyGenerator: (req, res) => req.ip || req.socket.remoteAddress, handler: (req, res, next, options) => { logger.warn({ ip: req.ip || req.socket.remoteAddress, route: req.originalUrl }, 'Rate limit exceeded'); res.status(options.statusCode).send(options.message); @@ -323,9 +310,8 @@ app.use('/api/mac-lookup', generalLimiter); // Haupt-Endpunkt: Liefert alle Infos zur IP des Clients app.get('/api/ipinfo', async (req, res) => { - const requestIp = req.ip || req.socket.remoteAddress; // req.ip berücksichtigt 'trust proxy' + const requestIp = req.ip || req.socket.remoteAddress; logger.info({ ip: requestIp, method: req.method, url: req.originalUrl }, 'ipinfo request received'); - const clientIp = getCleanIp(requestIp); logger.debug({ rawIp: requestIp, cleanedIp: clientIp }, 'IP cleaning result'); @@ -357,7 +343,7 @@ app.get('/api/ipinfo', async (req, res) => { longitude: geoData.location?.longitude, timezone: geoData.location?.timeZone, }; - geo = Object.fromEntries(Object.entries(geo).filter(([_, v]) => v != null)); // Entferne leere Werte + geo = Object.fromEntries(Object.entries(geo).filter(([_, v]) => v != null)); logger.debug({ ip: clientIp, geo }, 'GeoIP lookup successful'); } catch (e) { logger.warn({ ip: clientIp, error: e.message }, `MaxMind City lookup failed`); @@ -371,7 +357,7 @@ app.get('/api/ipinfo', async (req, res) => { number: asnData.autonomousSystemNumber, organization: asnData.autonomousSystemOrganization, }; - asn = Object.fromEntries(Object.entries(asn).filter(([_, v]) => v != null)); // Entferne leere Werte + asn = Object.fromEntries(Object.entries(asn).filter(([_, v]) => v != null)); logger.debug({ ip: clientIp, asn }, 'ASN lookup successful'); } catch (e) { logger.warn({ ip: clientIp, error: e.message }, `MaxMind ASN lookup failed`); @@ -417,7 +403,6 @@ app.get('/api/ping', async (req, res) => { logger.warn({ requestIp, targetIp }, 'Invalid target IP for ping'); return res.status(400).json({ error: 'Invalid target IP address provided.' }); } - if (isPrivateIp(targetIp)) { logger.warn({ requestIp, targetIp }, 'Attempt to ping private IP blocked'); return res.status(403).json({ error: 'Operations on private or local IP addresses are not allowed.' }); @@ -426,7 +411,7 @@ app.get('/api/ping', async (req, res) => { try { const pingCount = process.env.PING_COUNT || '4'; const countArg = parseInt(pingCount, 10) || 4; - const args = ['-c', `${countArg}`, targetIp]; // Linux/macOS + const args = ['-c', `${countArg}`, targetIp]; const command = 'ping'; logger.info({ requestIp, targetIp, command: `${command} ${args.join(' ')}` }, 'Executing ping'); @@ -437,10 +422,8 @@ app.get('/api/ping', async (req, res) => { res.json({ success: true, ...parsedResult }); } catch (error) { - // executeCommand loggt bereits Details logger.error({ requestIp, targetIp, error: error.message }, 'Ping command failed'); - // Sende strukturierte Fehlermeldung, wenn möglich - const parsedError = parsePingOutput(error.message); // Versuche, Fehler aus Ping-Output zu parsen + const parsedError = parsePingOutput(error.message); res.status(500).json({ success: false, error: `Ping command failed: ${parsedError.error || error.message}`, @@ -450,7 +433,7 @@ app.get('/api/ping', async (req, res) => { }); // Traceroute Endpunkt (Server-Sent Events) -app.get('/api/traceroute', (req, res) => { // Beachte: nicht async, da wir streamen +app.get('/api/traceroute', (req, res) => { const targetIpRaw = req.query.targetIp; const targetIp = typeof targetIpRaw === 'string' ? targetIpRaw.trim() : targetIpRaw; const requestIp = req.ip || req.socket.remoteAddress; @@ -461,7 +444,6 @@ app.get('/api/traceroute', (req, res) => { // Beachte: nicht async, da wir strea logger.warn({ requestIp, targetIp }, 'Invalid target IP for traceroute'); return res.status(400).json({ error: 'Invalid target IP address provided.' }); } - if (isPrivateIp(targetIp)) { logger.warn({ requestIp, targetIp }, 'Attempt to traceroute private IP blocked'); return res.status(403).json({ error: 'Operations on private or local IP addresses are not allowed.' }); @@ -469,27 +451,25 @@ app.get('/api/traceroute', (req, res) => { // Beachte: nicht async, da wir strea try { logger.info({ requestIp, targetIp }, `Starting traceroute stream...`); - - // Set SSE Headers res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); - res.setHeader('X-Accel-Buffering', 'no'); // Wichtig für Nginx-Proxies - res.flushHeaders(); // Send headers immediately + res.setHeader('X-Accel-Buffering', 'no'); + res.flushHeaders(); - const args = ['-n', targetIp]; // Linux/macOS, -n für keine Namensauflösung (schneller) + const args = ['-n', targetIp]; const command = 'traceroute'; const proc = spawn(command, args); logger.info({ requestIp, targetIp, command: `${command} ${args.join(' ')}` }, 'Spawned traceroute process'); - let buffer = ''; // Buffer für unvollständige Zeilen + let buffer = ''; const sendEvent = (event, data) => { try { res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`); } catch (e) { logger.error({ requestIp, targetIp, event, error: e.message }, "Error writing to SSE stream (client likely disconnected)"); - proc.kill(); // Beende Prozess, wenn Schreiben fehlschlägt + proc.kill(); if (!res.writableEnded) res.end(); } }; @@ -497,8 +477,7 @@ app.get('/api/traceroute', (req, res) => { // Beachte: nicht async, da wir strea proc.stdout.on('data', (data) => { buffer += data.toString(); let lines = buffer.split('\n'); - buffer = lines.pop() || ''; // Letzte (evtl. unvollständige) Zeile zurück in den Buffer - + buffer = lines.pop() || ''; lines.forEach(line => { const parsed = parseTracerouteLine(line); if (parsed) { @@ -524,15 +503,11 @@ app.get('/api/traceroute', (req, res) => { // Beachte: nicht async, da wir strea }); proc.on('close', (code) => { - if (buffer) { // Verarbeite letzte Zeile im Buffer + if (buffer) { const parsed = parseTracerouteLine(buffer); - if (parsed) { - sendEvent('hop', parsed); - } else if (buffer.trim()) { - sendEvent('info', { message: buffer.trim() }); - } + if (parsed) sendEvent('hop', parsed); + else if (buffer.trim()) sendEvent('info', { message: buffer.trim() }); } - if (code !== 0) { logger.error({ requestIp, targetIp, exitCode: code }, `Traceroute command finished with error code ${code}`); sendEvent('error', { error: `Traceroute command failed with exit code ${code}` }); @@ -543,22 +518,17 @@ app.get('/api/traceroute', (req, res) => { // Beachte: nicht async, da wir strea if (!res.writableEnded) res.end(); }); - // Handle client disconnect req.on('close', () => { logger.info({ requestIp, targetIp }, 'Client disconnected from traceroute stream, killing process.'); - if (!proc.killed) { - proc.kill(); - } + if (!proc.killed) proc.kill(); if (!res.writableEnded) res.end(); }); } catch (error) { - // Dieser Catch ist eher für synchrone Fehler vor dem Spawn logger.error({ requestIp, targetIp, error: error.message, stack: error.stack }, 'Error setting up traceroute stream'); if (!res.headersSent) { res.status(500).json({ success: false, error: `Failed to initiate traceroute: ${error.message}` }); } else { - // Wenn Header gesendet wurden, können wir nur noch versuchen, einen Fehler zu schreiben und zu beenden try { if (!res.writableEnded) { res.write(`event: error\ndata: ${JSON.stringify({ error: `Internal server error: ${error.message}` })}\n\n`); @@ -567,43 +537,35 @@ app.get('/api/traceroute', (req, res) => { // Beachte: nicht async, da wir strea } catch (e) { logger.error({ requestIp, targetIp, error: e.message }, "Error writing final error to SSE stream"); } } } -}); // Ende von app.get('/api/traceroute'...) +}); // Lookup Endpunkt für beliebige IP (GeoIP, ASN, rDNS) app.get('/api/lookup', async (req, res) => { - const targetIpRaw = req.query.targetIp; // IP kommt jetzt als Query-Parameter 'targetIp' + const targetIpRaw = req.query.targetIp; const targetIp = typeof targetIpRaw === 'string' ? targetIpRaw.trim() : targetIpRaw; - const requestIp = req.ip || req.socket.remoteAddress; // Nur für Logging + const requestIp = req.ip || req.socket.remoteAddress; logger.info({ requestIp, targetIp }, 'Lookup request received'); - // Validierung: Ist es eine gültige IP? if (!isValidIp(targetIp)) { logger.warn({ requestIp, targetIp }, 'Invalid target IP for lookup'); return res.status(400).json({ error: 'Invalid IP address provided for lookup.' }); } - - // Validierung: Ist es eine private IP? if (isPrivateIp(targetIp)) { logger.warn({ requestIp, targetIp }, 'Attempt to lookup private IP blocked'); return res.status(403).json({ error: 'Lookup for private or local IP addresses is not supported.' }); } - // Führe die gleichen Lookups wie bei /api/ipinfo durch, aber für targetIp try { let geo = null; try { const geoData = cityReader.city(targetIp); 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, + 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 = Object.fromEntries(Object.entries(geo).filter(([_, v]) => v != null)); logger.debug({ targetIp, geo }, 'GeoIP lookup successful for lookup'); @@ -615,10 +577,7 @@ app.get('/api/lookup', async (req, res) => { let asn = null; try { const asnData = asnReader.asn(targetIp); - asn = { - number: asnData.autonomousSystemNumber, - organization: asnData.autonomousSystemOrganization, - }; + asn = { number: asnData.autonomousSystemNumber, organization: asnData.autonomousSystemOrganization }; asn = Object.fromEntries(Object.entries(asn).filter(([_, v]) => v != null)); logger.debug({ targetIp, asn }, 'ASN lookup successful for lookup'); } catch (e) { @@ -632,15 +591,11 @@ app.get('/api/lookup', async (req, res) => { rdns = hostnames; logger.debug({ targetIp, rdns }, 'rDNS lookup successful for lookup'); } catch (e) { - if (e.code !== 'ENOTFOUND' && e.code !== 'ENODATA') { - logger.warn({ targetIp, error: e.message, code: e.code }, `rDNS lookup error for lookup`); - } else { - logger.debug({ targetIp, code: e.code }, 'rDNS lookup failed (No record) for lookup'); - } + if (e.code !== 'ENOTFOUND' && e.code !== 'ENODATA') logger.warn({ targetIp, error: e.message, code: e.code }, `rDNS lookup error for lookup`); + else logger.debug({ targetIp, code: e.code }, 'rDNS lookup failed (No record) for lookup'); rdns = { error: `rDNS lookup failed (${e.code || 'Unknown error'})` }; } - // Gib die gesammelten Daten zurück res.json({ ip: targetIp, geo: geo.error ? geo : (Object.keys(geo).length > 0 ? geo : null), @@ -678,32 +633,16 @@ app.get('/api/dns-lookup', async (req, res) => { } try { - // dns.resolve unterstützt 'ANY', aber gibt oft nur einen Teil zurück oder wirft Fehler. - // Besser spezifische Typen abfragen oder dns.resolveAny verwenden (wenn verfügbar und gewünscht). - // Für Einfachheit hier dns.resolve. let records; if (type === 'ANY') { - // Versuche, gängige Typen einzeln abzufragen, da resolveAny oft nicht wie erwartet funktioniert const promises = [ - dns.resolve(domain, 'A').catch(() => []), - dns.resolve(domain, 'AAAA').catch(() => []), - dns.resolve(domain, 'MX').catch(() => []), - dns.resolve(domain, 'TXT').catch(() => []), - dns.resolve(domain, 'NS').catch(() => []), - dns.resolve(domain, 'CNAME').catch(() => []), + dns.resolve(domain, 'A').catch(() => []), dns.resolve(domain, 'AAAA').catch(() => []), + dns.resolve(domain, 'MX').catch(() => []), dns.resolve(domain, 'TXT').catch(() => []), + dns.resolve(domain, 'NS').catch(() => []), dns.resolve(domain, 'CNAME').catch(() => []), dns.resolve(domain, 'SOA').catch(() => []), ]; const results = await Promise.all(promises); - records = { - A: results[0], - AAAA: results[1], - MX: results[2], - TXT: results[3], - NS: results[4], - CNAME: results[5], - SOA: results[6], - }; - // Entferne leere Ergebnisse + records = { A: results[0], AAAA: results[1], MX: results[2], TXT: results[3], NS: results[4], CNAME: results[5], SOA: results[6] }; records = Object.fromEntries(Object.entries(records).filter(([_, v]) => Array.isArray(v) ? v.length > 0 : v)); } else { records = await dns.resolve(domain, type); @@ -726,52 +665,45 @@ app.get('/api/whois-lookup', async (req, res) => { logger.info({ requestIp, query }, 'WHOIS lookup request received'); - // Einfache Validierung: Muss entweder eine gültige IP oder eine Domain sein if (!isValidIp(query) && !isValidDomain(query)) { logger.warn({ requestIp, query }, 'Invalid query for WHOIS lookup'); return res.status(400).json({ success: false, error: 'Invalid domain name or IP address provided for WHOIS lookup.' }); } try { - // whois-json kann manchmal sehr lange dauern oder fehlschlagen - const result = await whois(query, { timeout: 10000 }); // 10 Sekunden Timeout - + const result = await whois(query, { timeout: 10000 }); logger.info({ requestIp, query }, 'WHOIS lookup successful'); res.json({ success: true, query, result }); } catch (error) { logger.error({ requestIp, query, error: error.message }, 'WHOIS lookup failed'); - // Versuche, eine spezifischere Fehlermeldung zu geben let errorMessage = error.message; - if (error.message.includes('ETIMEDOUT') || error.message.includes('ESOCKETTIMEDOUT')) { - errorMessage = 'WHOIS server timed out.'; - } else if (error.message.includes('ENOTFOUND')) { - errorMessage = 'Domain or IP not found or WHOIS server unavailable.'; - } + if (error.message.includes('ETIMEDOUT') || error.message.includes('ESOCKETTIMEDOUT')) errorMessage = 'WHOIS server timed out.'; + else if (error.message.includes('ENOTFOUND')) errorMessage = 'Domain or IP not found or WHOIS server unavailable.'; res.status(500).json({ success: false, error: `WHOIS lookup failed: ${errorMessage}` }); } }); -// MAC Address Lookup Endpunkt (mit 'oui' Bibliothek) -app.get('/api/mac-lookup', async (req, res) => { // async ist hier nicht unbedingt nötig, aber schadet nicht +// MAC Address Lookup Endpunkt (mit 'macaddress' Bibliothek) +app.get('/api/mac-lookup', async (req, res) => { const macRaw = req.query.mac; const mac = typeof macRaw === 'string' ? macRaw.trim() : macRaw; const requestIp = req.ip || req.socket.remoteAddress; logger.info({ requestIp, mac }, 'MAC lookup request received'); - if (!isValidMac(mac)) { // Vorabprüfung beibehalten + if (!isValidMac(mac)) { logger.warn({ requestIp, mac }, 'Invalid MAC address format for lookup'); return res.status(400).json({ success: false, error: 'Invalid MAC address format provided.' }); } try { - // oui() lädt die DB bei Bedarf und gibt den Vendor-String oder null zurück - const vendor = oui(mac); // Einfacher Aufruf + // Verwende macaddress.one() für asynchronen Lookup + const result = await macaddress.one(mac); - if (vendor) { - logger.info({ requestIp, mac, vendor }, 'MAC lookup successful'); - res.json({ success: true, mac, vendor }); + if (result && result.vendor) { // Prüfe, ob Ergebnis und Vendor existieren + logger.info({ requestIp, mac, vendor: result.vendor }, 'MAC lookup successful'); + res.json({ success: true, mac, vendor: result.vendor }); } else { logger.info({ requestIp, mac }, 'MAC lookup successful, but no vendor found'); res.json({ success: true, mac, vendor: null, message: 'Vendor not found for this MAC address prefix.' }); @@ -779,7 +711,7 @@ app.get('/api/mac-lookup', async (req, res) => { // async ist hier nicht unbedin } catch (error) { // Fehler können auftreten, wenn die interne DB nicht geladen werden kann - // oder die Eingabe trotz Regex ungültig ist (sollte selten sein) + // oder die Eingabe ungültig ist logger.error({ requestIp, mac, error: error.message }, 'MAC lookup failed'); res.status(500).json({ success: false, error: `MAC lookup failed: ${error.message}` }); } @@ -809,17 +741,15 @@ initialize().then(() => { logger.info(` http://localhost:${PORT}/api/version`); }); }).catch(error => { - // Fehler bei der Initialisierung wurde bereits geloggt. logger.fatal("Server could not start due to initialization errors."); - process.exit(1); // Beenden bei schwerwiegendem Startfehler + process.exit(1); }); -// Graceful Shutdown Handling (optional aber gut für Produktion) +// Graceful Shutdown Handling const signals = { 'SIGINT': 2, 'SIGTERM': 15 }; Object.keys(signals).forEach((signal) => { process.on(signal, () => { logger.info(`Received ${signal}, shutting down gracefully...`); - // Hier könnten noch Aufräumarbeiten stattfinden (z.B. DB-Verbindungen schließen) process.exit(128 + signals[signal]); }); }); \ No newline at end of file