""" Session manager for DNSRecon multi-user support. Manages individual scanner instances per user session with automatic cleanup. """ import threading import time import uuid from typing import Dict, Optional, Any from datetime import datetime, timezone from core.scanner import Scanner class SessionManager: """ Manages multiple scanner instances for concurrent user sessions. Provides session isolation and automatic cleanup of inactive sessions. """ def __init__(self, session_timeout_minutes: int = 60): """ Initialize session manager. Args: session_timeout_minutes: Minutes of inactivity before session cleanup """ self.sessions: Dict[str, Dict[str, Any]] = {} self.session_timeout = session_timeout_minutes * 60 # Convert to seconds self.lock = threading.Lock() # Start cleanup thread self.cleanup_thread = threading.Thread(target=self._cleanup_loop, daemon=True) self.cleanup_thread.start() print(f"SessionManager initialized with {session_timeout_minutes}min timeout") def create_session(self) -> str: """ Create a new user session with dedicated scanner instance and configuration. Enhanced with better debugging and race condition protection. Returns: Unique session ID """ session_id = str(uuid.uuid4()) print(f"=== CREATING SESSION {session_id} ===") try: # Create session-specific configuration from core.session_config import create_session_config session_config = create_session_config() print(f"Created session config for {session_id}") # Create scanner with session config from core.scanner import Scanner scanner_instance = Scanner(session_config=session_config) print(f"Created scanner instance {id(scanner_instance)} for session {session_id}") print(f"Initial scanner status: {scanner_instance.status}") with self.lock: self.sessions[session_id] = { 'scanner': scanner_instance, 'config': session_config, 'created_at': time.time(), 'last_activity': time.time(), 'user_agent': '', 'status': 'active' } print(f"Session {session_id} stored in session manager") print(f"Total active sessions: {len([s for s in self.sessions.values() if s['status'] == 'active'])}") print(f"=== SESSION {session_id} CREATED SUCCESSFULLY ===") return session_id except Exception as e: print(f"ERROR: Failed to create session {session_id}: {e}") raise def get_session(self, session_id: str) -> Optional[object]: """ Get scanner instance for a session with enhanced debugging. Args: session_id: Session identifier Returns: Scanner instance or None if session doesn't exist """ if not session_id: print("get_session called with empty session_id") return None with self.lock: if session_id not in self.sessions: print(f"Session {session_id} not found in session manager") print(f"Available sessions: {list(self.sessions.keys())}") return None session_data = self.sessions[session_id] # Check if session is still active if session_data['status'] != 'active': print(f"Session {session_id} is not active (status: {session_data['status']})") return None # Update last activity session_data['last_activity'] = time.time() scanner = session_data['scanner'] print(f"Retrieved scanner {id(scanner)} for session {session_id}") print(f"Scanner status: {scanner.status}") return scanner def get_or_create_session(self, session_id: Optional[str] = None) -> tuple[str, Scanner]: """ Get existing session or create new one. Args: session_id: Optional existing session ID Returns: Tuple of (session_id, scanner_instance) """ if session_id and self.get_session(session_id): return session_id, self.get_session(session_id) else: new_session_id = self.create_session() return new_session_id, self.get_session(new_session_id) def terminate_session(self, session_id: str) -> bool: """ Terminate a specific session and cleanup resources. Args: session_id: Session to terminate Returns: True if session was terminated successfully """ with self.lock: if session_id not in self.sessions: return False session_data = self.sessions[session_id] scanner = session_data['scanner'] # Stop any running scan try: if scanner.status == 'running': scanner.stop_scan() print(f"Stopped scan for session: {session_id}") except Exception as e: print(f"Error stopping scan for session {session_id}: {e}") # Mark as terminated session_data['status'] = 'terminated' session_data['terminated_at'] = time.time() # Remove from active sessions after a brief delay to allow cleanup threading.Timer(5.0, lambda: self._remove_session(session_id)).start() print(f"Terminated session: {session_id}") return True def _remove_session(self, session_id: str) -> None: """Remove session from memory.""" with self.lock: if session_id in self.sessions: del self.sessions[session_id] print(f"Removed session from memory: {session_id}") def get_session_info(self, session_id: str) -> Optional[Dict[str, Any]]: """ Get session information without updating activity. Args: session_id: Session identifier Returns: Session information dictionary or None """ with self.lock: if session_id not in self.sessions: return None session_data = self.sessions[session_id] scanner = session_data['scanner'] return { 'session_id': session_id, 'created_at': datetime.fromtimestamp(session_data['created_at'], timezone.utc).isoformat(), 'last_activity': datetime.fromtimestamp(session_data['last_activity'], timezone.utc).isoformat(), 'status': session_data['status'], 'scan_status': scanner.status, 'current_target': scanner.current_target, 'uptime_seconds': time.time() - session_data['created_at'] } def list_active_sessions(self) -> Dict[str, Dict[str, Any]]: """ List all active sessions with enhanced debugging info. Returns: Dictionary of session information """ active_sessions = {} with self.lock: for session_id, session_data in self.sessions.items(): if session_data['status'] == 'active': scanner = session_data['scanner'] active_sessions[session_id] = { 'session_id': session_id, 'created_at': datetime.fromtimestamp(session_data['created_at'], timezone.utc).isoformat(), 'last_activity': datetime.fromtimestamp(session_data['last_activity'], timezone.utc).isoformat(), 'status': session_data['status'], 'scan_status': scanner.status, 'current_target': scanner.current_target, 'uptime_seconds': time.time() - session_data['created_at'], 'scanner_object_id': id(scanner) } return active_sessions def _cleanup_loop(self) -> None: """Background thread to cleanup inactive sessions.""" while True: try: current_time = time.time() sessions_to_cleanup = [] with self.lock: for session_id, session_data in self.sessions.items(): if session_data['status'] != 'active': continue inactive_time = current_time - session_data['last_activity'] if inactive_time > self.session_timeout: sessions_to_cleanup.append(session_id) # Cleanup outside of lock to avoid deadlock for session_id in sessions_to_cleanup: print(f"Cleaning up inactive session: {session_id}") self.terminate_session(session_id) # Sleep for 5 minutes between cleanup cycles time.sleep(300) except Exception as e: print(f"Error in session cleanup loop: {e}") time.sleep(60) # Sleep for 1 minute on error def get_statistics(self) -> Dict[str, Any]: """ Get session manager statistics. Returns: Statistics dictionary """ with self.lock: active_count = sum(1 for s in self.sessions.values() if s['status'] == 'active') running_scans = sum(1 for s in self.sessions.values() if s['status'] == 'active' and s['scanner'].status == 'running') return { 'total_sessions': len(self.sessions), 'active_sessions': active_count, 'running_scans': running_scans, 'session_timeout_minutes': self.session_timeout / 60 } # Global session manager instance session_manager = SessionManager(session_timeout_minutes=60)