diff --git a/SVSim.BattleNode/Hosting/BattleNodeWebSocketHandler.cs b/SVSim.BattleNode/Hosting/BattleNodeWebSocketHandler.cs index dfd1230..1a2d27a 100644 --- a/SVSim.BattleNode/Hosting/BattleNodeWebSocketHandler.cs +++ b/SVSim.BattleNode/Hosting/BattleNodeWebSocketHandler.cs @@ -27,10 +27,15 @@ public sealed class BattleNodeWebSocketHandler return; } - var battleId = ctx.Request.Query["BattleId"].ToString(); - var encryptedViewerId = ctx.Request.Query["viewerId"].ToString(); + // BestHTTP's SocketOptions.AdditionalQueryParams puts these on HTTP request HEADERS + // for the WebSocket-only transport (not on the URL query string). Real clients + // therefore send BattleId/viewerId as headers; the integration test sends them as + // query params for convenience. Check headers first, fall back to query. + var battleId = ReadCredential(ctx, "BattleId"); + var encryptedViewerId = ReadCredential(ctx, "viewerId"); if (string.IsNullOrEmpty(battleId) || string.IsNullOrEmpty(encryptedViewerId)) { + _log.LogWarning("WS upgrade missing BattleId or viewerId (header or query)."); ctx.Response.StatusCode = StatusCodes.Status400BadRequest; return; } @@ -43,7 +48,7 @@ public sealed class BattleNodeWebSocketHandler } catch (Exception ex) { - _log.LogWarning(ex, "viewerId query param failed to decrypt"); + _log.LogWarning(ex, "viewerId failed to decrypt (encryptedLen={Len})", encryptedViewerId.Length); ctx.Response.StatusCode = StatusCodes.Status400BadRequest; return; } @@ -61,4 +66,11 @@ public sealed class BattleNodeWebSocketHandler var session = new BattleSession(ws, battleId, viewerId, _loggerFactory.CreateLogger()); await session.RunAsync(ctx.RequestAborted); } + + private static string ReadCredential(HttpContext ctx, string name) + { + var header = ctx.Request.Headers[name].ToString(); + if (!string.IsNullOrEmpty(header)) return header; + return ctx.Request.Query[name].ToString(); + } } diff --git a/SVSim.EmulatedEntrypoint/Security/SteamSessionAuthentication/SteamSessionAuthenticationHandler.cs b/SVSim.EmulatedEntrypoint/Security/SteamSessionAuthentication/SteamSessionAuthenticationHandler.cs index ccab962..485bf25 100644 --- a/SVSim.EmulatedEntrypoint/Security/SteamSessionAuthentication/SteamSessionAuthenticationHandler.cs +++ b/SVSim.EmulatedEntrypoint/Security/SteamSessionAuthentication/SteamSessionAuthenticationHandler.cs @@ -35,6 +35,15 @@ public class SteamSessionAuthenticationHandler : AuthenticationHandler HandleAuthenticateAsync() { string path = Request.Path; + // WebSocket upgrades carry no body — Request.Body.Seek throws NotSupportedException + // on Kestrel's HttpRequestStream. The battle node has its own per-connection auth + // (encrypted viewerId query/header validated against the matched battle id), so the + // Steam handler has nothing to do here. Returning NoResult lets the request proceed + // unauthenticated to the WS endpoint. + if (Context.WebSockets.IsWebSocketRequest) + { + return AuthenticateResult.NoResult(); + } byte[] requestBytes; try {