From 9e6a7bca2c28154820b2cb7cdc6a489d5ab808d7 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 13 Dec 2020 14:27:36 -0600 Subject: [PATCH 1/3] All the code --- .gitignore | 4 + .../Api/Messages/GetGuestToken.cs | 21 +++ .../Api/Messages/GetToken.cs | 14 ++ .../Api/Messages/PostGameInvitation.cs | 15 ++ ...board.ShogiUI.Sockets.ServiceModels.csproj | 7 + .../Socket/Interfaces/IRequest.cs | 9 + .../Socket/Interfaces/IResponse.cs | 8 + .../Socket/Messages/CreateGame.cs | 25 +++ .../Socket/Messages/ErrorResponse.cs | 16 ++ .../Socket/Messages/JoinByCode.cs | 11 ++ .../Socket/Messages/JoinGame.cs | 24 +++ .../Socket/Messages/ListGames.cs | 24 +++ .../Socket/Messages/LoadGame.cs | 25 +++ .../Socket/Messages/Move.cs | 26 +++ .../Socket/Types/ClientActionEnum.cs | 13 ++ .../Socket/Types/Coords.cs | 8 + .../Socket/Types/Game.cs | 8 + .../Socket/Types/Move.cs | 33 ++++ Gameboard.ShogiUI.Sockets.sln | 31 ++++ .../Controllers/GameController.cs | 30 ++++ .../Controllers/SocketController.cs | 63 +++++++ .../Extensions/LogMiddleware.cs | 43 +++++ .../Extensions/WebSocketExtensions.cs | 24 +++ .../Gameboard.ShogiUI.Sockets.csproj | 26 +++ .../Gameboard.ShogiUI.Sockets.csproj.user | 11 ++ .../ClientActionHandlers/CreateGameHandler.cs | 67 +++++++ .../ClientActionHandlers/IActionHandler.cs | 16 ++ .../ClientActionHandlers/JoinByCodeHandler.cs | 75 ++++++++ .../ClientActionHandlers/JoinGameHandler.cs | 54 ++++++ .../ClientActionHandlers/ListGamesHandler.cs | 51 ++++++ .../ClientActionHandlers/LoadGameHandler.cs | 63 +++++++ .../ClientActionHandlers/MoveHandler.cs | 77 ++++++++ .../Managers/SocketCommunicationManager.cs | 166 ++++++++++++++++++ .../Managers/SocketConnectionManager.cs | 44 +++++ .../Managers/SocketTokenManager.cs | 57 ++++++ .../Managers/Utility/JsonRequest.cs | 15 ++ .../Managers/Utility/Mapper.cs | 67 +++++++ .../Managers/Utility/Request.cs | 11 ++ Gameboard.ShogiUI.Sockets/Program.cs | 20 +++ .../Properties/launchSettings.json | 29 +++ .../Repositories/GameboardRepository.cs | 127 ++++++++++++++ .../Repositories/PlayerRepository.cs | 32 ++++ .../GameboardRepositoryManager.cs | 43 +++++ .../Utility/AuthenticatedHttpClient.cs | 103 +++++++++++ Gameboard.ShogiUI.Sockets/Startup.cs | 145 +++++++++++++++ .../appsettings.Development.json | 9 + Gameboard.ShogiUI.Sockets/appsettings.json | 17 ++ azure-pipelines.yml | 64 +++++++ nuget.config | 7 + 49 files changed, 1878 insertions(+) create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetGuestToken.cs create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetToken.cs create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/PostGameInvitation.cs create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IRequest.cs create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IResponse.cs create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/CreateGame.cs create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ErrorResponse.cs create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinByCode.cs create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinGame.cs create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/Move.cs create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/ClientActionEnum.cs create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Coords.cs create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs create mode 100644 Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Move.cs create mode 100644 Gameboard.ShogiUI.Sockets.sln create mode 100644 Gameboard.ShogiUI.Sockets/Controllers/GameController.cs create mode 100644 Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs create mode 100644 Gameboard.ShogiUI.Sockets/Extensions/LogMiddleware.cs create mode 100644 Gameboard.ShogiUI.Sockets/Extensions/WebSocketExtensions.cs create mode 100644 Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj create mode 100644 Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj.user create mode 100644 Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs create mode 100644 Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/IActionHandler.cs create mode 100644 Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs create mode 100644 Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs create mode 100644 Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs create mode 100644 Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs create mode 100644 Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs create mode 100644 Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs create mode 100644 Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs create mode 100644 Gameboard.ShogiUI.Sockets/Managers/SocketTokenManager.cs create mode 100644 Gameboard.ShogiUI.Sockets/Managers/Utility/JsonRequest.cs create mode 100644 Gameboard.ShogiUI.Sockets/Managers/Utility/Mapper.cs create mode 100644 Gameboard.ShogiUI.Sockets/Managers/Utility/Request.cs create mode 100644 Gameboard.ShogiUI.Sockets/Program.cs create mode 100644 Gameboard.ShogiUI.Sockets/Properties/launchSettings.json create mode 100644 Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs create mode 100644 Gameboard.ShogiUI.Sockets/Repositories/PlayerRepository.cs create mode 100644 Gameboard.ShogiUI.Sockets/Repositories/RepositoryManagers/GameboardRepositoryManager.cs create mode 100644 Gameboard.ShogiUI.Sockets/Repositories/Utility/AuthenticatedHttpClient.cs create mode 100644 Gameboard.ShogiUI.Sockets/Startup.cs create mode 100644 Gameboard.ShogiUI.Sockets/appsettings.Development.json create mode 100644 Gameboard.ShogiUI.Sockets/appsettings.json create mode 100644 azure-pipelines.yml create mode 100644 nuget.config diff --git a/.gitignore b/.gitignore index b24d71e..26787d3 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ Thumbs.db *.app *.exe *.war +*.vs # Large media files *.mp4 @@ -48,3 +49,6 @@ Thumbs.db *.mov *.wmv +#Luke +bin +obj diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetGuestToken.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetGuestToken.cs new file mode 100644 index 0000000..81d3dcd --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetGuestToken.cs @@ -0,0 +1,21 @@ +using System; + +namespace AspShogiSockets.ServiceModels.Api.Messages +{ + public class GetGuestToken + { + public string ClientId { get; set; } + } + + public class GetGuestTokenResponse + { + public string ClientId { get; } + public Guid OneTimeToken { get; } + + public GetGuestTokenResponse(string clientId, Guid token) + { + ClientId = clientId; + OneTimeToken = token; + } + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetToken.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetToken.cs new file mode 100644 index 0000000..bfcc716 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetToken.cs @@ -0,0 +1,14 @@ +using System; + +namespace Websockets.ServiceModels +{ + public class GetTokenResponse + { + public Guid OneTimeToken { get; } + + public GetTokenResponse(Guid token) + { + OneTimeToken = token; + } + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/PostGameInvitation.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/PostGameInvitation.cs new file mode 100644 index 0000000..6cec25a --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/PostGameInvitation.cs @@ -0,0 +1,15 @@ +namespace AspShogiSockets.ServiceModels.Api.Messages +{ + public class PostGameInvitation + { + public string SessionName { get; set; } + } + public class PostGameInvitationResponse + { + public string Code { get; } + public PostGameInvitationResponse(string code) + { + Code = code; + } + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj b/Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj new file mode 100644 index 0000000..cb63190 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj @@ -0,0 +1,7 @@ + + + + netcoreapp3.1 + + + diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IRequest.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IRequest.cs new file mode 100644 index 0000000..dc174c7 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IRequest.cs @@ -0,0 +1,9 @@ +using Websockets.ServiceModels.Types; + +namespace Websockets.ServiceModels.Interfaces +{ + public interface IRequest + { + ClientAction Action { get; set; } + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IResponse.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IResponse.cs new file mode 100644 index 0000000..3712e66 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IResponse.cs @@ -0,0 +1,8 @@ +namespace Websockets.ServiceModels.Interfaces +{ + public interface IResponse + { + string Action { get; } + string Error { get; set; } + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/CreateGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/CreateGame.cs new file mode 100644 index 0000000..43603d7 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/CreateGame.cs @@ -0,0 +1,25 @@ +using Websockets.ServiceModels.Interfaces; +using Websockets.ServiceModels.Types; + +namespace Websockets.ServiceModels.Messages +{ + public class CreateGameRequest : IRequest + { + public ClientAction Action { get; set; } + public string GameName { get; set; } + public bool IsPrivate { get; set; } + } + + public class CreateGameResponse : IResponse + { + public string Action { get; private set; } + public string Error { get; set; } + public Game Game { get; set; } + public string PlayerName { get; set; } + + public CreateGameResponse(ClientAction action) + { + Action = action.ToString(); + } + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ErrorResponse.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ErrorResponse.cs new file mode 100644 index 0000000..9875612 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ErrorResponse.cs @@ -0,0 +1,16 @@ +using Websockets.ServiceModels.Interfaces; +using Websockets.ServiceModels.Types; + +namespace Websockets.ServiceModels.Messages +{ + public class ErrorResponse : IResponse + { + public string Action { get; private set; } + public string Error { get; set; } + + public ErrorResponse(ClientAction action) + { + Action = action.ToString(); + } + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinByCode.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinByCode.cs new file mode 100644 index 0000000..b0d20b8 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinByCode.cs @@ -0,0 +1,11 @@ +using Websockets.ServiceModels.Interfaces; +using Websockets.ServiceModels.Types; + +namespace Websockets.ServiceModels.Messages +{ + public class JoinByCode : IRequest + { + public ClientAction Action { get; set; } + public string JoinCode { get; set; } + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinGame.cs new file mode 100644 index 0000000..8a2cd08 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinGame.cs @@ -0,0 +1,24 @@ +using Websockets.ServiceModels.Interfaces; +using Websockets.ServiceModels.Types; + +namespace Websockets.ServiceModels.Messages +{ + public class JoinGameRequest : IRequest + { + public ClientAction Action { get; set; } + public string GameName { get; set; } + } + + public class JoinGameResponse : IResponse + { + public string Action { get; private set; } + public string Error { get; set; } + public string GameName { get; set; } + public string PlayerName { get; set; } + + public JoinGameResponse(ClientAction action) + { + Action = action.ToString(); + } + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs new file mode 100644 index 0000000..ebb3691 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +using Websockets.ServiceModels.Interfaces; +using Websockets.ServiceModels.Types; + +namespace Websockets.ServiceModels.Messages +{ + public class ListGamesRequest : IRequest + { + public ClientAction Action { get; set; } + } + + public class ListGamesResponse : IResponse + { + public string Action { get; private set; } + public string Error { get; set; } + public IEnumerable Games { get; set; } + + public ListGamesResponse(ClientAction action) + { + Action = action.ToString(); + } + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs new file mode 100644 index 0000000..e9b1ee7 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Websockets.ServiceModels.Interfaces; +using Websockets.ServiceModels.Types; + +namespace Websockets.ServiceModels.Messages +{ + public class LoadGameRequest : IRequest + { + public ClientAction Action { get; set; } + public string GameName { get; set; } + } + + public class LoadGameResponse : IResponse + { + public string Action { get; private set; } + public Game Game { get; set; } + public IEnumerable Moves { get; set; } + public string Error { get; set; } + + public LoadGameResponse(ClientAction action) + { + Action = action.ToString(); + } + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/Move.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/Move.cs new file mode 100644 index 0000000..eb5d8eb --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/Move.cs @@ -0,0 +1,26 @@ +using Websockets.ServiceModels.Interfaces; +using Websockets.ServiceModels.Types; + +namespace Websockets.ServiceModels.Messages +{ + public class MoveRequest : IRequest + { + public ClientAction Action { get; set; } + public string GameName { get; set; } + public Move Move { get; set; } + } + + public class MoveResponse : IResponse + { + public string Action { get; } + public string Error { get; set; } + public string GameName { get; set; } + public Move Move { get; set; } + public string PlayerName { get; set; } + + public MoveResponse(ClientAction action) + { + Action = action.ToString(); + } + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/ClientActionEnum.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/ClientActionEnum.cs new file mode 100644 index 0000000..f921e73 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/ClientActionEnum.cs @@ -0,0 +1,13 @@ +namespace Websockets.ServiceModels.Types +{ + public enum ClientAction + { + ListGames, + CreateGame, + JoinGame, + JoinByCode, + LoadGame, + Move, + KeepAlive + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Coords.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Coords.cs new file mode 100644 index 0000000..1588e7c --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Coords.cs @@ -0,0 +1,8 @@ +namespace Websockets.ServiceModels.Types +{ + public class Coords + { + public int X { get; set; } + public int Y { get; set; } + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs new file mode 100644 index 0000000..4da5eac --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs @@ -0,0 +1,8 @@ +namespace Websockets.ServiceModels.Types +{ + public class Game + { + public string GameName { get; set; } + public string[] Players { get; set; } + } +} diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Move.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Move.cs new file mode 100644 index 0000000..17693d4 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Move.cs @@ -0,0 +1,33 @@ +namespace Websockets.ServiceModels.Types +{ + public class Move + { + public string PieceFromCaptured { get; set; } + public Coords From { get; set; } + public Coords To { get; set; } + public bool IsPromotion { get; set; } + + /// + /// Toggles perspective of this move. (ie from player 1 to player 2) + /// + public static Move ConvertPerspective(Move m) + { + var convertedMove = new Move + { + To = new Coords + { + X = 8 - m.To.X, + Y = 8 - m.To.Y + }, + From = new Coords + { + X = 8 - m.From.X, + Y = 8 - m.From.Y + }, + IsPromotion = m.IsPromotion, + PieceFromCaptured = m.PieceFromCaptured + }; + return convertedMove; + } + } +} diff --git a/Gameboard.ShogiUI.Sockets.sln b/Gameboard.ShogiUI.Sockets.sln new file mode 100644 index 0000000..85cdd75 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30503.244 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.Sockets", "Gameboard.ShogiUI.Sockets\Gameboard.ShogiUI.Sockets.csproj", "{4FF35F9D-E525-46CF-A8A6-A147FE50AD68}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.Sockets.ServiceModels", "Gameboard.ShogiUI.Sockets.ServiceModels\Gameboard.ShogiUI.Sockets.ServiceModels.csproj", "{FE775DE4-50F0-4C5D-AD2B-01320B1E7086}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4FF35F9D-E525-46CF-A8A6-A147FE50AD68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4FF35F9D-E525-46CF-A8A6-A147FE50AD68}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4FF35F9D-E525-46CF-A8A6-A147FE50AD68}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4FF35F9D-E525-46CF-A8A6-A147FE50AD68}.Release|Any CPU.Build.0 = Release|Any CPU + {FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1D0B04F2-0DA1-4CB4-A82A-5A1C3B52ACEB} + EndGlobalSection +EndGlobal diff --git a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs new file mode 100644 index 0000000..414be02 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs @@ -0,0 +1,30 @@ +using AspShogiSockets.ServiceModels.Api.Messages; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Linq; +using System.Threading.Tasks; +using Websockets.Repositories; + +namespace AspShogiSockets.Controllers +{ + [Authorize] + [ApiController] + [Route("[controller]")] + public class GameController : ControllerBase + { + private readonly IGameboardRepository gameboardRepository; + + public GameController(IGameboardRepository gameboardRepository) + { + this.gameboardRepository = gameboardRepository; + } + + [Route("JoinCode")] + public async Task PostGameInvitation([FromBody] PostGameInvitation request) + { + var userName = HttpContext.User.Claims.First(c => c.Type == "preferred_username").Value; + var code = (await gameboardRepository.PostJoinCode(request.SessionName, userName)).JoinCode; + return new CreatedResult("", new PostGameInvitationResponse(code)); + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs new file mode 100644 index 0000000..51058e6 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs @@ -0,0 +1,63 @@ +using AspShogiSockets.Repositories.RepositoryManagers; +using AspShogiSockets.ServiceModels.Api.Messages; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Linq; +using System.Threading.Tasks; +using Websockets.Managers; +using Websockets.Repositories; +using Websockets.ServiceModels; + +namespace Websockets.Controllers +{ + [Authorize] + [Route("[controller]")] + [ApiController] + public class SocketController : ControllerBase + { + private readonly ISocketTokenManager tokenManager; + private readonly IGameboardRepository gameboardRepository; + private readonly IGameboardRepositoryManager gameboardManager; + + public SocketController( + ISocketTokenManager tokenManager, + IGameboardRepository gameboardRepository, + IGameboardRepositoryManager gameboardManager) + { + this.tokenManager = tokenManager; + this.gameboardRepository = gameboardRepository; + this.gameboardManager = gameboardManager; + } + + [Route("Token")] + public IActionResult GetToken() + { + var userName = HttpContext.User.Claims.First(c => c.Type == "preferred_username").Value; + var token = tokenManager.GenerateToken(userName); + return new JsonResult(new GetTokenResponse(token)); + } + + [AllowAnonymous] + [Route("GuestToken")] + public async Task GetGuestToken([FromQuery] GetGuestToken request) + { + if (request.ClientId == null) + { + var clientId = await gameboardManager.CreateGuestUser(); + var token = tokenManager.GenerateToken(clientId); + return new JsonResult(new GetGuestTokenResponse(clientId, token)); + } + else + { + var response = await gameboardRepository.GetPlayer(request.ClientId); + if (response != null && response.Player != null) + { + var token = tokenManager.GenerateToken(response.Player.Name); + return new JsonResult(new GetGuestTokenResponse(response.Player.Name, token)); + } + } + return new UnauthorizedResult(); + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Extensions/LogMiddleware.cs b/Gameboard.ShogiUI.Sockets/Extensions/LogMiddleware.cs new file mode 100644 index 0000000..3c39341 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Extensions/LogMiddleware.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; + +namespace Gameboard.ShogiUI.Sockets.Extensions +{ + public class LogMiddleware + { + private readonly RequestDelegate next; + private readonly ILogger logger; + + public LogMiddleware(RequestDelegate next, ILoggerFactory factory) + { + this.next = next; + logger = factory.CreateLogger(); + } + + public async Task Invoke(HttpContext context) + { + try + { + await next(context); + } + finally + { + logger.LogInformation("Request {method} {url} => {statusCode}", + context.Request?.Method, + context.Request?.Path.Value, + context.Response?.StatusCode); + } + } + } + + public static class IApplicationBuilderExtensions + { + public static IApplicationBuilder UseRequestResponseLogging(this IApplicationBuilder builder) + { + builder.UseMiddleware(); + return builder; + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Extensions/WebSocketExtensions.cs b/Gameboard.ShogiUI.Sockets/Extensions/WebSocketExtensions.cs new file mode 100644 index 0000000..a97f75b --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Extensions/WebSocketExtensions.cs @@ -0,0 +1,24 @@ +using System; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace AspShogiSockets.Extensions +{ + public static class WebSocketExtensions + { + public static async Task SendTextAsync(this WebSocket self, string message) + { + await self.SendAsync(Encoding.UTF8.GetBytes(message), WebSocketMessageType.Text, true, CancellationToken.None); + } + + public static async Task ReceiveTextAsync(this WebSocket self) + { + var buffer = new ArraySegment(new byte[2048]); + var receive = await self.ReceiveAsync(buffer, CancellationToken.None); + return Encoding.UTF8.GetString(buffer.Slice(0, receive.Count)); + // TODO: Make this robust to multi-frame messages. + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj new file mode 100644 index 0000000..e12061c --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj @@ -0,0 +1,26 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + + + + + + + + + + + diff --git a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj.user b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj.user new file mode 100644 index 0000000..d757c8c --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj.user @@ -0,0 +1,11 @@ + + + + ApiControllerEmptyScaffolder + root/Controller + AspShogiSockets + + + ProjectDebugger + + \ No newline at end of file diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs new file mode 100644 index 0000000..f8e5fc0 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs @@ -0,0 +1,67 @@ +using AspShogiSockets.Extensions; +using Gameboard.Shogi.Api.ServiceModels.Messages; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System.Net.WebSockets; +using System.Threading.Tasks; +using Websockets.Repositories; +using Websockets.ServiceModels.Messages; +using Websockets.ServiceModels.Types; + +namespace Websockets.Managers.ClientActionHandlers +{ + public class CreateGameHandler : IActionHandler + { + private readonly ILogger logger; + private readonly IGameboardRepository repository; + private readonly ISocketCommunicationManager communicationManager; + + public CreateGameHandler( + ILogger logger, + ISocketCommunicationManager communicationManager, + IGameboardRepository repository) + { + this.logger = logger; + this.repository = repository; + this.communicationManager = communicationManager; + } + + public async Task Handle(WebSocket socket, string json, string userName) + { + logger.LogInformation("Socket Request \n{0}\n", new[] { json }); + var request = JsonConvert.DeserializeObject(json); + var postGameResponse = await repository.PostGame(new PostGame + { + GameName = request.GameName, + PlayerName = userName, // TODO : Investigate if needed by UI + IsPrivate = request.IsPrivate + }); + + var response = new CreateGameResponse(request.Action) + { + PlayerName = userName, + Game = new Game + { + GameName = postGameResponse.GameName, + Players = new string[] { userName } + } + }; + + if (string.IsNullOrWhiteSpace(postGameResponse.GameName)) + { + response.Error = "Game already exists."; + } + + var serialized = JsonConvert.SerializeObject(response); + logger.LogInformation("Socket Response \n{0}\n", new[] { serialized }); + if (request.IsPrivate) + { + await socket.SendTextAsync(serialized); + } + else + { + await communicationManager.BroadcastToAll(serialized); + } + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/IActionHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/IActionHandler.cs new file mode 100644 index 0000000..13cd3db --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/IActionHandler.cs @@ -0,0 +1,16 @@ +using System.Net.WebSockets; +using System.Threading.Tasks; +using Websockets.ServiceModels.Types; + +namespace Websockets.Managers.ClientActionHandlers +{ + public interface IActionHandler + { + /// + /// Responsible for parsing json and handling the request. + /// + Task Handle(WebSocket socket, string json, string userName); + } + + public delegate IActionHandler ActionHandlerResolver(ClientAction action); +} diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs new file mode 100644 index 0000000..56cedf1 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs @@ -0,0 +1,75 @@ +using AspShogiSockets.Extensions; +using Gameboard.Shogi.Api.ServiceModels.Messages; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System.Net.WebSockets; +using System.Threading.Tasks; +using Websockets.Repositories; +using Websockets.ServiceModels.Messages; +using Websockets.ServiceModels.Types; + +namespace Websockets.Managers.ClientActionHandlers +{ + public class JoinByCodeHandler : IActionHandler + { + private readonly ILogger logger; + private readonly IGameboardRepository repository; + private readonly ISocketCommunicationManager communicationManager; + + public JoinByCodeHandler( + ILogger logger, + ISocketCommunicationManager communicationManager, + IGameboardRepository repository) + { + this.logger = logger; + this.repository = repository; + this.communicationManager = communicationManager; + } + + public async Task Handle(WebSocket socket, string json, string userName) + { + logger.LogInformation("Socket Request \n{0}\n", new[] { json }); + var request = JsonConvert.DeserializeObject(json); + var joinGameResponse = await repository.PostJoinByCode(new PostJoinByCode + { + PlayerName = userName, + JoinCode = request.JoinCode + }); + + if (joinGameResponse.JoinSucceeded) + { + var gameName = (await repository.GetGame(joinGameResponse.GameName)).GameName; + + // Other members of the game see a regular JoinGame occur. + var response = new JoinGameResponse(ClientAction.JoinGame) + { + PlayerName = userName, + GameName = gameName + }; + var serialized = JsonConvert.SerializeObject(response); + await communicationManager.BroadcastToGame(gameName, serialized); + communicationManager.SubscribeToGame(socket, gameName, userName); + + // But the player joining sees the JoinByCode occur. + response = new JoinGameResponse(ClientAction.JoinByCode) + { + PlayerName = userName, + GameName = gameName + }; + serialized = JsonConvert.SerializeObject(response); + await socket.SendTextAsync(serialized); + } + else + { + var response = new JoinGameResponse(ClientAction.JoinByCode) + { + PlayerName = userName, + Error = "Error joining game." + }; + var serialized = JsonConvert.SerializeObject(response); + logger.LogInformation("Socket Response \n{0}\n", new[] { serialized }); + await socket.SendTextAsync(serialized); + } + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs new file mode 100644 index 0000000..6fd209e --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs @@ -0,0 +1,54 @@ +using Gameboard.Shogi.Api.ServiceModels.Messages; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System.Net.WebSockets; +using System.Threading.Tasks; +using Websockets.Repositories; +using Websockets.ServiceModels.Messages; +using Websockets.ServiceModels.Types; + +namespace Websockets.Managers.ClientActionHandlers +{ + public class JoinGameHandler : IActionHandler + { + private readonly ILogger logger; + private readonly IGameboardRepository gameboardRepository; + private readonly ISocketCommunicationManager communicationManager; + public JoinGameHandler( + ILogger logger, + ISocketCommunicationManager communicationManager, + IGameboardRepository gameboardRepository) + { + this.logger = logger; + this.gameboardRepository = gameboardRepository; + this.communicationManager = communicationManager; + } + + public async Task Handle(WebSocket socket, string json, string userName) + { + logger.LogInformation("Socket Request \n{0}\n", new[] { json }); + var request = JsonConvert.DeserializeObject(json); + var response = new JoinGameResponse(ClientAction.JoinGame) + { + PlayerName = userName + }; + + var joinGameResponse = await gameboardRepository.PostJoinGame(request.GameName, new PostJoinGame + { + PlayerName = userName + }); + + if (joinGameResponse.JoinSucceeded) + { + response.GameName = request.GameName; + } + else + { + response.Error = "Game is full or code is incorrect."; + } + var serialized = JsonConvert.SerializeObject(response); + logger.LogInformation("Socket Response \n{0}\n", new[] { serialized }); + await communicationManager.BroadcastToAll(serialized); + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs new file mode 100644 index 0000000..3957910 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs @@ -0,0 +1,51 @@ +using AspShogiSockets.Extensions; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System.Linq; +using System.Net.WebSockets; +using System.Threading.Tasks; +using Websockets.Repositories; +using Websockets.ServiceModels.Messages; +using Websockets.ServiceModels.Types; + +namespace Websockets.Managers.ClientActionHandlers +{ + public class ListGamesHandler : IActionHandler + { + private readonly ILogger logger; + private readonly IGameboardRepository repository; + + public ListGamesHandler( + ILogger logger, + IGameboardRepository repository) + { + this.logger = logger; + this.repository = repository; + } + + public async Task Handle(WebSocket socket, string json, string userName) + { + logger.LogInformation("Socket Request \n{0}\n", new[] { json }); + var request = JsonConvert.DeserializeObject(json); + var getGamesResponse = string.IsNullOrWhiteSpace(userName) + ? await repository.GetGames() + : await repository.GetGames(userName); + + var games = getGamesResponse.Games + .OrderBy(g => g.Players.Contains(userName)) + .Select(g => new Game + { + GameName = g.GameName, + Players = g.Players + }); + var response = new ListGamesResponse(ClientAction.ListGames) + { + Games = games ?? new Game[0] + }; + + var serialized = JsonConvert.SerializeObject(response); + logger.LogInformation("Socket Response \n{0}\n", new[] { serialized }); + await socket.SendTextAsync(serialized); + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs new file mode 100644 index 0000000..6eb674c --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs @@ -0,0 +1,63 @@ +using AspShogiSockets.Extensions; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System.Linq; +using System.Net.WebSockets; +using System.Threading.Tasks; +using Websockets.Managers.Utility; +using Websockets.Repositories; +using Websockets.ServiceModels.Messages; +using Websockets.ServiceModels.Types; + +namespace Websockets.Managers.ClientActionHandlers +{ + public class LoadGameHandler : IActionHandler + { + private readonly ILogger logger; + private readonly IGameboardRepository gameboardRepository; + private readonly ISocketCommunicationManager communicationManager; + + public LoadGameHandler( + ILogger logger, + ISocketCommunicationManager communicationManager, + IGameboardRepository gameboardRepository) + { + this.logger = logger; + this.gameboardRepository = gameboardRepository; + this.communicationManager = communicationManager; + } + + public async Task Handle(WebSocket socket, string json, string userName) + { + logger.LogInformation("Socket Request \n{0}\n", json); + var request = JsonConvert.DeserializeObject(json); + var response = new LoadGameResponse(ClientAction.LoadGame); + var getGameResponse = await gameboardRepository.GetGame(request.GameName); + var getMovesResponse = await gameboardRepository.GetMoves(request.GameName); + + if (getGameResponse == null || getMovesResponse == null) + { + response.Error = $"Could not find game."; + } + else + { + var player1 = getGameResponse.Players[0]; + response.Game = new Game + { + GameName = getGameResponse.GameName, + Players = getGameResponse.Players + }; + + response.Moves = userName.Equals(player1) + ? getMovesResponse.Moves.Select(_ => Mapper.Map(_)) + : getMovesResponse.Moves.Select(_ => Move.ConvertPerspective(Mapper.Map(_))); + + communicationManager.SubscribeToGame(socket, getGameResponse.GameName, userName); + } + + var serialized = JsonConvert.SerializeObject(response); + logger.LogInformation("Socket Response \n{0}\n", serialized); + await socket.SendTextAsync(serialized); + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs new file mode 100644 index 0000000..13759b4 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs @@ -0,0 +1,77 @@ +using AspShogiSockets.Extensions; +using Gameboard.Shogi.Api.ServiceModels.Messages; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System.Net.WebSockets; +using System.Threading.Tasks; +using Websockets.Managers.Utility; +using Websockets.Repositories; +using Websockets.ServiceModels.Messages; +using Websockets.ServiceModels.Types; + +namespace Websockets.Managers.ClientActionHandlers +{ + public class MoveHandler : IActionHandler + { + private readonly ILogger logger; + private readonly IGameboardRepository gameboardRepository; + private readonly ISocketCommunicationManager communicationManager; + public MoveHandler( + ILogger logger, + ISocketCommunicationManager communicationManager, + IGameboardRepository gameboardRepository) + { + this.logger = logger; + this.gameboardRepository = gameboardRepository; + this.communicationManager = communicationManager; + } + + public async Task Handle(WebSocket socket, string json, string userName) + { + logger.LogInformation("Socket Request \n{0}\n", new[] { json }); + var request = JsonConvert.DeserializeObject(json); + // Basic move validation + var move = request.Move; + if (move.To.Equals(move.From)) + { + var serialized = JsonConvert.SerializeObject( + new ErrorResponse(ClientAction.Move) + { + Error = "Error: moving piece from tile to the same tile." + }); + await socket.SendTextAsync(serialized); + return; + } + + var getGameResponse = await gameboardRepository.GetGame(request.GameName); + var isPlayer1 = userName.Equals(getGameResponse.Players[0]); + if (!isPlayer1) + { + // Convert the move coords to player1 perspective. + move = Move.ConvertPerspective(move); + } + + await gameboardRepository.PostMove( + request.GameName, + new PostMove { Move = Mapper.Map(move) }); + + var response = new MoveResponse(ClientAction.Move) + { + GameName = request.GameName, + PlayerName = userName + }; + await communicationManager.BroadcastToGame( + request.GameName, + (playerName, sslStream) => + { + response.Move = playerName.Equals(userName) + ? request.Move + : Move.ConvertPerspective(request.Move); + var serialized = JsonConvert.SerializeObject(response); + logger.LogInformation("Socket Response \n{0}\n", new[] { serialized }); + return serialized; + } + ); + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs new file mode 100644 index 0000000..88a13f2 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs @@ -0,0 +1,166 @@ +using AspShogiSockets.Extensions; +using AspShogiSockets.Managers.Utility; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net.WebSockets; +using System.Threading.Tasks; +using Websockets.Managers.ClientActionHandlers; +using Websockets.ServiceModels.Types; + +namespace Websockets.Managers +{ + public interface ISocketCommunicationManager + { + Task CommunicateWith(WebSocket w, string s); + Task BroadcastToAll(string msg); + Task BroadcastToGame(string gameName, Func msgBuilder); + Task BroadcastToGame(string gameName, string msg); + void SubscribeToGame(WebSocket socket, string gameName, string playerName); + void SubscribeToBroadcast(WebSocket socket, string playerName); + void UnsubscribeFromBroadcastAndGames(string playerName); + void UnsubscribeFromGame(string gameName, string playerName); + } + + public class SocketCommunicationManager : ISocketCommunicationManager + { + private readonly ConcurrentDictionary connections; + private readonly ConcurrentDictionary> gameSeats; + private readonly ILogger logger; + private readonly ActionHandlerResolver handlerResolver; + + public SocketCommunicationManager( + ILogger logger, + ActionHandlerResolver handlerResolver) + { + this.logger = logger; + this.handlerResolver = handlerResolver; + connections = new ConcurrentDictionary(); + gameSeats = new ConcurrentDictionary>(); + } + + public async Task CommunicateWith(WebSocket socket, string userName) + { + SubscribeToBroadcast(socket, userName); + + while (!socket.CloseStatus.HasValue) + { + try + { + var message = await socket.ReceiveTextAsync(); + if (string.IsNullOrWhiteSpace(message)) continue; + + var request = JsonConvert.DeserializeObject(message); + if (!Enum.IsDefined(typeof(ClientAction), request.Action)) + { + await socket.SendTextAsync("Error: Action not recognized."); + } + else + { + var handler = handlerResolver(request.Action); + await handler.Handle(socket, message, userName); + } + } + catch (OperationCanceledException ex) + { + logger.LogError(ex.Message); + } + } + UnsubscribeFromBroadcastAndGames(userName); + } + + public void SubscribeToBroadcast(WebSocket socket, string playerName) + { + logger.LogInformation("Subscribing [{0}] to broadcast", playerName); + connections.TryAdd(playerName, socket); + } + + public void UnsubscribeFromBroadcastAndGames(string playerName) + { + logger.LogInformation("Unsubscribing [{0}] from broadcast", playerName); + connections.TryRemove(playerName, out _); + foreach (var game in gameSeats) + { + game.Value.Remove(playerName); + } + } + + /// + /// Unsubscribes the player from their current game, then subscribes to the new game. + /// + public void SubscribeToGame(WebSocket socket, string gameName, string playerName) + { + // Unsubscribe from any other games + foreach (var kvp in gameSeats) + { + var gameNameKey = kvp.Key; + UnsubscribeFromGame(gameNameKey, playerName); + } + + // Subscribe + logger.LogInformation("Subscribing player [{0}] to game [{1}]", playerName, gameName); + var addSuccess = gameSeats.TryAdd(gameName, new List { playerName }); + if (!addSuccess && !gameSeats[gameName].Contains(playerName)) + { + gameSeats[gameName].Add(playerName); + } + } + + public void UnsubscribeFromGame(string gameName, string playerName) + { + if (gameSeats.ContainsKey(gameName)) + { + logger.LogInformation("Unsubscribing player [{0}] from game [{1}]", playerName, gameName); + gameSeats[gameName].Remove(playerName); + if (gameSeats[gameName].Count == 0) gameSeats.TryRemove(gameName, out _); + } + } + + public async Task BroadcastToAll(string msg) + { + var tasks = connections.Select(kvp => + { + var player = kvp.Key; + var socket = kvp.Value; + logger.LogInformation("Broadcasting to player [{0}] \n{1}\n", new[] { player, msg }); + return socket.SendTextAsync(msg); + }); + await Task.WhenAll(tasks); + } + + public async Task BroadcastToGame(string gameName, string msg) + { + if (gameSeats.ContainsKey(gameName)) + { + var tasks = gameSeats[gameName] + .Select(playerName => + { + logger.LogInformation("Broadcasting to game [{0}], player [{0}] \n{1}\n", gameName, playerName, msg); + return connections[playerName]; + }) + .Where(stream => stream != null) + .Select(socket => socket.SendTextAsync(msg)); + await Task.WhenAll(tasks); + } + } + + public async Task BroadcastToGame(string gameName, Func msgBuilder) + { + if (gameSeats.ContainsKey(gameName)) + { + var tasks = gameSeats[gameName] + .Select(playerName => + { + var socket = connections[playerName]; + var msg = msgBuilder(playerName, socket); + logger.LogInformation("Broadcasting to game [{0}], player [{0}] \n{1}\n", gameName, playerName, msg); + return socket.SendTextAsync(msg); + }); + await Task.WhenAll(tasks); + } + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs new file mode 100644 index 0000000..9c4c579 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Http; +using System; +using System.Net; +using System.Threading.Tasks; + +namespace Websockets.Managers +{ + public interface ISocketConnectionManager + { + Task HandleSocketRequest(HttpContext context); + } + + public class SocketConnectionManager : ISocketConnectionManager + { + private readonly ISocketCommunicationManager communicationManager; + private readonly ISocketTokenManager tokenManager; + + public SocketConnectionManager(ISocketCommunicationManager communicationManager, ISocketTokenManager tokenManager) : base() + { + this.communicationManager = communicationManager; + this.tokenManager = tokenManager; + + } + + public async Task HandleSocketRequest(HttpContext context) + { + var hasToken = context.Request.Query.Keys.Contains("token"); + if (hasToken) + { + var oneTimeToken = context.Request.Query["token"][0]; + var tokenAsGuid = Guid.Parse(oneTimeToken); + var userName = tokenManager.GetUsername(tokenAsGuid); + if (!string.IsNullOrEmpty(userName)) + { + var socket = await context.WebSockets.AcceptWebSocketAsync(); + await communicationManager.CommunicateWith(socket, userName); + return; + } + } + context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + return; + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketTokenManager.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketTokenManager.cs new file mode 100644 index 0000000..9a7eb2a --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Managers/SocketTokenManager.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Websockets.Managers +{ + public interface ISocketTokenManager + { + Guid GenerateToken(string s); + string GetUsername(Guid g); + } + + public class SocketTokenManager : ISocketTokenManager + { + /// + /// Key is userName + /// + private readonly Dictionary Tokens; + + public SocketTokenManager() + { + Tokens = new Dictionary(); + } + + public Guid GenerateToken(string userName) + { + var guid = Guid.NewGuid(); + + if (Tokens.ContainsKey(userName)) + { + Tokens.Remove(userName); + } + Tokens.Add(userName, guid); + + _ = Task.Run(async () => + { + await Task.Delay(TimeSpan.FromMinutes(1)); + Tokens.Remove(userName); + }); + + return guid; + } + + /// User name associated to the guid or null. + public string GetUsername(Guid guid) + { + if (Tokens.ContainsValue(guid)) + { + var username = Tokens.First(kvp => kvp.Value == guid).Key; + Tokens.Remove(username); + return username; + } + return null; + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Managers/Utility/JsonRequest.cs b/Gameboard.ShogiUI.Sockets/Managers/Utility/JsonRequest.cs new file mode 100644 index 0000000..0a2719a --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Managers/Utility/JsonRequest.cs @@ -0,0 +1,15 @@ +using Websockets.ServiceModels.Interfaces; + +namespace Websockets.Managers.Utility +{ + public class JsonRequest + { + public IRequest Request { get; private set; } + public string Json { get; private set; } + public JsonRequest(IRequest request, string json) + { + Request = request; + Json = json; + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Managers/Utility/Mapper.cs b/Gameboard.ShogiUI.Sockets/Managers/Utility/Mapper.cs new file mode 100644 index 0000000..37662b7 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Managers/Utility/Mapper.cs @@ -0,0 +1,67 @@ +using Microsoft.FSharp.Core; +using Websockets.ServiceModels.Types; +using GameboardTypes = Gameboard.Shogi.Api.ServiceModels.Types; + +namespace Websockets.Managers.Utility +{ + public static class Mapper + { + public static GameboardTypes.Move Map(Move source) + { + var from = source.From; + var to = source.To; + FSharpOption pieceFromCaptured = source.PieceFromCaptured switch + { + "B" => new FSharpOption(GameboardTypes.PieceName.Bishop), + "G" => new FSharpOption(GameboardTypes.PieceName.GoldenGeneral), + "K" => new FSharpOption(GameboardTypes.PieceName.King), + "k" => new FSharpOption(GameboardTypes.PieceName.Knight), + "L" => new FSharpOption(GameboardTypes.PieceName.Lance), + "P" => new FSharpOption(GameboardTypes.PieceName.Pawn), + "R" => new FSharpOption(GameboardTypes.PieceName.Rook), + "S" => new FSharpOption(GameboardTypes.PieceName.SilverGeneral), + _ => null + }; + var target = new GameboardTypes.Move + { + Origin = new GameboardTypes.BoardLocation { X = from.X, Y = from.Y }, + Destination = new GameboardTypes.BoardLocation { X = to.X, Y = to.Y }, + IsPromotion = source.IsPromotion, + PieceFromCaptured = pieceFromCaptured + }; + return target; + } + + public static Move Map(GameboardTypes.Move source) + { + var origin = source.Origin; + var destination = source.Destination; + string pieceFromCaptured = null; + if (source.PieceFromCaptured != null) + { + pieceFromCaptured = source.PieceFromCaptured.Value switch + { + GameboardTypes.PieceName.Bishop => "B", + GameboardTypes.PieceName.GoldenGeneral => "G", + GameboardTypes.PieceName.King => "K", + GameboardTypes.PieceName.Knight => "k", + GameboardTypes.PieceName.Lance => "L", + GameboardTypes.PieceName.Pawn => "P", + GameboardTypes.PieceName.Rook => "R", + GameboardTypes.PieceName.SilverGeneral => "S", + _ => "" + }; + } + + var target = new Move + { + From = new Coords { X = origin.X, Y = origin.Y }, + To = new Coords { X = destination.X, Y = destination.Y }, + IsPromotion = source.IsPromotion, + PieceFromCaptured = pieceFromCaptured + }; + + return target; + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Managers/Utility/Request.cs b/Gameboard.ShogiUI.Sockets/Managers/Utility/Request.cs new file mode 100644 index 0000000..b0c4e95 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Managers/Utility/Request.cs @@ -0,0 +1,11 @@ +using Websockets.ServiceModels.Interfaces; +using Websockets.ServiceModels.Types; + +namespace AspShogiSockets.Managers.Utility +{ + public class Request : IRequest + { + public ClientAction Action { get; set; } + public string PlayerName { get; set; } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Program.cs b/Gameboard.ShogiUI.Sockets/Program.cs new file mode 100644 index 0000000..af5f468 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Websockets +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/Gameboard.ShogiUI.Sockets/Properties/launchSettings.json b/Gameboard.ShogiUI.Sockets/Properties/launchSettings.json new file mode 100644 index 0000000..9d79795 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:63676", + "sslPort": 44396 + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchUrl": "weatherforecast", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "AspShogiSockets": { + "commandName": "Project", + "launchBrowser": false, + "launchUrl": "Socket/Token", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://127.0.0.1:5101" + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs new file mode 100644 index 0000000..72f318b --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs @@ -0,0 +1,127 @@ +using Gameboard.Shogi.Api.ServiceModels.Messages; +using Newtonsoft.Json; +using System; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Websockets.Repositories.Utility; + +namespace Websockets.Repositories +{ + public interface IGameboardRepository + { + Task DeleteGame(string gameName); + Task GetGame(string gameName); + Task GetGames(); + Task GetGames(string playerName); + Task GetMoves(string gameName); + Task PostGame(PostGame request); + Task PostJoinByCode(PostJoinByCode request); + Task PostJoinGame(string gameName, PostJoinGame request); + Task PostMove(string gameName, PostMove request); + Task PostJoinCode(string gameName, string userName); + Task GetPlayer(string userName); + Task PostPlayer(PostPlayer request); + } + + public class GameboardRepository : IGameboardRepository + { + private readonly IAuthenticatedHttpClient client; + public GameboardRepository(IAuthenticatedHttpClient client) + { + this.client = client; + } + + public async Task GetGames() + { + var response = await client.GetAsync("Games"); + var json = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(json); + } + + public async Task GetGames(string playerName) + { + var uri = $"Games/{playerName}"; + var response = await client.GetAsync(Uri.EscapeUriString(uri)); + var json = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(json); + } + + public async Task GetGame(string gameName) + { + var uri = $"Game/{gameName}"; + var response = await client.GetAsync(Uri.EscapeUriString(uri)); + var json = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(json); + } + + public async Task DeleteGame(string gameName) + { + var uri = $"Game/{gameName}"; + await client.DeleteAsync(Uri.EscapeUriString(uri)); + } + + public async Task PostGame(PostGame request) + { + var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json"); + var response = await client.PostAsync("Game", content); + var json = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(json); + } + + public async Task PostJoinGame(string gameName, PostJoinGame request) + { + var uri = $"Game/{gameName}/Join"; + var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json"); + var response = await client.PostAsync(Uri.EscapeUriString(uri), content); + var json = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(json); + } + + public async Task PostJoinByCode(PostJoinByCode request) + { + var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json"); + var response = await client.PostAsync("Game/Join", content); + var json = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(json); + } + + public async Task GetMoves(string gameName) + { + var uri = $"Game/{gameName}/Moves"; + var response = await client.GetAsync(Uri.EscapeUriString(uri)); + var json = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(json); + } + + public async Task PostMove(string gameName, PostMove request) + { + var uri = $"Game/{gameName}/Move"; + var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json"); + await client.PostAsync(Uri.EscapeUriString(uri), content); + } + + public async Task PostJoinCode(string gameName, string userName) + { + var uri = $"JoinCode/{gameName}"; + var serialized = JsonConvert.SerializeObject(new PostJoinCode { PlayerName = userName }); + var content = new StringContent(serialized, Encoding.UTF8, "application/json"); + var json = await (await client.PostAsync(Uri.EscapeUriString(uri), content)).Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(json); + } + + public async Task GetPlayer(string playerName) + { + var uri = $"Player/{playerName}"; + var response = await client.GetAsync(Uri.EscapeUriString(uri)); + var json = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(json); + } + + public async Task PostPlayer(PostPlayer request) + { + var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json"); + return await client.PostAsync("Player", content); + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/PlayerRepository.cs b/Gameboard.ShogiUI.Sockets/Repositories/PlayerRepository.cs new file mode 100644 index 0000000..843f9e0 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Repositories/PlayerRepository.cs @@ -0,0 +1,32 @@ +using Gameboard.Shogi.Api.ServiceModels.Messages; +using Newtonsoft.Json; +using System; +using System.Threading.Tasks; +using Websockets.Repositories.Utility; + +namespace Websockets.Repositories +{ + [Obsolete("Use GameboardRepository. Functions from PlayerRepository will be moved.")] + public class PlayerRepository + { + private readonly IAuthenticatedHttpClient client; + + public PlayerRepository(IAuthenticatedHttpClient client) + { + this.client = client; + } + + public async Task GetPlayer(string playerName) + { + var response = await client.GetAsync($"/Player/{playerName}"); + var json = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(json); + } + + public async Task DeletePlayer(string playerName) + { + var response = await client.DeleteAsync($"/Player/{playerName}"); + await response.Content.ReadAsStringAsync(); + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/RepositoryManagers/GameboardRepositoryManager.cs b/Gameboard.ShogiUI.Sockets/Repositories/RepositoryManagers/GameboardRepositoryManager.cs new file mode 100644 index 0000000..646b467 --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Repositories/RepositoryManagers/GameboardRepositoryManager.cs @@ -0,0 +1,43 @@ +using Gameboard.Shogi.Api.ServiceModels.Messages; +using System; +using System.Threading.Tasks; +using Websockets.Repositories; + +namespace AspShogiSockets.Repositories.RepositoryManagers +{ + public interface IGameboardRepositoryManager + { + Task CreateGuestUser(); + } + + public class GameboardRepositoryManager : IGameboardRepositoryManager + { + private readonly IGameboardRepository repository; + private const int MaxTries = 3; + + public GameboardRepositoryManager(IGameboardRepository repository) + { + this.repository = repository; + } + + public async Task CreateGuestUser() + { + var count = 0; + while (count < MaxTries) + { + count++; + var clientId = $"Guest-{Guid.NewGuid()}"; + var request = new PostPlayer + { + PlayerName = clientId + }; + var response = await repository.PostPlayer(request); + if (response.IsSuccessStatusCode) + { + return clientId; + } + } + throw new OperationCanceledException($"Failed to create guest user after {MaxTries} tries."); + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Repositories/Utility/AuthenticatedHttpClient.cs b/Gameboard.ShogiUI.Sockets/Repositories/Utility/AuthenticatedHttpClient.cs new file mode 100644 index 0000000..62e066f --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Repositories/Utility/AuthenticatedHttpClient.cs @@ -0,0 +1,103 @@ +using IdentityModel.Client; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Websockets.Repositories.Utility +{ + public interface IAuthenticatedHttpClient + { + Task DeleteAsync(string requestUri); + Task GetAsync(string requestUri); + Task PostAsync(string requestUri, HttpContent content); + } + + public class AuthenticatedHttpClient : HttpClient, IAuthenticatedHttpClient + { + private readonly ILogger logger; + private readonly string identityServerUrl; + private TokenResponse tokenResponse; + private readonly string clientId; + private readonly string clientSecret; + + public AuthenticatedHttpClient(ILogger logger, IConfiguration configuration) : base() + { + this.logger = logger; + identityServerUrl = configuration["AppSettings:IdentityServer"]; + clientId = configuration["AppSettings:ClientId"]; + clientSecret = configuration["AppSettings:ClientSecret"]; + BaseAddress = new Uri(configuration["AppSettings:GameboardShogiApi"]); + } + + private async Task RefreshBearerToken() + { + var disco = await this.GetDiscoveryDocumentAsync(identityServerUrl); + if (disco.IsError) + { + logger.LogError("{DiscoveryErrorType}", disco.ErrorType); + throw new Exception(disco.Error); + } + + var request = new ClientCredentialsTokenRequest + { + Address = disco.TokenEndpoint, + ClientId = clientId, + ClientSecret = clientSecret + }; + var response = await this.RequestClientCredentialsTokenAsync(request); + if (response.IsError) + { + throw new Exception(response.Error); + } + tokenResponse = response; + logger.LogInformation("Refreshing Bearer Token to {BaseAddress}", BaseAddress); + this.SetBearerToken(tokenResponse.AccessToken); + } + + public async new Task GetAsync(string requestUri) + { + var response = await base.GetAsync(requestUri); + if (response.StatusCode == HttpStatusCode.Unauthorized) + { + await RefreshBearerToken(); + response = await base.GetAsync(requestUri); + } + logger.LogInformation( + "Repository GET to {BaseUrl}{RequestUrl} \nResponse: {Response}\n", + BaseAddress, + requestUri, + await response.Content.ReadAsStringAsync()); + return response; + } + public async new Task PostAsync(string requestUri, HttpContent content) + { + var response = await base.PostAsync(requestUri, content); + if (response.StatusCode == HttpStatusCode.Unauthorized) + { + await RefreshBearerToken(); + response = await base.PostAsync(requestUri, content); + } + logger.LogInformation( + "Repository POST to {BaseUrl}{RequestUrl} \nRequest: {Request}\nResponse: {Response}\n", + BaseAddress, + requestUri, + await content.ReadAsStringAsync(), + await response.Content.ReadAsStringAsync()); + return response; + } + public async new Task DeleteAsync(string requestUri) + { + var response = await base.DeleteAsync(requestUri); + if (response.StatusCode == HttpStatusCode.Unauthorized) + { + await RefreshBearerToken(); + response = await base.DeleteAsync(requestUri); + } + logger.LogInformation("Repository DELETE to {BaseUrl}{RequestUrl}", BaseAddress, requestUri); + return response; + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/Startup.cs b/Gameboard.ShogiUI.Sockets/Startup.cs new file mode 100644 index 0000000..ba57a9f --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/Startup.cs @@ -0,0 +1,145 @@ +using AspShogiSockets.Repositories.RepositoryManagers; +using Gameboard.ShogiUI.Sockets.Extensions; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; +using System; +using System.Collections.Generic; +using System.Linq; +using Websockets.Managers; +using Websockets.Managers.ClientActionHandlers; +using Websockets.Repositories; +using Websockets.Repositories.Utility; +using Websockets.ServiceModels.Types; + +namespace Websockets +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + // Socket ActionHandlers + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // Managers + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddScoped(); + services.AddSingleton(sp => action => + { + return action switch + { + ClientAction.ListGames => sp.GetService(), + ClientAction.CreateGame => sp.GetService(), + ClientAction.JoinGame => sp.GetService(), + ClientAction.JoinByCode => sp.GetService(), + ClientAction.LoadGame => sp.GetService(), + ClientAction.Move => sp.GetService(), + _ => throw new KeyNotFoundException($"Unable to resolve {nameof(IActionHandler)} for {nameof(ClientAction)} {action}"), + }; + }); + + // Repositories + services.AddTransient(); + services.AddSingleton(); + + services.AddControllers(); + + services + .AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + options.Authority = "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0"; + options.Audience = "935df672-efa6-45fa-b2e8-b76dfd65a122"; + options.TokenValidationParameters.ValidateIssuer = true; + options.TokenValidationParameters.ValidateAudience = true; + }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ISocketConnectionManager socketConnectionManager) + { + var origins = new[] { "https://localhost:3000", "https://dev.lucaserver.space", "https://lucaserver.space" }; + var socketOptions = new WebSocketOptions(); + foreach (var o in origins) + socketOptions.AllowedOrigins.Add(o); + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + app.UseHsts(); + } + app + .UseRequestResponseLogging() + .UseCors( + opt => opt + .WithOrigins(origins) + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials() + ) + .UseRouting() + .UseAuthentication() + .UseAuthorization() + .UseWebSockets(socketOptions) + .UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }) + .Use(async (context, next) => + { + var isUpgradeHeader = context + .Request + .Headers + .Any(h => h.Key.Contains("upgrade", StringComparison.InvariantCultureIgnoreCase) + && h.Value.ToString().Contains("websocket", StringComparison.InvariantCultureIgnoreCase)); + if (isUpgradeHeader) + { + await socketConnectionManager.HandleSocketRequest(context); + } + else + { + await next(); + } + }); + + JsonConvert.DefaultSettings = () => new JsonSerializerSettings + { + Formatting = Formatting.Indented, + ContractResolver = new DefaultContractResolver + { + NamingStrategy = new CamelCaseNamingStrategy(), + }, + Converters = new[] { new StringEnumConverter() }, + NullValueHandling = NullValueHandling.Ignore + }; + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/appsettings.Development.json b/Gameboard.ShogiUI.Sockets/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/Gameboard.ShogiUI.Sockets/appsettings.json b/Gameboard.ShogiUI.Sockets/appsettings.json new file mode 100644 index 0000000..48c43bd --- /dev/null +++ b/Gameboard.ShogiUI.Sockets/appsettings.json @@ -0,0 +1,17 @@ +{ + "AppSettings": { + "IdentityServer": "https://identity.lucaserver.space/", + "GameboardShogiApi": "https://api.lucaserver.space/Gameboard.Shogi.Api/", + "ClientId": "DevClientId", + "ClientSecret": "DevSecret", + "Scope": "DevEnvironment" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} \ No newline at end of file diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 0000000..2477225 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,64 @@ +# ASP.NET Core +# Build and test ASP.NET Core projects targeting .NET Core. +# Add steps that run tests, create a NuGet package, deploy, and more: +# https://docs.microsoft.com/azure/devops/pipelines/languages/dotnet-core + +trigger: +- master + +pr: +- none + +pool: + vmImage: 'windows-latest' + +variables: + solution: '**/*.sln' + buildPlatform: 'Any CPU' + buildConfiguration: 'Release' + projectName: 'Gameboard.ShogiUI.Sockets' + +steps: + +- task: NuGetToolInstaller@1 + +- task: NuGetCommand@2 + inputs: + command: 'restore' + restoreSolution: '**/*.sln' + feedsToUse: 'config' + +- task: DotNetCoreCLI@2 + displayName: Publish + env : + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + inputs: + command: 'publish' + publishWebProjects: false + projects: '**/*.csproj ' + zipAfterPublish: false + +- task: FileTransform@1 + inputs: + folderPath: '$(System.DefaultWorkingDirectory)' + fileType: 'json' + targetFiles: '**/appsettings.json' + +- task: CopyFilesOverSSH@0 + displayName: SSH Copy to 1UB + inputs: + sshEndpoint: 'LucaServer' + sourceFolder: '$(System.DefaultWorkingDirectory)\$(projectName)\bin\Debug\netcoreapp3.1\publish' + targetFolder: '/var/www/api/production/$(projectName)' + contents: '**' + failOnEmptySource: true + cleanTargetFolder: true + +- task: SSH@0 + displayName: Restart Kestrel + inputs: + sshEndpoint: 'LucaServer' + runOptions: 'commands' + commands: 'sudo systemctl restart kestrel-gameboard.shogiui.sockets.service' + readyTimeout: '20000' + diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..346522c --- /dev/null +++ b/nuget.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file From ccad4e179fed8dfa1245b7fda4361ba8f3af8836 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Tue, 19 Jan 2021 22:54:17 +0000 Subject: [PATCH 2/3] Set up CI with Azure Pipelines [skip ci] --- azure-pipelines-development.yml | 34 +++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 azure-pipelines-development.yml diff --git a/azure-pipelines-development.yml b/azure-pipelines-development.yml new file mode 100644 index 0000000..bf56801 --- /dev/null +++ b/azure-pipelines-development.yml @@ -0,0 +1,34 @@ +# ASP.NET +# Build and test ASP.NET projects. +# Add steps that publish symbols, save build artifacts, deploy, and more: +# https://docs.microsoft.com/azure/devops/pipelines/apps/aspnet/build-aspnet-4 + +trigger: +- main + +pool: + vmImage: 'windows-latest' + +variables: + solution: '**/*.sln' + buildPlatform: 'Any CPU' + buildConfiguration: 'Release' + +steps: +- task: NuGetToolInstaller@1 + +- task: NuGetCommand@2 + inputs: + restoreSolution: '$(solution)' + +- task: VSBuild@1 + inputs: + solution: '$(solution)' + msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:PackageLocation="$(build.artifactStagingDirectory)"' + platform: '$(buildPlatform)' + configuration: '$(buildConfiguration)' + +- task: VSTest@2 + inputs: + platform: '$(buildPlatform)' + configuration: '$(buildConfiguration)' From b2211da4fc9732412f321a88eccf31a358fc9c55 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Tue, 19 Jan 2021 22:56:21 +0000 Subject: [PATCH 3/3] azure-pipelines-development.yml edited online with Bitbucket --- azure-pipelines-development.yml | 58 +++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/azure-pipelines-development.yml b/azure-pipelines-development.yml index bf56801..b6b5af4 100644 --- a/azure-pipelines-development.yml +++ b/azure-pipelines-development.yml @@ -1,10 +1,13 @@ -# ASP.NET -# Build and test ASP.NET projects. -# Add steps that publish symbols, save build artifacts, deploy, and more: -# https://docs.microsoft.com/azure/devops/pipelines/apps/aspnet/build-aspnet-4 +# ASP.NET Core +# Build and test ASP.NET Core projects targeting .NET Core. +# Add steps that run tests, create a NuGet package, deploy, and more: +# https://docs.microsoft.com/azure/devops/pipelines/languages/dotnet-core trigger: -- main +- development + +pr: +- none pool: vmImage: 'windows-latest' @@ -13,22 +16,49 @@ variables: solution: '**/*.sln' buildPlatform: 'Any CPU' buildConfiguration: 'Release' + projectName: 'Gameboard.ShogiUI.Sockets' steps: + - task: NuGetToolInstaller@1 - task: NuGetCommand@2 inputs: - restoreSolution: '$(solution)' + command: 'restore' + restoreSolution: '**/*.sln' + feedsToUse: 'config' -- task: VSBuild@1 +- task: DotNetCoreCLI@2 + displayName: Publish + env : + DOTNET_CLI_TELEMETRY_OPTOUT: 1 inputs: - solution: '$(solution)' - msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:PackageLocation="$(build.artifactStagingDirectory)"' - platform: '$(buildPlatform)' - configuration: '$(buildConfiguration)' + command: 'publish' + publishWebProjects: false + projects: '**/*.csproj ' + zipAfterPublish: false -- task: VSTest@2 +- task: FileTransform@1 inputs: - platform: '$(buildPlatform)' - configuration: '$(buildConfiguration)' + folderPath: '$(System.DefaultWorkingDirectory)' + fileType: 'json' + targetFiles: '**/appsettings.json' + +- task: CopyFilesOverSSH@0 + displayName: SSH Copy to 1UB + inputs: + sshEndpoint: 'LucaServer' + sourceFolder: '$(System.DefaultWorkingDirectory)\$(projectName)\bin\Debug\net5.0\publish' + targetFolder: '/var/www/api/development/$(projectName)' + contents: '**' + failOnEmptySource: true + cleanTargetFolder: true + +- task: SSH@0 + displayName: Restart Kestrel + inputs: + sshEndpoint: 'LucaServer' + runOptions: 'commands' + commands: 'sudo systemctl restart kestrel-dev.gameboard.shogiui.sockets.service' + readyTimeout: '20000' +