Better HttpClient handling.

This commit is contained in:
flash 2023-02-06 21:14:50 +01:00
parent d2fef02e08
commit 513539319f
11 changed files with 145 additions and 122 deletions

View file

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2019-2022 flashwave Copyright (c) 2019-2023 flashwave
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View file

@ -5,6 +5,15 @@ VisualStudioVersion = 17.2.32630.192
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpChat", "SharpChat\SharpChat.csproj", "{DDB24C19-B802-4C96-AC15-0449C6FC77F2}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpChat", "SharpChat\SharpChat.csproj", "{DDB24C19-B802-4C96-AC15-0449C6FC77F2}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{DF7A7073-A67A-4D93-92C6-F9D0F95E2359}"
ProjectSection(SolutionItems) = preProject
.gitattributes = .gitattributes
.gitignore = .gitignore
LICENSE = LICENSE
Protocol.md = Protocol.md
README.md = README.md
EndProjectSection
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU

View file

@ -3,6 +3,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
namespace SharpChat { namespace SharpChat {
public interface IBan { public interface IBan {
@ -43,15 +45,18 @@ namespace SharpChat {
} }
public class BanManager : IDisposable { public class BanManager : IDisposable {
private readonly List<IBan> BanList = new List<IBan>(); private readonly List<IBan> BanList = new();
private readonly HttpClient HttpClient;
public readonly ChatContext Context; public readonly ChatContext Context;
public bool IsDisposed { get; private set; } public bool IsDisposed { get; private set; }
public BanManager(ChatContext context) { public BanManager(HttpClient httpClient, ChatContext context) {
HttpClient = httpClient;
Context = context; Context = context;
RefreshFlashiiBans(); RefreshFlashiiBans().Wait();
} }
public void Add(ChatUser user, DateTimeOffset expires) { public void Add(ChatUser user, DateTimeOffset expires) {
@ -143,26 +148,20 @@ namespace SharpChat {
BanList.RemoveAll(x => x.Expires <= DateTimeOffset.Now); BanList.RemoveAll(x => x.Expires <= DateTimeOffset.Now);
} }
public void RefreshFlashiiBans() { public async Task RefreshFlashiiBans() {
FlashiiBan.GetList(SockChatServer.HttpClient).ContinueWith(x => { IEnumerable<FlashiiBan> bans = await FlashiiBan.GetListAsync(HttpClient);
if(x.IsFaulted) {
Logger.Write($@"Ban Refresh: {x.Exception}");
return;
}
if(!x.Result.Any()) if(!bans.Any())
return; return;
lock(BanList) { lock(BanList)
foreach(FlashiiBan fb in x.Result) { foreach(FlashiiBan fb in bans) {
if(!BanList.OfType<BannedUser>().Any(x => x.UserId == fb.UserId)) if(!BanList.OfType<BannedUser>().Any(x => x.UserId == fb.UserId))
Add(new BannedUser(fb)); Add(new BannedUser(fb));
if(!BanList.OfType<BannedIPAddress>().Any(x => x.Address.ToString() == fb.UserIP)) if(!BanList.OfType<BannedIPAddress>().Any(x => x.Address.ToString() == fb.UserIP))
Add(new BannedIPAddress(fb)); Add(new BannedIPAddress(fb));
} }
} }
});
}
public IEnumerable<IBan> All() { public IEnumerable<IBan> All() {
lock(BanList) lock(BanList)
@ -170,20 +169,19 @@ namespace SharpChat {
} }
~BanManager() ~BanManager()
=> Dispose(false); => DoDispose();
public void Dispose() public void Dispose() {
=> Dispose(true); DoDispose();
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing) { private void DoDispose() {
if(IsDisposed) if(IsDisposed)
return; return;
IsDisposed = true; IsDisposed = true;
BanList.Clear(); BanList.Clear();
if (disposing)
GC.SuppressFinalize(this);
} }
} }
} }

View file

@ -4,12 +4,15 @@ using SharpChat.Packet;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net; using System.Net;
using System.Net.Http;
using System.Threading; using System.Threading;
namespace SharpChat { namespace SharpChat {
public class ChatContext : IDisposable, IPacketTarget { public class ChatContext : IDisposable, IPacketTarget {
public bool IsDisposed { get; private set; } public bool IsDisposed { get; private set; }
private readonly HttpClient HttpClient;
public SockChatServer Server { get; } public SockChatServer Server { get; }
public Timer BumpTimer { get; } public Timer BumpTimer { get; }
public BanManager Bans { get; } public BanManager Bans { get; }
@ -19,14 +22,15 @@ namespace SharpChat {
public string TargetName => @"@broadcast"; public string TargetName => @"@broadcast";
public ChatContext(SockChatServer server) { public ChatContext(HttpClient httpClient, SockChatServer server) {
HttpClient = httpClient;
Server = server; Server = server;
Bans = new BanManager(this); Bans = new BanManager(httpClient, this);
Users = new UserManager(this); Users = new UserManager(this);
Channels = new ChannelManager(this); Channels = new ChannelManager(this);
Events = new ChatEventManager(this); Events = new ChatEventManager(this);
BumpTimer = new Timer(e => FlashiiBump.Submit(SockChatServer.HttpClient, Users.WithActiveConnections()), null, TimeSpan.Zero, TimeSpan.FromMinutes(1)); BumpTimer = new Timer(e => FlashiiBump.SubmitAsync(HttpClient, Users.WithActiveConnections()).Wait(), null, TimeSpan.Zero, TimeSpan.FromMinutes(1));
} }
public void Update() { public void Update() {
@ -166,13 +170,16 @@ namespace SharpChat {
user.Send(packet); user.Send(packet);
} }
~ChatContext() ~ChatContext() {
=> Dispose(false); DoDispose();
}
public void Dispose() public void Dispose() {
=> Dispose(true); DoDispose();
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing) { private void DoDispose() {
if (IsDisposed) if (IsDisposed)
return; return;
IsDisposed = true; IsDisposed = true;
@ -182,9 +189,6 @@ namespace SharpChat {
Channels?.Dispose(); Channels?.Dispose();
Users?.Dispose(); Users?.Dispose();
Bans?.Dispose(); Bans?.Dispose();
if (disposing)
GC.SuppressFinalize(this);
} }
} }
} }

View file

@ -1,5 +1,4 @@
using Microsoft.Win32.SafeHandles; using System;
using System;
using System.Net.Http; using System.Net.Http;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
@ -49,7 +48,7 @@ namespace SharpChat.Flashii {
[JsonPropertyName(@"perms")] [JsonPropertyName(@"perms")]
public ChatUserPermissions Permissions { get; set; } public ChatUserPermissions Permissions { get; set; }
public static async Task<FlashiiAuth> Attempt(HttpClient httpClient, FlashiiAuthRequest authRequest) { public static async Task<FlashiiAuth> AttemptAsync(HttpClient httpClient, FlashiiAuthRequest authRequest) {
if(httpClient == null) if(httpClient == null)
throw new ArgumentNullException(nameof(httpClient)); throw new ArgumentNullException(nameof(httpClient));
if(authRequest == null) if(authRequest == null)
@ -68,7 +67,7 @@ namespace SharpChat.Flashii {
}; };
#endif #endif
using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, FlashiiUrls.AuthURL) { HttpRequestMessage request = new(HttpMethod.Post, FlashiiUrls.AuthURL) {
Content = new ByteArrayContent(authRequest.GetJSON()), Content = new ByteArrayContent(authRequest.GetJSON()),
Headers = { Headers = {
{ @"X-SharpChat-Signature", authRequest.Hash }, { @"X-SharpChat-Signature", authRequest.Hash },

View file

@ -21,11 +21,11 @@ namespace SharpChat.Flashii {
[JsonPropertyName(@"username")] [JsonPropertyName(@"username")]
public string Username { get; set; } public string Username { get; set; }
public static async Task<IEnumerable<FlashiiBan>> GetList(HttpClient httpClient) { public static async Task<IEnumerable<FlashiiBan>> GetListAsync(HttpClient httpClient) {
if(httpClient == null) if(httpClient == null)
throw new ArgumentNullException(nameof(httpClient)); throw new ArgumentNullException(nameof(httpClient));
using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, FlashiiUrls.BansURL) { HttpRequestMessage request = new(HttpMethod.Get, FlashiiUrls.BansURL) {
Headers = { Headers = {
{ @"X-SharpChat-Signature", STRING.GetSignedHash() }, { @"X-SharpChat-Signature", STRING.GetSignedHash() },
}, },

View file

@ -14,14 +14,17 @@ namespace SharpChat.Flashii {
[JsonPropertyName(@"ip")] [JsonPropertyName(@"ip")]
public string UserIP { get; set; } public string UserIP { get; set; }
public static void Submit(HttpClient httpClient, IEnumerable<ChatUser> users) { public static async Task SubmitAsync(HttpClient httpClient, IEnumerable<ChatUser> users) {
List<FlashiiBump> bups = users.Where(u => u.HasSessions).Select(x => new FlashiiBump { UserId = x.UserId, UserIP = x.RemoteAddresses.First().ToString() }).ToList(); FlashiiBump[] bups = users.Where(u => u.HasSessions).Select(x => new FlashiiBump {
UserId = x.UserId,
UserIP = x.RemoteAddresses.First().ToString()
}).ToArray();
if(bups.Any()) if(bups.Any())
Submit(httpClient, bups); await SubmitAsync(httpClient, bups);
} }
public static void Submit(HttpClient httpClient, IEnumerable<FlashiiBump> users) { public static async Task SubmitAsync(HttpClient httpClient, IEnumerable<FlashiiBump> users) {
if(httpClient == null) if(httpClient == null)
throw new ArgumentNullException(nameof(httpClient)); throw new ArgumentNullException(nameof(httpClient));
if(users == null) if(users == null)
@ -31,17 +34,14 @@ namespace SharpChat.Flashii {
byte[] data = JsonSerializer.SerializeToUtf8Bytes(users); byte[] data = JsonSerializer.SerializeToUtf8Bytes(users);
using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, FlashiiUrls.BumpURL) { HttpRequestMessage request = new(HttpMethod.Post, FlashiiUrls.BumpURL) {
Content = new ByteArrayContent(data), Content = new ByteArrayContent(data),
Headers = { Headers = {
{ @"X-SharpChat-Signature", data.GetSignedHash() }, { @"X-SharpChat-Signature", data.GetSignedHash() },
} }
}; };
httpClient.SendAsync(request).ContinueWith(x => { await httpClient.SendAsync(request);
if(x.IsFaulted)
Logger.Write($@"Flashii Bump Error: {x.Exception}");
});
} }
} }
} }

View file

@ -68,6 +68,7 @@ namespace SharpChat.Packet {
// Abbreviated class name because otherwise shit gets wide // Abbreviated class name because otherwise shit gets wide
public static class LCR { public static class LCR {
public const string GENERIC_ERROR = @"generr";
public const string COMMAND_NOT_FOUND = @"nocmd"; public const string COMMAND_NOT_FOUND = @"nocmd";
public const string COMMAND_NOT_ALLOWED = @"cmdna"; public const string COMMAND_NOT_ALLOWED = @"cmdna";
public const string COMMAND_FORMAT_ERROR = @"cmderr"; public const string COMMAND_FORMAT_ERROR = @"cmderr";

View file

@ -1,7 +1,5 @@
using SharpChat.Flashii; using System;
using System; using System.Net.Http;
using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
namespace SharpChat { namespace SharpChat {
@ -21,9 +19,27 @@ namespace SharpChat {
Database.ReadConfig(); Database.ReadConfig();
using ManualResetEvent mre = new ManualResetEvent(false); using ManualResetEvent mre = new(false);
using SockChatServer scs = new SockChatServer(mre, PORT); bool hasCancelled = false;
Console.CancelKeyPress += (s, e) => { e.Cancel = true; mre.Set(); };
void cancelKeyPressHandler(object sender, ConsoleCancelEventArgs ev) {
Console.CancelKeyPress -= cancelKeyPressHandler;
hasCancelled = true;
ev.Cancel = true;
mre.Set();
};
Console.CancelKeyPress += cancelKeyPressHandler;
if(hasCancelled) return;
using HttpClient httpClient = new();
httpClient.DefaultRequestHeaders.Add(@"User-Agent", @"SharpChat/20230206");
if(hasCancelled) return;
using SockChatServer scs = new(httpClient, PORT);
scs.Listen(mre);
mre.WaitOne(); mre.WaitOne();
} }
} }

View file

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View file

@ -9,7 +9,6 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
@ -39,7 +38,7 @@ namespace SharpChat {
public IWebSocketServer Server { get; } public IWebSocketServer Server { get; }
public ChatContext Context { get; } public ChatContext Context { get; }
public static HttpClient HttpClient { get; } private readonly HttpClient HttpClient;
private IReadOnlyCollection<IChatCommand> Commands { get; } = new IChatCommand[] { private IReadOnlyCollection<IChatCommand> Commands { get; } = new IChatCommand[] {
new AFKCommand(), new AFKCommand(),
@ -53,21 +52,15 @@ namespace SharpChat {
return Sessions.FirstOrDefault(x => x.Connection == conn); return Sessions.FirstOrDefault(x => x.Connection == conn);
} }
static SockChatServer() { private ManualResetEvent Shutdown { get; set; }
// "fuck it"
HttpClient = new HttpClient();
HttpClient.DefaultRequestHeaders.UserAgent.ParseAdd(@"SharpChat");
}
private ManualResetEvent Shutdown { get; }
private bool IsShuttingDown = false; private bool IsShuttingDown = false;
public SockChatServer(ManualResetEvent mre, ushort port) { public SockChatServer(HttpClient httpClient, ushort port) {
Logger.Write("Starting Sock Chat server..."); Logger.Write("Starting Sock Chat server...");
Shutdown = mre ?? throw new ArgumentNullException(nameof(mre)); HttpClient = httpClient;
Context = new ChatContext(this); Context = new ChatContext(HttpClient, this);
Context.Channels.Add(new ChatChannel(@"Lounge")); Context.Channels.Add(new ChatChannel(@"Lounge"));
#if DEBUG #if DEBUG
@ -79,6 +72,10 @@ namespace SharpChat {
Context.Channels.Add(new ChatChannel(@"Staff") { Rank = 5 }); Context.Channels.Add(new ChatChannel(@"Staff") { Rank = 5 });
Server = new SharpChatWebSocketServer($@"ws://0.0.0.0:{port}"); Server = new SharpChatWebSocketServer($@"ws://0.0.0.0:{port}");
}
public void Listen(ManualResetEvent mre) {
Shutdown = mre;
Server.Start(sock => { Server.Start(sock => {
if(IsShuttingDown || IsDisposed) { if(IsShuttingDown || IsDisposed) {
@ -143,7 +140,7 @@ namespace SharpChat {
return; return;
} }
if(sess.User is ChatUser && sess.User.HasFloodProtection) { if(sess.User is not null && sess.User.HasFloodProtection) {
sess.User.RateLimiter.AddTimePoint(); sess.User.RateLimiter.AddTimePoint();
if(sess.User.RateLimiter.State == ChatRateLimitState.Kick) { if(sess.User.RateLimiter.State == ChatRateLimitState.Kick) {
@ -182,7 +179,7 @@ namespace SharpChat {
if(args.Length < 3 || !long.TryParse(args[1], out long aUserId)) if(args.Length < 3 || !long.TryParse(args[1], out long aUserId))
break; break;
FlashiiAuth.Attempt(HttpClient, new FlashiiAuthRequest { FlashiiAuth.AttemptAsync(HttpClient, new FlashiiAuthRequest {
UserId = aUserId, UserId = aUserId,
Token = args[2], Token = args[2],
IPAddress = sess.RemoteAddress.ToString(), IPAddress = sess.RemoteAddress.ToString(),
@ -276,7 +273,7 @@ namespace SharpChat {
} }
if(messageText.Length > MSG_LENGTH_MAX) if(messageText.Length > MSG_LENGTH_MAX)
messageText = messageText.Substring(0, MSG_LENGTH_MAX); messageText = messageText[..MSG_LENGTH_MAX];
messageText = messageText.Trim(); messageText = messageText.Trim();
@ -293,8 +290,7 @@ namespace SharpChat {
break; break;
} }
if(message == null) message ??= new ChatMessage {
message = new ChatMessage {
Target = mChannel, Target = mChannel,
TargetName = mChannel.TargetName, TargetName = mChannel.TargetName,
DateTime = DateTimeOffset.UtcNow, DateTime = DateTimeOffset.UtcNow,
@ -335,7 +331,7 @@ namespace SharpChat {
} }
public IChatMessage HandleV1Command(string message, ChatUser user, ChatChannel channel) { public IChatMessage HandleV1Command(string message, ChatUser user, ChatChannel channel) {
string[] parts = message.Substring(1).Split(' '); string[] parts = message[1..].Split(' ');
string commandName = parts[0].Replace(@".", string.Empty).ToLowerInvariant(); string commandName = parts[0].Replace(@".", string.Empty).ToLowerInvariant();
for(int i = 1; i < parts.Length; i++) for(int i = 1; i < parts.Length; i++)
@ -370,8 +366,7 @@ namespace SharpChat {
offset = 2; offset = 2;
} }
if(targetUser == null) targetUser ??= user;
targetUser = user;
if(parts.Length < offset) { if(parts.Length < offset) {
user.Send(new LegacyCommandResponse(LCR.COMMAND_FORMAT_ERROR)); user.Send(new LegacyCommandResponse(LCR.COMMAND_FORMAT_ERROR));
@ -389,7 +384,7 @@ namespace SharpChat {
if(nickStr == targetUser.Username) if(nickStr == targetUser.Username)
nickStr = null; nickStr = null;
else if(nickStr.Length > 15) else if(nickStr.Length > 15)
nickStr = nickStr.Substring(0, 15); nickStr = nickStr[..15];
else if(string.IsNullOrEmpty(nickStr)) else if(string.IsNullOrEmpty(nickStr))
nickStr = null; nickStr = null;
@ -454,7 +449,7 @@ namespace SharpChat {
Flags = ChatMessageFlags.Action, Flags = ChatMessageFlags.Action,
}; };
case @"who": // gets all online users/online users in a channel if arg case @"who": // gets all online users/online users in a channel if arg
StringBuilder whoChanSB = new StringBuilder(); StringBuilder whoChanSB = new();
string whoChanStr = parts.Length > 1 && !string.IsNullOrEmpty(parts[1]) ? parts[1] : string.Empty; string whoChanStr = parts.Length > 1 && !string.IsNullOrEmpty(parts[1]) ? parts[1] : string.Empty;
if(!string.IsNullOrEmpty(whoChanStr)) { if(!string.IsNullOrEmpty(whoChanStr)) {
@ -476,7 +471,7 @@ namespace SharpChat {
if(whoUser == user) if(whoUser == user)
whoChanSB.Append(@" style=""font-weight: bold;"""); whoChanSB.Append(@" style=""font-weight: bold;""");
whoChanSB.Append(@">"); whoChanSB.Append('>');
whoChanSB.Append(whoUser.DisplayName); whoChanSB.Append(whoUser.DisplayName);
whoChanSB.Append(@"</a>, "); whoChanSB.Append(@"</a>, ");
} }
@ -492,7 +487,7 @@ namespace SharpChat {
if(whoUser == user) if(whoUser == user)
whoChanSB.Append(@" style=""font-weight: bold;"""); whoChanSB.Append(@" style=""font-weight: bold;""");
whoChanSB.Append(@">"); whoChanSB.Append('>');
whoChanSB.Append(whoUser.DisplayName); whoChanSB.Append(whoUser.DisplayName);
whoChanSB.Append(@"</a>, "); whoChanSB.Append(@"</a>, ");
} }
@ -546,7 +541,8 @@ namespace SharpChat {
int createChanHierarchy = 0; int createChanHierarchy = 0;
if(createChanHasHierarchy) if(createChanHasHierarchy)
int.TryParse(parts[1], out createChanHierarchy); if(!int.TryParse(parts[1], out createChanHierarchy))
createChanHierarchy = 0;
if(createChanHierarchy > user.Rank) { if(createChanHierarchy > user.Rank) {
user.Send(new LegacyCommandResponse(LCR.INSUFFICIENT_HIERARCHY)); user.Send(new LegacyCommandResponse(LCR.INSUFFICIENT_HIERARCHY));
@ -554,7 +550,7 @@ namespace SharpChat {
} }
string createChanName = string.Join('_', parts.Skip(createChanHasHierarchy ? 2 : 1)); string createChanName = string.Join('_', parts.Skip(createChanHasHierarchy ? 2 : 1));
ChatChannel createChan = new ChatChannel { ChatChannel createChan = new() {
Name = createChanName, Name = createChanName,
IsTemporary = !user.Can(ChatUserPermissions.SetChannelPermanent), IsTemporary = !user.Can(ChatUserPermissions.SetChannelPermanent),
Rank = createChanHierarchy, Rank = createChanHierarchy,
@ -848,7 +844,7 @@ namespace SharpChat {
Sessions.ForEach(s => s.PrepareForRestart()); Sessions.ForEach(s => s.PrepareForRestart());
Context.Update(); Context.Update();
Shutdown.Set(); Shutdown?.Set();
break; break;
default: default:
@ -859,13 +855,16 @@ namespace SharpChat {
return null; return null;
} }
~SockChatServer() ~SockChatServer() {
=> Dispose(false); DoDispose();
}
public void Dispose() public void Dispose() {
=> Dispose(true); DoDispose();
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing) { private void DoDispose() {
if(IsDisposed) if(IsDisposed)
return; return;
IsDisposed = true; IsDisposed = true;
@ -876,9 +875,6 @@ namespace SharpChat {
Server?.Dispose(); Server?.Dispose();
Context?.Dispose(); Context?.Dispose();
HttpClient?.Dispose(); HttpClient?.Dispose();
if(disposing)
GC.SuppressFinalize(this);
} }
} }
} }