Previous fix used Context.WebSockets.IsWebSocketRequest, but that requires UseWebSockets() to have already run — and UseBattleNode (which calls UseWebSockets) is registered AFTER UseAuthentication in Program.cs, so the WS feature isn't installed when auth runs. Switch to reading the raw Upgrade header, which works regardless of middleware order. Also split the WS handler's "Unknown battle/viewer pair" warning into two distinct cases so we can tell unknown-BattleId from viewer-id-mismatch (which lets us see whether the bridge stored the right viewer or the client is encrypting a different id). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
88 lines
3.3 KiB
C#
88 lines
3.3 KiB
C#
// 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<BattleNodeWebSocketHandler> _log;
|
|
private readonly ILoggerFactory _loggerFactory;
|
|
|
|
public BattleNodeWebSocketHandler(IBattleSessionStore store, ILoggerFactory loggerFactory)
|
|
{
|
|
_store = store;
|
|
_loggerFactory = loggerFactory;
|
|
_log = loggerFactory.CreateLogger<BattleNodeWebSocketHandler>();
|
|
}
|
|
|
|
public async Task HandleAsync(HttpContext ctx)
|
|
{
|
|
if (!ctx.WebSockets.IsWebSocketRequest)
|
|
{
|
|
ctx.Response.StatusCode = StatusCodes.Status400BadRequest;
|
|
return;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
long viewerId;
|
|
try
|
|
{
|
|
var plain = NodeCrypto.DecryptForNode(encryptedViewerId);
|
|
viewerId = long.Parse(plain);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.LogWarning(ex, "viewerId failed to decrypt (encryptedLen={Len})", encryptedViewerId.Length);
|
|
ctx.Response.StatusCode = StatusCodes.Status400BadRequest;
|
|
return;
|
|
}
|
|
|
|
var pending = _store.TryGetPending(battleId);
|
|
if (pending is null)
|
|
{
|
|
_log.LogWarning(
|
|
"WS upgrade for unknown BattleId={Bid} (decrypted viewerId={Vid}). " +
|
|
"Bridge may not have minted this battle, or it was already consumed/expired.",
|
|
battleId, viewerId);
|
|
ctx.Response.StatusCode = StatusCodes.Status400BadRequest;
|
|
return;
|
|
}
|
|
if (pending.ViewerId != viewerId)
|
|
{
|
|
_log.LogWarning(
|
|
"WS upgrade viewer-id mismatch on BattleId={Bid}: bridge expected={Expected}, decrypted={Got}.",
|
|
battleId, pending.ViewerId, 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<BattleSession>());
|
|
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();
|
|
}
|
|
}
|