From c8a589c1c12de62c1560ade0bc3b7c068eda9158 Mon Sep 17 00:00:00 2001 From: flashwave Date: Thu, 16 Feb 2023 22:16:06 +0100 Subject: [PATCH] Un-switch packet handlers. --- SharpChat/ChatPacketHandlerContext.cs | 27 ++ SharpChat/IChatPacketHandler.cs | 6 + SharpChat/PacketHandlers/AuthHandler.cs | 138 ++++++++++ SharpChat/PacketHandlers/PingHandler.cs | 51 ++++ .../PacketHandlers/SendMessageHandler.cs | 104 ++++++++ SharpChat/SockChatServer.cs | 238 ++---------------- 6 files changed, 345 insertions(+), 219 deletions(-) create mode 100644 SharpChat/ChatPacketHandlerContext.cs create mode 100644 SharpChat/IChatPacketHandler.cs create mode 100644 SharpChat/PacketHandlers/AuthHandler.cs create mode 100644 SharpChat/PacketHandlers/PingHandler.cs create mode 100644 SharpChat/PacketHandlers/SendMessageHandler.cs diff --git a/SharpChat/ChatPacketHandlerContext.cs b/SharpChat/ChatPacketHandlerContext.cs new file mode 100644 index 0000000..4661017 --- /dev/null +++ b/SharpChat/ChatPacketHandlerContext.cs @@ -0,0 +1,27 @@ +using System; + +namespace SharpChat { + public class ChatPacketHandlerContext { + public string Text { get; } + public ChatContext Chat { get; } + public ChatUserSession Session { get; } + + public ChatPacketHandlerContext( + string text, + ChatContext chat, + ChatUserSession session + ) { + Text = text ?? throw new ArgumentNullException(nameof(text)); + Chat = chat ?? throw new ArgumentNullException(nameof(chat)); + Session = session ?? throw new ArgumentNullException(nameof(session)); + } + + public bool CheckPacketId(string packetId) { + return Text == packetId || Text.StartsWith(packetId + '\t'); + } + + public string[] SplitText(int expect) { + return Text.Split('\t', expect + 1); + } + } +} diff --git a/SharpChat/IChatPacketHandler.cs b/SharpChat/IChatPacketHandler.cs new file mode 100644 index 0000000..043964b --- /dev/null +++ b/SharpChat/IChatPacketHandler.cs @@ -0,0 +1,6 @@ +namespace SharpChat { + public interface IChatPacketHandler { + bool IsMatch(ChatPacketHandlerContext ctx); + void Handle(ChatPacketHandlerContext ctx); + } +} diff --git a/SharpChat/PacketHandlers/AuthHandler.cs b/SharpChat/PacketHandlers/AuthHandler.cs new file mode 100644 index 0000000..f3193e9 --- /dev/null +++ b/SharpChat/PacketHandlers/AuthHandler.cs @@ -0,0 +1,138 @@ +using SharpChat.Config; +using SharpChat.Misuzu; +using SharpChat.Packet; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace SharpChat.PacketHandlers { + public class AuthHandler : IChatPacketHandler { + private readonly MisuzuClient Misuzu; + private readonly ChatChannel DefaultChannel; + private readonly CachedValue MaxMessageLength; + private readonly CachedValue MaxConnections; + + public AuthHandler( + MisuzuClient msz, + ChatChannel defaultChannel, + CachedValue maxMsgLength, + CachedValue maxConns + ) { + Misuzu = msz ?? throw new ArgumentNullException(nameof(msz)); + DefaultChannel = defaultChannel ?? throw new ArgumentNullException(nameof(defaultChannel)); + MaxMessageLength = maxMsgLength ?? throw new ArgumentNullException(nameof(maxMsgLength)); + MaxConnections = maxConns ?? throw new ArgumentNullException(nameof(maxConns)); + } + + public bool IsMatch(ChatPacketHandlerContext ctx) { + return ctx.CheckPacketId("1"); + } + + public void Handle(ChatPacketHandlerContext ctx) { + string[] args = ctx.SplitText(3); + + string authMethod = args.ElementAtOrDefault(1); + if(string.IsNullOrWhiteSpace(authMethod)) { + ctx.Session.Send(new AuthFailPacket(AuthFailReason.AuthInvalid)); + ctx.Session.Dispose(); + return; + } + + string authToken = args.ElementAtOrDefault(2); + if(string.IsNullOrWhiteSpace(authToken)) { + ctx.Session.Send(new AuthFailPacket(AuthFailReason.AuthInvalid)); + ctx.Session.Dispose(); + return; + } + + if(authMethod.All(c => c is >= '0' and <= '9') && authToken.Contains(':')) { + string[] tokenParts = authToken.Split(':', 2); + authMethod = tokenParts[0]; + authToken = tokenParts[1]; + } + + Task.Run(async () => { + MisuzuAuthInfo fai; + string ipAddr = ctx.Session.RemoteAddress.ToString(); + + try { + fai = await Misuzu.AuthVerifyAsync(authMethod, authToken, ipAddr); + } catch(Exception ex) { + Logger.Write($"<{ctx.Session.Id}> Failed to authenticate: {ex}"); + ctx.Session.Send(new AuthFailPacket(AuthFailReason.AuthInvalid)); + ctx.Session.Dispose(); +#if DEBUG + throw; +#else + return; +#endif + } + + if(!fai.Success) { + Logger.Debug($"<{ctx.Session.Id}> Auth fail: {fai.Reason}"); + ctx.Session.Send(new AuthFailPacket(AuthFailReason.AuthInvalid)); + ctx.Session.Dispose(); + return; + } + + MisuzuBanInfo fbi; + try { + fbi = await Misuzu.CheckBanAsync(fai.UserId.ToString(), ipAddr); + } catch(Exception ex) { + Logger.Write($"<{ctx.Session.Id}> Failed auth ban check: {ex}"); + ctx.Session.Send(new AuthFailPacket(AuthFailReason.AuthInvalid)); + ctx.Session.Dispose(); +#if DEBUG + throw; +#else + return; +#endif + } + + if(fbi.IsBanned && !fbi.HasExpired) { + Logger.Write($"<{ctx.Session.Id}> User is banned."); + ctx.Session.Send(new AuthFailPacket(AuthFailReason.Banned, fbi)); + ctx.Session.Dispose(); + return; + } + + lock(ctx.Chat.UsersAccess) { + ChatUser aUser = ctx.Chat.Users.FirstOrDefault(u => u.UserId == fai.UserId); + + if(aUser == null) + aUser = new ChatUser(fai); + else { + aUser.ApplyAuth(fai); + aUser.Channel?.Send(new UserUpdatePacket(aUser)); + } + + // Enforce a maximum amount of connections per user + if(aUser.SessionCount >= MaxConnections) { + ctx.Session.Send(new AuthFailPacket(AuthFailReason.MaxSessions)); + ctx.Session.Dispose(); + return; + } + + // Bumping the ping to prevent upgrading + ctx.Session.BumpPing(); + + aUser.AddSession(ctx.Session); + + ctx.Session.Send(new LegacyCommandResponse(LCR.WELCOME, false, $"Welcome to Flashii Chat, {aUser.Username}!")); + + if(File.Exists("welcome.txt")) { + IEnumerable lines = File.ReadAllLines("welcome.txt").Where(x => !string.IsNullOrWhiteSpace(x)); + string line = lines.ElementAtOrDefault(RNG.Next(lines.Count())); + + if(!string.IsNullOrWhiteSpace(line)) + ctx.Session.Send(new LegacyCommandResponse(LCR.WELCOME, false, line)); + } + + ctx.Chat.HandleJoin(aUser, DefaultChannel, ctx.Session, MaxMessageLength); + } + }).Wait(); + } + } +} diff --git a/SharpChat/PacketHandlers/PingHandler.cs b/SharpChat/PacketHandlers/PingHandler.cs new file mode 100644 index 0000000..13bb6df --- /dev/null +++ b/SharpChat/PacketHandlers/PingHandler.cs @@ -0,0 +1,51 @@ +using SharpChat.Misuzu; +using SharpChat.Packet; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace SharpChat.PacketHandlers { + public class PingHandler : IChatPacketHandler { + private readonly MisuzuClient Misuzu; + + private readonly object BumpAccess = new(); + private readonly TimeSpan BumpInterval = TimeSpan.FromMinutes(1); + private DateTimeOffset LastBump = DateTimeOffset.MinValue; + + public PingHandler(MisuzuClient msz) { + Misuzu = msz ?? throw new ArgumentNullException(nameof(msz)); + } + + public bool IsMatch(ChatPacketHandlerContext ctx) { + return ctx.CheckPacketId("0"); + } + + public void Handle(ChatPacketHandlerContext ctx) { + string[] parts = ctx.SplitText(2); + + if(!int.TryParse(parts.FirstOrDefault(), out int pTime)) + return; + + ctx.Session.BumpPing(); + ctx.Session.Send(new PongPacket(ctx.Session.LastPing)); + + lock(BumpAccess) { + if(LastBump < DateTimeOffset.UtcNow - BumpInterval) { + (string, string)[] bumpList; + lock(ctx.Chat.UsersAccess) + bumpList = ctx.Chat.Users + .Where(u => u.HasSessions && u.Status == ChatUserStatus.Online) + .Select(u => (u.UserId.ToString(), u.RemoteAddresses.FirstOrDefault()?.ToString() ?? string.Empty)) + .ToArray(); + + if(bumpList.Any()) + Task.Run(async () => { + await Misuzu.BumpUsersOnlineAsync(bumpList); + }).Wait(); + + LastBump = DateTimeOffset.UtcNow; + } + } + } + } +} diff --git a/SharpChat/PacketHandlers/SendMessageHandler.cs b/SharpChat/PacketHandlers/SendMessageHandler.cs new file mode 100644 index 0000000..af29630 --- /dev/null +++ b/SharpChat/PacketHandlers/SendMessageHandler.cs @@ -0,0 +1,104 @@ +using SharpChat.Commands; +using SharpChat.Config; +using SharpChat.Events; +using SharpChat.Packet; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace SharpChat.PacketHandlers { + public class SendMessageHandler : IChatPacketHandler { + private readonly CachedValue MaxMessageLength; + + private List Commands { get; } = new(); + + public SendMessageHandler(CachedValue maxMsgLength) { + MaxMessageLength = maxMsgLength ?? throw new ArgumentNullException(nameof(maxMsgLength)); + } + + public void AddCommand(IChatCommand command) { + Commands.Add(command ?? throw new ArgumentNullException(nameof(command))); + } + + public void AddCommands(IEnumerable commands) { + Commands.AddRange(commands ?? throw new ArgumentNullException(nameof(commands))); + } + + public bool IsMatch(ChatPacketHandlerContext ctx) { + return ctx.CheckPacketId("2"); + } + + public void Handle(ChatPacketHandlerContext ctx) { + string[] args = ctx.SplitText(3); + + ChatUser user = ctx.Session.User; + + // No longer concats everything after index 1 with \t, no previous implementation did that either + string messageText = args.ElementAtOrDefault(2); + + if(user == null || !user.Can(ChatUserPermissions.SendMessage) || string.IsNullOrWhiteSpace(messageText)) + return; + + // Extra validation step, not necessary at all but enforces proper formatting in SCv1. + if(!long.TryParse(args[1], out long mUserId) || user.UserId != mUserId) + return; + ChatChannel channel = user.CurrentChannel; + + if(channel == null + || !user.InChannel(channel) + || (user.IsSilenced && !user.Can(ChatUserPermissions.SilenceUser))) + return; + + if(user.Status != ChatUserStatus.Online) { + user.Status = ChatUserStatus.Online; + channel.Send(new UserUpdatePacket(user)); + } + + int maxMsgLength = MaxMessageLength; + if(messageText.Length > maxMsgLength) + messageText = messageText[..maxMsgLength]; + + messageText = messageText.Trim(); + +#if DEBUG + Logger.Write($"<{ctx.Session.Id} {user.Username}> {messageText}"); +#endif + + IChatMessage message = null; + + if(messageText.StartsWith("/")) { + ChatCommandContext context = new(messageText, ctx.Chat, user, ctx.Session, channel); + + IChatCommand command = null; + + foreach(IChatCommand cmd in Commands) + if(cmd.IsMatch(context)) { + command = cmd; + break; + } + + if(command != null) { + if(command is ActionCommand actionCommand) + message = actionCommand.ActionDispatch(context); + else { + command.Dispatch(context); + return; + } + } + } + + message ??= new ChatMessage { + Target = channel, + TargetName = channel.TargetName, + DateTime = DateTimeOffset.UtcNow, + Sender = user, + Text = messageText, + }; + + lock(ctx.Chat.EventsAccess) { + ctx.Chat.Events.AddEvent(message); + channel.Send(new ChatMessageAddPacket(message)); + } + } + } +} diff --git a/SharpChat/SockChatServer.cs b/SharpChat/SockChatServer.cs index 2181575..4c50127 100644 --- a/SharpChat/SockChatServer.cs +++ b/SharpChat/SockChatServer.cs @@ -1,13 +1,12 @@ using Fleck; using SharpChat.Commands; using SharpChat.Config; -using SharpChat.Events; using SharpChat.EventStorage; using SharpChat.Misuzu; using SharpChat.Packet; +using SharpChat.PacketHandlers; using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Net.Http; using System.Threading; @@ -39,7 +38,9 @@ namespace SharpChat { private readonly CachedValue MaxConnections; private readonly CachedValue FloodKickLength; - private List Commands { get; } = new(); + private readonly List GuestHandlers = new(); + private readonly List AuthedHandlers = new(); + private readonly SendMessageHandler SendMessageHandler; private bool IsShuttingDown = false; @@ -76,7 +77,14 @@ namespace SharpChat { DefaultChannel ??= channelInfo; } - Commands.AddRange(new IChatCommand[] { + GuestHandlers.Add(new AuthHandler(Misuzu, DefaultChannel, MaxMessageLength, MaxConnections)); + + AuthedHandlers.AddRange(new IChatPacketHandler[] { + new PingHandler(Misuzu), + SendMessageHandler = new SendMessageHandler(MaxMessageLength), + }); + + SendMessageHandler.AddCommands(new IChatCommand[] { new AFKCommand(), new NickCommand(), new WhisperCommand(), @@ -104,7 +112,7 @@ namespace SharpChat { public void Listen(ManualResetEvent waitHandle) { if(waitHandle != null) - Commands.Add(new ShutdownRestartCommand(waitHandle, () => !IsShuttingDown && (IsShuttingDown = true))); + SendMessageHandler.AddCommand(new ShutdownRestartCommand(waitHandle, () => !IsShuttingDown && (IsShuttingDown = true))); Server.Start(sock => { if(IsShuttingDown || IsDisposed) { @@ -171,10 +179,6 @@ namespace SharpChat { Context.Update(); } - private readonly object BumpAccess = new(); - private readonly TimeSpan BumpInterval = TimeSpan.FromMinutes(1); - private DateTimeOffset LastBump = DateTimeOffset.MinValue; - private void OnMessage(IWebSocketConnection conn, string msg) { Context.Update(); @@ -187,6 +191,7 @@ namespace SharpChat { return; } + // this doesn't affect non-authed connections????? if(sess.User is not null && sess.User.HasFloodProtection) { sess.User.RateLimiter.AddTimePoint(); @@ -208,217 +213,12 @@ namespace SharpChat { sess.User.Send(new FloodWarningPacket()); } - string[] args = msg.Split('\t'); - if(args.Length < 1) - return; + ChatPacketHandlerContext context = new(msg, Context, sess); + IChatPacketHandler handler = sess.User is null + ? GuestHandlers.FirstOrDefault(h => h.IsMatch(context)) + : AuthedHandlers.FirstOrDefault(h => h.IsMatch(context)); - switch(args[0]) { - case "0": - if(!int.TryParse(args[1], out int pTime)) - break; - - sess.BumpPing(); - sess.Send(new PongPacket(sess.LastPing)); - - lock(BumpAccess) { - if(LastBump < DateTimeOffset.UtcNow - BumpInterval) { - (string, string)[] bumpList; - lock(Context.UsersAccess) - bumpList = Context.Users - .Where(u => u.HasSessions && u.Status == ChatUserStatus.Online) - .Select(u => (u.UserId.ToString(), u.RemoteAddresses.FirstOrDefault()?.ToString() ?? string.Empty)) - .ToArray(); - - if(bumpList.Any()) - Task.Run(async () => { - await Misuzu.BumpUsersOnlineAsync(bumpList); - }).Wait(); - - LastBump = DateTimeOffset.UtcNow; - } - } - break; - - case "1": - if(sess.User != null) - break; - - string authMethod = args.ElementAtOrDefault(1); - if(string.IsNullOrWhiteSpace(authMethod)) { - sess.Send(new AuthFailPacket(AuthFailReason.AuthInvalid)); - sess.Dispose(); - break; - } - - string authToken = args.ElementAtOrDefault(2); - if(string.IsNullOrWhiteSpace(authToken)) { - sess.Send(new AuthFailPacket(AuthFailReason.AuthInvalid)); - sess.Dispose(); - break; - } - - if(authMethod.All(c => c is >= '0' and <= '9') && authToken.Contains(':')) { - string[] tokenParts = authToken.Split(':', 2); - authMethod = tokenParts[0]; - authToken = tokenParts[1]; - } - - Task.Run(async () => { - MisuzuAuthInfo fai; - string ipAddr = sess.RemoteAddress.ToString(); - - try { - fai = await Misuzu.AuthVerifyAsync(authMethod, authToken, ipAddr); - } catch(Exception ex) { - Logger.Write($"<{sess.Id}> Failed to authenticate: {ex}"); - sess.Send(new AuthFailPacket(AuthFailReason.AuthInvalid)); - sess.Dispose(); -#if DEBUG - throw; -#else - return; -#endif - } - - if(!fai.Success) { - Logger.Debug($"<{sess.Id}> Auth fail: {fai.Reason}"); - sess.Send(new AuthFailPacket(AuthFailReason.AuthInvalid)); - sess.Dispose(); - return; - } - - MisuzuBanInfo fbi; - try { - fbi = await Misuzu.CheckBanAsync(fai.UserId.ToString(), ipAddr); - } catch(Exception ex) { - Logger.Write($"<{sess.Id}> Failed auth ban check: {ex}"); - sess.Send(new AuthFailPacket(AuthFailReason.AuthInvalid)); - sess.Dispose(); -#if DEBUG - throw; -#else - return; -#endif - } - - if(fbi.IsBanned && !fbi.HasExpired) { - Logger.Write($"<{sess.Id}> User is banned."); - sess.Send(new AuthFailPacket(AuthFailReason.Banned, fbi)); - sess.Dispose(); - return; - } - - lock(Context.UsersAccess) { - ChatUser aUser = Context.Users.FirstOrDefault(u => u.UserId == fai.UserId); - - if(aUser == null) - aUser = new ChatUser(fai); - else { - aUser.ApplyAuth(fai); - aUser.Channel?.Send(new UserUpdatePacket(aUser)); - } - - // Enforce a maximum amount of connections per user - if(aUser.SessionCount >= MaxConnections) { - sess.Send(new AuthFailPacket(AuthFailReason.MaxSessions)); - sess.Dispose(); - return; - } - - // Bumping the ping to prevent upgrading - sess.BumpPing(); - - aUser.AddSession(sess); - - sess.Send(new LegacyCommandResponse(LCR.WELCOME, false, $"Welcome to Flashii Chat, {aUser.Username}!")); - - if(File.Exists("welcome.txt")) { - IEnumerable lines = File.ReadAllLines("welcome.txt").Where(x => !string.IsNullOrWhiteSpace(x)); - string line = lines.ElementAtOrDefault(RNG.Next(lines.Count())); - - if(!string.IsNullOrWhiteSpace(line)) - sess.Send(new LegacyCommandResponse(LCR.WELCOME, false, line)); - } - - Context.HandleJoin(aUser, DefaultChannel, sess, MaxMessageLength); - } - }).Wait(); - break; - - case "2": - if(args.Length < 3) - break; - - ChatUser mUser = sess.User; - - // No longer concats everything after index 1 with \t, no previous implementation did that either - string messageText = args.ElementAtOrDefault(2); - - if(mUser == null || !mUser.Can(ChatUserPermissions.SendMessage) || string.IsNullOrWhiteSpace(messageText)) - break; - - // Extra validation step, not necessary at all but enforces proper formatting in SCv1. - if(!long.TryParse(args[1], out long mUserId) || mUser.UserId != mUserId) - break; - ChatChannel mChannel = mUser.CurrentChannel; - - if(mChannel == null - || !mUser.InChannel(mChannel) - || (mUser.IsSilenced && !mUser.Can(ChatUserPermissions.SilenceUser))) - break; - - if(mUser.Status != ChatUserStatus.Online) { - mUser.Status = ChatUserStatus.Online; - mChannel.Send(new UserUpdatePacket(mUser)); - } - - int maxMsgLength = MaxMessageLength; - if(messageText.Length > maxMsgLength) - messageText = messageText[..maxMsgLength]; - - messageText = messageText.Trim(); - -#if DEBUG - Logger.Write($"<{sess.Id} {mUser.Username}> {messageText}"); -#endif - - IChatMessage message = null; - - if(messageText.StartsWith("/")) { - ChatCommandContext context = new(messageText, Context, mUser, sess, mChannel); - - IChatCommand command = null; - - foreach(IChatCommand cmd in Commands) - if(cmd.IsMatch(context)) { - command = cmd; - break; - } - - if(command != null) { - if(command is ActionCommand actionCommand) - message = actionCommand.ActionDispatch(context); - else { - command.Dispatch(context); - break; - } - } - } - - message ??= new ChatMessage { - Target = mChannel, - TargetName = mChannel.TargetName, - DateTime = DateTimeOffset.UtcNow, - Sender = mUser, - Text = messageText, - }; - - lock(Context.EventsAccess) { - Context.Events.AddEvent(message); - mChannel.Send(new ChatMessageAddPacket(message)); - } - break; - } + handler?.Handle(context); } ~SockChatServer() {