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 ISocketTokenManager tokenManager; private readonly ICreateGameHandler createGameHandler; private readonly IJoinByCodeHandler joinByCodeHandler; private readonly IJoinGameHandler joinGameHandler; private readonly IListGamesHandler listGamesHandler; private readonly ILoadGameHandler loadGameHandler; private readonly IMoveHandler moveHandler; private readonly IValidator createGameRequestValidator; private readonly IValidator joinByCodeRequestValidator; private readonly IValidator joinGameRequestValidator; private readonly IValidator listGamesRequestValidator; private readonly IValidator loadGameRequestValidator; private readonly IValidator moveRequestValidator; public SocketService( ILogger logger, ISocketConnectionManager communicationManager, IGameboardRepository gameboardRepository, ISocketTokenManager tokenManager, ICreateGameHandler createGameHandler, IJoinByCodeHandler joinByCodeHandler, IJoinGameHandler joinGameHandler, IListGamesHandler listGamesHandler, ILoadGameHandler loadGameHandler, IMoveHandler moveHandler, IValidator createGameRequestValidator, IValidator joinByCodeRequestValidator, IValidator joinGameRequestValidator, IValidator listGamesRequestValidator, IValidator loadGameRequestValidator, IValidator moveRequestValidator ) : base() { this.logger = logger; this.communicationManager = communicationManager; this.gameboardRepository = gameboardRepository; this.tokenManager = tokenManager; this.createGameHandler = createGameHandler; this.joinByCodeHandler = joinByCodeHandler; this.joinGameHandler = joinGameHandler; this.listGamesHandler = listGamesHandler; this.loadGameHandler = loadGameHandler; this.moveHandler = moveHandler; this.createGameRequestValidator = createGameRequestValidator; this.joinByCodeRequestValidator = joinByCodeRequestValidator; this.joinGameRequestValidator = joinGameRequestValidator; this.listGamesRequestValidator = listGamesRequestValidator; this.loadGameRequestValidator = loadGameRequestValidator; this.moveRequestValidator = moveRequestValidator; } public async Task HandleSocketRequest(HttpContext context) { string? userName = null; if (context.Request.Cookies.ContainsKey(SocketController.WebSessionKey)) { // Guest account var webSessionId = Guid.Parse(context.Request.Cookies[SocketController.WebSessionKey]!); userName = (await gameboardRepository.ReadGuestUser(webSessionId))?.Name; } 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.ListGames: { var req = JsonConvert.DeserializeObject(message); if (await ValidateRequestAndReplyIfInvalid(socket, listGamesRequestValidator, req)) { await listGamesHandler.Handle(req, userName); } break; } case ClientAction.CreateGame: { var req = JsonConvert.DeserializeObject(message); if (await ValidateRequestAndReplyIfInvalid(socket, createGameRequestValidator, req)) { await createGameHandler.Handle(req, userName); } break; } 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; } case ClientAction.LoadGame: { var req = JsonConvert.DeserializeObject(message); if (await ValidateRequestAndReplyIfInvalid(socket, loadGameRequestValidator, req)) { await loadGameHandler.Handle(req, userName); } break; } case ClientAction.Move: { var req = JsonConvert.DeserializeObject(message); if (await ValidateRequestAndReplyIfInvalid(socket, moveRequestValidator, req)) { await moveHandler.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; } } }