Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvements to Chat RPC commands #292

Merged
merged 7 commits into from
May 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Arrowgene.Ddon.GameServer/Chat/ChatMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
8 changes: 8 additions & 0 deletions Arrowgene.Ddon.GameServer/Chat/Log/ChatLogHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,13 @@ public void Handle(GameClient client, ChatMessage message, List<ChatResponse> 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);
}
}
}
10 changes: 10 additions & 0 deletions Arrowgene.Ddon.GameServer/Chat/Log/ChatMessageLogEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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; }
Expand Down
105 changes: 105 additions & 0 deletions Arrowgene.Ddon.Rpc.Web/Middleware/AuthMiddleware.cs
Original file line number Diff line number Diff line change
@@ -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<Logger>(typeof(AuthMiddleware));

private readonly IDatabase _database;
private readonly Dictionary<string, AccountStateType> _routeAndRequiredMinimumState;

public AuthMiddleware(IDatabase database)
{
_database = database;
_routeAndRequiredMinimumState = new Dictionary<string, AccountStateType>();
}

public void Require(AccountStateType minimumState, string route)
{
_routeAndRequiredMinimumState.Add(route, minimumState);
}

public async Task<WebResponse> 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);
}
}
17 changes: 15 additions & 2 deletions Arrowgene.Ddon.Rpc.Web/Route/ChatRoute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -27,7 +26,7 @@ public override async Task<WebResponse> Get(WebRequest request)
{
string dateString = request.QueryParameter.Get("since");
try {
chat = new ChatCommand(DateTime.Parse(dateString));
chat = new ChatCommand(ParseSinceDate(dateString));
}
catch(FormatException e)
{
Expand Down Expand Up @@ -85,5 +84,19 @@ public override async Task<WebResponse> 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();
}
}
}
11 changes: 9 additions & 2 deletions Arrowgene.Ddon.Rpc.Web/RpcWebServer.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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);
}
}
}
10 changes: 5 additions & 5 deletions Arrowgene.Ddon.Rpc/Command/ChatCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ChatMessageLogEntry> 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);
}
}
Expand Down
3 changes: 2 additions & 1 deletion Arrowgene.Ddon.Rpc/Command/ChatPostCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
2 changes: 1 addition & 1 deletion Arrowgene.Ddon.Rpc/RpcServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public class RpcServer : IRpcExecuter
{
private static readonly ILogger Logger = LogProvider.Logger<Logger>(typeof(RpcServer));

private readonly DdonGameServer _gameServer;
protected readonly DdonGameServer _gameServer;

public RpcServer(DdonGameServer gameServer)
{
Expand Down
1 change: 1 addition & 0 deletions Arrowgene.Ddon.Test/Arrowgene.Ddon.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<ItemGroup>
<ProjectReference Include="..\Arrowgene.Ddon.Client\Arrowgene.Ddon.Client.csproj" />
<ProjectReference Include="..\Arrowgene.Ddon.Cli\Arrowgene.Ddon.Cli.csproj" />
<ProjectReference Include="..\Arrowgene.Ddon.GameServer\Arrowgene.Ddon.GameServer.csproj" />
<ProjectReference Include="..\Arrowgene.Ddon.Server\Arrowgene.Ddon.Server.csproj" />
</ItemGroup>
<ItemGroup>
Expand Down
22 changes: 22 additions & 0 deletions Arrowgene.Ddon.Test/GameServer/Chat/Log/ChatMessageLogEntryTest.cs
Original file line number Diff line number Diff line change
@@ -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<ChatMessageLogEntry>(json);
Assert.NotNull(res);
Assert.Equal(obj.ChatMessage.Type, res.ChatMessage.Type);
}
}
12 changes: 11 additions & 1 deletion Arrowgene.Ddon.WebServer/DdonWebServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Loading