// Global WebSocket connection state variables let webSocket = null; let reconnectAttempts = 0; let reconnectDelay = 60000; // Fixed 60 second delay let heartbeatInterval = null; let isReconnecting = false; let connectionHealthCheck = null; // Page visibility and connection health tracking let isPageVisible = !document.hidden; let isWakingUp = false; // Flag to suppress animation burst on tab wake window.lastWebSocketMessageTime = Date.now(); // For connection health (heartbeat) window.lastValidDataTime = Date.now(); // For "Connected" vs "Idle" state tracking // Global connection flag for dashboard synchronization window.webSocketConnected = false; // FIX: Leaflet Grid Lines / Tile Gaps // Override the internal _initTile method to force tiles to be 1px larger // This creates a 1px overlap that hides the sub-pixel rendering gaps in Chrome/Edge // We exclude Firefox (Gecko) because it handles rendering differently and this fix causes artifacts there (function(){ if (L.Browser.gecko) return; // Skip for Firefox var originalInitTile = L.GridLayer.prototype._initTile; L.GridLayer.include({ _initTile: function (tile) { originalInitTile.call(this, tile); var tileSize = this.getTileSize(); tile.style.width = tileSize.x + 1 + 'px'; tile.style.height = tileSize.y + 1 + 'px'; } }); })(); // Map theme support var mapLayers = { dark: L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { attribution: '© OpenStreetMap © CARTO', detectRetina: true, subdomains: 'abcd', minZoom: 2, maxZoom: 8, tileSize: 256 }), light: L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { attribution: '© OpenStreetMap © CARTO', detectRetina: true, subdomains: 'abcd', minZoom: 2, maxZoom: 8, tileSize: 256 }) }; // Get current theme var currentTheme = document.documentElement.getAttribute('data-theme') || 'dark'; var base = mapLayers[currentTheme]; // Check if map container is already initialized if (window.map) { window.map.remove(); } var map = L.map('map', { layers: [base], tap: false, // ref https://github.com/Leaflet/Leaflet/issues/7255 center: new L.LatLng(0, 0), trackResize: true, worldCopyJump: true, minZoom: 2, maxZoom: 8, zoom: 3, zoomSnap: 0.2, // Allow fractional zoom levels zoomDelta: 0.2, // Match zoomSnap fullscreenControl: true, fullscreenControlOptions: { title:"Fullscreen Mode", titleCancel:"Exit Fullscreen Mode" } }); // Make map globally accessible window.map = map; // Enhanced attack map with modern dashboard integration // Make map globally accessible window.map = map; // Enhanced marker clustering var circles = new L.LayerGroup(); var markers = new L.LayerGroup(); var attackLines = new L.LayerGroup(); map.addLayer(circles); map.addLayer(markers); map.addLayer(attackLines); // Cache restoration function for map markers window.processRestoredAttack = function(event) { console.log('[MAP-RESTORE] Processing restored attack:', event); // Skip if event doesn't have required data if (!event.source_ip || !event.destination_ip) { console.log('[MAP-RESTORE] Skipping event - missing IP data'); return; } // Create a simplified message object from cached event const restoredMsg = { // Source (attacker) data country: event.country || 'Unknown', iso_code: event.country_code || 'XX', src_ip: event.source_ip || event.ip, ip_rep: event.ip_rep || event.reputation || event.ip_reputation || 'Unknown', color: event.color || getProtocolColor(event.protocol), // Destination (honeypot) data - use original WebSocket field names dst_country_name: event.dst_country_name || event.destination_country || 'Local', dst_iso_code: event.dst_iso_code || event.destination_country_code || 'XX', dst_ip: event.destination_ip, tpot_hostname: event.tpot_hostname || event.honeypot || 'honeypot', honeypot: event.honeypot, protocol: event.protocol, dst_port: event.destination_port || event.port, // Coordinates (if available in cached data) src_lat: event.source_lat, src_long: event.source_lng || event.source_long, dst_lat: event.destination_lat, dst_long: event.destination_lng || event.destination_long }; // If we have coordinates in the cached data, use them directly if (restoredMsg.src_lat && restoredMsg.src_long && restoredMsg.dst_lat && restoredMsg.dst_long) { const srcLatLng = new L.LatLng(restoredMsg.src_lat, restoredMsg.src_long); const dstLatLng = new L.LatLng(restoredMsg.dst_lat, restoredMsg.dst_long); restoreMarkerData(restoredMsg, srcLatLng, dstLatLng, event); } else { // Fallback: get coordinates from country/location data Promise.all([ getCoordinates(restoredMsg.country, restoredMsg.iso_code), getCoordinates(restoredMsg.dst_country_name, restoredMsg.dst_iso_code) ]).then(([srcCoords, dstCoords]) => { if (srcCoords && dstCoords) { const srcLatLng = new L.LatLng(srcCoords.lat, srcCoords.lng); const dstLatLng = new L.LatLng(dstCoords.lat, dstCoords.lng); restoreMarkerData(restoredMsg, srcLatLng, dstLatLng, event); } }).catch(error => { console.log('[MAP-RESTORE] Error getting coordinates:', error); }); } }; // Helper function to restore marker data and add visual elements function restoreMarkerData(restoredMsg, srcLatLng, dstLatLng, originalEvent) { const srcKey = srcLatLng.lat + "," + srcLatLng.lng; const dstKey = dstLatLng.lat + "," + dstLatLng.lng; // Initialize or update circleAttackData for source location if (!circleAttackData[srcKey]) { circleAttackData[srcKey] = { country: restoredMsg.country, iso_code: restoredMsg.iso_code, attacks: [], totalAttacks: 0, ips: {}, firstSeen: new Date(originalEvent.timestamp), lastSeen: new Date(originalEvent.timestamp), lastProtocol: restoredMsg.protocol, lastColor: restoredMsg.color }; } else { // Update protocol tracking for restored attacks // For restoration, we want to preserve the latest protocol/color from actual restore order circleAttackData[srcKey].lastProtocol = restoredMsg.protocol; circleAttackData[srcKey].lastColor = restoredMsg.color; circleAttackData[srcKey].lastSeen = new Date(originalEvent.timestamp); } // Initialize IP data if needed if (!circleAttackData[srcKey].ips[restoredMsg.src_ip]) { circleAttackData[srcKey].ips[restoredMsg.src_ip] = { src_ip: restoredMsg.src_ip, ip_rep: restoredMsg.ip_rep, attacks: [], firstSeen: new Date(originalEvent.timestamp), lastSeen: new Date(originalEvent.timestamp) }; } else { // Update reputation if new data is provided if (restoredMsg.ip_rep) { circleAttackData[srcKey].ips[restoredMsg.src_ip].ip_rep = restoredMsg.ip_rep; } } // Add attack data to source location const attackData = { protocol: restoredMsg.protocol, port: restoredMsg.dst_port, timestamp: new Date(originalEvent.timestamp), src_ip: restoredMsg.src_ip }; circleAttackData[srcKey].attacks.push(attackData); circleAttackData[srcKey].totalAttacks++; circleAttackData[srcKey].lastSeen = new Date(originalEvent.timestamp); circleAttackData[srcKey].ips[restoredMsg.src_ip].attacks.push(attackData); circleAttackData[srcKey].ips[restoredMsg.src_ip].lastSeen = new Date(originalEvent.timestamp); // Initialize or update markerAttackData for destination (honeypot) if (!markerAttackData[dstKey]) { markerAttackData[dstKey] = { country: restoredMsg.dst_country_name, iso_code: restoredMsg.dst_iso_code, dst_ip: restoredMsg.dst_ip, hostname: restoredMsg.tpot_hostname, attacks: [], totalAttacks: 0, uniqueAttackers: new Set(), protocolStats: {}, firstSeen: new Date(originalEvent.timestamp), lastUpdate: new Date(originalEvent.timestamp) }; } // Add attack to honeypot data markerAttackData[dstKey].attacks.push({ src_ip: restoredMsg.src_ip, protocol: restoredMsg.protocol, port: restoredMsg.dst_port, timestamp: new Date(originalEvent.timestamp) }); markerAttackData[dstKey].totalAttacks++; markerAttackData[dstKey].uniqueAttackers.add(restoredMsg.src_ip); markerAttackData[dstKey].protocolStats[restoredMsg.protocol] = (markerAttackData[dstKey].protocolStats[restoredMsg.protocol] || 0) + 1; markerAttackData[dstKey].lastUpdate = new Date(originalEvent.timestamp); // Keep only last 50 attacks per location for performance if (markerAttackData[dstKey].attacks.length > 50) { markerAttackData[dstKey].attacks = markerAttackData[dstKey].attacks.slice(-50); } if (circleAttackData[srcKey].attacks.length > 50) { circleAttackData[srcKey].attacks = circleAttackData[srcKey].attacks.slice(-50); } // Add visual elements (circle for attacker and marker for honeypot) addCircle(restoredMsg.country, restoredMsg.iso_code, restoredMsg.src_ip, restoredMsg.ip_rep, restoredMsg.color, srcLatLng, restoredMsg.protocol); addMarker(restoredMsg.dst_country_name, restoredMsg.dst_iso_code, restoredMsg.dst_ip, restoredMsg.tpot_hostname, dstLatLng); } // Helper function to get protocol color (matches existing logic) function getProtocolColor(protocol) { // Use the same color mapping as the dashboard for consistency const colors = { 'CHARGEN': '#4CAF50', 'FTP-DATA': '#F44336', 'FTP': '#FF5722', 'SSH': '#FF9800', 'TELNET': '#FFC107', 'SMTP': '#8BC34A', 'WINS': '#009688', 'DNS': '#00BCD4', 'DHCP': '#03A9F4', 'TFTP': '#2196F3', 'HTTP': '#3F51B5', 'DICOM': '#9C27B0', 'POP3': '#E91E63', 'NTP': '#795548', 'RPC': '#607D8B', 'IMAP': '#9E9E9E', 'SNMP': '#FF6B35', 'LDAP': '#FF8E53', 'HTTPS': '#0080FF', 'SMB': '#BF00FF', 'SMTPS': '#80FF00', 'EMAIL': '#00FF80', 'IPMI': '#00FFFF', 'IPP': '#8000FF', 'IMAPS': '#FF0080', 'POP3S': '#80FF80', 'NFS': '#FF8080', 'SOCKS': '#8080FF', 'SQL': '#00FF00', 'ORACLE': '#FFFF00', 'PPTP': '#FF00FF', 'MQTT': '#00FF40', 'SSDP': '#40FF00', 'IEC104': '#FF4000', 'HL7': '#4000FF', 'MYSQL': '#00FF00', 'RDP': '#FF0060', 'IPSEC': '#60FF00', 'SIP': '#FFCCFF', 'POSTGRESQL': '#00CCFF', 'ADB': '#FFCCCC', 'VNC': '#0000FF', 'REDIS': '#CC00FF', 'IRC': '#FFCC00', 'JETDIRECT': '#8000FF', 'ELASTICSEARCH': '#FF8000', 'INDUSTRIAL': '#80FF40', 'MEMCACHED': '#40FF80', 'MONGODB': '#FF4080', 'SCADA': '#8040FF', 'OTHER': '#78909C' }; // Normalize the protocol like the dashboard does function normalizeProtocol(protocol) { if (!protocol) return 'OTHER'; // Check if protocol is a numeric string (port number) - convert to OTHER if (/^\d+$/.test(protocol.toString())) { return 'OTHER'; } // List of known protocols to check against const knownProtocols = [ 'CHARGEN', 'FTP-DATA', 'FTP', 'SSH', 'TELNET', 'SMTP', 'WINS', 'DNS', 'DHCP', 'TFTP', 'HTTP', 'DICOM', 'POP3', 'NTP', 'RPC', 'IMAP', 'SNMP', 'LDAP', 'HTTPS', 'SMB', 'SMTPS', 'EMAIL', 'IPMI', 'IPP', 'IMAPS', 'POP3S', 'NFS', 'SOCKS', 'SQL', 'ORACLE', 'PPTP', 'MQTT', 'SSDP', 'IEC104', 'HL7', 'MYSQL', 'RDP', 'IPSEC', 'SIP', 'POSTGRESQL', 'ADB', 'VNC', 'REDIS', 'IRC', 'JETDIRECT', 'ELASTICSEARCH', 'INDUSTRIAL', 'MEMCACHED', 'MONGODB', 'SCADA' ]; const protocolUpper = protocol.toUpperCase(); // If protocol is not in the known list, use "OTHER" if (!knownProtocols.includes(protocolUpper)) { return 'OTHER'; } return protocolUpper; } const normalizedProtocol = normalizeProtocol(protocol); // Return color for the normalized protocol return colors[normalizedProtocol] || colors['OTHER']; } // Use Leaflet's built-in SVG renderer to handle zoom/pan and event bubbling correctly var svgRenderer = L.svg({ clickable: true }).addTo(map); // Select the SVG element and append a group for D3 animations // We use a group to keep our elements separate from Leaflet's internal layers var svg = d3.select(svgRenderer._container).append("g").attr("class", "d3-overlay"); // Ensure the SVG container doesn't block map interactions // Leaflet usually handles this, but we enforce it to fix the Firefox panning issue d3.select(svgRenderer._container).style("pointer-events", "none"); // Clear animations on zoom start to prevent coordinate desync // D3 elements don't automatically re-project during zoom, so we clear them map.on("zoomstart", function() { svg.selectAll("*").remove(); }); // No need for manual translateSVG or moveend listener as Leaflet handles the SVG renderer function calcMidpoint(x1, y1, x2, y2, bend) { if(y2= 200) { // Find the key with the oldest lastSeen time let oldestKey = null; let oldestTime = new Date(); // Start with current time, anything older will be smaller // Only iterate over keys that actually exist on the map to avoid ghost entries const validKeys = Object.keys(circlesObject); for (const key of validKeys) { const data = circleAttackData[key]; if (data && data.lastSeen < oldestTime) { oldestTime = data.lastSeen; oldestKey = key; } } if (oldestKey) { circles.removeLayer(circlesObject[oldestKey]); delete circlesObject[oldestKey]; delete circleAttackData[oldestKey]; } else { // Fallback if something goes wrong const layerToRemove = circleArray[0]; circles.removeLayer(layerToRemove); // Try to find and clean up the key for this layer for (const [key, layer] of Object.entries(circlesObject)) { if (layer === layerToRemove) { delete circlesObject[key]; delete circleAttackData[key]; break; } } } } var key = srcLatLng.lat + "," + srcLatLng.lng; // Check if circle exists and needs color update if (circlesObject[key]) { // Circle exists - check if protocol/color has changed const existingCircle = circlesObject[key]; const currentColor = existingCircle.options.color; // If color changed, update the circle if (currentColor !== color) { console.log(`[CIRCLE-UPDATE] Updating circle color at ${key} from ${currentColor} to ${color} (protocol: ${protocol})`); // Update circle style existingCircle.setStyle({ color: color, fillColor: color, fillOpacity: 0.2 }); // Update protocol tracking in attack data if (circleAttackData[key]) { circleAttackData[key].lastProtocol = protocol; circleAttackData[key].lastColor = color; circleAttackData[key].lastSeen = new Date(); } } // Update IP data if needed if (circleAttackData[key] && circleAttackData[key].ips[src_ip]) { // Update reputation if new data is provided if (ip_rep) { circleAttackData[key].ips[src_ip].ip_rep = ip_rep; } } return; // Circle exists and has been updated if needed } // Create new circle if it doesn't exist // Attack data should already be created in Traffic handler // If for some reason it doesn't exist, create it (fallback) if (!circleAttackData[key]) { circleAttackData[key] = { country: country, iso_code: iso_code, location_key: key, attacks: [], firstSeen: new Date(), lastSeen: new Date(), lastProtocol: protocol, lastColor: color, ips: {} }; } else { // Update protocol tracking for existing data circleAttackData[key].lastProtocol = protocol; circleAttackData[key].lastColor = color; circleAttackData[key].lastSeen = new Date(); } // Ensure IP data exists (fallback) if (!circleAttackData[key].ips[src_ip]) { circleAttackData[key].ips[src_ip] = { src_ip: src_ip, ip_rep: ip_rep, attacks: [], firstSeen: new Date(), lastSeen: new Date() }; } else { // Update reputation if new data is provided if (ip_rep) { circleAttackData[key].ips[src_ip].ip_rep = ip_rep; } } var circle = L.circle(srcLatLng, 50000, { color: color, fillColor: color, fillOpacity: 0.2 }); // Enhanced popup with modern styling var popupContent = createAttackerPopup(circleAttackData[key]); circle.bindPopup(popupContent, { maxWidth: 350, className: 'modern-popup attacker-popup' }); // Add click event for enhanced interaction circle.on('click', function(e) { // Update popup content with latest data var updatedContent = createAttackerPopup(circleAttackData[key]); circle.setPopupContent(updatedContent); }); circlesObject[key] = circle.addTo(circles); } var markersObject = {}; // Store attack data for each marker for enhanced tooltips var markerAttackData = {}; function addMarker(dst_country_name, dst_iso_code, dst_ip, tpot_hostname, dstLatLng) { // Validate parameters if (!dstLatLng || !dstLatLng.lat || !dstLatLng.lng) { return; } markerCount = markers.getLayers().length; markerArray = markers.getLayers(); // Only allow 200 markers to be on the map at a time if (markerCount >= 200) { // Find the key with the oldest lastUpdate time let oldestKey = null; let oldestTime = new Date(); // Only iterate over keys that actually exist on the map to avoid ghost entries const validKeys = Object.keys(markersObject); for (const key of validKeys) { const data = markerAttackData[key]; if (data && data.lastUpdate < oldestTime) { oldestTime = data.lastUpdate; oldestKey = key; } } if (oldestKey) { markers.removeLayer(markersObject[oldestKey]); delete markersObject[oldestKey]; delete markerAttackData[oldestKey]; } else { // Fallback markers.removeLayer(markerArray[0]); // Reset objects if we can't track properly (original behavior fallback) markersObject = {}; markerAttackData = {}; } } var key = dstLatLng.lat + "," + dstLatLng.lng; // Only draw marker if its coordinates are not already present in markersObject if (!markersObject[key]) { // Attack data should already be created in Traffic handler // If for some reason it doesn't exist, create it (fallback) if (!markerAttackData[key]) { markerAttackData[key] = { country: dst_country_name, iso_code: dst_iso_code, dst_ip: dst_ip, hostname: tpot_hostname, attacks: [], totalAttacks: 0, uniqueAttackers: new Set(), protocolStats: {}, firstSeen: new Date(), lastUpdate: new Date() }; } var marker = L.marker(dstLatLng, { icon: L.icon({ iconUrl: 'static/images/honeypot-marker.svg', iconSize: [48, 48], // Match original square size iconAnchor: [24, 40], // Adjusted anchor to fix hovering (was 48) popupAnchor: [0, -48], // Match original popup position className: 'honeypot-marker' }), }); // Enhanced popup with modern styling var popupContent = createHoneypotPopup(markerAttackData[key]); marker.bindPopup(popupContent, { maxWidth: 400, className: 'modern-popup honeypot-popup' }); // Add click event for enhanced interaction marker.on('click', function(e) { // Update popup content with latest data var updatedContent = createHoneypotPopup(markerAttackData[key]); marker.setPopupContent(updatedContent); }); markersObject[key] = marker.addTo(markers); } } function handleStats(msg) { const last = ["last_1m", "last_1h", "last_24h"]; // Check if message contains any stats data const hasData = last.some(key => msg[key] !== undefined && msg[key] !== null); if (!hasData) { // If message is empty (backend failed to fetch stats), just return // We don't want to spam the console with warnings every 10 seconds console.log('[WARNING] Stats message contains no valid data:', msg); return; } // Valid data received - update timestamp for connection status console.log('[STATS] Valid stats data received, updating last valid timestamp.'); window.lastValidDataTime = Date.now(); last.forEach(function(i) { const element = document.getElementById(i); if (element) { const oldValue = element.textContent; const newValue = msg[i]; // Check if newValue exists and is not undefined if (newValue !== undefined && newValue !== null) { // Only animate if value actually changed if (oldValue !== newValue.toString()) { element.textContent = newValue; element.setAttribute('data-updated', 'true'); // Remove animation class after animation completes setTimeout(() => { element.removeAttribute('data-updated'); }, 600); } } else { console.warn('[WARNING] Stats value is undefined for:', i, 'in message:', msg); } } }); }; // WEBSOCKET STUFF // Helper function to format reputation with line breaks for multi-word values function formatReputation(reputation) { if (!reputation) return 'Unknown'; // Add line break if the value contains multiple words (space separated) const words = reputation.trim().split(/\s+/); if (words.length > 1) { return words.join('
'); } return reputation; } // Modern popup creation functions function createAttackerPopup(attackerData) { // Validate attackerData structure if (!attackerData || typeof attackerData !== 'object') { console.error('[ERROR] Invalid attackerData:', attackerData); const errorDiv = document.createElement('div'); errorDiv.className = 'popup-content'; const errorRow = document.createElement('div'); errorRow.className = 'info-row'; errorRow.textContent = 'Error: Invalid data'; errorDiv.appendChild(errorRow); return errorDiv; } // Ensure required fields exist with defaults if (!attackerData.firstSeen) attackerData.firstSeen = new Date(); if (!attackerData.lastSeen) attackerData.lastSeen = new Date(); if (!attackerData.attacks) attackerData.attacks = []; if (!attackerData.ips) attackerData.ips = {}; if (!attackerData.country) attackerData.country = 'Unknown'; if (!attackerData.iso_code) attackerData.iso_code = 'XX'; const now = new Date(); const firstSeenAgo = formatTimeAgo(attackerData.firstSeen); const lastSeenAgo = formatTimeAgo(attackerData.lastSeen); // Get list of unique IPs at this location const ips = Object.keys(attackerData.ips); const totalAttacks = attackerData.attacks.length; // Get protocol stats from all attacks const protocolCounts = {}; attackerData.attacks.forEach(attack => { protocolCounts[attack.protocol] = (protocolCounts[attack.protocol] || 0) + 1; }); const topProtocol = Object.keys(protocolCounts).reduce((a, b) => protocolCounts[a] > protocolCounts[b] ? a : b, 'N/A'); const container = document.createElement('div'); // Header const header = document.createElement('div'); header.className = 'popup-header'; const flagImg = document.createElement('img'); flagImg.src = `static/flags/${attackerData.iso_code}.svg`; flagImg.width = 64; flagImg.height = 44; flagImg.className = 'flag-icon'; header.appendChild(flagImg); const titleDiv = document.createElement('div'); titleDiv.className = 'popup-title'; const h4 = document.createElement('h4'); const subtitle = document.createElement('span'); subtitle.className = 'popup-subtitle'; subtitle.textContent = attackerData.country; titleDiv.appendChild(h4); titleDiv.appendChild(subtitle); header.appendChild(titleDiv); container.appendChild(header); const content = document.createElement('div'); content.className = 'popup-content'; container.appendChild(content); // Helper to create info row function createInfoRow(label, value, valueClass = '') { const row = document.createElement('div'); row.className = 'info-row'; const labelSpan = document.createElement('span'); labelSpan.className = 'info-label'; labelSpan.textContent = label; const valueSpan = document.createElement('span'); valueSpan.className = 'info-value ' + valueClass; if (value instanceof Node) { valueSpan.appendChild(value); } else { valueSpan.textContent = value; } row.appendChild(labelSpan); row.appendChild(valueSpan); return row; } if (ips.length === 1) { // Single IP h4.textContent = 'Origen del Atacante'; const ipData = attackerData.ips[ips[0]]; if (!ipData) { console.error('[ERROR] IP data is missing for:', ips[0]); const err = document.createElement('div'); err.className = 'info-row'; err.textContent = 'Error: IP data corrupted'; content.appendChild(err); return container; } // Defaults if (!ipData.src_ip) ipData.src_ip = ips[0] || 'Unknown'; if (ipData.ip_rep === undefined || ipData.ip_rep === null) ipData.ip_rep = 'Unknown'; content.appendChild(createInfoRow('IP Origen:', ipData.src_ip)); // Handle reputation with safe line breaks const repFragment = document.createDocumentFragment(); const words = (ipData.ip_rep || 'Unknown').trim().split(/\s+/); words.forEach((word, index) => { if (index > 0) repFragment.appendChild(document.createElement('br')); repFragment.appendChild(document.createTextNode(word)); }); content.appendChild(createInfoRow('Reputación:', repFragment, getReputationClass(ipData.ip_rep))); content.appendChild(createInfoRow('Total Ataques:', ipData.attacks.length)); // Protocol Badge const protoRow = document.createElement('div'); protoRow.className = 'info-row'; const protoLabel = document.createElement('span'); protoLabel.className = 'info-label'; protoLabel.textContent = 'Protocolo Principal:'; const protoBadge = document.createElement('span'); protoBadge.className = `protocol-badge protocol-${topProtocol.toLowerCase()}`; protoBadge.textContent = topProtocol; protoRow.appendChild(protoLabel); protoRow.appendChild(protoBadge); content.appendChild(protoRow); content.appendChild(createInfoRow('Primera vez visto:', formatTimeAgo(ipData.firstSeen || new Date()))); content.appendChild(createInfoRow('Última vez visto:', formatTimeAgo(ipData.lastSeen || new Date()))); } else { // Multiple IPs h4.textContent = 'Múltiples Atacantes'; const sortedIps = ips.map(ip => { const ipData = attackerData.ips[ip]; if (!ipData || !ipData.attacks) return { ip: ip, attackCount: 0 }; return { ip: ip, attackCount: ipData.attacks.length }; }).sort((a, b) => b.attackCount - a.attackCount); const topIps = sortedIps.slice(0, 3); content.appendChild(createInfoRow('Total IPs:', ips.length)); content.appendChild(createInfoRow('Total Ataques:', totalAttacks)); // Protocol Badge const protoRow = document.createElement('div'); protoRow.className = 'info-row'; const protoLabel = document.createElement('span'); protoLabel.className = 'info-label'; protoLabel.textContent = 'Protocolo Principal:'; const protoBadge = document.createElement('span'); protoBadge.className = `protocol-badge protocol-${topProtocol.toLowerCase()}`; protoBadge.textContent = topProtocol; protoRow.appendChild(protoLabel); protoRow.appendChild(protoBadge); content.appendChild(protoRow); // Top Source IPs Section const section = document.createElement('div'); section.className = 'info-section'; const sectionLabel = document.createElement('span'); sectionLabel.className = 'section-label'; sectionLabel.textContent = 'IPs Origen Principales:'; section.appendChild(sectionLabel); topIps.forEach(ipInfo => { const detail = document.createElement('div'); detail.className = 'ip-detail'; const ipAddr = document.createElement('span'); ipAddr.className = 'ip-address'; ipAddr.textContent = ipInfo.ip; const ipCount = document.createElement('span'); ipCount.className = 'ip-count'; ipCount.textContent = `${ipInfo.attackCount} ataques`; detail.appendChild(ipAddr); detail.appendChild(ipCount); section.appendChild(detail); }); if (ips.length > 3) { const more = document.createElement('div'); more.className = 'ip-detail more-ips'; more.textContent = `... y ${ips.length - 3} más`; section.appendChild(more); } content.appendChild(section); content.appendChild(createInfoRow('Primera vez visto:', firstSeenAgo)); content.appendChild(createInfoRow('Última vez visto:', lastSeenAgo)); } return container; } function createHoneypotPopup(honeypotData) { const now = new Date(); const lastUpdateAgo = formatTimeAgo(honeypotData.lastUpdate); // Get top 3 protocols const sortedProtocols = Object.entries(honeypotData.protocolStats) .sort(([,a], [,b]) => b - a) .slice(0, 3); const container = document.createElement('div'); // Header const header = document.createElement('div'); header.className = 'popup-header'; const flagImg = document.createElement('img'); flagImg.src = `static/flags/${honeypotData.iso_code}.svg`; flagImg.width = 64; flagImg.height = 44; flagImg.className = 'flag-icon'; header.appendChild(flagImg); const titleDiv = document.createElement('div'); titleDiv.className = 'popup-title'; const h4 = document.createElement('h4'); h4.textContent = 'Honeypot T-Pot'; const subtitle = document.createElement('span'); subtitle.className = 'popup-subtitle'; subtitle.textContent = honeypotData.country; titleDiv.appendChild(h4); titleDiv.appendChild(subtitle); header.appendChild(titleDiv); container.appendChild(header); const content = document.createElement('div'); content.className = 'popup-content'; container.appendChild(content); // Helper to create info row function createInfoRow(label, value) { const row = document.createElement('div'); row.className = 'info-row'; const labelSpan = document.createElement('span'); labelSpan.className = 'info-label'; labelSpan.textContent = label; const valueSpan = document.createElement('span'); valueSpan.className = 'info-value'; valueSpan.textContent = value; row.appendChild(labelSpan); row.appendChild(valueSpan); return row; } content.appendChild(createInfoRow('Hostname:', honeypotData.hostname)); content.appendChild(createInfoRow('IP Destino:', honeypotData.dst_ip)); content.appendChild(createInfoRow('Total Ataques:', honeypotData.totalAttacks)); content.appendChild(createInfoRow('Atacantes Únicos:', honeypotData.uniqueAttackers.size)); if (sortedProtocols.length > 0) { const section = document.createElement('div'); section.className = 'info-section'; const sectionLabel = document.createElement('span'); sectionLabel.className = 'section-label'; sectionLabel.textContent = 'Protocolos Principales:'; section.appendChild(sectionLabel); sortedProtocols.forEach(([protocol, count]) => { const stat = document.createElement('div'); stat.className = 'protocol-stat'; const badge = document.createElement('span'); badge.className = `protocol-badge protocol-${protocol.toLowerCase()}`; badge.textContent = protocol; const countSpan = document.createElement('span'); countSpan.className = 'protocol-count'; countSpan.textContent = count; stat.appendChild(badge); stat.appendChild(countSpan); section.appendChild(stat); }); content.appendChild(section); } content.appendChild(createInfoRow('Última Actualización:', lastUpdateAgo)); return container; } function formatTimeAgo(date) { const now = new Date(); const diffMs = now - date; const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMins / 60); const diffDays = Math.floor(diffHours / 24); if (diffMins < 1) return 'Ahora mismo'; if (diffMins < 60) return `hace ${diffMins}m`; if (diffHours < 24) return `hace ${diffHours}h`; return `hace ${diffDays}d`; } function getReputationClass(reputation) { if (reputation === 'MALICIOUS') return 'reputation-malicious'; if (reputation === 'SUSPICIOUS') return 'reputation-suspicious'; return 'reputation-clean'; } const messageHandlers = { Traffic: (msg) => { // Valid data received - update timestamp for connection status window.lastValidDataTime = Date.now(); var srcLatLng = new L.LatLng(msg.src_lat, msg.src_long); var dstLatLng = new L.LatLng(msg.dst_lat, msg.dst_long); var dstPoint = map.latLngToLayerPoint(dstLatLng); var srcPoint = map.latLngToLayerPoint(srcLatLng); // Store attack data for tooltips var srcKey = srcLatLng.lat + "," + srcLatLng.lng; var dstKey = dstLatLng.lat + "," + dstLatLng.lng; // Pre-create attacker data structure if needed if (!circleAttackData[srcKey]) { circleAttackData[srcKey] = { country: msg.country, iso_code: msg.iso_code, location_key: srcKey, attacks: [], firstSeen: new Date(), lastSeen: new Date(), lastProtocol: msg.protocol, lastColor: msg.color, // Track multiple IPs at the same location ips: {} }; } else { // Update protocol tracking for existing location circleAttackData[srcKey].lastProtocol = msg.protocol; circleAttackData[srcKey].lastColor = msg.color; circleAttackData[srcKey].lastSeen = new Date(); } // Initialize IP-specific data if this is a new IP at this location if (!circleAttackData[srcKey].ips[msg.src_ip]) { circleAttackData[srcKey].ips[msg.src_ip] = { src_ip: msg.src_ip, ip_rep: msg.ip_rep, attacks: [], firstSeen: new Date(), lastSeen: new Date() }; } // Pre-create honeypot data structure if needed if (!markerAttackData[dstKey]) { markerAttackData[dstKey] = { country: msg.dst_country_name, iso_code: msg.dst_iso_code, dst_ip: msg.dst_ip, hostname: msg.tpot_hostname, attacks: [], totalAttacks: 0, uniqueAttackers: new Set(), protocolStats: {}, firstSeen: new Date(), lastUpdate: new Date() }; } Promise.all([ addCircle(msg.country, msg.iso_code, msg.src_ip, msg.ip_rep, msg.color, srcLatLng, msg.protocol), addMarker(msg.dst_country_name, msg.dst_iso_code, msg.dst_ip, msg.tpot_hostname, dstLatLng), handleParticle(msg.color, srcPoint), handleTraffic(msg.color, srcPoint, dstPoint, srcLatLng) ]).then(() => { // Add attack data AFTER visual elements are created/updated const attackData = { protocol: msg.protocol, port: msg.dst_port, honeypot: msg.honeypot, timestamp: new Date(), src_ip: msg.src_ip }; // Add to overall location attacks circleAttackData[srcKey].attacks.push(attackData); circleAttackData[srcKey].lastSeen = new Date(); // Add to IP-specific attacks circleAttackData[srcKey].ips[msg.src_ip].attacks.push(attackData); circleAttackData[srcKey].ips[msg.src_ip].lastSeen = new Date(); // Add attack to honeypot data markerAttackData[dstKey].attacks.push({ src_ip: msg.src_ip, protocol: msg.protocol, port: msg.dst_port, timestamp: new Date() }); markerAttackData[dstKey].totalAttacks++; markerAttackData[dstKey].uniqueAttackers.add(msg.src_ip); markerAttackData[dstKey].protocolStats[msg.protocol] = (markerAttackData[dstKey].protocolStats[msg.protocol] || 0) + 1; markerAttackData[dstKey].lastUpdate = new Date(); // Keep only last 50 attacks per honeypot for performance if (markerAttackData[dstKey].attacks.length > 50) { markerAttackData[dstKey].attacks = markerAttackData[dstKey].attacks.slice(-50); } }); // Send to dashboard for Live Feed processing with correct field mapping if (window.attackMapDashboard) { const attackData = { ip: msg.src_ip, source_ip: msg.src_ip, src_ip: msg.src_ip, ip_rep: msg.ip_rep, tpot_hostname: msg.tpot_hostname, color: msg.color, country: msg.country, country_code: msg.iso_code, iso_code: msg.iso_code, protocol: msg.protocol, honeypot: msg.honeypot, // Use honeypot field from message, not tpot_hostname port: msg.dst_port, dst_port: msg.dst_port, destination_ip: msg.dst_ip, destination_port: msg.dst_port, // Add honeypot location data for proper flag restoration dst_country_name: msg.dst_country_name, dst_iso_code: msg.dst_iso_code, destination_country: msg.dst_country_name, // Alternative field name destination_country_code: msg.dst_iso_code, // Alternative field name // Add coordinate data for map restoration source_lat: msg.src_lat, source_lng: msg.src_long, destination_lat: msg.dst_lat, destination_lng: msg.dst_long, timestamp: Date.now(), event_time: msg.event_time }; // Send to live feed window.attackMapDashboard.addAttackEvent(attackData); // Send to honeypot performance tracking window.attackMapDashboard.processAttackForDashboard(attackData); } }, Stats: (msg) => { handleStats(msg); }, }; // Enhanced WebSocket handling with dashboard integration function connectWebSocket() { // Prevent multiple connection attempts if (isReconnecting) { console.log('[INFO] Connection attempt already in progress'); return; } // Close existing connection if it exists to prevent resource leaks if (window.webSocket) { try { console.log('[INFO] Cleaning up existing WebSocket before reconnection'); window.webSocket.close(); } catch (e) { console.log('[WARN] Error closing existing WebSocket:', e); } } isReconnecting = true; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const WS_HOST = protocol + '//' + window.location.host + '/websocket'; // Update status to connecting when attempting connection if (window.attackMapDashboard) { window.attackMapDashboard.updateConnectionStatus('connecting'); } // Make WebSocket globally accessible for dashboard monitoring window.webSocket = webSocket = new WebSocket(WS_HOST); webSocket.onopen = function () { // Reset reconnection tracking isReconnecting = false; reconnectAttempts = 0; // Reset last message time to prevent immediate timeout on reconnection window.lastWebSocketMessageTime = Date.now(); window.lastValidDataTime = Date.now(); // Reset valid data timer // Set global connection flag immediately window.webSocketConnected = true; // Start heartbeat to monitor connection health startHeartbeat(); // Update connection status in dashboard with better retry logic function updateStatusWithRetry(attempts = 0) { const maxAttempts = 10; // Try for up to 5 seconds if (window.attackMapDashboard) { window.attackMapDashboard.updateConnectionStatus('connected'); console.log('[*] WebSocket connection status updated to connected'); } else if (attempts < maxAttempts) { // Dashboard not ready yet, retry with exponential backoff const delay = Math.min(100 + (attempts * 100), 1000); // 100ms to 1000ms setTimeout(() => updateStatusWithRetry(attempts + 1), delay); } else { console.log('[WARNING] Dashboard not available after retries, but flag is set'); } } updateStatusWithRetry(); console.log('[*] WebSocket connection established.'); }; webSocket.onclose = function (event) { // Stop heartbeat when connection closes stopHeartbeat(); // Clear the WebSocket connected flag window.webSocketConnected = false; var reason = "Unknown error reason?"; if (event.code == 1000) reason = "[ ] Endpoint terminating connection: Normal closure"; else if(event.code == 1001) reason = "[ ] Endpoint terminating connection: Endpoint is \"going away\""; else if(event.code == 1002) reason = "[ ] Endpoint terminating connection: Protocol error"; else if(event.code == 1003) reason = "[ ] Endpoint terminating connection: Unknown data"; else if(event.code == 1004) reason = "[ ] Endpoint terminating connection: Reserved"; else if(event.code == 1005) reason = "[ ] Endpoint terminating connection: No status code"; else if(event.code == 1006) reason = "[ ] Endpoint terminating connection: Connection closed abnormally"; else if(event.code == 1007) reason = "[ ] Endpoint terminating connection: Message was not consistent with the type of the message"; else if(event.code == 1008) reason = "[ ] Endpoint terminating connection: Message \"violates policy\""; else if(event.code == 1009) reason = "[ ] Endpoint terminating connection: Message is too big"; else if(event.code == 1010) reason = "[ ] Endpoint terminating connection: Client failed to negotiate ("+event.reason+")"; else if(event.code == 1011) reason = "[ ] Endpoint terminating connection: Server encountered an unexpected condition"; else if(event.code == 1015) reason = "[ ] Endpoint terminating connection: Connection closed due TLS handshake failure"; else reason = "[ ] Endpoint terminating connection; Unknown reason"; // Update dashboard connection status if (window.attackMapDashboard) { window.attackMapDashboard.updateConnectionStatus('disconnected'); } console.log(reason); // Always attempt to reconnect if not a clean closure (or even if it is, depending on requirements, but usually 1000 is manual) // User requirement: "Every 60 seconds a reconnection attempt should be made" if (event.code !== 1000) { const delay = reconnectDelay; console.log(`[INFO] Connection lost. Attempting reconnection in ${delay}ms`); setTimeout(() => { reconnectAttempts++; isReconnecting = false; // Reset flag to allow new connection attempt connectWebSocket(); }, delay); } else { isReconnecting = false; console.log('[INFO] Connection closed normally. No auto-reconnect.'); } }; webSocket.onerror = function (error) { console.log('[ERROR] WebSocket error:', error); // Stop heartbeat on error stopHeartbeat(); // Update status to disconnected on error if (window.attackMapDashboard) { window.attackMapDashboard.updateConnectionStatus('disconnected'); } }; webSocket.onmessage = function (e) { try { // Update last message time for connection health monitoring window.lastWebSocketMessageTime = Date.now(); var msg = JSON.parse(e.data); let handler = messageHandlers[msg.type]; if (handler) { handler(msg); } else { console.warn('[WARNING] No handler found for message type:', msg.type); } // Let dashboard handle its own processing through messageHandlers // Removed duplicate addAttackEvent call to prevent double entries } catch (error) { console.error('[ERROR] Failed to parse WebSocket message:', error); console.log('[ERROR] Raw message data:', e.data); } }; } // Heartbeat functions to monitor connection health function startHeartbeat() { stopHeartbeat(); // Clear any existing heartbeat heartbeatInterval = setInterval(() => { const now = Date.now(); const timeSinceLastMessage = now - window.lastWebSocketMessageTime; // Log warning if no messages for extended time, but do NOT force close // This allows for "Idle" state if (timeSinceLastMessage > 60000) { console.log('[INFO] No messages received for 1 minute. Connection state should be Idle.'); } }, 30000); // Check every 30 seconds } function stopHeartbeat() { if (heartbeatInterval) { clearInterval(heartbeatInterval); heartbeatInterval = null; } } // Enhanced function to check connection health function checkConnectionHealth() { if (!webSocket || webSocket.readyState !== WebSocket.OPEN) { console.log('[INFO] WebSocket not connected, attempting to reconnect...'); if (window.attackMapDashboard) { window.attackMapDashboard.updateConnectionStatus('disconnected'); } return false; } // Simple check: Is the socket technically open? if (!webSocket || webSocket.readyState !== WebSocket.OPEN) { return false; } return true; } // Initialize connection when DOM is ready document.addEventListener('DOMContentLoaded', function() { connectWebSocket(); }); // Map theme update function function updateMapTheme(theme) { if (!window.map || !mapLayers[theme]) return; // Remove current layer window.map.eachLayer(function(layer) { if (layer._url && layer._url.includes('basemaps.cartocdn.com')) { window.map.removeLayer(layer); } }); // Add new theme layer mapLayers[theme].addTo(window.map); } // Listen for theme changes document.addEventListener('DOMContentLoaded', function() { const observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { if (mutation.type === 'attributes' && mutation.attributeName === 'data-theme') { const newTheme = document.documentElement.getAttribute('data-theme'); updateMapTheme(newTheme); } }); }); observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] }); // Add page visibility change handler document.addEventListener('visibilitychange', function() { isPageVisible = !document.hidden; if (isPageVisible) { // Set waking up flag to suppress animation burst from buffered messages isWakingUp = true; setTimeout(() => { isWakingUp = false; }, 2000); // 2 second grace period // Clean up any stuck D3 animations from background throttling if (typeof svg !== 'undefined' && svg) { svg.selectAll("*").remove(); } // Check connection health and reconnect if needed if (!checkConnectionHealth()) { console.log('Connection lost while backgrounded, reconnecting...'); isReconnecting = false; connectWebSocket(); } } else { // Page hidden - background operation mode } }); // Start connection health monitoring // Removed aggressive health check as per new logic: // - Connected: Data < 30s // - Idle: No Data > 30s (but socket open) // - Disconnected: Socket Closed /* function startConnectionHealthCheck() { if (connectionHealthCheck) clearInterval(connectionHealthCheck); connectionHealthCheck = setInterval(() => { // ... removed ... }, 30000); } startConnectionHealthCheck(); */ });