/** * Main application logic for DNSRecon web interface * Handles UI interactions, API communication, and data flow * DEBUG VERSION WITH EXTRA LOGGING */ class DNSReconApp { constructor() { console.log('DNSReconApp constructor called'); this.graphManager = null; this.scanStatus = 'idle'; this.pollInterval = null; this.currentSessionId = null; // UI Elements this.elements = {}; // Application state this.isScanning = false; this.lastGraphUpdate = null; this.init(); } /** * Initialize the application */ init() { console.log('DNSReconApp init called'); document.addEventListener('DOMContentLoaded', () => { console.log('DOM loaded, initializing application...'); try { this.initializeElements(); this.setupEventHandlers(); this.initializeGraph(); this.updateStatus(); this.loadProviders(); console.log('DNSRecon application initialized successfully'); } catch (error) { console.error('Failed to initialize DNSRecon application:', error); this.showError(`Initialization failed: ${error.message}`); } }); } /** * Initialize DOM element references */ initializeElements() { console.log('Initializing DOM elements...'); this.elements = { // Form elements targetDomain: document.getElementById('target-domain'), maxDepth: document.getElementById('max-depth'), startScan: document.getElementById('start-scan'), addToGraph: document.getElementById('add-to-graph'), stopScan: document.getElementById('stop-scan'), exportResults: document.getElementById('export-results'), configureApiKeys: document.getElementById('configure-api-keys'), // Status elements scanStatus: document.getElementById('scan-status'), targetDisplay: document.getElementById('target-display'), depthDisplay: document.getElementById('depth-display'), progressDisplay: document.getElementById('progress-display'), indicatorsDisplay: document.getElementById('indicators-display'), relationshipsDisplay: document.getElementById('relationships-display'), progressFill: document.getElementById('progress-fill'), // Provider elements providerList: document.getElementById('provider-list'), // Node Modal elements nodeModal: document.getElementById('node-modal'), modalTitle: document.getElementById('modal-title'), modalDetails: document.getElementById('modal-details'), modalClose: document.getElementById('modal-close'), // API Key Modal elements apiKeyModal: document.getElementById('api-key-modal'), apiKeyModalClose: document.getElementById('api-key-modal-close'), virustotalApiKey: document.getElementById('virustotal-api-key'), shodanApiKey: document.getElementById('shodan-api-key'), saveApiKeys: document.getElementById('save-api-keys'), resetApiKeys: document.getElementById('reset-api-keys'), // Other elements sessionId: document.getElementById('session-id'), connectionStatus: document.getElementById('connection-status') }; // Verify critical elements exist const requiredElements = ['targetDomain', 'startScan', 'scanStatus']; for (const elementName of requiredElements) { if (!this.elements[elementName]) { throw new Error(`Required element '${elementName}' not found in DOM`); } } console.log('DOM elements initialized successfully'); this.createMessageContainer(); } /** * Create a message container for showing user feedback */ createMessageContainer() { // Check if message container already exists let messageContainer = document.getElementById('message-container'); if (!messageContainer) { messageContainer = document.createElement('div'); messageContainer.id = 'message-container'; messageContainer.className = 'message-container'; messageContainer.style.cssText = ` position: fixed; top: 20px; right: 20px; z-index: 1000; max-width: 400px; `; document.body.appendChild(messageContainer); console.log('Message container created'); } } /** * Setup event handlers */ setupEventHandlers() { console.log('Setting up event handlers...'); try { // Form interactions this.elements.startScan.addEventListener('click', (e) => { console.log('Start scan button clicked'); e.preventDefault(); this.startScan(); }); this.elements.addToGraph.addEventListener('click', (e) => { e.preventDefault(); this.startScan(false); }); this.elements.stopScan.addEventListener('click', (e) => { console.log('Stop scan button clicked'); e.preventDefault(); this.stopScan(); }); this.elements.exportResults.addEventListener('click', (e) => { console.log('Export results button clicked'); e.preventDefault(); this.exportResults(); }); this.elements.configureApiKeys.addEventListener('click', () => this.showApiKeyModal()); // Enter key support for target domain input this.elements.targetDomain.addEventListener('keypress', (e) => { if (e.key === 'Enter' && !this.isScanning) { console.log('Enter key pressed in domain input'); this.startScan(); } }); // Node Modal interactions if (this.elements.modalClose) { this.elements.modalClose.addEventListener('click', () => this.hideModal()); } if (this.elements.nodeModal) { this.elements.nodeModal.addEventListener('click', (e) => { if (e.target === this.elements.nodeModal) this.hideModal(); }); } // API Key Modal interactions if (this.elements.apiKeyModalClose) { this.elements.apiKeyModalClose.addEventListener('click', () => this.hideApiKeyModal()); } if (this.elements.apiKeyModal) { this.elements.apiKeyModal.addEventListener('click', (e) => { if (e.target === this.elements.apiKeyModal) this.hideApiKeyModal(); }); } if (this.elements.saveApiKeys) { this.elements.saveApiKeys.addEventListener('click', () => this.saveApiKeys()); } if (this.elements.resetApiKeys) { this.elements.resetApiKeys.addEventListener('click', () => this.resetApiKeys()); } // Custom events document.addEventListener('nodeSelected', (e) => { this.showNodeModal(e.detail.nodeId, e.detail.node); }); // Keyboard shortcuts document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { this.hideModal(); this.hideApiKeyModal(); } }); // Window events window.addEventListener('beforeunload', () => { if (this.isScanning) { return 'A scan is currently in progress. Are you sure you want to leave?'; } }); console.log('Event handlers set up successfully'); } catch (error) { console.error('Failed to setup event handlers:', error); throw error; } } /** * Initialize graph visualization */ initializeGraph() { try { console.log('Initializing graph manager...'); this.graphManager = new GraphManager('network-graph'); console.log('Graph manager initialized successfully'); } catch (error) { console.error('Failed to initialize graph manager:', error); this.showError('Failed to initialize graph visualization'); } } /** * Start a reconnaissance scan */ async startScan(clearGraph = true) { console.log('=== STARTING SCAN ==='); try { const targetDomain = this.elements.targetDomain.value.trim(); const maxDepth = parseInt(this.elements.maxDepth.value); console.log(`Target domain: "${targetDomain}", Max depth: ${maxDepth}`); // Validation if (!targetDomain) { console.log('Validation failed: empty domain'); this.showError('Please enter a target domain'); this.elements.targetDomain.focus(); return; } if (!this.isValidDomain(targetDomain)) { console.log(`Validation failed: invalid domain format for "${targetDomain}"`); this.showError('Please enter a valid domain name (e.g., example.com)'); this.elements.targetDomain.focus(); return; } console.log('Validation passed, setting UI state to scanning...'); this.setUIState('scanning'); this.showInfo('Starting reconnaissance scan...'); console.log('Making API call to start scan...'); const requestData = { target_domain: targetDomain, max_depth: maxDepth, clear_graph: clearGraph }; console.log('Request data:', requestData); const response = await this.apiCall('/api/scan/start', 'POST', requestData); console.log('API response received:', response); if (response.success) { this.currentSessionId = response.scan_id; this.startPolling(); this.showSuccess('Reconnaissance scan started successfully'); if (clearGraph) { this.graphManager.clear(); } console.log(`Scan started for ${targetDomain} with depth ${maxDepth}`); // Force an immediate status update console.log('Forcing immediate status update...'); setTimeout(() => { this.updateStatus(); this.updateGraph(); }, 100); } else { throw new Error(response.error || 'Failed to start scan'); } } catch (error) { console.error('Failed to start scan:', error); this.showError(`Failed to start scan: ${error.message}`); this.setUIState('idle'); } } /** * Stop the current scan */ async stopScan() { try { console.log('Stopping scan...'); const response = await this.apiCall('/api/scan/stop', 'POST'); if (response.success) { this.showSuccess('Scan stop requested'); console.log('Scan stop requested'); } else { throw new Error(response.error || 'Failed to stop scan'); } } catch (error) { console.error('Failed to stop scan:', error); this.showError(`Failed to stop scan: ${error.message}`); } } /** * Export scan results */ async exportResults() { try { console.log('Exporting results...'); // Create a temporary link to trigger download const link = document.createElement('a'); link.href = '/api/export'; link.download = ''; // Let server determine filename document.body.appendChild(link); link.click(); document.body.removeChild(link); this.showSuccess('Results export initiated'); console.log('Results export initiated'); } catch (error) { console.error('Failed to export results:', error); this.showError(`Failed to export results: ${error.message}`); } } /** * Start polling for scan updates */ startPolling() { console.log('=== STARTING POLLING ==='); if (this.pollInterval) { console.log('Clearing existing poll interval'); clearInterval(this.pollInterval); } this.pollInterval = setInterval(() => { console.log('--- Polling tick ---'); this.updateStatus(); this.updateGraph(); this.loadProviders(); }, 1000); // Poll every 1 second for debugging console.log('Polling started with 1 second interval'); } /** * Stop polling for updates */ stopPolling() { console.log('=== STOPPING POLLING ==='); if (this.pollInterval) { clearInterval(this.pollInterval); this.pollInterval = null; } } /** * Update scan status from server */ async updateStatus() { try { console.log('Updating status...'); const response = await this.apiCall('/api/scan/status'); console.log('Status response:', response); if (response.success) { const status = response.status; console.log('Current scan status:', status.status); console.log('Current progress:', status.progress_percentage + '%'); console.log('Graph stats:', status.graph_statistics); this.updateStatusDisplay(status); // Handle status changes if (status.status !== this.scanStatus) { console.log(`*** STATUS CHANGED: ${this.scanStatus} -> ${status.status} ***`); this.handleStatusChange(status.status); } this.scanStatus = status.status; } else { console.error('Status update failed:', response); } } catch (error) { console.error('Failed to update status:', error); this.showConnectionError(); } } /** * Update graph from server */ async updateGraph() { try { console.log('Updating graph...'); const response = await this.apiCall('/api/graph'); console.log('Graph response:', response); if (response.success) { const graphData = response.graph; console.log('Graph data received:'); console.log('- Nodes:', graphData.nodes ? graphData.nodes.length : 0); console.log('- Edges:', graphData.edges ? graphData.edges.length : 0); if (graphData.nodes) { graphData.nodes.forEach(node => { console.log(` Node: ${node.id} (${node.type})`); }); } if (graphData.edges) { graphData.edges.forEach(edge => { console.log(` Edge: ${edge.from} -> ${edge.to} (${edge.label})`); }); } // Only update if data has changed if (this.hasGraphChanged(graphData)) { console.log('*** GRAPH DATA CHANGED - UPDATING VISUALIZATION ***'); this.graphManager.updateGraph(graphData); this.lastGraphUpdate = Date.now(); // Update relationship count in status const edgeCount = graphData.edges ? graphData.edges.length : 0; if (this.elements.relationshipsDisplay) { this.elements.relationshipsDisplay.textContent = edgeCount; } } else { console.log('Graph data unchanged, skipping update'); } } else { console.error('Graph update failed:', response); } } catch (error) { console.error('Failed to update graph:', error); // Don't show error for graph updates to avoid spam } } /** * Update status display elements * @param {Object} status - Status object from server */ updateStatusDisplay(status) { try { console.log('Updating status display...'); // Update status text with animation if (this.elements.scanStatus) { const formattedStatus = this.formatStatus(status.status); if (this.elements.scanStatus.textContent !== formattedStatus) { this.elements.scanStatus.textContent = formattedStatus; this.elements.scanStatus.classList.add('fade-in'); setTimeout(() => this.elements.scanStatus.classList.remove('fade-in'), 300); } // Add status-specific classes for styling this.elements.scanStatus.className = `status-value status-${status.status}`; } if (this.elements.targetDisplay) { this.elements.targetDisplay.textContent = status.target_domain || 'None'; } if (this.elements.depthDisplay) { this.elements.depthDisplay.textContent = `${status.current_depth}/${status.max_depth}`; } if (this.elements.progressDisplay) { this.elements.progressDisplay.textContent = `${status.progress_percentage.toFixed(1)}%`; } if (this.elements.indicatorsDisplay) { this.elements.indicatorsDisplay.textContent = status.indicators_processed || 0; } // Update progress bar with smooth animation if (this.elements.progressFill) { this.elements.progressFill.style.width = `${status.progress_percentage}%`; // Add pulsing animation for active scans if (status.status === 'running') { this.elements.progressFill.parentElement.classList.add('scanning'); } else { this.elements.progressFill.parentElement.classList.remove('scanning'); } } // Update session ID display with user session info if (this.elements.sessionId) { const scanSessionId = this.currentSessionId; const userSessionId = status.user_session_id; if (scanSessionId && userSessionId) { this.elements.sessionId.textContent = `Session: ${userSessionId.substring(0, 8)}... | Scan: ${scanSessionId}`; } else if (userSessionId) { this.elements.sessionId.textContent = `User Session: ${userSessionId.substring(0, 8)}...`; } else { this.elements.sessionId.textContent = 'Session: Loading...'; } } console.log('Status display updated successfully'); } catch (error) { console.error('Error updating status display:', error); } } /** * Handle status changes * @param {string} newStatus - New scan status */ handleStatusChange(newStatus) { console.log(`=== STATUS CHANGE: ${this.scanStatus} -> ${newStatus} ===`); switch (newStatus) { case 'running': this.setUIState('scanning'); this.showSuccess('Scan is running'); // Reset polling frequency for active scans this.pollFrequency = 2000; this.updateConnectionStatus('active'); break; case 'completed': this.setUIState('completed'); this.stopPolling(); this.showSuccess('Scan completed successfully'); this.updateConnectionStatus('completed'); this.loadProviders(); // Force a final graph update console.log('Scan completed - forcing final graph update'); setTimeout(() => this.updateGraph(), 100); break; case 'failed': this.setUIState('failed'); this.stopPolling(); this.showError('Scan failed'); this.updateConnectionStatus('error'); this.loadProviders(); break; case 'stopped': this.setUIState('stopped'); this.stopPolling(); this.showSuccess('Scan stopped'); this.updateConnectionStatus('stopped'); this.loadProviders(); break; case 'idle': this.setUIState('idle'); this.stopPolling(); this.updateConnectionStatus('idle'); break; } } /** * Update connection status indicator * @param {string} status - Connection status */ updateConnectionStatus(status) { if (!this.elements.connectionStatus) return; const statusColors = { 'idle': '#c7c7c7', 'active': '#00ff41', 'completed': '#00aa2e', 'stopped': '#ff9900', 'error': '#ff6b6b' }; this.elements.connectionStatus.style.backgroundColor = statusColors[status] || '#c7c7c7'; const statusText = this.elements.connectionStatus.parentElement?.querySelector('.status-text'); if (statusText) { const statusTexts = { 'idle': 'System Ready', 'active': 'Scanning Active', 'completed': 'Scan Complete', 'stopped': 'Scan Stopped', 'error': 'Connection Error' }; statusText.textContent = statusTexts[status] || 'System Online'; } } /** * Set UI state based on scan status * @param {string} state - UI state */ setUIState(state) { console.log(`Setting UI state to: ${state}`); switch (state) { case 'scanning': this.isScanning = true; if (this.elements.startScan) { this.elements.startScan.disabled = true; this.elements.startScan.classList.add('loading'); } if (this.elements.addToGraph) { this.elements.addToGraph.disabled = true; this.elements.addToGraph.classList.add('loading'); } if (this.elements.stopScan) { this.elements.stopScan.disabled = false; this.elements.stopScan.classList.remove('loading'); } if (this.elements.targetDomain) this.elements.targetDomain.disabled = true; if (this.elements.maxDepth) this.elements.maxDepth.disabled = true; if (this.elements.configureApiKeys) this.elements.configureApiKeys.disabled = true; break; case 'idle': case 'completed': case 'failed': case 'stopped': this.isScanning = false; if (this.elements.startScan) { this.elements.startScan.disabled = false; this.elements.startScan.classList.remove('loading'); } if (this.elements.addToGraph) { this.elements.addToGraph.disabled = false; this.elements.addToGraph.classList.remove('loading'); } if (this.elements.stopScan) { this.elements.stopScan.disabled = true; } if (this.elements.targetDomain) this.elements.targetDomain.disabled = false; if (this.elements.maxDepth) this.elements.maxDepth.disabled = false; if (this.elements.configureApiKeys) this.elements.configureApiKeys.disabled = false; break; } } /** * Load provider information */ async loadProviders() { try { console.log('Loading providers...'); const response = await this.apiCall('/api/providers'); if (response.success) { this.updateProviderDisplay(response.providers); console.log('Providers loaded successfully'); } } catch (error) { console.error('Failed to load providers:', error); } } /** * Update provider display * @param {Object} providers - Provider information */ updateProviderDisplay(providers) { if (!this.elements.providerList) return; this.elements.providerList.innerHTML = ''; for (const [name, info] of Object.entries(providers)) { const providerItem = document.createElement('div'); providerItem.className = 'provider-item fade-in'; let statusClass = 'disabled'; let statusText = 'Disabled'; if (info.enabled) { statusClass = 'enabled'; statusText = 'Enabled'; } else if (info.requires_api_key) { statusClass = 'api-key-required'; statusText = 'API Key Required'; } providerItem.innerHTML = `
${name.toUpperCase()}
${statusText}
Requests: ${info.statistics.total_requests || 0}
Success Rate: ${(info.statistics.success_rate || 0).toFixed(1)}%
Relationships: ${info.statistics.relationships_found || 0}
Rate Limit: ${info.rate_limit}/min
`; this.elements.providerList.appendChild(providerItem); } } /** * Generates the HTML for the node details view. * @param {Object} node - The node object. * @returns {string} The HTML string for the node details. */ generateNodeDetailsHtml(node) { if(!node) return '
Details not available.
'; let detailsHtml = ''; const createDetailRow = (label, value, statusIcon = '') => { const baseId = `detail-${node.id.replace(/[^a-zA-Z0-9]/g, '-')}-${label.replace(/[^a-zA-Z0-9]/g, '-')}`; if (value === null || value === undefined || (Array.isArray(value) && value.length === 0) || (typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0)) { return `
${label} N/A
`; } if (Array.isArray(value)) { return value.map((item, index) => { const itemId = `${baseId}-${index}`; const itemLabel = index === 0 ? `${label} ` : ''; return `
${itemLabel} ${this.formatValue(item)}
`; }).join(''); } else { const valueId = `${baseId}-0`; const icon = statusIcon || ''; return `
${label} ${icon} ${this.formatValue(value)}
`; } }; const metadata = node.metadata || {}; detailsHtml += createDetailRow('Node Descriptor', node.id); switch (node.type) { case 'domain': detailsHtml += createDetailRow('DNS Records', metadata.dns_records); detailsHtml += createDetailRow('Related Domains (SAN)', metadata.related_domains_san); detailsHtml += createDetailRow('Passive DNS', metadata.passive_dns); detailsHtml += createDetailRow('Shodan Data', metadata.shodan); detailsHtml += createDetailRow('VirusTotal Data', metadata.virustotal); break; case 'ip': detailsHtml += createDetailRow('Hostnames', metadata.hostnames); detailsHtml += createDetailRow('Passive DNS', metadata.passive_dns); detailsHtml += createDetailRow('Shodan Data', metadata.shodan); detailsHtml += createDetailRow('VirusTotal Data', metadata.virustotal); break; } if (metadata.certificate_data && Object.keys(metadata.certificate_data).length > 0) { const cert = metadata.certificate_data; detailsHtml += `
Certificate Summary
`; detailsHtml += createDetailRow('Total Found', cert.total_certificates); detailsHtml += createDetailRow('Currently Valid', cert.valid_certificates); detailsHtml += createDetailRow('Expires Soon (<30d)', cert.expires_soon_count); detailsHtml += createDetailRow('Unique Issuers', cert.unique_issuers ? cert.unique_issuers.join(', ') : 'N/A'); if (cert.latest_certificate) { detailsHtml += `
Latest Certificate
`; detailsHtml += createDetailRow('Common Name', cert.latest_certificate.common_name); detailsHtml += createDetailRow('Issuer', cert.latest_certificate.issuer_name); detailsHtml += createDetailRow('Valid From', new Date(cert.latest_certificate.not_before).toLocaleString()); detailsHtml += createDetailRow('Valid Until', new Date(cert.latest_certificate.not_after).toLocaleString()); } } if (metadata.asn_data && Object.keys(metadata.asn_data).length > 0) { detailsHtml += `
ASN Information
`; detailsHtml += createDetailRow('ASN', metadata.asn_data.asn); detailsHtml += createDetailRow('Organization', metadata.asn_data.description); detailsHtml += createDetailRow('ISP', metadata.asn_data.isp); detailsHtml += createDetailRow('Country', metadata.asn_data.country); } return detailsHtml; } /** * Show node details modal * @param {string} nodeId - Node identifier * @param {Object} node - Node data */ showNodeModal(nodeId, node) { if (!this.elements.nodeModal) return; if (this.elements.modalTitle) { this.elements.modalTitle.textContent = `Node Details`; } let detailsHtml = ''; if (node.type === 'large_entity') { const metadata = node.metadata || {}; const nodes = metadata.nodes || []; const node_type = metadata.node_type || 'nodes'; detailsHtml += `
Contains ${metadata.count} ${node_type}s
`; detailsHtml += '
'; for(const innerNodeId of nodes) { const innerNode = this.graphManager.nodes.get(innerNodeId); detailsHtml += `
`; detailsHtml += `${innerNodeId}`; detailsHtml += this.generateNodeDetailsHtml(innerNode); detailsHtml += `
`; } detailsHtml += '
'; } else { detailsHtml = this.generateNodeDetailsHtml(node); } if (this.elements.modalDetails) { this.elements.modalDetails.innerHTML = detailsHtml; } this.elements.nodeModal.style.display = 'block'; } /** * Hide the modal */ hideModal() { if (this.elements.nodeModal) { this.elements.nodeModal.style.display = 'none'; } } /** * Show API Key modal */ showApiKeyModal() { if (this.elements.apiKeyModal) { this.elements.apiKeyModal.style.display = 'block'; } } /** * Hide API Key modal */ hideApiKeyModal() { if (this.elements.apiKeyModal) { this.elements.apiKeyModal.style.display = 'none'; } } /** * Save API Keys */ async saveApiKeys() { const shodanKey = this.elements.shodanApiKey.value.trim(); const virustotalKey = this.elements.virustotalApiKey.value.trim(); const keys = {}; if (shodanKey) keys.shodan = shodanKey; if (virustotalKey) keys.virustotal = virustotalKey; if (Object.keys(keys).length === 0) { this.showWarning('No API keys were entered.'); return; } try { const response = await this.apiCall('/api/config/api-keys', 'POST', keys); if (response.success) { this.showSuccess(response.message); this.hideApiKeyModal(); this.loadProviders(); // Refresh provider status } else { throw new Error(response.error || 'Failed to save API keys'); } } catch (error) { this.showError(`Error saving API keys: ${error.message}`); } } /** * Reset API Key fields */ resetApiKeys() { this.elements.shodanApiKey.value = ''; this.elements.virustotalApiKey.value = ''; } /** * Check if graph data has changed * @param {Object} graphData - New graph data * @returns {boolean} True if data has changed */ hasGraphChanged(graphData) { // Simple check based on node and edge counts and timestamps const currentStats = this.graphManager.getStatistics(); const newNodeCount = graphData.nodes ? graphData.nodes.length : 0; const newEdgeCount = graphData.edges ? graphData.edges.length : 0; // Check if counts changed const countsChanged = currentStats.nodeCount !== newNodeCount || currentStats.edgeCount !== newEdgeCount; // Also check if we have new timestamp data const hasNewTimestamp = graphData.statistics && graphData.statistics.last_modified && graphData.statistics.last_modified !== this.lastGraphTimestamp; if (hasNewTimestamp) { this.lastGraphTimestamp = graphData.statistics.last_modified; } const changed = countsChanged || hasNewTimestamp; console.log(`Graph change check: Current(${currentStats.nodeCount}n, ${currentStats.edgeCount}e) vs New(${newNodeCount}n, ${newEdgeCount}e) = ${changed}`); return changed; } /** * Make API call to server * @param {string} endpoint - API endpoint * @param {string} method - HTTP method * @param {Object} data - Request data * @returns {Promise} Response data */ async apiCall(endpoint, method = 'GET', data = null) { console.log(`Making API call: ${method} ${endpoint}`, data ? data : '(no data)'); try { const options = { method: method, headers: { 'Content-Type': 'application/json' } }; if (data && method !== 'GET') { options.body = JSON.stringify(data); console.log('Request body:', options.body); } console.log('Fetch options:', options); const response = await fetch(endpoint, options); console.log('Response status:', response.status, response.statusText); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const result = await response.json(); console.log('Response data:', result); return result; } catch (error) { console.error(`API call failed for ${method} ${endpoint}:`, error); throw error; } } /** * Validate domain name - improved validation * @param {string} domain - Domain to validate * @returns {boolean} True if valid */ isValidDomain(domain) { console.log(`Validating domain: "${domain}"`); // Basic checks if (!domain || typeof domain !== 'string') { console.log('Validation failed: empty or non-string domain'); return false; } if (domain.length > 253) { console.log('Validation failed: domain too long'); return false; } if (domain.startsWith('.') || domain.endsWith('.')) { console.log('Validation failed: domain starts or ends with dot'); return false; } if (domain.includes('..')) { console.log('Validation failed: domain contains double dots'); return false; } // Split into parts and validate each const parts = domain.split('.'); if (parts.length < 2) { console.log('Validation failed: domain has less than 2 parts'); return false; } // Check each part for (const part of parts) { if (!part || part.length > 63) { console.log(`Validation failed: invalid part "${part}"`); return false; } if (part.startsWith('-') || part.endsWith('-')) { console.log(`Validation failed: part "${part}" starts or ends with hyphen`); return false; } if (!/^[a-zA-Z0-9-]+$/.test(part)) { console.log(`Validation failed: part "${part}" contains invalid characters`); return false; } } // Check TLD (last part) is alphabetic const tld = parts[parts.length - 1]; if (!/^[a-zA-Z]{2,}$/.test(tld)) { console.log(`Validation failed: invalid TLD "${tld}"`); return false; } console.log('Domain validation passed'); return true; } /** * Format status text for display * @param {string} status - Raw status * @returns {string} Formatted status */ formatStatus(status) { const statusMap = { 'idle': 'Idle', 'running': 'Running', 'completed': 'Completed', 'failed': 'Failed', 'stopped': 'Stopped' }; return statusMap[status] || status; } /** * Format label for display * @param {string} label - Raw label * @returns {string} Formatted label */ formatLabel(label) { return label.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); } /** * Format value for display * @param {*} value - Raw value * @returns {string} Formatted value */ formatValue(value) { if (typeof value === 'object' && value !== null) { // Use
 for nicely formatted JSON
            return `
${JSON.stringify(value, null, 2)}
`; } else { // Escape HTML to prevent XSS issues with string values const strValue = String(value); return strValue.replace(//g, ">"); } } /** * Show success message * @param {string} message - Success message */ showSuccess(message) { this.showMessage(message, 'success'); } /** * Show info message * @param {string} message - Info message */ showInfo(message) { this.showMessage(message, 'info'); } showWarning(message) { this.showMessage(message, 'warning'); } /** * Show error message * @param {string} message - Error message */ showError(message) { this.showMessage(message, 'error'); } /** * Show connection error */ showConnectionError() { this.updateConnectionStatus('error'); } /** * Show message with visual feedback * @param {string} message - Message text * @param {string} type - Message type (success, error, warning, info) */ showMessage(message, type = 'info') { console.log(`${type.toUpperCase()}: ${message}`); // Create message element const messageElement = document.createElement('div'); messageElement.className = `message-toast message-${type}`; messageElement.innerHTML = `
${message}
`; // Add to container const container = document.getElementById('message-container'); if (container) { container.appendChild(messageElement); // Auto-remove after delay setTimeout(() => { if (messageElement.parentNode) { messageElement.style.animation = 'slideOutRight 0.3s ease-out'; setTimeout(() => { if (messageElement.parentNode) { messageElement.remove(); } }, 300); } }, type === 'error' ? 8000 : 5000); // Errors stay longer } // Update connection status to show activity if (type === 'success' && this.consecutiveErrors === 0) { this.updateConnectionStatus(this.isScanning ? 'active' : 'idle'); } } /** * Get message background color based on type * @param {string} type - Message type * @returns {string} CSS color */ getMessageColor(type) { const colors = { 'success': '#2c5c34', 'error': '#5c2c2c', 'warning': '#5c4c2c', 'info': '#2c3e5c' }; return colors[type] || colors.info; } /** * Get message border color based on type * @param {string} type - Message type * @returns {string} CSS color */ getMessageBorderColor(type) { const colors = { 'success': '#00ff41', 'error': '#ff6b6b', 'warning': '#ff9900', 'info': '#00aaff' }; return colors[type] || colors.info; } } // Add CSS animations for message toasts const style = document.createElement('style'); style.textContent = ` @keyframes slideInRight { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOutRight { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } } .message-container { pointer-events: auto; } .message-toast { pointer-events: auto; } `; document.head.appendChild(style); // Initialize application when page loads console.log('Creating DNSReconApp instance...'); const app = new DNSReconApp();