diff --git a/SharpChat/ChatContext.cs b/SharpChat/ChatContext.cs index b6b2739..ed4557b 100644 --- a/SharpChat/ChatContext.cs +++ b/SharpChat/ChatContext.cs @@ -1,79 +1,74 @@ -using Fleck; -using SharpChat.Events; +using SharpChat.Events; using SharpChat.EventStorage; using SharpChat.Packet; using System; using System.Collections.Generic; using System.Linq; using System.Net; +using System.Threading; namespace SharpChat { public class ChatContext { public record ChannelUserAssoc(long UserId, string ChannelName); + public readonly SemaphoreSlim ContextAccess = new(1, 1); + public HashSet Channels { get; } = new(); - public readonly object ChannelsAccess = new(); - public HashSet Connections { get; } = new(); - public readonly object ConnectionsAccess = new(); - public HashSet Users { get; } = new(); - public readonly object UsersAccess = new(); - public IEventStorage Events { get; } - public readonly object EventsAccess = new(); - public HashSet ChannelUsers { get; } = new(); - public readonly object ChannelUsersAccess = new(); + public Dictionary UserRateLimiters { get; } = new(); public ChatContext(IEventStorage evtStore) { Events = evtStore ?? throw new ArgumentNullException(nameof(evtStore)); } public void Update() { - lock(ConnectionsAccess) { - foreach(ChatConnection conn in Connections) - if(!conn.IsDisposed && conn.HasTimedOut) { - conn.Dispose(); - Logger.Write($"Nuked connection {conn.Id} associated with {conn.User}."); - } + foreach(ChatConnection conn in Connections) + if(!conn.IsDisposed && conn.HasTimedOut) { + conn.Dispose(); + Logger.Write($"Nuked connection {conn.Id} associated with {conn.User}."); + } - Connections.RemoveWhere(conn => conn.IsDisposed); + Connections.RemoveWhere(conn => conn.IsDisposed); - lock(UsersAccess) - foreach(ChatUser user in Users) - if(!Connections.Any(conn => conn.User == user)) { - HandleDisconnect(user, UserDisconnectReason.TimeOut); - Logger.Write($"Timed out {user} (no more connections)."); - } + foreach(ChatUser user in Users) + if(!Connections.Any(conn => conn.User == user)) { + HandleDisconnect(user, UserDisconnectReason.TimeOut); + Logger.Write($"Timed out {user} (no more connections)."); + } + } + + public void SafeUpdate() { + ContextAccess.Wait(); + try { + Update(); + } finally { + ContextAccess.Release(); } } public bool IsInChannel(ChatUser user, ChatChannel channel) { - lock(ChannelUsersAccess) - return ChannelUsers.Contains(new ChannelUserAssoc(user.UserId, channel.Name)); + return ChannelUsers.Contains(new ChannelUserAssoc(user.UserId, channel.Name)); } public string[] GetUserChannelNames(ChatUser user) { - lock(ChannelUsersAccess) - return ChannelUsers.Where(cu => cu.UserId == user.UserId).Select(cu => cu.ChannelName).ToArray(); + return ChannelUsers.Where(cu => cu.UserId == user.UserId).Select(cu => cu.ChannelName).ToArray(); } public ChatChannel[] GetUserChannels(ChatUser user) { string[] names = GetUserChannelNames(user); - lock(ChannelsAccess) - return Channels.Where(c => names.Any(n => c.NameEquals(n))).ToArray(); + return Channels.Where(c => names.Any(n => c.NameEquals(n))).ToArray(); } public long[] GetChannelUserIds(ChatChannel channel) { - lock(ChannelUsersAccess) - return ChannelUsers.Where(cu => channel.NameEquals(cu.ChannelName)).Select(cu => cu.UserId).ToArray(); + return ChannelUsers.Where(cu => channel.NameEquals(cu.ChannelName)).Select(cu => cu.UserId).ToArray(); } public ChatUser[] GetChannelUsers(ChatChannel channel) { long[] ids = GetChannelUserIds(channel); - lock(UsersAccess) - return Users.Where(u => ids.Contains(u.UserId)).ToArray(); + return Users.Where(u => ids.Contains(u.UserId)).ToArray(); } public void BanUser(ChatUser user, TimeSpan duration, UserDisconnectReason reason = UserDisconnectReason.Kicked) { @@ -82,63 +77,48 @@ namespace SharpChat { else SendTo(user, new ForceDisconnectPacket(ForceDisconnectReason.Kicked)); - lock(ConnectionsAccess) { - foreach(ChatConnection conn in Connections) - if(conn.User == user) - conn.Dispose(); - Connections.RemoveWhere(conn => conn.IsDisposed); - } + foreach(ChatConnection conn in Connections) + if(conn.User == user) + conn.Dispose(); + Connections.RemoveWhere(conn => conn.IsDisposed); HandleDisconnect(user, reason); } public void HandleJoin(ChatUser user, ChatChannel chan, ChatConnection conn, int maxMsgLength) { - lock(EventsAccess) { - if(!IsInChannel(user, chan)) { - SendTo(chan, new UserConnectPacket(DateTimeOffset.Now, user)); - Events.AddEvent(new UserConnectEvent(DateTimeOffset.Now, user, chan)); - } - - conn.Send(new AuthSuccessPacket(user, chan, conn, maxMsgLength)); - conn.Send(new ContextUsersPacket(GetChannelUsers(chan).Except(new[] { user }).OrderByDescending(u => u.Rank))); - - foreach(IChatEvent msg in Events.GetChannelEventLog(chan.Name)) - conn.Send(new ContextMessagePacket(msg)); - - lock(ChannelsAccess) - conn.Send(new ContextChannelsPacket(Channels.Where(c => c.Rank <= user.Rank))); - - lock(UsersAccess) - Users.Add(user); - - lock(ChannelUsersAccess) { - ChannelUsers.Add(new ChannelUserAssoc(user.UserId, chan.Name)); - user.CurrentChannel = chan; - } + if(!IsInChannel(user, chan)) { + SendTo(chan, new UserConnectPacket(DateTimeOffset.Now, user)); + Events.AddEvent(new UserConnectEvent(DateTimeOffset.Now, user, chan)); } + + conn.Send(new AuthSuccessPacket(user, chan, conn, maxMsgLength)); + conn.Send(new ContextUsersPacket(GetChannelUsers(chan).Except(new[] { user }).OrderByDescending(u => u.Rank))); + + foreach(IChatEvent msg in Events.GetChannelEventLog(chan.Name)) + conn.Send(new ContextMessagePacket(msg)); + + conn.Send(new ContextChannelsPacket(Channels.Where(c => c.Rank <= user.Rank))); + + Users.Add(user); + + ChannelUsers.Add(new ChannelUserAssoc(user.UserId, chan.Name)); + user.CurrentChannel = chan; } public void HandleDisconnect(ChatUser user, UserDisconnectReason reason = UserDisconnectReason.Leave) { user.Status = ChatUserStatus.Offline; + Users.Remove(user); - lock(EventsAccess) { - lock(UsersAccess) - Users.Remove(user); + ChatChannel[] channels = GetUserChannels(user); - lock(ChannelUsersAccess) { - ChatChannel[] channels = GetUserChannels(user); + foreach(ChatChannel chan in channels) { + ChannelUsers.Remove(new ChannelUserAssoc(user.UserId, chan.Name)); - foreach(ChatChannel chan in channels) { - ChannelUsers.Remove(new ChannelUserAssoc(user.UserId, chan.Name)); + SendTo(chan, new UserDisconnectPacket(DateTimeOffset.Now, user, reason)); + Events.AddEvent(new UserDisconnectEvent(DateTimeOffset.Now, user, chan, reason)); - SendTo(chan, new UserDisconnectPacket(DateTimeOffset.Now, user, reason)); - Events.AddEvent(new UserDisconnectEvent(DateTimeOffset.Now, user, chan, reason)); - - if(chan.IsTemporary && chan.Owner == user) - lock(ChannelsAccess) - RemoveChannel(chan); - } - } + if(chan.IsTemporary && chan.Owner == user) + RemoveChannel(chan); } } @@ -166,46 +146,39 @@ namespace SharpChat { } public void ForceChannelSwitch(ChatUser user, ChatChannel chan) { - lock(ChannelsAccess) - if(!Channels.Contains(chan)) - return; + if(!Channels.Contains(chan)) + return; ChatChannel oldChan = user.CurrentChannel; - lock(EventsAccess) { - SendTo(oldChan, new UserChannelLeavePacket(user)); - Events.AddEvent(new UserChannelLeaveEvent(DateTimeOffset.Now, user, oldChan)); - SendTo(chan, new UserChannelJoinPacket(user)); - Events.AddEvent(new UserChannelJoinEvent(DateTimeOffset.Now, user, chan)); + SendTo(oldChan, new UserChannelLeavePacket(user)); + Events.AddEvent(new UserChannelLeaveEvent(DateTimeOffset.Now, user, oldChan)); + SendTo(chan, new UserChannelJoinPacket(user)); + Events.AddEvent(new UserChannelJoinEvent(DateTimeOffset.Now, user, chan)); - SendTo(user, new ContextClearPacket(chan, ContextClearMode.MessagesUsers)); - SendTo(user, new ContextUsersPacket(GetChannelUsers(chan).Except(new[] { user }).OrderByDescending(u => u.Rank))); + SendTo(user, new ContextClearPacket(chan, ContextClearMode.MessagesUsers)); + SendTo(user, new ContextUsersPacket(GetChannelUsers(chan).Except(new[] { user }).OrderByDescending(u => u.Rank))); - foreach(IChatEvent msg in Events.GetChannelEventLog(chan.Name)) - SendTo(user, new ContextMessagePacket(msg)); + foreach(IChatEvent msg in Events.GetChannelEventLog(chan.Name)) + SendTo(user, new ContextMessagePacket(msg)); - ForceChannel(user, chan); + ForceChannel(user, chan); - lock(ChannelUsersAccess) { - ChannelUsers.Remove(new ChannelUserAssoc(user.UserId, oldChan.Name)); - ChannelUsers.Add(new ChannelUserAssoc(user.UserId, chan.Name)); - user.CurrentChannel = chan; - } - } + ChannelUsers.Remove(new ChannelUserAssoc(user.UserId, oldChan.Name)); + ChannelUsers.Add(new ChannelUserAssoc(user.UserId, chan.Name)); + user.CurrentChannel = chan; if(oldChan.IsTemporary && oldChan.Owner == user) - lock(ChannelsAccess) - RemoveChannel(oldChan); + RemoveChannel(oldChan); } public void Send(IServerPacket packet) { if(packet == null) throw new ArgumentNullException(nameof(packet)); - lock(ConnectionsAccess) - foreach(ChatConnection conn in Connections) - if(conn.IsAuthed) - conn.Send(packet); + foreach(ChatConnection conn in Connections) + if(conn.IsAuthed) + conn.Send(packet); } public void SendTo(ChatUser user, IServerPacket packet) { @@ -214,10 +187,9 @@ namespace SharpChat { if(packet == null) throw new ArgumentNullException(nameof(packet)); - lock(ConnectionsAccess) - foreach(ChatConnection conn in Connections) - if(conn.IsAlive && conn.User == user) - conn.Send(packet); + foreach(ChatConnection conn in Connections) + if(conn.IsAlive && conn.User == user) + conn.Send(packet); } public void SendTo(ChatChannel channel, IServerPacket packet) { @@ -227,16 +199,13 @@ namespace SharpChat { throw new ArgumentNullException(nameof(packet)); // might be faster to grab the users first and then cascade into that SendTo - lock(ConnectionsAccess) { - IEnumerable conns = Connections.Where(c => c.IsAuthed && IsInChannel(c.User, channel)); - foreach(ChatConnection conn in conns) - conn.Send(packet); - } + IEnumerable conns = Connections.Where(c => c.IsAuthed && IsInChannel(c.User, channel)); + foreach(ChatConnection conn in conns) + conn.Send(packet); } public IPAddress[] GetRemoteAddresses(ChatUser user) { - lock(ConnectionsAccess) - return Connections.Where(c => c.IsAlive && c.User == user).Select(c => c.RemoteAddress).Distinct().ToArray(); + return Connections.Where(c => c.IsAlive && c.User == user).Select(c => c.RemoteAddress).Distinct().ToArray(); } public void ForceChannel(ChatUser user, ChatChannel chan = null) { @@ -273,13 +242,12 @@ namespace SharpChat { channel.Password = password; // Users that no longer have access to the channel/gained access to the channel by the hierarchy change should receive delete and create packets respectively - lock(UsersAccess) - foreach(ChatUser user in Users.Where(u => u.Rank >= channel.Rank)) { - SendTo(user, new ChannelUpdatePacket(prevName, channel)); + foreach(ChatUser user in Users.Where(u => u.Rank >= channel.Rank)) { + SendTo(user, new ChannelUpdatePacket(prevName, channel)); - if(nameUpdated) - ForceChannel(user); - } + if(nameUpdated) + ForceChannel(user); + } } public void RemoveChannel(ChatChannel channel) { @@ -299,9 +267,8 @@ namespace SharpChat { SwitchChannel(user, defaultChannel, string.Empty); // Broadcast deletion of channel - lock(UsersAccess) - foreach(ChatUser user in Users.Where(u => u.Rank >= channel.Rank)) - SendTo(user, new ChannelDeletePacket(channel)); + foreach(ChatUser user in Users.Where(u => u.Rank >= channel.Rank)) + SendTo(user, new ChannelDeletePacket(channel)); } } } diff --git a/SharpChat/ChatUser.cs b/SharpChat/ChatUser.cs index 24c9a57..6d8e489 100644 --- a/SharpChat/ChatUser.cs +++ b/SharpChat/ChatUser.cs @@ -22,8 +22,6 @@ namespace SharpChat { public bool HasFloodProtection => Rank < RANK_NO_FLOOD; - public readonly RateLimiter RateLimiter = new(DEFAULT_SIZE, DEFAULT_MINIMUM_DELAY, DEFAULT_RISKY_OFFSET); - // This needs to be a session thing public ChatChannel CurrentChannel { get; set; } diff --git a/SharpChat/Commands/CreateChannelCommand.cs b/SharpChat/Commands/CreateChannelCommand.cs index 018d8b0..e9d9713 100644 --- a/SharpChat/Commands/CreateChannelCommand.cs +++ b/SharpChat/Commands/CreateChannelCommand.cs @@ -38,28 +38,24 @@ namespace SharpChat.Commands { return; } - lock(ctx.Chat.ChannelsAccess) { - if(ctx.Chat.Channels.Any(c => c.NameEquals(createChanName))) { - ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.CHANNEL_ALREADY_EXISTS, true, createChanName)); - return; - } - - ChatChannel createChan = new() { - Name = createChanName, - IsTemporary = !ctx.User.Can(ChatUserPermissions.SetChannelPermanent), - Rank = createChanHierarchy, - Owner = ctx.User, - }; - - ctx.Chat.Channels.Add(createChan); - lock(ctx.Chat.UsersAccess) { - foreach(ChatUser ccu in ctx.Chat.Users.Where(u => u.Rank >= ctx.Channel.Rank)) - ctx.Chat.SendTo(ccu, new ChannelCreatePacket(ctx.Channel)); - } - - ctx.Chat.SwitchChannel(ctx.User, createChan, createChan.Password); - ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.CHANNEL_CREATED, false, createChan.Name)); + if(ctx.Chat.Channels.Any(c => c.NameEquals(createChanName))) { + ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.CHANNEL_ALREADY_EXISTS, true, createChanName)); + return; } + + ChatChannel createChan = new() { + Name = createChanName, + IsTemporary = !ctx.User.Can(ChatUserPermissions.SetChannelPermanent), + Rank = createChanHierarchy, + Owner = ctx.User, + }; + + ctx.Chat.Channels.Add(createChan); + foreach(ChatUser ccu in ctx.Chat.Users.Where(u => u.Rank >= ctx.Channel.Rank)) + ctx.Chat.SendTo(ccu, new ChannelCreatePacket(ctx.Channel)); + + ctx.Chat.SwitchChannel(ctx.User, createChan, createChan.Password); + ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.CHANNEL_CREATED, false, createChan.Name)); } } } diff --git a/SharpChat/Commands/DeleteChannelCommand.cs b/SharpChat/Commands/DeleteChannelCommand.cs index c975729..94d4563 100644 --- a/SharpChat/Commands/DeleteChannelCommand.cs +++ b/SharpChat/Commands/DeleteChannelCommand.cs @@ -17,9 +17,7 @@ namespace SharpChat.Commands { } string delChanName = string.Join('_', ctx.Args); - ChatChannel delChan; - lock(ctx.Chat.ChannelsAccess) - delChan = ctx.Chat.Channels.FirstOrDefault(c => c.NameEquals(delChanName)); + ChatChannel delChan = ctx.Chat.Channels.FirstOrDefault(c => c.NameEquals(delChanName)); if(delChan == null) { ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.CHANNEL_NOT_FOUND, true, delChanName)); @@ -31,8 +29,7 @@ namespace SharpChat.Commands { return; } - lock(ctx.Chat.ChannelsAccess) - ctx.Chat.RemoveChannel(delChan); + ctx.Chat.RemoveChannel(delChan); ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.CHANNEL_DELETED, false, delChan.Name)); } } diff --git a/SharpChat/Commands/DeleteMessageCommand.cs b/SharpChat/Commands/DeleteMessageCommand.cs index f79b7b8..0213bb2 100644 --- a/SharpChat/Commands/DeleteMessageCommand.cs +++ b/SharpChat/Commands/DeleteMessageCommand.cs @@ -26,17 +26,15 @@ namespace SharpChat.Commands { return; } - lock(ctx.Chat.EventsAccess) { - IChatEvent delMsg = ctx.Chat.Events.GetEvent(delSeqId); + IChatEvent delMsg = ctx.Chat.Events.GetEvent(delSeqId); - if(delMsg == null || delMsg.Sender.Rank > ctx.User.Rank || (!deleteAnyMessage && delMsg.Sender.UserId != ctx.User.UserId)) { - ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.MESSAGE_DELETE_ERROR)); - return; - } - - ctx.Chat.Events.RemoveEvent(delMsg); - ctx.Chat.Send(new ChatMessageDeletePacket(delMsg.SequenceId)); + if(delMsg == null || delMsg.Sender.Rank > ctx.User.Rank || (!deleteAnyMessage && delMsg.Sender.UserId != ctx.User.UserId)) { + ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.MESSAGE_DELETE_ERROR)); + return; } + + ctx.Chat.Events.RemoveEvent(delMsg); + ctx.Chat.Send(new ChatMessageDeletePacket(delMsg.SequenceId)); } } } diff --git a/SharpChat/Commands/JoinChannelCommand.cs b/SharpChat/Commands/JoinChannelCommand.cs index a2b90cc..0e20dfe 100644 --- a/SharpChat/Commands/JoinChannelCommand.cs +++ b/SharpChat/Commands/JoinChannelCommand.cs @@ -9,9 +9,7 @@ namespace SharpChat.Commands { public void Dispatch(ChatCommandContext ctx) { string joinChanStr = ctx.Args.FirstOrDefault(); - ChatChannel joinChan; - lock(ctx.Chat.ChannelsAccess) - joinChan = ctx.Chat.Channels.FirstOrDefault(c => c.NameEquals(joinChanStr)); + ChatChannel joinChan = ctx.Chat.Channels.FirstOrDefault(c => c.NameEquals(joinChanStr)); if(joinChan == null) { ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.CHANNEL_NOT_FOUND, true, joinChanStr)); diff --git a/SharpChat/Commands/KickBanCommand.cs b/SharpChat/Commands/KickBanCommand.cs index 9064840..7e4393c 100644 --- a/SharpChat/Commands/KickBanCommand.cs +++ b/SharpChat/Commands/KickBanCommand.cs @@ -30,11 +30,10 @@ namespace SharpChat.Commands { int banReasonIndex = 1; ChatUser banUser = null; - lock(ctx.Chat.UsersAccess) - if(banUserTarget == null || (banUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(banUserTarget))) == null) { - ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, banUser == null ? "User" : banUserTarget)); - return; - } + if(banUserTarget == null || (banUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(banUserTarget))) == null) { + ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, banUser == null ? "User" : banUserTarget)); + return; + } if(banUser == ctx.User || banUser.Rank >= ctx.User.Rank) { ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.KICK_NOT_ALLOWED, true, banUser.DisplayName)); diff --git a/SharpChat/Commands/NickCommand.cs b/SharpChat/Commands/NickCommand.cs index 9ee260e..1bb272e 100644 --- a/SharpChat/Commands/NickCommand.cs +++ b/SharpChat/Commands/NickCommand.cs @@ -18,8 +18,7 @@ namespace SharpChat.Commands { int offset = 0; if(setOthersNick && long.TryParse(ctx.Args.FirstOrDefault(), out long targetUserId) && targetUserId > 0) { - lock(ctx.Chat.UsersAccess) - targetUser = ctx.Chat.Users.FirstOrDefault(u => u.UserId == targetUserId); + targetUser = ctx.Chat.Users.FirstOrDefault(u => u.UserId == targetUserId); ++offset; } @@ -42,11 +41,10 @@ namespace SharpChat.Commands { else if(string.IsNullOrEmpty(nickStr)) nickStr = null; - lock(ctx.Chat.UsersAccess) - if(!string.IsNullOrWhiteSpace(nickStr) && ctx.Chat.Users.Any(u => u.NameEquals(nickStr))) { - ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.NAME_IN_USE, true, nickStr)); - return; - } + if(!string.IsNullOrWhiteSpace(nickStr) && ctx.Chat.Users.Any(u => u.NameEquals(nickStr))) { + ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.NAME_IN_USE, true, nickStr)); + return; + } string previousName = targetUser == ctx.User ? (targetUser.Nickname ?? targetUser.Username) : null; targetUser.Nickname = nickStr; diff --git a/SharpChat/Commands/PardonUserCommand.cs b/SharpChat/Commands/PardonUserCommand.cs index e7cb6ee..80ccbfa 100644 --- a/SharpChat/Commands/PardonUserCommand.cs +++ b/SharpChat/Commands/PardonUserCommand.cs @@ -30,13 +30,10 @@ namespace SharpChat.Commands { return; } - ChatUser unbanUser; - lock(ctx.Chat.UsersAccess) - unbanUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(unbanUserTarget)); + ChatUser unbanUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(unbanUserTarget)); if(unbanUser == null && long.TryParse(unbanUserTarget, out long unbanUserId)) { unbanUserTargetIsName = false; - lock(ctx.Chat.UsersAccess) - unbanUser = ctx.Chat.Users.FirstOrDefault(u => u.UserId == unbanUserId); + unbanUser = ctx.Chat.Users.FirstOrDefault(u => u.UserId == unbanUserId); } if(unbanUser != null) diff --git a/SharpChat/Commands/PasswordChannelCommand.cs b/SharpChat/Commands/PasswordChannelCommand.cs index 4e91b03..2eb0fb3 100644 --- a/SharpChat/Commands/PasswordChannelCommand.cs +++ b/SharpChat/Commands/PasswordChannelCommand.cs @@ -18,8 +18,7 @@ namespace SharpChat.Commands { if(string.IsNullOrWhiteSpace(chanPass)) chanPass = string.Empty; - lock(ctx.Chat.ChannelsAccess) - ctx.Chat.UpdateChannel(ctx.Channel, password: chanPass); + ctx.Chat.UpdateChannel(ctx.Channel, password: chanPass); ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.CHANNEL_PASSWORD_CHANGED, false)); } } diff --git a/SharpChat/Commands/RankChannelCommand.cs b/SharpChat/Commands/RankChannelCommand.cs index 15f80c0..4197334 100644 --- a/SharpChat/Commands/RankChannelCommand.cs +++ b/SharpChat/Commands/RankChannelCommand.cs @@ -20,8 +20,7 @@ namespace SharpChat.Commands { return; } - lock(ctx.Chat.ChannelsAccess) - ctx.Chat.UpdateChannel(ctx.Channel, hierarchy: chanHierarchy); + ctx.Chat.UpdateChannel(ctx.Channel, hierarchy: chanHierarchy); ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.CHANNEL_HIERARCHY_CHANGED, false)); } } diff --git a/SharpChat/Commands/RemoteAddressCommand.cs b/SharpChat/Commands/RemoteAddressCommand.cs index eddce85..3da853d 100644 --- a/SharpChat/Commands/RemoteAddressCommand.cs +++ b/SharpChat/Commands/RemoteAddressCommand.cs @@ -18,11 +18,10 @@ namespace SharpChat.Commands { string ipUserStr = ctx.Args.FirstOrDefault(); ChatUser ipUser; - lock(ctx.Chat.UsersAccess) - if(string.IsNullOrWhiteSpace(ipUserStr) || (ipUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(ipUserStr))) == null) { - ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, ipUserStr ?? "User")); - return; - } + if(string.IsNullOrWhiteSpace(ipUserStr) || (ipUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(ipUserStr))) == null) { + ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, ipUserStr ?? "User")); + return; + } foreach(IPAddress ip in ctx.Chat.GetRemoteAddresses(ipUser)) ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.IP_ADDRESS, false, ipUser.Username, ip)); diff --git a/SharpChat/Commands/ShutdownRestartCommand.cs b/SharpChat/Commands/ShutdownRestartCommand.cs index 627f127..ce5ab27 100644 --- a/SharpChat/Commands/ShutdownRestartCommand.cs +++ b/SharpChat/Commands/ShutdownRestartCommand.cs @@ -27,9 +27,8 @@ namespace SharpChat.Commands { return; if(ctx.NameEquals("restart")) - lock(ctx.Chat.ConnectionsAccess) - foreach(ChatConnection conn in ctx.Chat.Connections) - conn.PrepareForRestart(); + foreach(ChatConnection conn in ctx.Chat.Connections) + conn.PrepareForRestart(); ctx.Chat.Update(); WaitHandle?.Set(); diff --git a/SharpChat/Commands/SilenceApplyCommand.cs b/SharpChat/Commands/SilenceApplyCommand.cs index b14f713..eb97cbc 100644 --- a/SharpChat/Commands/SilenceApplyCommand.cs +++ b/SharpChat/Commands/SilenceApplyCommand.cs @@ -17,11 +17,10 @@ namespace SharpChat.Commands { string silUserStr = ctx.Args.FirstOrDefault(); ChatUser silUser; - lock(ctx.Chat.UsersAccess) - if(string.IsNullOrWhiteSpace(silUserStr) || (silUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(silUserStr))) == null) { - ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, silUserStr ?? "User")); - return; - } + if(string.IsNullOrWhiteSpace(silUserStr) || (silUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(silUserStr))) == null) { + ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, silUserStr ?? "User")); + return; + } if(silUser == ctx.User) { ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.SILENCE_SELF)); diff --git a/SharpChat/Commands/SilenceRevokeCommand.cs b/SharpChat/Commands/SilenceRevokeCommand.cs index a2f55c9..06c5ce8 100644 --- a/SharpChat/Commands/SilenceRevokeCommand.cs +++ b/SharpChat/Commands/SilenceRevokeCommand.cs @@ -17,11 +17,10 @@ namespace SharpChat.Commands { string unsilUserStr = ctx.Args.FirstOrDefault(); ChatUser unsilUser; - lock(ctx.Chat.UsersAccess) - if(string.IsNullOrWhiteSpace(unsilUserStr) || (unsilUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(unsilUserStr))) == null) { - ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, unsilUserStr ?? "User")); - return; - } + if(string.IsNullOrWhiteSpace(unsilUserStr) || (unsilUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(unsilUserStr))) == null) { + ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, unsilUserStr ?? "User")); + return; + } if(unsilUser.Rank >= ctx.User.Rank) { ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.UNSILENCE_HIERARCHY)); diff --git a/SharpChat/Commands/WhisperCommand.cs b/SharpChat/Commands/WhisperCommand.cs index e1eb1da..f114852 100644 --- a/SharpChat/Commands/WhisperCommand.cs +++ b/SharpChat/Commands/WhisperCommand.cs @@ -16,15 +16,14 @@ namespace SharpChat.Commands { return; } - ChatUser whisperUser; string whisperUserStr = ctx.Args.FirstOrDefault(); - lock(ctx.Chat.UsersAccess) - whisperUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(whisperUserStr)); + ChatUser whisperUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(whisperUserStr)); if(whisperUser == null) { ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, whisperUserStr)); return; } + if(whisperUser == ctx.User) return; diff --git a/SharpChat/Commands/WhoCommand.cs b/SharpChat/Commands/WhoCommand.cs index 87c843d..6eb7b60 100644 --- a/SharpChat/Commands/WhoCommand.cs +++ b/SharpChat/Commands/WhoCommand.cs @@ -13,26 +13,23 @@ namespace SharpChat.Commands { string whoChanStr = ctx.Args.FirstOrDefault(); if(string.IsNullOrEmpty(whoChanStr)) { - lock(ctx.Chat.UsersAccess) - foreach(ChatUser whoUser in ctx.Chat.Users) { - whoChanSB.Append(@"'); - whoChanSB.Append(whoUser.DisplayName); - whoChanSB.Append(", "); - } + whoChanSB.Append('>'); + whoChanSB.Append(whoUser.DisplayName); + whoChanSB.Append(", "); + } if(whoChanSB.Length > 2) whoChanSB.Length -= 2; ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.USERS_LISTING_SERVER, false, whoChanSB)); } else { - ChatChannel whoChan; - lock(ctx.Chat.ChannelsAccess) - whoChan = ctx.Chat.Channels.FirstOrDefault(c => c.NameEquals(whoChanStr)); + ChatChannel whoChan = ctx.Chat.Channels.FirstOrDefault(c => c.NameEquals(whoChanStr)); if(whoChan == null) { ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.CHANNEL_NOT_FOUND, true, whoChanStr)); diff --git a/SharpChat/PacketHandlers/AuthHandler.cs b/SharpChat/PacketHandlers/AuthHandler.cs index d00ed22..4b6d066 100644 --- a/SharpChat/PacketHandlers/AuthHandler.cs +++ b/SharpChat/PacketHandlers/AuthHandler.cs @@ -98,39 +98,36 @@ namespace SharpChat.PacketHandlers { return; } - lock(ctx.Chat.UsersAccess) { - ChatUser user = ctx.Chat.Users.FirstOrDefault(u => u.UserId == fai.UserId); + ChatUser user = ctx.Chat.Users.FirstOrDefault(u => u.UserId == fai.UserId); - if(user == null) - user = new ChatUser(fai); - else { - user.ApplyAuth(fai); - if(user.CurrentChannel != null) - ctx.Chat.SendTo(user.CurrentChannel, new UserUpdatePacket(user)); - } - - // Enforce a maximum amount of connections per user - lock(ctx.Chat.ConnectionsAccess) - if(ctx.Chat.Connections.Count(conn => conn.User == user) >= MaxConnections) { - ctx.Connection.Send(new AuthFailPacket(AuthFailReason.MaxSessions)); - ctx.Connection.Dispose(); - return; - } - - ctx.Connection.BumpPing(); - ctx.Connection.User = user; - ctx.Connection.Send(new LegacyCommandResponse(LCR.WELCOME, false, $"Welcome to Flashii Chat, {user.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.Connection.Send(new LegacyCommandResponse(LCR.WELCOME, false, line)); - } - - ctx.Chat.HandleJoin(user, DefaultChannel, ctx.Connection, MaxMessageLength); + if(user == null) + user = new ChatUser(fai); + else { + user.ApplyAuth(fai); + if(user.CurrentChannel != null) + ctx.Chat.SendTo(user.CurrentChannel, new UserUpdatePacket(user)); } + + // Enforce a maximum amount of connections per user + if(ctx.Chat.Connections.Count(conn => conn.User == user) >= MaxConnections) { + ctx.Connection.Send(new AuthFailPacket(AuthFailReason.MaxSessions)); + ctx.Connection.Dispose(); + return; + } + + ctx.Connection.BumpPing(); + ctx.Connection.User = user; + ctx.Connection.Send(new LegacyCommandResponse(LCR.WELCOME, false, $"Welcome to Flashii Chat, {user.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.Connection.Send(new LegacyCommandResponse(LCR.WELCOME, false, line)); + } + + ctx.Chat.HandleJoin(user, DefaultChannel, ctx.Connection, MaxMessageLength); }).Wait(); } } diff --git a/SharpChat/PacketHandlers/PingHandler.cs b/SharpChat/PacketHandlers/PingHandler.cs index 2e16007..d6f4e8a 100644 --- a/SharpChat/PacketHandlers/PingHandler.cs +++ b/SharpChat/PacketHandlers/PingHandler.cs @@ -32,17 +32,10 @@ namespace SharpChat.PacketHandlers { lock(BumpAccess) { if(LastBump < DateTimeOffset.UtcNow - BumpInterval) { - (string, string)[] bumpList; - lock(ctx.Chat.UsersAccess) { - IEnumerable filtered = ctx.Chat.Users.Where(u => u.Status == ChatUserStatus.Online); - - lock(ctx.Chat.ConnectionsAccess) - filtered = filtered.Where(u => ctx.Chat.Connections.Any(c => c.User == u)); - - bumpList = filtered - .Select(u => (u.UserId.ToString(), ctx.Chat.GetRemoteAddresses(u).FirstOrDefault()?.ToString() ?? string.Empty)) - .ToArray(); - } + (string, string)[] bumpList = ctx.Chat.Users + .Where(u => u.Status == ChatUserStatus.Online && ctx.Chat.Connections.Any(c => c.User == u)) + .Select(u => (u.UserId.ToString(), ctx.Chat.GetRemoteAddresses(u).FirstOrDefault()?.ToString() ?? string.Empty)) + .ToArray(); if(bumpList.Any()) Task.Run(async () => { diff --git a/SharpChat/PacketHandlers/SendMessageHandler.cs b/SharpChat/PacketHandlers/SendMessageHandler.cs index 7460cc1..dd43a53 100644 --- a/SharpChat/PacketHandlers/SendMessageHandler.cs +++ b/SharpChat/PacketHandlers/SendMessageHandler.cs @@ -94,10 +94,8 @@ namespace SharpChat.PacketHandlers { Text = messageText, }; - lock(ctx.Chat.EventsAccess) { - ctx.Chat.Events.AddEvent(message); - ctx.Chat.SendTo(channel, new ChatMessageAddPacket(message)); - } + ctx.Chat.Events.AddEvent(message); + ctx.Chat.SendTo(channel, new ChatMessageAddPacket(message)); } } } diff --git a/SharpChat/RateLimiter.cs b/SharpChat/RateLimiter.cs index 394194d..75911f3 100644 --- a/SharpChat/RateLimiter.cs +++ b/SharpChat/RateLimiter.cs @@ -7,7 +7,7 @@ namespace SharpChat { private readonly int RiskyOffset; private readonly long[] TimePoints; - public RateLimiter(int size, int minDelay, int riskyOffset) { + public RateLimiter(int size, int minDelay, int riskyOffset = 0) { if(size < 2) throw new ArgumentException("Size is too small.", nameof(size)); if(minDelay < 1000) diff --git a/SharpChat/SockChatServer.cs b/SharpChat/SockChatServer.cs index d4f8b77..438fd30 100644 --- a/SharpChat/SockChatServer.cs +++ b/SharpChat/SockChatServer.cs @@ -10,7 +10,6 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Threading; -using System.Threading.Tasks; namespace SharpChat { public class SockChatServer : IDisposable { @@ -19,15 +18,6 @@ namespace SharpChat { public const int DEFAULT_MAX_CONNECTIONS = 5; public const int DEFAULT_FLOOD_KICK_LENGTH = 30; - public bool IsDisposed { get; private set; } - - public static ChatUser Bot { get; } = new ChatUser { - UserId = -1, - Username = "ChatBot", - Rank = 0, - Colour = new ChatColour(), - }; - public IWebSocketServer Server { get; } public ChatContext Context { get; } @@ -115,14 +105,13 @@ namespace SharpChat { SendMessageHandler.AddCommand(new ShutdownRestartCommand(waitHandle, () => !IsShuttingDown && (IsShuttingDown = true))); Server.Start(sock => { - if(IsShuttingDown || IsDisposed) { + if(IsShuttingDown) { sock.Close(1013); return; } - ChatConnection conn; - lock(Context.ConnectionsAccess) - Context.Connections.Add(conn = new(sock)); + ChatConnection conn = new(sock); + Context.Connections.Add(conn); sock.OnOpen = () => OnOpen(conn); sock.OnClose = () => OnClose(conn); @@ -135,51 +124,78 @@ namespace SharpChat { private void OnOpen(ChatConnection conn) { Logger.Write($"Connection opened from {conn.RemoteAddress}:{conn.RemotePort}"); + Context.SafeUpdate(); + } - Context.Update(); + private void OnError(ChatConnection conn, Exception ex) { + Logger.Write($"[{conn.Id} {conn.RemoteAddress}] {ex}"); + Context.SafeUpdate(); } private void OnClose(ChatConnection conn) { Logger.Write($"Connection closed from {conn.RemoteAddress}:{conn.RemotePort}"); - lock(Context.ConnectionsAccess) { + Context.ContextAccess.Wait(); + try { Context.Connections.Remove(conn); if(conn.User != null && !Context.Connections.Any(c => c.User == conn.User)) Context.HandleDisconnect(conn.User); + + Context.Update(); + } finally { + Context.ContextAccess.Release(); } - - Context.Update(); - } - - private void OnError(ChatConnection conn, Exception ex) { - Logger.Write($"[{conn.Id} {conn.RemoteAddress}] {ex}"); - Context.Update(); } private void OnMessage(ChatConnection conn, string msg) { - Context.Update(); + Context.SafeUpdate(); // this doesn't affect non-authed connections????? if(conn.User is not null && conn.User.HasFloodProtection) { - conn.User.RateLimiter.Update(); + ChatUser banUser = null; + string banAddr = string.Empty; + TimeSpan banDuration = TimeSpan.MinValue; - if(conn.User.RateLimiter.IsExceeded) { - Task.Run(async () => { - TimeSpan duration = TimeSpan.FromSeconds(FloodKickLength); + Context.ContextAccess.Wait(); + try { + if(!Context.UserRateLimiters.TryGetValue(conn.User.UserId, out RateLimiter rateLimiter)) + Context.UserRateLimiters.Add(conn.User.UserId, rateLimiter = new RateLimiter( + ChatUser.DEFAULT_SIZE, + ChatUser.DEFAULT_MINIMUM_DELAY, + ChatUser.DEFAULT_RISKY_OFFSET + )); - await Misuzu.CreateBanAsync( - conn.User.UserId.ToString(), conn.RemoteAddress.ToString(), - string.Empty, "::1", - duration, - "Kicked from chat for flood protection." - ); + rateLimiter.Update(); - Context.BanUser(conn.User, duration, UserDisconnectReason.Flood); - }).Wait(); - return; - } else if(conn.User.RateLimiter.IsRisky) - Context.SendTo(conn.User, new FloodWarningPacket()); + if(rateLimiter.IsExceeded) { + banDuration = TimeSpan.FromSeconds(FloodKickLength); + banUser = conn.User; + banAddr = conn.RemoteAddress.ToString(); + } else if(rateLimiter.IsRisky) { + banUser = conn.User; + } + + if(banUser is not null) { + if(banDuration == TimeSpan.MinValue) { + Context.SendTo(conn.User, new FloodWarningPacket()); + } else { + Context.BanUser(conn.User, banDuration, UserDisconnectReason.Flood); + + if(banDuration > TimeSpan.Zero) + Misuzu.CreateBanAsync( + conn.User.UserId.ToString(), conn.RemoteAddress.ToString(), + string.Empty, "::1", + banDuration, + "Kicked from chat for flood protection." + ).Wait(); + + return; + } + } + } finally { + Context.ContextAccess.Release(); + } } ChatPacketHandlerContext context = new(msg, Context, conn); @@ -187,9 +203,18 @@ namespace SharpChat { ? GuestHandlers.FirstOrDefault(h => h.IsMatch(context)) : AuthedHandlers.FirstOrDefault(h => h.IsMatch(context)); - handler?.Handle(context); + if(handler is not null) { + Context.ContextAccess.Wait(); + try { + handler.Handle(context); + } finally { + Context.ContextAccess.Release(); + } + } } + private bool IsDisposed; + ~SockChatServer() { DoDispose(); } @@ -203,10 +228,10 @@ namespace SharpChat { if(IsDisposed) return; IsDisposed = true; + IsShuttingDown = true; - lock(Context.ConnectionsAccess) - foreach(ChatConnection conn in Context.Connections) - conn.Dispose(); + foreach(ChatConnection conn in Context.Connections) + conn.Dispose(); Server?.Dispose(); HttpClient?.Dispose();