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:
2026-10-28 18:10:51 +01:00
co-authored by Claude Opus 5.5
parent cda31cd070
commit 4e333c8250
7 changed files with 258 additions and 11 deletions
+2 -2
View File
@@ -10,13 +10,13 @@ namespace BSO.Net
public static class Protocol
{
/// <summary>Bei jeder inkompatiblen Aenderung erhoehen - der Server lehnt andere Versionen ab.</summary>
public const int Version = 1;
public const int Version = 2; // 2: Hello mit Spiel-Ticket
public const int DefaultPort = 7777;
}
public enum MsgType : byte
{
Hello = 1, // Client -> Server: int Protokoll, string Mod-Version, string Name
Hello = 1, // Client -> Server: int Protokoll, string Mod-Version, string Name, string Spiel-Ticket (vom Launcher)
Welcome = 2, // Server -> Client: int eigene ID, string Servername, int n, je [int ID, string Name]
Reject = 3, // Server -> Client: string Grund
PlayerState = 4, // Client -> Server: PlayerState; Server -> Client: int ID + PlayerState
+62
View File
@@ -0,0 +1,62 @@
using System;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace BSO.Net
{
/// <summary>
/// Spiel-Ticket der BSO-Website: base64url(JSON {u, id, iat, exp}) + "." + base64url(RSA-SHA256-Signatur). Der Launcher
/// holt es beim Spielen und schreibt es in BSO.json, die Mod schickt es im Hello mit. Die Mod liest es nur (Name, Ablauf),
/// pruefen kann es nur der Server mit dem oeffentlichen Schluessel der Website (Verify) - wer die Mod veraendert, kommt so
/// trotzdem nicht ohne gueltiges Konto auf einen Server.
/// </summary>
public sealed class Ticket
{
public string Username { get; private set; }
public string AccountId { get; private set; }
public DateTime Expires { get; private set; }
public bool Expired => DateTime.UtcNow >= Expires;
/// <summary>Inhalt lesen, ohne die Signatur zu pruefen. null = kein Ticket / kaputt.</summary>
public static Ticket Read(string ticket)
{
try
{
if (string.IsNullOrEmpty(ticket) || ticket.Length > 4096) return null;
string[] parts = ticket.Split('.');
if (parts.Length != 2) return null;
using var doc = JsonDocument.Parse(FromB64(parts[0]));
var r = doc.RootElement;
return new Ticket
{
Username = r.GetProperty("u").GetString(),
AccountId = r.GetProperty("id").GetString(),
Expires = DateTimeOffset.FromUnixTimeSeconds(r.GetProperty("exp").GetInt64()).UtcDateTime
};
}
catch { return null; }
}
/// <summary>Signatur pruefen (Server). Gueltig und nicht abgelaufen -> Ticket, sonst null mit Grund.</summary>
public static Ticket Verify(string ticket, RSA publicKey, out string error)
{
error = null;
Ticket t = Read(ticket);
if (t == null) { error = "Kein gueltiges BSO-Konto - bitte ueber den BSO Launcher starten"; return null; }
string[] parts = ticket.Split('.');
bool ok;
try { ok = publicKey.VerifyData(Encoding.ASCII.GetBytes(parts[0]), FromB64(parts[1]), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); }
catch { ok = false; }
if (!ok) { error = "Ungueltiges Spiel-Ticket - bitte ueber den BSO Launcher starten"; return null; }
if (t.Expired) { error = "Spiel-Ticket abgelaufen - bitte im BSO Launcher neu auf SPIELEN druecken"; return null; }
return t;
}
static byte[] FromB64(string s)
{
s = s.Replace('-', '+').Replace('_', '/');
return Convert.FromBase64String(s.PadRight(s.Length + (4 - s.Length % 4) % 4, '='));
}
}
}