Fuenf von sechs Befunden einer externen Integration. Der sechste - unsignierte Lizenzurteile - ist bestaetigt, aber bewusst nicht Teil dieses Commits. 1. Lizenzschluessel stand in der Kommandozeile Der Agent nahm --license-key nur als Argument und las keine Umgebungsvariablen. "ps" zeigt argv jedem Benutzer der Maschine - exakt die Begruendung, mit der UPGRADE.md §5 den Crontab-Weg verwirft. Damit nahm das SDK einen bereits geloesten Sicherheitsbefund zurueck. Der Agent liest jetzt DC_LICENSE_KEY, DC_DOWNLOAD_USER und DC_DOWNLOAD_PASSWORD, Umgebung vor Argument. LaunchUpdateAgent uebergibt den Schluessel nicht mehr als Argument, sondern setzt die Variable auf dem eigenen Prozess: das Kind erbt den Umgebungsblock, danach wird sie wieder entfernt. Das funktioniert auch mit UseShellExecute=true, wo sich ProcessStartInfo.Environment nicht setzen laesst. 2. --wait-timeout war nicht durchgereicht Der Agent kannte den Parameter, LaunchUpdateAgent hatte keinen dafuer - es galten fest 60 Sekunden. Eine Anwendung, die allein fuer host.StopAsync 30 Sekunden braucht, kommt damit gefaehrlich nah an die Grenze. Neu: waitTimeoutSeconds. Ausserdem ist im Quelltext und in der Doku jetzt festgehalten, dass exitCurrentApp:true ueber Environment.Exit(0) laeuft und damit finally-Bloecke und IHostApplicationLifetime uebergeht - bei offenem Zustand die falsche Wahl. 3. ILicensePrompt war tot Der Konstruktor nahm es entgegen, legte es in _prompt ab und benutzte es nirgends. Wer darauf eine headless-Story aufbaute, baute auf Sand. Neu: EnsureLicensedAsync() - zwischengespeicherten Schluessel nehmen, sonst fragen, pruefen, bei Ablehnung erneut fragen. allowPrompt:false lehnt ohne Cache ab, statt auf eine Eingabe zu warten, die im Dienst nie kommt. Ein voruebergehender Netzfehler fuehrt nicht zur erneuten Abfrage - der Schluessel ist ja nicht falsch. 4. Der Packager warnte nur Er bricht jetzt ab. Anlass war ein echter API-Schluessel in einem oeffentlich abrufbaren Paket - und die Warnung war damals ausgerechnet unterdrueckt, weil die Datei auf der preserve-Liste stand. Zwei Stufen: Dateiname (appsettings.Local.json, master.key, *.pfx, *.db, server_settings.xml) und Inhalt (gefuelltes Password=, sk-, ghp_, dc_master_, AKIA, private Schluessel). Die Inhaltspruefung findet auch Dateien mit unverdaechtigem Namen. Platzhalter loesen bewusst nicht aus: "sk-DEIN-SCHLUESSEL-HIER" haette sonst jede ausgelieferte Vorlage blockiert, und --allow-secrets waere nach einer Woche Gewohnheit. Beim Erproben zuerst genau in diese Falle gelaufen. 5. BuildInfo.targets war nur per Pfad-Import zu haben Die Anleitung empfahl einen <Import> ins Nachbar-Repository - das setzt voraus, dass beide Arbeitskopien nebeneinander liegen und in derselben Fassung stehen. Das Client-Projekt ist jetzt packbar und legt das Target unter build/ ins Paket, wo NuGet es selbst importiert. Ausserdem: Unauthorized wurde nur im statischen Zweig erkannt, im API-Zweig kam ein 401 als gewoehnlicher HTTP-Fehler an. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
615 lines
22 KiB
C#
615 lines
22 KiB
C#
using System;
|
|
using System.Net.Http;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace Deploymentcenter.Client;
|
|
|
|
public class LicenseValidationResult
|
|
{
|
|
public bool IsValid { get; set; }
|
|
public string Status { get; set; } = "unknown";
|
|
public string Message { get; set; } = string.Empty;
|
|
public string HardwareId { get; set; } = string.Empty;
|
|
public bool IsCached { get; set; }
|
|
|
|
/// <summary>Ablauf der Lizenz selbst (Unix-Zeit), nicht der des Caches.</summary>
|
|
public long? ExpiresAt { get; set; }
|
|
|
|
/// <summary>
|
|
/// Ablauf der Offline-Gnadenfrist (Unix-Zeit). Danach verlangt der Client
|
|
/// wieder eine erreichbare Gegenstelle, auch wenn die Lizenz laenger laeuft.
|
|
/// </summary>
|
|
public long? CacheExpiresAt { get; set; }
|
|
|
|
/// <summary>
|
|
/// Wahr, wenn kein Lizenzurteil vorliegt, sondern nur die Verbindung zum
|
|
/// Server gescheitert ist (Netzfehler, HTTP 429/5xx, unlesbare Antwort).
|
|
///
|
|
/// Ein solcher Zustand darf eine Anwendung nicht beenden. Nur ein vom
|
|
/// Server geliefertes Urteil (revoked, expired, not_found,
|
|
/// activation_limit, suspended) ist endgueltig.
|
|
/// </summary>
|
|
public bool IsTransient { get; set; }
|
|
}
|
|
|
|
public class LicenseClient
|
|
{
|
|
/// <summary>
|
|
/// Voreinstellung fuer app_version, wenn ein Aufrufer sie nicht je Aufruf
|
|
/// uebergibt. Einmal beim Start setzen, z. B. auf BuildInfo.Version.
|
|
/// </summary>
|
|
public static string? DefaultAppVersion { get; set; }
|
|
|
|
/// <summary>
|
|
/// Standard-Zeitgrenze fuer den intern erzeugten HttpClient. Ohne sie
|
|
/// stand eine Anwendung beim Start bis zu 100 Sekunden still, wenn der
|
|
/// Server nicht antwortete.
|
|
/// </summary>
|
|
public static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(15);
|
|
|
|
private static readonly Lazy<HttpClient> SharedHttpClient = new Lazy<HttpClient>(
|
|
() => new HttpClient { Timeout = DefaultTimeout },
|
|
LazyThreadSafetyMode.ExecutionAndPublication);
|
|
|
|
/// <summary>Standard-Gnadenfrist, wenn der Server keine TTL mitschickt.</summary>
|
|
private const int FallbackCacheTtlHours = 168;
|
|
|
|
private readonly HttpClient _httpClient;
|
|
private readonly ILicensePrompt _prompt;
|
|
|
|
/// <summary>
|
|
/// Ohne eigenen HttpClient wird eine gemeinsame Instanz mit
|
|
/// <see cref="DefaultTimeout"/> verwendet. Ein uebergebener HttpClient
|
|
/// wird nicht veraendert - dessen Zeitgrenze verantwortet der Aufrufer.
|
|
/// </summary>
|
|
public LicenseClient(HttpClient? httpClient = null, ILicensePrompt? prompt = null)
|
|
{
|
|
_httpClient = httpClient ?? SharedHttpClient.Value;
|
|
_prompt = prompt ?? new ConsoleLicensePrompt();
|
|
}
|
|
|
|
public Task<LicenseValidationResult> ValidateAsync(
|
|
string productSlug,
|
|
string licenseKey,
|
|
string serverBaseUrl,
|
|
CancellationToken cancellationToken)
|
|
=> ValidateAsync(productSlug, licenseKey, serverBaseUrl, null, cancellationToken);
|
|
|
|
/// <param name="appVersion">
|
|
/// Version der aufrufenden Anwendung. Landet in der Aktivierungsliste des
|
|
/// Deploymentcenters. Ohne Angabe wird <see cref="DefaultAppVersion"/> und
|
|
/// danach die Version der Startassembly verwendet.
|
|
/// </param>
|
|
public async Task<LicenseValidationResult> ValidateAsync(
|
|
string productSlug,
|
|
string licenseKey,
|
|
string serverBaseUrl,
|
|
string? appVersion = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var hwInfo = HardwareId.GetHardwareId(productSlug);
|
|
long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
|
|
|
string? failureReason = null;
|
|
HttpResponseMessage? response = null;
|
|
|
|
try
|
|
{
|
|
var payload = new
|
|
{
|
|
product = productSlug,
|
|
license_key = licenseKey,
|
|
hardware_id = hwInfo.HardwareId,
|
|
legacy_hardware_id = hwInfo.LegacyHardwareId,
|
|
hwid_version = hwInfo.HwidVersion,
|
|
hwid_source = hwInfo.HwidSource,
|
|
platform = hwInfo.Platform,
|
|
hostname = Environment.MachineName,
|
|
app_version = ResolveAppVersion(appVersion),
|
|
nonce = Guid.NewGuid().ToString("N")
|
|
};
|
|
|
|
string jsonStr = JsonSerializer.Serialize(payload);
|
|
var content = new StringContent(jsonStr, Encoding.UTF8, "application/json");
|
|
string endpoint = $"{serverBaseUrl.TrimEnd('/')}/api/license/v1/validate";
|
|
|
|
response = await _httpClient.PostAsync(endpoint, content, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
|
{
|
|
throw;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
failureReason = ex.Message;
|
|
}
|
|
|
|
using (response)
|
|
{
|
|
if (response != null)
|
|
{
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
string resBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
|
var verdict = TryReadVerdict(resBody, productSlug, licenseKey, hwInfo, now);
|
|
|
|
if (verdict != null)
|
|
{
|
|
return verdict;
|
|
}
|
|
|
|
// Erfolgreiche Antwort, aber kein lesbares Urteil - etwa die
|
|
// Fehlerseite eines Proxys mit Statuscode 200. Das ist keine
|
|
// Aussage ueber die Lizenz, also wird sie auch nicht als
|
|
// solche behandelt.
|
|
failureReason = "Antwort des Servers war nicht auswertbar";
|
|
}
|
|
else
|
|
{
|
|
// WICHTIG: Ein HTTP-Fehler ist kein Lizenzurteil.
|
|
//
|
|
// Frueher lag der Cache-Zweig ausschliesslich im catch.
|
|
// Ein 429 (Drosselung) oder 500 warf keine Exception,
|
|
// sondern fiel aus dem Erfolgszweig heraus und endete als
|
|
// "unknown_error" - ohne den Cache auch nur zu befragen.
|
|
// Ein Serverfehler entzog damit die Lizenz, ein gezogenes
|
|
// Netzkabel nicht. Jetzt fuehrt jeder Nicht-Erfolg in
|
|
// denselben Offline-Zweig.
|
|
failureReason = $"HTTP {(int)response.StatusCode} {response.ReasonPhrase}".Trim();
|
|
}
|
|
}
|
|
}
|
|
|
|
return OfflineFallback(productSlug, hwInfo, now, failureReason ?? "Server nicht erreichbar");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Wertet die Serverantwort aus. Liefert null, wenn sie kein lesbares
|
|
/// Lizenzurteil enthaelt - dann greift der Offline-Zweig.
|
|
/// </summary>
|
|
private static LicenseValidationResult? TryReadVerdict(
|
|
string resBody,
|
|
string productSlug,
|
|
string licenseKey,
|
|
HardwareIdResult hwInfo,
|
|
long now)
|
|
{
|
|
string status;
|
|
string message;
|
|
long? expiresAt;
|
|
int cacheTtlHours;
|
|
|
|
try
|
|
{
|
|
using var doc = JsonDocument.Parse(resBody);
|
|
var root = doc.RootElement;
|
|
|
|
if (root.ValueKind != JsonValueKind.Object || !root.TryGetProperty("status", out var sProp))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
status = sProp.GetString() ?? string.Empty;
|
|
if (string.IsNullOrWhiteSpace(status))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
message = root.TryGetProperty("message", out var mProp) ? mProp.GetString() ?? "" : "";
|
|
expiresAt = root.TryGetProperty("expires_at", out var eProp) && eProp.ValueKind == JsonValueKind.Number
|
|
? eProp.GetInt64()
|
|
: (long?)null;
|
|
|
|
// Der Server bestimmt die Laenge der Offline-Gnadenfrist je Projekt.
|
|
// Vorher wurde das Feld ignoriert und stattdessen das Ablaufdatum
|
|
// der Lizenz eingetragen - bei einer Lizenz bis 2040 war die Frist
|
|
// praktisch unbegrenzt.
|
|
cacheTtlHours = root.TryGetProperty("cache_ttl_hours", out var tProp) && tProp.ValueKind == JsonValueKind.Number
|
|
? tProp.GetInt32()
|
|
: FallbackCacheTtlHours;
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (cacheTtlHours <= 0)
|
|
{
|
|
cacheTtlHours = FallbackCacheTtlHours;
|
|
}
|
|
|
|
if (!status.Equals("valid", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return new LicenseValidationResult
|
|
{
|
|
IsValid = false,
|
|
Status = status,
|
|
Message = message,
|
|
HardwareId = hwInfo.HardwareId,
|
|
IsCached = false,
|
|
ExpiresAt = expiresAt,
|
|
IsTransient = false
|
|
};
|
|
}
|
|
|
|
// Die Gnadenfrist endet mit der TTL - spaetestens aber mit der Lizenz.
|
|
long cacheExpiresAt = now + (long)cacheTtlHours * 3600L;
|
|
if (expiresAt.HasValue && expiresAt.Value > 0 && expiresAt.Value < cacheExpiresAt)
|
|
{
|
|
cacheExpiresAt = expiresAt.Value;
|
|
}
|
|
|
|
var cache = new LocalCacheData
|
|
{
|
|
SchemaVersion = StateStore.CurrentSchemaVersion,
|
|
ProductSlug = productSlug,
|
|
LicenseKey = licenseKey,
|
|
HardwareId = hwInfo.HardwareId,
|
|
Status = "valid",
|
|
IssuedAt = now,
|
|
ExpiresAt = expiresAt ?? 0,
|
|
CacheExpiresAt = cacheExpiresAt,
|
|
CacheTtlHours = cacheTtlHours,
|
|
MaxSeenTime = now,
|
|
Checksum = hwInfo.HardwareId
|
|
};
|
|
|
|
StateStore.Save(productSlug, hwInfo.HardwareId, cache);
|
|
|
|
return new LicenseValidationResult
|
|
{
|
|
IsValid = true,
|
|
Status = status,
|
|
Message = message,
|
|
HardwareId = hwInfo.HardwareId,
|
|
IsCached = false,
|
|
ExpiresAt = expiresAt,
|
|
CacheExpiresAt = cacheExpiresAt,
|
|
IsTransient = false
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gemeinsamer Zweig fuer jeden Fall, in dem der Server kein Urteil
|
|
/// geliefert hat: Netzfehler, HTTP-Fehler, unlesbare Antwort.
|
|
/// </summary>
|
|
private static LicenseValidationResult OfflineFallback(
|
|
string productSlug,
|
|
HardwareIdResult hwInfo,
|
|
long now,
|
|
string reason)
|
|
{
|
|
var cache = StateStore.Load(productSlug, hwInfo.HardwareId);
|
|
|
|
if (cache != null && cache.Status == "valid")
|
|
{
|
|
// Rueckdrehen der Systemuhr erkennen
|
|
if (now < cache.MaxSeenTime)
|
|
{
|
|
return new LicenseValidationResult
|
|
{
|
|
IsValid = false,
|
|
Status = "clock_rollback",
|
|
Message = "System clock rollback detected! Online verification required.",
|
|
HardwareId = hwInfo.HardwareId,
|
|
IsTransient = false
|
|
};
|
|
}
|
|
|
|
long cacheExpiresAt = ResolveCacheExpiry(cache);
|
|
|
|
if (cacheExpiresAt > 0 && now > cacheExpiresAt)
|
|
{
|
|
return new LicenseValidationResult
|
|
{
|
|
IsValid = false,
|
|
Status = "cache_expired",
|
|
Message = $"Offline-Gnadenfrist abgelaufen, Server nicht erreichbar ({reason}).",
|
|
HardwareId = hwInfo.HardwareId,
|
|
ExpiresAt = cache.ExpiresAt > 0 ? cache.ExpiresAt : null,
|
|
CacheExpiresAt = cacheExpiresAt,
|
|
IsTransient = true
|
|
};
|
|
}
|
|
|
|
// Auch offline darf eine abgelaufene Lizenz nicht weiterlaufen.
|
|
if (cache.ExpiresAt > 0 && now > cache.ExpiresAt)
|
|
{
|
|
return new LicenseValidationResult
|
|
{
|
|
IsValid = false,
|
|
Status = "expired",
|
|
Message = "Cached license has expired.",
|
|
HardwareId = hwInfo.HardwareId,
|
|
ExpiresAt = cache.ExpiresAt,
|
|
IsTransient = false
|
|
};
|
|
}
|
|
|
|
cache.MaxSeenTime = now;
|
|
StateStore.Save(productSlug, hwInfo.HardwareId, cache);
|
|
|
|
return new LicenseValidationResult
|
|
{
|
|
IsValid = true,
|
|
Status = "valid_offline",
|
|
Message = $"License validated via secure offline cache ({reason}).",
|
|
HardwareId = hwInfo.HardwareId,
|
|
IsCached = true,
|
|
ExpiresAt = cache.ExpiresAt > 0 ? cache.ExpiresAt : null,
|
|
CacheExpiresAt = cacheExpiresAt,
|
|
IsTransient = false
|
|
};
|
|
}
|
|
|
|
return new LicenseValidationResult
|
|
{
|
|
IsValid = false,
|
|
Status = "server_unavailable",
|
|
Message = $"Server communication error and no valid cache available: {reason}",
|
|
HardwareId = hwInfo.HardwareId,
|
|
IsTransient = true
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Caches aus Schema 2 kennen kein CacheExpiresAt. Fuer sie wird die Frist
|
|
/// aus dem Ausstellungszeitpunkt und der Standard-TTL abgeleitet, statt
|
|
/// unbegrenzt zu gelten.
|
|
/// </summary>
|
|
private static long ResolveCacheExpiry(LocalCacheData cache)
|
|
{
|
|
if (cache.CacheExpiresAt > 0)
|
|
{
|
|
return cache.CacheExpiresAt;
|
|
}
|
|
|
|
int ttlHours = cache.CacheTtlHours > 0 ? cache.CacheTtlHours : FallbackCacheTtlHours;
|
|
return cache.IssuedAt > 0 ? cache.IssuedAt + (long)ttlHours * 3600L : 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Liest den zuletzt erfolgreich geprueften Schluessel aus dem lokalen
|
|
/// Cache. Damit laesst sich ein Schalter wie --license-status oder ein
|
|
/// stiller Neustart bauen, ohne den Schluessel ein zweites Mal abzulegen.
|
|
/// Liefert null, wenn kein brauchbarer Cache vorliegt.
|
|
/// </summary>
|
|
/// <summary>
|
|
/// Stellt sicher, dass eine gueltige Lizenz vorliegt - und fragt notfalls
|
|
/// nach dem Schluessel.
|
|
///
|
|
/// Bis Version 2.5 nahm der Konstruktor ein <see cref="ILicensePrompt"/>
|
|
/// entgegen, legte es ab und benutzte es nie. Das Versprechen der
|
|
/// Schnittstelle - Schluessel abfragen, Fehler und Erfolg melden - loeste
|
|
/// nichts ein; wer darauf eine headless-Story aufbaute, baute auf Sand.
|
|
/// Diese Methode ist die Naht, die dabei fehlte.
|
|
///
|
|
/// Ablauf: zwischengespeicherten Schluessel nehmen, sonst fragen; pruefen;
|
|
/// bei Ablehnung erneut fragen, bis <paramref name="maxAttempts"/>
|
|
/// erschoepft ist.
|
|
/// </summary>
|
|
/// <param name="allowPrompt">
|
|
/// Auf false setzen, wenn kein Mensch davorsitzt - etwa in einem Dienst.
|
|
/// Dann wird ausschliesslich der zwischengespeicherte Schluessel geprueft
|
|
/// und bei dessen Fehlen abgelehnt, statt auf eine Eingabe zu warten, die
|
|
/// nie kommt.
|
|
/// </param>
|
|
public async Task<LicenseValidationResult> EnsureLicensedAsync(
|
|
string productSlug,
|
|
string serverBaseUrl,
|
|
bool allowPrompt = true,
|
|
int maxAttempts = 3,
|
|
string? appVersion = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
string? key = TryGetCachedKey(productSlug);
|
|
LicenseValidationResult? last = null;
|
|
|
|
for (int attempt = 0; attempt < Math.Max(1, maxAttempts); attempt++)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(key))
|
|
{
|
|
if (!allowPrompt)
|
|
{
|
|
_prompt.ShowLicenseError(
|
|
"Keine Lizenz hinterlegt",
|
|
$"Fuer \"{productSlug}\" liegt kein Schluessel vor, und es kann nicht danach gefragt werden.");
|
|
|
|
return last ?? new LicenseValidationResult
|
|
{
|
|
IsValid = false,
|
|
Status = "no_license",
|
|
Message = "Kein Lizenzschluessel hinterlegt."
|
|
};
|
|
}
|
|
|
|
key = await _prompt.RequestLicenseKeyAsync(productSlug).ConfigureAwait(false);
|
|
|
|
if (string.IsNullOrWhiteSpace(key))
|
|
{
|
|
// Abbruch durch den Benutzer - erneutes Fragen waere
|
|
// Bevormundung.
|
|
return new LicenseValidationResult
|
|
{
|
|
IsValid = false,
|
|
Status = "no_license",
|
|
Message = "Es wurde kein Lizenzschluessel eingegeben."
|
|
};
|
|
}
|
|
}
|
|
|
|
last = await ValidateAsync(productSlug, key!, serverBaseUrl, appVersion, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
if (last.IsValid)
|
|
{
|
|
_prompt.ShowLicenseInfo("Lizenz gueltig", last.Message);
|
|
return last;
|
|
}
|
|
|
|
// Ein Netzwerk- oder Serverproblem ist kein Grund, den Schluessel
|
|
// erneut abzufragen - er ist ja nicht falsch.
|
|
if (last.IsTransient)
|
|
{
|
|
_prompt.ShowLicenseError("Lizenzpruefung nicht moeglich", last.Message);
|
|
return last;
|
|
}
|
|
|
|
_prompt.ShowLicenseError("Lizenz abgelehnt", last.Message);
|
|
key = null;
|
|
}
|
|
|
|
return last ?? new LicenseValidationResult
|
|
{
|
|
IsValid = false,
|
|
Status = "invalid",
|
|
Message = "Lizenzpruefung fehlgeschlagen."
|
|
};
|
|
}
|
|
|
|
public static string? TryGetCachedKey(string productSlug)
|
|
{
|
|
var cache = TryGetCachedState(productSlug);
|
|
return string.IsNullOrWhiteSpace(cache?.LicenseKey) ? null : cache!.LicenseKey;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Vollstaendiger Cache-Eintrag fuer Statusausgaben (Ablauf, Gnadenfrist,
|
|
/// Hardware-ID). Liefert null, wenn keiner vorliegt oder er nicht zu dieser
|
|
/// Maschine gehoert.
|
|
/// </summary>
|
|
public static LocalCacheData? TryGetCachedState(string productSlug)
|
|
{
|
|
var hwInfo = HardwareId.GetHardwareId(productSlug);
|
|
return StateStore.Load(productSlug, hwInfo.HardwareId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Prueft erneut mit dem zuletzt zwischengespeicherten Schluessel.
|
|
/// Liefert not_found, wenn noch nie erfolgreich aktiviert wurde.
|
|
/// </summary>
|
|
public async Task<LicenseValidationResult> RevalidateAsync(
|
|
string productSlug,
|
|
string serverBaseUrl,
|
|
string? appVersion = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
string? key = TryGetCachedKey(productSlug);
|
|
|
|
if (key == null)
|
|
{
|
|
var hwInfo = HardwareId.GetHardwareId(productSlug);
|
|
return new LicenseValidationResult
|
|
{
|
|
IsValid = false,
|
|
Status = "not_found",
|
|
Message = "Kein zwischengespeicherter Lizenzschluessel vorhanden.",
|
|
HardwareId = hwInfo.HardwareId,
|
|
IsTransient = false
|
|
};
|
|
}
|
|
|
|
return await ValidateAsync(productSlug, key, serverBaseUrl, appVersion, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
public async Task<bool> DeactivateAsync(
|
|
string productSlug,
|
|
string licenseKey,
|
|
string serverBaseUrl,
|
|
string authToken = "",
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var hwInfo = HardwareId.GetHardwareId(productSlug);
|
|
var payload = new
|
|
{
|
|
product = productSlug,
|
|
license_key = licenseKey,
|
|
hardware_id = hwInfo.HardwareId,
|
|
nonce = Guid.NewGuid().ToString("N")
|
|
};
|
|
|
|
string jsonStr = JsonSerializer.Serialize(payload);
|
|
var content = new StringContent(jsonStr, Encoding.UTF8, "application/json");
|
|
string endpoint = $"{serverBaseUrl.TrimEnd('/')}/api/license/v1/deactivate";
|
|
|
|
using var request = new HttpRequestMessage(HttpMethod.Post, endpoint)
|
|
{
|
|
Content = content
|
|
};
|
|
|
|
if (!string.IsNullOrWhiteSpace(authToken))
|
|
{
|
|
// Nur noch der Standardweg. Der zusaetzliche X-Watchdog-Key war ein
|
|
// Ueberbleibsel des alten Servers und irrefuehrend benannt: hier
|
|
// gehoert der shared_key hin, kein Watchdog-Token. Erschwerend
|
|
// prueft der Server X-Watchdog-Key VOR Authorization - ein dort
|
|
// versehentlich eingetragenes Watchdog-Token haette das richtige
|
|
// Bearer-Token stillschweigend verdraengt.
|
|
request.Headers.Add("Authorization", $"Bearer {authToken}");
|
|
}
|
|
|
|
try
|
|
{
|
|
using HttpResponseMessage response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
|
return response.IsSuccessStatusCode;
|
|
}
|
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
|
{
|
|
throw;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static string ResolveAppVersion(string? explicitVersion)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(explicitVersion))
|
|
{
|
|
return explicitVersion!.Trim();
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(DefaultAppVersion))
|
|
{
|
|
return DefaultAppVersion!.Trim();
|
|
}
|
|
|
|
// Vorher stand hier fest "1.0.0". In der Aktivierungsliste des
|
|
// Deploymentcenters trug damit jede Installation dieselbe Version,
|
|
// obwohl die Spalte app_version dafuer vorgesehen ist.
|
|
try
|
|
{
|
|
var assembly = Assembly.GetEntryAssembly();
|
|
if (assembly != null)
|
|
{
|
|
string? informational = assembly
|
|
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
|
|
|
|
if (!string.IsNullOrWhiteSpace(informational))
|
|
{
|
|
// Das SDK haengt bei manchen Buildeinstellungen "+<commit>" an.
|
|
int plus = informational!.IndexOf('+');
|
|
return plus > 0 ? informational.Substring(0, plus) : informational;
|
|
}
|
|
|
|
var version = assembly.GetName().Version;
|
|
if (version != null)
|
|
{
|
|
return version.ToString();
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// Version zu ermitteln darf eine Lizenzpruefung nie scheitern lassen.
|
|
}
|
|
|
|
return "unknown";
|
|
}
|
|
}
|