using FluentValidation; using Shogi.Contracts.Socket; using Shogi.Contracts.Types; using Shogi.Api.Extensions; using Shogi.Api.Managers; using System.Net; using System.Net.WebSockets; using System.Text.Json; namespace Shogi.Api.Services { public interface ISocketService { Task HandleSocketRequest(HttpContext context); } /// /// Services a single websocket connection. Authenticates the socket connection, accepts messages, and sends messages. /// public class SocketService : ISocketService { private readonly ILogger logger; private readonly ISocketConnectionManager communicationManager; private readonly ISocketTokenCache tokenManager; public SocketService( ILogger logger, ISocketConnectionManager communicationManager, ISocketTokenCache tokenManager) : base() { this.logger = logger; this.communicationManager = communicationManager; this.tokenManager = tokenManager; } public async Task HandleSocketRequest(HttpContext context) { if (!context.Request.Query.Keys.Contains("token")) { context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; return; } var token = Guid.Parse(context.Request.Query["token"][0] ?? throw new InvalidOperationException("Token expected during socket connection request, but was not sent.")); var userName = tokenManager.GetUsername(token); if (string.IsNullOrEmpty(userName)) { context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; return; } var socket = await context.WebSockets.AcceptWebSocketAsync(); communicationManager.Subscribe(socket, userName); // TODO: I probably don't need this while-loop anymore? Perhaps unsubscribe when a disconnect is detected instead. while (socket.State.HasFlag(WebSocketState.Open)) { try { var message = await socket.ReceiveTextAsync(); if (string.IsNullOrWhiteSpace(message)) continue; logger.LogInformation("Request \n{0}\n", message); var request = JsonSerializer.Deserialize(message); if (request == null || !Enum.IsDefined(typeof(SocketAction), request.Action)) { await socket.SendTextAsync("Error: Action not recognized."); continue; } switch (request.Action) { default: await socket.SendTextAsync($"Received your message with action {request.Action}, but did no work."); break; } } catch (OperationCanceledException ex) { logger.LogError(ex.Message); } catch (WebSocketException ex) { logger.LogInformation($"{nameof(WebSocketException)} in {nameof(SocketConnectionManager)}."); logger.LogInformation("Probably tried writing to a closed socket."); logger.LogError(ex.Message); } communicationManager.Unsubscribe(userName); if (!socket.State.HasFlag(WebSocketState.Closed) && !socket.State.HasFlag(WebSocketState.Aborted)) { try { await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Socket closed", CancellationToken.None); } catch (Exception ex) { Console.WriteLine($"Ignored exception during socket closing. {ex.Message}"); } } } } } }