From 1dd6a70e8dc6a995089f30a50428199e14886b05 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sun, 31 May 2026 22:34:54 -0400 Subject: [PATCH] feat(battle-node): WebSocket endpoint at /socket.io/ + DI extension methods --- .../Hosting/BattleNodeExtensions.cs | 32 ++++++++++ .../Hosting/BattleNodeWebSocketHandler.cs | 64 +++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 SVSim.BattleNode/Hosting/BattleNodeExtensions.cs create mode 100644 SVSim.BattleNode/Hosting/BattleNodeWebSocketHandler.cs diff --git a/SVSim.BattleNode/Hosting/BattleNodeExtensions.cs b/SVSim.BattleNode/Hosting/BattleNodeExtensions.cs new file mode 100644 index 0000000..adb2d78 --- /dev/null +++ b/SVSim.BattleNode/Hosting/BattleNodeExtensions.cs @@ -0,0 +1,32 @@ +// SVSim.BattleNode/Hosting/BattleNodeExtensions.cs +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using SVSim.BattleNode.Bridge; +using SVSim.BattleNode.Sessions; + +namespace SVSim.BattleNode.Hosting; + +public static class BattleNodeExtensions +{ + public static IServiceCollection AddBattleNode(this IServiceCollection services, Action? configure = null) + { + var options = new BattleNodeOptions(); + configure?.Invoke(options); + services.AddSingleton(options); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + return services; + } + + public static IApplicationBuilder UseBattleNode(this IApplicationBuilder app) + { + app.UseWebSockets(); + app.Map("/socket.io", branch => branch.Run(async ctx => + { + var handler = ctx.RequestServices.GetRequiredService(); + await handler.HandleAsync(ctx); + })); + return app; + } +} diff --git a/SVSim.BattleNode/Hosting/BattleNodeWebSocketHandler.cs b/SVSim.BattleNode/Hosting/BattleNodeWebSocketHandler.cs new file mode 100644 index 0000000..dfd1230 --- /dev/null +++ b/SVSim.BattleNode/Hosting/BattleNodeWebSocketHandler.cs @@ -0,0 +1,64 @@ +// 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); + } +}