Behebt eine Reihe zusammenhaengender Fehler im Update-Weg, die zusammen verhindert haben, fuer mehr als eine Plattform auszuliefern - und die im Fehlerfall halb aktualisierte Installationen hinterliessen. Server - Migration 009: Spalte platform samt neuem Unique-Key. Zuvor verdraengte das zuletzt veroeffentlichte Paket alle anderen Plattformen derselben Version, weil ON DUPLICATE KEY auf (slug, version, channel) griff. Ein Linux-System zog sich damit das Windows-Paket. - Aufloesungsregel: je Version das plattformgenaue Paket, sonst das plattformunabhaengige. Ein Client ohne Plattformangabe sieht ausschliesslich 'any' - lieber kein Update als das falsche. - manifest_json wird endlich befuellt; die Spalte blieb bisher immer leer, wodurch die API nie der Rueckfall sein konnte, als der sie gedacht war. - Releases werden serverseitig mit RSA-SHA256 signiert, neuer Endpunkt /api/updateservice/v1/pubkey. Bewusst kein HMAC: der Pruefende laeuft auf fremden Systemen und darf den Signierschluessel nicht besitzen. Packager - Bricht ab, statt die Versionshistorie zu verlieren. Schlug das Lesen der bestehenden latest.json fehl, ersetzte ein leeres catch die komplette Historie durch einen einzigen Eintrag - ohne jede Meldung. - Echte Glob-Muster. Zuvor trafen "logs/**" und "scratch/**" aus der mitgelieferten Beispielkonfiguration nie zu. - preservePatterns: Konfigurationsvorlagen werden ausgeliefert, ersetzen am Ziel aber keine vorhandene Datei. Eine settings.json mit Zugangsdaten ueberschrieb bisher beim Update die Konfiguration jedes Zielsystems. - Warnt vor Dateien, die nach Zugangsdaten aussehen und auf keiner Liste stehen. - Prueft --version gegen die Hauptassembly. Eine Abweichung fuehrte zu einer Endlosschleife: Clients aktualisieren, melden weiter die alte Version, halten das Release erneut fuer neu. - --platform mit Ableitung aus dem Publish-Pfad. Agent - Anwenden mit Plan, Backup und vollstaendigem Rollback. Die Stelle war als "Atomic Replace with Backup" kommentiert und war eine Kopierschleife. - Verwaiste Dateien werden entfernt, aber nur solche aus dem Manifest der Vorversion. Was nicht aus einem Release stammt, bleibt liegen. - Das laufende Agent-Binary wird zur Seite gelegt statt ueberschrieben. - API-Rueckfall in FetchManifestAsync; bisher nur im SDK vorhanden, weshalb die Anwendung "Update verfuegbar" und der Agent "kein Release" sagen konnte. - Installierte Version aus --current-version oder manifest.json statt des Textes "Unbekannt", der als 0 gelesen wurde und jede Version neuer erscheinen liess. Reparatur funktioniert damit auch ohne manifest.json. - Setzt das Ausfuehrungsbit fuer Linux-Pakete, die unter Windows gebaut wurden. SDK - ResolveAgentPath() liefert den plattformrichtigen Namen; ein fest verdrahtetes "update-agent.exe" wird unter Linux nie gefunden. - LaunchUpdateAgent uebergibt jetzt --restart (wurde nie uebergeben, die Anwendung blieb nach dem Update zu), --wait-for-pid (kein Wettlauf mehr mit dem Herunterfahren) und --platform. Enthaelt ausserdem die bislang nicht committete Arbeit an Watchdog, Lizenz- Client und cli/tick.php samt Migration 008; die betroffenen Dateien liessen sich nicht getrennt stagen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
419 lines
16 KiB
Markdown
419 lines
16 KiB
Markdown
# Deploymentcenter — Lizenzsystem Integration für KI-Agenten
|
||
|
||
> **Zielgruppe**: KI-Agenten & Softwareentwickler
|
||
> **Gültig ab**: Hardware-ID v2, Deploymentcenter 2.0
|
||
> **Plattformen**: Windows, Linux (inkl. systemd und Docker), macOS
|
||
|
||
> **⚠️ Änderungen in Version 2.0**
|
||
> - `/api/license/v1/validate` bleibt **unverändert** und ohne Token erreichbar.
|
||
> Ausgelieferte Clients laufen ohne Anpassung weiter.
|
||
> - `/api/license/v1/deactivate` verlangt weiterhin Authentifizierung — aber mit
|
||
> dem **neu erzeugten** `shared_key`. Der alte Wert lag im Repository und wurde
|
||
> ersetzt; wer ihn irgendwo eingetragen hat, muss nachziehen.
|
||
> - Die Signatur der Offline-Lizenzdateien heißt jetzt ehrlich HMAC-SHA256 statt
|
||
> irreführend `ED25519_SIG_`. Der frühere „Server Public Key" im WebUI war frei
|
||
> erfunden und wurde entfernt.
|
||
>
|
||
> Umstellungsschritte: **[UPGRADE.md](./UPGRADE.md)**
|
||
|
||
> **⚠️ Änderungen in Version 2.1 — betreffen jede bestehende Integration**
|
||
> - **Ein HTTP-Fehler ist kein Lizenzurteil mehr.** `ValidateAsync` fällt jetzt
|
||
> bei jedem erfolglosen Aufruf auf den Offline-Cache zurück, nicht nur bei
|
||
> einer Exception. Der Status `unknown_error` entfällt, siehe **[§6](#6-fehlerfälle-richtig-behandeln)**.
|
||
> - **`cache_ttl_hours` wird ausgewertet.** Die Offline-Gnadenfrist ist damit
|
||
> tatsächlich begrenzt, siehe **[§3](#3-zustandsspeicher-statestorecs--cache-härtung)**.
|
||
> - `ValidateAsync` nimmt `appVersion` und einen `CancellationToken` entgegen.
|
||
> - Neu: `LicenseClient.TryGetCachedKey()` und `RevalidateAsync()`.
|
||
> - `DeactivateAsync` sendet den Schlüssel nur noch als `Authorization: Bearer`.
|
||
> Der zusätzliche `X-Watchdog-Key` ist entfallen.
|
||
> - Standard-Zeitgrenze von 15 s statt 100 s.
|
||
|
||
---
|
||
|
||
## 0. Antwortformat — bitte beachten
|
||
|
||
Die Lizenz-Endpunkte antworten **ohne** den `status`/`error`-Umschlag der
|
||
übrigen Deploymentcenter-API. Das Feld `status` auf oberster Ebene trägt den
|
||
**Lizenzzustand**:
|
||
|
||
```json
|
||
{
|
||
"type": "validation_result",
|
||
"status": "valid",
|
||
"issued_at": 1786435199,
|
||
"expires_at": 1817971199,
|
||
"cache_ttl_hours": 168,
|
||
"hardware_id": "2:win:a765bd47...",
|
||
"license_key": "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX",
|
||
"nonce": "der übergebene Wert",
|
||
"endpoints": { "validate": "/api/license/v1/validate",
|
||
"deactivate": "/api/license/v1/deactivate" },
|
||
"message": "License is valid"
|
||
}
|
||
```
|
||
|
||
| `status` | Bedeutung |
|
||
|---|---|
|
||
| `valid` | Lizenz gültig, Aktivierung eingetragen |
|
||
| `not_found` | Projekt oder Schlüssel unbekannt |
|
||
| `revoked` | Lizenz widerrufen **oder diese Hardware gesperrt** |
|
||
| `suspended` | Lizenz vorübergehend ausgesetzt |
|
||
| `expired` | Ablaufdatum überschritten |
|
||
| `activation_limit` | Maximale Anzahl Aktivierungen erreicht |
|
||
|
||
> Diese Sonderstellung ist bewusst: Ein Umschlag mit `status: "success"` würde
|
||
> von jedem bestehenden Client als „nicht valid" gelesen — sämtliche Lizenzen
|
||
> gälten schlagartig als ungültig. Wer eine eigene Anbindung schreibt, muss
|
||
> `status` also **von der obersten Ebene** lesen, nicht aus einem `result`-Objekt.
|
||
|
||
---
|
||
|
||
## 1. Architektur & Konzepte
|
||
|
||
Das Lizenzsystem von Deploymentcenter schützt Anwendungen über eine Kombination aus serverseitiger Validierung, plattformunabhängiger Hardware-ID v2 und einem gehärteten lokalen Cache (`LLS2` format with AES-GCM encryption).
|
||
|
||
### 1.1 Hardware-ID v2 Format
|
||
Format: `2:<plattform>:<64-Hex-Zeichen>`
|
||
|
||
Beispiele:
|
||
- `2:win:9f3ab7c1...` (Windows, `HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid`)
|
||
- `2:lin:41e0d5aa...` (Linux, `/etc/machine-id`)
|
||
- `2:lin:7c9182ff...` (Linux/Container via Umgebungsvariable `DEPLOYMENTCENTER_HWID`)
|
||
|
||
### 1.2 Hash-Berechnung (KEIN MachineName im Hash)
|
||
`sha256("LicenseLabrador-HWID-v2" + "\n" + plattform + "\n" + quelle + "\n" + rohwert)`
|
||
|
||
> **WICHTIG**: Der Rechnername steckt **nicht** im Hash. Ein Umbenennen der Maschine verändert die Hardware-ID nicht und verbraucht keine zusätzlichen Aktivierungsplätze.
|
||
|
||
### 1.3 Quellen-Priorisierung je Plattform
|
||
|
||
#### Windows:
|
||
1. `HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid` (`machine-guid`)
|
||
2. Verkettete physische MAC-Adressen (`mac`)
|
||
3. Erzeugte Schlüsseldatei `machine.key` im StorageDirectory (`keyfile`)
|
||
|
||
#### Linux:
|
||
1. `/etc/machine-id` (`machine-id`) — muss plausibel sein (Länge >= 16, nicht `uninitialized`, nicht nur Nullen).
|
||
2. `/var/lib/dbus/machine-id` (`dbus-machine-id`)
|
||
3. `/sys/class/dmi/id/product_uuid` (`dmi-uuid`)
|
||
4. Verkettete physische MAC-Adressen (`mac`)
|
||
5. Erzeugte Schlüsseldatei `machine.key` (`keyfile`)
|
||
|
||
#### Container / Headless Overrides:
|
||
Wenn `DEPLOYMENTCENTER_HWID` oder `LICENSELABRADOR_HWID` gesetzt ist, gewinnt diese Variable Plattform-weit (`override`).
|
||
|
||
---
|
||
|
||
## 2. Einbindung in C# (.NET Core / .NET 8+)
|
||
|
||
Verwende das NuGet-Paket/Projekt `Deploymentcenter.Client` (Multi-Targeting `netstandard2.0;net8.0`).
|
||
|
||
### 2.1 Initialisierung und Standard-Validierung
|
||
|
||
```csharp
|
||
using System;
|
||
using System.Threading.Tasks;
|
||
using Deploymentcenter.Client;
|
||
|
||
public class Program
|
||
{
|
||
private static readonly string ServerUrl = "https://dc.mhdf.de";
|
||
private static readonly string ProductSlug = "myapp"; // In dc_projects hinterlegter Slug
|
||
|
||
public static async Task Main(string[] args)
|
||
{
|
||
// 1. CLI Schalter für Headless/Admin-Operationen abfangen
|
||
if (args.Length > 0 && args[0] == "--license-status")
|
||
{
|
||
var hwInfo = HardwareId.GetHardwareId(ProductSlug);
|
||
Console.WriteLine($"HWID v2: {hwInfo.HardwareId} ({hwInfo.HwidSource})");
|
||
return;
|
||
}
|
||
|
||
// 2. LicenseClient instanziieren
|
||
var client = new LicenseClient();
|
||
string licenseKey = "LLAB1-98A72-B3C4D-5E6F7-89012";
|
||
|
||
// 3. Online-Validierung durchführen
|
||
LicenseValidationResult res = await client.ValidateAsync(
|
||
ProductSlug, licenseKey, ServerUrl, appVersion: BuildInfo.Version);
|
||
|
||
if (res.IsValid)
|
||
{
|
||
Console.WriteLine($"[✔] Lizenz gültig! (Status: {res.Status}, Cached: {res.IsCached})");
|
||
}
|
||
else if (res.IsTransient)
|
||
{
|
||
// Kein Urteil, nur keine Verbindung. Weiterlaufen lassen.
|
||
Console.WriteLine($"[!] Lizenz vorerst nicht prüfbar: {res.Message}");
|
||
}
|
||
else
|
||
{
|
||
Console.WriteLine($"[✖] Lizenz ungültig: {res.Message}");
|
||
Environment.Exit(1);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2.2 Signatur
|
||
|
||
```csharp
|
||
Task<LicenseValidationResult> ValidateAsync(
|
||
string productSlug,
|
||
string licenseKey,
|
||
string serverBaseUrl,
|
||
string? appVersion = null,
|
||
CancellationToken cancellationToken = default);
|
||
```
|
||
|
||
`appVersion` landet in der Aktivierungsliste des WebUI. Ohne Angabe greift
|
||
`LicenseClient.DefaultAppVersion` und danach die Version der Startassembly.
|
||
Einmal beim Start setzen genügt:
|
||
|
||
```csharp
|
||
LicenseClient.DefaultAppVersion = BuildInfo.Version;
|
||
```
|
||
|
||
> Vorher stand hier fest `"1.0.0"`. In der Verwaltungsansicht trug dadurch jede
|
||
> Installation dieselbe Version, obwohl die Spalte `app_version` dafür da ist.
|
||
|
||
### 2.3 Ergebnisfelder
|
||
|
||
| Feld | Bedeutung |
|
||
|---|---|
|
||
| `IsValid` | Darf die Anwendung laufen? |
|
||
| `Status` | `valid`, `valid_offline`, `revoked`, `expired`, `not_found`, `activation_limit`, `suspended`, `cache_expired`, `clock_rollback`, `server_unavailable` |
|
||
| `IsTransient` | **Neu.** Wahr, wenn kein Urteil vorliegt, sondern nur die Verbindung scheiterte |
|
||
| `IsCached` | Ergebnis stammt aus dem Offline-Cache |
|
||
| `ExpiresAt` | Ablauf der Lizenz (Unix-Zeit) |
|
||
| `CacheExpiresAt` | **Neu.** Ende der Offline-Gnadenfrist (Unix-Zeit) |
|
||
|
||
### 2.4 Zwischengespeicherten Schlüssel lesen
|
||
|
||
```csharp
|
||
// Für --license-status: was liegt lokal vor?
|
||
string? key = LicenseClient.TryGetCachedKey(ProductSlug);
|
||
LocalCacheData? state = LicenseClient.TryGetCachedState(ProductSlug);
|
||
|
||
// Erneut prüfen, ohne den Schlüssel selbst vorhalten zu müssen
|
||
var res = await client.RevalidateAsync(ProductSlug, ServerUrl);
|
||
```
|
||
|
||
`ValidateAsync` legt den Schlüssel bei erfolgreicher Prüfung selbst im
|
||
verschlüsselten Cache ab — eine eigene Ablage braucht es dafür nicht.
|
||
|
||
---
|
||
|
||
## 3. Zustandsspeicher (`StateStore.cs`) & Cache-Härtung
|
||
|
||
- **Format**: File Envelope mit Magic `"LLS2"`, 12-Byte Nonce, AES-256-GCM Ciphertext und 16-Byte GCM Tag.
|
||
- **Schlüsselableitung**: HKDF-SHA256 aus `HardwareId` + `ProductSlug`.
|
||
- **Windows**: DPAPI-Zusatzhülle um AES-GCM Payload.
|
||
- **Linux**: Dateirechte `0600` (`chmod 600 state.dat`).
|
||
- **Sicherheitsvorgabe**: Kein Klartext-Rückfall! Beschädigte oder manipulierte Cache-Dateien werden strikt als Cache-Fehltreffer behandelt.
|
||
|
||
### 3.1 Offline-Gnadenfrist
|
||
|
||
Der Server meldet in jeder Antwort `cache_ttl_hours` — den projektspezifischen
|
||
Wert aus `dc_projects.default_cache_ttl_hours`, den du im WebUI unter
|
||
**Projekte** einstellst. Der Client trägt daraus `CacheExpiresAt` in den Cache
|
||
ein: **frühestens** die TTL, spätestens der Ablauf der Lizenz selbst.
|
||
|
||
> Vorher wurde das Feld ignoriert und stattdessen das Ablaufdatum der Lizenz als
|
||
> Cache-Ablauf eingetragen. Bei einer Lizenz bis 2040 war die Frist ohne
|
||
> Serverkontakt damit praktisch unbegrenzt — die Einstellung im WebUI blieb
|
||
> wirkungslos.
|
||
|
||
Wer eine eigene Anbindung schreibt: `cache_ttl_hours` auswerten, nicht durch
|
||
`expires_at` ersetzen.
|
||
|
||
### 3.2 Schema-Stand des Caches
|
||
|
||
`state.dat` trägt jetzt Schema 3 (zusätzlich `CacheExpiresAt`, `CacheTtlHours`).
|
||
Schema 2 wird weiterhin gelesen; die Frist wird dort aus `IssuedAt` plus der
|
||
Standard-TTL von 168 h abgeleitet. Ein SDK-Wechsel erzwingt also **keine**
|
||
Online-Prüfung.
|
||
|
||
### 3.3 Migration fremder Cache-Dateien
|
||
|
||
Eine Datei ohne `LLS2`-Kopf wird nur übernommen, wenn ihr `ProductSlug` zu
|
||
diesem Produkt passt und sie einen Lizenzschlüssel enthält.
|
||
|
||
> Vorher wurde beliebiges JSON nach `LocalCacheData` deserialisiert und sofort
|
||
> zurückgeschrieben. Passte kein einziges Feld, entstand ein Standardobjekt, das
|
||
> die ursprüngliche Datei überschrieb. Da LicenseLabrador denselben Pfad und
|
||
> Dateinamen verwendet — `GetStorageDirectory` berücksichtigt eigens
|
||
> `LICENSELABRADOR_STORAGE_DIR` —, zerstörte das dessen Cache still.
|
||
|
||
---
|
||
|
||
## 4. Kopfloser Betrieb (Headless Services / systemd)
|
||
|
||
> **Diese Schalter baut deine Anwendung selbst** — das SDK bringt keine
|
||
> CLI mit. Es liefert die Bausteine dafür; die folgende Empfehlung ist eine
|
||
> Konvention, damit alle Dienste gleich zu bedienen sind.
|
||
|
||
```bash
|
||
# Status der Hardware-ID und des lokalen Caches ausgeben
|
||
my-service --license-status
|
||
|
||
# Lizenzschlüssel festlegen & aktivieren
|
||
my-service --license-set-key LLAB1-98A72-B3C4D-5E6F7-89012
|
||
|
||
# Aktivierung für diesen Host aufheben (Freigabe am Server)
|
||
my-service --license-deactivate
|
||
```
|
||
|
||
Umsetzung mit den Bausteinen des SDK:
|
||
|
||
```csharp
|
||
var client = new LicenseClient();
|
||
|
||
switch (args[0])
|
||
{
|
||
case "--license-status":
|
||
{
|
||
var hw = HardwareId.GetHardwareId(ProductSlug);
|
||
var state = LicenseClient.TryGetCachedState(ProductSlug);
|
||
|
||
Console.WriteLine($"HWID v2: {hw.HardwareId} ({hw.HwidSource})");
|
||
Console.WriteLine($"Schlüssel: {LicenseClient.TryGetCachedKey(ProductSlug) ?? "keiner"}");
|
||
Console.WriteLine($"Cache: {(state == null ? "leer" : state.Status)}");
|
||
|
||
if (state != null && state.CacheExpiresAt > 0)
|
||
{
|
||
var until = DateTimeOffset.FromUnixTimeSeconds(state.CacheExpiresAt);
|
||
Console.WriteLine($"Offline noch bis: {until:u}");
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
case "--license-set-key":
|
||
// ValidateAsync legt den Schlüssel bei Erfolg selbst im Cache ab.
|
||
var res = await client.ValidateAsync(ProductSlug, args[1], ServerUrl);
|
||
Console.WriteLine(res.Message);
|
||
return res.IsValid ? 0 : 1;
|
||
|
||
case "--license-deactivate":
|
||
string? key = LicenseClient.TryGetCachedKey(ProductSlug);
|
||
if (key == null) { Console.Error.WriteLine("Kein Schlüssel hinterlegt."); return 1; }
|
||
|
||
bool ok = await client.DeactivateAsync(
|
||
ProductSlug, key, ServerUrl,
|
||
Environment.GetEnvironmentVariable("DC_SHARED_KEY") ?? "");
|
||
return ok ? 0 : 1;
|
||
}
|
||
```
|
||
|
||
> `TryGetCachedKey` und `TryGetCachedState` sind neu in 2.1. Vorher gab es
|
||
> keinen Weg, den Schlüssel aus dem eigenen Cache zu lesen — jede Anwendung
|
||
> musste ihn ein zweites Mal selbst ablegen oder direkt `StateStore.Load()`
|
||
> aufrufen.
|
||
|
||
### Deaktivierung braucht Authentifizierung
|
||
|
||
`/api/license/v1/deactivate` gibt einen Aktivierungsplatz frei und ist deshalb
|
||
geschützt — sonst könnte jeder fremde Installationen abmelden. Der Aufruf
|
||
verlangt den `shared_key` aus `config/config.php`:
|
||
|
||
```csharp
|
||
// Der Schlüssel gehört auf den Administrationsrechner, nicht in die
|
||
// ausgelieferte Anwendung.
|
||
var client = new LicenseClient();
|
||
bool released = await client.DeactivateAsync(
|
||
productSlug: "myapp",
|
||
licenseKey: "LLAB1-98A72-B3C4D-5E6F7-89012",
|
||
serverBaseUrl: "https://dc.mhdf.de",
|
||
authToken: Environment.GetEnvironmentVariable("DC_SHARED_KEY"));
|
||
```
|
||
|
||
```bash
|
||
curl -X POST https://dc.mhdf.de/api/license/v1/deactivate \
|
||
-H "Authorization: Bearer $DC_SHARED_KEY" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"product":"myapp","license_key":"XXXXX-...","hardware_id":"2:win:a765..."}'
|
||
```
|
||
|
||
Alternativ genügt für Einzelfälle der Knopf **Freigeben** in der Hardware-Liste
|
||
des WebUI — das ist der übliche Weg und braucht keinen Schlüssel im Feld.
|
||
|
||
> **Nur noch `Authorization: Bearer`.** Frühere SDK-Fassungen schickten denselben
|
||
> Wert zusätzlich als `X-Watchdog-Key`. Der Name führte in die Irre — hier gehört
|
||
> der `shared_key` hin, kein Watchdog-Token — und der Server prüft
|
||
> `X-Watchdog-Key` **vor** `Authorization`. Ein dort versehentlich eingetragenes
|
||
> Watchdog-Token hätte das richtige Bearer-Token stillschweigend verdrängt und
|
||
> zu einem unerklärlichen 401 geführt.
|
||
|
||
---
|
||
|
||
## 5. Migration v1 → v2 ohne Platzverlust
|
||
|
||
Wenn ein bestehender Windows-Client auf Hardware-ID v2 aktualisiert wird:
|
||
- Der Client schickt `hardware_id` (v2) **und** `legacy_hardware_id` (v1) mit.
|
||
- Der Server findet die alte Aktivierung unter `legacy_hardware_id` und zieht den Datenbank-Eintrag lautlos auf v2 um.
|
||
- Es wird kein zusätzlicher Aktivierungsplatz verbraucht!
|
||
|
||
---
|
||
|
||
## 6. Fehlerfälle richtig behandeln
|
||
|
||
**Nur ein gelesenes Urteil ist ein Urteil.** Ein HTTP-Fehler sagt nichts über
|
||
die Lizenz aus — er sagt, dass die Frage nicht beantwortet wurde.
|
||
|
||
`ValidateAsync` fällt deshalb bei **jedem** erfolglosen Aufruf auf den
|
||
Offline-Cache zurück: Netzfehler, Zeitüberschreitung, `429`, `5xx`, und auch bei
|
||
einer erfolgreichen Antwort, die sich nicht auswerten lässt (etwa der
|
||
Fehlerseite eines Proxys mit Statuscode 200).
|
||
|
||
```csharp
|
||
var res = await client.ValidateAsync(ProductSlug, key, ServerUrl);
|
||
|
||
if (res.IsValid)
|
||
{
|
||
// valid oder valid_offline — weiterarbeiten
|
||
}
|
||
else if (res.IsTransient)
|
||
{
|
||
// server_unavailable, cache_expired: kein Urteil, nur keine Auskunft.
|
||
// Warnen, weiterlaufen lassen, später erneut prüfen.
|
||
logger.Warn(res.Message);
|
||
}
|
||
else
|
||
{
|
||
// revoked, expired, not_found, activation_limit, suspended, clock_rollback
|
||
// Das ist ein Urteil. Jetzt darf beendet werden.
|
||
Environment.Exit(1);
|
||
}
|
||
```
|
||
|
||
| Status | `IsTransient` | Bedeutung |
|
||
|---|---|---|
|
||
| `valid` | – | Vom Server bestätigt |
|
||
| `valid_offline` | – | Aus dem Cache, Gnadenfrist läuft noch |
|
||
| `server_unavailable` | **ja** | Keine Verbindung **und** kein brauchbarer Cache |
|
||
| `cache_expired` | **ja** | Gnadenfrist abgelaufen, Server weiterhin stumm |
|
||
| `revoked`, `expired`, `not_found`, `activation_limit`, `suspended` | nein | Endgültig |
|
||
| `clock_rollback` | nein | Systemuhr zurückgedreht, Online-Prüfung nötig |
|
||
|
||
> **Was sich geändert hat:** Der Cache-Zweig lag zuvor ausschließlich im
|
||
> `catch`. Ein `429` oder `500` wirft aber keine Exception — die Prüfung fiel aus
|
||
> dem Erfolgszweig heraus und endete als `unknown_error`, **ohne den Cache auch
|
||
> nur zu befragen**. Ein Server-Schluckauf entzog damit die Lizenz, ein gezogenes
|
||
> Netzkabel nicht. Der Status `unknown_error` existiert nicht mehr.
|
||
|
||
### Ratenbegrenzung
|
||
|
||
`/validate` ist auf 120 Anfragen pro Minute und IP begrenzt. Darüber kommt
|
||
`429` mit `{"status":"error","error":{"code":"rate_limited"}}` — hier greift
|
||
ausnahmsweise das Umschlagformat, weil die Drosselung vor der Lizenzlogik
|
||
zuschlägt. Hinter einem NAT mit vielen Installationen ist das erreichbar. Der
|
||
Client verwendet in diesem Fall den lokalen Cache und versucht es später erneut.
|
||
|
||
### Zeitgrenze
|
||
|
||
Der intern erzeugte `HttpClient` hat jetzt 15 s statt der 100 s des .NET-Standards
|
||
— sonst stand eine Anwendung beim Start bis zu anderthalb Minuten still, wenn
|
||
der Server nicht antwortete. Ein selbst übergebener `HttpClient` wird nicht
|
||
verändert; dessen Zeitgrenze verantwortet der Aufrufer.
|