using SharpChat.Auth; using SharpChat.Bans; using SharpChat.Channels; using SharpChat.Configuration; using SharpChat.Connections; using SharpChat.Messages; using SharpChat.Snowflake; using SharpChat.SockChat.S2CPackets; using SharpChat.Users; using ZLogger; namespace SharpChat.C2SPacketHandlers; public class AuthC2SPacketHandler( AuthClient authClient, BansClient bansClient, ChannelsContext channelsCtx, RandomSnowflake snowflake, CachedValue<int> maxMsgLength, CachedValue<int> maxConns ) : C2SPacketHandler { public bool IsMatch(C2SPacketHandlerContext ctx) { return ctx.CheckPacketId("1"); } public async Task Handle(C2SPacketHandlerContext ctx) { if(ctx.Session is not null) return; string[] args = ctx.SplitText(3); string? authMethod = args.ElementAtOrDefault(1); string? authToken = args.ElementAtOrDefault(2); if(string.IsNullOrWhiteSpace(authMethod) || string.IsNullOrWhiteSpace(authToken)) { ctx.Logger.ZLogInformation($"Received empty authentication information."); await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.AuthInvalid)); ctx.Connection.Close(ConnectionCloseReason.Unauthorized); return; } if(authMethod.All(c => c is >= '0' and <= '9') && authToken.Contains(':')) { string[] tokenParts = authToken.Split(':', 2); authMethod = tokenParts[0]; authToken = tokenParts[1]; } try { AuthResult authResult = await authClient.AuthVerify( ctx.Connection.RemoteEndPoint.Address, authMethod, authToken ); BanInfo? banInfo = await bansClient.BanGet(authResult.UserId, ctx.Connection.RemoteEndPoint.Address); if(banInfo is not null) { ctx.Logger.ZLogInformation($"User {authResult.UserId} is banned."); await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.Banned, banInfo.IsPermanent ? DateTimeOffset.MaxValue : banInfo.ExpiresAt)); ctx.Connection.Close(ConnectionCloseReason.AccessDenied); return; } await ctx.Chat.ContextAccess.WaitAsync(); try { User user = ctx.Chat.Users.CreateOrUpdateUser(authResult); // Enforce a maximum amount of connections per user if(ctx.Chat.Sessions.CountNonSuspendedActiveSessions(user) >= maxConns) { ctx.Logger.ZLogInformation($"Too many active connections."); await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.MaxSessions)); ctx.Connection.Close(ConnectionCloseReason.TooManyConnections); return; } ctx.Chat.Sessions.CreateSession(user, ctx.Connection); await ctx.Connection.Send(new CommandResponseS2CPacket(0, LCR.WELCOME, false, $"Welcome to Flashii Chat, {user.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)) await ctx.Connection.Send(new CommandResponseS2CPacket(0, LCR.WELCOME, false, line)); } Channel channel = channelsCtx.GetDefaultChannel(); if(!ctx.Chat.ChannelsUsers.HasChannelUser(channel, user)) { long msgId = snowflake.Next(); await ctx.Chat.SendTo(channel, new UserConnectS2CPacket(msgId, DateTimeOffset.Now, user.UserId, user.GetLegacyNameWithStatus(), user.Colour, user.Rank, user.Permissions)); await ctx.Chat.Messages.LogMessage(msgId, "user:connect", channel.Name, user.UserId, user.UserName, user.Colour, user.Rank, user.NickName, user.Permissions); } await ctx.Connection.Send(new AuthSuccessS2CPacket( user.UserId, user.GetLegacyNameWithStatus(), user.Colour, user.Rank, user.Permissions, channel.Name, maxMsgLength )); await ctx.Connection.Send(new ContextUsersS2CPacket( ctx.Chat.ChannelsUsers.GetChannelUsers(channel).Except([user]).OrderByDescending(u => u.Rank) .Select(u => new ContextUsersS2CPacket.Entry( u.UserId, u.GetLegacyNameWithStatus(), u.Colour, u.Rank, u.Permissions, true )) )); IEnumerable<Message> msgs = await ctx.Chat.Messages.GetMessages(channel.Name); foreach(Message msg in msgs) await ctx.Connection.Send(new ContextMessageS2CPacket(msg)); await ctx.Connection.Send(new ContextChannelsS2CPacket( ctx.Chat.Channels.GetChannels(user.Rank) .Select(c => new ContextChannelsS2CPacket.Entry(c.Name, c.HasPassword, c.IsTemporary)) )); ctx.Chat.ChannelsUsers.AddChannelUser(channel, user); } finally { ctx.Chat.ContextAccess.Release(); } } catch(AuthFailedException ex) { ctx.Chat.Sessions.DestroySession(ctx.Connection); ctx.Logger.ZLogWarning($"Failed to authenticate (expected): {ex}"); await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.AuthInvalid)); ctx.Connection.Close(ConnectionCloseReason.Unauthorized); throw; } catch(Exception ex) { ctx.Chat.Sessions.DestroySession(ctx.Connection); ctx.Logger.ZLogError($"Failed to authenticate (unexpected): {ex}"); await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.Exception)); ctx.Connection.Close(ConnectionCloseReason.Error); throw; } } }