165 lines
		
	
	
		
			6.4 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			165 lines
		
	
	
		
			6.4 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
# dnsrecon/core/session_manager.py
 | 
						|
 | 
						|
import threading
 | 
						|
import time
 | 
						|
import uuid
 | 
						|
import redis
 | 
						|
import pickle
 | 
						|
from typing import Dict, Optional, Any
 | 
						|
 | 
						|
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 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 with a Redis backend.
 | 
						|
        """
 | 
						|
        self.redis_client = redis.StrictRedis(db=0, decode_responses=False)
 | 
						|
        self.session_timeout = session_timeout_minutes * 60  # Convert to seconds
 | 
						|
        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 Redis backend and {session_timeout_minutes}min timeout")
 | 
						|
    
 | 
						|
    def __getstate__(self):
 | 
						|
        """Prepare SessionManager for pickling."""
 | 
						|
        state = self.__dict__.copy()
 | 
						|
        # Exclude unpickleable attributes - Redis client and threading objects
 | 
						|
        unpicklable_attrs = ['lock', 'cleanup_thread', 'redis_client']
 | 
						|
        for attr in unpicklable_attrs:
 | 
						|
            if attr in state:
 | 
						|
                del state[attr]
 | 
						|
        return state
 | 
						|
 | 
						|
    def __setstate__(self, state):
 | 
						|
        """Restore SessionManager after unpickling."""
 | 
						|
        self.__dict__.update(state)
 | 
						|
        # Re-initialize unpickleable attributes
 | 
						|
        import redis
 | 
						|
        self.redis_client = redis.StrictRedis(db=0, decode_responses=False)
 | 
						|
        self.lock = threading.Lock()
 | 
						|
        self.cleanup_thread = threading.Thread(target=self._cleanup_loop, daemon=True)
 | 
						|
        self.cleanup_thread.start()
 | 
						|
 | 
						|
    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 and store it in Redis.
 | 
						|
        """
 | 
						|
        session_id = str(uuid.uuid4())
 | 
						|
        print(f"=== CREATING SESSION {session_id} IN REDIS ===")
 | 
						|
        
 | 
						|
        try:
 | 
						|
            from core.session_config import create_session_config
 | 
						|
            session_config = create_session_config()
 | 
						|
            scanner_instance = Scanner(session_config=session_config)
 | 
						|
            
 | 
						|
            session_data = {
 | 
						|
                'scanner': scanner_instance,
 | 
						|
                'config': session_config,
 | 
						|
                'created_at': time.time(),
 | 
						|
                'last_activity': time.time(),
 | 
						|
                'status': 'active'
 | 
						|
            }
 | 
						|
            
 | 
						|
            # Serialize the entire session data dictionary using pickle
 | 
						|
            serialized_data = pickle.dumps(session_data)
 | 
						|
            
 | 
						|
            # 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_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 update_session_scanner(self, session_id: str, scanner: 'Scanner'):
 | 
						|
        """Updates just the scanner object in a session."""
 | 
						|
        session_data = self._get_session_data(session_id)
 | 
						|
        if session_data:
 | 
						|
            session_data['scanner'] = scanner
 | 
						|
            # We don't need to update last_activity here, as that's for user interaction
 | 
						|
            self._save_session_data(session_id, session_data)
 | 
						|
 | 
						|
    def get_session(self, session_id: str) -> Optional[Scanner]:
 | 
						|
        """
 | 
						|
        Get scanner instance for a session from Redis.
 | 
						|
        """
 | 
						|
        if not session_id:
 | 
						|
            return None
 | 
						|
            
 | 
						|
        session_data = self._get_session_data(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 in Redis.
 | 
						|
        """
 | 
						|
        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) |