""" Session Manager für die Lizenz-Session-Verwaltung mit Heartbeat. """ import threading import time import logging import json import os import requests from datetime import datetime from typing import Optional, Dict, Any from .api_client import LicenseAPIClient from .hardware_fingerprint import HardwareFingerprint logger = logging.getLogger("session_manager") logger.setLevel(logging.DEBUG) # Füge Console Handler hinzu falls noch nicht vorhanden if not logger.handlers: handler = logging.StreamHandler() handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) logger.addHandler(handler) class SessionManager: """Verwaltet die Lizenz-Session und Heartbeat.""" SESSION_FILE = os.path.join("config", ".session_data") HEARTBEAT_INTERVAL = 60 # Sekunden def __init__(self, api_client: Optional[LicenseAPIClient] = None): """ Initialisiert den Session Manager. Args: api_client: Optional vorkonfigurierter API Client """ self.api_client = api_client or LicenseAPIClient() self.hardware_fingerprint = HardwareFingerprint() self.session_token: Optional[str] = None self.license_key: Optional[str] = None self.activation_id: Optional[int] = None self.heartbeat_thread: Optional[threading.Thread] = None self.stop_heartbeat = threading.Event() self.is_active = False # Lade Session-IP-Konfiguration self._load_ip_config() # Session-Daten laden falls vorhanden self._load_session_data() def _save_session_data(self) -> None: """Speichert die aktuelle Session-Daten.""" try: os.makedirs("config", exist_ok=True) session_data = { "session_token": self.session_token, "license_key": self.license_key, "activation_id": self.activation_id, "timestamp": datetime.now().isoformat() } with open(self.SESSION_FILE, 'w') as f: json.dump(session_data, f) logger.debug("Session-Daten gespeichert") except Exception as e: logger.error(f"Fehler beim Speichern der Session-Daten: {e}") def _load_session_data(self) -> None: """Lädt gespeicherte Session-Daten.""" if os.path.exists(self.SESSION_FILE): try: with open(self.SESSION_FILE, 'r') as f: data = json.load(f) self.session_token = data.get("session_token") self.license_key = data.get("license_key") self.activation_id = data.get("activation_id") logger.info("Session-Daten geladen") except Exception as e: logger.warning(f"Fehler beim Laden der Session-Daten: {e}") def _clear_session_data(self) -> None: """Löscht die gespeicherten Session-Daten.""" try: if os.path.exists(self.SESSION_FILE): os.remove(self.SESSION_FILE) logger.debug("Session-Daten gelöscht") except Exception as e: logger.error(f"Fehler beim Löschen der Session-Daten: {e}") def start_session(self, license_key: str, activation_id: Optional[int] = None) -> Dict[str, Any]: """ Startet eine neue Session für die Lizenz. Args: license_key: Der Lizenzschlüssel activation_id: Optional die Aktivierungs-ID Returns: Dictionary mit Session-Informationen oder Fehler """ if self.is_active: logger.warning("Session läuft bereits") return { "success": False, "error": "Session already active" } # Hardware-Info sammeln hw_hash = self.hardware_fingerprint.get_or_create_fingerprint() machine_name = self.hardware_fingerprint.get_machine_name() # IP-Adresse ermitteln client_ip = self._get_session_ip() logger.info(f"Starte Session für Lizenz: {license_key[:4]}...") logger.debug(f"Session-Parameter: machine_name={machine_name}, hw_hash={hw_hash[:8]}..., ip={client_ip}") # Session-Start API Call mit IP-Adresse result = self.api_client.start_session( license_key=license_key, machine_id=machine_name, hardware_hash=hw_hash, version="1.0.0", # TODO: Version aus config lesen ip_address=client_ip # NEU: IP-Adresse hinzugefügt ) logger.debug(f"Session-Start Response: {result}") if result.get("success"): data = result.get("data", {}) # Prüfe ob die Session wirklich erfolgreich war if data.get("success") is False: # Session wurde abgelehnt error_msg = data.get("message", "Session start failed") logger.error(f"Session abgelehnt: {error_msg}") return { "success": False, "error": error_msg, "code": "SESSION_REJECTED" } self.session_token = data.get("session_token") self.license_key = license_key self.activation_id = activation_id or data.get("activation_id") self.is_active = True if self.session_token else False # Session-Daten speichern self._save_session_data() # Heartbeat starten self._start_heartbeat() logger.info(f"Session erfolgreich gestartet: {self.session_token}") # Update-Info prüfen if data.get("update_available"): logger.info(f"Update verfügbar: {data.get('latest_version')}") return { "success": True, "session_token": self.session_token, "update_info": { "available": data.get("update_available", False), "version": data.get("latest_version"), "download_url": data.get("download_url") } } else: error = result.get("error", "Unknown error") logger.error(f"Session-Start fehlgeschlagen: {error}") # Bei Konflikt (409) bedeutet es, dass bereits eine Session läuft if result.get("status") == 409: return { "success": False, "error": "Another session is already active for this license", "code": "SESSION_CONFLICT" } return { "success": False, "error": error, "code": result.get("code", "SESSION_START_FAILED") } def _start_heartbeat(self) -> None: """Startet den Heartbeat-Thread.""" if self.heartbeat_thread and self.heartbeat_thread.is_alive(): logger.warning("Heartbeat läuft bereits") return self.stop_heartbeat.clear() self.heartbeat_thread = threading.Thread( target=self._heartbeat_worker, daemon=True, name="LicenseHeartbeat" ) self.heartbeat_thread.start() logger.info("Heartbeat-Thread gestartet") def _heartbeat_worker(self) -> None: """Worker-Funktion für den Heartbeat-Thread.""" logger.info(f"Heartbeat-Worker gestartet (Interval: {self.HEARTBEAT_INTERVAL}s)") while not self.stop_heartbeat.is_set(): try: # Warte das Interval oder bis Stop-Signal if self.stop_heartbeat.wait(self.HEARTBEAT_INTERVAL): break # Sende Heartbeat if self.session_token and self.license_key: logger.debug("Sende Heartbeat...") result = self.api_client.session_heartbeat( session_token=self.session_token, license_key=self.license_key ) if result.get("success"): logger.debug("Heartbeat erfolgreich") else: logger.error(f"Heartbeat fehlgeschlagen: {result.get('error')}") # Bei bestimmten Fehlern Session beenden if result.get("status") in [401, 404]: logger.error("Session ungültig, beende...") self.end_session() break else: logger.warning("Keine Session-Daten für Heartbeat") except Exception as e: logger.error(f"Fehler im Heartbeat-Worker: {e}") logger.info("Heartbeat-Worker beendet") def end_session(self) -> Dict[str, Any]: """ Beendet die aktuelle Session. Returns: Dictionary mit Informationen über die beendete Session """ if not self.is_active: logger.warning("Keine aktive Session zum Beenden") return { "success": False, "error": "No active session" } logger.info("Beende Session...") # Heartbeat stoppen self.stop_heartbeat.set() if self.heartbeat_thread: self.heartbeat_thread.join(timeout=5) # Session beenden API Call result = {"success": True} if self.session_token: result = self.api_client.end_session(self.session_token) if result.get("success"): logger.info("Session erfolgreich beendet") else: logger.error(f"Fehler beim Beenden der Session: {result.get('error')}") # Session-Daten löschen self.session_token = None self.license_key = None self.activation_id = None self.is_active = False self._clear_session_data() return result def resume_session(self) -> bool: """ Versucht eine gespeicherte Session fortzusetzen. Returns: True wenn erfolgreich, False sonst """ if self.is_active: logger.info("Session läuft bereits") return True if not self.session_token or not self.license_key: logger.info("Keine gespeicherten Session-Daten vorhanden") return False logger.info("Versuche Session fortzusetzen...") # Teste mit Heartbeat ob Session noch gültig ist result = self.api_client.session_heartbeat( session_token=self.session_token, license_key=self.license_key ) if result.get("success"): logger.info("Session erfolgreich fortgesetzt") self.is_active = True self._start_heartbeat() return True else: logger.warning("Gespeicherte Session ungültig") self._clear_session_data() return False def is_session_active(self) -> bool: """ Prüft ob eine Session aktiv ist. Returns: True wenn aktiv, False sonst """ return self.is_active def get_session_info(self) -> Dict[str, Any]: """ Gibt Informationen über die aktuelle Session zurück. Returns: Dictionary mit Session-Informationen """ return { "active": self.is_active, "session_token": self.session_token[:8] + "..." if self.session_token else None, "license_key": self.license_key[:4] + "..." if self.license_key else None, "activation_id": self.activation_id, "heartbeat_interval": self.HEARTBEAT_INTERVAL } def set_heartbeat_interval(self, seconds: int) -> None: """ Setzt das Heartbeat-Interval. Args: seconds: Interval in Sekunden (min 30, max 300) """ if 30 <= seconds <= 300: self.HEARTBEAT_INTERVAL = seconds logger.info(f"Heartbeat-Interval auf {seconds}s gesetzt") # Restart Heartbeat wenn aktiv if self.is_active: self.stop_heartbeat.set() if self.heartbeat_thread: self.heartbeat_thread.join(timeout=5) self._start_heartbeat() else: logger.warning(f"Ungültiges Heartbeat-Interval: {seconds}") def _load_ip_config(self) -> None: """Lädt die IP-Konfiguration aus license_config.json.""" config_path = os.path.join("config", "license_config.json") self.session_ip_mode = "auto" # Default self.ip_fallback = "0.0.0.0" try: if os.path.exists(config_path): with open(config_path, 'r') as f: config = json.load(f) self.session_ip_mode = config.get("session_ip_mode", "auto") self.ip_fallback = config.get("ip_fallback", "0.0.0.0") logger.debug(f"IP-Konfiguration geladen: mode={self.session_ip_mode}, fallback={self.ip_fallback}") except Exception as e: logger.warning(f"Fehler beim Laden der IP-Konfiguration: {e}") def _get_session_ip(self) -> str: """ Ermittelt die IP-Adresse für die Session basierend auf der Konfiguration. TESTBETRIEB: Temporäre Lösung - wird durch Server-Ressourcenmanagement ersetzt Returns: Die IP-Adresse als String """ if self.session_ip_mode == "auto": # TESTBETRIEB: Auto-Erkennung der öffentlichen IP logger.info("TESTBETRIEB: Ermittle öffentliche IP-Adresse automatisch") try: response = requests.get("https://api.ipify.org?format=json", timeout=5) if response.status_code == 200: ip = response.json().get("ip") logger.info(f"Öffentliche IP ermittelt: {ip}") return ip else: logger.warning(f"IP-Ermittlung fehlgeschlagen: Status {response.status_code}") except Exception as e: logger.error(f"Fehler bei IP-Ermittlung: {e}") # Fallback verwenden logger.warning(f"Verwende Fallback-IP: {self.ip_fallback}") return self.ip_fallback elif self.session_ip_mode == "server_assigned": # TODO: Implementierung für Server-zugewiesene IPs logger.info("Server-assigned IP mode noch nicht implementiert, verwende Fallback") return self.ip_fallback elif self.session_ip_mode == "proxy": # TODO: Proxy-IP verwenden wenn Proxy aktiv logger.info("Proxy IP mode noch nicht implementiert, verwende Fallback") return self.ip_fallback else: logger.warning(f"Unbekannter IP-Modus: {self.session_ip_mode}, verwende Fallback") return self.ip_fallback # Test-Funktion if __name__ == "__main__": logging.basicConfig( level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) print("=== Session Manager Test ===\n") # Session Manager erstellen session_mgr = SessionManager() # Session-Info anzeigen print("Aktuelle Session-Info:") info = session_mgr.get_session_info() for key, value in info.items(): print(f" {key}: {value}") # Versuche gespeicherte Session fortzusetzen print("\nVersuche Session fortzusetzen...") if session_mgr.resume_session(): print(" ✓ Session fortgesetzt") else: print(" ✗ Keine gültige Session gefunden") print("\n=== Test abgeschlossen ===")