using SharpChat.Auth; using SharpChat.Bans; using SharpChat.Configuration; using SharpChat.SockChat.S2CPackets; namespace SharpChat.C2SPacketHandlers; public class AuthC2SPacketHandler( AuthClient authClient, BansClient bansClient, Channel defaultChannel, CachedValue<int> maxMsgLength, CachedValue<int> maxConns ) : C2SPacketHandler { private readonly Channel DefaultChannel = defaultChannel ?? throw new ArgumentNullException(nameof(defaultChannel)); private readonly CachedValue<int> MaxMessageLength = maxMsgLength ?? throw new ArgumentNullException(nameof(maxMsgLength)); private readonly CachedValue<int> MaxConnections = maxConns ?? throw new ArgumentNullException(nameof(maxConns)); public bool IsMatch(C2SPacketHandlerContext ctx) { return ctx.CheckPacketId("1"); } public async Task Handle(C2SPacketHandlerContext ctx) { string[] args = ctx.SplitText(3); string? authMethod = args.ElementAtOrDefault(1); string? authToken = args.ElementAtOrDefault(2); if(string.IsNullOrWhiteSpace(authMethod) || string.IsNullOrWhiteSpace(authToken)) { await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.AuthInvalid)); ctx.Connection.Dispose(); 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.AuthVerifyAsync( ctx.Connection.RemoteAddress, authMethod, authToken ); BanInfo? banInfo = await bansClient.BanGetAsync(authResult.UserId, ctx.Connection.RemoteAddress); if(banInfo is not null) { Logger.Write($"<{ctx.Connection.Id}> User is banned."); await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.Banned, banInfo.IsPermanent ? DateTimeOffset.MaxValue : banInfo.ExpiresAt)); ctx.Connection.Dispose(); return; } await ctx.Chat.ContextAccess.WaitAsync(); try { User? user = ctx.Chat.Users.FirstOrDefault(u => u.UserId == authResult.UserId); if(user == null) user = new User( authResult.UserId, authResult.UserName ?? $"({authResult.UserId})", authResult.UserColour, authResult.UserRank, authResult.UserPermissions ); else await ctx.Chat.UpdateUser( user, userName: authResult.UserName ?? $"({authResult.UserId})", colour: authResult.UserColour, rank: authResult.UserRank, perms: authResult.UserPermissions ); // Enforce a maximum amount of connections per user if(ctx.Chat.Connections.Count(conn => conn.User == user) >= MaxConnections) { await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.MaxSessions)); ctx.Connection.Dispose(); return; } ctx.Connection.BumpPing(); ctx.Connection.User = user; 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)); } await ctx.Chat.HandleJoin(user, DefaultChannel, ctx.Connection, MaxMessageLength); } finally { ctx.Chat.ContextAccess.Release(); } } catch(AuthFailedException ex) { Logger.Write($"<{ctx.Connection.Id}> Failed to authenticate: {ex}"); await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.AuthInvalid)); ctx.Connection.Dispose(); throw; } catch(Exception ex) { Logger.Write($"<{ctx.Connection.Id}> Failed to authenticate: {ex}"); await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.Exception)); ctx.Connection.Dispose(); throw; } } }