using System; using System.Net.Http; using System.Text; namespace Deploymentcenter.Client { /// /// Zugangsdaten fuer die Release-Ablage. /// /// Die Verzeichnisse unter /releases/ liegen hinter HTTP-Basic-Auth. Wer /// herunterladen darf, entscheidet der Lizenzschluessel: Eine Anwendung /// mit gueltiger Lizenz versorgt sich damit selbst weiter, ohne dass /// zusaetzliche Geheimnisse verteilt werden muessten. Zuvor war die Ablage /// offen - und damit auch fuer jeden im Internet herunterladbar. /// /// Bei einer Erstinstallation gibt es noch keinen Schluessel; dort treten /// die Zugangsdaten des Installationskontos an seine Stelle. /// public sealed class ReleaseCredentials { private ReleaseCredentials(string user, string password) { User = user; Password = password; } public string User { get; } public string Password { get; } /// /// Zugang ueber den Lizenzschluessel. /// /// Der Benutzername wird aus dem Schluessel abgeleitet, das Passwort /// ist der Schluessel selbst. Der Grund liegt im htpasswd-Format: /// gehasht wird dort nur die Passwortspalte. Stuende der Schluessel /// auch als Benutzername in der Datei, waere sie eine vollstaendige /// Klartext-Kundenliste und der Hash daneben blosse Dekoration. /// /// MUSS zeichengenau mit ReleaseGuard::licenseUsername() auf dem /// Server uebereinstimmen. /// public static ReleaseCredentials? FromLicenseKey(string? licenseKey) { string key = (licenseKey ?? string.Empty).Trim(); if (key.Length == 0) return null; return new ReleaseCredentials(UsernameForLicenseKey(key), key); } /// /// Ableitung des Benutzernamens: "lic_" plus die ersten 16 Hexzeichen /// des SHA-256 ueber den Schluessel. /// public static string UsernameForLicenseKey(string licenseKey) { using var sha256 = System.Security.Cryptography.SHA256.Create(); byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes((licenseKey ?? string.Empty).Trim())); var builder = new StringBuilder("lic_", 20); for (int i = 0; i < 8; i++) { builder.Append(hash[i].ToString("x2")); } return builder.ToString(); } /// Zugang ueber ein Installationskonto. public static ReleaseCredentials? FromUser(string? user, string? password) { string u = (user ?? string.Empty).Trim(); string p = password ?? string.Empty; return u.Length == 0 ? null : new ReleaseCredentials(u, p); } /// Wert fuer den Authorization-Header. public string ToHeaderValue() { string raw = User + ":" + Password; return "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(raw)); } /// /// Haengt den Header an eine Anfrage. Ohne Zugangsdaten passiert /// nichts - eine noch ungeschuetzte Ablage bleibt damit erreichbar. /// public static void Apply(HttpRequestMessage request, ReleaseCredentials? credentials) { if (credentials == null || request == null) return; // Nicht doppelt setzen, falls der Aufrufer schon etwas mitgibt. if (request.Headers.Contains("Authorization")) return; request.Headers.TryAddWithoutValidation("Authorization", credentials.ToHeaderValue()); } /// /// Erklaerung fuer den Menschen davor, wenn der Server 401 antwortet. /// Ohne diesen Hinweis sieht ein abgelaufener Vertrag aus wie ein /// Netzwerkfehler, und man sucht an der falschen Stelle. /// public static string DescribeUnauthorized(ReleaseCredentials? credentials) { if (credentials == null) { return "Die Release-Ablage verlangt Zugangsdaten, es wurden aber keine mitgegeben. " + "Erwartet wird der Lizenzschluessel dieser Installation."; } return "Die Release-Ablage hat die Zugangsdaten abgelehnt. Moegliche Gruende: die Lizenz " + "ist abgelaufen, wurde widerrufen oder gehoert zu einem anderen Produkt."; } } }