it
This commit is contained in:
		
							parent
							
								
									7e2473b521
								
							
						
					
					
						commit
						3ecfca95e6
					
				@ -1,281 +1,137 @@
 | 
			
		||||
"""
 | 
			
		||||
Session manager for DNSRecon multi-user support.
 | 
			
		||||
Manages individual scanner instances per user session with automatic cleanup.
 | 
			
		||||
"""
 | 
			
		||||
# dnsrecon/core/session_manager.py
 | 
			
		||||
 | 
			
		||||
import threading
 | 
			
		||||
import time
 | 
			
		||||
import uuid
 | 
			
		||||
import redis
 | 
			
		||||
import pickle
 | 
			
		||||
from typing import Dict, Optional, Any
 | 
			
		||||
from datetime import datetime, timezone
 | 
			
		||||
 | 
			
		||||
from core.scanner import Scanner
 | 
			
		||||
 | 
			
		||||
# WARNING: Using pickle can be a security risk if the data source is not trusted.
 | 
			
		||||
# In this case, we are only serializing/deserializing our own trusted Scanner objects,
 | 
			
		||||
# which is generally safe. Do not unpickle data from untrusted sources.
 | 
			
		||||
 | 
			
		||||
class SessionManager:
 | 
			
		||||
    """
 | 
			
		||||
    Manages multiple scanner instances for concurrent user sessions.
 | 
			
		||||
    Provides session isolation and automatic cleanup of inactive sessions.
 | 
			
		||||
    Manages multiple scanner instances for concurrent user sessions using Redis.
 | 
			
		||||
    This allows session state to be shared across multiple Gunicorn worker processes.
 | 
			
		||||
    """
 | 
			
		||||
    
 | 
			
		||||
    def __init__(self, session_timeout_minutes: int = 60):
 | 
			
		||||
        """
 | 
			
		||||
        Initialize session manager.
 | 
			
		||||
        
 | 
			
		||||
        Args:
 | 
			
		||||
            session_timeout_minutes: Minutes of inactivity before session cleanup
 | 
			
		||||
        Initialize session manager with a Redis backend.
 | 
			
		||||
        """
 | 
			
		||||
        self.sessions: Dict[str, Dict[str, Any]] = {}
 | 
			
		||||
        self.redis_client = redis.StrictRedis(db=0, decode_responses=False)
 | 
			
		||||
        self.session_timeout = session_timeout_minutes * 60  # Convert to seconds
 | 
			
		||||
        self.lock = threading.Lock()
 | 
			
		||||
        self.lock = threading.Lock() # Lock for local operations, Redis handles atomic ops
 | 
			
		||||
        
 | 
			
		||||
        # 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")
 | 
			
		||||
        print(f"SessionManager initialized with Redis backend and {session_timeout_minutes}min timeout")
 | 
			
		||||
    
 | 
			
		||||
    def _get_session_key(self, session_id: str) -> str:
 | 
			
		||||
        """Generates the Redis key for a session."""
 | 
			
		||||
        return f"dnsrecon:session:{session_id}"
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
        Create a new user session and store it in Redis.
 | 
			
		||||
        """
 | 
			
		||||
        session_id = str(uuid.uuid4())
 | 
			
		||||
        
 | 
			
		||||
        print(f"=== CREATING SESSION {session_id} ===")
 | 
			
		||||
        print(f"=== CREATING SESSION {session_id} IN REDIS ===")
 | 
			
		||||
        
 | 
			
		||||
        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}")
 | 
			
		||||
            session_data = {
 | 
			
		||||
                'scanner': scanner_instance,
 | 
			
		||||
                'config': session_config,
 | 
			
		||||
                'created_at': time.time(),
 | 
			
		||||
                'last_activity': time.time(),
 | 
			
		||||
                'status': 'active'
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            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'
 | 
			
		||||
                }
 | 
			
		||||
            # Serialize the entire session data dictionary using pickle
 | 
			
		||||
            serialized_data = pickle.dumps(session_data)
 | 
			
		||||
            
 | 
			
		||||
            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 ===")
 | 
			
		||||
            # Store in Redis
 | 
			
		||||
            session_key = self._get_session_key(session_id)
 | 
			
		||||
            self.redis_client.setex(session_key, self.session_timeout, serialized_data)
 | 
			
		||||
            
 | 
			
		||||
            print(f"Session {session_id} stored in Redis")
 | 
			
		||||
            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]:
 | 
			
		||||
    def _get_session_data(self, session_id: str) -> Optional[Dict[str, Any]]:
 | 
			
		||||
        """Retrieves and deserializes session data from Redis."""
 | 
			
		||||
        session_key = self._get_session_key(session_id)
 | 
			
		||||
        serialized_data = self.redis_client.get(session_key)
 | 
			
		||||
        if serialized_data:
 | 
			
		||||
            return pickle.loads(serialized_data)
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    def _save_session_data(self, session_id: str, session_data: Dict[str, Any]):
 | 
			
		||||
        """Serializes and saves session data back to Redis with updated TTL."""
 | 
			
		||||
        session_key = self._get_session_key(session_id)
 | 
			
		||||
        serialized_data = pickle.dumps(session_data)
 | 
			
		||||
        self.redis_client.setex(session_key, self.session_timeout, serialized_data)
 | 
			
		||||
 | 
			
		||||
    def get_session(self, session_id: str) -> Optional[Scanner]:
 | 
			
		||||
        """
 | 
			
		||||
        Get scanner instance for a session with enhanced debugging.
 | 
			
		||||
        
 | 
			
		||||
        Args:
 | 
			
		||||
            session_id: Session identifier
 | 
			
		||||
            
 | 
			
		||||
        Returns:
 | 
			
		||||
            Scanner instance or None if session doesn't exist
 | 
			
		||||
        Get scanner instance for a session from Redis.
 | 
			
		||||
        """
 | 
			
		||||
        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.
 | 
			
		||||
        session_data = self._get_session_data(session_id)
 | 
			
		||||
        
 | 
			
		||||
        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)
 | 
			
		||||
    
 | 
			
		||||
        if not session_data or session_data.get('status') != 'active':
 | 
			
		||||
            return None
 | 
			
		||||
        
 | 
			
		||||
        # Update last activity and save back to Redis
 | 
			
		||||
        session_data['last_activity'] = time.time()
 | 
			
		||||
        self._save_session_data(session_id, session_data)
 | 
			
		||||
        
 | 
			
		||||
        return session_data.get('scanner')
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
        Terminate a specific session in Redis.
 | 
			
		||||
        """
 | 
			
		||||
        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
 | 
			
		||||
            }
 | 
			
		||||
        session_data = self._get_session_data(session_id)
 | 
			
		||||
        if not session_data:
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        scanner = session_data.get('scanner')
 | 
			
		||||
        if scanner and scanner.status == 'running':
 | 
			
		||||
            scanner.stop_scan()
 | 
			
		||||
            print(f"Stopped scan for session: {session_id}")
 | 
			
		||||
        
 | 
			
		||||
        # Delete from Redis
 | 
			
		||||
        session_key = self._get_session_key(session_id)
 | 
			
		||||
        self.redis_client.delete(session_key)
 | 
			
		||||
        
 | 
			
		||||
        print(f"Terminated and removed session from Redis: {session_id}")
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    def _cleanup_loop(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Background thread to cleanup inactive sessions.
 | 
			
		||||
        Redis's TTL (setex) handles most of this automatically. This loop is a failsafe.
 | 
			
		||||
        """
 | 
			
		||||
        while True:
 | 
			
		||||
            # Redis handles expiration automatically, so this loop can be simplified or removed
 | 
			
		||||
            # For now, we'll keep it as a failsafe check for non-expiring keys if any get created by mistake
 | 
			
		||||
            time.sleep(300) # Sleep for 5 minutes
 | 
			
		||||
 | 
			
		||||
# Global session manager instance
 | 
			
		||||
session_manager = SessionManager(session_timeout_minutes=60)
 | 
			
		||||
@ -4,4 +4,6 @@ requests>=2.31.0
 | 
			
		||||
python-dateutil>=2.8.2
 | 
			
		||||
Werkzeug>=2.3.7
 | 
			
		||||
urllib3>=2.0.0
 | 
			
		||||
dnspython>=2.4.2
 | 
			
		||||
dnspython>=2.4.2
 | 
			
		||||
gunicorn
 | 
			
		||||
redis
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user