From e1e3def62cb4631654c018e0226b5caacbc1f8aa Mon Sep 17 00:00:00 2001 From: flashwave Date: Thu, 9 Feb 2023 00:53:42 +0100 Subject: [PATCH] Ported the config system from old master. --- .gitignore | 2 + SharpChat/ChatContext.cs | 4 +- SharpChat/ChatUser.cs | 6 +- SharpChat/Config/CachedValue.cs | 49 +++++ SharpChat/Config/ConfigExceptions.cs | 16 ++ SharpChat/Config/IConfig.cs | 35 ++++ SharpChat/Config/ScopedConfig.cs | 45 +++++ SharpChat/Config/StreamConfig.cs | 112 ++++++++++++ SharpChat/Database.cs | 16 +- SharpChat/Extensions.cs | 23 +-- SharpChat/Flashii/FlashiiAuthInfo.cs | 64 ------- SharpChat/Flashii/FlashiiBanInfo.cs | 183 ------------------- SharpChat/Flashii/FlashiiUrls.cs | 92 ---------- SharpChat/Misuzu/MisuzuAuthInfo.cs | 30 ++++ SharpChat/Misuzu/MisuzuBanInfo.cs | 32 ++++ SharpChat/Misuzu/MisuzuClient.cs | 249 ++++++++++++++++++++++++++ SharpChat/Packet/AuthFailPacket.cs | 6 +- SharpChat/Packet/AuthSuccessPacket.cs | 11 +- SharpChat/Packet/BanListPacket.cs | 8 +- SharpChat/Program.cs | 120 +++++++++++-- SharpChat/SharpChat.csproj | 12 ++ SharpChat/SharpInfo.cs | 37 ++++ SharpChat/SockChatServer.cs | 100 ++++++----- 23 files changed, 814 insertions(+), 438 deletions(-) create mode 100644 SharpChat/Config/CachedValue.cs create mode 100644 SharpChat/Config/ConfigExceptions.cs create mode 100644 SharpChat/Config/IConfig.cs create mode 100644 SharpChat/Config/ScopedConfig.cs create mode 100644 SharpChat/Config/StreamConfig.cs delete mode 100644 SharpChat/Flashii/FlashiiAuthInfo.cs delete mode 100644 SharpChat/Flashii/FlashiiBanInfo.cs delete mode 100644 SharpChat/Flashii/FlashiiUrls.cs create mode 100644 SharpChat/Misuzu/MisuzuAuthInfo.cs create mode 100644 SharpChat/Misuzu/MisuzuBanInfo.cs create mode 100644 SharpChat/Misuzu/MisuzuClient.cs create mode 100644 SharpChat/SharpInfo.cs diff --git a/.gitignore b/.gitignore index de3d414..16b1c56 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ login_key.txt http-motd.txt _webdb.txt msz_url.txt +sharpchat.cfg +SharpChat/version.txt # User-specific files *.suo diff --git a/SharpChat/ChatContext.cs b/SharpChat/ChatContext.cs index dea69ef..6bb4d0f 100644 --- a/SharpChat/ChatContext.cs +++ b/SharpChat/ChatContext.cs @@ -31,13 +31,13 @@ namespace SharpChat { UserLeave(user.Channel, user, reason); } - public void HandleJoin(ChatUser user, ChatChannel chan, ChatUserSession sess) { + public void HandleJoin(ChatUser user, ChatChannel chan, ChatUserSession sess, int maxMsgLength) { if(!chan.HasUser(user)) { chan.Send(new UserConnectPacket(DateTimeOffset.Now, user)); Events.Add(new UserConnectEvent(DateTimeOffset.Now, user, chan)); } - sess.Send(new AuthSuccessPacket(user, chan, sess)); + sess.Send(new AuthSuccessPacket(user, chan, sess, maxMsgLength)); sess.Send(new ContextUsersPacket(chan.GetUsers(new[] { user }))); IEnumerable msgs = Events.GetTargetLog(chan); diff --git a/SharpChat/ChatUser.cs b/SharpChat/ChatUser.cs index ae2a6fd..7a97c01 100644 --- a/SharpChat/ChatUser.cs +++ b/SharpChat/ChatUser.cs @@ -1,4 +1,4 @@ -using SharpChat.Flashii; +using SharpChat.Misuzu; using SharpChat.Packet; using System; using System.Collections.Generic; @@ -120,12 +120,12 @@ namespace SharpChat { public ChatUser() { } - public ChatUser(FlashiiAuthInfo auth) { + public ChatUser(MisuzuAuthInfo auth) { UserId = auth.UserId; ApplyAuth(auth, true); } - public void ApplyAuth(FlashiiAuthInfo auth, bool invalidateRestrictions = false) { + public void ApplyAuth(MisuzuAuthInfo auth, bool invalidateRestrictions = false) { Username = auth.Username; if(Status == ChatUserStatus.Offline) diff --git a/SharpChat/Config/CachedValue.cs b/SharpChat/Config/CachedValue.cs new file mode 100644 index 0000000..157415b --- /dev/null +++ b/SharpChat/Config/CachedValue.cs @@ -0,0 +1,49 @@ +using System; + +namespace SharpChat.Config { + public class CachedValue { + private IConfig Config { get; } + private string Name { get; } + private TimeSpan Lifetime { get; } + private T Fallback { get; } + private object Sync { get; } = new(); + + private object CurrentValue { get; set; } + private DateTimeOffset LastRead { get; set; } + + public T Value { + get { + lock(Sync) { + DateTimeOffset now = DateTimeOffset.Now; + if((now - LastRead) >= Lifetime) { + LastRead = now; + CurrentValue = Config.ReadValue(Name, Fallback); + Logger.Debug($"Read {Name} ({CurrentValue})"); + } + } + return (T)CurrentValue; + } + } + + public static implicit operator T(CachedValue val) => val.Value; + + public CachedValue(IConfig config, string name, TimeSpan lifetime, T fallback) { + Config = config ?? throw new ArgumentNullException(nameof(config)); + Name = name ?? throw new ArgumentNullException(nameof(name)); + Lifetime = lifetime; + Fallback = fallback; + if(string.IsNullOrWhiteSpace(name)) + throw new ArgumentException("Name cannot be empty.", nameof(name)); + } + + public void Refresh() { + lock(Sync) { + LastRead = DateTimeOffset.MinValue; + } + } + + public override string ToString() { + return Value.ToString(); + } + } +} diff --git a/SharpChat/Config/ConfigExceptions.cs b/SharpChat/Config/ConfigExceptions.cs new file mode 100644 index 0000000..b4dead8 --- /dev/null +++ b/SharpChat/Config/ConfigExceptions.cs @@ -0,0 +1,16 @@ +using System; + +namespace SharpChat.Config { + public abstract class ConfigException : Exception { + public ConfigException(string message) : base(message) { } + public ConfigException(string message, Exception ex) : base(message, ex) { } + } + + public class ConfigLockException : ConfigException { + public ConfigLockException() : base("Unable to acquire lock for reading configuration.") { } + } + + public class ConfigTypeException : ConfigException { + public ConfigTypeException(Exception ex) : base("Given type does not match the value in the configuration.", ex) { } + } +} diff --git a/SharpChat/Config/IConfig.cs b/SharpChat/Config/IConfig.cs new file mode 100644 index 0000000..4c27514 --- /dev/null +++ b/SharpChat/Config/IConfig.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SharpChat.Config { + public interface IConfig : IDisposable { + /// + /// Creates a proxy object that forces all names to start with the given prefix. + /// + IConfig ScopeTo(string prefix); + + /// + /// Reads a raw (string) value from the config. + /// + string ReadValue(string name, string fallback = null); + + /// + /// Reads and casts value from the config. + /// + /// Type conversion failed. + T ReadValue(string name, T fallback = default); + + /// + /// Reads and casts a value from the config. Returns fallback when type conversion fails. + /// + T SafeReadValue(string name, T fallback); + + /// + /// Creates an object that caches the read value for a certain amount of time, avoiding disk reads for frequently used non-static values. + /// + CachedValue ReadCached(string name, T fallback = default, TimeSpan? lifetime = null); + } +} diff --git a/SharpChat/Config/ScopedConfig.cs b/SharpChat/Config/ScopedConfig.cs new file mode 100644 index 0000000..912d074 --- /dev/null +++ b/SharpChat/Config/ScopedConfig.cs @@ -0,0 +1,45 @@ +using System; + +namespace SharpChat.Config { + public class ScopedConfig : IConfig { + private IConfig Config { get; } + private string Prefix { get; } + + public ScopedConfig(IConfig config, string prefix) { + Config = config ?? throw new ArgumentNullException(nameof(config)); + Prefix = prefix ?? throw new ArgumentNullException(nameof(prefix)); + if(string.IsNullOrWhiteSpace(prefix)) + throw new ArgumentException("Prefix must exist.", nameof(prefix)); + if(Prefix[^1] != ':') + Prefix += ':'; + } + + private string GetName(string name) { + return Prefix + name; + } + + public string ReadValue(string name, string fallback = null) { + return Config.ReadValue(GetName(name), fallback); + } + + public T ReadValue(string name, T fallback = default) { + return Config.ReadValue(GetName(name), fallback); + } + + public T SafeReadValue(string name, T fallback) { + return Config.SafeReadValue(GetName(name), fallback); + } + + public IConfig ScopeTo(string prefix) { + return Config.ScopeTo(GetName(prefix)); + } + + public CachedValue ReadCached(string name, T fallback = default, TimeSpan? lifetime = null) { + return Config.ReadCached(GetName(name), fallback, lifetime); + } + + public void Dispose() { + GC.SuppressFinalize(this); + } + } +} diff --git a/SharpChat/Config/StreamConfig.cs b/SharpChat/Config/StreamConfig.cs new file mode 100644 index 0000000..5192def --- /dev/null +++ b/SharpChat/Config/StreamConfig.cs @@ -0,0 +1,112 @@ +using System; +using System.IO; +using System.Text; +using System.Threading; + +namespace SharpChat.Config { + public class StreamConfig : IConfig { + private Stream Stream { get; } + private StreamReader StreamReader { get; } + private Mutex Lock { get; } + + private const int LOCK_TIMEOUT = 10000; + + private static readonly TimeSpan CACHE_LIFETIME = TimeSpan.FromMinutes(15); + + public StreamConfig(string fileName) + : this(new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.Read, FileShare.ReadWrite)) { } + + public StreamConfig(Stream stream) { + Stream = stream ?? throw new ArgumentNullException(nameof(stream)); + if(!Stream.CanRead) + throw new ArgumentException("Provided stream must be readable.", nameof(stream)); + if(!Stream.CanSeek) + throw new ArgumentException("Provided stream must be seekable.", nameof(stream)); + StreamReader = new StreamReader(stream, new UTF8Encoding(false), false); + Lock = new Mutex(); + } + + public string ReadValue(string name, string fallback = null) { + if(!Lock.WaitOne(LOCK_TIMEOUT)) // don't catch this, if this happens something is Very Wrong + throw new ConfigLockException(); + + try { + Stream.Seek(0, SeekOrigin.Begin); + + string line; + while((line = StreamReader.ReadLine()) != null) { + if(string.IsNullOrWhiteSpace(line)) + continue; + + line = line.TrimStart(); + if(line.StartsWith(";") || line.StartsWith("#")) + continue; + + string[] parts = line.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); + if(parts.Length < 2 || !string.Equals(parts[0], name)) + continue; + + return parts[1]; + } + } finally { + Lock.ReleaseMutex(); + } + + return fallback; + } + + public T ReadValue(string name, T fallback = default) { + object value = ReadValue(name); + if(value == null) + return fallback; + + Type type = typeof(T); + if(value is string strVal) { + if(type == typeof(bool)) + value = !string.Equals(strVal, "0", StringComparison.InvariantCultureIgnoreCase) + && !string.Equals(strVal, "false", StringComparison.InvariantCultureIgnoreCase); + else if(type == typeof(string[])) + value = strVal.Split(' '); + } + + try { + return (T)Convert.ChangeType(value, type); + } catch(InvalidCastException ex) { + throw new ConfigTypeException(ex); + } + } + + public T SafeReadValue(string name, T fallback) { + try { + return ReadValue(name, fallback); + } catch(ConfigTypeException) { + return fallback; + } + } + + public IConfig ScopeTo(string prefix) { + return new ScopedConfig(this, prefix); + } + + public CachedValue ReadCached(string name, T fallback = default, TimeSpan? lifetime = null) { + return new CachedValue(this, name, lifetime ?? CACHE_LIFETIME, fallback); + } + + private bool IsDisposed; + ~StreamConfig() + => DoDispose(); + public void Dispose() { + DoDispose(); + GC.SuppressFinalize(this); + } + private void DoDispose() { + if(IsDisposed) + return; + IsDisposed = true; + + StreamReader.Dispose(); + Stream.Dispose(); + Lock.Dispose(); + } + } +} diff --git a/SharpChat/Database.cs b/SharpChat/Database.cs index 28942ce..178d8c0 100644 --- a/SharpChat/Database.cs +++ b/SharpChat/Database.cs @@ -1,8 +1,8 @@ using MySqlConnector; +using SharpChat.Config; using SharpChat.Events; using System; using System.Collections.Generic; -using System.IO; using System.Text; using System.Text.Json; @@ -13,13 +13,13 @@ namespace SharpChat { public static bool HasDatabase => !string.IsNullOrWhiteSpace(ConnectionString); - public static void ReadConfig() { - if(!File.Exists("mariadb.txt")) - return; - string[] config = File.ReadAllLines("mariadb.txt"); - if(config.Length < 4) - return; - Init(config[0], config[1], config[2], config[3]); + public static void Init(IConfig config) { + Init( + config.ReadValue("host", "localhost"), + config.ReadValue("user", string.Empty), + config.ReadValue("pass", string.Empty), + config.ReadValue("db", "sharpchat") + ); } public static void Init(string host, string username, string password, string database) { diff --git a/SharpChat/Extensions.cs b/SharpChat/Extensions.cs index a714e30..144db14 100644 --- a/SharpChat/Extensions.cs +++ b/SharpChat/Extensions.cs @@ -1,28 +1,7 @@ -using System.IO; -using System.Security.Cryptography; -using System.Text; +using System.Text; namespace SharpChat { public static class Extensions { - public static string GetSignedHash(this string str, string key = null) { - return Encoding.UTF8.GetBytes(str).GetSignedHash(key); - } - - public static string GetSignedHash(this byte[] bytes, string key = null) { - key ??= File.Exists("login_key.txt") ? File.ReadAllText("login_key.txt") : "woomy"; - - StringBuilder sb = new(); - - using(HMACSHA256 algo = new(Encoding.UTF8.GetBytes(key))) { - byte[] hash = algo.ComputeHash(bytes); - - foreach(byte b in hash) - sb.AppendFormat("{0:x2}", b); - } - - return sb.ToString(); - } - public static string GetIdString(this byte[] buffer) { const string id_chars = "abcdefghijklmnopqrstuvwxyz0123456789-_ABCDEFGHIJKLMNOPQRSTUVWXYZ"; StringBuilder sb = new(); diff --git a/SharpChat/Flashii/FlashiiAuthInfo.cs b/SharpChat/Flashii/FlashiiAuthInfo.cs deleted file mode 100644 index c1dbeb6..0000000 --- a/SharpChat/Flashii/FlashiiAuthInfo.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading.Tasks; - -namespace SharpChat.Flashii { - public class FlashiiAuthInfo { - [JsonPropertyName("success")] - public bool Success { get; set; } - - [JsonPropertyName("reason")] - public string Reason { get; set; } = "none"; - - [JsonPropertyName("user_id")] - public long UserId { get; set; } - - [JsonPropertyName("username")] - public string Username { get; set; } - - [JsonPropertyName("colour_raw")] - public int ColourRaw { get; set; } - - [JsonPropertyName("hierarchy")] - public int Rank { get; set; } - - [JsonPropertyName("is_silenced")] - public DateTimeOffset SilencedUntil { get; set; } - - [JsonPropertyName("perms")] - public ChatUserPermissions Permissions { get; set; } - - private const string SIG_FMT = "verify#{0}#{1}#{2}"; - - public static async Task VerifyAsync(HttpClient client, string method, string token, string ipAddr) { - if(client == null) - throw new ArgumentNullException(nameof(client)); - - method ??= string.Empty; - token ??= string.Empty; - ipAddr ??= string.Empty; - - string sig = string.Format(SIG_FMT, method, token, ipAddr); - - HttpRequestMessage req = new(HttpMethod.Post, FlashiiUrls.VerifyURL) { - Content = new FormUrlEncodedContent(new Dictionary { - { "method", method }, - { "token", token }, - { "ipaddr", ipAddr }, - }), - Headers = { - { "X-SharpChat-Signature", sig.GetSignedHash() }, - }, - }; - - using HttpResponseMessage res = await client.SendAsync(req); - - return JsonSerializer.Deserialize( - await res.Content.ReadAsByteArrayAsync() - ); - } - } -} diff --git a/SharpChat/Flashii/FlashiiBanInfo.cs b/SharpChat/Flashii/FlashiiBanInfo.cs deleted file mode 100644 index 2b247b5..0000000 --- a/SharpChat/Flashii/FlashiiBanInfo.cs +++ /dev/null @@ -1,183 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading.Tasks; - -namespace SharpChat.Flashii { - public class FlashiiBanInfo { - [JsonPropertyName("is_ban")] - public bool IsBanned { get; set; } - - [JsonPropertyName("user_id")] - public string UserId { get; set; } - - [JsonPropertyName("ip_addr")] - public string RemoteAddress { get; set; } - - [JsonPropertyName("is_perma")] - public bool IsPermanent { get; set; } - - [JsonPropertyName("expires")] - public DateTimeOffset ExpiresAt { get; set; } - - // only populated in list request - [JsonPropertyName("user_name")] - public string UserName { get; set; } - - [JsonPropertyName("user_colour")] - public int UserColourRaw { get; set; } - - public bool HasExpired => !IsPermanent && DateTimeOffset.UtcNow >= ExpiresAt; - - public ChatColour UserColour => ChatColour.FromMisuzu(UserColourRaw); - - private const string CHECK_SIG_FMT = "check#{0}#{1}#{2}#{3}"; - private const string REVOKE_SIG_FMT = "revoke#{0}#{1}#{2}"; - private const string CREATE_SIG_FMT = "create#{0}#{1}#{2}#{3}#{4}#{5}#{6}#{7}"; - private const string LIST_SIG_FMT = "list#{0}"; - - public static async Task CheckAsync(HttpClient client, string userId = null, string ipAddr = null, bool userIdIsName = false) { - if(client == null) - throw new ArgumentNullException(nameof(client)); - - userId ??= string.Empty; - ipAddr ??= string.Empty; - - string userIdIsNameStr = userIdIsName ? "1" : "0"; - string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); - string url = string.Format(FlashiiUrls.BansCheckURL, Uri.EscapeDataString(userId), Uri.EscapeDataString(ipAddr), Uri.EscapeDataString(now), Uri.EscapeDataString(userIdIsNameStr)); - string sig = string.Format(CHECK_SIG_FMT, now, userId, ipAddr, userIdIsNameStr); - - HttpRequestMessage req = new(HttpMethod.Get, url) { - Headers = { - { "X-SharpChat-Signature", sig.GetSignedHash() }, - }, - }; - - using HttpResponseMessage res = await client.SendAsync(req); - - return JsonSerializer.Deserialize( - await res.Content.ReadAsByteArrayAsync() - ); - } - - public static async Task GetListAsync(HttpClient client) { - if(client == null) - throw new ArgumentNullException(nameof(client)); - - string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); - string url = string.Format(FlashiiUrls.BansListURL, Uri.EscapeDataString(now)); - string sig = string.Format(LIST_SIG_FMT, now); - - HttpRequestMessage req = new(HttpMethod.Get, url) { - Headers = { - { "X-SharpChat-Signature", sig.GetSignedHash() }, - }, - }; - - using HttpResponseMessage res = await client.SendAsync(req); - - return JsonSerializer.Deserialize( - await res.Content.ReadAsByteArrayAsync() - ); - } - - public enum RevokeKind { - UserId, - RemoteAddress, - } - - public async Task RevokeAsync(HttpClient client, RevokeKind kind) { - if(client == null) - throw new ArgumentNullException(nameof(client)); - - string type = kind switch { - RevokeKind.UserId => "user", - RevokeKind.RemoteAddress => "addr", - _ => throw new ArgumentException("Invalid kind specified.", nameof(kind)), - }; - - string target = kind switch { - RevokeKind.UserId => UserId, - RevokeKind.RemoteAddress => RemoteAddress, - _ => string.Empty, - }; - - string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); - string url = string.Format(FlashiiUrls.BansRevokeURL, Uri.EscapeDataString(type), Uri.EscapeDataString(target), Uri.EscapeDataString(now)); - string sig = string.Format(REVOKE_SIG_FMT, now, type, target); - - HttpRequestMessage req = new(HttpMethod.Delete, url) { - Headers = { - { "X-SharpChat-Signature", sig.GetSignedHash() }, - }, - }; - - using HttpResponseMessage res = await client.SendAsync(req); - - if(res.StatusCode == HttpStatusCode.NotFound) - return false; - - res.EnsureSuccessStatusCode(); - - return res.StatusCode == HttpStatusCode.NoContent; - } - - public static async Task CreateAsync( - HttpClient client, - string targetId, - string targetAddr, - string modId, - string modAddr, - TimeSpan duration, - string reason - ) { - if(client == null) - throw new ArgumentNullException(nameof(client)); - if(string.IsNullOrWhiteSpace(targetAddr)) - throw new ArgumentNullException(nameof(targetAddr)); - if(string.IsNullOrWhiteSpace(modAddr)) - throw new ArgumentNullException(nameof(modAddr)); - if(duration <= TimeSpan.Zero) - return; - - modId ??= string.Empty; - targetId ??= string.Empty; - reason ??= string.Empty; - - string isPerma = duration == TimeSpan.MaxValue ? "1" : "0"; - string durationStr = duration == TimeSpan.MaxValue ? "-1" : duration.TotalSeconds.ToString(); - - string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); - string sig = string.Format( - CREATE_SIG_FMT, - now, targetId, targetAddr, - modId, modAddr, - durationStr, isPerma, reason - ); - - HttpRequestMessage req = new(HttpMethod.Post, FlashiiUrls.BansCreateURL) { - Headers = { - { "X-SharpChat-Signature", sig.GetSignedHash() }, - }, - Content = new FormUrlEncodedContent(new Dictionary { - { "t", now }, - { "ui", targetId }, - { "ua", targetAddr }, - { "mi", modId }, - { "ma", modAddr }, - { "d", durationStr }, - { "p", isPerma }, - { "r", reason }, - }), - }; - - using HttpResponseMessage res = await client.SendAsync(req); - - res.EnsureSuccessStatusCode(); - } - } -} diff --git a/SharpChat/Flashii/FlashiiUrls.cs b/SharpChat/Flashii/FlashiiUrls.cs deleted file mode 100644 index 041d858..0000000 --- a/SharpChat/Flashii/FlashiiUrls.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; - -namespace SharpChat.Flashii { - public static class FlashiiUrls { - private const string BASE_URL_FILE = "msz_url.txt"; - private const string BASE_URL_FALLBACK = "https://flashii.net"; - - private const string BUMP = "/_sockchat/bump"; - private const string VERIFY = "/_sockchat/verify"; - - private const string BANS_CHECK = "/_sockchat/bans/check?u={0}&a={1}&x={2}&n={3}"; - private const string BANS_CREATE = "/_sockchat/bans/create"; - private const string BANS_REVOKE = "/_sockchat/bans/revoke?t={0}&s={1}&x={2}"; - private const string BANS_LIST = "/_sockchat/bans/list?x={0}"; - - public static string BumpURL { get; } - public static string VerifyURL { get; } - - public static string BansCheckURL { get; } - public static string BansCreateURL { get; } - public static string BansRevokeURL { get; } - public static string BansListURL { get; } - - static FlashiiUrls() { - BumpURL = GetURL(BUMP); - VerifyURL = GetURL(VERIFY); - BansCheckURL = GetURL(BANS_CHECK); - BansCreateURL = GetURL(BANS_CREATE); - BansRevokeURL = GetURL(BANS_REVOKE); - BansListURL = GetURL(BANS_LIST); - } - - public static string GetBaseURL() { - if(!File.Exists(BASE_URL_FILE)) - return BASE_URL_FALLBACK; - string url = File.ReadAllText(BASE_URL_FILE).Trim().Trim('/'); - if(string.IsNullOrEmpty(url)) - return BASE_URL_FALLBACK; - return url; - } - - public static string GetURL(string path) { - return GetBaseURL() + path; - } - - public static async Task BumpUsersOnlineAsync(HttpClient client, IEnumerable<(string userId, string ipAddr)> list) { - if(client == null) - throw new ArgumentNullException(nameof(client)); - if(list == null) - throw new ArgumentNullException(nameof(list)); - if(!list.Any()) - return; - - string now = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(); - StringBuilder sb = new(); - sb.AppendFormat("bump#{0}", now); - - Dictionary formData = new() { - { "t", now } - }; - - foreach(var (userId, ipAddr) in list) { - sb.AppendFormat("#{0}:{1}", userId, ipAddr); - formData.Add(string.Format("u[{0}]", userId), ipAddr); - } - - HttpRequestMessage req = new(HttpMethod.Post, BumpURL) { - Headers = { - { "X-SharpChat-Signature", sb.ToString().GetSignedHash() } - }, - Content = new FormUrlEncodedContent(formData), - }; - - using HttpResponseMessage res = await client.SendAsync(req); - - try { - res.EnsureSuccessStatusCode(); - } catch(HttpRequestException) { - Logger.Debug(await res.Content.ReadAsStringAsync()); -#if DEBUG - throw; -#endif - } - } - } -} diff --git a/SharpChat/Misuzu/MisuzuAuthInfo.cs b/SharpChat/Misuzu/MisuzuAuthInfo.cs new file mode 100644 index 0000000..75a9f8b --- /dev/null +++ b/SharpChat/Misuzu/MisuzuAuthInfo.cs @@ -0,0 +1,30 @@ +using System; +using System.Text.Json.Serialization; + +namespace SharpChat.Misuzu { + public class MisuzuAuthInfo { + [JsonPropertyName("success")] + public bool Success { get; set; } + + [JsonPropertyName("reason")] + public string Reason { get; set; } = "none"; + + [JsonPropertyName("user_id")] + public long UserId { get; set; } + + [JsonPropertyName("username")] + public string Username { get; set; } + + [JsonPropertyName("colour_raw")] + public int ColourRaw { get; set; } + + [JsonPropertyName("hierarchy")] + public int Rank { get; set; } + + [JsonPropertyName("is_silenced")] + public DateTimeOffset SilencedUntil { get; set; } + + [JsonPropertyName("perms")] + public ChatUserPermissions Permissions { get; set; } + } +} diff --git a/SharpChat/Misuzu/MisuzuBanInfo.cs b/SharpChat/Misuzu/MisuzuBanInfo.cs new file mode 100644 index 0000000..45b2e8a --- /dev/null +++ b/SharpChat/Misuzu/MisuzuBanInfo.cs @@ -0,0 +1,32 @@ +using System; +using System.Text.Json.Serialization; + +namespace SharpChat.Misuzu { + public class MisuzuBanInfo { + [JsonPropertyName("is_ban")] + public bool IsBanned { get; set; } + + [JsonPropertyName("user_id")] + public string UserId { get; set; } + + [JsonPropertyName("ip_addr")] + public string RemoteAddress { get; set; } + + [JsonPropertyName("is_perma")] + public bool IsPermanent { get; set; } + + [JsonPropertyName("expires")] + public DateTimeOffset ExpiresAt { get; set; } + + // only populated in list request + [JsonPropertyName("user_name")] + public string UserName { get; set; } + + [JsonPropertyName("user_colour")] + public int UserColourRaw { get; set; } + + public bool HasExpired => !IsPermanent && DateTimeOffset.UtcNow >= ExpiresAt; + + public ChatColour UserColour => ChatColour.FromMisuzu(UserColourRaw); + } +} diff --git a/SharpChat/Misuzu/MisuzuClient.cs b/SharpChat/Misuzu/MisuzuClient.cs new file mode 100644 index 0000000..8f564cd --- /dev/null +++ b/SharpChat/Misuzu/MisuzuClient.cs @@ -0,0 +1,249 @@ +using SharpChat.Config; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace SharpChat.Misuzu { + public class MisuzuClient { + private const string DEFAULT_BASE_URL = "https://flashii.net/_sockchat"; + private const string DEFAULT_SECRET_KEY = "woomy"; + + private const string BUMP_ONLINE_URL = "{0}/bump"; + private const string AUTH_VERIFY_URL = "{0}/verify"; + + private const string BANS_CHECK_URL = "{0}/bans/check?u={1}&a={2}&x={3}&n={4}"; + private const string BANS_CREATE_URL = "{0}/bans/create"; + private const string BANS_REVOKE_URL = "{0}/bans/revoke?t={1}&s={2}&x={3}"; + private const string BANS_LIST_URL = "{0}/bans/list?x={1}"; + + private const string VERIFY_SIG = "verify#{0}#{1}#{2}"; + private const string BANS_CHECK_SIG = "check#{0}#{1}#{2}#{3}"; + private const string BANS_REVOKE_SIG = "revoke#{0}#{1}#{2}"; + private const string BANS_CREATE_SIG = "create#{0}#{1}#{2}#{3}#{4}#{5}#{6}#{7}"; + private const string BANS_LIST_SIG = "list#{0}"; + + private readonly HttpClient HttpClient; + + private CachedValue BaseURL { get; } + private CachedValue SecretKey { get; } + + public MisuzuClient(HttpClient httpClient, IConfig config) { + if(config == null) + throw new ArgumentNullException(nameof(config)); + HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); + + BaseURL = config.ReadCached("url", DEFAULT_BASE_URL); + SecretKey = config.ReadCached("secret", DEFAULT_SECRET_KEY); + } + + public string CreateStringSignature(string str) { + return CreateBufferSignature(Encoding.UTF8.GetBytes(str)); + } + + public string CreateBufferSignature(byte[] bytes) { + using HMACSHA256 algo = new(Encoding.UTF8.GetBytes(SecretKey)); + return string.Concat(algo.ComputeHash(bytes).Select(c => c.ToString("x2"))); + } + + public async Task AuthVerifyAsync(string method, string token, string ipAddr) { + method ??= string.Empty; + token ??= string.Empty; + ipAddr ??= string.Empty; + + string sig = string.Format(VERIFY_SIG, method, token, ipAddr); + + HttpRequestMessage req = new(HttpMethod.Post, string.Format(AUTH_VERIFY_URL, BaseURL)) { + Content = new FormUrlEncodedContent(new Dictionary { + { "method", method }, + { "token", token }, + { "ipaddr", ipAddr }, + }), + Headers = { + { "X-SharpChat-Signature", CreateStringSignature(sig) }, + }, + }; + + using HttpResponseMessage res = await HttpClient.SendAsync(req); + + return JsonSerializer.Deserialize( + await res.Content.ReadAsByteArrayAsync() + ); + } + + public async Task BumpUsersOnlineAsync(IEnumerable<(string userId, string ipAddr)> list) { + if(list == null) + throw new ArgumentNullException(nameof(list)); + if(!list.Any()) + return; + + string now = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(); + StringBuilder sb = new(); + sb.AppendFormat("bump#{0}", now); + + Dictionary formData = new() { + { "t", now } + }; + + foreach(var (userId, ipAddr) in list) { + sb.AppendFormat("#{0}:{1}", userId, ipAddr); + formData.Add(string.Format("u[{0}]", userId), ipAddr); + } + + HttpRequestMessage req = new(HttpMethod.Post, string.Format(BUMP_ONLINE_URL, BaseURL)) { + Headers = { + { "X-SharpChat-Signature", CreateStringSignature(sb.ToString()) } + }, + Content = new FormUrlEncodedContent(formData), + }; + + using HttpResponseMessage res = await HttpClient.SendAsync(req); + + try { + res.EnsureSuccessStatusCode(); + } catch(HttpRequestException) { + Logger.Debug(await res.Content.ReadAsStringAsync()); +#if DEBUG + throw; +#endif + } + } + + public async Task CheckBanAsync(string userId = null, string ipAddr = null, bool userIdIsName = false) { + userId ??= string.Empty; + ipAddr ??= string.Empty; + + string userIdIsNameStr = userIdIsName ? "1" : "0"; + string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); + string url = string.Format(BANS_CHECK_URL, BaseURL, Uri.EscapeDataString(userId), Uri.EscapeDataString(ipAddr), Uri.EscapeDataString(now), Uri.EscapeDataString(userIdIsNameStr)); + string sig = string.Format(BANS_CHECK_SIG, now, userId, ipAddr, userIdIsNameStr); + + HttpRequestMessage req = new(HttpMethod.Get, url) { + Headers = { + { "X-SharpChat-Signature", CreateStringSignature(sig) }, + }, + }; + + using HttpResponseMessage res = await HttpClient.SendAsync(req); + + return JsonSerializer.Deserialize( + await res.Content.ReadAsByteArrayAsync() + ); + } + + public async Task GetBanListAsync() { + string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); + string url = string.Format(BANS_LIST_URL, BaseURL, Uri.EscapeDataString(now)); + string sig = string.Format(BANS_LIST_SIG, now); + + HttpRequestMessage req = new(HttpMethod.Get, url) { + Headers = { + { "X-SharpChat-Signature", CreateStringSignature(sig) }, + }, + }; + + using HttpResponseMessage res = await HttpClient.SendAsync(req); + + return JsonSerializer.Deserialize( + await res.Content.ReadAsByteArrayAsync() + ); + } + + public enum BanRevokeKind { + UserId, + RemoteAddress, + } + + public async Task RevokeBanAsync(MisuzuBanInfo banInfo, BanRevokeKind kind) { + if(banInfo == null) + throw new ArgumentNullException(nameof(banInfo)); + + string type = kind switch { + BanRevokeKind.UserId => "user", + BanRevokeKind.RemoteAddress => "addr", + _ => throw new ArgumentException("Invalid kind specified.", nameof(kind)), + }; + + string target = kind switch { + BanRevokeKind.UserId => banInfo.UserId, + BanRevokeKind.RemoteAddress => banInfo.RemoteAddress, + _ => string.Empty, + }; + + string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); + string url = string.Format(BANS_REVOKE_URL, BaseURL, Uri.EscapeDataString(type), Uri.EscapeDataString(target), Uri.EscapeDataString(now)); + string sig = string.Format(BANS_REVOKE_SIG, now, type, target); + + HttpRequestMessage req = new(HttpMethod.Delete, url) { + Headers = { + { "X-SharpChat-Signature", CreateStringSignature(sig) }, + }, + }; + + using HttpResponseMessage res = await HttpClient.SendAsync(req); + + if(res.StatusCode == HttpStatusCode.NotFound) + return false; + + res.EnsureSuccessStatusCode(); + + return res.StatusCode == HttpStatusCode.NoContent; + } + + public async Task CreateBanAsync( + string targetId, + string targetAddr, + string modId, + string modAddr, + TimeSpan duration, + string reason + ) { + if(string.IsNullOrWhiteSpace(targetAddr)) + throw new ArgumentNullException(nameof(targetAddr)); + if(string.IsNullOrWhiteSpace(modAddr)) + throw new ArgumentNullException(nameof(modAddr)); + if(duration <= TimeSpan.Zero) + return; + + modId ??= string.Empty; + targetId ??= string.Empty; + reason ??= string.Empty; + + string isPerma = duration == TimeSpan.MaxValue ? "1" : "0"; + string durationStr = duration == TimeSpan.MaxValue ? "-1" : duration.TotalSeconds.ToString(); + + string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); + string sig = string.Format( + BANS_CREATE_SIG, + now, targetId, targetAddr, + modId, modAddr, + durationStr, isPerma, reason + ); + + HttpRequestMessage req = new(HttpMethod.Post, string.Format(BANS_CREATE_URL, BaseURL)) { + Headers = { + { "X-SharpChat-Signature", CreateStringSignature(sig) }, + }, + Content = new FormUrlEncodedContent(new Dictionary { + { "t", now }, + { "ui", targetId }, + { "ua", targetAddr }, + { "mi", modId }, + { "ma", modAddr }, + { "d", durationStr }, + { "p", isPerma }, + { "r", reason }, + }), + }; + + using HttpResponseMessage res = await HttpClient.SendAsync(req); + + res.EnsureSuccessStatusCode(); + } + } +} diff --git a/SharpChat/Packet/AuthFailPacket.cs b/SharpChat/Packet/AuthFailPacket.cs index b664e89..8e5e0a9 100644 --- a/SharpChat/Packet/AuthFailPacket.cs +++ b/SharpChat/Packet/AuthFailPacket.cs @@ -1,4 +1,4 @@ -using SharpChat.Flashii; +using SharpChat.Misuzu; using System; using System.Collections.Generic; using System.Text; @@ -12,9 +12,9 @@ namespace SharpChat.Packet { public class AuthFailPacket : ServerPacket { public AuthFailReason Reason { get; private set; } - public FlashiiBanInfo BanInfo { get; private set; } + public MisuzuBanInfo BanInfo { get; private set; } - public AuthFailPacket(AuthFailReason reason, FlashiiBanInfo fbi = null) { + public AuthFailPacket(AuthFailReason reason, MisuzuBanInfo fbi = null) { Reason = reason; if(reason == AuthFailReason.Banned) diff --git a/SharpChat/Packet/AuthSuccessPacket.cs b/SharpChat/Packet/AuthSuccessPacket.cs index 8c78735..3b07454 100644 --- a/SharpChat/Packet/AuthSuccessPacket.cs +++ b/SharpChat/Packet/AuthSuccessPacket.cs @@ -7,11 +7,18 @@ namespace SharpChat.Packet { public ChatUser User { get; private set; } public ChatChannel Channel { get; private set; } public ChatUserSession Session { get; private set; } + public int MaxMessageLength { get; private set; } - public AuthSuccessPacket(ChatUser user, ChatChannel channel, ChatUserSession sess) { + public AuthSuccessPacket( + ChatUser user, + ChatChannel channel, + ChatUserSession sess, + int maxMsgLength + ) { User = user ?? throw new ArgumentNullException(nameof(user)); Channel = channel ?? throw new ArgumentNullException(nameof(channel)); Session = sess ?? throw new ArgumentNullException(nameof(channel)); + MaxMessageLength = maxMsgLength; } public override IEnumerable Pack() { @@ -23,7 +30,7 @@ namespace SharpChat.Packet { sb.Append('\t'); sb.Append(Channel.Name); sb.Append('\t'); - sb.Append(SockChatServer.MSG_LENGTH_MAX); + sb.Append(MaxMessageLength); return new[] { sb.ToString() }; } diff --git a/SharpChat/Packet/BanListPacket.cs b/SharpChat/Packet/BanListPacket.cs index fb3cfde..03b2b2c 100644 --- a/SharpChat/Packet/BanListPacket.cs +++ b/SharpChat/Packet/BanListPacket.cs @@ -1,4 +1,4 @@ -using SharpChat.Flashii; +using SharpChat.Misuzu; using System; using System.Collections.Generic; using System.Linq; @@ -6,9 +6,9 @@ using System.Text; namespace SharpChat.Packet { public class BanListPacket : ServerPacket { - public IEnumerable Bans { get; private set; } + public IEnumerable Bans { get; private set; } - public BanListPacket(IEnumerable bans) { + public BanListPacket(IEnumerable bans) { Bans = bans ?? throw new ArgumentNullException(nameof(bans)); } @@ -20,7 +20,7 @@ namespace SharpChat.Packet { sb.Append(DateTimeOffset.Now.ToUnixTimeSeconds()); sb.Append("\t-1\t0\fbanlist\f"); - foreach(FlashiiBanInfo ban in Bans) { + foreach(MisuzuBanInfo ban in Bans) { string banStr = string.IsNullOrEmpty(ban.UserName) ? ban.RemoteAddress : ban.UserName; sb.AppendFormat(@"{0}, ", banStr); } diff --git a/SharpChat/Program.cs b/SharpChat/Program.cs index c12309a..2a7dee4 100644 --- a/SharpChat/Program.cs +++ b/SharpChat/Program.cs @@ -1,10 +1,14 @@ -using System; +using SharpChat.Config; +using SharpChat.Misuzu; +using System; +using System.IO; using System.Net.Http; +using System.Text; using System.Threading; namespace SharpChat { public class Program { - public const ushort PORT = 6770; + public const string CONFIG = "sharpchat.cfg"; public static void Main(string[] args) { Console.WriteLine(@" _____ __ ________ __ "); @@ -12,12 +16,14 @@ namespace SharpChat { Console.WriteLine(@" \__ \/ __ \/ __ `/ ___/ __ \/ / / __ \/ __ `/ __/"); Console.WriteLine(@" ___/ / / / / /_/ / / / /_/ / /___/ / / / /_/ / /_ "); Console.WriteLine(@"/____/_/ /_/\__,_/_/ / .___/\____/_/ /_/\__,_/\__/ "); - Console.WriteLine(@" / _/ Sock Chat Server"); -#if DEBUG - Console.WriteLine(@"============================================ DEBUG =="); -#endif - - Database.ReadConfig(); + /**/Console.Write(@" /__/"); + if(SharpInfo.IsDebugBuild) { + Console.WriteLine(); + Console.Write(@"== "); + Console.Write(SharpInfo.VersionString); + Console.WriteLine(@" == DBG =="); + } else + Console.WriteLine(SharpInfo.VersionStringShort.PadLeft(28, ' ')); using ManualResetEvent mre = new(false); bool hasCancelled = false; @@ -32,17 +38,105 @@ namespace SharpChat { if(hasCancelled) return; - using HttpClient httpClient = new(new HttpClientHandler() { - UseProxy = false, // we will never and the initial resolving takes forever on linux - }); - httpClient.DefaultRequestHeaders.Add("User-Agent", "SharpChat/20230206"); + string configFile = CONFIG; + + // If the config file doesn't exist and we're using the default path, run the converter + if(!File.Exists(configFile) && configFile == CONFIG) + ConvertConfiguration(); + + using IConfig config = new StreamConfig(configFile); + + Database.Init(config.ScopeTo("mariadb")); if(hasCancelled) return; - using SockChatServer scs = new(httpClient, PORT); + using HttpClient httpClient = new(new HttpClientHandler() { + UseProxy = false, + }); + httpClient.DefaultRequestHeaders.Add("User-Agent", SharpInfo.ProgramName); + + if(hasCancelled) return; + + MisuzuClient msz = new(httpClient, config.ScopeTo("msz")); + + if(hasCancelled) return; + + using SockChatServer scs = new(httpClient, msz, config.ScopeTo("chat")); scs.Listen(mre); mre.WaitOne(); } + + private static void ConvertConfiguration() { + using Stream s = new FileStream(CONFIG, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); + s.SetLength(0); + s.Flush(); + + using StreamWriter sw = new(s, new UTF8Encoding(false)); + sw.WriteLine("# and ; can be used at the start of a line for comments."); + sw.WriteLine(); + + sw.WriteLine("# General Configuration"); + sw.WriteLine($"#chat:port {SockChatServer.DEFAULT_PORT}"); + sw.WriteLine($"#chat:msgMaxLength {SockChatServer.DEFAULT_MSG_LENGTH_MAX}"); + sw.WriteLine($"#chat:connMaxCount {SockChatServer.DEFAULT_MAX_CONNECTIONS}"); + sw.WriteLine($"#chat:floodKickLength {SockChatServer.DEFAULT_FLOOD_KICK_LENGTH}"); + + + sw.WriteLine(); + sw.WriteLine("# Channels"); + sw.WriteLine("chat:channels lounge staff"); + sw.WriteLine(); + + sw.WriteLine("# Lounge channel settings"); + sw.WriteLine("chat:channels:lounge:name Lounge"); + sw.WriteLine("chat:channels:lounge:autoJoin true"); + sw.WriteLine(); + + sw.WriteLine("# Staff channel settings"); + sw.WriteLine("chat:channels:staff:name Staff"); + sw.WriteLine("chat:channels:staff:minRank 5"); + + + const string msz_secret = "login_key.txt"; + const string msz_url = "msz_url.txt"; + + sw.WriteLine(); + sw.WriteLine("# Misuzu integration settings"); + if(File.Exists(msz_secret)) + sw.WriteLine(string.Format("msz:secret {0}", File.ReadAllText(msz_secret).Trim())); + else + sw.WriteLine("#msz:secret woomy"); + if(File.Exists(msz_url)) + sw.WriteLine(string.Format("msz:url {0}/_sockchat", File.ReadAllText(msz_url).Trim())); + else + sw.WriteLine("#msz:url https://flashii.net/_sockchat"); + + + const string mdb_config = @"mariadb.txt"; + bool hasMDB = File.Exists(mdb_config); + string[] mdbCfg = File.Exists(mdb_config) ? File.ReadAllLines(mdb_config) : Array.Empty(); + + sw.WriteLine(); + sw.WriteLine("# MariaDB configuration"); + if(!string.IsNullOrWhiteSpace(mdbCfg[0])) + sw.WriteLine($"mariadb:host {mdbCfg[0]}"); + else + sw.WriteLine($"#mariadb:host "); + if(mdbCfg.Length > 1) + sw.WriteLine($"mariadb:user {mdbCfg[1]}"); + else + sw.WriteLine($"#mariadb:user "); + if(mdbCfg.Length > 2) + sw.WriteLine($"mariadb:pass {mdbCfg[2]}"); + else + sw.WriteLine($"#mariadb:pass "); + if(mdbCfg.Length > 3) + sw.WriteLine($"mariadb:db {mdbCfg[3]}"); + else + sw.WriteLine($"#mariadb:db "); + + sw.Flush(); + } } } diff --git a/SharpChat/SharpChat.csproj b/SharpChat/SharpChat.csproj index 22ec985..efbe516 100644 --- a/SharpChat/SharpChat.csproj +++ b/SharpChat/SharpChat.csproj @@ -10,4 +10,16 @@ + + + + + + + + + + + + diff --git a/SharpChat/SharpInfo.cs b/SharpChat/SharpInfo.cs new file mode 100644 index 0000000..aa97fff --- /dev/null +++ b/SharpChat/SharpInfo.cs @@ -0,0 +1,37 @@ +using System.IO; +using System.Reflection; +using System.Text; + +namespace SharpChat { + public static class SharpInfo { + private const string NAME = @"SharpChat"; + private const string UNKNOWN = @"XXXXXXX"; + + public static string VersionString { get; } + public static string VersionStringShort { get; } + public static bool IsDebugBuild { get; } + + public static string ProgramName { get; } + + static SharpInfo() { +#if DEBUG + IsDebugBuild = true; +#endif + + try { + using Stream s = Assembly.GetExecutingAssembly().GetManifestResourceStream(@"SharpChat.version.txt"); + using StreamReader sr = new(s); + VersionString = sr.ReadLine().Trim(); + VersionStringShort = VersionString.Length > 10 ? VersionString[..10] : VersionString; + } catch { + VersionStringShort = VersionString = UNKNOWN; + } + + StringBuilder sb = new(); + sb.Append(NAME); + sb.Append('/'); + sb.Append(VersionStringShort); + ProgramName = sb.ToString(); + } + } +} diff --git a/SharpChat/SockChatServer.cs b/SharpChat/SockChatServer.cs index ab48c9f..969e6db 100644 --- a/SharpChat/SockChatServer.cs +++ b/SharpChat/SockChatServer.cs @@ -1,7 +1,8 @@ using Fleck; using SharpChat.Commands; +using SharpChat.Config; using SharpChat.Events; -using SharpChat.Flashii; +using SharpChat.Misuzu; using SharpChat.Packet; using System; using System.Collections.Generic; @@ -15,15 +16,10 @@ using System.Threading.Tasks; namespace SharpChat { public class SockChatServer : IDisposable { - public const int MSG_LENGTH_MAX = 5000; - -#if DEBUG - public const int MAX_CONNECTIONS = 9001; - public const int FLOOD_KICK_LENGTH = 5; -#else - public const int MAX_CONNECTIONS = 5; - public const int FLOOD_KICK_LENGTH = 30; -#endif + 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 bool IsDisposed { get; private set; } @@ -38,6 +34,11 @@ namespace SharpChat { public ChatContext Context { get; } private readonly HttpClient HttpClient; + private readonly MisuzuClient Misuzu; + + private readonly CachedValue MaxMessageLength; + private readonly CachedValue MaxConnections; + private readonly CachedValue FloodKickLength; private IReadOnlyCollection Commands { get; } = new IChatCommand[] { new AFKCommand(), @@ -54,22 +55,36 @@ namespace SharpChat { private ManualResetEvent Shutdown { get; set; } private bool IsShuttingDown = false; - public SockChatServer(HttpClient httpClient, ushort port) { - Logger.Write("Starting Sock Chat server..."); + public SockChatServer(HttpClient httpClient, MisuzuClient msz, IConfig config) { + Logger.Write("Initialising Sock Chat server..."); - HttpClient = httpClient; + HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); + Misuzu = msz ?? throw new ArgumentNullException(nameof(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(); - Context.Channels.Add(new ChatChannel("Lounge")); -#if DEBUG - Context.Channels.Add(new ChatChannel("Programming")); - Context.Channels.Add(new ChatChannel("Games")); - Context.Channels.Add(new ChatChannel("Splatoon")); - Context.Channels.Add(new ChatChannel("Password") { Password = "meow", }); -#endif - Context.Channels.Add(new ChatChannel("Staff") { Rank = 5 }); + string[] channelNames = config.ReadValue("channels", new[] { "lounge" }); + foreach(string channelName in channelNames) { + ChatChannel channelInfo = new(channelName); + IConfig channelCfg = config.ScopeTo($"channels:{channelName}"); + + string tmp; + tmp = channelCfg.SafeReadValue("name", string.Empty); + if(!string.IsNullOrWhiteSpace(tmp)) + channelInfo.Name = tmp; + + channelInfo.Password = channelCfg.SafeReadValue("password", string.Empty); + channelInfo.Rank = channelCfg.SafeReadValue("minRank", 0); + + Context.Channels.Add(channelInfo); + } + + ushort port = config.SafeReadValue("port", DEFAULT_PORT); Server = new SharpChatWebSocketServer($"ws://0.0.0.0:{port}"); } @@ -87,6 +102,8 @@ namespace SharpChat { sock.OnError = err => OnError(sock, err); sock.OnMessage = msg => OnMessage(sock, msg); }); + + Logger.Write("Listening..."); } private void OnOpen(IWebSocketConnection conn) { @@ -148,10 +165,9 @@ namespace SharpChat { if(sess.User.RateLimiter.State == ChatRateLimitState.Kick) { Task.Run(async () => { - TimeSpan duration = TimeSpan.FromSeconds(FLOOD_KICK_LENGTH); + TimeSpan duration = TimeSpan.FromSeconds(FloodKickLength); - await FlashiiBanInfo.CreateAsync( - HttpClient, + await Misuzu.CreateBanAsync( sess.User.UserId.ToString(), sess.RemoteAddress.ToString(), string.Empty, "::1", duration, @@ -186,7 +202,7 @@ namespace SharpChat { if(bumpList.Any()) Task.Run(async () => { - await FlashiiUrls.BumpUsersOnlineAsync(HttpClient, bumpList); + await Misuzu.BumpUsersOnlineAsync(bumpList); }).Wait(); LastBump = DateTimeOffset.UtcNow; @@ -219,11 +235,11 @@ namespace SharpChat { } Task.Run(async () => { - FlashiiAuthInfo fai; + MisuzuAuthInfo fai; string ipAddr = sess.RemoteAddress.ToString(); try { - fai = await FlashiiAuthInfo.VerifyAsync(HttpClient, authMethod, authToken, ipAddr); + fai = await Misuzu.AuthVerifyAsync(authMethod, authToken, ipAddr); } catch(Exception ex) { Logger.Write($"<{sess.Id}> Failed to authenticate: {ex}"); sess.Send(new AuthFailPacket(AuthFailReason.AuthInvalid)); @@ -242,9 +258,9 @@ namespace SharpChat { return; } - FlashiiBanInfo fbi; + MisuzuBanInfo fbi; try { - fbi = await FlashiiBanInfo.CheckAsync(HttpClient, fai.UserId.ToString(), ipAddr); + fbi = await Misuzu.CheckBanAsync(fai.UserId.ToString(), ipAddr); } catch(Exception ex) { Logger.Write($"<{sess.Id}> Failed auth ban check: {ex}"); sess.Send(new AuthFailPacket(AuthFailReason.AuthInvalid)); @@ -273,7 +289,7 @@ namespace SharpChat { } // Enforce a maximum amount of connections per user - if(aUser.SessionCount >= MAX_CONNECTIONS) { + if(aUser.SessionCount >= MaxConnections) { sess.Send(new AuthFailPacket(AuthFailReason.MaxSessions)); sess.Dispose(); return; @@ -294,7 +310,7 @@ namespace SharpChat { sess.Send(new LegacyCommandResponse(LCR.WELCOME, false, line)); } - Context.HandleJoin(aUser, Context.Channels.DefaultChannel, sess); + Context.HandleJoin(aUser, Context.Channels.DefaultChannel, sess, MaxMessageLength); }).Wait(); break; @@ -327,8 +343,9 @@ namespace SharpChat { mChannel.Send(new UserUpdatePacket(mUser)); } - if(messageText.Length > MSG_LENGTH_MAX) - messageText = messageText[..MSG_LENGTH_MAX]; + int maxMsgLength = MaxMessageLength; + if(messageText.Length > maxMsgLength) + messageText = messageText[..maxMsgLength]; messageText = messageText.Trim(); @@ -727,8 +744,8 @@ namespace SharpChat { Task.Run(async () => { // obviously it makes no sense to only check for one ip address but that's current misuzu limitations - FlashiiBanInfo fbi = await FlashiiBanInfo.CheckAsync( - HttpClient, banUser.UserId.ToString(), banUser.RemoteAddresses.First().ToString() + MisuzuBanInfo fbi = await Misuzu.CheckBanAsync( + banUser.UserId.ToString(), banUser.RemoteAddresses.First().ToString() ); if(fbi.IsBanned && !fbi.HasExpired) { @@ -736,8 +753,7 @@ namespace SharpChat { return; } - await FlashiiBanInfo.CreateAsync( - HttpClient, + await Misuzu.CreateBanAsync( banUser.UserId.ToString(), banUser.RemoteAddresses.First().ToString(), user.UserId.ToString(), sess.RemoteAddress.ToString(), duration, banReason @@ -770,14 +786,14 @@ namespace SharpChat { unbanUserTarget = unbanUser.UserId.ToString(); Task.Run(async () => { - FlashiiBanInfo banInfo = await FlashiiBanInfo.CheckAsync(HttpClient, unbanUserTarget, userIdIsName: unbanUserTargetIsName); + MisuzuBanInfo banInfo = await Misuzu.CheckBanAsync(unbanUserTarget, userIdIsName: unbanUserTargetIsName); if(!banInfo.IsBanned || banInfo.HasExpired) { user.Send(new LegacyCommandResponse(LCR.USER_NOT_BANNED, true, unbanUserTarget)); return; } - bool wasBanned = await banInfo.RevokeAsync(HttpClient, FlashiiBanInfo.RevokeKind.UserId); + bool wasBanned = await Misuzu.RevokeBanAsync(banInfo, MisuzuClient.BanRevokeKind.UserId); if(wasBanned) user.Send(new LegacyCommandResponse(LCR.USER_UNBANNED, false, unbanUserTarget)); else @@ -800,14 +816,14 @@ namespace SharpChat { unbanAddrTarget = unbanAddr.ToString(); Task.Run(async () => { - FlashiiBanInfo banInfo = await FlashiiBanInfo.CheckAsync(HttpClient, ipAddr: unbanAddrTarget); + MisuzuBanInfo banInfo = await Misuzu.CheckBanAsync(ipAddr: unbanAddrTarget); if(!banInfo.IsBanned || banInfo.HasExpired) { user.Send(new LegacyCommandResponse(LCR.USER_NOT_BANNED, true, unbanAddrTarget)); return; } - bool wasBanned = await banInfo.RevokeAsync(HttpClient, FlashiiBanInfo.RevokeKind.RemoteAddress); + bool wasBanned = await Misuzu.RevokeBanAsync(banInfo, MisuzuClient.BanRevokeKind.RemoteAddress); if(wasBanned) user.Send(new LegacyCommandResponse(LCR.USER_UNBANNED, false, unbanAddrTarget)); else @@ -823,7 +839,7 @@ namespace SharpChat { Task.Run(async () => { user.Send(new BanListPacket( - await FlashiiBanInfo.GetListAsync(HttpClient) + await Misuzu.GetBanListAsync() )); }).Wait(); break;