using System.Net.Http.Json; namespace MSCO.Server; /// /// Alle Server hängen am offiziellen Master (Keymaster + Serverliste). Die Adresse ist fest eingebaut, /// damit niemand die Schlüsselprüfung über einen eigenen Master umgehen kann - nicht über server.json, /// Umgebungsvariablen oder Startparameter änderbar. /// public static class MasterServer { public const string OfficialUrl = "https://msco.mbm.wtf"; public static string Url => OfficialUrl; } /// Spricht mit dem Master-Server: Lizenzschlüssel prüfen und Server in der Liste melden. public sealed class MasterClient { public sealed record KeyInfo(bool Valid, string Owner, string Label, string Error) { /// Master war nicht erreichbar (Netzwerk, Proxy-Fehler) - dann später erneut versuchen. public bool Unreachable { get; init; } } record HeartbeatBody(string Key, string Name, string Description, string Address, int Port, int Players, int MaxPlayers, string Version, bool Public, bool RequireSteam, List Tags); readonly HttpClient http = new() { Timeout = TimeSpan.FromSeconds(10) }; readonly string baseUrl; public MasterClient(string masterUrl) { baseUrl = masterUrl.TrimEnd('/'); } public async Task ValidateKeyAsync(string key) { try { var response = await http.PostAsJsonAsync(baseUrl + "/api/keys/validate", new { key }); // 5xx (z.B. 502 vom Reverse Proxy, Master gerade nicht da) = nicht erreichbar, nicht "ungültig" if ((int)response.StatusCode >= 500) return new KeyInfo(false, null, null, "HTTP " + (int)response.StatusCode) { Unreachable = true }; var info = await response.Content.ReadFromJsonAsync(); return info ?? new KeyInfo(false, null, null, "Empty response") { Unreachable = true }; } catch (Exception e) { return new KeyInfo(false, null, null, e.Message) { Unreachable = true }; } } /// /// Antwort auf den Heartbeat. Status: HTTP-Code, 0 = Master nicht erreichbar, 401 = Schlüssel ungültig. /// Reachable: ob der Master den Server von außen erreicht (null = Prüfung läuft noch). /// public sealed record HeartbeatResult(int Status, bool? Reachable, bool Listed, string Address, int Port); record HeartbeatReply(bool? Reachable, bool Listed, string Address, int Port); /// Beim Beenden: sofort aus der Serverliste nehmen (sonst erst nach dem Heartbeat-Timeout). public async Task UnregisterAsync(string key) { try { using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); await http.PostAsJsonAsync(baseUrl + "/api/servers/unregister", new { key }, cts.Token); } catch { } } public async Task HeartbeatAsync(ServerConfig config, int players, string version) { try { var body = new HeartbeatBody(config.LicenseKey, config.Name, config.Description, config.PublicAddress, config.Port, players, config.MaxPlayers, version, config.Public, config.RequireSteam, ServerConfig.NormalizeTags(config.Tags, out _)); var response = await http.PostAsJsonAsync(baseUrl + "/api/servers/heartbeat", body); int status = (int)response.StatusCode; if (!response.IsSuccessStatusCode) return new HeartbeatResult(status, null, false, null, 0); HeartbeatReply reply = null; try { reply = await response.Content.ReadFromJsonAsync(); } catch { } return new HeartbeatResult(status, reply?.Reachable, reply?.Listed ?? false, reply?.Address, reply?.Port ?? 0); } catch { return new HeartbeatResult(0, null, false, null, 0); } } }