sharp-chat/SharpChat.SockChat/SockChatServer.cs

266 lines
9.9 KiB
C#
Raw Normal View History

2022-08-30 15:00:58 +00:00
using Fleck;
using SharpChat.Config;
using SharpChat.Events;
using SharpChat.EventStorage;
using SharpChat.Misuzu;
using SharpChat.SockChat.Commands;
using SharpChat.SockChat.PacketsC2S;
using SharpChat.SockChat.PacketsS2C;
2022-08-30 15:00:58 +00:00
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
2022-08-30 15:00:58 +00:00
namespace SharpChat.SockChat {
2022-08-30 15:00:58 +00:00
public class SockChatServer : IDisposable {
public const ushort DEFAULT_PORT = 6770;
public const int DEFAULT_MSG_LENGTH_MAX = 5000;
public const int DEFAULT_MAX_CONNECTIONS = 5;
public const int DEFAULT_FLOOD_KICK_LENGTH = 30;
2022-08-30 15:00:58 +00:00
public IWebSocketServer Server { get; }
public SockChatContext Context { get; }
2022-08-30 15:00:58 +00:00
2023-02-06 20:14:50 +00:00
private readonly HttpClient HttpClient;
private readonly MisuzuClient Misuzu;
private readonly CachedValue<int> MaxMessageLength;
private readonly CachedValue<int> MaxConnections;
private readonly CachedValue<int> FloodKickLength;
private readonly List<IC2SPacketHandler> GuestHandlers = new();
private readonly List<IC2SPacketHandler> AuthedHandlers = new();
private readonly SendMessageC2SPacketHandler SendMessageHandler;
2022-08-30 15:00:58 +00:00
private bool IsShuttingDown = false;
2024-05-20 16:16:32 +00:00
private bool IsRestarting = false;
public SockChatServer(HttpClient httpClient, MisuzuClient msz, IEventStorage evtStore, IConfig config) {
Logger.Write("Initialising Sock Chat server...");
DateTimeOffset started = DateTimeOffset.UtcNow;
HttpClient = httpClient;
Misuzu = msz;
2022-08-30 15:00:58 +00:00
MaxMessageLength = config.ReadCached("msgMaxLength", DEFAULT_MSG_LENGTH_MAX);
MaxConnections = config.ReadCached("connMaxCount", DEFAULT_MAX_CONNECTIONS);
FloodKickLength = config.ReadCached("floodKickLength", DEFAULT_FLOOD_KICK_LENGTH);
Context = new SockChatContext(evtStore);
2022-08-30 15:00:58 +00:00
2024-05-10 19:18:55 +00:00
string[]? channelNames = config.ReadValue("channels", new[] { "lounge" });
2024-05-10 19:18:55 +00:00
if(channelNames != null)
foreach(string channelName in channelNames) {
IConfig channelCfg = config.ScopeTo($"channels:{channelName}");
2024-05-10 19:18:55 +00:00
string? name = channelCfg.SafeReadValue("name", string.Empty);
if(string.IsNullOrWhiteSpace(name))
name = channelName;
ChannelInfo channelInfo = new(
2024-05-10 19:18:55 +00:00
name,
channelCfg.SafeReadValue("password", string.Empty),
rank: channelCfg.SafeReadValue("minRank", 0)
);
2022-08-30 15:00:58 +00:00
2024-05-19 21:02:17 +00:00
Context.Channels.Add(channelInfo);
2024-05-10 19:18:55 +00:00
}
2024-05-19 21:02:17 +00:00
if(Context.Channels.PublicCount < 1)
Context.Channels.Add(new ChannelInfo("Default"));
CachedValue<string> motdHeaderFormat = config.ReadCached("motd", @"Welcome to Flashii Chat, {0}!");
GuestHandlers.Add(new AuthC2SPacketHandler(
started,
Misuzu,
Context.Channels.MainChannel,
motdHeaderFormat,
MaxMessageLength,
MaxConnections
));
2023-02-16 21:16:06 +00:00
AuthedHandlers.AddRange(new IC2SPacketHandler[] {
new PingC2SPacketHandler(Misuzu),
SendMessageHandler = new SendMessageC2SPacketHandler(MaxMessageLength),
2023-02-16 21:16:06 +00:00
});
SendMessageHandler.AddCommands(new ISockChatClientCommand[] {
2024-05-14 22:17:25 +00:00
new UserAFKCommand(),
new UserNickCommand(),
new MessageWhisperCommand(),
new MessageActionCommand(),
new WhoCommand(),
2024-05-14 22:17:25 +00:00
new ChannelJoinCommand(),
new ChannelCreateCommand(),
new ChannelDeleteCommand(),
new ChannelPasswordCommand(),
new ChannelRankCommand(),
new MessageBroadcastCommand(),
new MessageDeleteCommand(),
new KickBanCommand(msz),
new PardonUserCommand(msz),
new PardonAddressCommand(msz),
new BanListCommand(msz),
new WhoisCommand(),
});
ushort port = config.SafeReadValue("port", DEFAULT_PORT);
Server = new SockChatWebSocketServer($"ws://0.0.0.0:{port}");
2023-02-06 20:14:50 +00:00
}
public void Listen(ManualResetEvent waitHandle) {
if(waitHandle != null)
2024-05-20 16:16:32 +00:00
SendMessageHandler.AddCommand(new ShutdownRestartCommand(
waitHandle,
() => IsShuttingDown,
restarting => {
IsShuttingDown = true;
IsRestarting = restarting;
}
));
2022-08-30 15:00:58 +00:00
Server.Start(sock => {
2023-02-19 22:27:08 +00:00
if(IsShuttingDown) {
sock.Close(1013);
return;
}
2024-05-21 20:08:23 +00:00
SockChatConnectionInfo conn = SockChatConnectionInfo.Create(sock);
2023-02-19 22:27:08 +00:00
Context.Connections.Add(conn);
sock.OnOpen = () => OnOpen(conn);
sock.OnClose = () => OnClose(conn);
sock.OnError = err => OnError(conn, err);
sock.OnMessage = msg => OnMessage(conn, msg);
2022-08-30 15:00:58 +00:00
});
Logger.Write("Listening...");
2022-08-30 15:00:58 +00:00
}
2024-05-21 20:08:23 +00:00
private void OnOpen(ConnectionInfo conn) {
2024-05-20 16:16:32 +00:00
Logger.Write($"Connection opened from {conn.RemoteEndPoint}");
2023-02-19 22:27:08 +00:00
Context.SafeUpdate();
}
2022-08-30 15:00:58 +00:00
2024-05-21 20:08:23 +00:00
private void OnError(ConnectionInfo conn, Exception ex) {
2024-05-20 16:16:32 +00:00
Logger.Write($"<{conn.RemoteEndPoint}> {ex}");
2023-02-19 22:27:08 +00:00
Context.SafeUpdate();
2022-08-30 15:00:58 +00:00
}
2024-05-21 20:08:23 +00:00
private void OnClose(ConnectionInfo conn) {
2024-05-20 16:16:32 +00:00
Logger.Write($"Connection closed from {conn.RemoteEndPoint}");
2023-02-19 22:27:08 +00:00
Context.ContextAccess.Wait();
try {
Context.Connections.Remove(conn);
2022-08-30 15:00:58 +00:00
2024-05-20 16:16:32 +00:00
if(!Context.Connections.HasUser(conn.UserId)) {
UserInfo? userInfo = Context.Users.Get(conn.UserId);
if(userInfo != null) {
Context.Events.Dispatch("user:delete", userInfo);
Context.Events.Dispatch(
"user:disconnect",
Context.ChannelsUsers.GetUserLastChannel(conn.UserId),
userInfo,
new UserDisconnectEventData(UserDisconnectReason.Leave)
);
}
2024-05-20 16:16:32 +00:00
}
2022-08-30 15:00:58 +00:00
2023-02-19 22:27:08 +00:00
Context.Update();
} finally {
Context.ContextAccess.Release();
}
2022-08-30 15:00:58 +00:00
}
private void OnMessage(SockChatConnectionInfo conn, string msg) {
2023-02-19 22:27:08 +00:00
Context.SafeUpdate();
2022-08-30 15:00:58 +00:00
2023-02-16 21:16:06 +00:00
// this doesn't affect non-authed connections?????
2024-05-20 16:16:32 +00:00
if(conn.UserId > 0) {
long banUserId = 0;
2023-02-19 22:27:08 +00:00
string banAddr = string.Empty;
TimeSpan banDuration = TimeSpan.MinValue;
Context.ContextAccess.Wait();
try {
2024-05-20 16:16:32 +00:00
if(!Context.UserRateLimiters.TryGetValue(conn.UserId, out RateLimiter? rateLimiter))
Context.UserRateLimiters.Add(conn.UserId, rateLimiter = new RateLimiter(
UserInfo.DEFAULT_SIZE,
UserInfo.DEFAULT_MINIMUM_DELAY,
UserInfo.DEFAULT_RISKY_OFFSET
2023-02-19 22:27:08 +00:00
));
rateLimiter.Update();
if(rateLimiter.IsExceeded) {
banDuration = TimeSpan.FromSeconds(FloodKickLength);
2024-05-20 16:16:32 +00:00
banUserId = conn.UserId;
banAddr = conn.RemoteAddress;
2023-02-19 22:27:08 +00:00
} else if(rateLimiter.IsRisky) {
2024-05-20 16:16:32 +00:00
banUserId = conn.UserId;
2023-02-19 22:27:08 +00:00
}
2024-05-20 16:16:32 +00:00
if(banUserId > 0) {
UserInfo? userInfo = Context.Users.Get(banUserId);
if(userInfo != null) {
if(banDuration == TimeSpan.MinValue) {
Context.SendTo(userInfo, new FloodWarningS2CPacket());
2024-05-20 16:16:32 +00:00
} else {
Context.Events.Dispatch("user:kickban", userInfo, UserKickBanEventData.OfDuration(UserDisconnectReason.Flood, banDuration));
2024-05-20 16:16:32 +00:00
if(banDuration > TimeSpan.Zero)
Misuzu.CreateBanAsync(
banUserId.ToString(), conn.RemoteAddress,
string.Empty, "::1",
banDuration,
"Kicked from chat for flood protection."
).Wait();
return;
}
2023-02-19 22:27:08 +00:00
}
}
} finally {
Context.ContextAccess.Release();
}
2022-08-30 15:00:58 +00:00
}
C2SPacketHandlerContext context = new(msg, Context, conn);
IC2SPacketHandler? handler = conn.UserId > 0
2024-05-20 16:16:32 +00:00
? AuthedHandlers.FirstOrDefault(h => h.IsMatch(context))
: GuestHandlers.FirstOrDefault(h => h.IsMatch(context));
2022-08-30 15:00:58 +00:00
handler?.Handle(context);
2022-08-30 15:00:58 +00:00
}
2023-02-19 22:27:08 +00:00
private bool IsDisposed;
2023-02-06 20:14:50 +00:00
~SockChatServer() {
DoDispose();
}
2022-08-30 15:00:58 +00:00
2023-02-06 20:14:50 +00:00
public void Dispose() {
DoDispose();
GC.SuppressFinalize(this);
}
2022-08-30 15:00:58 +00:00
2023-02-06 20:14:50 +00:00
private void DoDispose() {
2022-08-30 15:00:58 +00:00
if(IsDisposed)
return;
IsDisposed = true;
2023-02-19 22:27:08 +00:00
IsShuttingDown = true;
2022-08-30 15:00:58 +00:00
2024-05-21 20:08:23 +00:00
Context.Connections.WithAll(conn => {
if(conn is SockChatConnectionInfo scConn)
scConn.Close(IsRestarting ? 1012 : 1001);
});
2022-08-30 15:00:58 +00:00
Server?.Dispose();
HttpClient?.Dispose();
2022-08-30 15:00:58 +00:00
}
}
}