0.5.0: Lobby ueber das BSO-Relay (ohne Portfreigabe)
- Server relay:<Lobby> verbindet per WebSocket ueber eine lokale Bruecke (RelayClient) - Ticket mit Lobby, Anzeige 'Lobby von ...' - Nach Szenenwechsel Mitspieler-Modelle erst nach 3 s neu anlegen Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BSO.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// BSO-Relay (Lobby-Server ohne Portfreigabe): eine TCP-Verbindung wird ueber einen WebSocket zum Relay
|
||||
/// durchgereicht. Die Mod spricht weiter ganz normal TCP (Connection) - nur eben mit 127.0.0.1, die Bruecke
|
||||
/// schickt alles ueber das Relay. Angemeldet wird mit dem Spiel-Ticket der Lobby (Kopfzeile X-BSO-Ticket).
|
||||
/// </summary>
|
||||
public static class RelayClient
|
||||
{
|
||||
/// <summary>https://site/relay/... -> wss://site/relay/...</summary>
|
||||
public static Uri WsUri(string master, string pathAndQuery)
|
||||
{
|
||||
string b = master.TrimEnd('/');
|
||||
if (b.StartsWith("https://")) b = "wss://" + b.Substring(8);
|
||||
else if (b.StartsWith("http://")) b = "ws://" + b.Substring(7);
|
||||
return new Uri(b + pathAndQuery);
|
||||
}
|
||||
|
||||
public static async Task<ClientWebSocket> ConnectAsync(string master, string pathAndQuery, string ticket, CancellationToken ct)
|
||||
{
|
||||
var ws = new ClientWebSocket();
|
||||
ws.Options.SetRequestHeader("X-BSO-Ticket", ticket ?? "");
|
||||
ws.Options.KeepAliveInterval = TimeSpan.FromSeconds(20);
|
||||
try
|
||||
{
|
||||
await ws.ConnectAsync(WsUri(master, pathAndQuery), ct);
|
||||
return ws;
|
||||
}
|
||||
catch (WebSocketException e)
|
||||
{
|
||||
ws.Dispose();
|
||||
string msg = e.Message ?? "";
|
||||
if (msg.Contains("404")) throw new Exception("Der Server der Lobby laeuft gerade nicht");
|
||||
if (msg.Contains("403")) throw new Exception("Kein Zugang zu dieser Lobby - im Launcher neu auf SPIELEN druecken");
|
||||
throw new Exception("BSO-Relay nicht erreichbar: " + msg);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mitspieler: mit dem Lobby-Server verbinden und eine lokale Bruecke oeffnen. Gibt den Port auf 127.0.0.1 zurueck,
|
||||
/// mit dem man sich dann normal per TCP verbindet. Die Bruecke nimmt genau eine Verbindung an.
|
||||
/// </summary>
|
||||
public static async Task<int> JoinAsync(string master, string lobbyId, string ticket, CancellationToken ct)
|
||||
{
|
||||
ClientWebSocket ws = await ConnectAsync(master, "/join?lobby=" + Uri.EscapeDataString(lobbyId), ticket, ct);
|
||||
var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
int port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using var accept = new CancellationTokenSource(TimeSpan.FromSeconds(15));
|
||||
using TcpClient tcp = await listener.AcceptTcpClientAsync(accept.Token);
|
||||
listener.Stop();
|
||||
await BridgeAsync(tcp, ws);
|
||||
}
|
||||
catch { }
|
||||
finally
|
||||
{
|
||||
try { listener.Stop(); } catch { }
|
||||
ws.Dispose();
|
||||
}
|
||||
});
|
||||
return port;
|
||||
}
|
||||
|
||||
/// <summary>Daten zwischen TCP und WebSocket in beide Richtungen, bis eine Seite trennt.</summary>
|
||||
public static async Task BridgeAsync(TcpClient tcp, WebSocket ws)
|
||||
{
|
||||
tcp.NoDelay = true;
|
||||
NetworkStream stream = tcp.GetStream();
|
||||
using var cts = new CancellationTokenSource();
|
||||
|
||||
async Task TcpToWs()
|
||||
{
|
||||
var buffer = new byte[64 * 1024];
|
||||
try
|
||||
{
|
||||
int n;
|
||||
while ((n = await stream.ReadAsync(buffer, cts.Token)) > 0)
|
||||
await ws.SendAsync(new ArraySegment<byte>(buffer, 0, n), WebSocketMessageType.Binary, true, cts.Token);
|
||||
}
|
||||
catch { }
|
||||
cts.Cancel();
|
||||
}
|
||||
|
||||
async Task WsToTcp()
|
||||
{
|
||||
var buffer = new byte[64 * 1024];
|
||||
try
|
||||
{
|
||||
while (!cts.IsCancellationRequested)
|
||||
{
|
||||
var r = await ws.ReceiveAsync(new ArraySegment<byte>(buffer), cts.Token);
|
||||
if (r.MessageType == WebSocketMessageType.Close) break;
|
||||
await stream.WriteAsync(buffer, 0, r.Count, cts.Token);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
cts.Cancel();
|
||||
}
|
||||
|
||||
await Task.WhenAll(TcpToWs(), WsToTcp());
|
||||
try { tcp.Close(); } catch { }
|
||||
try { if (ws.State == WebSocketState.Open) await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); } catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-2
@@ -6,7 +6,7 @@ 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
|
||||
/// Spiel-Ticket der BSO-Website: base64url(JSON {u, id, iat, exp, lb?, ln?}) + "." + 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.
|
||||
@@ -16,6 +16,9 @@ namespace BSO.Net
|
||||
public string Username { get; private set; }
|
||||
public string AccountId { get; private set; }
|
||||
public DateTime Expires { get; private set; }
|
||||
/// <summary>Lobby, fuer die das Ticket gilt (null = keine) und ihr Name ("Lobby von ...").</summary>
|
||||
public string Lobby { get; private set; }
|
||||
public string LobbyName { get; private set; }
|
||||
public bool Expired => DateTime.UtcNow >= Expires;
|
||||
|
||||
/// <summary>Inhalt lesen, ohne die Signatur zu pruefen. null = kein Ticket / kaputt.</summary>
|
||||
@@ -32,7 +35,9 @@ namespace BSO.Net
|
||||
{
|
||||
Username = r.GetProperty("u").GetString(),
|
||||
AccountId = r.GetProperty("id").GetString(),
|
||||
Expires = DateTimeOffset.FromUnixTimeSeconds(r.GetProperty("exp").GetInt64()).UtcDateTime
|
||||
Expires = DateTimeOffset.FromUnixTimeSeconds(r.GetProperty("exp").GetInt64()).UtcDateTime,
|
||||
Lobby = r.TryGetProperty("lb", out var lb) ? lb.GetString() : null,
|
||||
LobbyName = r.TryGetProperty("ln", out var ln) ? ln.GetString() : null
|
||||
};
|
||||
}
|
||||
catch { return null; }
|
||||
|
||||
Reference in New Issue
Block a user