diff --git a/.editorconfig b/.editorconfig index fa54a6e..4aa2717 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,4 @@ -root = true +root = true [*] end_of_line = lf @@ -11,3 +11,134 @@ indent_size = 4 # IDE1006: Naming Styles dotnet_diagnostic.IDE1006.severity = none +csharp_indent_labels = one_less_than_current +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = true:silent +csharp_style_namespace_declarations = block_scoped:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_prefer_primary_constructors = true:suggestion +csharp_prefer_system_threading_lock = true:suggestion +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_prefer_unbound_generic_type_in_nameof = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_prefer_static_local_function = true:suggestion +csharp_style_prefer_readonly_struct = true:suggestion +csharp_prefer_static_anonymous_function = true:suggestion +csharp_style_prefer_readonly_struct_member = true:suggestion +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent +csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent +csharp_style_conditional_delegate_call = true:suggestion +csharp_style_prefer_switch_expression = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_pattern_matching = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_prefer_extended_property_pattern = true:suggestion +csharp_style_var_for_built_in_types = false:silent +csharp_style_var_when_type_is_apparent = false:silent +csharp_style_var_elsewhere = false:silent + +[*.{cs,vb}] +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion +dotnet_style_namespace_match_folder = true:suggestion +dotnet_style_readonly_field = true:suggestion +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent +dotnet_style_allow_statement_immediately_after_block_experimental = true:silent +dotnet_style_allow_multiple_blank_lines_experimental = true:silent +dotnet_code_quality_unused_parameters = all:suggestion +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent +dotnet_style_qualification_for_property = false:silent +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_event = false:silent +dotnet_style_qualification_for_method = false:silent diff --git a/SharpChat.Misuzu/FlashiiAuthResult.cs b/SharpChat.Misuzu/FlashiiAuthResult.cs new file mode 100644 index 0000000..8bcc823 --- /dev/null +++ b/SharpChat.Misuzu/FlashiiAuthResult.cs @@ -0,0 +1,31 @@ +using SharpChat.Auth; +using System.Text.Json.Serialization; + +namespace SharpChat.Flashii { + public class FlashiiAuthResult : AuthResult { + public string UserId => UserIdRaw.ToString(); + public string UserName => UserNameRaw ?? string.Empty; + public ColourInheritable UserColour => ColourInheritable.FromMisuzu(UserColourRaw); + + [JsonPropertyName("success")] + public bool Success { get; init; } + + [JsonPropertyName("reason")] + public string? Reason { get; init; } + + [JsonPropertyName("user_id")] + public long UserIdRaw { get; init; } + + [JsonPropertyName("username")] + public string? UserNameRaw { get; init; } + + [JsonPropertyName("colour_raw")] + public int UserColourRaw { get; init; } + + [JsonPropertyName("hierarchy")] + public int UserRank { get; init; } + + [JsonPropertyName("perms")] + public UserPermissions UserPermissions { get; init; } + } +} diff --git a/SharpChat.Misuzu/FlashiiBanInfo.cs b/SharpChat.Misuzu/FlashiiBanInfo.cs new file mode 100644 index 0000000..ed6906c --- /dev/null +++ b/SharpChat.Misuzu/FlashiiBanInfo.cs @@ -0,0 +1,13 @@ +using SharpChat.Bans; + +namespace SharpChat.Flashii { + public abstract class FlashiiBanInfo( + BanKind kind, + FlashiiRawBanInfo rawBanInfo + ) : BanInfo { + public BanKind Kind { get; } = kind; + public bool IsPermanent { get; } = rawBanInfo.IsPermanent; + public DateTimeOffset ExpiresAt { get; } = rawBanInfo.ExpiresAt; + public abstract override string ToString(); + } +} diff --git a/SharpChat.Misuzu/FlashiiClient.cs b/SharpChat.Misuzu/FlashiiClient.cs new file mode 100644 index 0000000..a749532 --- /dev/null +++ b/SharpChat.Misuzu/FlashiiClient.cs @@ -0,0 +1,240 @@ +using SharpChat.Auth; +using SharpChat.Bans; +using SharpChat.Configuration; +using System.Net; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; + +namespace SharpChat.Flashii { + public class FlashiiClient(HttpClient httpClient, Config config) : AuthClient, BansClient { + private const string DEFAULT_BASE_URL = "https://flashii.net/_sockchat"; + private readonly CachedValue<string> BaseURL = config.ReadCached("url", DEFAULT_BASE_URL); + + private const string DEFAULT_SECRET_KEY = "woomy"; + private readonly CachedValue<string> SecretKey = config.ReadCached("secret", DEFAULT_SECRET_KEY); + + private string CreateStringSignature(string str) { + return CreateBufferSignature(Encoding.UTF8.GetBytes(str)); + } + + private string CreateBufferSignature(byte[] bytes) { + using HMACSHA256 algo = new(Encoding.UTF8.GetBytes(SecretKey!)); + return string.Concat(algo.ComputeHash(bytes).Select(c => c.ToString("x2"))); + } + + private const string AUTH_VERIFY_URL = "{0}/verify"; + private const string AUTH_VERIFY_SIG = "verify#{0}#{1}#{2}"; + + public async Task<AuthResult> AuthVerifyAsync(IPAddress remoteAddr, string scheme, string token) { + string remoteAddrStr = remoteAddr.ToString(); + + HttpRequestMessage request = new(HttpMethod.Post, string.Format(AUTH_VERIFY_URL, BaseURL)) { + Content = new FormUrlEncodedContent(new Dictionary<string, string> { + { "method", scheme }, + { "token", token }, + { "ipaddr", remoteAddrStr }, + }), + Headers = { + { "X-SharpChat-Signature", CreateStringSignature(string.Format(AUTH_VERIFY_SIG, scheme, token, remoteAddrStr)) }, + }, + }; + + using HttpResponseMessage response = await httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + + using Stream stream = await response.Content.ReadAsStreamAsync(); + FlashiiAuthResult? authResult = await JsonSerializer.DeserializeAsync<FlashiiAuthResult>(stream); + if(authResult?.Success != true) + throw new AuthFailedException(authResult?.Reason ?? "none"); + + return authResult; + } + + private const string AUTH_BUMP_USERS_ONLINE_URL = "{0}/bump"; + + public async Task AuthBumpUsersOnlineAsync(IEnumerable<(IPAddress remoteAddr, string userId)> entries) { + if(!entries.Any()) + return; + + string now = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(); + StringBuilder sb = new(); + sb.AppendFormat("bump#{0}", now); + + Dictionary<string, string> formData = new() { + { "t", now }, + }; + + foreach(var (remoteAddr, userId) in entries) { + string remoteAddrStr = remoteAddr.ToString(); + sb.AppendFormat("#{0}:{1}", userId, remoteAddrStr); + formData.Add(string.Format("u[{0}]", userId), remoteAddrStr); + } + + HttpRequestMessage request = new(HttpMethod.Post, string.Format(AUTH_BUMP_USERS_ONLINE_URL, BaseURL)) { + Headers = { + { "X-SharpChat-Signature", CreateStringSignature(sb.ToString()) } + }, + Content = new FormUrlEncodedContent(formData), + }; + + using HttpResponseMessage response = await httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + } + + private const string BANS_CREATE_URL = "{0}/bans/create"; + private const string BANS_CREATE_SIG = "create#{0}#{1}#{2}#{3}#{4}#{5}#{6}#{7}"; + + public async Task BanCreateAsync( + BanKind kind, + TimeSpan duration, + IPAddress remoteAddr, + string? userId = null, + string? reason = null, + IPAddress? issuerRemoteAddr = null, + string? issuerUserId = null + ) { + if(duration <= TimeSpan.Zero || kind != BanKind.User) + return; + + issuerUserId ??= string.Empty; + userId ??= string.Empty; + reason ??= string.Empty; + issuerRemoteAddr ??= IPAddress.IPv6None; + + string isPerma = duration == TimeSpan.MaxValue ? "1" : "0"; + string durationStr = duration == TimeSpan.MaxValue ? "-1" : duration.TotalSeconds.ToString(); + string remoteAddrStr = remoteAddr.ToString(); + string issuerRemoteAddrStr = issuerRemoteAddr.ToString(); + + string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); + string sig = string.Format( + BANS_CREATE_SIG, + now, userId, remoteAddrStr, + issuerUserId, issuerRemoteAddrStr, + durationStr, isPerma, reason + ); + + HttpRequestMessage request = new(HttpMethod.Post, string.Format(BANS_CREATE_URL, BaseURL)) { + Headers = { + { "X-SharpChat-Signature", CreateStringSignature(sig) }, + }, + Content = new FormUrlEncodedContent(new Dictionary<string, string> { + { "t", now }, + { "ui", userId }, + { "ua", remoteAddrStr }, + { "mi", issuerUserId }, + { "ma", issuerRemoteAddrStr }, + { "d", durationStr }, + { "p", isPerma }, + { "r", reason }, + }), + }; + + using HttpResponseMessage response = await httpClient.SendAsync(request); + + response.EnsureSuccessStatusCode(); + } + + private const string BANS_REVOKE_URL = "{0}/bans/revoke?t={1}&s={2}&x={3}"; + private const string BANS_REVOKE_SIG = "revoke#{0}#{1}#{2}"; + + public async Task<bool> BanRevokeAsync(BanInfo info) { + string type; + string target; + + if(info is UserBanInfo ubi) { + if(info.Kind != BanKind.User) + throw new ArgumentException("info argument is an instance of UserBanInfo but Kind was not set to BanKind.User", nameof(info)); + + type = "user"; + target = ubi.UserId; + } else if(info is IPAddressBanInfo iabi) { + if(info.Kind != BanKind.IPAddress) + throw new ArgumentException("info argument is an instance of IPAddressBanInfo but Kind was not set to BanKind.IPAddress", nameof(info)); + + type = "addr"; + target = iabi.Address.ToString(); + } else throw new ArgumentException("info argument is set to unsupported implementation", nameof(info)); + + 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 request = new(HttpMethod.Delete, url) { + Headers = { + { "X-SharpChat-Signature", CreateStringSignature(sig) }, + }, + }; + + using HttpResponseMessage response = await httpClient.SendAsync(request); + if(response.StatusCode == HttpStatusCode.NotFound) + return false; + + response.EnsureSuccessStatusCode(); + + return response.StatusCode == HttpStatusCode.NoContent; + } + + private const string BANS_CHECK_URL = "{0}/bans/check?u={1}&a={2}&x={3}&n={4}"; + private const string BANS_CHECK_SIG = "check#{0}#{1}#{2}#{3}"; + + public async Task<BanInfo?> BanGetAsync(string? userIdOrName = null, IPAddress? remoteAddr = null) { + userIdOrName ??= "0"; + remoteAddr ??= IPAddress.None; + + string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); + bool usingUserName = string.IsNullOrEmpty(userIdOrName) || userIdOrName.Any(c => c is < '0' or > '9'); + string remoteAddrStr = remoteAddr.ToString(); + string usingUserNameStr = usingUserName ? "1" : "0"; + string url = string.Format(BANS_CHECK_URL, BaseURL, Uri.EscapeDataString(userIdOrName), Uri.EscapeDataString(remoteAddrStr), Uri.EscapeDataString(now), Uri.EscapeDataString(usingUserNameStr)); + string sig = string.Format(BANS_CHECK_SIG, now, userIdOrName, remoteAddrStr, usingUserNameStr); + + HttpRequestMessage request = new(HttpMethod.Get, url) { + Headers = { + { "X-SharpChat-Signature", CreateStringSignature(sig) }, + }, + }; + + using HttpResponseMessage response = await httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + + using Stream stream = await response.Content.ReadAsStreamAsync(); + FlashiiRawBanInfo? rawBanInfo = await JsonSerializer.DeserializeAsync<FlashiiRawBanInfo>(stream); + if(rawBanInfo?.IsBanned != true || rawBanInfo.HasExpired) + return null; + + return rawBanInfo.RemoteAddress is null or "::" + ? new FlashiiUserBanInfo(rawBanInfo) + : new FlashiiIPAddressBanInfo(rawBanInfo); + } + + private const string BANS_LIST_URL = "{0}/bans/list?x={1}"; + private const string BANS_LIST_SIG = "list#{0}"; + + public async Task<BanInfo[]> BanGetListAsync() { + 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 request = new(HttpMethod.Get, url) { + Headers = { + { "X-SharpChat-Signature", CreateStringSignature(sig) }, + }, + }; + + using HttpResponseMessage response = await httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + + using Stream stream = await response.Content.ReadAsStreamAsync(); + FlashiiRawBanInfo[]? list = await JsonSerializer.DeserializeAsync<FlashiiRawBanInfo[]>(stream); + if(list is null || list.Length < 1) + return []; + + return [.. list.Where(b => b?.IsBanned == true && !b.HasExpired).Select(b => { + return (BanInfo)(b.RemoteAddress is null or "::" + ? new FlashiiUserBanInfo(b) : new FlashiiIPAddressBanInfo(b)); + })]; + } + } +} diff --git a/SharpChat.Misuzu/FlashiiIPAddressBanInfo.cs b/SharpChat.Misuzu/FlashiiIPAddressBanInfo.cs new file mode 100644 index 0000000..7bd6fc5 --- /dev/null +++ b/SharpChat.Misuzu/FlashiiIPAddressBanInfo.cs @@ -0,0 +1,9 @@ +using SharpChat.Bans; +using System.Net; + +namespace SharpChat.Flashii { + public class FlashiiIPAddressBanInfo(FlashiiRawBanInfo rawBanInfo) : FlashiiBanInfo(BanKind.IPAddress, rawBanInfo), IPAddressBanInfo { + public IPAddress Address { get; } = IPAddress.TryParse(rawBanInfo.RemoteAddress, out IPAddress? addr) && addr is not null ? addr : IPAddress.IPv6None; + public override string ToString() => Address.ToString(); + } +} diff --git a/SharpChat/Misuzu/MisuzuBanInfo.cs b/SharpChat.Misuzu/FlashiiRawBanInfo.cs similarity index 88% rename from SharpChat/Misuzu/MisuzuBanInfo.cs rename to SharpChat.Misuzu/FlashiiRawBanInfo.cs index 7012b87..3263fde 100644 --- a/SharpChat/Misuzu/MisuzuBanInfo.cs +++ b/SharpChat.Misuzu/FlashiiRawBanInfo.cs @@ -1,13 +1,20 @@ using System.Text.Json.Serialization; -namespace SharpChat.Misuzu { - public class MisuzuBanInfo { +namespace SharpChat.Flashii { + public class FlashiiRawBanInfo { [JsonPropertyName("is_ban")] public bool IsBanned { get; set; } [JsonPropertyName("user_id")] public string? UserId { get; set; } + [JsonPropertyName("user_name")] + public string? UserName { get; set; } + + [JsonPropertyName("user_colour")] + public int UserColourRaw { get; set; } + public ColourInheritable UserColour => ColourInheritable.FromMisuzu(UserColourRaw); + [JsonPropertyName("ip_addr")] public string? RemoteAddress { get; set; } @@ -17,15 +24,6 @@ namespace SharpChat.Misuzu { [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 ColourInheritable UserColour => ColourInheritable.FromMisuzu(UserColourRaw); } } diff --git a/SharpChat.Misuzu/FlashiiUserBanInfo.cs b/SharpChat.Misuzu/FlashiiUserBanInfo.cs new file mode 100644 index 0000000..6124547 --- /dev/null +++ b/SharpChat.Misuzu/FlashiiUserBanInfo.cs @@ -0,0 +1,10 @@ +using SharpChat.Bans; + +namespace SharpChat.Flashii { + public class FlashiiUserBanInfo(FlashiiRawBanInfo rawBanInfo) : FlashiiBanInfo(BanKind.User, rawBanInfo), UserBanInfo { + public string UserId { get; } = rawBanInfo.UserId ?? string.Empty; + public string UserName { get; } = rawBanInfo.UserName ?? $"({rawBanInfo.UserId ?? string.Empty})"; + public ColourInheritable UserColour { get; } = ColourInheritable.FromMisuzu(rawBanInfo.UserColourRaw); + public override string ToString() => UserName; + } +} diff --git a/SharpChat.Misuzu/SharpChat.Flashii.csproj b/SharpChat.Misuzu/SharpChat.Flashii.csproj new file mode 100644 index 0000000..a4d46fe --- /dev/null +++ b/SharpChat.Misuzu/SharpChat.Flashii.csproj @@ -0,0 +1,13 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net9.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\SharpChatCommon\SharpChatCommon.csproj" /> + </ItemGroup> + +</Project> diff --git a/SharpChat.sln b/SharpChat.sln index 15cc935..13814a2 100644 --- a/SharpChat.sln +++ b/SharpChat.sln @@ -17,6 +17,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpChatCommon", "SharpChatCommon\SharpChatCommon.csproj", "{C8B619A7-7815-426D-B459-20EE26F7460E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpChat.Flashii", "SharpChat.Misuzu\SharpChat.Flashii.csproj", "{A9B0B652-C20F-4C62-A96A-EF7ACD2079E9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -31,6 +33,10 @@ Global {C8B619A7-7815-426D-B459-20EE26F7460E}.Debug|Any CPU.Build.0 = Debug|Any CPU {C8B619A7-7815-426D-B459-20EE26F7460E}.Release|Any CPU.ActiveCfg = Release|Any CPU {C8B619A7-7815-426D-B459-20EE26F7460E}.Release|Any CPU.Build.0 = Release|Any CPU + {A9B0B652-C20F-4C62-A96A-EF7ACD2079E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A9B0B652-C20F-4C62-A96A-EF7ACD2079E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A9B0B652-C20F-4C62-A96A-EF7ACD2079E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A9B0B652-C20F-4C62-A96A-EF7ACD2079E9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/SharpChat/C2SPacketHandlers/AuthC2SPacketHandler.cs b/SharpChat/C2SPacketHandlers/AuthC2SPacketHandler.cs index 6bef4b1..064928f 100644 --- a/SharpChat/C2SPacketHandlers/AuthC2SPacketHandler.cs +++ b/SharpChat/C2SPacketHandlers/AuthC2SPacketHandler.cs @@ -1,15 +1,16 @@ -using SharpChat.Config; -using SharpChat.Misuzu; +using SharpChat.Auth; +using SharpChat.Bans; +using SharpChat.Configuration; using SharpChat.S2CPackets; namespace SharpChat.C2SPacketHandlers { public class AuthC2SPacketHandler( - MisuzuClient msz, + AuthClient authClient, + BansClient bansClient, Channel defaultChannel, CachedValue<int> maxMsgLength, CachedValue<int> maxConns ) : C2SPacketHandler { - private readonly MisuzuClient Misuzu = msz ?? throw new ArgumentNullException(nameof(msz)); private readonly Channel DefaultChannel = defaultChannel ?? throw new ArgumentNullException(nameof(defaultChannel)); private readonly CachedValue<int> MaxMessageLength = maxMsgLength ?? throw new ArgumentNullException(nameof(maxMsgLength)); private readonly CachedValue<int> MaxConnections = maxConns ?? throw new ArgumentNullException(nameof(maxConns)); @@ -22,14 +23,9 @@ namespace SharpChat.C2SPacketHandlers { string[] args = ctx.SplitText(3); string? authMethod = args.ElementAtOrDefault(1); - if(string.IsNullOrWhiteSpace(authMethod)) { - ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.AuthInvalid)); - ctx.Connection.Dispose(); - return; - } - string? authToken = args.ElementAtOrDefault(2); - if(string.IsNullOrWhiteSpace(authToken)) { + + if(string.IsNullOrWhiteSpace(authMethod) || string.IsNullOrWhiteSpace(authToken)) { ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.AuthInvalid)); ctx.Connection.Dispose(); return; @@ -42,93 +38,75 @@ namespace SharpChat.C2SPacketHandlers { } Task.Run(async () => { - MisuzuAuthInfo? fai; - string ipAddr = ctx.Connection.RemoteAddress.ToString(); - try { - fai = await Misuzu.AuthVerifyAsync(authMethod, authToken, ipAddr); - } catch(Exception ex) { - Logger.Write($"<{ctx.Connection.Id}> Failed to authenticate: {ex}"); - ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.AuthInvalid)); - ctx.Connection.Dispose(); -#if DEBUG - throw; -#else - return; -#endif - } + AuthResult authResult = await authClient.AuthVerifyAsync( + ctx.Connection.RemoteAddress, + authMethod, + authToken + ); - if(fai?.Success != true) { - Logger.Debug($"<{ctx.Connection.Id}> Auth fail: {fai?.Reason ?? "unknown"}"); - ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.AuthInvalid)); - ctx.Connection.Dispose(); - return; - } - - MisuzuBanInfo? fbi; - try { - fbi = await Misuzu.CheckBanAsync(fai.UserId.ToString(), ipAddr); - } catch(Exception ex) { - Logger.Write($"<{ctx.Connection.Id}> Failed auth ban check: {ex}"); - ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.AuthInvalid)); - ctx.Connection.Dispose(); -#if DEBUG - throw; -#else - return; -#endif - } - - if(fbi?.IsBanned == true && !fbi.HasExpired) { - Logger.Write($"<{ctx.Connection.Id}> User is banned."); - ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.Banned, fbi.IsPermanent ? DateTimeOffset.MaxValue : fbi.ExpiresAt)); - ctx.Connection.Dispose(); - return; - } - - await ctx.Chat.ContextAccess.WaitAsync(); - try { - User? user = ctx.Chat.Users.FirstOrDefault(u => u.UserId == fai.UserId); - - if(user == null) - user = new User( - fai.UserId, - fai.UserName ?? $"({fai.UserId})", - fai.Colour, - fai.Rank, - fai.Permissions - ); - else - ctx.Chat.UpdateUser( - user, - userName: fai.UserName ?? $"({fai.UserId})", - colour: fai.Colour, - rank: fai.Rank, - perms: fai.Permissions - ); - - // Enforce a maximum amount of connections per user - if(ctx.Chat.Connections.Count(conn => conn.User == user) >= MaxConnections) { - ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.MaxSessions)); + BanInfo? banInfo = await bansClient.BanGetAsync(authResult.UserId, ctx.Connection.RemoteAddress); + if(banInfo is not null) { + Logger.Write($"<{ctx.Connection.Id}> User is banned."); + ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.Banned, banInfo.IsPermanent ? DateTimeOffset.MaxValue : banInfo.ExpiresAt)); ctx.Connection.Dispose(); return; } - ctx.Connection.BumpPing(); - ctx.Connection.User = user; - ctx.Connection.Send(new CommandResponseS2CPacket(0, LCR.WELCOME, false, $"Welcome to Flashii Chat, {user.UserName}!")); + await ctx.Chat.ContextAccess.WaitAsync(); + try { + User? user = ctx.Chat.Users.FirstOrDefault(u => u.UserId == authResult.UserId); - if(File.Exists("welcome.txt")) { - IEnumerable<string> lines = File.ReadAllLines("welcome.txt").Where(x => !string.IsNullOrWhiteSpace(x)); - string? line = lines.ElementAtOrDefault(RNG.Next(lines.Count())); + if(user == null) + user = new User( + authResult.UserId, + authResult.UserName ?? $"({authResult.UserId})", + authResult.UserColour, + authResult.UserRank, + authResult.UserPermissions + ); + else + ctx.Chat.UpdateUser( + user, + userName: authResult.UserName ?? $"({authResult.UserId})", + colour: authResult.UserColour, + rank: authResult.UserRank, + perms: authResult.UserPermissions + ); - if(!string.IsNullOrWhiteSpace(line)) - ctx.Connection.Send(new CommandResponseS2CPacket(0, LCR.WELCOME, false, line)); + // Enforce a maximum amount of connections per user + if(ctx.Chat.Connections.Count(conn => conn.User == user) >= MaxConnections) { + ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.MaxSessions)); + ctx.Connection.Dispose(); + return; + } + + ctx.Connection.BumpPing(); + ctx.Connection.User = user; + ctx.Connection.Send(new CommandResponseS2CPacket(0, LCR.WELCOME, false, $"Welcome to Flashii Chat, {user.UserName}!")); + + if(File.Exists("welcome.txt")) { + IEnumerable<string> lines = File.ReadAllLines("welcome.txt").Where(x => !string.IsNullOrWhiteSpace(x)); + string? line = lines.ElementAtOrDefault(RNG.Next(lines.Count())); + + if(!string.IsNullOrWhiteSpace(line)) + ctx.Connection.Send(new CommandResponseS2CPacket(0, LCR.WELCOME, false, line)); + } + + ctx.Chat.HandleJoin(user, DefaultChannel, ctx.Connection, MaxMessageLength); + } finally { + ctx.Chat.ContextAccess.Release(); } - - ctx.Chat.HandleJoin(user, DefaultChannel, ctx.Connection, MaxMessageLength); - } finally { - ctx.Chat.ContextAccess.Release(); + } catch(AuthFailedException ex) { + Logger.Write($"<{ctx.Connection.Id}> Failed to authenticate: {ex}"); + ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.AuthInvalid)); + ctx.Connection.Dispose(); + throw; + } catch(Exception ex) { + Logger.Write($"<{ctx.Connection.Id}> Failed to authenticate: {ex}"); + ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.Exception)); + ctx.Connection.Dispose(); + throw; } }).Wait(); } diff --git a/SharpChat/C2SPacketHandlers/PingC2SPacketHandler.cs b/SharpChat/C2SPacketHandlers/PingC2SPacketHandler.cs index ecec108..2046fcd 100644 --- a/SharpChat/C2SPacketHandlers/PingC2SPacketHandler.cs +++ b/SharpChat/C2SPacketHandlers/PingC2SPacketHandler.cs @@ -1,10 +1,9 @@ -using SharpChat.Misuzu; +using SharpChat.Auth; using SharpChat.S2CPackets; +using System.Net; namespace SharpChat.C2SPacketHandlers { - public class PingC2SPacketHandler(MisuzuClient msz) : C2SPacketHandler { - private readonly MisuzuClient Misuzu = msz ?? throw new ArgumentNullException(nameof(msz)); - + public class PingC2SPacketHandler(AuthClient authClient) : C2SPacketHandler { private readonly TimeSpan BumpInterval = TimeSpan.FromMinutes(1); private DateTimeOffset LastBump = DateTimeOffset.MinValue; @@ -24,13 +23,13 @@ namespace SharpChat.C2SPacketHandlers { ctx.Chat.ContextAccess.Wait(); try { if(LastBump < DateTimeOffset.UtcNow - BumpInterval) { - (string, string)[] bumpList = [.. ctx.Chat.Users + (IPAddress, string)[] bumpList = [.. ctx.Chat.Users .Where(u => u.Status == UserStatus.Online && ctx.Chat.Connections.Any(c => c.User == u)) - .Select(u => (u.UserId.ToString(), ctx.Chat.GetRemoteAddresses(u).FirstOrDefault()?.ToString() ?? string.Empty))]; + .Select(u => (ctx.Chat.GetRemoteAddresses(u).FirstOrDefault() ?? IPAddress.None, u.UserId))]; if(bumpList.Length > 0) Task.Run(async () => { - await Misuzu.BumpUsersOnlineAsync(bumpList); + await authClient.AuthBumpUsersOnlineAsync(bumpList); }).Wait(); LastBump = DateTimeOffset.UtcNow; diff --git a/SharpChat/C2SPacketHandlers/SendMessageC2SPacketHandler.cs b/SharpChat/C2SPacketHandlers/SendMessageC2SPacketHandler.cs index 3330986..eb01b43 100644 --- a/SharpChat/C2SPacketHandlers/SendMessageC2SPacketHandler.cs +++ b/SharpChat/C2SPacketHandlers/SendMessageC2SPacketHandler.cs @@ -1,4 +1,4 @@ -using SharpChat.Config; +using SharpChat.Configuration; using SharpChat.Events; using SharpChat.Snowflake; using System.Globalization; @@ -35,7 +35,7 @@ namespace SharpChat.C2SPacketHandlers { return; // Extra validation step, not necessary at all but enforces proper formatting in SCv1. - if(!long.TryParse(args[1], out long mUserId) || user.UserId != mUserId) + if(!long.TryParse(args[1], out long mUserId) || user.UserId != mUserId.ToString()) return; ctx.Chat.ContextAccess.Wait(); diff --git a/SharpChat/Channel.cs b/SharpChat/Channel.cs index e1a2c1e..c23edfe 100644 --- a/SharpChat/Channel.cs +++ b/SharpChat/Channel.cs @@ -1,16 +1,16 @@ -namespace SharpChat { +namespace SharpChat { public class Channel( string name, string password = "", bool isTemporary = false, int rank = 0, - long ownerId = 0 + string ownerId = "" ) { public string Name { get; } = name ?? throw new ArgumentNullException(nameof(name)); public string Password { get; set; } = password ?? string.Empty; public bool IsTemporary { get; set; } = isTemporary; public int Rank { get; set; } = rank; - public long OwnerId { get; set; } = ownerId; + public string OwnerId { get; set; } = ownerId; public bool HasPassword => !string.IsNullOrWhiteSpace(Password); @@ -20,7 +20,7 @@ } public bool IsOwner(User user) { - return OwnerId > 0 + return string.IsNullOrEmpty(OwnerId) && user != null && OwnerId == user.UserId; } diff --git a/SharpChat/ClientCommands/BanListClientCommand.cs b/SharpChat/ClientCommands/BanListClientCommand.cs index 61a4382..477e74d 100644 --- a/SharpChat/ClientCommands/BanListClientCommand.cs +++ b/SharpChat/ClientCommands/BanListClientCommand.cs @@ -1,10 +1,8 @@ -using SharpChat.Misuzu; +using SharpChat.Bans; using SharpChat.S2CPackets; namespace SharpChat.ClientCommands { - public class BanListClientCommand(MisuzuClient msz) : ClientCommand { - private readonly MisuzuClient Misuzu = msz ?? throw new ArgumentNullException(nameof(msz)); - + public class BanListClientCommand(BansClient bansClient) : ClientCommand { public bool IsMatch(ClientCommandContext ctx) { return ctx.NameEquals("bans") || ctx.NameEquals("banned"); @@ -19,18 +17,15 @@ namespace SharpChat.ClientCommands { } Task.Run(async () => { - MisuzuBanInfo[]? mbis = await Misuzu.GetBanListAsync(); - if(mbis is null) - ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.GENERIC_ERROR, true)); - else + try { + BanInfo[] banInfos = await bansClient.BanGetListAsync(); ctx.Chat.SendTo(ctx.User, new BanListS2CPacket( msgId, - mbis.Where(mbi => mbi.IsBanned && !mbi.HasExpired) - .Select(mbi => new BanListS2CPacket.Entry( - BanListS2CPacket.Type.UserName, // Misuzu currently only does username bans so we can just do this - mbi.UserName ?? $"({mbi.UserId})" - )) + banInfos.Select(bi => new BanListS2CPacket.Entry(bi.Kind, bi.ToString())) )); + } catch(Exception) { + ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.GENERIC_ERROR, true)); + } }).Wait(); } } diff --git a/SharpChat/ClientCommands/KickBanClientCommand.cs b/SharpChat/ClientCommands/KickBanClientCommand.cs index 7c4c7e8..ac50925 100644 --- a/SharpChat/ClientCommands/KickBanClientCommand.cs +++ b/SharpChat/ClientCommands/KickBanClientCommand.cs @@ -1,10 +1,9 @@ -using SharpChat.Misuzu; +using SharpChat.Bans; using SharpChat.S2CPackets; +using System.Net; namespace SharpChat.ClientCommands { - public class KickBanClientCommand(MisuzuClient msz) : ClientCommand { - private readonly MisuzuClient Misuzu = msz ?? throw new ArgumentNullException(nameof(msz)); - + public class KickBanClientCommand(BansClient bansClient) : ClientCommand { public bool IsMatch(ClientCommandContext ctx) { return ctx.NameEquals("kick") || ctx.NameEquals("ban"); @@ -53,25 +52,20 @@ namespace SharpChat.ClientCommands { string banReason = string.Join(' ', ctx.Args.Skip(banReasonIndex)); Task.Run(async () => { - string userId = banUser.UserId.ToString(); - string userIp = ctx.Chat.GetRemoteAddresses(banUser).FirstOrDefault()?.ToString() ?? string.Empty; - - // obviously it makes no sense to only check for one ip address but that's current misuzu limitations - MisuzuBanInfo? fbi = await Misuzu.CheckBanAsync(userId, userIp); - if(fbi is null) { - ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.GENERIC_ERROR, true)); - return; - } - - if(fbi.IsBanned && !fbi.HasExpired) { + BanInfo? banInfo = await bansClient.BanGetAsync(banUser.UserId); + if(banInfo is not null) { ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.KICK_NOT_ALLOWED, true, banUser.LegacyName)); return; } - await Misuzu.CreateBanAsync( - userId, userIp, - ctx.User.UserId.ToString(), ctx.Connection.RemoteAddress.ToString(), - duration, banReason + await bansClient.BanCreateAsync( + BanKind.User, + duration, + ctx.Chat.GetRemoteAddresses(banUser).FirstOrDefault() ?? IPAddress.None, + banUser.UserId, + banReason, + ctx.Connection.RemoteAddress, + ctx.User.UserId ); ctx.Chat.BanUser(banUser, duration); diff --git a/SharpChat/ClientCommands/NickClientCommand.cs b/SharpChat/ClientCommands/NickClientCommand.cs index 7217f98..1b8a279 100644 --- a/SharpChat/ClientCommands/NickClientCommand.cs +++ b/SharpChat/ClientCommands/NickClientCommand.cs @@ -1,4 +1,4 @@ -using SharpChat.S2CPackets; +using SharpChat.S2CPackets; using System.Globalization; using System.Text; @@ -24,7 +24,7 @@ namespace SharpChat.ClientCommands { int offset = 0; if(setOthersNick && long.TryParse(ctx.Args.FirstOrDefault(), out long targetUserId) && targetUserId > 0) { - targetUser = ctx.Chat.Users.FirstOrDefault(u => u.UserId == targetUserId); + targetUser = ctx.Chat.Users.FirstOrDefault(u => u.UserId == targetUserId.ToString()); ++offset; } diff --git a/SharpChat/ClientCommands/PardonAddressClientCommand.cs b/SharpChat/ClientCommands/PardonAddressClientCommand.cs index 97b8425..f1769af 100644 --- a/SharpChat/ClientCommands/PardonAddressClientCommand.cs +++ b/SharpChat/ClientCommands/PardonAddressClientCommand.cs @@ -1,11 +1,9 @@ -using SharpChat.Misuzu; +using SharpChat.Bans; using SharpChat.S2CPackets; using System.Net; namespace SharpChat.ClientCommands { - public class PardonAddressClientCommand(MisuzuClient msz) : ClientCommand { - private readonly MisuzuClient Misuzu = msz ?? throw new ArgumentNullException(nameof(msz)); - + public class PardonAddressClientCommand(BansClient bansClient) : ClientCommand { public bool IsMatch(ClientCommandContext ctx) { return ctx.NameEquals("pardonip") || ctx.NameEquals("unbanip"); @@ -28,19 +26,13 @@ namespace SharpChat.ClientCommands { unbanAddrTarget = unbanAddr.ToString(); Task.Run(async () => { - MisuzuBanInfo? banInfo = await Misuzu.CheckBanAsync(ipAddr: unbanAddrTarget); - if(banInfo is null) { - ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.GENERIC_ERROR, true)); - return; - } - - if(!banInfo.IsBanned || banInfo.HasExpired) { + BanInfo? banInfo = await bansClient.BanGetAsync(remoteAddr: unbanAddr); + if(banInfo?.Kind != BanKind.IPAddress) { ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanAddrTarget)); return; } - bool wasBanned = await Misuzu.RevokeBanAsync(banInfo, MisuzuClient.BanRevokeKind.RemoteAddress); - if(wasBanned) + if(await bansClient.BanRevokeAsync(banInfo)) ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_UNBANNED, false, unbanAddrTarget)); else ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanAddrTarget)); diff --git a/SharpChat/ClientCommands/PardonUserClientCommand.cs b/SharpChat/ClientCommands/PardonUserClientCommand.cs index 593505b..48a0e55 100644 --- a/SharpChat/ClientCommands/PardonUserClientCommand.cs +++ b/SharpChat/ClientCommands/PardonUserClientCommand.cs @@ -1,10 +1,9 @@ -using SharpChat.Misuzu; +using SharpChat.Bans; using SharpChat.S2CPackets; +using System.Text.Json; namespace SharpChat.ClientCommands { - public class PardonUserClientCommand(MisuzuClient msz) : ClientCommand { - private readonly MisuzuClient Misuzu = msz ?? throw new ArgumentNullException(nameof(msz)); - + public class PardonUserClientCommand(BansClient bansClient) : ClientCommand { public bool IsMatch(ClientCommandContext ctx) { return ctx.NameEquals("pardon") || ctx.NameEquals("unban"); @@ -18,39 +17,32 @@ namespace SharpChat.ClientCommands { return; } - bool unbanUserTargetIsName = true; string? unbanUserTarget = ctx.Args.FirstOrDefault(); if(string.IsNullOrWhiteSpace(unbanUserTarget)) { ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR)); return; } + string unbanUserDisplay = unbanUserTarget; User? unbanUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(unbanUserTarget)); - if(unbanUser == null && long.TryParse(unbanUserTarget, out long unbanUserId)) { - unbanUserTargetIsName = false; - unbanUser = ctx.Chat.Users.FirstOrDefault(u => u.UserId == unbanUserId); + if(unbanUser == null && long.TryParse(unbanUserTarget, out long unbanUserId)) + unbanUser = ctx.Chat.Users.FirstOrDefault(u => u.UserId == unbanUserId.ToString()); + if(unbanUser != null) { + unbanUserTarget = unbanUser.UserId; + unbanUserDisplay = unbanUser.UserName; } - if(unbanUser != null) - unbanUserTarget = unbanUser.UserId.ToString(); - Task.Run(async () => { - MisuzuBanInfo? banInfo = await Misuzu.CheckBanAsync(unbanUserTarget, userIdIsName: unbanUserTargetIsName); - if(banInfo is null) { - ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.GENERIC_ERROR, true)); + BanInfo? banInfo = await bansClient.BanGetAsync(unbanUserTarget); + if(banInfo?.Kind != BanKind.User) { + ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanUserDisplay)); return; } - if(!banInfo.IsBanned || banInfo.HasExpired) { - ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanUserTarget)); - return; - } - - bool wasBanned = await Misuzu.RevokeBanAsync(banInfo, MisuzuClient.BanRevokeKind.UserId); - if(wasBanned) - ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_UNBANNED, false, unbanUserTarget)); + if(await bansClient.BanRevokeAsync(banInfo)) + ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_UNBANNED, false, unbanUserDisplay)); else - ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanUserTarget)); + ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanUserDisplay)); }).Wait(); } } diff --git a/SharpChat/ClientCommands/ShutdownRestartClientCommand.cs b/SharpChat/ClientCommands/ShutdownRestartClientCommand.cs index 30b6e60..6d1001a 100644 --- a/SharpChat/ClientCommands/ShutdownRestartClientCommand.cs +++ b/SharpChat/ClientCommands/ShutdownRestartClientCommand.cs @@ -1,4 +1,4 @@ -using SharpChat.S2CPackets; +using SharpChat.S2CPackets; namespace SharpChat.ClientCommands { public class ShutdownRestartClientCommand(ManualResetEvent waitHandle, Func<bool> shutdownCheck) : ClientCommand { @@ -11,7 +11,7 @@ namespace SharpChat.ClientCommands { } public void Dispatch(ClientCommandContext ctx) { - if(ctx.User.UserId != 1) { + if(!ctx.User.UserId.Equals("1")) { long msgId = ctx.Chat.RandomSnowflake.Next(); ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}")); return; diff --git a/SharpChat/Context.cs b/SharpChat/Context.cs index e66332b..58c21a8 100644 --- a/SharpChat/Context.cs +++ b/SharpChat/Context.cs @@ -1,4 +1,4 @@ -using SharpChat.Events; +using SharpChat.Events; using SharpChat.EventStorage; using SharpChat.S2CPackets; using SharpChat.Snowflake; @@ -6,7 +6,7 @@ using System.Net; namespace SharpChat { public class Context { - public record ChannelUserAssoc(long UserId, string ChannelName); + public record ChannelUserAssoc(string UserId, string ChannelName); public readonly SemaphoreSlim ContextAccess = new(1, 1); @@ -18,8 +18,8 @@ namespace SharpChat { public HashSet<User> Users { get; } = []; public EventStorage.EventStorage Events { get; } public HashSet<ChannelUserAssoc> ChannelUsers { get; } = []; - public Dictionary<long, RateLimiter> UserRateLimiters { get; } = []; - public Dictionary<long, Channel> UserLastChannel { get; } = []; + public Dictionary<string, RateLimiter> UserRateLimiters { get; } = []; + public Dictionary<string, Channel> UserLastChannel { get; } = []; public Context(EventStorage.EventStorage evtStore) { Events = evtStore ?? throw new ArgumentNullException(nameof(evtStore)); @@ -38,7 +38,7 @@ namespace SharpChat { if(!mce.ChannelName.StartsWith('@')) return; - IEnumerable<long> uids = mce.ChannelName[1..].Split('-', 3).Select(u => long.TryParse(u, out long up) ? up : -1); + IEnumerable<string> uids = mce.ChannelName[1..].Split('-', 3).Select(u => (long.TryParse(u, out long up) ? up : -1).ToString()); if(uids.Count() != 2) return; @@ -120,12 +120,12 @@ namespace SharpChat { return [.. Channels.Where(c => names.Any(n => c.NameEquals(n)))]; } - public long[] GetChannelUserIds(Channel channel) { + public string[] GetChannelUserIds(Channel channel) { return [.. ChannelUsers.Where(cu => channel.NameEquals(cu.ChannelName)).Select(cu => cu.UserId)]; } public User[] GetChannelUsers(Channel channel) { - long[] ids = GetChannelUserIds(channel); + string[] ids = GetChannelUserIds(channel); return [.. Users.Where(u => ids.Contains(u.UserId))]; } diff --git a/SharpChat/EventStorage/EventStorage.cs b/SharpChat/EventStorage/EventStorage.cs index 7a9e819..ae0f344 100644 --- a/SharpChat/EventStorage/EventStorage.cs +++ b/SharpChat/EventStorage/EventStorage.cs @@ -1,10 +1,10 @@ -namespace SharpChat.EventStorage { +namespace SharpChat.EventStorage { public interface EventStorage { void AddEvent( long id, string type, string channelName, - long senderId, + string senderId, string senderName, ColourInheritable senderColour, int senderRank, diff --git a/SharpChat/EventStorage/MariaDBEventStorage.cs b/SharpChat/EventStorage/MariaDBEventStorage.cs index 22b4e7b..900b9be 100644 --- a/SharpChat/EventStorage/MariaDBEventStorage.cs +++ b/SharpChat/EventStorage/MariaDBEventStorage.cs @@ -1,4 +1,4 @@ -using MySqlConnector; +using MySqlConnector; using System.Text; using System.Text.Json; @@ -10,7 +10,7 @@ namespace SharpChat.EventStorage { long id, string type, string channelName, - long senderId, + string senderId, string senderName, ColourInheritable senderColour, int senderRank, @@ -19,8 +19,6 @@ namespace SharpChat.EventStorage { object? data = null, StoredEventFlags flags = StoredEventFlags.None ) { - ArgumentNullException.ThrowIfNull(type); - RunCommand( "INSERT INTO `sqc_events` (`event_id`, `event_created`, `event_type`, `event_target`, `event_flags`, `event_data`" + ", `event_sender`, `event_sender_name`, `event_sender_colour`, `event_sender_rank`, `event_sender_nick`, `event_sender_perms`)" @@ -31,7 +29,7 @@ namespace SharpChat.EventStorage { new MySqlParameter("target", string.IsNullOrWhiteSpace(channelName) ? null : channelName), new MySqlParameter("flags", (byte)flags), new MySqlParameter("data", data == null ? "{}" : JsonSerializer.SerializeToUtf8Bytes(data)), - new MySqlParameter("sender", senderId < 1 ? null : senderId), + new MySqlParameter("sender", long.TryParse(senderId, out long senderId64) && senderId64 > 0 ? senderId64 : null), new MySqlParameter("sender_name", senderName), new MySqlParameter("sender_colour", senderColour.ToMisuzu()), new MySqlParameter("sender_rank", senderRank), @@ -72,7 +70,7 @@ namespace SharpChat.EventStorage { reader.GetInt64("event_id"), Encoding.ASCII.GetString((byte[])reader["event_type"]), reader.IsDBNull(reader.GetOrdinal("event_sender")) ? null : new User( - reader.GetInt64("event_sender"), + reader.GetInt64("event_sender").ToString(), reader.IsDBNull(reader.GetOrdinal("event_sender_name")) ? string.Empty : reader.GetString("event_sender_name"), ColourInheritable.FromMisuzu(reader.GetInt32("event_sender_colour")), reader.GetInt32("event_sender_rank"), diff --git a/SharpChat/EventStorage/MariaDBEventStorage_Database.cs b/SharpChat/EventStorage/MariaDBEventStorage_Database.cs index 88e4b7e..1035626 100644 --- a/SharpChat/EventStorage/MariaDBEventStorage_Database.cs +++ b/SharpChat/EventStorage/MariaDBEventStorage_Database.cs @@ -1,9 +1,9 @@ -using MySqlConnector; -using SharpChat.Config; +using MySqlConnector; +using SharpChat.Configuration; namespace SharpChat.EventStorage { public partial class MariaDBEventStorage { - public static string BuildConnString(Config.Config config) { + public static string BuildConnString(Configuration.Config config) { return BuildConnString( config.ReadValue("host", "localhost"), config.ReadValue("user", string.Empty), diff --git a/SharpChat/EventStorage/VirtualEventStorage.cs b/SharpChat/EventStorage/VirtualEventStorage.cs index b96c8ea..72ccc21 100644 --- a/SharpChat/EventStorage/VirtualEventStorage.cs +++ b/SharpChat/EventStorage/VirtualEventStorage.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using System.Text.Json; namespace SharpChat.EventStorage { public class VirtualEventStorage : EventStorage { @@ -8,7 +8,7 @@ namespace SharpChat.EventStorage { long id, string type, string channelName, - long senderId, + string senderId, string senderName, ColourInheritable senderColour, int senderRank, @@ -17,18 +17,28 @@ namespace SharpChat.EventStorage { object? data = null, StoredEventFlags flags = StoredEventFlags.None ) { - ArgumentNullException.ThrowIfNull(type); - - // VES is meant as an emergency fallback but this is something else - JsonDocument hack = JsonDocument.Parse(data == null ? "{}" : JsonSerializer.Serialize(data)); - Events.Add(id, new(id, type, senderId < 1 ? null : new User( - senderId, - senderName, - senderColour, - senderRank, - senderPerms, - senderNick - ), DateTimeOffset.Now, null, channelName, hack, flags)); + Events.Add( + id, + new( + id, + type, + long.TryParse(senderId, out long senderId64) && senderId64 > 0 + ? new User( + senderId, + senderName, + senderColour, + senderRank, + senderPerms, + senderNick + ) + : null, + DateTimeOffset.Now, + null, + channelName, + JsonDocument.Parse(data == null ? "{}" : JsonSerializer.Serialize(data)), + flags + ) + ); } public StoredEventInfo? GetEvent(long seqId) { diff --git a/SharpChat/Events/MessageCreateEvent.cs b/SharpChat/Events/MessageCreateEvent.cs index cf28c52..22bae88 100644 --- a/SharpChat/Events/MessageCreateEvent.cs +++ b/SharpChat/Events/MessageCreateEvent.cs @@ -1,8 +1,8 @@ -namespace SharpChat.Events { +namespace SharpChat.Events { public class MessageCreateEvent( long msgId, string channelName, - long senderId, + string senderId, string senderName, ColourInheritable senderColour, int senderRank, @@ -16,7 +16,7 @@ ) : ChatEvent { public long MessageId { get; } = msgId; public string ChannelName { get; } = channelName; - public long SenderId { get; } = senderId; + public string SenderId { get; } = senderId; public string SenderName { get; } = senderName; public ColourInheritable SenderColour { get; } = senderColour; public int SenderRank { get; } = senderRank; diff --git a/SharpChat/Misuzu/MisuzuAuthInfo.cs b/SharpChat/Misuzu/MisuzuAuthInfo.cs deleted file mode 100644 index 424e02f..0000000 --- a/SharpChat/Misuzu/MisuzuAuthInfo.cs +++ /dev/null @@ -1,28 +0,0 @@ -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; } - - public ColourInheritable Colour => ColourInheritable.FromMisuzu(ColourRaw); - - [JsonPropertyName("hierarchy")] - public int Rank { get; set; } - - [JsonPropertyName("perms")] - public UserPermissions Permissions { get; set; } - } -} diff --git a/SharpChat/Misuzu/MisuzuClient.cs b/SharpChat/Misuzu/MisuzuClient.cs deleted file mode 100644 index 66038ee..0000000 --- a/SharpChat/Misuzu/MisuzuClient.cs +++ /dev/null @@ -1,241 +0,0 @@ -using SharpChat.Config; -using System.Net; -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; - -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<string> BaseURL { get; } - private CachedValue<string> SecretKey { get; } - - public MisuzuClient(HttpClient httpClient, Config.Config config) { - ArgumentNullException.ThrowIfNull(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<MisuzuAuthInfo?> 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<string, string> { - { "method", method }, - { "token", token }, - { "ipaddr", ipAddr }, - }), - Headers = { - { "X-SharpChat-Signature", CreateStringSignature(sig) }, - }, - }; - - using HttpResponseMessage res = await HttpClient.SendAsync(req); - - return JsonSerializer.Deserialize<MisuzuAuthInfo>( - await res.Content.ReadAsByteArrayAsync() - ); - } - - public async Task BumpUsersOnlineAsync(IEnumerable<(string userId, string ipAddr)> list) { - ArgumentNullException.ThrowIfNull(list); - if(!list.Any()) - return; - - string now = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(); - StringBuilder sb = new(); - sb.AppendFormat("bump#{0}", now); - - Dictionary<string, string> 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<MisuzuBanInfo?> 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<MisuzuBanInfo>( - await res.Content.ReadAsByteArrayAsync() - ); - } - - public async Task<MisuzuBanInfo[]?> 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<MisuzuBanInfo[]>( - await res.Content.ReadAsByteArrayAsync() - ); - } - - public enum BanRevokeKind { - UserId, - RemoteAddress, - } - - public async Task<bool> RevokeBanAsync(MisuzuBanInfo banInfo, BanRevokeKind kind) { - ArgumentNullException.ThrowIfNull(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, - _ => null, - } ?? 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<string, string> { - { "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/Program.cs b/SharpChat/Program.cs index 1d1c6e9..148a43e 100644 --- a/SharpChat/Program.cs +++ b/SharpChat/Program.cs @@ -1,6 +1,6 @@ -using SharpChat.Config; +using SharpChat.Configuration; using SharpChat.EventStorage; -using SharpChat.Misuzu; +using SharpChat.Flashii; using System.Text; namespace SharpChat { @@ -52,7 +52,7 @@ namespace SharpChat { if(hasCancelled) return; - MisuzuClient msz = new(httpClient, config.ScopeTo("msz")); + FlashiiClient flashii = new(httpClient, config.ScopeTo("msz")); if(hasCancelled) return; @@ -67,7 +67,7 @@ namespace SharpChat { if(hasCancelled) return; - using SockChatServer scs = new(httpClient, msz, evtStore, config.ScopeTo("chat")); + using SockChatServer scs = new(httpClient, flashii, flashii, evtStore, config.ScopeTo("chat")); scs.Listen(mre); mre.WaitOne(); diff --git a/SharpChat/S2CPackets/AuthFailS2CPacket.cs b/SharpChat/S2CPackets/AuthFailS2CPacket.cs index 4c290a1..28b9434 100644 --- a/SharpChat/S2CPackets/AuthFailS2CPacket.cs +++ b/SharpChat/S2CPackets/AuthFailS2CPacket.cs @@ -1,4 +1,4 @@ -using System.Text; +using System.Text; namespace SharpChat.S2CPackets { public class AuthFailS2CPacket( @@ -9,6 +9,7 @@ namespace SharpChat.S2CPackets { AuthInvalid, MaxSessions, Banned, + Exception, } public string Pack() { @@ -21,6 +22,9 @@ namespace SharpChat.S2CPackets { default: sb.Append("authfail"); break; + case Reason.Exception: + sb.Append("userfail"); + break; case Reason.MaxSessions: sb.Append("sockfail"); break; diff --git a/SharpChat/S2CPackets/AuthSuccessS2CPacket.cs b/SharpChat/S2CPackets/AuthSuccessS2CPacket.cs index dc92a39..5bdee5f 100644 --- a/SharpChat/S2CPackets/AuthSuccessS2CPacket.cs +++ b/SharpChat/S2CPackets/AuthSuccessS2CPacket.cs @@ -1,8 +1,8 @@ -using System.Text; +using System.Text; namespace SharpChat.S2CPackets { public class AuthSuccessS2CPacket( - long userId, + string userId, string userName, ColourInheritable userColour, int userRank, diff --git a/SharpChat/S2CPackets/BanListS2CPacket.cs b/SharpChat/S2CPackets/BanListS2CPacket.cs index e55dadd..9fc1a2e 100644 --- a/SharpChat/S2CPackets/BanListS2CPacket.cs +++ b/SharpChat/S2CPackets/BanListS2CPacket.cs @@ -1,16 +1,12 @@ -using System.Text; +using SharpChat.Bans; +using System.Text; namespace SharpChat.S2CPackets { public class BanListS2CPacket( long msgId, IEnumerable<BanListS2CPacket.Entry> entries ) : S2CPacket { - public enum Type { - UserName, - IPAddress, - } - - public record Entry(Type type, string value); + public record Entry(BanKind type, string value); public string Pack() { StringBuilder sb = new(); @@ -20,7 +16,7 @@ namespace SharpChat.S2CPackets { sb.Append("\t-1\t0\fbanlist\f"); sb.Append(string.Join(", ", entries.Select(entry => string.Format( @"<a href=""javascript:void(0);"" onclick=""Chat.SendMessageWrapper('{0} '+ this.innerHTML);"">{1}</a>", - entry.type == Type.IPAddress ? "/unbanip" : "/unban", + entry.type == BanKind.IPAddress ? "/unbanip" : "/unban", entry.value )))); sb.Append('\t'); diff --git a/SharpChat/S2CPackets/ChatMessageAddS2CPacket.cs b/SharpChat/S2CPackets/ChatMessageAddS2CPacket.cs index d694194..1494de8 100644 --- a/SharpChat/S2CPackets/ChatMessageAddS2CPacket.cs +++ b/SharpChat/S2CPackets/ChatMessageAddS2CPacket.cs @@ -1,10 +1,10 @@ -using System.Text; +using System.Text; namespace SharpChat.S2CPackets { public class ChatMessageAddS2CPacket( long msgId, DateTimeOffset created, - long userId, + string userId, string text, bool isAction, bool isPrivate diff --git a/SharpChat/S2CPackets/ContextUsersS2CPacket.cs b/SharpChat/S2CPackets/ContextUsersS2CPacket.cs index 4d11566..2cf488c 100644 --- a/SharpChat/S2CPackets/ContextUsersS2CPacket.cs +++ b/SharpChat/S2CPackets/ContextUsersS2CPacket.cs @@ -1,8 +1,8 @@ -using System.Text; +using System.Text; namespace SharpChat.S2CPackets { public class ContextUsersS2CPacket(IEnumerable<ContextUsersS2CPacket.Entry> entries) : S2CPacket { - public record Entry(long id, string name, ColourInheritable colour, int rank, UserPermissions perms, bool visible); + public record Entry(string id, string name, ColourInheritable colour, int rank, UserPermissions perms, bool visible); public string Pack() { StringBuilder sb = new(); diff --git a/SharpChat/S2CPackets/UserChannelJoinS2CPacket.cs b/SharpChat/S2CPackets/UserChannelJoinS2CPacket.cs index f2e4f32..102a3c0 100644 --- a/SharpChat/S2CPackets/UserChannelJoinS2CPacket.cs +++ b/SharpChat/S2CPackets/UserChannelJoinS2CPacket.cs @@ -1,9 +1,9 @@ -using System.Text; +using System.Text; namespace SharpChat.S2CPackets { public class UserChannelJoinS2CPacket( long msgId, - long userId, + string userId, string userName, ColourInheritable userColour, int userRank, diff --git a/SharpChat/S2CPackets/UserChannelLeaveS2CPacket.cs b/SharpChat/S2CPackets/UserChannelLeaveS2CPacket.cs index ace8e00..b5dbb6a 100644 --- a/SharpChat/S2CPackets/UserChannelLeaveS2CPacket.cs +++ b/SharpChat/S2CPackets/UserChannelLeaveS2CPacket.cs @@ -1,7 +1,7 @@ -using System.Text; +using System.Text; namespace SharpChat.S2CPackets { - public class UserChannelLeaveS2CPacket(long msgId, long userId) : S2CPacket { + public class UserChannelLeaveS2CPacket(long msgId, string userId) : S2CPacket { public string Pack() { StringBuilder sb = new(); diff --git a/SharpChat/S2CPackets/UserConnectS2CPacket.cs b/SharpChat/S2CPackets/UserConnectS2CPacket.cs index 8e04800..6c8a3a5 100644 --- a/SharpChat/S2CPackets/UserConnectS2CPacket.cs +++ b/SharpChat/S2CPackets/UserConnectS2CPacket.cs @@ -1,10 +1,10 @@ -using System.Text; +using System.Text; namespace SharpChat.S2CPackets { public class UserConnectS2CPacket( long msgId, DateTimeOffset joined, - long userId, + string userId, string userName, ColourInheritable userColour, int userRank, diff --git a/SharpChat/S2CPackets/UserDisconnectS2CPacket.cs b/SharpChat/S2CPackets/UserDisconnectS2CPacket.cs index a0e3ba7..5d1c6fd 100644 --- a/SharpChat/S2CPackets/UserDisconnectS2CPacket.cs +++ b/SharpChat/S2CPackets/UserDisconnectS2CPacket.cs @@ -1,10 +1,10 @@ -using System.Text; +using System.Text; namespace SharpChat.S2CPackets { public class UserDisconnectS2CPacket( long msgId, DateTimeOffset disconnected, - long userId, + string userId, string userName, UserDisconnectS2CPacket.Reason reason ) : S2CPacket { diff --git a/SharpChat/S2CPackets/UserUpdateS2CPacket.cs b/SharpChat/S2CPackets/UserUpdateS2CPacket.cs index 9d7cab9..cc41808 100644 --- a/SharpChat/S2CPackets/UserUpdateS2CPacket.cs +++ b/SharpChat/S2CPackets/UserUpdateS2CPacket.cs @@ -1,8 +1,8 @@ -using System.Text; +using System.Text; namespace SharpChat.S2CPackets { public class UserUpdateS2CPacket( - long userId, + string userId, string userName, ColourInheritable userColour, int userRank, diff --git a/SharpChat/SharpChat.csproj b/SharpChat/SharpChat.csproj index df29243..5fdb581 100644 --- a/SharpChat/SharpChat.csproj +++ b/SharpChat/SharpChat.csproj @@ -33,6 +33,7 @@ </ItemGroup> <ItemGroup> + <ProjectReference Include="..\SharpChat.Misuzu\SharpChat.Flashii.csproj" /> <ProjectReference Include="..\SharpChatCommon\SharpChatCommon.csproj" /> </ItemGroup> diff --git a/SharpChat/SockChatServer.cs b/SharpChat/SockChatServer.cs index da602b0..07b2909 100644 --- a/SharpChat/SockChatServer.cs +++ b/SharpChat/SockChatServer.cs @@ -1,10 +1,11 @@ -using Fleck; -using SharpChat.ClientCommands; -using SharpChat.Config; -using SharpChat.EventStorage; -using SharpChat.Misuzu; -using SharpChat.S2CPackets; +using Fleck; +using SharpChat.Auth; +using SharpChat.Bans; using SharpChat.C2SPacketHandlers; +using SharpChat.ClientCommands; +using SharpChat.Configuration; +using SharpChat.S2CPackets; +using System.Net; namespace SharpChat { public class SockChatServer : IDisposable { @@ -18,7 +19,7 @@ namespace SharpChat { public Context Context { get; } private readonly HttpClient HttpClient; - private readonly MisuzuClient Misuzu; + private readonly BansClient BansClient; private readonly CachedValue<int> MaxMessageLength; private readonly CachedValue<int> MaxConnections; @@ -35,11 +36,17 @@ namespace SharpChat { private Channel DefaultChannel { get; set; } - public SockChatServer(HttpClient httpClient, MisuzuClient msz, EventStorage.EventStorage evtStore, Config.Config config) { + public SockChatServer( + HttpClient httpClient, + AuthClient authClient, + BansClient bansClient, + EventStorage.EventStorage evtStore, + Config config + ) { Logger.Write("Initialising Sock Chat server..."); - HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); - Misuzu = msz ?? throw new ArgumentNullException(nameof(msz)); + HttpClient = httpClient; + BansClient = bansClient; MaxMessageLength = config.ReadCached("msgMaxLength", DEFAULT_MSG_LENGTH_MAX); MaxConnections = config.ReadCached("connMaxCount", DEFAULT_MAX_CONNECTIONS); @@ -51,7 +58,7 @@ namespace SharpChat { string[]? channelNames = config.ReadValue("channels", DEFAULT_CHANNELS); if(channelNames is not null) foreach(string channelName in channelNames) { - Config.Config channelCfg = config.ScopeTo($"channels:{channelName}"); + Config channelCfg = config.ScopeTo($"channels:{channelName}"); string name = channelCfg.SafeReadValue("name", string.Empty)!; if(string.IsNullOrWhiteSpace(name)) @@ -70,10 +77,10 @@ namespace SharpChat { if(DefaultChannel is null) throw new Exception("The default channel could not be determined."); - GuestHandlers.Add(new AuthC2SPacketHandler(Misuzu, DefaultChannel, MaxMessageLength, MaxConnections)); + GuestHandlers.Add(new AuthC2SPacketHandler(authClient, bansClient, DefaultChannel, MaxMessageLength, MaxConnections)); AuthedHandlers.AddRange([ - new PingC2SPacketHandler(Misuzu), + new PingC2SPacketHandler(authClient), SendMessageHandler = new SendMessageC2SPacketHandler(Context.RandomSnowflake, MaxMessageLength), ]); @@ -90,10 +97,10 @@ namespace SharpChat { new RankChannelClientCommand(), new BroadcastClientCommand(), new DeleteMessageClientCommand(), - new KickBanClientCommand(msz), - new PardonUserClientCommand(msz), - new PardonAddressClientCommand(msz), - new BanListClientCommand(msz), + new KickBanClientCommand(bansClient), + new PardonUserClientCommand(bansClient), + new PardonAddressClientCommand(bansClient), + new BanListClientCommand(bansClient), new RemoteAddressClientCommand(), ]); @@ -184,11 +191,13 @@ namespace SharpChat { Context.BanUser(conn.User, banDuration, UserDisconnectS2CPacket.Reason.Flood); if(banDuration > TimeSpan.Zero) - Misuzu.CreateBanAsync( - conn.User.UserId.ToString(), conn.RemoteAddress.ToString(), - string.Empty, "::1", + BansClient.BanCreateAsync( + BanKind.User, banDuration, - "Kicked from chat for flood protection." + conn.RemoteAddress, + conn.User.UserId, + "Kicked from chat for flood protection.", + IPAddress.IPv6Loopback ).Wait(); return; diff --git a/SharpChat/User.cs b/SharpChat/User.cs index 8fd29e9..b87c439 100644 --- a/SharpChat/User.cs +++ b/SharpChat/User.cs @@ -1,10 +1,10 @@ -using SharpChat.ClientCommands; +using SharpChat.ClientCommands; using System.Globalization; using System.Text; namespace SharpChat { public class User( - long userId, + string userId, string userName, ColourInheritable colour, int rank, @@ -17,7 +17,7 @@ namespace SharpChat { public const int DEFAULT_MINIMUM_DELAY = 10000; public const int DEFAULT_RISKY_OFFSET = 5; - public long UserId { get; } = userId; + public string UserId { get; } = userId; public string UserName { get; set; } = userName ?? throw new ArgumentNullException(nameof(userName)); public ColourInheritable Colour { get; set; } = colour; public int Rank { get; set; } = rank; @@ -65,9 +65,9 @@ namespace SharpChat { } public static string GetDMChannelName(User user1, User user2) { - return user1.UserId < user2.UserId - ? $"@{user1.UserId}-{user2.UserId}" - : $"@{user2.UserId}-{user1.UserId}"; + return string.Compare(user1.UserId, user2.UserId, StringComparison.InvariantCultureIgnoreCase) > 0 + ? $"@{user2.UserId}-{user1.UserId}" + : $"@{user1.UserId}-{user2.UserId}"; } } } diff --git a/SharpChatCommon/Auth/AuthClient.cs b/SharpChatCommon/Auth/AuthClient.cs new file mode 100644 index 0000000..c0e8528 --- /dev/null +++ b/SharpChatCommon/Auth/AuthClient.cs @@ -0,0 +1,8 @@ +using System.Net; + +namespace SharpChat.Auth { + public interface AuthClient { + Task<AuthResult> AuthVerifyAsync(IPAddress remoteAddr, string scheme, string token); + Task AuthBumpUsersOnlineAsync(IEnumerable<(IPAddress remoteAddr, string userId)> entries); + } +} diff --git a/SharpChatCommon/Auth/AuthFailedException.cs b/SharpChatCommon/Auth/AuthFailedException.cs new file mode 100644 index 0000000..460d00a --- /dev/null +++ b/SharpChatCommon/Auth/AuthFailedException.cs @@ -0,0 +1,3 @@ +namespace SharpChat.Auth { + public class AuthFailedException(string message) : Exception(message) {} +} diff --git a/SharpChatCommon/Auth/AuthResult.cs b/SharpChatCommon/Auth/AuthResult.cs new file mode 100644 index 0000000..95efcae --- /dev/null +++ b/SharpChatCommon/Auth/AuthResult.cs @@ -0,0 +1,9 @@ +namespace SharpChat.Auth { + public interface AuthResult { + string UserId { get; } + string UserName { get; } + ColourInheritable UserColour { get; } + int UserRank { get; } + UserPermissions UserPermissions { get; } + } +} diff --git a/SharpChatCommon/Bans/BanInfo.cs b/SharpChatCommon/Bans/BanInfo.cs new file mode 100644 index 0000000..412364e --- /dev/null +++ b/SharpChatCommon/Bans/BanInfo.cs @@ -0,0 +1,9 @@ +namespace SharpChat.Bans { + public interface BanInfo { + BanKind Kind { get; } + bool IsPermanent { get; } + DateTimeOffset ExpiresAt { get; } + public bool HasExpired => !IsPermanent && DateTimeOffset.UtcNow >= ExpiresAt; + string ToString(); + } +} diff --git a/SharpChatCommon/Bans/BanKind.cs b/SharpChatCommon/Bans/BanKind.cs new file mode 100644 index 0000000..cea18cd --- /dev/null +++ b/SharpChatCommon/Bans/BanKind.cs @@ -0,0 +1,6 @@ +namespace SharpChat.Bans { + public enum BanKind { + User, + IPAddress, + } +} diff --git a/SharpChatCommon/Bans/BansClient.cs b/SharpChatCommon/Bans/BansClient.cs new file mode 100644 index 0000000..34b3ed7 --- /dev/null +++ b/SharpChatCommon/Bans/BansClient.cs @@ -0,0 +1,18 @@ +using System.Net; + +namespace SharpChat.Bans { + public interface BansClient { + Task BanCreateAsync( + BanKind kind, + TimeSpan duration, + IPAddress remoteAddr, + string? userId = null, + string? reason = null, + IPAddress? issuerRemoteAddr = null, + string? issuerUserId = null + ); + Task<bool> BanRevokeAsync(BanInfo info); + Task<BanInfo?> BanGetAsync(string? userIdOrName = null, IPAddress? remoteAddr = null); + Task<BanInfo[]> BanGetListAsync(); + } +} diff --git a/SharpChatCommon/Bans/IPAddressBanInfo.cs b/SharpChatCommon/Bans/IPAddressBanInfo.cs new file mode 100644 index 0000000..ebae8e2 --- /dev/null +++ b/SharpChatCommon/Bans/IPAddressBanInfo.cs @@ -0,0 +1,7 @@ +using System.Net; + +namespace SharpChat.Bans { + public interface IPAddressBanInfo : BanInfo { + IPAddress Address { get; } + } +} diff --git a/SharpChatCommon/Bans/UserBanInfo.cs b/SharpChatCommon/Bans/UserBanInfo.cs new file mode 100644 index 0000000..1050007 --- /dev/null +++ b/SharpChatCommon/Bans/UserBanInfo.cs @@ -0,0 +1,7 @@ +namespace SharpChat.Bans { + public interface UserBanInfo : BanInfo { + string UserId { get; } + string UserName { get; } + ColourInheritable UserColour { get; } + } +} diff --git a/SharpChat/Config/CachedValue.cs b/SharpChatCommon/Configuration/CachedValue.cs similarity index 97% rename from SharpChat/Config/CachedValue.cs rename to SharpChatCommon/Configuration/CachedValue.cs index 11625a1..feaf1d5 100644 --- a/SharpChat/Config/CachedValue.cs +++ b/SharpChatCommon/Configuration/CachedValue.cs @@ -1,4 +1,4 @@ -namespace SharpChat.Config { +namespace SharpChat.Configuration { public class CachedValue<T>(Config config, string name, TimeSpan lifetime, T? fallback) { private Config Config { get; } = config ?? throw new ArgumentNullException(nameof(config)); private string Name { get; } = name ?? throw new ArgumentNullException(nameof(name)); diff --git a/SharpChat/Config/Config.cs b/SharpChatCommon/Configuration/Config.cs similarity index 96% rename from SharpChat/Config/Config.cs rename to SharpChatCommon/Configuration/Config.cs index d0d60c1..04c4140 100644 --- a/SharpChat/Config/Config.cs +++ b/SharpChatCommon/Configuration/Config.cs @@ -1,4 +1,4 @@ -namespace SharpChat.Config { +namespace SharpChat.Configuration { public interface Config : IDisposable { /// <summary> /// Creates a proxy object that forces all names to start with the given prefix. diff --git a/SharpChat/Config/ConfigExceptions.cs b/SharpChatCommon/Configuration/ConfigExceptions.cs similarity index 92% rename from SharpChat/Config/ConfigExceptions.cs rename to SharpChatCommon/Configuration/ConfigExceptions.cs index 47f395e..6e851bf 100644 --- a/SharpChat/Config/ConfigExceptions.cs +++ b/SharpChatCommon/Configuration/ConfigExceptions.cs @@ -1,4 +1,4 @@ -namespace SharpChat.Config { +namespace SharpChat.Configuration { public abstract class ConfigException : Exception { public ConfigException(string message) : base(message) { } public ConfigException(string message, Exception ex) : base(message, ex) { } diff --git a/SharpChat/Config/ScopedConfig.cs b/SharpChatCommon/Configuration/ScopedConfig.cs similarity index 97% rename from SharpChat/Config/ScopedConfig.cs rename to SharpChatCommon/Configuration/ScopedConfig.cs index 56ded38..735496b 100644 --- a/SharpChat/Config/ScopedConfig.cs +++ b/SharpChatCommon/Configuration/ScopedConfig.cs @@ -1,4 +1,4 @@ -namespace SharpChat.Config { +namespace SharpChat.Configuration { public class ScopedConfig(Config config, string prefix) : Config { private Config Config { get; } = config ?? throw new ArgumentNullException(nameof(config)); private string Prefix { get; } = prefix ?? throw new ArgumentNullException(nameof(prefix)); diff --git a/SharpChat/Config/StreamConfig.cs b/SharpChatCommon/Configuration/StreamConfig.cs similarity index 98% rename from SharpChat/Config/StreamConfig.cs rename to SharpChatCommon/Configuration/StreamConfig.cs index bef573f..3c6887e 100644 --- a/SharpChat/Config/StreamConfig.cs +++ b/SharpChatCommon/Configuration/StreamConfig.cs @@ -1,6 +1,6 @@ -using System.Text; +using System.Text; -namespace SharpChat.Config { +namespace SharpChat.Configuration { public class StreamConfig : Config { private Stream Stream { get; } private StreamReader StreamReader { get; } diff --git a/SharpChat/UserPermissions.cs b/SharpChatCommon/UserPermissions.cs similarity index 100% rename from SharpChat/UserPermissions.cs rename to SharpChatCommon/UserPermissions.cs