diff --git a/Arrowgene.Ddon.GameServer/Chat/ChatMessage.cs b/Arrowgene.Ddon.GameServer/Chat/ChatMessage.cs index 591ff11fd..be6e65f9f 100644 --- a/Arrowgene.Ddon.GameServer/Chat/ChatMessage.cs +++ b/Arrowgene.Ddon.GameServer/Chat/ChatMessage.cs @@ -18,7 +18,7 @@ public ChatMessage(LobbyChatMsgType messageType, byte unk2, uint unk3, uint unk4 Deliver = true; } - public LobbyChatMsgType Type { get; } + public LobbyChatMsgType Type { get; set; } public byte Unk2 { get; set; } public uint Unk3 { get; set; } public uint Unk4 { get; set; } diff --git a/Arrowgene.Ddon.GameServer/Chat/Log/ChatLogHandler.cs b/Arrowgene.Ddon.GameServer/Chat/Log/ChatLogHandler.cs index 59d692dca..6bfe70045 100644 --- a/Arrowgene.Ddon.GameServer/Chat/Log/ChatLogHandler.cs +++ b/Arrowgene.Ddon.GameServer/Chat/Log/ChatLogHandler.cs @@ -29,5 +29,13 @@ public void Handle(GameClient client, ChatMessage message, List re ChatMessageLogEntry logEntry = new ChatMessageLogEntry(client.Character, message); _chatMessageLog.Add(logEntry); } + + public void AddEntry(uint characterId, string firstName, string lastName, ChatMessage message) + { + Logger.Info("Chat message: "+message.Message); + + ChatMessageLogEntry logEntry = new ChatMessageLogEntry(characterId, firstName, lastName, message); + _chatMessageLog.Add(logEntry); + } } } diff --git a/Arrowgene.Ddon.GameServer/Chat/Log/ChatMessageLogEntry.cs b/Arrowgene.Ddon.GameServer/Chat/Log/ChatMessageLogEntry.cs index 146d876ec..7d4d8fb3f 100644 --- a/Arrowgene.Ddon.GameServer/Chat/Log/ChatMessageLogEntry.cs +++ b/Arrowgene.Ddon.GameServer/Chat/Log/ChatMessageLogEntry.cs @@ -10,6 +10,15 @@ public ChatMessageLogEntry() { } + public ChatMessageLogEntry(uint characterId, string firstName, string lastName, ChatMessage chatMessage) + { + DateTime = DateTime.UtcNow; + FirstName = firstName; + LastName = lastName; + CharacterId = characterId; + ChatMessage = chatMessage; + } + public ChatMessageLogEntry(Character character, ChatMessage chatMessage) { DateTime = DateTime.UtcNow; @@ -20,6 +29,7 @@ public ChatMessageLogEntry(Character character, ChatMessage chatMessage) } public DateTime DateTime { get; set; } + public long UnixTimeMillis { get => ((DateTimeOffset) DateTime.SpecifyKind(this.DateTime, DateTimeKind.Utc)).ToUnixTimeMilliseconds(); } public string FirstName { get; set; } public string LastName { get; set; } public uint CharacterId { get; set; } diff --git a/Arrowgene.Ddon.Rpc.Web/Middleware/AuthMiddleware.cs b/Arrowgene.Ddon.Rpc.Web/Middleware/AuthMiddleware.cs new file mode 100644 index 000000000..a7ee9a79b --- /dev/null +++ b/Arrowgene.Ddon.Rpc.Web/Middleware/AuthMiddleware.cs @@ -0,0 +1,105 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Arrowgene.Ddon.Database; +using Arrowgene.Ddon.Database.Model; +using Arrowgene.Ddon.Shared.Crypto; +using Arrowgene.Logging; +using Arrowgene.WebServer; +using Arrowgene.WebServer.Middleware; + +public class AuthMiddleware : IWebMiddleware +{ + private static readonly ILogger Logger = LogProvider.Logger(typeof(AuthMiddleware)); + + private readonly IDatabase _database; + private readonly Dictionary _routeAndRequiredMinimumState; + + public AuthMiddleware(IDatabase database) + { + _database = database; + _routeAndRequiredMinimumState = new Dictionary(); + } + + public void Require(AccountStateType minimumState, string route) + { + _routeAndRequiredMinimumState.Add(route, minimumState); + } + + public async Task Handle(WebRequest request, WebMiddlewareDelegate next) + { + if(!_routeAndRequiredMinimumState.ContainsKey(request.Path)) + { + // Don't intercept request if the request path isn't registered in the middleware + return await next(request); + } + + string authHeader = request.Header.Get("authorization"); + if(authHeader == null) + { + Logger.Error("Attempted to access auth protected route with no Authorization header"); + WebResponse response = new WebResponse(); + response.StatusCode = 401; + await response.WriteAsync("Attempted to access auth protected route with no Authorization header"); + return response; + } + + if(!authHeader.StartsWith("Basic ")) + { + Logger.Error("Attempted to access auth protected route with an invalid Authorization method. Only Basic auth is supported."); + WebResponse response = new WebResponse(); + response.StatusCode = 401; + await response.WriteAsync("Attempted to access auth protected route with an invalid Authorization method. Only Basic auth is supported."); + return response; + } + + string encodedUserAndPassword = authHeader.Substring("Basic ".Length); + Encoding encoding = Encoding.GetEncoding("iso-8859-1"); + string[] usernameAndPassword = encoding.GetString(Convert.FromBase64String(encodedUserAndPassword)).Split(":"); + if(usernameAndPassword.Length != 2) + { + Logger.Error("Attempted to access auth protected route with an invalid Basic auth header."); + WebResponse response = new WebResponse(); + response.StatusCode = 401; + await response.WriteAsync("Attempted to access auth protected route with an invalid Basic auth header."); + return response; + } + + string username = usernameAndPassword[0]; + string password = usernameAndPassword[1]; + + Account account = _database.SelectAccountByName(username); + if (account == null) + { + Logger.Error($"Attempted to authenticate as a nonexistant user {username}."); + WebResponse response = new WebResponse(); + response.StatusCode = 401; + await response.WriteAsync($"Failed to authenticate as {username}."); + return response; + } + + if (!PasswordHash.Verify(password, account.Hash)) + { + Logger.Error($"Attempted to authenticate as {username} with an incorrect password."); + WebResponse response = new WebResponse(); + response.StatusCode = 401; + await response.WriteAsync($"Failed to authenticate as {username}."); + return response; + } + + AccountStateType minimumRequiredAccountStateType = _routeAndRequiredMinimumState[request.Path]; + if(account.State < minimumRequiredAccountStateType) + { + Logger.Error($"Attempted to access auth protected route as {username} without enough permissions (Account has {account.State}, minimum required {minimumRequiredAccountStateType})."); + WebResponse response = new WebResponse(); + response.StatusCode = 403; + await response.WriteAsync($"Attempted to access auth protected route as {username} without enough permissions."); + return response; + } + + return await next(request); + } +} \ No newline at end of file diff --git a/Arrowgene.Ddon.Rpc.Web/Route/ChatRoute.cs b/Arrowgene.Ddon.Rpc.Web/Route/ChatRoute.cs index 986c7f0d9..6a34a1c2d 100644 --- a/Arrowgene.Ddon.Rpc.Web/Route/ChatRoute.cs +++ b/Arrowgene.Ddon.Rpc.Web/Route/ChatRoute.cs @@ -5,7 +5,6 @@ using Arrowgene.WebServer; using System.Text.Json; using Arrowgene.Ddon.GameServer.Chat.Log; -using static Arrowgene.Ddon.GameServer.Chat.ChatManager; namespace Arrowgene.Ddon.Rpc.Web.Route { @@ -27,7 +26,7 @@ public override async Task Get(WebRequest request) { string dateString = request.QueryParameter.Get("since"); try { - chat = new ChatCommand(DateTime.Parse(dateString)); + chat = new ChatCommand(ParseSinceDate(dateString)); } catch(FormatException e) { @@ -85,5 +84,19 @@ public override async Task Post(WebRequest request) } } + private static long ParseSinceDate(string dateString) + { + if(DateTime.TryParse(dateString, out DateTime date)) + { + return ((DateTimeOffset) DateTime.SpecifyKind(date, DateTimeKind.Utc)).ToUnixTimeMilliseconds(); + } + + if(long.TryParse(dateString, out long unixTimeMillis)) + { + return unixTimeMillis; + } + + throw new FormatException(); + } } } diff --git a/Arrowgene.Ddon.Rpc.Web/RpcWebServer.cs b/Arrowgene.Ddon.Rpc.Web/RpcWebServer.cs index ed9394b01..1fbabbbff 100644 --- a/Arrowgene.Ddon.Rpc.Web/RpcWebServer.cs +++ b/Arrowgene.Ddon.Rpc.Web/RpcWebServer.cs @@ -1,4 +1,5 @@ -using Arrowgene.Ddon.GameServer; +using Arrowgene.Ddon.Database.Model; +using Arrowgene.Ddon.GameServer; using Arrowgene.Ddon.Rpc.Web.Route; using Arrowgene.Ddon.WebServer; @@ -20,7 +21,13 @@ public void Init() { _webServer.AddRoute(new SpawnRoute(this)); _webServer.AddRoute(new InfoRoute(this)); - _webServer.AddRoute(new ChatRoute(this)); + + ChatRoute chatRoute = new ChatRoute(this); + _webServer.AddRoute(chatRoute); + + AuthMiddleware authMiddleware = new AuthMiddleware(_gameServer.Database); + authMiddleware.Require(AccountStateType.GameMaster, chatRoute.Route); + _webServer.AddMiddleware(authMiddleware); } } } diff --git a/Arrowgene.Ddon.Rpc/Command/ChatCommand.cs b/Arrowgene.Ddon.Rpc/Command/ChatCommand.cs index 40ef4b754..a4a549080 100644 --- a/Arrowgene.Ddon.Rpc/Command/ChatCommand.cs +++ b/Arrowgene.Ddon.Rpc/Command/ChatCommand.cs @@ -12,22 +12,22 @@ public class ChatCommand : IRpcCommand public ChatCommand() { - _since = DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc); + _sinceUnixMillis = long.MinValue; } - public ChatCommand(DateTime since) + public ChatCommand(long sinceUnixMillis) { - _since = since; + _sinceUnixMillis = sinceUnixMillis; } public IEnumerable ChatMessageLog { get; set; } - private readonly DateTime _since; + private readonly long _sinceUnixMillis; public RpcCommandResult Execute(DdonGameServer gameServer) { ChatMessageLog = gameServer.ChatLogHandler.ChatMessageLog - .Where(entry => entry.DateTime >= _since); + .Where(entry => entry.UnixTimeMillis > _sinceUnixMillis); return new RpcCommandResult(this, true); } } diff --git a/Arrowgene.Ddon.Rpc/Command/ChatPostCommand.cs b/Arrowgene.Ddon.Rpc/Command/ChatPostCommand.cs index 2e98eab8a..75a07dc9e 100644 --- a/Arrowgene.Ddon.Rpc/Command/ChatPostCommand.cs +++ b/Arrowgene.Ddon.Rpc/Command/ChatPostCommand.cs @@ -25,7 +25,8 @@ public RpcCommandResult Execute(DdonGameServer gameServer) _entry.LastName, _entry.ChatMessage.Type, gameServer.ClientLookup.GetAll() - ); + ); + gameServer.ChatLogHandler.AddEntry(0, _entry.FirstName, _entry.LastName, _entry.ChatMessage); return new RpcCommandResult(this, true); } diff --git a/Arrowgene.Ddon.Rpc/RpcServer.cs b/Arrowgene.Ddon.Rpc/RpcServer.cs index b38ef19a2..d09503d39 100644 --- a/Arrowgene.Ddon.Rpc/RpcServer.cs +++ b/Arrowgene.Ddon.Rpc/RpcServer.cs @@ -11,7 +11,7 @@ public class RpcServer : IRpcExecuter { private static readonly ILogger Logger = LogProvider.Logger(typeof(RpcServer)); - private readonly DdonGameServer _gameServer; + protected readonly DdonGameServer _gameServer; public RpcServer(DdonGameServer gameServer) { diff --git a/Arrowgene.Ddon.Test/Arrowgene.Ddon.Test.csproj b/Arrowgene.Ddon.Test/Arrowgene.Ddon.Test.csproj index e7240a537..228cac9f4 100644 --- a/Arrowgene.Ddon.Test/Arrowgene.Ddon.Test.csproj +++ b/Arrowgene.Ddon.Test/Arrowgene.Ddon.Test.csproj @@ -27,6 +27,7 @@ + diff --git a/Arrowgene.Ddon.Test/GameServer/Chat/Log/ChatMessageLogEntryTest.cs b/Arrowgene.Ddon.Test/GameServer/Chat/Log/ChatMessageLogEntryTest.cs new file mode 100644 index 000000000..fb74ad179 --- /dev/null +++ b/Arrowgene.Ddon.Test/GameServer/Chat/Log/ChatMessageLogEntryTest.cs @@ -0,0 +1,22 @@ +using System.Text.Json; +using Arrowgene.Ddon.GameServer.Chat; +using Arrowgene.Ddon.GameServer.Chat.Log; +using Arrowgene.Ddon.Shared.Model; +using Xunit; + +namespace Arrowgene.Ddon.Test.GameServer.Chat.Log; + +public class ChatMessageLogEntryTest +{ + [Fact] + public void TestJsonSerialize() + { + ChatMessageLogEntry obj = new ChatMessageLogEntry(); + obj.ChatMessage = new ChatMessage(); + obj.ChatMessage.Type = LobbyChatMsgType.Party; + string json = JsonSerializer.Serialize(obj); + ChatMessageLogEntry res = JsonSerializer.Deserialize(json); + Assert.NotNull(res); + Assert.Equal(obj.ChatMessage.Type, res.ChatMessage.Type); + } +} diff --git a/Arrowgene.Ddon.WebServer/DdonWebServer.cs b/Arrowgene.Ddon.WebServer/DdonWebServer.cs index b8e618535..32a37db15 100644 --- a/Arrowgene.Ddon.WebServer/DdonWebServer.cs +++ b/Arrowgene.Ddon.WebServer/DdonWebServer.cs @@ -2,6 +2,7 @@ using Arrowgene.Ddon.Database; using Arrowgene.Logging; using Arrowgene.WebServer; +using Arrowgene.WebServer.Middleware; using Arrowgene.WebServer.Route; using Arrowgene.WebServer.Server; using Arrowgene.WebServer.Server.Kestrel; @@ -33,12 +34,21 @@ public DdonWebServer(WebServerSetting setting, IDatabase database) Logger.Info(servingFile); } - _webService.AddMiddleware(staticFile); + AddMiddleware(staticFile); AddRoute(new IndexRoute()); AddRoute(new AccountRoute(database)); } + public void AddMiddleware(IWebMiddleware middleware) + { + _webService.AddMiddleware(middleware); + if (_running) + { + Logger.Info($"Registered new middleware `{middleware.GetType().Name}`"); + } + } + public void AddRoute(IWebRoute route) { _webService.AddRoute(route);