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:
Generated
+31
-1
@@ -26,6 +26,9 @@ namespace PolyTraderSharp.Ui.Views
|
||||
btn_loadsettings = new ToolStripButton();
|
||||
btnGenMasterKey = new ToolStripButton();
|
||||
btnSetOpenRouterKey = new ToolStripButton();
|
||||
btnSetWatchdogToken = new ToolStripButton();
|
||||
btnTestWatchdog = new ToolStripButton();
|
||||
btnSetLicenseKey = new ToolStripButton();
|
||||
tabPage2 = new TabPage();
|
||||
pgAccount = new PropertyGrid();
|
||||
dgvAccounts = new DataGridView();
|
||||
@@ -74,7 +77,7 @@ namespace PolyTraderSharp.Ui.Views
|
||||
// toolStrip2
|
||||
//
|
||||
toolStrip2.ImageScalingSize = new Size(24, 24);
|
||||
toolStrip2.Items.AddRange(new ToolStripItem[] { btn_save, btn_loadsettings, btnGenMasterKey, btnSetOpenRouterKey });
|
||||
toolStrip2.Items.AddRange(new ToolStripItem[] { btn_save, btn_loadsettings, btnGenMasterKey, btnSetOpenRouterKey, btnSetWatchdogToken, btnTestWatchdog, btnSetLicenseKey });
|
||||
toolStrip2.Location = new Point(3, 3);
|
||||
toolStrip2.Name = "toolStrip2";
|
||||
toolStrip2.Size = new Size(1171, 34);
|
||||
@@ -112,6 +115,30 @@ namespace PolyTraderSharp.Ui.Views
|
||||
btnSetOpenRouterKey.Size = new Size(220, 29);
|
||||
btnSetOpenRouterKey.Text = "OpenRouter-Key setzen …";
|
||||
btnSetOpenRouterKey.ToolTipText = "Speichert den OpenRouter-API-Key für den Supervisor (gitignorierte Datei openrouter.key).";
|
||||
//
|
||||
// btnSetWatchdogToken
|
||||
//
|
||||
btnSetWatchdogToken.DisplayStyle = ToolStripItemDisplayStyle.Text;
|
||||
btnSetWatchdogToken.Name = "btnSetWatchdogToken";
|
||||
btnSetWatchdogToken.Size = new Size(220, 29);
|
||||
btnSetWatchdogToken.Text = "Watchdog-Token setzen …";
|
||||
btnSetWatchdogToken.ToolTipText = "Speichert den Watchdog-Agent-Token (maskierte Eingabe, bei gesetztem Master-Key verschlüsselt).";
|
||||
//
|
||||
// btnTestWatchdog
|
||||
//
|
||||
btnTestWatchdog.DisplayStyle = ToolStripItemDisplayStyle.Text;
|
||||
btnTestWatchdog.Name = "btnTestWatchdog";
|
||||
btnTestWatchdog.Size = new Size(210, 29);
|
||||
btnTestWatchdog.Text = "Test-Heartbeat senden";
|
||||
btnTestWatchdog.ToolTipText = "Sendet sofort einen Heartbeat an den konfigurierten Watchdog-Server und meldet das Ergebnis.";
|
||||
//
|
||||
// btnSetLicenseKey
|
||||
//
|
||||
btnSetLicenseKey.DisplayStyle = ToolStripItemDisplayStyle.Text;
|
||||
btnSetLicenseKey.Name = "btnSetLicenseKey";
|
||||
btnSetLicenseKey.Size = new Size(210, 29);
|
||||
btnSetLicenseKey.Text = "Lizenz prüfen / setzen …";
|
||||
btnSetLicenseKey.ToolTipText = "Öffnet den Lizenzdialog (Hardware-ID + Server-Validierung). Gültiger Schlüssel wird gespeichert (bei Master-Key verschlüsselt).";
|
||||
//
|
||||
// tabPage2
|
||||
//
|
||||
@@ -207,6 +234,9 @@ namespace PolyTraderSharp.Ui.Views
|
||||
private ToolStripButton btn_loadsettings;
|
||||
private ToolStripButton btnGenMasterKey;
|
||||
private ToolStripButton btnSetOpenRouterKey;
|
||||
private ToolStripButton btnSetWatchdogToken;
|
||||
private ToolStripButton btnTestWatchdog;
|
||||
private ToolStripButton btnSetLicenseKey;
|
||||
private ToolStrip toolStripAccounts;
|
||||
private ToolStripButton btnAccNew;
|
||||
private ToolStripButton btnAccDelete;
|
||||
|
||||
+161
-1
@@ -3,8 +3,10 @@ using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using PolyTrader.Core.Persistence;
|
||||
using PolyTrader.Core.Security;
|
||||
using PolyTraderSharp.Models;
|
||||
using PolyTraderSharp.Services;
|
||||
|
||||
@@ -23,6 +25,7 @@ namespace PolyTraderSharp.Ui.Views
|
||||
private ServerSettings _settings = new();
|
||||
private ThreemaService? _threema;
|
||||
private MullvadVpnService? _vpn;
|
||||
private WatchdogHeartbeatService? _watchdog;
|
||||
private TerminalLogger? _logger;
|
||||
|
||||
private IAccountRepository? _accountRepo;
|
||||
@@ -37,6 +40,9 @@ namespace PolyTraderSharp.Ui.Views
|
||||
btn_loadsettings.Click += (_, _) => Reload();
|
||||
btnGenMasterKey.Click += (_, _) => GenerateMasterKey();
|
||||
btnSetOpenRouterKey.Click += (_, _) => SetOpenRouterKey();
|
||||
btnSetWatchdogToken.Click += (_, _) => SetWatchdogToken();
|
||||
btnTestWatchdog.Click += async (_, _) => await TestWatchdogAsync();
|
||||
btnSetLicenseKey.Click += (_, _) => SetLicenseKey();
|
||||
UpdateMasterKeyButtonState();
|
||||
|
||||
btnAccNew.Click += (_, _) => AddAccount();
|
||||
@@ -52,13 +58,14 @@ namespace PolyTraderSharp.Ui.Views
|
||||
}
|
||||
|
||||
public void Initialize(ThreemaService threema, MullvadVpnService vpn, TerminalLogger logger,
|
||||
IAccountRepository accountRepo, TradingState state)
|
||||
IAccountRepository accountRepo, TradingState state, WatchdogHeartbeatService? watchdog = null)
|
||||
{
|
||||
_threema = threema;
|
||||
_vpn = vpn;
|
||||
_logger = logger;
|
||||
_accountRepo = accountRepo;
|
||||
_state = state;
|
||||
_watchdog = watchdog;
|
||||
|
||||
Reload();
|
||||
LoadAccounts();
|
||||
@@ -79,6 +86,7 @@ namespace PolyTraderSharp.Ui.Views
|
||||
_settings.Save(SettingsPath);
|
||||
_threema?.ReloadSettings();
|
||||
_vpn?.ReloadSettings();
|
||||
_watchdog?.ReloadSettings();
|
||||
_logger?.Info("Server-Einstellungen gespeichert und Services neu geladen.");
|
||||
MessageBox.Show("Server-Einstellungen gespeichert.", "Erfolg",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
@@ -135,6 +143,158 @@ namespace PolyTraderSharp.Ui.Views
|
||||
dgvAccounts.Refresh();
|
||||
}
|
||||
|
||||
// ===== Watchdog =====
|
||||
|
||||
/// <summary>
|
||||
/// Setzt/entfernt den Watchdog-Agent-Token über eine maskierte Eingabe – damit das Secret
|
||||
/// nicht offen im PropertyGrid steht und die server_settings.xml nicht per Hand bearbeitet
|
||||
/// werden muss. Ist ein Master-Key gesetzt, wird der Token verschlüsselt abgelegt.
|
||||
/// Wird sofort gespeichert, damit der Test-Button und der laufende Dienst ihn direkt nutzen.
|
||||
/// </summary>
|
||||
private void SetWatchdogToken()
|
||||
{
|
||||
bool exists = !string.IsNullOrWhiteSpace(_settings.WatchdogToken);
|
||||
string? token = PromptForSecret("Watchdog Agent-Token",
|
||||
"Agent-Token aus dem Watchdog-Admin eingeben (Header X-Watchdog-Key).\n" +
|
||||
"Wird in der gitignorierten server_settings.xml gespeichert (leer = entfernen).\n" +
|
||||
(exists ? $"Aktuell: {_settings.WatchdogTokenStatus}." : "Aktuell ist KEIN Token gesetzt."));
|
||||
if (token == null) return; // Abbruch
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
_settings.WatchdogToken = "";
|
||||
_settings.Save(SettingsPath);
|
||||
_watchdog?.ReloadSettings();
|
||||
propertyGrid.Refresh();
|
||||
_logger?.Info("Watchdog-Agent-Token entfernt.");
|
||||
MessageBox.Show("Watchdog-Token entfernt.", "Watchdog",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
// Protect() gibt ohne Master-Key den Klartext unverändert zurück – kein stiller
|
||||
// Sicherheitsverlust, der Nutzer wird darauf hingewiesen.
|
||||
_settings.WatchdogToken = SecretProtection.Protect(token.Trim());
|
||||
_settings.Save(SettingsPath);
|
||||
_watchdog?.ReloadSettings();
|
||||
propertyGrid.Refresh();
|
||||
|
||||
bool encrypted = SecretProtection.IsEncrypted(_settings.WatchdogToken);
|
||||
_logger?.Info($"Watchdog-Agent-Token gespeichert ({(encrypted ? "verschlüsselt" : "Klartext")}).");
|
||||
MessageBox.Show(
|
||||
"Watchdog-Token gespeichert und sofort aktiv.\n\n" +
|
||||
(encrypted
|
||||
? "Der Token liegt mit dem Master-Key verschlüsselt in server_settings.xml."
|
||||
: "Hinweis: Es ist kein Master-Key gesetzt – der Token liegt im Klartext in der " +
|
||||
"(gitignorierten) server_settings.xml. Mit „Master-Key erzeugen\" kannst du das ändern."),
|
||||
"Watchdog", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"Fehler beim Speichern des Watchdog-Tokens: {ex.Message}",
|
||||
"Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sendet einen Heartbeat mit den GESPEICHERTEN Einstellungen und meldet das Ergebnis.
|
||||
/// Ungespeicherte Änderungen im PropertyGrid wirken bewusst nicht – sonst würde ein
|
||||
/// erfolgreicher Test eine Konfiguration bestätigen, die so nicht auf der Platte liegt.
|
||||
/// </summary>
|
||||
private async Task TestWatchdogAsync()
|
||||
{
|
||||
if (_watchdog == null)
|
||||
{
|
||||
MessageBox.Show("Der Watchdog-Dienst ist nicht verfügbar.", "Watchdog",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
btnTestWatchdog.Enabled = false;
|
||||
try
|
||||
{
|
||||
var result = await _watchdog.SendHeartbeatAsync(
|
||||
"ok", "Test-Heartbeat aus dem PolyTrader-Settings-Fenster");
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
_logger?.Info($"✅ Watchdog-Test-Heartbeat erfolgreich gesendet ({result.Detail}).");
|
||||
MessageBox.Show(
|
||||
$"Test-Heartbeat erfolgreich gesendet ({result.Detail}).\n\n" +
|
||||
"Der Monitor sollte im Watchdog-Dashboard jetzt auf 'up' stehen.",
|
||||
"Watchdog", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger?.Warning($"⚠️ Watchdog-Test-Heartbeat fehlgeschlagen: {result.Detail}");
|
||||
MessageBox.Show(
|
||||
$"Test-Heartbeat fehlgeschlagen:\n\n{result.Detail}\n\n" +
|
||||
"Bitte URL, Agent-Token und Source prüfen und die Einstellungen vorher speichern.",
|
||||
"Watchdog", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
btnTestWatchdog.Enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Lizenz (LicenseLabrador) =====
|
||||
|
||||
/// <summary>
|
||||
/// Öffnet denselben Lizenzdialog wie der Programmstart (Hardware-ID + Server-Validierung),
|
||||
/// nur im Verwalten-Modus. Ein erfolgreich validierter Schlüssel wird – bei gesetztem
|
||||
/// Master-Key verschlüsselt – in server_settings.xml gespeichert und gilt ab dem nächsten Start.
|
||||
/// </summary>
|
||||
private void SetLicenseKey()
|
||||
{
|
||||
string current = string.Empty;
|
||||
try { current = SecretProtection.Unprotect(_settings.LicenseKey); }
|
||||
catch { /* nicht lesbar → als leer behandeln */ }
|
||||
|
||||
LicenseLabrador.Client.LicenseClient client;
|
||||
try
|
||||
{
|
||||
client = new LicenseLabrador.Client.LicenseClient(PolyTraderSharp.Licensing.LicenseGate.BuildConfig());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"Lizenzprüfung nicht verfügbar: {ex.Message}", "Lizenz",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
using var dlg = new LicenseDialog(client, current, initialResult: null, startupContext: false);
|
||||
dlg.ShowDialog(this);
|
||||
|
||||
if (dlg.ValidatedResult == null || !dlg.ValidatedResult.IsUsable || string.IsNullOrWhiteSpace(dlg.ValidatedKey))
|
||||
return; // abgebrochen oder nicht validiert – nichts speichern
|
||||
|
||||
try
|
||||
{
|
||||
_settings.LicenseKey = SecretProtection.Protect(dlg.ValidatedKey!.Trim());
|
||||
_settings.Save(SettingsPath);
|
||||
propertyGrid.Refresh();
|
||||
|
||||
bool encrypted = SecretProtection.IsEncrypted(_settings.LicenseKey);
|
||||
_logger?.Info($"Lizenz validiert und gespeichert ({(encrypted ? "verschlüsselt" : "Klartext")}).");
|
||||
MessageBox.Show(
|
||||
"Lizenz validiert und gespeichert.\n\n" +
|
||||
(encrypted
|
||||
? "Der Schlüssel liegt mit dem Master-Key verschlüsselt in server_settings.xml."
|
||||
: "Hinweis: Es ist kein Master-Key gesetzt – der Schlüssel liegt im Klartext in der " +
|
||||
"(gitignorierten) server_settings.xml."),
|
||||
"Lizenz", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"Fehler beim Speichern des Lizenzschlüssels: {ex.Message}",
|
||||
"Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Master-Key (at-rest-Verschlüsselung, F1) =====
|
||||
|
||||
/// <summary>Pfad der Master-Key-Datei – identisch zu Program.cs (App-Ordner, gitignored).</summary>
|
||||
|
||||
Reference in New Issue
Block a user