Initial commit
Dieser Commit ist enthalten in:
440
licensing/session_manager.py
Normale Datei
440
licensing/session_manager.py
Normale Datei
@ -0,0 +1,440 @@
|
||||
"""
|
||||
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 ===")
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren