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);
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);
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);
}
}
}