Un-switch packet handlers.

This commit is contained in:
flash 2023-02-16 22:16:06 +01:00
parent ea56af0210
commit c8a589c1c1
6 changed files with 345 additions and 219 deletions

View file

@ -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);
}
}
}

View file

@ -0,0 +1,6 @@
namespace SharpChat {
public interface IChatPacketHandler {
bool IsMatch(ChatPacketHandlerContext ctx);
void Handle(ChatPacketHandlerContext ctx);
}
}

View file

@ -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<int> MaxMessageLength;
private readonly CachedValue<int> MaxConnections;
public AuthHandler(
MisuzuClient msz,
ChatChannel defaultChannel,
CachedValue<int> maxMsgLength,
CachedValue<int> 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<string> 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();
}
}
}

View file

@ -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;
}
}
}
}
}

View file

@ -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<int> MaxMessageLength;
private List<IChatCommand> Commands { get; } = new();
public SendMessageHandler(CachedValue<int> 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<IChatCommand> 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));
}
}
}
}

View file

@ -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<int> MaxConnections;
private readonly CachedValue<int> FloodKickLength;
private List<IChatCommand> Commands { get; } = new();
private readonly List<IChatPacketHandler> GuestHandlers = new();
private readonly List<IChatPacketHandler> 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<string> 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() {