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
Copyright (c) 2019-2022 flashwave
Copyright (c) 2019-2023 flashwave
Permission is hereby granted, free of charge, to any person obtaining a copy
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
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpChat", "SharpChat\SharpChat.csproj", "{DDB24C19-B802-4C96-AC15-0449C6FC77F2}"
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
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU

View file

@ -3,6 +3,8 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
namespace SharpChat {
public interface IBan {
@ -43,25 +45,28 @@ namespace SharpChat {
}
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 bool IsDisposed { get; private set; }
public BanManager(ChatContext context) {
public BanManager(HttpClient httpClient, ChatContext context) {
HttpClient = httpClient;
Context = context;
RefreshFlashiiBans();
RefreshFlashiiBans().Wait();
}
public void Add(ChatUser user, DateTimeOffset expires) {
if (expires <= DateTimeOffset.Now)
if(expires <= DateTimeOffset.Now)
return;
lock (BanList) {
lock(BanList) {
BannedUser ban = BanList.OfType<BannedUser>().FirstOrDefault(x => x.UserId == user.UserId);
if (ban == null)
if(ban == null)
Add(new BannedUser { UserId = user.UserId, Expires = expires, Username = user.Username });
else
ban.Expires = expires;
@ -69,13 +74,13 @@ namespace SharpChat {
}
public void Add(IPAddress addr, DateTimeOffset expires) {
if (expires <= DateTimeOffset.Now)
if(expires <= DateTimeOffset.Now)
return;
lock (BanList) {
lock(BanList) {
BannedIPAddress ban = BanList.OfType<BannedIPAddress>().FirstOrDefault(x => x.Address.Equals(addr));
if (ban == null)
if(ban == null)
Add(new BannedIPAddress { Address = addr, Expires = expires });
else
ban.Expires = expires;
@ -83,11 +88,11 @@ namespace SharpChat {
}
private void Add(IBan ban) {
if (ban == null)
if(ban == null)
return;
lock (BanList)
if (!BanList.Contains(ban))
lock(BanList)
if(!BanList.Contains(ban))
BanList.Add(ban);
}
@ -102,12 +107,12 @@ namespace SharpChat {
}
public void Remove(IBan ban) {
lock (BanList)
lock(BanList)
BanList.Remove(ban);
}
public DateTimeOffset Check(ChatUser user) {
if (user == null)
if(user == null)
return DateTimeOffset.MinValue;
lock(BanList)
@ -115,26 +120,26 @@ namespace SharpChat {
}
public DateTimeOffset Check(IPAddress addr) {
if (addr == null)
if(addr == null)
return DateTimeOffset.MinValue;
lock (BanList)
lock(BanList)
return BanList.OfType<BannedIPAddress>().Where(x => x.Address.Equals(addr)).FirstOrDefault()?.Expires ?? DateTimeOffset.MinValue;
}
public BannedUser GetUser(string username) {
if (username == null)
if(username == null)
return null;
if (!long.TryParse(username, out long userId))
if(!long.TryParse(username, out long userId))
userId = 0;
lock (BanList)
lock(BanList)
return BanList.OfType<BannedUser>().FirstOrDefault(x => x.Username.ToLowerInvariant() == username.ToLowerInvariant() || (userId > 0 && x.UserId == userId));
}
public BannedIPAddress GetIPAddress(IPAddress addr) {
lock (BanList)
lock(BanList)
return BanList.OfType<BannedIPAddress>().FirstOrDefault(x => x.Address.Equals(addr));
}
@ -143,47 +148,40 @@ namespace SharpChat {
BanList.RemoveAll(x => x.Expires <= DateTimeOffset.Now);
}
public void RefreshFlashiiBans() {
FlashiiBan.GetList(SockChatServer.HttpClient).ContinueWith(x => {
if(x.IsFaulted) {
Logger.Write($@"Ban Refresh: {x.Exception}");
return;
}
public async Task RefreshFlashiiBans() {
IEnumerable<FlashiiBan> bans = await FlashiiBan.GetListAsync(HttpClient);
if(!x.Result.Any())
return;
if(!bans.Any())
return;
lock(BanList) {
foreach(FlashiiBan fb in x.Result) {
if(!BanList.OfType<BannedUser>().Any(x => x.UserId == fb.UserId))
Add(new BannedUser(fb));
if(!BanList.OfType<BannedIPAddress>().Any(x => x.Address.ToString() == fb.UserIP))
Add(new BannedIPAddress(fb));
}
lock(BanList)
foreach(FlashiiBan fb in bans) {
if(!BanList.OfType<BannedUser>().Any(x => x.UserId == fb.UserId))
Add(new BannedUser(fb));
if(!BanList.OfType<BannedIPAddress>().Any(x => x.Address.ToString() == fb.UserIP))
Add(new BannedIPAddress(fb));
}
});
}
public IEnumerable<IBan> All() {
lock (BanList)
lock(BanList)
return BanList.ToList();
}
~BanManager()
=> Dispose(false);
=> DoDispose();
public void Dispose()
=> Dispose(true);
public void Dispose() {
DoDispose();
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing) {
if (IsDisposed)
private void DoDispose() {
if(IsDisposed)
return;
IsDisposed = true;
BanList.Clear();
if (disposing)
GC.SuppressFinalize(this);
}
}
}

View file

@ -4,12 +4,15 @@ using SharpChat.Packet;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading;
namespace SharpChat {
public class ChatContext : IDisposable, IPacketTarget {
public bool IsDisposed { get; private set; }
private readonly HttpClient HttpClient;
public SockChatServer Server { get; }
public Timer BumpTimer { get; }
public BanManager Bans { get; }
@ -19,14 +22,15 @@ namespace SharpChat {
public string TargetName => @"@broadcast";
public ChatContext(SockChatServer server) {
public ChatContext(HttpClient httpClient, SockChatServer server) {
HttpClient = httpClient;
Server = server;
Bans = new BanManager(this);
Bans = new BanManager(httpClient, this);
Users = new UserManager(this);
Channels = new ChannelManager(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() {
@ -166,13 +170,16 @@ namespace SharpChat {
user.Send(packet);
}
~ChatContext()
=> Dispose(false);
~ChatContext() {
DoDispose();
}
public void Dispose()
=> Dispose(true);
public void Dispose() {
DoDispose();
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing) {
private void DoDispose() {
if (IsDisposed)
return;
IsDisposed = true;
@ -182,9 +189,6 @@ namespace SharpChat {
Channels?.Dispose();
Users?.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.Text.Json;
using System.Text.Json.Serialization;
@ -49,7 +48,7 @@ namespace SharpChat.Flashii {
[JsonPropertyName(@"perms")]
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)
throw new ArgumentNullException(nameof(httpClient));
if(authRequest == null)
@ -68,7 +67,7 @@ namespace SharpChat.Flashii {
};
#endif
using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, FlashiiUrls.AuthURL) {
HttpRequestMessage request = new(HttpMethod.Post, FlashiiUrls.AuthURL) {
Content = new ByteArrayContent(authRequest.GetJSON()),
Headers = {
{ @"X-SharpChat-Signature", authRequest.Hash },

View file

@ -21,11 +21,11 @@ namespace SharpChat.Flashii {
[JsonPropertyName(@"username")]
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)
throw new ArgumentNullException(nameof(httpClient));
using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, FlashiiUrls.BansURL) {
HttpRequestMessage request = new(HttpMethod.Get, FlashiiUrls.BansURL) {
Headers = {
{ @"X-SharpChat-Signature", STRING.GetSignedHash() },
},

View file

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

View file

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

View file

@ -1,7 +1,5 @@
using SharpChat.Flashii;
using System;
using System.Collections.Generic;
using System.Linq;
using System;
using System.Net.Http;
using System.Threading;
namespace SharpChat {
@ -21,9 +19,27 @@ namespace SharpChat {
Database.ReadConfig();
using ManualResetEvent mre = new ManualResetEvent(false);
using SockChatServer scs = new SockChatServer(mre, PORT);
Console.CancelKeyPress += (s, e) => { e.Cancel = true; mre.Set(); };
using ManualResetEvent mre = new(false);
bool hasCancelled = false;
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();
}
}

View file

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

View file

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