using System; using System.Net; using System.Net.Sockets; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; namespace BSO.Net { /// /// 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). /// public static class RelayClient { /// https://site/relay/... -> wss://site/relay/... 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 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); } } /// /// 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. /// public static async Task 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; } /// Daten zwischen TCP und WebSocket in beide Richtungen, bis eine Seite trennt. 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(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(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 { } } } }