sharp-chat/SharpChat/SockChatServer.cs

252 lines
9.1 KiB
C#

using Fleck;
using SharpChat.Commands;
using SharpChat.Config;
using SharpChat.EventStorage;
using SharpChat.Misuzu;
using SharpChat.PacketsS2C;
using SharpChat.PacketsC2S;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
namespace SharpChat {
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;
public IWebSocketServer Server { get; }
public ChatContext Context { get; }
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;
private bool IsShuttingDown = false;
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;
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 ChatContext(evtStore);
string[]? channelNames = config.ReadValue("channels", new[] { "lounge" });
if(channelNames != null)
foreach(string channelName in channelNames) {
IConfig channelCfg = config.ScopeTo($"channels:{channelName}");
string? name = channelCfg.SafeReadValue("name", string.Empty);
if(string.IsNullOrWhiteSpace(name))
name = channelName;
ChannelInfo channelInfo = new(
name,
channelCfg.SafeReadValue("password", string.Empty),
rank: channelCfg.SafeReadValue("minRank", 0)
);
Context.Channels.Add(channelInfo);
}
if(Context.Channels.PublicCount < 1)
Context.Channels.Add(new ChannelInfo("Default"));
GuestHandlers.Add(new AuthC2SPacketHandler(
started,
Misuzu,
Context.Channels.MainChannel,
MaxMessageLength,
MaxConnections
));
AuthedHandlers.AddRange(new IC2SPacketHandler[] {
new PingC2SPacketHandler(Misuzu),
SendMessageHandler = new SendMessageC2SPacketHandler(MaxMessageLength),
});
SendMessageHandler.AddCommands(new ISockChatClientCommand[] {
new UserAFKCommand(),
new UserNickCommand(),
new MessageWhisperCommand(),
new MessageActionCommand(),
new WhoCommand(),
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 SharpChatWebSocketServer($"ws://0.0.0.0:{port}");
}
public void Listen(ManualResetEvent waitHandle) {
if(waitHandle != null)
SendMessageHandler.AddCommand(new ShutdownRestartCommand(
waitHandle,
() => IsShuttingDown,
restarting => {
IsShuttingDown = true;
IsRestarting = restarting;
}
));
Server.Start(sock => {
if(IsShuttingDown) {
sock.Close(1013);
return;
}
ConnectionInfo conn = new(sock);
Context.Connections.Add(conn);
sock.OnOpen = () => OnOpen(conn);
sock.OnClose = () => OnClose(conn);
sock.OnError = err => OnError(conn, err);
sock.OnMessage = msg => OnMessage(conn, msg);
});
Logger.Write("Listening...");
}
private void OnOpen(ConnectionInfo conn) {
Logger.Write($"Connection opened from {conn.RemoteEndPoint}");
Context.SafeUpdate();
}
private void OnError(ConnectionInfo conn, Exception ex) {
Logger.Write($"<{conn.RemoteEndPoint}> {ex}");
Context.SafeUpdate();
}
private void OnClose(ConnectionInfo conn) {
Logger.Write($"Connection closed from {conn.RemoteEndPoint}");
Context.ContextAccess.Wait();
try {
Context.Connections.Remove(conn);
if(!Context.Connections.HasUser(conn.UserId)) {
UserInfo? userInfo = Context.Users.Get(conn.UserId);
if(userInfo != null)
Context.HandleDisconnect(userInfo);
}
Context.Update();
} finally {
Context.ContextAccess.Release();
}
}
private void OnMessage(ConnectionInfo conn, string msg) {
Context.SafeUpdate();
// this doesn't affect non-authed connections?????
if(conn.UserId > 0) {
long banUserId = 0;
string banAddr = string.Empty;
TimeSpan banDuration = TimeSpan.MinValue;
Context.ContextAccess.Wait();
try {
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
));
rateLimiter.Update();
if(rateLimiter.IsExceeded) {
banDuration = TimeSpan.FromSeconds(FloodKickLength);
banUserId = conn.UserId;
banAddr = conn.RemoteAddress;
} else if(rateLimiter.IsRisky) {
banUserId = conn.UserId;
}
if(banUserId > 0) {
UserInfo? userInfo = Context.Users.Get(banUserId);
if(userInfo != null) {
if(banDuration == TimeSpan.MinValue) {
Context.SendTo(userInfo, new FloodWarningS2CPacket());
} else {
Context.BanUser(userInfo, banDuration, UserDisconnectReason.Flood);
if(banDuration > TimeSpan.Zero)
Misuzu.CreateBanAsync(
banUserId.ToString(), conn.RemoteAddress,
string.Empty, "::1",
banDuration,
"Kicked from chat for flood protection."
).Wait();
return;
}
}
}
} finally {
Context.ContextAccess.Release();
}
}
C2SPacketHandlerContext context = new(msg, Context, conn);
IC2SPacketHandler? handler = conn.UserId > 0
? AuthedHandlers.FirstOrDefault(h => h.IsMatch(context))
: GuestHandlers.FirstOrDefault(h => h.IsMatch(context));
handler?.Handle(context);
}
private bool IsDisposed;
~SockChatServer() {
DoDispose();
}
public void Dispose() {
DoDispose();
GC.SuppressFinalize(this);
}
private void DoDispose() {
if(IsDisposed)
return;
IsDisposed = true;
IsShuttingDown = true;
Context.Connections.WithAll(conn => conn.Close(IsRestarting ? 1012 : 1001));
Server?.Dispose();
HttpClient?.Dispose();
}
}
}