Watchdog-Heartbeat + LicenseLabrador-Lizenzpruefung eingebunden
Slices W-A/W-B/W-C und L-A aus UMSETZUNGSPLAN-Watchdog-LicenseLabrador-Integration. Watchdog (Dead-Man's-Switch, externer Server): - WatchdogHeartbeatService als BackgroundService + DI-Singleton; jeder Sendeversuch gekapselt, ein Ausfall des Watchdogs beeintraechtigt PolyTrader nie. - Eigene Implementierung statt Test-Client des Fremdprojekts: TLS-Pruefung bleibt aktiv, http:// nur fuer localhost (Agent-Token nicht im Klartext ins Netz). - Status aus dem App-Log abgeleitet (Error mit 5-Minuten-Sticky-Fenster, entprellt), Lifecycle-Events started/stopping. - Konfiguration in ServerSettings; Agent-Token [Browsable(false)] mit maskierter Eingabe + Statusanzeige, bei gesetztem Master-Key verschluesselt (enc:v1:). Lizenz (LicenseLabrador, Ed25519): - LicenseGate.RunStartupGate prueft beim Start; bei ungueltiger Lizenz wird die Modulliste leer gebaut, sodass nur die Core-Shell (Terminal/Einstellungen) startet. Bewusst kein Environment.Exit - ein Trading-Bot darf nicht mitten im Lauf hart sterben. TamperSuspected gilt als nicht nutzbar. - LicenseDialog (partial + .Designer.cs) fuer Start- und Verwalten-Modus, mit Hardware-ID zum Kopieren; Smoke-UI konstruiert beide Modi headless. - Master-Key wird jetzt VOR dem Host-Build geladen, da auch der Lizenzschluessel entschluesselt werden muss; derselbe TerminalLogger wird als Singleton weitergereicht, damit die Startmeldungen im Terminal-Fenster erscheinen. Der Lizenz-SDK-Client wird per Cross-Repo-Projektreferenz auf ..\..\LicenseLabrador eingebunden, damit SDK-Fixes ohne Kopie einfliessen. 484 Zeilen Tests fuer den Heartbeat; Suite gruen (438 Tests). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
using System;
|
||||
using LicenseLabrador.Client;
|
||||
using PolyTrader.Core.Security;
|
||||
using PolyTraderSharp.Models;
|
||||
using PolyTraderSharp.Services;
|
||||
|
||||
namespace PolyTraderSharp.Licensing
|
||||
{
|
||||
/// <summary>
|
||||
/// Lizenz-Torwächter für den Programmstart (B.4). Prüft beim Start die LicenseLabrador-Lizenz und
|
||||
/// entscheidet, ob PolyTrader voll (mit allen Modulen) oder eingeschränkt (nur Core-Shell, damit
|
||||
/// Terminal und Einstellungen zum Eintragen einer Lizenz erreichbar bleiben) startet.
|
||||
///
|
||||
/// **Grundsatz:** kein harter Abbruch (<c>Environment.Exit</c>) bei ungültiger Lizenz. Ein
|
||||
/// Trading-Bot mit offenen Positionen darf nicht mitten im Lauf hart abgeschossen werden – im
|
||||
/// Zweifel startet die Shell ohne die Trading-/Analyse-Module weiter. Der Gate wirft nie; jeder
|
||||
/// Fehler in der Prüfung führt zum eingeschränkten Modus, nicht zum Absturz.
|
||||
///
|
||||
/// Die eigentliche Kryptografie (Ed25519-Signaturprüfung, Nonce, Offline-Kulanz, Hardware-Bindung)
|
||||
/// liegt im <see cref="LicenseClient"/> des SDK; dieser Gate ist nur die Anwendungslogik drumherum.
|
||||
/// </summary>
|
||||
public static class LicenseGate
|
||||
{
|
||||
/// <summary>
|
||||
/// Öffentlicher Ed25519-Schlüssel des Lizenzservers (license.mhdf.de), Base64. Bewusst fest
|
||||
/// eingebettet: nur mit diesem Schlüssel lässt sich eine Server-Antwort verifizieren – ein
|
||||
/// Angreifer kann ohne den (nur serverseitigen) privaten Schlüssel kein „valid" fälschen.
|
||||
/// </summary>
|
||||
private const string PublicKeyBase64 = "L7YR1wMKk8+lNefatzL+DMvAtHFVkZWYXAxXGrro+/U=";
|
||||
|
||||
/// <summary>Produkt-Slug im LicenseLabrador-Admin.</summary>
|
||||
private const string ProductSlug = "pt";
|
||||
|
||||
/// <summary>
|
||||
/// Lizenzserver-Endpunkt. Ausschließlich HTTPS – über http:// gingen Lizenzschlüssel und
|
||||
/// Hardware-ID im Klartext übers Netz (die signierte Antwort bliebe zwar fälschungssicher,
|
||||
/// der Schlüssel würde aber geleakt).
|
||||
/// </summary>
|
||||
private static readonly string[] Endpoints = { "https://license.mhdf.de" };
|
||||
|
||||
/// <summary>Offline-Kulanz, falls der Server das Feld nicht liefert (7 Tage).</summary>
|
||||
private const int OfflineGraceHoursFallback = 168;
|
||||
|
||||
public static LicenseConfig BuildConfig() => new LicenseConfig
|
||||
{
|
||||
ProductSlug = ProductSlug,
|
||||
PublicKeyBase64 = PublicKeyBase64,
|
||||
Endpoints = Endpoints,
|
||||
OfflineGraceHoursFallback = OfflineGraceHoursFallback
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Führt die Startprüfung durch. Rückgabe <c>true</c> = App darf voll (mit Modulen) starten.
|
||||
/// Zeigt bei ungültiger Lizenz einen modalen Dialog (Schlüssel eingeben, Hardware-ID anzeigen,
|
||||
/// eingeschränkt starten). Wirft nie.
|
||||
/// </summary>
|
||||
public static bool RunStartupGate(ServerSettings settings, TerminalLogger logger, string settingsPath)
|
||||
{
|
||||
LicenseClient client;
|
||||
try
|
||||
{
|
||||
client = new LicenseClient(BuildConfig());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"❌ Lizenzprüfung nicht initialisierbar: {ex.Message}. Start im eingeschränkten Modus.");
|
||||
return false;
|
||||
}
|
||||
|
||||
string storedKey = "";
|
||||
try
|
||||
{
|
||||
// Erlaubt einen mit dem Master-Key verschlüsselten Schlüssel (enc:v1:…); Klartext
|
||||
// wird unverändert durchgereicht.
|
||||
storedKey = SecretProtection.Unprotect(settings.LicenseKey);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Warning($"⚠️ Gespeicherter Lizenzschlüssel nicht lesbar ({ex.Message}) – behandle als 'keine Lizenz'.");
|
||||
}
|
||||
|
||||
LicenseResult result = EvaluateBlocking(client, storedKey);
|
||||
|
||||
if (result.IsUsable)
|
||||
{
|
||||
LogUsable(result, logger);
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.Warning($"⚠️ Keine gültige Lizenz: {Describe(result)}. Öffne Lizenzdialog.");
|
||||
|
||||
LicenseResult? dialogResult;
|
||||
string? dialogKey;
|
||||
try
|
||||
{
|
||||
using var dlg = new PolyTraderSharp.Ui.LicenseDialog(client, storedKey, result);
|
||||
dlg.ShowDialog();
|
||||
dialogResult = dlg.ValidatedResult;
|
||||
dialogKey = dlg.ValidatedKey;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"❌ Lizenzdialog fehlgeschlagen: {ex.Message}. Start im eingeschränkten Modus.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (dialogResult != null && dialogResult.IsUsable && !string.IsNullOrWhiteSpace(dialogKey))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Bei gesetztem Master-Key verschlüsselt speichern; sonst Klartext (Passthrough).
|
||||
settings.LicenseKey = SecretProtection.Protect(dialogKey!.Trim());
|
||||
settings.Save(settingsPath);
|
||||
logger.Info("🔑 Lizenz validiert und gespeichert.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"Lizenz gültig, aber Speichern des Schlüssels fehlgeschlagen: {ex.Message}.");
|
||||
}
|
||||
|
||||
LogUsable(dialogResult, logger);
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.Warning(
|
||||
"⚠️ Start im EINGESCHRÄNKTEN Modus – keine Module aktiv, nur Terminal und Einstellungen. " +
|
||||
"Lizenz in den Einstellungen setzen und PolyTrader neu starten.");
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validiert synchron (blockierend). Das SDK arbeitet durchgängig mit ConfigureAwait(false),
|
||||
/// daher ist das Blockieren vor der Message-Loop unbedenklich. Ein leerer Schlüssel ergibt
|
||||
/// planmäßig <see cref="LicenseState.NoLicense"/>.
|
||||
/// </summary>
|
||||
internal static LicenseResult EvaluateBlocking(LicenseClient client, string? key)
|
||||
{
|
||||
return client.ValidateAsync(key ?? string.Empty).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private static void LogUsable(LicenseResult result, TerminalLogger logger)
|
||||
{
|
||||
switch (result.State)
|
||||
{
|
||||
case LicenseState.Valid:
|
||||
logger.Info("🔑 Lizenz gültig (online geprüft).");
|
||||
break;
|
||||
case LicenseState.ValidOffline:
|
||||
logger.Warning($"🔑 Lizenz im Offline-Kulanzmodus gültig bis {result.GraceUntil:g} " +
|
||||
"(Server nicht erreichbar).");
|
||||
break;
|
||||
case LicenseState.ValidLocalFile:
|
||||
logger.Info("🔑 Lizenz über lokale Offline-Lizenzdatei gültig.");
|
||||
break;
|
||||
default:
|
||||
logger.Info($"🔑 Lizenz nutzbar ({result.State}).");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Kurzbeschreibung eines nicht nutzbaren Ergebnisses für das Log/den Dialog.</summary>
|
||||
internal static string Describe(LicenseResult result)
|
||||
{
|
||||
return result.State switch
|
||||
{
|
||||
LicenseState.NoLicense => "kein oder ungültiger Schlüssel / Server nicht erreichbar",
|
||||
LicenseState.NotFound => "Schlüssel dem Server unbekannt",
|
||||
LicenseState.Revoked => "Lizenz wurde widerrufen",
|
||||
LicenseState.Expired => "Lizenz abgelaufen",
|
||||
LicenseState.ActivationLimit => "maximale Aktivierungen erreicht",
|
||||
LicenseState.TamperSuspected => "Manipulation erkannt (Signatur/Uhr) – Prüfung verweigert",
|
||||
_ => result.State.ToString()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user