diff --git a/SharpChat.sln b/SharpChat.sln index d98fcb6..f5b141f 100644 --- a/SharpChat.sln +++ b/SharpChat.sln @@ -12,6 +12,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution LICENSE = LICENSE Protocol.md = Protocol.md README.md = README.md + start.sh = start.sh EndProjectSection EndProject Global diff --git a/SharpChat/BanManager.cs b/SharpChat/BanManager.cs deleted file mode 100644 index 56e8c07..0000000 --- a/SharpChat/BanManager.cs +++ /dev/null @@ -1,192 +0,0 @@ -using SharpChat.Flashii; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; - -namespace SharpChat { - public interface IBan { - DateTimeOffset Expires { get; } - string ToString(); - } - - public class BannedUser : IBan { - public long UserId { get; set; } - public DateTimeOffset Expires { get; set; } - public string Username { get; set; } - - public BannedUser() { - } - - public BannedUser(FlashiiBan fb) { - UserId = fb.UserId; - Expires = fb.Expires; - Username = fb.Username; - } - - public override string ToString() { - return Username; - } - } - - public class BannedIPAddress : IBan { - public IPAddress Address { get; set; } - public DateTimeOffset Expires { get; set; } - - public BannedIPAddress() { - } - - public BannedIPAddress(FlashiiBan fb) { - Address = IPAddress.Parse(fb.UserIP); - Expires = fb.Expires; - } - - public override string ToString() { - return Address.ToString(); - } - } - - public class BanManager : IDisposable { - private readonly List BanList = new(); - - private readonly HttpClient HttpClient; - public readonly ChatContext Context; - - - public bool IsDisposed { get; private set; } - - public BanManager(HttpClient httpClient, ChatContext context) { - HttpClient = httpClient; - Context = context; - RefreshFlashiiBans().Wait(); - } - - public void Add(ChatUser user, DateTimeOffset expires) { - if(expires <= DateTimeOffset.Now) - return; - - lock(BanList) { - BannedUser ban = BanList.OfType().FirstOrDefault(x => x.UserId == user.UserId); - - if(ban == null) - Add(new BannedUser { UserId = user.UserId, Expires = expires, Username = user.Username }); - else - ban.Expires = expires; - } - } - - public void Add(IPAddress addr, DateTimeOffset expires) { - if(expires <= DateTimeOffset.Now) - return; - - lock(BanList) { - BannedIPAddress ban = BanList.OfType().FirstOrDefault(x => x.Address.Equals(addr)); - - if(ban == null) - Add(new BannedIPAddress { Address = addr, Expires = expires }); - else - ban.Expires = expires; - } - } - - private void Add(IBan ban) { - if(ban == null) - return; - - lock(BanList) - if(!BanList.Contains(ban)) - BanList.Add(ban); - } - - public void Remove(ChatUser user) { - lock(BanList) - BanList.RemoveAll(x => x is BannedUser ub && ub.UserId == user.UserId); - } - - public void Remove(IPAddress addr) { - lock(BanList) - BanList.RemoveAll(x => x is BannedIPAddress ib && ib.Address.Equals(addr)); - } - - public void Remove(IBan ban) { - lock(BanList) - BanList.Remove(ban); - } - - public DateTimeOffset Check(ChatUser user) { - if(user == null) - return DateTimeOffset.MinValue; - - lock(BanList) - return BanList.OfType().Where(x => x.UserId == user.UserId).FirstOrDefault()?.Expires ?? DateTimeOffset.MinValue; - } - - public DateTimeOffset Check(IPAddress addr) { - if(addr == null) - return DateTimeOffset.MinValue; - - lock(BanList) - return BanList.OfType().Where(x => x.Address.Equals(addr)).FirstOrDefault()?.Expires ?? DateTimeOffset.MinValue; - } - - public BannedUser GetUser(string username) { - if(username == null) - return null; - - if(!long.TryParse(username, out long userId)) - userId = 0; - - lock(BanList) - return BanList.OfType().FirstOrDefault(x => x.Username.ToLowerInvariant() == username.ToLowerInvariant() || (userId > 0 && x.UserId == userId)); - } - - public BannedIPAddress GetIPAddress(IPAddress addr) { - lock(BanList) - return BanList.OfType().FirstOrDefault(x => x.Address.Equals(addr)); - } - - public void RemoveExpired() { - lock(BanList) - BanList.RemoveAll(x => x.Expires <= DateTimeOffset.Now); - } - - public async Task RefreshFlashiiBans() { - IEnumerable bans = await FlashiiBan.GetListAsync(HttpClient); - - if(!bans.Any()) - return; - - lock(BanList) - foreach(FlashiiBan fb in bans) { - if(!BanList.OfType().Any(x => x.UserId == fb.UserId)) - Add(new BannedUser(fb)); - if(!BanList.OfType().Any(x => x.Address.ToString() == fb.UserIP)) - Add(new BannedIPAddress(fb)); - } - } - - public IEnumerable All() { - lock(BanList) - return BanList.ToList(); - } - - ~BanManager() { - DoDispose(); - } - - public void Dispose() { - DoDispose(); - GC.SuppressFinalize(this); - } - - private void DoDispose() { - if(IsDisposed) - return; - IsDisposed = true; - - BanList.Clear(); - } - } -} diff --git a/SharpChat/ChatContext.cs b/SharpChat/ChatContext.cs index 54097c5..1632fd9 100644 --- a/SharpChat/ChatContext.cs +++ b/SharpChat/ChatContext.cs @@ -16,9 +16,6 @@ namespace SharpChat { public SockChatServer Server { get; } public Timer BumpTimer { get; } - public BanManager Bans { get; } - public readonly object BansAccess = new(); - public ChannelManager Channels { get; } public UserManager Users { get; } public ChatEventManager Events { get; } @@ -28,7 +25,6 @@ namespace SharpChat { public ChatContext(HttpClient httpClient, SockChatServer server) { HttpClient = httpClient; Server = server; - Bans = new(httpClient, this); Users = new(this); Channels = new(this); Events = new(this); @@ -37,26 +33,13 @@ namespace SharpChat { } public void Update() { - lock(BansAccess) - Bans.RemoveExpired(); CheckPings(); } - public void BanUser(ChatUser user, DateTimeOffset? until = null, bool banIPs = false, UserDisconnectReason reason = UserDisconnectReason.Kicked) { - if(until.HasValue && until.Value <= DateTimeOffset.UtcNow) - until = null; - - if(until.HasValue) { - user.Send(new ForceDisconnectPacket(ForceDisconnectReason.Banned, until.Value)); - - lock(BansAccess) { - Bans.Add(user, until.Value); - - if(banIPs) - foreach(IPAddress ip in user.RemoteAddresses) - Bans.Add(ip, until.Value); - } - } else + public void BanUser(ChatUser user, TimeSpan duration, UserDisconnectReason reason = UserDisconnectReason.Kicked) { + if(duration > TimeSpan.Zero) + user.Send(new ForceDisconnectPacket(ForceDisconnectReason.Banned, DateTimeOffset.Now + duration)); + else user.Send(new ForceDisconnectPacket(ForceDisconnectReason.Kicked)); user.Close(); @@ -194,7 +177,6 @@ namespace SharpChat { Events?.Dispose(); Channels?.Dispose(); Users?.Dispose(); - Bans?.Dispose(); } } } diff --git a/SharpChat/ChatUser.cs b/SharpChat/ChatUser.cs index 12b0efd..4c77724 100644 --- a/SharpChat/ChatUser.cs +++ b/SharpChat/ChatUser.cs @@ -120,12 +120,12 @@ namespace SharpChat { public ChatUser() { } - public ChatUser(FlashiiAuth auth) { + public ChatUser(FlashiiAuthInfo auth) { UserId = auth.UserId; ApplyAuth(auth, true); } - public void ApplyAuth(FlashiiAuth auth, bool invalidateRestrictions = false) { + public void ApplyAuth(FlashiiAuthInfo auth, bool invalidateRestrictions = false) { Username = auth.Username; if(Status == ChatUserStatus.Offline) diff --git a/SharpChat/Flashii/FlashiiAuth.cs b/SharpChat/Flashii/FlashiiAuth.cs deleted file mode 100644 index 008011d..0000000 --- a/SharpChat/Flashii/FlashiiAuth.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Net.Http; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading.Tasks; - -namespace SharpChat.Flashii { - public class FlashiiAuthRequest { - [JsonPropertyName(@"user_id")] - public long UserId { get; set; } - - [JsonPropertyName(@"token")] - public string Token { get; set; } - - [JsonPropertyName(@"ip")] - public string IPAddress { get; set; } - - [JsonIgnore] - public string Hash - => string.Join(@"#", UserId, Token, IPAddress).GetSignedHash(); - - public byte[] GetJSON() { - return JsonSerializer.SerializeToUtf8Bytes(this); - } - } - - public class FlashiiAuth { - [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; } - - public static async Task AttemptAsync(HttpClient httpClient, FlashiiAuthRequest authRequest) { - if(httpClient == null) - throw new ArgumentNullException(nameof(httpClient)); - if(authRequest == null) - throw new ArgumentNullException(nameof(authRequest)); - -#if DEBUG - if(authRequest.UserId >= 10000) - return new FlashiiAuth { - Success = true, - UserId = authRequest.UserId, - Username = @"Misaka-" + (authRequest.UserId - 10000), - ColourRaw = (RNG.Next(0, 255) << 16) | (RNG.Next(0, 255) << 8) | RNG.Next(0, 255), - Rank = 0, - SilencedUntil = DateTimeOffset.MinValue, - Permissions = ChatUserPermissions.SendMessage | ChatUserPermissions.EditOwnMessage | ChatUserPermissions.DeleteOwnMessage, - }; -#endif - - HttpRequestMessage request = new(HttpMethod.Post, FlashiiUrls.AuthURL) { - Content = new ByteArrayContent(authRequest.GetJSON()), - Headers = { - { @"X-SharpChat-Signature", authRequest.Hash }, - }, - }; - - using HttpResponseMessage response = await httpClient.SendAsync(request); - - return JsonSerializer.Deserialize( - await response.Content.ReadAsByteArrayAsync() - ); - } - } -} diff --git a/SharpChat/Flashii/FlashiiAuthInfo.cs b/SharpChat/Flashii/FlashiiAuthInfo.cs new file mode 100644 index 0000000..315b683 --- /dev/null +++ b/SharpChat/Flashii/FlashiiAuthInfo.cs @@ -0,0 +1,64 @@ +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/FlashiiBan.cs b/SharpChat/Flashii/FlashiiBan.cs deleted file mode 100644 index c2ede72..0000000 --- a/SharpChat/Flashii/FlashiiBan.cs +++ /dev/null @@ -1,39 +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 FlashiiBan { - private const string STRING = @"givemethebeans"; - - [JsonPropertyName(@"id")] - public int UserId { get; set; } - - [JsonPropertyName(@"ip")] - public string UserIP { get; set; } - - [JsonPropertyName(@"expires")] - public DateTimeOffset Expires { get; set; } - - [JsonPropertyName(@"username")] - public string Username { get; set; } - - public static async Task> GetListAsync(HttpClient httpClient) { - if(httpClient == null) - throw new ArgumentNullException(nameof(httpClient)); - - HttpRequestMessage request = new(HttpMethod.Get, FlashiiUrls.BansURL) { - Headers = { - { @"X-SharpChat-Signature", STRING.GetSignedHash() }, - }, - }; - - using HttpResponseMessage response = await httpClient.SendAsync(request); - - return JsonSerializer.Deserialize>(await response.Content.ReadAsByteArrayAsync()); - } - } -} diff --git a/SharpChat/Flashii/FlashiiBanInfo.cs b/SharpChat/Flashii/FlashiiBanInfo.cs new file mode 100644 index 0000000..9f2a376 --- /dev/null +++ b/SharpChat/Flashii/FlashiiBanInfo.cs @@ -0,0 +1,183 @@ +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 index bf8209b..1b1302b 100644 --- a/SharpChat/Flashii/FlashiiUrls.cs +++ b/SharpChat/Flashii/FlashiiUrls.cs @@ -2,21 +2,32 @@ 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 BASE_URL_FILE = "msz_url.txt"; + private const string BASE_URL_FALLBACK = "https://flashii.net"; - private const string AUTH = @"/_sockchat/verify"; - private const string BANS = @"/_sockchat/bans"; - private const string BUMP = @"/_sockchat/bump"; + private const string VERIFY = "/_sockchat/verify"; + private const string BUMP = "/_sockchat/bump"; + + 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 AuthURL { get; } - public static string BansURL { get; } 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() { - AuthURL = GetURL(AUTH); - BansURL = GetURL(BANS); 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() { diff --git a/SharpChat/Packet/AuthFailPacket.cs b/SharpChat/Packet/AuthFailPacket.cs index fb8d0c9..9f6cce0 100644 --- a/SharpChat/Packet/AuthFailPacket.cs +++ b/SharpChat/Packet/AuthFailPacket.cs @@ -1,4 +1,5 @@ -using System; +using SharpChat.Flashii; +using System; using System.Collections.Generic; using System.Text; @@ -11,16 +12,13 @@ namespace SharpChat.Packet { public class AuthFailPacket : ServerPacket { public AuthFailReason Reason { get; private set; } - public DateTimeOffset Expires { get; private set; } + public FlashiiBanInfo BanInfo { get; private set; } - public AuthFailPacket(AuthFailReason reason, DateTimeOffset? expires = null) { + public AuthFailPacket(AuthFailReason reason, FlashiiBanInfo fbi = null) { Reason = reason; - if(reason == AuthFailReason.Banned) { - if(!expires.HasValue) - throw new ArgumentNullException(nameof(expires)); - Expires = expires.Value; - } + if(reason == AuthFailReason.Banned) + BanInfo = fbi ?? throw new ArgumentNullException(nameof(fbi)); } public override IEnumerable Pack() { @@ -45,10 +43,10 @@ namespace SharpChat.Packet { if(Reason == AuthFailReason.Banned) { sb.Append('\t'); - if(Expires == DateTimeOffset.MaxValue) - sb.Append(@"-1"); + if(BanInfo.IsPermanent) + sb.Append("-1"); else - sb.Append(Expires.ToUnixTimeSeconds()); + sb.Append(BanInfo.ExpiresAt.ToUnixTimeSeconds()); } yield return sb.ToString(); diff --git a/SharpChat/Packet/BanListPacket.cs b/SharpChat/Packet/BanListPacket.cs index 581e2d0..fb3cfde 100644 --- a/SharpChat/Packet/BanListPacket.cs +++ b/SharpChat/Packet/BanListPacket.cs @@ -1,13 +1,14 @@ -using System; +using SharpChat.Flashii; +using System; using System.Collections.Generic; using System.Linq; 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)); } @@ -19,8 +20,10 @@ namespace SharpChat.Packet { sb.Append(DateTimeOffset.Now.ToUnixTimeSeconds()); sb.Append("\t-1\t0\fbanlist\f"); - foreach(IBan ban in Bans) - sb.AppendFormat(@"{0}, ", ban); + foreach(FlashiiBanInfo ban in Bans) { + string banStr = string.IsNullOrEmpty(ban.UserName) ? ban.RemoteAddress : ban.UserName; + sb.AppendFormat(@"{0}, ", banStr); + } if(Bans.Any()) sb.Length -= 2; diff --git a/SharpChat/Packet/ForceDisconnectPacket.cs b/SharpChat/Packet/ForceDisconnectPacket.cs index 8458ec9..96f06c6 100644 --- a/SharpChat/Packet/ForceDisconnectPacket.cs +++ b/SharpChat/Packet/ForceDisconnectPacket.cs @@ -31,7 +31,10 @@ namespace SharpChat.Packet { if(Reason == ForceDisconnectReason.Banned) { sb.Append('\t'); - sb.Append(Expires.ToUnixTimeSeconds()); + if(Expires.Year >= 2100) + sb.Append("-1"); + else + sb.Append(Expires.ToUnixTimeSeconds()); } yield return sb.ToString(); diff --git a/SharpChat/SockChatServer.cs b/SharpChat/SockChatServer.cs index 29f64e8..e00718e 100644 --- a/SharpChat/SockChatServer.cs +++ b/SharpChat/SockChatServer.cs @@ -11,6 +11,7 @@ using System.Net; using System.Net.Http; using System.Text; using System.Threading; +using System.Threading.Tasks; namespace SharpChat { public class SockChatServer : IDisposable { @@ -19,11 +20,9 @@ namespace SharpChat { #if DEBUG public const int MAX_CONNECTIONS = 9001; public const int FLOOD_KICK_LENGTH = 5; - public const bool ENABLE_TYPING_EVENT = true; #else public const int MAX_CONNECTIONS = 5; public const int FLOOD_KICK_LENGTH = 30; - public const bool ENABLE_TYPING_EVENT = false; #endif public bool IsDisposed { get; private set; } @@ -144,14 +143,25 @@ namespace SharpChat { sess.User.RateLimiter.AddTimePoint(); if(sess.User.RateLimiter.State == ChatRateLimitState.Kick) { - Context.BanUser(sess.User, DateTimeOffset.UtcNow.AddSeconds(FLOOD_KICK_LENGTH), false, UserDisconnectReason.Flood); + Task.Run(async () => { + TimeSpan duration = TimeSpan.FromSeconds(FLOOD_KICK_LENGTH); + + await FlashiiBanInfo.CreateAsync( + HttpClient, + sess.User.UserId.ToString(), sess.RemoteAddress.ToString(), + string.Empty, "::1", + duration, + "Kicked from chat for flood protection." + ); + + Context.BanUser(sess.User, duration, UserDisconnectReason.Flood); + }).Wait(); return; } else if(sess.User.RateLimiter.State == ChatRateLimitState.Warning) - sess.User.Send(new FloodWarningPacket()); // make it so this thing only sends once + sess.User.Send(new FloodWarningPacket()); } string[] args = msg.Split('\t'); - if(args.Length < 1) return; @@ -168,58 +178,80 @@ namespace SharpChat { if(sess.User != null) break; - DateTimeOffset aBanned; - lock(Context.BansAccess) - aBanned = Context.Bans.Check(sess.RemoteAddress); - - if(aBanned > DateTimeOffset.UtcNow) { - sess.Send(new AuthFailPacket(AuthFailReason.Banned, aBanned)); + string authMethod = args.ElementAtOrDefault(1); + if(string.IsNullOrWhiteSpace(authMethod)) { + sess.Send(new AuthFailPacket(AuthFailReason.AuthInvalid)); sess.Dispose(); break; } - if(args.Length < 3 || !long.TryParse(args[1], out long aUserId)) + string authToken = args.ElementAtOrDefault(2); + if(string.IsNullOrWhiteSpace(authToken)) { + sess.Send(new AuthFailPacket(AuthFailReason.AuthInvalid)); + sess.Dispose(); break; + } - FlashiiAuth.AttemptAsync(HttpClient, new FlashiiAuthRequest { - UserId = aUserId, - Token = args[2], - IPAddress = sess.RemoteAddress.ToString(), - }).ContinueWith(authTask => { - if(authTask.IsFaulted) { - Logger.Write($@"<{sess.Id}> Auth task fail: {authTask.Exception}"); + if(authMethod.All(c => c is >= '0' and <= '9') && authToken.Contains(':')) { + string[] tokenParts = authToken.Split(':', 2); + authMethod = tokenParts[0]; + authToken = tokenParts[1]; + } + + Task.Run(async () => { + FlashiiAuthInfo fai; + string ipAddr = sess.RemoteAddress.ToString(); + + try { + fai = await FlashiiAuthInfo.VerifyAsync(HttpClient, authMethod, authToken, ipAddr); + } catch(Exception ex) { + Logger.Write($@"<{sess.Id}> Failed to authenticate: {ex}"); + sess.Send(new AuthFailPacket(AuthFailReason.AuthInvalid)); + sess.Dispose(); +#if DEBUG + throw; +#else + return; +#endif + } + + if(!fai.Success) { + Logger.Debug($@"<{sess.Id}> Auth fail: {fai.Reason}"); sess.Send(new AuthFailPacket(AuthFailReason.AuthInvalid)); sess.Dispose(); return; } - FlashiiAuth auth = authTask.Result; - - if(!auth.Success) { - Logger.Debug($@"<{sess.Id}> Auth fail: {auth.Reason}"); + FlashiiBanInfo fbi; + try { + fbi = await FlashiiBanInfo.CheckAsync(HttpClient, fai.UserId.ToString(), ipAddr); + } catch(Exception ex) { + Logger.Write($@"<{sess.Id}> Failed auth ban check: {ex}"); sess.Send(new AuthFailPacket(AuthFailReason.AuthInvalid)); sess.Dispose(); +#if DEBUG + throw; +#else + return; +#endif + } + + if(fbi.IsBanned && !fbi.HasExpired) { + Logger.Write($@"<{sess.Id}> User is banned."); + sess.Send(new AuthFailPacket(AuthFailReason.Banned, fbi)); + sess.Dispose(); return; } - ChatUser aUser = Context.Users.Get(auth.UserId); + ChatUser aUser = Context.Users.Get(fai.UserId); if(aUser == null) - aUser = new ChatUser(auth); + aUser = new ChatUser(fai); else { - aUser.ApplyAuth(auth); + aUser.ApplyAuth(fai); aUser.Channel?.Send(new UserUpdatePacket(aUser)); } - lock(Context.BansAccess) - aBanned = Context.Bans.Check(aUser); - - if(aBanned > DateTimeOffset.Now) { - sess.Send(new AuthFailPacket(AuthFailReason.Banned, aBanned)); - sess.Dispose(); - return; - } - // Enforce a maximum amount of connections per user if(aUser.SessionCount >= MAX_CONNECTIONS) { sess.Send(new AuthFailPacket(AuthFailReason.MaxSessions)); @@ -243,7 +275,7 @@ namespace SharpChat { } Context.HandleJoin(aUser, Context.Channels.DefaultChannel, sess); - }); + }).Wait(); break; case "2": @@ -287,7 +319,7 @@ namespace SharpChat { IChatMessage message = null; if(messageText[0] == '/') { - message = HandleV1Command(messageText, mUser, mChannel); + message = HandleV1Command(messageText, mUser, mChannel, sess); if(message == null) break; @@ -307,7 +339,7 @@ namespace SharpChat { } } - public IChatMessage HandleV1Command(string message, ChatUser user, ChatChannel channel) { + public IChatMessage HandleV1Command(string message, ChatUser user, ChatChannel channel, ChatUserSession sess) { string[] parts = message[1..].Split(' '); string commandName = parts[0].Replace(@".", string.Empty).ToLowerInvariant(); @@ -640,10 +672,13 @@ namespace SharpChat { break; } - ChatUser banUser; + string banUserTarget = parts.ElementAtOrDefault(1); + string banDurationStr = parts.ElementAtOrDefault(2); + int banReasonIndex = 2; + ChatUser banUser = null; - if(parts.Length < 2 || (banUser = Context.Users.Get(parts[1])) == null) { - user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, parts.Length < 2 ? @"User" : parts[1])); + if(banUserTarget == null || (banUser = Context.Users.Get(banUserTarget)) == null) { + user.Send(new LegacyCommandResponse(LCR.USER_NOT_FOUND, true, banUser == null ? @"User" : banUserTarget)); break; } @@ -652,24 +687,44 @@ namespace SharpChat { break; } - lock(Context.BansAccess) - if(Context.Bans.Check(banUser) > DateTimeOffset.Now) { - user.Send(new LegacyCommandResponse(LCR.KICK_NOT_ALLOWED, true, banUser.DisplayName)); - break; - } - - DateTimeOffset? banUntil = isBanning ? (DateTimeOffset?)DateTimeOffset.MaxValue : null; - - if(parts.Length > 2) { - if(!double.TryParse(parts[2], out double silenceSeconds)) { + TimeSpan duration = isBanning ? TimeSpan.MaxValue : TimeSpan.Zero; + if(!string.IsNullOrWhiteSpace(banDurationStr) && double.TryParse(banDurationStr, out double durationSeconds)) { + if(durationSeconds < 0) { user.Send(new LegacyCommandResponse(LCR.COMMAND_FORMAT_ERROR)); break; } - banUntil = DateTimeOffset.UtcNow.AddSeconds(silenceSeconds); + duration = TimeSpan.FromSeconds(durationSeconds); + ++banReasonIndex; } - Context.BanUser(banUser, banUntil, isBanning); + if(duration <= TimeSpan.Zero) { + Context.BanUser(banUser, duration); + break; + } + + string banReason = string.Join(' ', parts.Skip(banReasonIndex)); + + 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() + ); + + if(fbi.IsBanned && !fbi.HasExpired) { + user.Send(new LegacyCommandResponse(LCR.KICK_NOT_ALLOWED, true, banUser.DisplayName)); + return; + } + + await FlashiiBanInfo.CreateAsync( + HttpClient, + banUser.UserId.ToString(), banUser.RemoteAddresses.First().ToString(), + user.UserId.ToString(), sess.RemoteAddress.ToString(), + duration, banReason + ); + + Context.BanUser(banUser, duration); + }).Wait(); break; case @"pardon": case @"unban": @@ -678,24 +733,36 @@ namespace SharpChat { break; } - if(parts.Length < 2) { - user.Send(new LegacyCommandResponse(LCR.USER_NOT_BANNED, true, string.Empty)); + bool unbanUserTargetIsName = true; + string unbanUserTarget = parts.ElementAtOrDefault(1); + if(string.IsNullOrWhiteSpace(unbanUserTarget)) { + user.Send(new LegacyCommandResponse(LCR.COMMAND_FORMAT_ERROR)); break; } - BannedUser unbanUser; - lock(Context.BansAccess) - unbanUser = Context.Bans.GetUser(parts[1]); - - if(unbanUser == null || unbanUser.Expires <= DateTimeOffset.Now) { - user.Send(new LegacyCommandResponse(LCR.USER_NOT_BANNED, true, unbanUser?.Username ?? parts[1])); - break; + ChatUser unbanUser = Context.Users.Get(unbanUserTarget); + if(unbanUser == null && long.TryParse(unbanUserTarget, out long unbanUserId)) { + unbanUserTargetIsName = false; + unbanUser = Context.Users.Get(unbanUserId); } - lock(Context.BansAccess) - Context.Bans.Remove(unbanUser); + if(unbanUser != null) + unbanUserTarget = unbanUser.UserId.ToString(); - user.Send(new LegacyCommandResponse(LCR.USER_UNBANNED, false, unbanUser)); + Task.Run(async () => { + FlashiiBanInfo banInfo = await FlashiiBanInfo.CheckAsync(HttpClient, 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); + if(wasBanned) + user.Send(new LegacyCommandResponse(LCR.USER_UNBANNED, false, unbanUserTarget)); + else + user.Send(new LegacyCommandResponse(LCR.USER_NOT_BANNED, true, unbanUserTarget)); + }).Wait(); break; case @"pardonip": case @"unbanip": @@ -704,21 +771,28 @@ namespace SharpChat { break; } - if(parts.Length < 2 || !IPAddress.TryParse(parts[1], out IPAddress unbanIP)) { - user.Send(new LegacyCommandResponse(LCR.USER_NOT_BANNED, true, string.Empty)); + string unbanAddrTarget = parts.ElementAtOrDefault(1); + if(string.IsNullOrWhiteSpace(unbanAddrTarget) || !IPAddress.TryParse(unbanAddrTarget, out IPAddress unbanAddr)) { + user.Send(new LegacyCommandResponse(LCR.COMMAND_FORMAT_ERROR)); break; } - lock(Context.BansAccess) { - if(Context.Bans.Check(unbanIP) <= DateTimeOffset.Now) { - user.Send(new LegacyCommandResponse(LCR.USER_NOT_BANNED, true, unbanIP)); - break; + unbanAddrTarget = unbanAddr.ToString(); + + Task.Run(async () => { + FlashiiBanInfo banInfo = await FlashiiBanInfo.CheckAsync(HttpClient, ipAddr: unbanAddrTarget); + + if(!banInfo.IsBanned || banInfo.HasExpired) { + user.Send(new LegacyCommandResponse(LCR.USER_NOT_BANNED, true, unbanAddrTarget)); + return; } - Context.Bans.Remove(unbanIP); - } - - user.Send(new LegacyCommandResponse(LCR.USER_UNBANNED, false, unbanIP)); + bool wasBanned = await banInfo.RevokeAsync(HttpClient, FlashiiBanInfo.RevokeKind.RemoteAddress); + if(wasBanned) + user.Send(new LegacyCommandResponse(LCR.USER_UNBANNED, false, unbanAddrTarget)); + else + user.Send(new LegacyCommandResponse(LCR.USER_NOT_BANNED, true, unbanAddrTarget)); + }).Wait(); break; case @"bans": // gets a list of bans case @"banned": @@ -727,8 +801,11 @@ namespace SharpChat { break; } - lock(Context.BansAccess) - user.Send(new BanListPacket(Context.Bans.All())); + Task.Run(async () => { + user.Send(new BanListPacket( + await FlashiiBanInfo.GetListAsync(HttpClient) + )); + }).Wait(); break; case @"silence": // silence a user if(!user.Can(ChatUserPermissions.SilenceUser)) { diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..480ee95 --- /dev/null +++ b/start.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +export DOTNET_CLI_TELEMETRY_OPTOUT=1 +export LANG=en_US.UTF-8 + +dotnet run --project SharpChat -c Release