feat(ui): complete Avalonia UI port with 7 main pages, tool settings & top MenuBar
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
# Deploymentcenter-Anbindung — Durchsicht
|
||||
|
||||
> **Nachtrag 2026-08-08 — die Anbindung ist umgestellt, Server und SDK stehen auf 2.1.**
|
||||
> Abschnitt 4 und 5 sind abgearbeitet; wie es jetzt aussieht, steht in
|
||||
> [Deploymentcenter-Integration](Deploymentcenter-Integration.md).
|
||||
>
|
||||
> Mit **SDK 2.1 erledigt** (waren Befunde aus Abschnitt 3 bzw. aus der Durchsicht der
|
||||
> 2.0-Anbindung):
|
||||
>
|
||||
> - `HttpClient` ohne Zeitgrenze → intern 15 s. Unsere Umgehung (eigener Client mit
|
||||
> 8 s) ist zurückgebaut.
|
||||
> - HTTP 429/5xx entzogen die Lizenz, ohne den Zwischenspeicher zu befragen → jeder
|
||||
> Nicht-Erfolg führt jetzt in denselben Offline-Zweig, `IsTransient` macht den
|
||||
> Unterschied sichtbar. Unsere Behelfsprüfung auf `unknown_error` ist entfernt.
|
||||
> - `cache_ttl_hours` wurde ignoriert, die Gnadenfrist war faktisch unbegrenzt.
|
||||
> - `app_version` fest `"1.0.0"` → kommt jetzt aus `ReleaseInfo.Version`.
|
||||
> - `BuildInfo.targets` war nicht einbindbar (CS0433/CS0103) → erzeugt die Klasse im
|
||||
> eigenen Namensraum, ist eingebunden.
|
||||
> - `UpdateClient`: API-Zweig las snake_case in ein camelCase-Modell → eigenes Modell
|
||||
> `ApiReleaseInfo`, `is_critical` von der obersten Ebene.
|
||||
> - `DeactivateAsync` schickte den Shared Key zusätzlich als `X-Watchdog-Key`.
|
||||
> - Kein `CancellationToken` in der Lizenz-API.
|
||||
>
|
||||
> **Weiterhin offen** — betrifft das Deploymentcenter, nicht ClawdDotNet:
|
||||
>
|
||||
> - **2.1 (keine Signaturprüfung)** — unverändert. `LicenseInfo.PublicKeyBase64` ist
|
||||
> gestrichen, damit nichts Totes stehenbleibt und niemand Schutz vermutet, wo keiner
|
||||
> ist. Kommt die Signatur, kommt das Feld mit ihr zurück.
|
||||
> - **2.2 (v1-Ersatzhash)** und **2.3 (Klartext-Rückfall)** — unverändert, beides im
|
||||
> SDK zu beheben.
|
||||
> - **3 (HW-ID bei jedem Aufruf neu)** — clientseitig umgangen: einmal berechnet und
|
||||
> behalten.
|
||||
> - **`parent_source` ist nur eine `source`, kein Paar** — damit schließen sich „ein
|
||||
> Monitor je Instanz" und instanzweise Alarmunterdrückung gegenseitig aus.
|
||||
|
||||
Stand: 2026-08-06. Geprüft: `J:\Softwareprojekte\Deploymentcenter` (Client, Server,
|
||||
Schema, beide Integrationsleitfäden) gegen den
|
||||
[HW-ID-v2-Vorschlag](Lizenz-HardwareId-v2-Implementierungsvorschlag.md) und die
|
||||
[Linux-Analyse](Linux-Portierung-Analyse.md).
|
||||
|
||||
**Ergebnis vorweg: Die Lizenz blockiert den Linux-Umzug nicht mehr.** Alles, was
|
||||
an Hardware-ID v2 plattformrelevant war, ist da und richtig. Was hier steht, sind
|
||||
Punkte aus derselben Durchsicht — drei davon würden beim Ausrollen wehtun.
|
||||
|
||||
---
|
||||
|
||||
## 1. Was erledigt ist
|
||||
|
||||
| Punkt aus dem Vorschlag | Umsetzung |
|
||||
|---|---|
|
||||
| Format `2:<plattform>:<hex>` | [HardwareId.cs:125](../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/HardwareId.cs) |
|
||||
| **Kein `MachineName` im Hash** | `ComputeV2Hash`, `:138` — der wichtigste Punkt, sauber umgesetzt |
|
||||
| Quellenkette Windows/Linux | `:42–107`, inklusive `dmi-uuid` |
|
||||
| `IsPlausibleMachineId` (Länge, `uninitialized`, nur Nullen) | `:161` |
|
||||
| MAC-Filter über locally-administered-Bit | `:224` |
|
||||
| `/sys/class/net/<name>/device`-Prüfung | `:228` |
|
||||
| Erweiterte Stoppwortliste | `:23` — inkl. `br-`, `virbr`, `cni`, `cali` |
|
||||
| `machine.key` mit `0600` | `:290`, `SetUnixPermissions` mit `#if NET8_0_OR_GREATER` |
|
||||
| Vorgabe per Umgebungsvariable | `LicenseConfig.HardwareIdOverride`, beide Namen |
|
||||
| XDG-Auflösungskette, nie leerer Pfad | [LicenseConfig.cs:28](../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/LicenseConfig.cs), mit `ValidateNonEmpty` |
|
||||
| Mehrfachziel `netstandard2.0;net8.0` | csproj, BouncyCastle nur im netstandard-Zweig |
|
||||
| `LLS2`-Hülle, AES-GCM, HKDF | [StateStore.cs:169](../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/StateStore.cs) — Schlüssel aus HW-ID abgeleitet, bindet den Cache also echt an die Maschine |
|
||||
| `ILicensePrompt` + Konsolenfassung | vorhanden — genau das, was der kopflose Host braucht |
|
||||
| Servermigration v1→v2 | [LicenseService.php:108](../../Deploymentcenter/src/Modules/License/LicenseService.php), mit Prüfprotokolleintrag `hwid_migrated` |
|
||||
| Schema `hwid_version`/`hwid_source`/`platform` | `sql/migrations/v2_hardware_id.sql`, rückwärtskompatibel |
|
||||
| Verwaltungsansicht zeigt Quelle/Plattform | `public/index.php:1227` |
|
||||
|
||||
`OperatingSystemHelpers` nutzt jetzt `RuntimeInformation`. Der Client hat auf dem
|
||||
Linux-Pfad keine Windows-Laufzeitabhängigkeit — `ProtectedData` wird nur unter
|
||||
`IsWindows()` aufgerufen.
|
||||
|
||||
**Für die Portierung heißt das:** Punkt 4 aus der Entscheidungsliste der
|
||||
Linux-Analyse („Erlaubt LicenseLabrador den Wechsel der Hardware-ID?") ist
|
||||
beantwortet. Der Aufwandsblock „Lizenz" schrumpft von 3–5 PT auf **2–3 PT** —
|
||||
das ist jetzt reine Anschlussarbeit in ClawdDotNet, keine Konzeptarbeit mehr.
|
||||
|
||||
---
|
||||
|
||||
## 2. Drei Befunde, die vor dem Ausrollen geklärt sein sollten
|
||||
|
||||
### 2.1 Es wird nichts signiert — die Lizenzprüfung ist eine Vertrauensfrage an DNS
|
||||
|
||||
[LicenseClient.cs:62](../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/LicenseClient.cs):
|
||||
|
||||
```csharp
|
||||
string status = root.TryGetProperty("status", out var sProp) ? sProp.GetString() ?? "unknown" : "unknown";
|
||||
if (status.Equals("valid", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// → gültig
|
||||
}
|
||||
```
|
||||
|
||||
Das ist die vollständige Prüfung. Es gibt im neuen Client **kein `Signature.cs`,
|
||||
keinen hinterlegten öffentlichen Schlüssel, keine Hüllenprüfung** — die Dateien
|
||||
`Signature.cs`, `LicenseResult.cs` und `LicenseState.cs` aus dem alten
|
||||
LicenseLabrador-SDK sind beim Umzug nicht mitgekommen.
|
||||
|
||||
Folge: Wer die HTTP-Anfrage umlenken kann, hat eine gültige Lizenz. Ein Eintrag
|
||||
in `/etc/hosts`, ein Proxy, ein eigener DNS — die Antwort `{"status":"valid"}`
|
||||
genügt. Auf einem Linux-Server, den der Betreiber ohnehin vollständig
|
||||
kontrolliert, ist das kein Kunststück.
|
||||
|
||||
Serverseitig sieht es passend dazu aus. `public/index.php:45`:
|
||||
|
||||
```php
|
||||
'signature' => 'ED25519_SIG_' . base64_encode(hash('sha256', $lic['license_key'] . 'DC_OFFLINE_SECRET', true))
|
||||
```
|
||||
|
||||
Das ist ein SHA-256 über den Lizenzschlüssel plus eine fest verdrahtete
|
||||
Zeichenkette — keine Signatur, sondern ein Wert, der jeder erzeugen kann, der den
|
||||
Quelltext kennt. Und `public/index.php:1993` im JavaScript:
|
||||
|
||||
```javascript
|
||||
"ED25519_SIG_" + btoa(key + hwId).substring(0, 32)
|
||||
```
|
||||
|
||||
Base64 der Eingabe, abgeschnitten. Auch kein Hash.
|
||||
|
||||
Das ist erkennbar ein Platzhalter — nur trägt er einen Namen, der nach fertigem
|
||||
Verfahren klingt, und darauf verlässt sich [LicenseGate](Services/LicenseGate.cs)
|
||||
mit seiner harten Startsperre. **Es ist keine Portierungsfrage** (unter Windows
|
||||
gilt heute dasselbe) und auch kein Grund, den Linux-Umzug aufzuhalten — aber es
|
||||
sollte eine bewusste Entscheidung sein und nicht in dem Glauben untergehen, die
|
||||
Signaturprüfung sei bereits da.
|
||||
|
||||
Wenn das Verfahren zurückkommen soll: Ed25519 über die kanonisch serialisierte
|
||||
Antwort, öffentlicher Schlüssel im Client einkompiliert, `nonce` aus der Anfrage
|
||||
in der signierten Nutzlast gegenprüfen (gegen Wiedereinspielung). Der alte
|
||||
`Signer.php` und `Signature.cs` sind im LicenseLabrador-Repo noch vorhanden und
|
||||
lassen sich als Vorlage nehmen.
|
||||
|
||||
### 2.2 Der v1-Ersatzhash trifft die alten Aktivierungen nicht
|
||||
|
||||
Der Migrationsweg ist auf beiden Seiten korrekt gebaut — er wird nur nie
|
||||
auslösen, weil der Client eine andere v1-ID berechnet als die, die in der
|
||||
Datenbank steht.
|
||||
|
||||
Alt ([LicenseLabrador/HardwareId.cs:20](../../LicenseLabrador/client-dotnet/LicenseLabrador.Client/HardwareId.cs)):
|
||||
|
||||
```csharp
|
||||
rawBuilder.Append(machineId); // MachineGuid, sonst MAC
|
||||
rawBuilder.Append(Environment.MachineName); // direkt angehängt, kein Trenner
|
||||
→ sha256(machineGuid + machineName)
|
||||
```
|
||||
|
||||
Neu ([HardwareId.cs:149](../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/HardwareId.cs)):
|
||||
|
||||
```csharp
|
||||
string raw = $"{Environment.MachineName}:{firstMac}";
|
||||
→ sha256(machineName + ":" + mac)
|
||||
```
|
||||
|
||||
Andere Reihenfolge, anderer Trenner, und **MAC statt MachineGuid**. Auf jedem
|
||||
Windows-Rechner, auf dem `MachineGuid` lesbar war — also praktisch allen —
|
||||
stimmen die Hashes nicht überein. Der Server sucht die Altaktivierung, findet
|
||||
nichts und legt eine neue an: **genau der Platzverbrauch, den die Migration
|
||||
verhindern sollte.** Bei `max_activations = 2` ist danach ein Platz für den
|
||||
Linux-Server weniger da.
|
||||
|
||||
Auch `GetFirstPhysicalMacLegacy` (`:254`) weicht ab: keine Stoppwortfilterung,
|
||||
keine Sortierung, erste Schnittstelle in Aufzählungsreihenfolge. Die alte
|
||||
Fassung nahm die alphabetisch erste *gefilterte* MAC.
|
||||
|
||||
Zu tun: `GetLegacyHardwareId()` muss den v1-Algorithmus zeichengenau
|
||||
nachbilden — inklusive der alten Stichwortliste (`virtual`, `veth`, `docker`,
|
||||
`hyper-v`, `wsl`, `mullvad`, `wireguard`, `tap`, `tun`, `vpn`, `bluetooth`,
|
||||
`vmware`, `box`, `pseudo`, `loopback`, `npcap`, `pcap`), `OrderBy(…, Ordinal)`
|
||||
und `FirstOrDefault()`. Der Code steht im LicenseLabrador-Repo noch da und kann
|
||||
weitgehend übernommen werden.
|
||||
|
||||
Am besten mit einem Test absichern, der einen bekannten Eingabewert gegen den
|
||||
erwarteten v1-Hash prüft — sonst fällt eine Abweichung erst auf, wenn die
|
||||
Aktivierungsplätze schon verbraucht sind.
|
||||
|
||||
### 2.3 Der Klartext-Rückfall ist noch da, nur woanders
|
||||
|
||||
[StateStore.cs:79](../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/StateStore.cs) —
|
||||
„Legacy Migration Check":
|
||||
|
||||
```csharp
|
||||
string legacyJson = Encoding.UTF8.GetString(payloadBytes);
|
||||
var legacyData = JsonSerializer.Deserialize<LocalCacheData>(legacyJson);
|
||||
if (legacyData != null)
|
||||
{
|
||||
legacyData.SchemaVersion = 2;
|
||||
Save(productSlug, hardwareId, legacyData);
|
||||
return legacyData;
|
||||
}
|
||||
```
|
||||
|
||||
Der LLS2-Zweig darüber ist genau richtig — Entschlüsselung fehlgeschlagen heißt
|
||||
Cache-Fehltreffer, kein Klartext. Der Zweig darunter hebt das wieder auf: Jede
|
||||
Datei ohne `LLS2`-Kennung wird als JSON gelesen und, wenn sie sich deserialisieren
|
||||
lässt, **übernommen und anschließend verschlüsselt neu geschrieben**.
|
||||
|
||||
Durchgespielt: Eine von Hand angelegte `state.dat` mit
|
||||
|
||||
```json
|
||||
{"SchemaVersion":2,"Status":"valid","ExpiresAt":99999999999,"MaxSeenTime":0}
|
||||
```
|
||||
|
||||
wird angenommen. In `ValidateAsync` greift bei fehlender Verbindung der
|
||||
Cache-Zweig (`:110`): `Status == "valid"` ✓, `now < MaxSeenTime` ✗, `now >
|
||||
ExpiresAt` ✗ → **`IsValid = true`**. Die Bindung an die Hardware, die
|
||||
`DeriveKey(hardwareId, …)` sonst herstellt, ist auf diesem Weg umgangen; die
|
||||
Datei ist zwischen Maschinen übertragbar.
|
||||
|
||||
Der Zweig hilft dabei nicht einmal beim eigentlichen Zweck. Die alte
|
||||
`LocalCacheData` hieß `last_envelope`, `max_seen_time`, `endpoints`,
|
||||
`last_license_key`; die neue `SchemaVersion`, `Status`, `ExpiresAt`, … Kein
|
||||
gemeinsames Feld, und `JsonSerializer` ist ohne
|
||||
`PropertyNameCaseInsensitive`/`JsonPropertyName` bei den Namen streng. Eine echte
|
||||
v1-Datei ergibt also ein Objekt mit lauter Vorgabewerten (`Status = "invalid"`)
|
||||
und ist als Cache wertlos.
|
||||
|
||||
**Empfehlung: den Zweig ersatzlos streichen.** Er kostet Sicherheit und leistet
|
||||
nichts. Alte Cachedateien sollen verworfen werden — eine einmalige
|
||||
Online-Prüfung ist der ganze Preis.
|
||||
|
||||
Nebenbei: `Checksum = hwInfo.HardwareId` ([LicenseClient.cs:79](../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/LicenseClient.cs))
|
||||
ist keine Prüfsumme, sondern eine Kopie der HW-ID. Das Feld ist damit ohne
|
||||
Funktion — entweder mit einem HMAC über die übrigen Felder füllen oder entfernen,
|
||||
damit niemand später Schutz vermutet, wo keiner ist.
|
||||
|
||||
---
|
||||
|
||||
## 3. Kleinere Punkte
|
||||
|
||||
| Fundstelle | Sache |
|
||||
|---|---|
|
||||
| [LicenseClient.cs:26](../../Deploymentcenter/client-dotnet/Deploymentcenter.Client/LicenseClient.cs) | Eigener `HttpClient` je Instanz, nie freigegeben, **ohne Zeitgrenze** (Vorgabe 100 s). Der alte `LicenseConfig.HttpTimeout` war 6 s. In `LicenseGate.RunStartupCheck` bedeutet das bis zu 100 s Standbild beim Start, wenn der Server nicht antwortet. |
|
||||
| `:107` | `catch (Exception ex)` um den gesamten Block: Auch ein Fehler beim Auswerten einer *erfolgreichen* Antwort landet im Offline-Zweig. Ein defekter Server gilt dann als „offline". |
|
||||
| `:47` | `app_version = "1.0.0"` fest verdrahtet. ClawdDotNet hat `BuildInfo.Build` — sollte Parameter sein, sonst steht in der Verwaltungsansicht bei jeder Instanz dasselbe. |
|
||||
| `:32`, `:172` | `HardwareId.GetHardwareId()` bei jedem Aufruf neu: liest unter Linux Dateien und zählt Netzwerkschnittstellen auf. Einmal berechnen und halten. |
|
||||
| `HardwareId.cs:205` | MAC-Auswahl überspringt Schnittstellen, die nicht `Up` oder `Unknown` sind. Ein Kabel, das beim Start nicht steckt, ändert damit die Hardware-ID. Für die Ausweichlösung sollte der Betriebszustand keine Rolle spielen — sonst ist sie genau in dem Moment instabil, in dem sie gebraucht wird. |
|
||||
| `HardwareId.cs:231` | `/sys/class/net/<name>/device` ist ein Symlink. `Directory.Exists`/`File.Exists` folgen ihm — funktioniert, ist aber Zufall und sollte kommentiert sein. |
|
||||
|
||||
---
|
||||
|
||||
## 4. Watchdog: die Anbindung passt noch nicht
|
||||
|
||||
Kein Linux-Thema, fällt aber in dieselbe Umbauarbeit.
|
||||
|
||||
[WatchdogClient.cs](src/ClawdDotNet.Core/Watchdog/WatchdogClient.cs) sendet an:
|
||||
|
||||
| ClawdDotNet | Deploymentcenter |
|
||||
|---|---|
|
||||
| `POST /api/heartbeat` | `POST /api/watchdog/v1/ping` (nimmt auch `/heartbeat`) |
|
||||
| `POST /api/event` | `POST /api/watchdog/v1/event` |
|
||||
| `POST /api/register` | **existiert nicht** |
|
||||
|
||||
Die Pfade sind also alle um `/watchdog/v1` zu ergänzen. Der Kopfzeilenname passt:
|
||||
`public/api/watchdog/v1/index.php:30` akzeptiert `X-Watchdog-Key`,
|
||||
`Authorization` und `X-Agent-Token`.
|
||||
|
||||
Der Selbstregistrierungsweg aus [Program.cs:334](Program.cs:334) — mit dem
|
||||
Master-Token einen eigenen Agent-Token holen und in der Instanzkonfiguration
|
||||
zwischenspeichern — hat serverseitig kein Gegenstück mehr. Zu klären: Tokens
|
||||
künftig von Hand in der Verwaltung anlegen und in die Instanzkonfiguration
|
||||
eintragen, oder `/register` im Deploymentcenter nachziehen. Für den ersten Weg
|
||||
spricht, dass er den Master-Token gar nicht erst auf die Instanzen verteilt.
|
||||
|
||||
Die Feldnamen des Ping-Rumpfs (`source`, `instance`, `type`, `status`, `message`,
|
||||
`interval`, `group`, `os`) sind gegen
|
||||
[InstanceHealthProvider](src/ClawdDotNet.Core/Watchdog/InstanceHealthProvider.cs)
|
||||
abzugleichen.
|
||||
|
||||
---
|
||||
|
||||
## 5. Was in ClawdDotNet zu tun ist
|
||||
|
||||
| Datei | Was |
|
||||
|---|---|
|
||||
| [ClawdDotNet.csproj](ClawdDotNet.csproj) | Projektverweis von `..\LicenseLabrador\client-dotnet\…` auf `..\Deploymentcenter\client-dotnet\Deploymentcenter.Client\…` umhängen. Langfristig als Submodul unter `external/` — der Kommentar dazu steht schon im csproj. |
|
||||
| [Services/LicenseGate.cs](Services/LicenseGate.cs) | Neu gegen `LicenseValidationResult` schreiben. `LicenseState` gibt es nicht mehr, `Status` ist jetzt eine Zeichenkette — `DescribeProblem` (`:125`) muss auf `revoked`/`expired`/`activation_limit`/`not_found` umgestellt werden. `MessageBox` durch `ILicensePrompt` ersetzen; die Konsolenfassung bringt der Client mit. |
|
||||
| [Services/LicenseInfo.cs](Services/LicenseInfo.cs) | `PublicKeyBase64` hat ohne Signaturprüfung keine Funktion mehr — entweder mit 2.1 zurückholen oder streichen, damit nichts Totes stehenbleibt. |
|
||||
| [Program.cs:112](Program.cs:112) | Lizenzprüfung so verlagern, dass sie ohne Fenster auskommt (kopfloser Host). |
|
||||
| Host (neu) | `--license-status`, `--license-set-key`, `--license-deactivate` — der Client bringt alles Nötige mit. |
|
||||
| [WatchdogClient.cs](src/ClawdDotNet.Core/Watchdog/WatchdogClient.cs) | Pfade auf `/api/watchdog/v1/…`; Registrierungsweg klären (Abschnitt 4). |
|
||||
| `docs/Integrationsplan-WatchDog-LicenseLabrador.md` | Abgelöst durch [Deploymentcenter-Integration](Deploymentcenter-Integration.md). |
|
||||
|
||||
---
|
||||
|
||||
## 6. Antwort auf die Ausgangsfrage
|
||||
|
||||
**Ja — Avalonia und Linux sind damit machbar.** Die einzige Frage, die ich als
|
||||
möglicher Blocker außerhalb unserer Hand markiert hatte, ist geklärt: Der Client
|
||||
läuft auf beiden Plattformen, zielt auf `net8.0` (von net10.0 problemlos
|
||||
verwendbar), löst seinen Ablageort auch ohne `HOME` auf, und die HW-ID ist
|
||||
container- und umbenennungsfest.
|
||||
|
||||
Der Lizenzblock in der Aufwandsschätzung fällt von 3–5 PT auf **2–3 PT**. Die
|
||||
Gesamtspanne bleibt bei **50–80 PT**, weil die Lizenz nie der große Posten war —
|
||||
das sind PropertyGrid und Chat-Ansicht.
|
||||
|
||||
Zwei Dinge sollten aber vor dem Ausrollen erledigt sein, unabhängig von Linux:
|
||||
|
||||
- **2.2 (v1-Ersatzhash)** — klein, aber wenn es beim Ausrollen falsch ist, sind
|
||||
Aktivierungsplätze verbraucht und man bekommt sie nur einzeln über die
|
||||
Verwaltung zurück. Das ist der Punkt mit dem schlechtesten Verhältnis von
|
||||
Aufwand zu Schaden.
|
||||
- **2.3 (Klartext-Rückfall)** — eine Zeile weniger Code, dafür wieder das
|
||||
Verhalten, das der `LLS2`-Umbau eigentlich herstellen sollte.
|
||||
|
||||
**2.1 (keine Signaturprüfung)** ist eine eigene Entscheidung mit eigenem Umfang
|
||||
und hält den Umzug nicht auf. Sie sollte nur getroffen und nicht übersehen
|
||||
werden — der Name `ED25519_SIG_` im Serverquelltext legt sonst nahe, dass die
|
||||
Sache erledigt sei.
|
||||
Reference in New Issue
Block a user