diff --git a/backend/routes/portScan.js b/backend/routes/portScan.js new file mode 100644 index 0000000..22c643e --- /dev/null +++ b/backend/routes/portScan.js @@ -0,0 +1,80 @@ +// backend/routes/portScan.js +const express = require('express'); +const Sentry = require("@sentry/node"); +const pino = require('pino'); +const { isValidIp, isPrivateIp, checkPort } = require('../utils'); + +const logger = pino({ level: process.env.LOG_LEVEL || 'info' }); +const router = express.Router(); + +const COMMON_PORTS_TO_SCAN = [ + 21, 22, 25, 53, 80, 110, 143, 443, 3306, 3389, 5432, 8080, 8443 +]; + +router.get('/', (req, res) => { + const targetIpRaw = req.query.targetIp; + const targetIp = typeof targetIpRaw === 'string' ? targetIpRaw.trim() : targetIpRaw; + const requestIp = req.ip || req.socket.remoteAddress; + + logger.info({ requestIp, targetIp }, 'Port scan stream request received'); + + if (!isValidIp(targetIp)) { + logger.warn({ requestIp, targetIp }, 'Invalid target IP for port scan'); + return res.status(400).json({ success: false, error: 'Invalid target IP address provided.' }); + } + if (isPrivateIp(targetIp)) { + logger.warn({ requestIp, targetIp }, 'Attempt to scan private IP blocked'); + return res.status(403).json({ success: false, error: 'Operations on private or local IP addresses are not allowed.' }); + } + + Sentry.configureScope(scope => { + scope.setContext("port_scan_details", { targetIp, requestIp }); + }); + + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + res.setHeader('X-Accel-Buffering', 'no'); + res.flushHeaders(); + + const sendEvent = (event, data) => { + if (!res.writableEnded) { + res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`); + } + }; + + let isConnectionClosed = false; + req.on('close', () => { + logger.info({ requestIp, targetIp }, 'Client disconnected from port scan stream.'); + isConnectionClosed = true; + }); + + (async () => { + try { + sendEvent('info', { message: `Starting scan of ${COMMON_PORTS_TO_SCAN.length} common ports on ${targetIp}...` }); + + for (const port of COMMON_PORTS_TO_SCAN) { + if (isConnectionClosed) break; + const result = await checkPort(port, targetIp, 2000); + sendEvent('port_status', result); + } + + if (!isConnectionClosed) { + logger.info({ requestIp, targetIp }, 'Port scan stream completed successfully.'); + sendEvent('end', { message: 'Scan complete.' }); + } + } catch (error) { + logger.error({ requestIp, targetIp, error: error.message }, 'Error during port scan stream'); + Sentry.captureException(error); + if (!isConnectionClosed) { + sendEvent('error', { error: 'An unexpected error occurred during the scan.' }); + } + } finally { + if (!res.writableEnded) { + res.end(); + } + } + })(); +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index c542616..b0a31e8 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,3 +1,4 @@ +// backend/server.js // server.js // Load .env variables FIRST! require('dotenv').config(); @@ -33,6 +34,7 @@ const lookupRoutes = require('./routes/lookup'); const dnsLookupRoutes = require('./routes/dnsLookup'); const whoisLookupRoutes = require('./routes/whoisLookup'); const versionRoutes = require('./routes/version'); +const portScanRoutes = require('./routes/portScan'); // --- Logger Initialisierung --- const logger = pino({ @@ -94,6 +96,7 @@ app.use('/api/traceroute', generalLimiter); app.use('/api/lookup', generalLimiter); app.use('/api/dns-lookup', generalLimiter); app.use('/api/whois-lookup', generalLimiter); +app.use('/api/port-scan', generalLimiter); // --- API Routes --- @@ -105,6 +108,7 @@ app.use('/api/lookup', lookupRoutes); app.use('/api/dns-lookup', dnsLookupRoutes); app.use('/api/whois-lookup', whoisLookupRoutes); app.use('/api/version', versionRoutes); +app.use('/api/port-scan', portScanRoutes); // --- Sentry Error Handler --- diff --git a/backend/utils.js b/backend/utils.js index 5acfa4d..0518fe1 100644 --- a/backend/utils.js +++ b/backend/utils.js @@ -1,4 +1,3 @@ -// backend/utils.js const net = require('net'); // Node.js built-in module for IP validation const { spawn } = require('child_process'); const pino = require('pino'); // Import pino for logging within utils if needed @@ -256,6 +255,48 @@ function parseTracerouteLine(line) { return null; } +/** + * Checks if a specific TCP port is open on a given host. + * @param {number} port - The port to check. + * @param {string} host - The target host IP address. + * @param {number} timeout - Connection timeout in milliseconds. + * @returns {Promise<{port: number, status: 'open'|'closed'|'timeout', service: string}>} A promise that resolves with the port status. + */ +function checkPort(port, host, timeout = 2000) { + // A small map of common ports to their services + const commonPorts = { + 21: 'FTP', 22: 'SSH', 23: 'Telnet', 25: 'SMTP', 53: 'DNS', 80: 'HTTP', + 110: 'POP3', 143: 'IMAP', 443: 'HTTPS', 445: 'SMB', 993: 'IMAPS', + 995: 'POP3S', 1433: 'MSSQL', 1521: 'Oracle', 3306: 'MySQL', 3389: 'RDP', + 5432: 'PostgreSQL', 5900: 'VNC', 8080: 'HTTP-Alt', 8443: 'HTTPS-Alt' + }; + const service = commonPorts[port] || 'Unknown'; + + return new Promise((resolve) => { + const socket = new net.Socket(); + socket.setTimeout(timeout); + + socket.on('connect', () => { + socket.destroy(); + resolve({ port, status: 'open', service }); + }); + + socket.on('timeout', () => { + socket.destroy(); + resolve({ port, status: 'timeout', service }); + }); + + socket.on('error', (err) => { + socket.destroy(); + // 'ECONNREFUSED' is the key for a closed port. Other errors might be network issues. + const status = err.code === 'ECONNREFUSED' ? 'closed' : 'error'; + resolve({ port, status, service, error: err.code }); + }); + + socket.connect(port, host); + }); +} + module.exports = { isValidIp, @@ -265,5 +306,5 @@ module.exports = { executeCommand, parsePingOutput, parseTracerouteLine, - // Note: logger is not exported, assuming it's managed globally or passed where needed + checkPort, }; \ No newline at end of file diff --git a/frontend/app/index.html b/frontend/app/index.html index af94bb1..df6c3d4 100644 --- a/frontend/app/index.html +++ b/frontend/app/index.html @@ -217,6 +217,7 @@