using FluentValidation; 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 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 IValidator joinByCodeRequestValidator; private readonly IValidator joinGameRequestValidator; public SocketService( ILogger logger, ISocketConnectionManager communicationManager, IGameboardRepository gameboardRepository, IGameboardManager gameboardManager, ISocketTokenCache tokenManager, IJoinByCodeHandler joinByCodeHandler, IValidator joinByCodeRequestValidator, IValidator joinGameRequestValidator ) : base() { this.logger = logger; this.communicationManager = communicationManager; this.gameboardRepository = gameboardRepository; this.gameboardManager = gameboardManager; this.tokenManager = tokenManager; this.joinByCodeHandler = joinByCodeHandler; this.joinByCodeRequestValidator = joinByCodeRequestValidator; this.joinGameRequestValidator = joinGameRequestValidator; } 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]); 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); 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 (request == null || !Enum.IsDefined(typeof(ClientAction), request.Action)) { await socket.SendTextAsync("Error: Action not recognized."); continue; } switch (request.Action) { case ClientAction.JoinByCode: { var req = JsonConvert.DeserializeObject(message); if (req != null && await ValidateRequestAndReplyIfInvalid(socket, joinByCodeRequestValidator, req)) { await joinByCodeHandler.Handle(req, userName); } break; } 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); } } 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)); await socket.SendTextAsync(errors); } return results.IsValid; } } }