Less haphazard locking (perhaps too?)

This commit is contained in:
flash 2023-02-19 23:27:08 +01:00
parent 70df99fe9b
commit 86a46539f2
22 changed files with 257 additions and 306 deletions

View file

@ -1,79 +1,74 @@
using Fleck; using SharpChat.Events;
using SharpChat.Events;
using SharpChat.EventStorage; using SharpChat.EventStorage;
using SharpChat.Packet; using SharpChat.Packet;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Threading;
namespace SharpChat { namespace SharpChat {
public class ChatContext { public class ChatContext {
public record ChannelUserAssoc(long UserId, string ChannelName); public record ChannelUserAssoc(long UserId, string ChannelName);
public readonly SemaphoreSlim ContextAccess = new(1, 1);
public HashSet<ChatChannel> Channels { get; } = new(); public HashSet<ChatChannel> Channels { get; } = new();
public readonly object ChannelsAccess = new();
public HashSet<ChatConnection> Connections { get; } = new(); public HashSet<ChatConnection> Connections { get; } = new();
public readonly object ConnectionsAccess = new();
public HashSet<ChatUser> Users { get; } = new(); public HashSet<ChatUser> Users { get; } = new();
public readonly object UsersAccess = new();
public IEventStorage Events { get; } public IEventStorage Events { get; }
public readonly object EventsAccess = new();
public HashSet<ChannelUserAssoc> ChannelUsers { get; } = new(); public HashSet<ChannelUserAssoc> ChannelUsers { get; } = new();
public readonly object ChannelUsersAccess = new(); public Dictionary<long, RateLimiter> UserRateLimiters { get; } = new();
public ChatContext(IEventStorage evtStore) { public ChatContext(IEventStorage evtStore) {
Events = evtStore ?? throw new ArgumentNullException(nameof(evtStore)); Events = evtStore ?? throw new ArgumentNullException(nameof(evtStore));
} }
public void Update() { public void Update() {
lock(ConnectionsAccess) { foreach(ChatConnection conn in Connections)
foreach(ChatConnection conn in Connections) if(!conn.IsDisposed && conn.HasTimedOut) {
if(!conn.IsDisposed && conn.HasTimedOut) { conn.Dispose();
conn.Dispose(); Logger.Write($"Nuked connection {conn.Id} associated with {conn.User}.");
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)
foreach(ChatUser user in Users) if(!Connections.Any(conn => conn.User == user)) {
if(!Connections.Any(conn => conn.User == user)) { HandleDisconnect(user, UserDisconnectReason.TimeOut);
HandleDisconnect(user, UserDisconnectReason.TimeOut); Logger.Write($"Timed out {user} (no more connections).");
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) { 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) { 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) { public ChatChannel[] GetUserChannels(ChatUser user) {
string[] names = GetUserChannelNames(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) { 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) { public ChatUser[] GetChannelUsers(ChatChannel channel) {
long[] ids = GetChannelUserIds(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) { public void BanUser(ChatUser user, TimeSpan duration, UserDisconnectReason reason = UserDisconnectReason.Kicked) {
@ -82,63 +77,48 @@ namespace SharpChat {
else else
SendTo(user, new ForceDisconnectPacket(ForceDisconnectReason.Kicked)); SendTo(user, new ForceDisconnectPacket(ForceDisconnectReason.Kicked));
lock(ConnectionsAccess) { foreach(ChatConnection conn in Connections)
foreach(ChatConnection conn in Connections) if(conn.User == user)
if(conn.User == user) conn.Dispose();
conn.Dispose(); Connections.RemoveWhere(conn => conn.IsDisposed);
Connections.RemoveWhere(conn => conn.IsDisposed);
}
HandleDisconnect(user, reason); HandleDisconnect(user, reason);
} }
public void HandleJoin(ChatUser user, ChatChannel chan, ChatConnection conn, int maxMsgLength) { public void HandleJoin(ChatUser user, ChatChannel chan, ChatConnection conn, int maxMsgLength) {
lock(EventsAccess) { if(!IsInChannel(user, chan)) {
if(!IsInChannel(user, chan)) { SendTo(chan, new UserConnectPacket(DateTimeOffset.Now, user));
SendTo(chan, new UserConnectPacket(DateTimeOffset.Now, user)); Events.AddEvent(new UserConnectEvent(DateTimeOffset.Now, user, chan));
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;
}
} }
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) { public void HandleDisconnect(ChatUser user, UserDisconnectReason reason = UserDisconnectReason.Leave) {
user.Status = ChatUserStatus.Offline; user.Status = ChatUserStatus.Offline;
Users.Remove(user);
lock(EventsAccess) { ChatChannel[] channels = GetUserChannels(user);
lock(UsersAccess)
Users.Remove(user);
lock(ChannelUsersAccess) { foreach(ChatChannel chan in channels) {
ChatChannel[] channels = GetUserChannels(user); ChannelUsers.Remove(new ChannelUserAssoc(user.UserId, chan.Name));
foreach(ChatChannel chan in channels) { SendTo(chan, new UserDisconnectPacket(DateTimeOffset.Now, user, reason));
ChannelUsers.Remove(new ChannelUserAssoc(user.UserId, chan.Name)); Events.AddEvent(new UserDisconnectEvent(DateTimeOffset.Now, user, chan, reason));
SendTo(chan, new UserDisconnectPacket(DateTimeOffset.Now, user, reason)); if(chan.IsTemporary && chan.Owner == user)
Events.AddEvent(new UserDisconnectEvent(DateTimeOffset.Now, user, chan, reason)); RemoveChannel(chan);
if(chan.IsTemporary && chan.Owner == user)
lock(ChannelsAccess)
RemoveChannel(chan);
}
}
} }
} }
@ -166,46 +146,39 @@ namespace SharpChat {
} }
public void ForceChannelSwitch(ChatUser user, ChatChannel chan) { public void ForceChannelSwitch(ChatUser user, ChatChannel chan) {
lock(ChannelsAccess) if(!Channels.Contains(chan))
if(!Channels.Contains(chan)) return;
return;
ChatChannel oldChan = user.CurrentChannel; ChatChannel oldChan = user.CurrentChannel;
lock(EventsAccess) { SendTo(oldChan, new UserChannelLeavePacket(user));
SendTo(oldChan, new UserChannelLeavePacket(user)); Events.AddEvent(new UserChannelLeaveEvent(DateTimeOffset.Now, user, oldChan));
Events.AddEvent(new UserChannelLeaveEvent(DateTimeOffset.Now, user, oldChan)); SendTo(chan, new UserChannelJoinPacket(user));
SendTo(chan, new UserChannelJoinPacket(user)); Events.AddEvent(new UserChannelJoinEvent(DateTimeOffset.Now, user, chan));
Events.AddEvent(new UserChannelJoinEvent(DateTimeOffset.Now, user, chan));
SendTo(user, new ContextClearPacket(chan, ContextClearMode.MessagesUsers)); SendTo(user, new ContextClearPacket(chan, ContextClearMode.MessagesUsers));
SendTo(user, new ContextUsersPacket(GetChannelUsers(chan).Except(new[] { user }).OrderByDescending(u => u.Rank))); SendTo(user, new ContextUsersPacket(GetChannelUsers(chan).Except(new[] { user }).OrderByDescending(u => u.Rank)));
foreach(IChatEvent msg in Events.GetChannelEventLog(chan.Name)) foreach(IChatEvent msg in Events.GetChannelEventLog(chan.Name))
SendTo(user, new ContextMessagePacket(msg)); SendTo(user, new ContextMessagePacket(msg));
ForceChannel(user, chan); ForceChannel(user, chan);
lock(ChannelUsersAccess) { ChannelUsers.Remove(new ChannelUserAssoc(user.UserId, oldChan.Name));
ChannelUsers.Remove(new ChannelUserAssoc(user.UserId, oldChan.Name)); ChannelUsers.Add(new ChannelUserAssoc(user.UserId, chan.Name));
ChannelUsers.Add(new ChannelUserAssoc(user.UserId, chan.Name)); user.CurrentChannel = chan;
user.CurrentChannel = chan;
}
}
if(oldChan.IsTemporary && oldChan.Owner == user) if(oldChan.IsTemporary && oldChan.Owner == user)
lock(ChannelsAccess) RemoveChannel(oldChan);
RemoveChannel(oldChan);
} }
public void Send(IServerPacket packet) { public void Send(IServerPacket packet) {
if(packet == null) if(packet == null)
throw new ArgumentNullException(nameof(packet)); throw new ArgumentNullException(nameof(packet));
lock(ConnectionsAccess) foreach(ChatConnection conn in Connections)
foreach(ChatConnection conn in Connections) if(conn.IsAuthed)
if(conn.IsAuthed) conn.Send(packet);
conn.Send(packet);
} }
public void SendTo(ChatUser user, IServerPacket packet) { public void SendTo(ChatUser user, IServerPacket packet) {
@ -214,10 +187,9 @@ namespace SharpChat {
if(packet == null) if(packet == null)
throw new ArgumentNullException(nameof(packet)); throw new ArgumentNullException(nameof(packet));
lock(ConnectionsAccess) foreach(ChatConnection conn in Connections)
foreach(ChatConnection conn in Connections) if(conn.IsAlive && conn.User == user)
if(conn.IsAlive && conn.User == user) conn.Send(packet);
conn.Send(packet);
} }
public void SendTo(ChatChannel channel, IServerPacket packet) { public void SendTo(ChatChannel channel, IServerPacket packet) {
@ -227,16 +199,13 @@ namespace SharpChat {
throw new ArgumentNullException(nameof(packet)); throw new ArgumentNullException(nameof(packet));
// might be faster to grab the users first and then cascade into that SendTo // might be faster to grab the users first and then cascade into that SendTo
lock(ConnectionsAccess) { IEnumerable<ChatConnection> conns = Connections.Where(c => c.IsAuthed && IsInChannel(c.User, channel));
IEnumerable<ChatConnection> conns = Connections.Where(c => c.IsAuthed && IsInChannel(c.User, channel)); foreach(ChatConnection conn in conns)
foreach(ChatConnection conn in conns) conn.Send(packet);
conn.Send(packet);
}
} }
public IPAddress[] GetRemoteAddresses(ChatUser user) { 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) { public void ForceChannel(ChatUser user, ChatChannel chan = null) {
@ -273,13 +242,12 @@ namespace SharpChat {
channel.Password = password; 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 // 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)) {
foreach(ChatUser user in Users.Where(u => u.Rank >= channel.Rank)) { SendTo(user, new ChannelUpdatePacket(prevName, channel));
SendTo(user, new ChannelUpdatePacket(prevName, channel));
if(nameUpdated) if(nameUpdated)
ForceChannel(user); ForceChannel(user);
} }
} }
public void RemoveChannel(ChatChannel channel) { public void RemoveChannel(ChatChannel channel) {
@ -299,9 +267,8 @@ namespace SharpChat {
SwitchChannel(user, defaultChannel, string.Empty); SwitchChannel(user, defaultChannel, string.Empty);
// Broadcast deletion of channel // Broadcast deletion of channel
lock(UsersAccess) foreach(ChatUser user in Users.Where(u => u.Rank >= channel.Rank))
foreach(ChatUser user in Users.Where(u => u.Rank >= channel.Rank)) SendTo(user, new ChannelDeletePacket(channel));
SendTo(user, new ChannelDeletePacket(channel));
} }
} }
} }

View file

@ -22,8 +22,6 @@ namespace SharpChat {
public bool HasFloodProtection public bool HasFloodProtection
=> Rank < RANK_NO_FLOOD; => Rank < RANK_NO_FLOOD;
public readonly RateLimiter RateLimiter = new(DEFAULT_SIZE, DEFAULT_MINIMUM_DELAY, DEFAULT_RISKY_OFFSET);
// This needs to be a session thing // This needs to be a session thing
public ChatChannel CurrentChannel { get; set; } public ChatChannel CurrentChannel { get; set; }

View file

@ -38,28 +38,24 @@ namespace SharpChat.Commands {
return; return;
} }
lock(ctx.Chat.ChannelsAccess) { if(ctx.Chat.Channels.Any(c => c.NameEquals(createChanName))) {
if(ctx.Chat.Channels.Any(c => c.NameEquals(createChanName))) { ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.CHANNEL_ALREADY_EXISTS, true, createChanName));
ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.CHANNEL_ALREADY_EXISTS, true, createChanName)); return;
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));
} }
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));
} }
} }
} }

View file

@ -17,9 +17,7 @@ namespace SharpChat.Commands {
} }
string delChanName = string.Join('_', ctx.Args); string delChanName = string.Join('_', ctx.Args);
ChatChannel delChan; ChatChannel delChan = ctx.Chat.Channels.FirstOrDefault(c => c.NameEquals(delChanName));
lock(ctx.Chat.ChannelsAccess)
delChan = ctx.Chat.Channels.FirstOrDefault(c => c.NameEquals(delChanName));
if(delChan == null) { if(delChan == null) {
ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.CHANNEL_NOT_FOUND, true, delChanName)); ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.CHANNEL_NOT_FOUND, true, delChanName));
@ -31,8 +29,7 @@ namespace SharpChat.Commands {
return; 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)); ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.CHANNEL_DELETED, false, delChan.Name));
} }
} }

View file

@ -26,17 +26,15 @@ namespace SharpChat.Commands {
return; 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)) { 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)); ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.MESSAGE_DELETE_ERROR));
return; return;
}
ctx.Chat.Events.RemoveEvent(delMsg);
ctx.Chat.Send(new ChatMessageDeletePacket(delMsg.SequenceId));
} }
ctx.Chat.Events.RemoveEvent(delMsg);
ctx.Chat.Send(new ChatMessageDeletePacket(delMsg.SequenceId));
} }
} }
} }

View file

@ -9,9 +9,7 @@ namespace SharpChat.Commands {
public void Dispatch(ChatCommandContext ctx) { public void Dispatch(ChatCommandContext ctx) {
string joinChanStr = ctx.Args.FirstOrDefault(); string joinChanStr = ctx.Args.FirstOrDefault();
ChatChannel joinChan; ChatChannel joinChan = ctx.Chat.Channels.FirstOrDefault(c => c.NameEquals(joinChanStr));
lock(ctx.Chat.ChannelsAccess)
joinChan = ctx.Chat.Channels.FirstOrDefault(c => c.NameEquals(joinChanStr));
if(joinChan == null) { if(joinChan == null) {
ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.CHANNEL_NOT_FOUND, true, joinChanStr)); ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.CHANNEL_NOT_FOUND, true, joinChanStr));

View file

@ -30,11 +30,10 @@ namespace SharpChat.Commands {
int banReasonIndex = 1; int banReasonIndex = 1;
ChatUser banUser = null; ChatUser banUser = null;
lock(ctx.Chat.UsersAccess) if(banUserTarget == null || (banUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(banUserTarget))) == null) {
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));
ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, banUser == null ? "User" : banUserTarget)); return;
return; }
}
if(banUser == ctx.User || banUser.Rank >= ctx.User.Rank) { if(banUser == ctx.User || banUser.Rank >= ctx.User.Rank) {
ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.KICK_NOT_ALLOWED, true, banUser.DisplayName)); ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.KICK_NOT_ALLOWED, true, banUser.DisplayName));

View file

@ -18,8 +18,7 @@ namespace SharpChat.Commands {
int offset = 0; int offset = 0;
if(setOthersNick && long.TryParse(ctx.Args.FirstOrDefault(), out long targetUserId) && targetUserId > 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; ++offset;
} }
@ -42,11 +41,10 @@ namespace SharpChat.Commands {
else if(string.IsNullOrEmpty(nickStr)) else if(string.IsNullOrEmpty(nickStr))
nickStr = null; nickStr = null;
lock(ctx.Chat.UsersAccess) if(!string.IsNullOrWhiteSpace(nickStr) && ctx.Chat.Users.Any(u => u.NameEquals(nickStr))) {
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));
ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.NAME_IN_USE, true, nickStr)); return;
return; }
}
string previousName = targetUser == ctx.User ? (targetUser.Nickname ?? targetUser.Username) : null; string previousName = targetUser == ctx.User ? (targetUser.Nickname ?? targetUser.Username) : null;
targetUser.Nickname = nickStr; targetUser.Nickname = nickStr;

View file

@ -30,13 +30,10 @@ namespace SharpChat.Commands {
return; return;
} }
ChatUser unbanUser; ChatUser unbanUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(unbanUserTarget));
lock(ctx.Chat.UsersAccess)
unbanUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(unbanUserTarget));
if(unbanUser == null && long.TryParse(unbanUserTarget, out long unbanUserId)) { if(unbanUser == null && long.TryParse(unbanUserTarget, out long unbanUserId)) {
unbanUserTargetIsName = false; 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) if(unbanUser != null)

View file

@ -18,8 +18,7 @@ namespace SharpChat.Commands {
if(string.IsNullOrWhiteSpace(chanPass)) if(string.IsNullOrWhiteSpace(chanPass))
chanPass = string.Empty; 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)); ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.CHANNEL_PASSWORD_CHANGED, false));
} }
} }

View file

@ -20,8 +20,7 @@ namespace SharpChat.Commands {
return; 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)); ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.CHANNEL_HIERARCHY_CHANGED, false));
} }
} }

View file

@ -18,11 +18,10 @@ namespace SharpChat.Commands {
string ipUserStr = ctx.Args.FirstOrDefault(); string ipUserStr = ctx.Args.FirstOrDefault();
ChatUser ipUser; ChatUser ipUser;
lock(ctx.Chat.UsersAccess) if(string.IsNullOrWhiteSpace(ipUserStr) || (ipUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(ipUserStr))) == null) {
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"));
ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, ipUserStr ?? "User")); return;
return; }
}
foreach(IPAddress ip in ctx.Chat.GetRemoteAddresses(ipUser)) foreach(IPAddress ip in ctx.Chat.GetRemoteAddresses(ipUser))
ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.IP_ADDRESS, false, ipUser.Username, ip)); ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.IP_ADDRESS, false, ipUser.Username, ip));

View file

@ -27,9 +27,8 @@ namespace SharpChat.Commands {
return; return;
if(ctx.NameEquals("restart")) if(ctx.NameEquals("restart"))
lock(ctx.Chat.ConnectionsAccess) foreach(ChatConnection conn in ctx.Chat.Connections)
foreach(ChatConnection conn in ctx.Chat.Connections) conn.PrepareForRestart();
conn.PrepareForRestart();
ctx.Chat.Update(); ctx.Chat.Update();
WaitHandle?.Set(); WaitHandle?.Set();

View file

@ -17,11 +17,10 @@ namespace SharpChat.Commands {
string silUserStr = ctx.Args.FirstOrDefault(); string silUserStr = ctx.Args.FirstOrDefault();
ChatUser silUser; ChatUser silUser;
lock(ctx.Chat.UsersAccess) if(string.IsNullOrWhiteSpace(silUserStr) || (silUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(silUserStr))) == null) {
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"));
ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, silUserStr ?? "User")); return;
return; }
}
if(silUser == ctx.User) { if(silUser == ctx.User) {
ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.SILENCE_SELF)); ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.SILENCE_SELF));

View file

@ -17,11 +17,10 @@ namespace SharpChat.Commands {
string unsilUserStr = ctx.Args.FirstOrDefault(); string unsilUserStr = ctx.Args.FirstOrDefault();
ChatUser unsilUser; ChatUser unsilUser;
lock(ctx.Chat.UsersAccess) if(string.IsNullOrWhiteSpace(unsilUserStr) || (unsilUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(unsilUserStr))) == null) {
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"));
ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, unsilUserStr ?? "User")); return;
return; }
}
if(unsilUser.Rank >= ctx.User.Rank) { if(unsilUser.Rank >= ctx.User.Rank) {
ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.UNSILENCE_HIERARCHY)); ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.UNSILENCE_HIERARCHY));

View file

@ -16,15 +16,14 @@ namespace SharpChat.Commands {
return; return;
} }
ChatUser whisperUser;
string whisperUserStr = ctx.Args.FirstOrDefault(); string whisperUserStr = ctx.Args.FirstOrDefault();
lock(ctx.Chat.UsersAccess) ChatUser whisperUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(whisperUserStr));
whisperUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(whisperUserStr));
if(whisperUser == null) { if(whisperUser == null) {
ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, whisperUserStr)); ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, whisperUserStr));
return; return;
} }
if(whisperUser == ctx.User) if(whisperUser == ctx.User)
return; return;

View file

@ -13,26 +13,23 @@ namespace SharpChat.Commands {
string whoChanStr = ctx.Args.FirstOrDefault(); string whoChanStr = ctx.Args.FirstOrDefault();
if(string.IsNullOrEmpty(whoChanStr)) { if(string.IsNullOrEmpty(whoChanStr)) {
lock(ctx.Chat.UsersAccess) foreach(ChatUser whoUser in ctx.Chat.Users) {
foreach(ChatUser whoUser in ctx.Chat.Users) { whoChanSB.Append(@"<a href=""javascript:void(0);"" onclick=""UI.InsertChatText(this.innerHTML);""");
whoChanSB.Append(@"<a href=""javascript:void(0);"" onclick=""UI.InsertChatText(this.innerHTML);""");
if(whoUser == ctx.User) if(whoUser == ctx.User)
whoChanSB.Append(@" style=""font-weight: bold;"""); whoChanSB.Append(@" style=""font-weight: bold;""");
whoChanSB.Append('>'); whoChanSB.Append('>');
whoChanSB.Append(whoUser.DisplayName); whoChanSB.Append(whoUser.DisplayName);
whoChanSB.Append("</a>, "); whoChanSB.Append("</a>, ");
} }
if(whoChanSB.Length > 2) if(whoChanSB.Length > 2)
whoChanSB.Length -= 2; whoChanSB.Length -= 2;
ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.USERS_LISTING_SERVER, false, whoChanSB)); ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.USERS_LISTING_SERVER, false, whoChanSB));
} else { } else {
ChatChannel whoChan; ChatChannel whoChan = ctx.Chat.Channels.FirstOrDefault(c => c.NameEquals(whoChanStr));
lock(ctx.Chat.ChannelsAccess)
whoChan = ctx.Chat.Channels.FirstOrDefault(c => c.NameEquals(whoChanStr));
if(whoChan == null) { if(whoChan == null) {
ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.CHANNEL_NOT_FOUND, true, whoChanStr)); ctx.Chat.SendTo(ctx.User, new LegacyCommandResponse(LCR.CHANNEL_NOT_FOUND, true, whoChanStr));

View file

@ -98,39 +98,36 @@ namespace SharpChat.PacketHandlers {
return; 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) if(user == null)
user = new ChatUser(fai); user = new ChatUser(fai);
else { else {
user.ApplyAuth(fai); user.ApplyAuth(fai);
if(user.CurrentChannel != null) if(user.CurrentChannel != null)
ctx.Chat.SendTo(user.CurrentChannel, new UserUpdatePacket(user)); 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<string> 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);
} }
// 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<string> 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(); }).Wait();
} }
} }

View file

@ -32,17 +32,10 @@ namespace SharpChat.PacketHandlers {
lock(BumpAccess) { lock(BumpAccess) {
if(LastBump < DateTimeOffset.UtcNow - BumpInterval) { if(LastBump < DateTimeOffset.UtcNow - BumpInterval) {
(string, string)[] bumpList; (string, string)[] bumpList = ctx.Chat.Users
lock(ctx.Chat.UsersAccess) { .Where(u => u.Status == ChatUserStatus.Online && ctx.Chat.Connections.Any(c => c.User == u))
IEnumerable<ChatUser> filtered = ctx.Chat.Users.Where(u => u.Status == ChatUserStatus.Online); .Select(u => (u.UserId.ToString(), ctx.Chat.GetRemoteAddresses(u).FirstOrDefault()?.ToString() ?? string.Empty))
.ToArray();
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();
}
if(bumpList.Any()) if(bumpList.Any())
Task.Run(async () => { Task.Run(async () => {

View file

@ -94,10 +94,8 @@ namespace SharpChat.PacketHandlers {
Text = messageText, Text = messageText,
}; };
lock(ctx.Chat.EventsAccess) { ctx.Chat.Events.AddEvent(message);
ctx.Chat.Events.AddEvent(message); ctx.Chat.SendTo(channel, new ChatMessageAddPacket(message));
ctx.Chat.SendTo(channel, new ChatMessageAddPacket(message));
}
} }
} }
} }

View file

@ -7,7 +7,7 @@ namespace SharpChat {
private readonly int RiskyOffset; private readonly int RiskyOffset;
private readonly long[] TimePoints; private readonly long[] TimePoints;
public RateLimiter(int size, int minDelay, int riskyOffset) { public RateLimiter(int size, int minDelay, int riskyOffset = 0) {
if(size < 2) if(size < 2)
throw new ArgumentException("Size is too small.", nameof(size)); throw new ArgumentException("Size is too small.", nameof(size));
if(minDelay < 1000) if(minDelay < 1000)

View file

@ -10,7 +10,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
namespace SharpChat { namespace SharpChat {
public class SockChatServer : IDisposable { public class SockChatServer : IDisposable {
@ -19,15 +18,6 @@ namespace SharpChat {
public const int DEFAULT_MAX_CONNECTIONS = 5; public const int DEFAULT_MAX_CONNECTIONS = 5;
public const int DEFAULT_FLOOD_KICK_LENGTH = 30; 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 IWebSocketServer Server { get; }
public ChatContext Context { get; } public ChatContext Context { get; }
@ -115,14 +105,13 @@ namespace SharpChat {
SendMessageHandler.AddCommand(new ShutdownRestartCommand(waitHandle, () => !IsShuttingDown && (IsShuttingDown = true))); SendMessageHandler.AddCommand(new ShutdownRestartCommand(waitHandle, () => !IsShuttingDown && (IsShuttingDown = true)));
Server.Start(sock => { Server.Start(sock => {
if(IsShuttingDown || IsDisposed) { if(IsShuttingDown) {
sock.Close(1013); sock.Close(1013);
return; return;
} }
ChatConnection conn; ChatConnection conn = new(sock);
lock(Context.ConnectionsAccess) Context.Connections.Add(conn);
Context.Connections.Add(conn = new(sock));
sock.OnOpen = () => OnOpen(conn); sock.OnOpen = () => OnOpen(conn);
sock.OnClose = () => OnClose(conn); sock.OnClose = () => OnClose(conn);
@ -135,51 +124,78 @@ namespace SharpChat {
private void OnOpen(ChatConnection conn) { private void OnOpen(ChatConnection conn) {
Logger.Write($"Connection opened from {conn.RemoteAddress}:{conn.RemotePort}"); 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) { private void OnClose(ChatConnection conn) {
Logger.Write($"Connection closed from {conn.RemoteAddress}:{conn.RemotePort}"); Logger.Write($"Connection closed from {conn.RemoteAddress}:{conn.RemotePort}");
lock(Context.ConnectionsAccess) { Context.ContextAccess.Wait();
try {
Context.Connections.Remove(conn); Context.Connections.Remove(conn);
if(conn.User != null && !Context.Connections.Any(c => c.User == conn.User)) if(conn.User != null && !Context.Connections.Any(c => c.User == conn.User))
Context.HandleDisconnect(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) { private void OnMessage(ChatConnection conn, string msg) {
Context.Update(); Context.SafeUpdate();
// this doesn't affect non-authed connections????? // this doesn't affect non-authed connections?????
if(conn.User is not null && conn.User.HasFloodProtection) { 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) { Context.ContextAccess.Wait();
Task.Run(async () => { try {
TimeSpan duration = TimeSpan.FromSeconds(FloodKickLength); 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( rateLimiter.Update();
conn.User.UserId.ToString(), conn.RemoteAddress.ToString(),
string.Empty, "::1",
duration,
"Kicked from chat for flood protection."
);
Context.BanUser(conn.User, duration, UserDisconnectReason.Flood); if(rateLimiter.IsExceeded) {
}).Wait(); banDuration = TimeSpan.FromSeconds(FloodKickLength);
return; banUser = conn.User;
} else if(conn.User.RateLimiter.IsRisky) banAddr = conn.RemoteAddress.ToString();
Context.SendTo(conn.User, new FloodWarningPacket()); } 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); ChatPacketHandlerContext context = new(msg, Context, conn);
@ -187,9 +203,18 @@ namespace SharpChat {
? GuestHandlers.FirstOrDefault(h => h.IsMatch(context)) ? GuestHandlers.FirstOrDefault(h => h.IsMatch(context))
: AuthedHandlers.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() { ~SockChatServer() {
DoDispose(); DoDispose();
} }
@ -203,10 +228,10 @@ namespace SharpChat {
if(IsDisposed) if(IsDisposed)
return; return;
IsDisposed = true; IsDisposed = true;
IsShuttingDown = true;
lock(Context.ConnectionsAccess) foreach(ChatConnection conn in Context.Connections)
foreach(ChatConnection conn in Context.Connections) conn.Dispose();
conn.Dispose();
Server?.Dispose(); Server?.Dispose();
HttpClient?.Dispose(); HttpClient?.Dispose();