- Heartbeat an die Website, Abmelden beim Beenden (--private, --public-address, --description, --master) - Spiel-Tickets pruefen (Signatur der Website), Name aus dem Konto, --no-auth fuer LAN - IPv4 und IPv6, Protokoll 2 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
122 lines
4.6 KiB
C#
122 lines
4.6 KiB
C#
using System.Net.Http.Json;
|
|
using System.Security.Cryptography;
|
|
using System.Text.Json;
|
|
using BSO.Net;
|
|
|
|
namespace BSO.Server;
|
|
|
|
/// <summary>
|
|
/// Verbindung zur BSO-Website (wie MasterClient bei MSCO): jede Minute ein Heartbeat fuer die Serverliste, beim Beenden
|
|
/// abmelden. Ausserdem der oeffentliche Ticket-Schluessel, mit dem der Server die Konten der Spieler prueft - er wird in
|
|
/// ticket-key.xml zwischengespeichert, damit ein Neustart auch ohne Website klappt.
|
|
/// </summary>
|
|
public class MasterClient
|
|
{
|
|
public const string DefaultUrl = "https://bso.mbm.wtf";
|
|
static readonly TimeSpan Interval = TimeSpan.FromSeconds(60);
|
|
|
|
readonly string url;
|
|
readonly GameServer server;
|
|
readonly int port;
|
|
readonly bool listed;
|
|
readonly string publicAddress, description;
|
|
readonly HttpClient http = new() { Timeout = TimeSpan.FromSeconds(15) };
|
|
readonly CancellationTokenSource stop = new();
|
|
bool? lastReachable;
|
|
|
|
public RSA TicketKey { get; private set; }
|
|
static string KeyFile => Path.Combine(AppContext.BaseDirectory, "ticket-key.xml");
|
|
|
|
public MasterClient(string url, GameServer server, int port, bool listed, string publicAddress, string description)
|
|
{
|
|
this.url = url.TrimEnd('/');
|
|
this.server = server;
|
|
this.port = port;
|
|
this.listed = listed;
|
|
this.publicAddress = publicAddress;
|
|
this.description = description;
|
|
http.DefaultRequestHeaders.UserAgent.ParseAdd("BSO-Server/" + GameServer.Version);
|
|
}
|
|
|
|
/// <summary>Ticket-Schluessel von der Website - sonst der zuletzt gespeicherte.</summary>
|
|
public async Task LoadTicketKeyAsync()
|
|
{
|
|
string xml = null;
|
|
try
|
|
{
|
|
var r = await http.GetFromJsonAsync<JsonElement>(url + "/api/auth/ticket-key");
|
|
xml = r.GetProperty("key").GetString();
|
|
File.WriteAllText(KeyFile, xml);
|
|
GameServer.Log("Ticket-Schluessel von " + url + " geladen - Spieler brauchen ein BSO-Konto");
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
if (File.Exists(KeyFile))
|
|
{
|
|
xml = File.ReadAllText(KeyFile);
|
|
GameServer.Log($"Website nicht erreichbar ({e.Message}) - nehme gespeicherten Ticket-Schluessel");
|
|
}
|
|
else GameServer.Log($"Website nicht erreichbar ({e.Message}) und kein gespeicherter Ticket-Schluessel - niemand kann beitreten (zum Testen: --no-auth)");
|
|
}
|
|
if (xml == null) return;
|
|
var rsa = RSA.Create();
|
|
rsa.FromXmlString(xml);
|
|
TicketKey = rsa;
|
|
}
|
|
|
|
public void Start()
|
|
{
|
|
if (!listed) { GameServer.Log("Nicht in der Serverliste (--private) - Beitritt nur per Adresse"); return; }
|
|
_ = Task.Run(async () =>
|
|
{
|
|
while (!stop.IsCancellationRequested)
|
|
{
|
|
await Heartbeat();
|
|
try { await Task.Delay(Interval, stop.Token); } catch (TaskCanceledException) { }
|
|
}
|
|
});
|
|
}
|
|
|
|
async Task Heartbeat()
|
|
{
|
|
try
|
|
{
|
|
var res = await http.PostAsJsonAsync(url + "/api/servers/heartbeat", new
|
|
{
|
|
name = server.Name,
|
|
description,
|
|
port,
|
|
players = server.PlayerCount,
|
|
maxPlayers = server.MaxPlayers,
|
|
version = GameServer.Version,
|
|
protocol = Protocol.Version,
|
|
publicAddress,
|
|
@public = true
|
|
});
|
|
var r = await res.Content.ReadFromJsonAsync<JsonElement>();
|
|
if (!res.IsSuccessStatusCode)
|
|
{
|
|
GameServer.Log("Serverliste: " + (r.TryGetProperty("error", out var err) ? err.GetString() : res.StatusCode.ToString()));
|
|
return;
|
|
}
|
|
bool? reachable = r.GetProperty("reachable").ValueKind switch { JsonValueKind.True => true, JsonValueKind.False => false, _ => null };
|
|
if (reachable != lastReachable && reachable != null)
|
|
GameServer.Log(reachable == true
|
|
? $"In der Serverliste als {r.GetProperty("address").GetString()}"
|
|
: $"Nicht in der Serverliste: die Website erreicht {r.GetProperty("address").GetString()} nicht - Port {port}/TCP freigeben (oder --public-address)");
|
|
lastReachable = reachable ?? lastReachable;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
GameServer.Log("Serverliste nicht erreichbar: " + e.Message);
|
|
}
|
|
}
|
|
|
|
public void Stop()
|
|
{
|
|
stop.Cancel();
|
|
if (!listed) return;
|
|
try { http.PostAsJsonAsync(url + "/api/servers/unregister", new { port, publicAddress }).Wait(3000); } catch { }
|
|
}
|
|
}
|