// SVSim.BattleNode/Hosting/BattleNodeWebSocketHandler.cs using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using SVSim.BattleNode.Sessions; using SVSim.BattleNode.Wire; namespace SVSim.BattleNode.Hosting; public sealed class BattleNodeWebSocketHandler { private readonly IBattleSessionStore _store; private readonly ILogger _log; private readonly ILoggerFactory _loggerFactory; public BattleNodeWebSocketHandler(IBattleSessionStore store, ILoggerFactory loggerFactory) { _store = store; _loggerFactory = loggerFactory; _log = loggerFactory.CreateLogger(); } public async Task HandleAsync(HttpContext ctx) { if (!ctx.WebSockets.IsWebSocketRequest) { ctx.Response.StatusCode = StatusCodes.Status400BadRequest; return; } var battleId = ctx.Request.Query["BattleId"].ToString(); var encryptedViewerId = ctx.Request.Query["viewerId"].ToString(); if (string.IsNullOrEmpty(battleId) || string.IsNullOrEmpty(encryptedViewerId)) { ctx.Response.StatusCode = StatusCodes.Status400BadRequest; return; } long viewerId; try { var plain = NodeCrypto.DecryptForNode(encryptedViewerId); viewerId = long.Parse(plain); } catch (Exception ex) { _log.LogWarning(ex, "viewerId query param failed to decrypt"); ctx.Response.StatusCode = StatusCodes.Status400BadRequest; return; } var pending = _store.TryGetPending(battleId); if (pending is null || pending.ViewerId != viewerId) { _log.LogWarning("Unknown battle/viewer pair: {Bid}/{Vid}", battleId, viewerId); ctx.Response.StatusCode = StatusCodes.Status400BadRequest; return; } var ws = await ctx.WebSockets.AcceptWebSocketAsync(); _store.RemovePending(battleId); var session = new BattleSession(ws, battleId, viewerId, _loggerFactory.CreateLogger()); await session.RunAsync(ctx.RequestAborted); } }