""" Flask application entry point for DNSRecon web interface. Provides REST API endpoints and serves the web interface with user session support. """ import json import traceback from flask import Flask, render_template, request, jsonify, send_file, session from datetime import datetime, timezone, timedelta import io from core.session_manager import session_manager from config import config app = Flask(__name__) app.config['SECRET_KEY'] = 'dnsrecon-dev-key-change-in-production' app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=2) # 2 hour session lifetime def get_user_scanner(): """ Retrieves the scanner for the current session, or creates a new session and scanner if one doesn't exist. """ # Get current Flask session info for debugging current_flask_session_id = session.get('dnsrecon_session_id') # Try to get existing session if current_flask_session_id: existing_scanner = session_manager.get_session(current_flask_session_id) if existing_scanner: return current_flask_session_id, existing_scanner # Create new session if none exists print("Creating new session as none was found...") new_session_id = session_manager.create_session() new_scanner = session_manager.get_session(new_session_id) if not new_scanner: raise Exception("Failed to create new scanner session") # Store in Flask session session['dnsrecon_session_id'] = new_session_id session.permanent = True return new_session_id, new_scanner @app.route('/') def index(): """Serve the main web interface.""" return render_template('index.html') @app.route('/api/scan/start', methods=['POST']) def start_scan(): """ Start a new reconnaissance scan. Creates a new isolated scanner if clear_graph is true, otherwise adds to the existing one. """ print("=== API: /api/scan/start called ===") try: data = request.get_json() if not data or 'target_domain' not in data: return jsonify({'success': False, 'error': 'Missing target_domain in request'}), 400 target_domain = data['target_domain'].strip() max_depth = data.get('max_depth', config.default_recursion_depth) clear_graph = data.get('clear_graph', True) print(f"Parsed - target_domain: '{target_domain}', max_depth: {max_depth}, clear_graph: {clear_graph}") # Validation if not target_domain: return jsonify({'success': False, 'error': 'Target domain cannot be empty'}), 400 if not isinstance(max_depth, int) or not 1 <= max_depth <= 5: return jsonify({'success': False, 'error': 'Max depth must be an integer between 1 and 5'}), 400 user_session_id, scanner = None, None if clear_graph: print("Clear graph requested: Creating a new, isolated scanner session.") old_session_id = session.get('dnsrecon_session_id') if old_session_id: session_manager.terminate_session(old_session_id) user_session_id = session_manager.create_session() session['dnsrecon_session_id'] = user_session_id session.permanent = True scanner = session_manager.get_session(user_session_id) else: print("Adding to existing graph: Reusing the current scanner session.") user_session_id, scanner = get_user_scanner() if not scanner: return jsonify({'success': False, 'error': 'Failed to get or create a scanner instance.'}), 500 print(f"Using scanner {id(scanner)} in session {user_session_id}") success = scanner.start_scan(target_domain, max_depth, clear_graph=clear_graph) if success: return jsonify({ 'success': True, 'message': 'Scan started successfully', 'scan_id': scanner.logger.session_id, 'user_session_id': user_session_id, }) else: return jsonify({ 'success': False, 'error': f'Failed to start scan (scanner status: {scanner.status})', }), 409 except Exception as e: print(f"ERROR: Exception in start_scan endpoint: {e}") traceback.print_exc() return jsonify({'success': False, 'error': f'Internal server error: {str(e)}'}), 500 @app.route('/api/scan/stop', methods=['POST']) def stop_scan(): """Stop the current scan with immediate GUI feedback.""" print("=== API: /api/scan/stop called ===") try: # Get user-specific scanner user_session_id, scanner = get_user_scanner() print(f"Stopping scan for session: {user_session_id}") if not scanner: return jsonify({ 'success': False, 'error': 'No scanner found for session' }), 404 # Ensure session ID is set if not scanner.session_id: scanner.session_id = user_session_id # Use the stop mechanism success = scanner.stop_scan() # Also set the Redis stop signal directly for extra reliability session_manager.set_stop_signal(user_session_id) # Force immediate status update session_manager.update_scanner_status(user_session_id, 'stopped') # Update the full scanner state session_manager.update_session_scanner(user_session_id, scanner) print(f"Stop scan completed. Success: {success}, Scanner status: {scanner.status}") return jsonify({ 'success': True, 'message': 'Scan stop requested - termination initiated', 'user_session_id': user_session_id, 'scanner_status': scanner.status, 'stop_method': 'cross_process' }) except Exception as e: print(f"ERROR: Exception in stop_scan endpoint: {e}") traceback.print_exc() return jsonify({ 'success': False, 'error': f'Internal server error: {str(e)}' }), 500 @app.route('/api/scan/status', methods=['GET']) def get_scan_status(): """Get current scan status with error handling.""" try: # Get user-specific scanner user_session_id, scanner = get_user_scanner() if not scanner: # Return default idle status if no scanner return jsonify({ 'success': True, 'status': { 'status': 'idle', 'target_domain': None, 'current_depth': 0, 'max_depth': 0, 'current_indicator': '', 'total_indicators_found': 0, 'indicators_processed': 0, 'progress_percentage': 0.0, 'enabled_providers': [], 'graph_statistics': {}, 'user_session_id': user_session_id } }) # Ensure session ID is set if not scanner.session_id: scanner.session_id = user_session_id status = scanner.get_scan_status() status['user_session_id'] = user_session_id # Additional debug info status['debug_info'] = { 'scanner_object_id': id(scanner), 'session_id_set': bool(scanner.session_id), 'has_scan_thread': bool(scanner.scan_thread and scanner.scan_thread.is_alive()) } return jsonify({ 'success': True, 'status': status }) except Exception as e: print(f"ERROR: Exception in get_scan_status endpoint: {e}") traceback.print_exc() return jsonify({ 'success': False, 'error': f'Internal server error: {str(e)}', 'fallback_status': { 'status': 'error', 'target_domain': None, 'current_depth': 0, 'max_depth': 0, 'progress_percentage': 0.0 } }), 500 @app.route('/api/graph', methods=['GET']) def get_graph_data(): """Get current graph data with error handling.""" try: # Get user-specific scanner user_session_id, scanner = get_user_scanner() if not scanner: # Return empty graph if no scanner return jsonify({ 'success': True, 'graph': { 'nodes': [], 'edges': [], 'statistics': { 'node_count': 0, 'edge_count': 0, 'creation_time': datetime.now(timezone.utc).isoformat(), 'last_modified': datetime.now(timezone.utc).isoformat() } }, 'user_session_id': user_session_id }) graph_data = scanner.get_graph_data() return jsonify({ 'success': True, 'graph': graph_data, 'user_session_id': user_session_id }) except Exception as e: print(f"ERROR: Exception in get_graph_data endpoint: {e}") traceback.print_exc() return jsonify({ 'success': False, 'error': f'Internal server error: {str(e)}', 'fallback_graph': { 'nodes': [], 'edges': [], 'statistics': {'node_count': 0, 'edge_count': 0} } }), 500 @app.route('/api/export', methods=['GET']) def export_results(): """Export complete scan results as downloadable JSON for the user session.""" try: # Get user-specific scanner user_session_id, scanner = get_user_scanner() # Get complete results results = scanner.export_results() # Add session information to export results['export_metadata'] = { 'user_session_id': user_session_id, 'export_timestamp': datetime.now(timezone.utc).isoformat(), 'export_type': 'user_session_results' } # Create filename with timestamp timestamp = datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S') target = scanner.current_target or 'unknown' filename = f"dnsrecon_{target}_{timestamp}_{user_session_id[:8]}.json" # Create in-memory file json_data = json.dumps(results, indent=2, ensure_ascii=False) file_obj = io.BytesIO(json_data.encode('utf-8')) return send_file( file_obj, as_attachment=True, download_name=filename, mimetype='application/json' ) except Exception as e: print(f"ERROR: Exception in export_results endpoint: {e}") traceback.print_exc() return jsonify({ 'success': False, 'error': f'Export failed: {str(e)}' }), 500 @app.route('/api/providers', methods=['GET']) def get_providers(): """Get information about available providers for the user session.""" print("=== API: /api/providers called ===") try: # Get user-specific scanner user_session_id, scanner = get_user_scanner() provider_info = scanner.get_provider_info() return jsonify({ 'success': True, 'providers': provider_info, 'user_session_id': user_session_id }) except Exception as e: print(f"ERROR: Exception in get_providers endpoint: {e}") traceback.print_exc() return jsonify({ 'success': False, 'error': f'Internal server error: {str(e)}' }), 500 @app.route('/api/config/api-keys', methods=['POST']) def set_api_keys(): """ Set API keys for providers for the user session only. """ try: data = request.get_json() if data is None: return jsonify({ 'success': False, 'error': 'No API keys provided' }), 400 # Get user-specific scanner and config user_session_id, scanner = get_user_scanner() session_config = scanner.config updated_providers = [] # Iterate over the API keys provided in the request data for provider_name, api_key in data.items(): # This allows us to both set and clear keys. The config # handles enabling/disabling based on if the key is empty. api_key_value = str(api_key or '').strip() success = session_config.set_api_key(provider_name.lower(), api_key_value) if success: updated_providers.append(provider_name) if updated_providers: # Reinitialize scanner providers to apply the new keys scanner._initialize_providers() # Persist the updated scanner object back to the user's session session_manager.update_session_scanner(user_session_id, scanner) return jsonify({ 'success': True, 'message': f'API keys updated for session {user_session_id}: {", ".join(updated_providers)}', 'updated_providers': updated_providers, 'user_session_id': user_session_id }) else: return jsonify({ 'success': False, 'error': 'No valid API keys were provided or provider names were incorrect.' }), 400 except Exception as e: print(f"ERROR: Exception in set_api_keys endpoint: {e}") traceback.print_exc() return jsonify({ 'success': False, 'error': f'Internal server error: {str(e)}' }), 500 @app.route('/api/session/info', methods=['GET']) def get_session_info(): """Get information about the current user session.""" try: user_session_id, scanner = get_user_scanner() session_info = session_manager.get_session_info(user_session_id) return jsonify({ 'success': True, 'session_info': session_info }) except Exception as e: print(f"ERROR: Exception in get_session_info endpoint: {e}") traceback.print_exc() return jsonify({ 'success': False, 'error': f'Internal server error: {str(e)}' }), 500 @app.route('/api/session/terminate', methods=['POST']) def terminate_session(): """Terminate the current user session.""" try: user_session_id = session.get('dnsrecon_session_id') if user_session_id: success = session_manager.terminate_session(user_session_id) # Clear Flask session session.pop('dnsrecon_session_id', None) return jsonify({ 'success': success, 'message': 'Session terminated' if success else 'Session not found' }) else: return jsonify({ 'success': False, 'error': 'No active session to terminate' }), 400 except Exception as e: print(f"ERROR: Exception in terminate_session endpoint: {e}") traceback.print_exc() return jsonify({ 'success': False, 'error': f'Internal server error: {str(e)}' }), 500 @app.route('/api/admin/sessions', methods=['GET']) def list_sessions(): """Admin endpoint to list all active sessions.""" try: sessions = session_manager.list_active_sessions() stats = session_manager.get_statistics() return jsonify({ 'success': True, 'sessions': sessions, 'statistics': stats }) except Exception as e: print(f"ERROR: Exception in list_sessions endpoint: {e}") traceback.print_exc() return jsonify({ 'success': False, 'error': f'Internal server error: {str(e)}' }), 500 @app.route('/api/health', methods=['GET']) def health_check(): """Health check endpoint.""" try: # Get session stats session_stats = session_manager.get_statistics() return jsonify({ 'success': True, 'status': 'healthy', 'timestamp': datetime.now(timezone.utc).isoformat(), 'version': '1.0.0-phase2', 'phase': 2, 'features': { 'multi_provider': True, 'concurrent_processing': True, 'real_time_updates': True, 'api_key_management': True, 'visualization': True, 'retry_logic': True, 'user_sessions': True, 'session_isolation': True }, 'session_statistics': session_stats }) except Exception as e: print(f"ERROR: Exception in health_check endpoint: {e}") return jsonify({ 'success': False, 'error': f'Health check failed: {str(e)}' }), 500 @app.errorhandler(404) def not_found(error): """Handle 404 errors.""" return jsonify({ 'success': False, 'error': 'Endpoint not found' }), 404 @app.errorhandler(500) def internal_error(error): """Handle 500 errors.""" print(f"ERROR: 500 Internal Server Error: {error}") traceback.print_exc() return jsonify({ 'success': False, 'error': 'Internal server error' }), 500 if __name__ == '__main__': print("Starting DNSRecon Flask application with user session support...") # Load configuration from environment config.load_from_env() # Start Flask application print(f"Starting server on {config.flask_host}:{config.flask_port}") app.run( host=config.flask_host, port=config.flask_port, debug=config.flask_debug, threaded=True )