Arbeitsstand gesichert: Deploymentcenter-Integration (D-0 bis D-5) und Einfenster-Shell
Sicherungscommit vor dem Aufraeumen des Repos, damit nachvollziehbar bleibt, welcher Stand vor der Bereinigung galt. Build gruen, 476 Tests gruen. Zwei Straenge, die sich ueber .csproj, Program.cs und appsettings.json ueberschneiden und darum gemeinsam abgelegt werden: Deploymentcenter-Integration (P3c, Plan D-0 bis D-5 code-seitig fertig): - Deploymentcenter.Client 2.5.0 als lokales Paket, Source-Mapping erweitert - DeploymentcenterOptions, DeploymentcenterErrorReporter, LicenseGate/LicenseCli - WatchdogHeartbeatService auf die Deploymentcenter-API umgestellt (version, os, checks, metrics, status stopped) - Security: MasterKeyResolver, SecretRedactor, FilePermissions - Directory.Build.props mit zentraler Version 0.1.0 (Packager-Versionsdisziplin) - deploy/: Packager-Vorlage und systemd-Unit; echte Zugangsdaten bleiben ueber .gitignore aussen vor - setup.json fuer die Erstinstallation - UMSETZUNGSPLAN-Deploymentcenter-Integration.md; ANALYSE-Linux-Portierung.md verweist auf den neuen Plan Einfenster-Shell (UI-Redesign): - ShellWindow + ShellNavModel als Seitenleisten-Shell - WindowMenuBar und WindowMenuModel entfallen - Modul- und Kernfenster auf die Shell-Einbettung angepasst Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Deploymentcenter.Client;
|
||||
using PolyTrader.Core;
|
||||
using PolyTrader.Core.Security;
|
||||
using PolyTraderSharp.Models;
|
||||
|
||||
namespace PolyTrader.App.Avalonia.Licensing
|
||||
{
|
||||
/// <summary>
|
||||
/// Kopflose Lizenzverwaltung von der Kommandozeile aus (D-2), nach der Konvention aus
|
||||
/// LICENSE_INTEGRATION_GUIDE §4: <c>--license-status</c>, <c>--license-set-key</c>,
|
||||
/// <c>--license-deactivate</c>. Diese Schalter baut die Anwendung selbst, das SDK bringt nur die
|
||||
/// Bausteine mit. Läuft VOR dem Host-Aufbau und beendet den Prozess über den Rückgabewert.
|
||||
/// </summary>
|
||||
internal static class LicenseCli
|
||||
{
|
||||
/// <summary>Liefert <c>true</c>, wenn einer der Lizenz-Schalter erkannt und abgearbeitet wurde.</summary>
|
||||
public static bool TryHandle(string[] args, out int exitCode)
|
||||
{
|
||||
exitCode = 0;
|
||||
|
||||
if (args.Any(a => string.Equals(a, "--license-status", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
exitCode = Status();
|
||||
return true;
|
||||
}
|
||||
|
||||
int setIdx = Array.FindIndex(args, a => string.Equals(a, "--license-set-key", StringComparison.OrdinalIgnoreCase));
|
||||
if (setIdx >= 0)
|
||||
{
|
||||
string? key = setIdx + 1 < args.Length ? args[setIdx + 1] : null;
|
||||
exitCode = SetKey(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (args.Any(a => string.Equals(a, "--license-deactivate", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
exitCode = Deactivate();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int Status()
|
||||
{
|
||||
var options = Program.LoadDeploymentcenterOptions();
|
||||
var hw = HardwareId.GetHardwareId(options.ProjectSlug);
|
||||
var cached = LicenseClient.TryGetCachedState(options.ProjectSlug);
|
||||
|
||||
Console.WriteLine($"HWID v2: {hw.HardwareId} ({hw.HwidSource})");
|
||||
Console.WriteLine($"Produkt: {options.ProjectSlug}");
|
||||
Console.WriteLine($"Server: {(string.IsNullOrWhiteSpace(options.BaseUrl) ? "(keine URL konfiguriert)" : options.BaseUrl)}");
|
||||
Console.WriteLine($"Cache: {(cached == null ? "leer" : cached.Status)}");
|
||||
|
||||
if (cached != null && cached.CacheExpiresAt > 0)
|
||||
{
|
||||
var until = DateTimeOffset.FromUnixTimeSeconds(cached.CacheExpiresAt);
|
||||
Console.WriteLine($"Offline noch bis: {until:u}");
|
||||
}
|
||||
|
||||
var settings = ServerSettings.Load("server_settings.xml");
|
||||
Console.WriteLine($"Gespeicherter Schlüssel (server_settings.xml): {(string.IsNullOrWhiteSpace(settings.LicenseKey) ? "keiner" : settings.LicenseKeyStatus)}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int SetKey(string? key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
Console.Error.WriteLine("Fehler: --license-set-key erwartet den Schlüssel als nächstes Argument.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var options = Program.LoadDeploymentcenterOptions();
|
||||
if (string.IsNullOrWhiteSpace(options.BaseUrl))
|
||||
{
|
||||
Console.Error.WriteLine("Fehler: Keine Deploymentcenter-URL konfiguriert (appsettings.json: Deploymentcenter:BaseUrl).");
|
||||
return 1;
|
||||
}
|
||||
|
||||
LicenseClient.DefaultAppVersion = BuildInfo.Version;
|
||||
var client = new LicenseClient();
|
||||
LicenseValidationResult result = client
|
||||
.ValidateAsync(options.ProjectSlug, key.Trim(), options.BaseUrl, BuildInfo.Version)
|
||||
.GetAwaiter().GetResult();
|
||||
|
||||
Console.WriteLine($"{result.Status}: {result.Message}");
|
||||
if (!result.IsValid)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
const string settingsPath = "server_settings.xml";
|
||||
var settings = ServerSettings.Load(settingsPath);
|
||||
settings.LicenseKey = SecretProtection.Protect(key.Trim());
|
||||
settings.Save(settingsPath);
|
||||
Console.WriteLine($"Lizenz gültig und in {settingsPath} gespeichert " +
|
||||
$"({(SecretProtection.IsEncrypted(settings.LicenseKey) ? "verschlüsselt" : "Klartext")}).");
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int Deactivate()
|
||||
{
|
||||
var options = Program.LoadDeploymentcenterOptions();
|
||||
var settings = ServerSettings.Load("server_settings.xml");
|
||||
|
||||
string key;
|
||||
try { key = SecretProtection.Unprotect(settings.LicenseKey); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Fehler: gespeicherter Schlüssel nicht lesbar: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
Console.Error.WriteLine("Fehler: kein Lizenzschlüssel hinterlegt.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Die Deaktivierung gibt einen Aktivierungsplatz frei und ist deshalb geschützt - der
|
||||
// shared_key gehört auf den Administrationsrechner, nicht in die ausgelieferte
|
||||
// Anwendung (siehe LICENSE_INTEGRATION_GUIDE). Ohne ihn: klarer Hinweis statt Versuch.
|
||||
string? sharedKey = Environment.GetEnvironmentVariable("DC_SHARED_KEY");
|
||||
if (string.IsNullOrWhiteSpace(sharedKey))
|
||||
{
|
||||
Console.Error.WriteLine("Fehler: DC_SHARED_KEY nicht gesetzt. Die Deaktivierung braucht den " +
|
||||
"administrativen Shared Key - alternativ im Deploymentcenter-WebUI unter " +
|
||||
"der Hardware-Liste auf „Freigeben\" klicken.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var client = new LicenseClient();
|
||||
bool ok = client.DeactivateAsync(options.ProjectSlug, key, options.BaseUrl, sharedKey)
|
||||
.GetAwaiter().GetResult();
|
||||
|
||||
Console.WriteLine(ok ? "Aktivierung freigegeben." : "Deaktivierung fehlgeschlagen.");
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
using System;
|
||||
using Deploymentcenter.Client;
|
||||
using PolyTrader.Core;
|
||||
using PolyTrader.Core.Configuration;
|
||||
using PolyTrader.Core.Security;
|
||||
using PolyTraderSharp.Models;
|
||||
using PolyTraderSharp.Services;
|
||||
|
||||
namespace PolyTrader.App.Avalonia.Licensing
|
||||
{
|
||||
/// <summary>
|
||||
/// Lizenz-Torwächter für den Programmstart (D-2). Ersetzt den früheren, gegen
|
||||
/// <c>LicenseLabrador.Client</c> gebauten <c>Licensing/LicenseGate.cs</c> im WinForms-Projekt
|
||||
/// (siehe docs/umsetzungsplaene/UMSETZUNGSPLAN-Deploymentcenter-Integration.md) – dort läuft
|
||||
/// ohnehin keine Lizenzprüfung, weil Avalonia die tatsächlich benutzte Anwendung ist.
|
||||
///
|
||||
/// **Grundsatz unverändert:** kein <see cref="Environment.Exit(int)"/> bei ungültiger Lizenz. Ein
|
||||
/// Trading-Bot mit offenen Positionen darf nicht mitten im Lauf abgeschossen werden. Bei
|
||||
/// fehlender/ungültiger Lizenz startet die Core-Shell (Terminal, Einstellungen) ohne die
|
||||
/// Trading-/Analyse-Module; der Schlüssel lässt sich jederzeit über den Button
|
||||
/// „Lizenzschlüssel setzen …" im Einstellungsfenster nachtragen (wirkt ab dem nächsten Start –
|
||||
/// eine Laufzeit-Revalidierung gibt es bewusst nicht, siehe Entscheidung vom 31.07.2026).
|
||||
///
|
||||
/// **Bewusst kein eigenes <see cref="ILicensePrompt"/>:** Der interaktive Weg läuft über die
|
||||
/// bereits vorhandene, erprobte Settings-UI (derselbe maskierte Prompt-Button wie beim
|
||||
/// Watchdog-Token); ein zusätzlicher Dialog nur für die Lizenz wäre eine zweite, redundante
|
||||
/// Eingabe-Oberfläche. Für den Kopflos-Betrieb (<c>--headless</c>, CLI-Schalter) genügt die vom
|
||||
/// SDK mitgelieferte <see cref="ConsoleLicensePrompt"/> – eine eigene Fassung böte keinen Mehrwert.
|
||||
/// </summary>
|
||||
public static class LicenseGate
|
||||
{
|
||||
/// <summary>
|
||||
/// Prüft still (ohne Dialog), ob die gespeicherte Lizenz aktuell nutzbar ist. Entscheidet
|
||||
/// darüber, ob die Trading-/Analyse-Module überhaupt registriert werden – muss deshalb vor
|
||||
/// dem Host-Aufbau laufen. Wirft nie: jeder Fehler in der Prüfung führt zum eingeschränkten
|
||||
/// Modus, nicht zum Absturz.
|
||||
/// </summary>
|
||||
public static bool CheckUsable(DeploymentcenterOptions options, ServerSettings settings, TerminalLogger logger)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(options.BaseUrl))
|
||||
{
|
||||
logger.Warning("⚠️ Keine Deploymentcenter-URL konfiguriert. Start im eingeschränkten Modus (nur Core-Shell).");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Befund D-3 aus dem Umsetzungsplan: das SDK erzwingt selbst kein TLS. Ohne diese
|
||||
// Prüfung ginge der Lizenzschlüssel im Klartext übers Netz, wenn die URL versehentlich
|
||||
// auf http:// steht.
|
||||
if (!Uri.TryCreate(options.BaseUrl, UriKind.Absolute, out var parsed) ||
|
||||
!(parsed.Scheme == Uri.UriSchemeHttps || (parsed.Scheme == Uri.UriSchemeHttp && parsed.IsLoopback)))
|
||||
{
|
||||
logger.Warning($"⚠️ Deploymentcenter-URL muss HTTPS verwenden (http:// nur für localhost): '{options.BaseUrl}'. " +
|
||||
"Start im eingeschränkten Modus.");
|
||||
return false;
|
||||
}
|
||||
|
||||
string key;
|
||||
try
|
||||
{
|
||||
// Erlaubt einen mit dem Master-Key verschlüsselten Schlüssel (enc:v1:…);
|
||||
// Klartext wird unverändert durchgereicht.
|
||||
key = SecretProtection.Unprotect(settings.LicenseKey);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Warning($"⚠️ Gespeicherter Lizenzschlüssel nicht lesbar ({ex.Message}) – behandle als 'keine Lizenz'.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
logger.Warning("⚠️ Kein Lizenzschlüssel gesetzt. Start im eingeschränkten Modus (nur Core-Shell). " +
|
||||
"Schlüssel über „Lizenzschlüssel setzen …\" im Einstellungsfenster eintragen.");
|
||||
return false;
|
||||
}
|
||||
|
||||
LicenseClient.DefaultAppVersion = BuildInfo.Version;
|
||||
var client = new LicenseClient();
|
||||
LicenseValidationResult result = client
|
||||
.ValidateAsync(options.ProjectSlug, key, options.BaseUrl, BuildInfo.Version)
|
||||
.GetAwaiter().GetResult();
|
||||
|
||||
if (result.IsValid)
|
||||
{
|
||||
logger.Info(result.IsCached
|
||||
? $"🔑 Lizenz im Offline-Kulanzmodus gültig ({result.Status})."
|
||||
: "🔑 Lizenz gültig (online geprüft).");
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.Warning($"⚠️ Keine gültige Lizenz ({result.Status}: {result.Message}). " +
|
||||
"Start im eingeschränkten Modus (nur Core-Shell, keine Module). " +
|
||||
"Schlüssel über „Lizenzschlüssel setzen …\" im Einstellungsfenster prüfen.");
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"❌ Lizenzprüfung unerwartet fehlgeschlagen: {ex.Message}. Start im eingeschränkten Modus.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user