440 Zeilen
16 KiB
Python
440 Zeilen
16 KiB
Python
"""
|
|
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 ===") |