using FluentValidation; using Gameboard.ShogiUI.Sockets.Controllers; using Gameboard.ShogiUI.Sockets.Extensions; using Gameboard.ShogiUI.Sockets.Managers; using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers; using Gameboard.ShogiUI.Sockets.Repositories; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket; using Gameboard.ShogiUI.Sockets.ServiceModels.Types; using Gameboard.ShogiUI.Sockets.Services.Utility; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using System; using System.Linq; using System.Net; using System.Net.WebSockets; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.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 IGameboardRepository gameboardRepository; private readonly IGameboardManager gameboardManager; private readonly ISocketTokenCache tokenManager; private readonly IJoinByCodeHandler joinByCodeHandler; private readonly IJoinGameHandler joinGameHandler; private readonly IValidator joinByCodeRequestValidator; private readonly IValidator joinGameRequestValidator; public SocketService( ILogger logger, ISocketConnectionManager communicationManager, IGameboardRepository gameboardRepository, IGameboardManager gameboardManager, ISocketTokenCache tokenManager, IJoinByCodeHandler joinByCodeHandler, IJoinGameHandler joinGameHandler, IValidator joinByCodeRequestValidator, IValidator joinGameRequestValidator ) : base() { this.logger = logger; this.communicationManager = communicationManager; this.gameboardRepository = gameboardRepository; this.gameboardManager = gameboardManager; this.tokenManager = tokenManager; this.joinByCodeHandler = joinByCodeHandler; this.joinGameHandler = joinGameHandler; this.joinByCodeRequestValidator = joinByCodeRequestValidator; this.joinGameRequestValidator = joinGameRequestValidator; } public async Task HandleSocketRequest(HttpContext context) { string? userName = null; var user = await gameboardManager.ReadUser(context.User); if (user?.WebSessionId != null) { // Guest account userName = tokenManager.GetUsername(user.WebSessionId.Value); } else if (context.Request.Query.Keys.Contains("token")) { // Microsoft account var token = Guid.Parse(context.Request.Query["token"][0]); userName = tokenManager.GetUsername(token); } if (string.IsNullOrEmpty(userName)) { context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; return; } else { var socket = await context.WebSockets.AcceptWebSocketAsync(); communicationManager.SubscribeToBroadcast(socket, userName); while (socket.State == WebSocketState.Open) { try { var message = await socket.ReceiveTextAsync(); if (string.IsNullOrWhiteSpace(message)) continue; logger.LogInformation("Request \n{0}\n", message); var request = JsonConvert.DeserializeObject(message); if (!Enum.IsDefined(typeof(ClientAction), request.Action)) { await socket.SendTextAsync("Error: Action not recognized."); continue; } switch (request.Action) { case ClientAction.JoinGame: { var req = JsonConvert.DeserializeObject(message); if (await ValidateRequestAndReplyIfInvalid(socket, joinGameRequestValidator, req)) { await joinGameHandler.Handle(req, userName); } break; } case ClientAction.JoinByCode: { var req = JsonConvert.DeserializeObject(message); if (await ValidateRequestAndReplyIfInvalid(socket, joinByCodeRequestValidator, req)) { await joinByCodeHandler.Handle(req, userName); } 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.UnsubscribeFromBroadcastAndGames(userName); return; } } public async Task ValidateRequestAndReplyIfInvalid(WebSocket socket, IValidator validator, TRequest request) { var results = validator.Validate(request); if (!results.IsValid) { var errors = string.Join('\n', results.Errors.Select(_ => _.ErrorMessage)); var message = JsonConvert.SerializeObject(new Response { Error = errors }); await socket.SendTextAsync(message); } return results.IsValid; } } }