0.3.0: Serverliste und BSO-Konten
- 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>
This commit is contained in:
+121
@@ -0,0 +1,121 @@
|
||||
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 { }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user