Imported stable branch.

master
flash 3 months ago
commit 4ceffeb48d
  1. 1
      .gitattributes
  2. 215
      .gitignore
  3. 7
      Hamakaze/Hamakaze.csproj
  4. 26
      Hamakaze/Headers/HttpAcceptEncodingHeader.cs
  5. 17
      Hamakaze/Headers/HttpConnectionHeader.cs
  6. 20
      Hamakaze/Headers/HttpContentEncodingHeader.cs
  7. 30
      Hamakaze/Headers/HttpContentLengthHeader.cs
  8. 20
      Hamakaze/Headers/HttpContentTypeHeader.cs
  9. 13
      Hamakaze/Headers/HttpCustomHeader.cs
  10. 18
      Hamakaze/Headers/HttpDateHeader.cs
  11. 41
      Hamakaze/Headers/HttpHeader.cs
  12. 37
      Hamakaze/Headers/HttpHostHeader.cs
  13. 35
      Hamakaze/Headers/HttpKeepAliveHeader.cs
  14. 14
      Hamakaze/Headers/HttpServerHeader.cs
  15. 26
      Hamakaze/Headers/HttpTeHeader.cs
  16. 20
      Hamakaze/Headers/HttpTransferEncodingHeader.cs
  17. 20
      Hamakaze/Headers/HttpUserAgentHeader.cs
  18. 118
      Hamakaze/HttpClient.cs
  19. 81
      Hamakaze/HttpConnection.cs
  20. 122
      Hamakaze/HttpConnectionManager.cs
  21. 69
      Hamakaze/HttpEncoding.cs
  22. 40
      Hamakaze/HttpException.cs
  23. 159
      Hamakaze/HttpMediaType.cs
  24. 46
      Hamakaze/HttpMessage.cs
  25. 190
      Hamakaze/HttpRequestMessage.cs
  26. 265
      Hamakaze/HttpResponseMessage.cs
  27. 189
      Hamakaze/HttpTask.cs
  28. 41
      Hamakaze/HttpTaskManager.cs
  29. 21
      LICENSE
  30. 1323
      Protocol-draft.md
  31. 1225
      Protocol.md
  32. 10
      README.md
  33. 31
      SharpChat.sln
  34. 184
      SharpChat/BanManager.cs
  35. 160
      SharpChat/ChannelManager.cs
  36. 106
      SharpChat/ChatChannel.cs
  37. 18
      SharpChat/ChatChannelTyping.cs
  38. 55
      SharpChat/ChatColour.cs
  39. 190
      SharpChat/ChatContext.cs
  40. 100
      SharpChat/ChatEventManager.cs
  41. 46
      SharpChat/ChatRateLimiter.cs
  42. 216
      SharpChat/ChatUser.cs
  43. 25
      SharpChat/ChatUserPermissions.cs
  44. 89
      SharpChat/ChatUserSession.cs
  45. 7
      SharpChat/ChatUserStatus.cs
  46. 31
      SharpChat/Commands/AFKCommand.cs
  47. 230
      SharpChat/Database.cs
  48. 60
      SharpChat/Database_Migrations.cs
  49. 31
      SharpChat/Events/ChatMessage.cs
  50. 25
      SharpChat/Events/IChatEvent.cs
  51. 32
      SharpChat/Events/UserChannelJoinEvent.cs
  52. 32
      SharpChat/Events/UserChannelLeaveEvent.cs
  53. 32
      SharpChat/Events/UserConnectEvent.cs
  54. 38
      SharpChat/Events/UserDisconnectEvent.cs
  55. 35
      SharpChat/Extensions.cs
  56. 81
      SharpChat/Flashii/FlashiiAuth.cs
  57. 40
      SharpChat/Flashii/FlashiiBan.cs
  58. 37
      SharpChat/Flashii/FlashiiBump.cs
  59. 14
      SharpChat/Flashii/FlashiiUrls.cs
  60. 8
      SharpChat/IChatCommand.cs
  61. 22
      SharpChat/IChatCommandContext.cs
  62. 6
      SharpChat/IPacketTarget.cs
  63. 22
      SharpChat/IServerPacket.cs
  64. 28
      SharpChat/Logger.cs
  65. 57
      SharpChat/Packet/AuthFailPacket.cs
  66. 33
      SharpChat/Packet/AuthSuccessPacket.cs
  67. 35
      SharpChat/Packet/BanListPacket.cs
  68. 24
      SharpChat/Packet/ChannelCreatePacket.cs
  69. 25
      SharpChat/Packet/ChannelDeletePacket.cs
  70. 28
      SharpChat/Packet/ChannelUpdatePacket.cs
  71. 57
      SharpChat/Packet/ChatMessageAddPacket.cs
  72. 22
      SharpChat/Packet/ChatMessageDeletePacket.cs
  73. 31
      SharpChat/Packet/ContextChannelsPacket.cs
  74. 37
      SharpChat/Packet/ContextClearPacket.cs
  75. 99
      SharpChat/Packet/ContextMessagePacket.cs
  76. 33
      SharpChat/Packet/ContextUsersPacket.cs
  77. 20
      SharpChat/Packet/FloodWarningPacket.cs
  78. 40
      SharpChat/Packet/ForceDisconnectPacket.cs
  79. 107
      SharpChat/Packet/LegacyCommandResponse.cs
  80. 23
      SharpChat/Packet/PongPacket.cs
  81. 29
      SharpChat/Packet/TypingPacket.cs
  82. 25
      SharpChat/Packet/UserChannelForceJoinPacket.cs
  83. 31
      SharpChat/Packet/UserChannelJoinPacket.cs
  84. 27
      SharpChat/Packet/UserChannelLeavePacket.cs
  85. 29
      SharpChat/Packet/UserConnectPacket.cs
  86. 58
      SharpChat/Packet/UserDisconnectPacket.cs
  87. 42
      SharpChat/Packet/UserUpdatePacket.cs
  88. 30
      SharpChat/Program.cs
  89. 30
      SharpChat/RNG.cs
  90. 17
      SharpChat/SharpChat.csproj
  91. 166
      SharpChat/SharpChatWebSocketServer.cs
  92. 48
      SharpChat/SockChatEnums.cs
  93. 836
      SharpChat/SockChatServer.cs
  94. 90
      SharpChat/UserManager.cs

1
.gitattributes vendored

@ -0,0 +1 @@
* text=auto

215
.gitignore vendored

@ -0,0 +1,215 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
build/
bld/
[Bb]in/
[Oo]bj/
# Visual Studio 2015 cache/options directory
.vs/
# JetBrains Rider cache/options directory
.idea/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# DNX
project.lock.json
artifacts/
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opensdf
*.sdf
*.cachefile
# Visual Studio profiler
*.psess
*.vsp
*.vspx
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# NCrunch
_NCrunch_*
.*crunch*.local.xml
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
## TODO: Comment the next line if you want to checkin your
## web deploy settings but do note that will include unencrypted
## passwords
#*.pubxml
*.publishproj
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# Windows Azure Build Output
csx/
*.build.csdef
# Windows Store app package directory
AppPackages/
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
[Ss]tyle[Cc]op.*
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.pfx
*.publishsettings
node_modules/
orleans.codegen.cs
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# LightSwitch generated files
GeneratedArtifacts/
_Pvt_Extensions/
ModelManifest.xml

@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
</Project>

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Hamakaze.Headers {
public class HttpAcceptEncodingHeader : HttpHeader {
public const string NAME = @"Accept-Encoding";
public override string Name => NAME;
public override object Value => string.Join(@", ", Encodings);
public HttpEncoding[] Encodings { get; }
public HttpAcceptEncodingHeader(string encodings) : this(
(encodings ?? throw new ArgumentNullException(nameof(encodings))).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
) { }
public HttpAcceptEncodingHeader(string[] encodings) : this(
(encodings ?? throw new ArgumentNullException(nameof(encodings))).Select(HttpEncoding.Parse)
) {}
public HttpAcceptEncodingHeader(IEnumerable<HttpEncoding> encodings) {
Encodings = (encodings ?? throw new ArgumentNullException(nameof(encodings))).ToArray();
}
}
}

@ -0,0 +1,17 @@
using System;
namespace Hamakaze.Headers {
public class HttpConnectionHeader : HttpHeader {
public const string NAME = @"Connection";
public override string Name => NAME;
public override object Value { get; }
public const string CLOSE = @"close";
public const string KEEP_ALIVE = @"keep-alive";
public HttpConnectionHeader(string mode) {
Value = mode ?? throw new ArgumentNullException(nameof(mode));
}
}
}

@ -0,0 +1,20 @@
using System;
namespace Hamakaze.Headers {
public class HttpContentEncodingHeader : HttpHeader {
public const string NAME = @"Content-Encoding";
public override string Name => NAME;
public override object Value => string.Join(@", ", Encodings);
public string[] Encodings { get; }
public HttpContentEncodingHeader(string encodings) : this(
(encodings ?? throw new ArgumentNullException(nameof(encodings))).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
) { }
public HttpContentEncodingHeader(string[] encodings) {
Encodings = encodings ?? throw new ArgumentNullException(nameof(encodings));
}
}
}

@ -0,0 +1,30 @@
using System;
using System.IO;
namespace Hamakaze.Headers {
public class HttpContentLengthHeader : HttpHeader {
public const string NAME = @"Content-Length";
public override string Name => NAME;
public override object Value => Stream?.Length ?? Length;
private Stream Stream { get; }
private long Length { get; }
public HttpContentLengthHeader(Stream stream) {
Stream = stream ?? throw new ArgumentNullException(nameof(stream));
if(!stream.CanRead || !stream.CanSeek)
throw new ArgumentException(@"Body must readable and seekable.", nameof(stream));
}
public HttpContentLengthHeader(long length) {
Length = length;
}
public HttpContentLengthHeader(string length) {
if(!long.TryParse(length, out long ll))
throw new ArgumentException(@"Invalid length value.", nameof(length));
Length = ll;
}
}
}

@ -0,0 +1,20 @@
using System;
namespace Hamakaze.Headers {
public class HttpContentTypeHeader : HttpHeader {
public const string NAME = @"Content-Type";
public override string Name => NAME;
public override object Value => MediaType.ToString();
public HttpMediaType MediaType { get; }
public HttpContentTypeHeader(string mediaType) {
MediaType = HttpMediaType.Parse(mediaType ?? throw new ArgumentNullException(nameof(mediaType)));
}
public HttpContentTypeHeader(HttpMediaType mediaType) {
MediaType = mediaType;
}
}
}

@ -0,0 +1,13 @@
using System;
namespace Hamakaze.Headers {
public class HttpCustomHeader : HttpHeader {
public override string Name { get; }
public override object Value { get; }
public HttpCustomHeader(string name, object value) {
Name = NormaliseName(name ?? throw new ArgumentNullException(nameof(name)));
Value = value;
}
}
}

@ -0,0 +1,18 @@
using System;
using System.Globalization;
namespace Hamakaze.Headers {
public class HttpDateHeader : HttpHeader {
public const string NAME = @"Date";
public override string Name => NAME;
public override object Value { get; }
public DateTimeOffset DateTime { get; }
public HttpDateHeader(string dateString) {
Value = dateString ?? throw new ArgumentNullException(nameof(dateString));
DateTime = DateTimeOffset.ParseExact(dateString, @"r", CultureInfo.InvariantCulture);
}
}
}

@ -0,0 +1,41 @@
using System;
using System.Globalization;
namespace Hamakaze.Headers {
public abstract class HttpHeader {
public abstract string Name { get; }
public abstract object Value { get; }
public override string ToString() {
return string.Format(@"{0}: {1}", Name, Value);
}
public static string NormaliseName(string name) {
if(string.IsNullOrWhiteSpace(name))
return string.Empty;
string[] parts = name.ToLowerInvariant().Split('-', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
for(int i = 0; i < parts.Length; ++i)
parts[i] = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(parts[i]);
return string.Join('-', parts);
}
public static HttpHeader Create(string name, object value) {
return name switch {
HttpTeHeader.NAME => new HttpTeHeader(value.ToString()),
HttpDateHeader.NAME => new HttpDateHeader(value.ToString()),
HttpHostHeader.NAME => new HttpHostHeader(value.ToString()),
HttpServerHeader.NAME => new HttpServerHeader(value.ToString()),
HttpUserAgentHeader.NAME => new HttpUserAgentHeader(value.ToString()),
HttpKeepAliveHeader.NAME => new HttpKeepAliveHeader(value.ToString()),
HttpConnectionHeader.NAME => new HttpConnectionHeader(value.ToString()),
HttpContentTypeHeader.NAME => new HttpContentTypeHeader(value.ToString()),
HttpContentLengthHeader.NAME => new HttpContentLengthHeader(value.ToString()),
HttpAcceptEncodingHeader.NAME => new HttpAcceptEncodingHeader(value.ToString()),
HttpContentEncodingHeader.NAME => new HttpContentEncodingHeader(value.ToString()),
HttpTransferEncodingHeader.NAME => new HttpTransferEncodingHeader(value.ToString()),
_ => new HttpCustomHeader(name, value),
};
}
}
}

@ -0,0 +1,37 @@
using System;
using System.Linq;
using System.Text;
namespace Hamakaze.Headers {
public class HttpHostHeader : HttpHeader {
public const string NAME = @"Host";
public override string Name => NAME;
public override object Value {
get {
StringBuilder sb = new();
sb.Append(Host);
if(Port != -1)
sb.AppendFormat(@":{0}", Port);
return sb.ToString();
}
}
public string Host { get; }
public int Port { get; }
public bool IsSecure { get; }
public HttpHostHeader(string host, int port) {
Host = host;
Port = port;
}
public HttpHostHeader(string hostAndPort) {
string[] parts = hostAndPort.Split(':', 2, StringSplitOptions.TrimEntries);
Host = parts.ElementAtOrDefault(0) ?? throw new ArgumentNullException(nameof(hostAndPort));
if(!ushort.TryParse(parts.ElementAtOrDefault(1), out ushort port))
throw new FormatException(@"Host is not in valid format.");
Port = port;
}
}
}

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
namespace Hamakaze.Headers {
public class HttpKeepAliveHeader : HttpHeader {
public const string NAME = @"Keep-Alive";
public override string Name => NAME;
public override object Value {
get {
List<string> parts = new();
if(MaxIdle != TimeSpan.MaxValue)
parts.Add(string.Format(@"timeout={0}", MaxIdle.TotalSeconds));
if(MaxRequests >= 0)
parts.Add(string.Format(@"max={0}", MaxRequests));
return string.Join(@", ", parts);
}
}
public TimeSpan MaxIdle { get; } = TimeSpan.MaxValue;
public int MaxRequests { get; } = -1;
public HttpKeepAliveHeader(string value) {
IEnumerable<string> kvps = (value ?? throw new ArgumentNullException(nameof(value))).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
foreach(string kvp in kvps) {
string[] parts = kvp.Split('=', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if(parts[0] == @"timeout" && int.TryParse(parts[1], out int timeout))
MaxIdle = TimeSpan.FromSeconds(timeout);
else if(parts[0] == @"max" && int.TryParse(parts[1], out int max))
MaxRequests = max;
}
}
}
}

@ -0,0 +1,14 @@
using System;
namespace Hamakaze.Headers {
public class HttpServerHeader : HttpHeader {
public const string NAME = @"Server";
public override string Name => NAME;
public override object Value { get; }
public HttpServerHeader(string server) {
Value = server ?? throw new ArgumentNullException(nameof(server));
}
}
}

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Hamakaze.Headers {
public class HttpTeHeader : HttpHeader {
public const string NAME = @"TE";
public override string Name => NAME;
public override object Value => string.Join(@", ", Encodings);
public HttpEncoding[] Encodings { get; }
public HttpTeHeader(string encodings) : this(
(encodings ?? throw new ArgumentNullException(nameof(encodings))).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
) { }
public HttpTeHeader(string[] encodings) : this(
(encodings ?? throw new ArgumentNullException(nameof(encodings))).Select(HttpEncoding.Parse)
) { }
public HttpTeHeader(IEnumerable<HttpEncoding> encodings) {
Encodings = (encodings ?? throw new ArgumentNullException(nameof(encodings))).ToArray();
}
}
}

@ -0,0 +1,20 @@
using System;
namespace Hamakaze.Headers {
public class HttpTransferEncodingHeader : HttpHeader {
public const string NAME = @"Transfer-Encoding";
public override string Name => NAME;
public override object Value => string.Join(@", ", Encodings);
public string[] Encodings { get; }
public HttpTransferEncodingHeader(string encodings) : this(
(encodings ?? throw new ArgumentNullException(nameof(encodings))).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
) {}
public HttpTransferEncodingHeader(string[] encodings) {
Encodings = encodings ?? throw new ArgumentNullException(nameof(encodings));
}
}
}

@ -0,0 +1,20 @@
using System;
namespace Hamakaze.Headers {
public class HttpUserAgentHeader : HttpHeader {
public const string NAME = @"User-Agent";
public override string Name => NAME;
public override object Value { get; }
public HttpUserAgentHeader(string userAgent) {
if(userAgent == null)
throw new ArgumentNullException(nameof(userAgent));
if(string.IsNullOrWhiteSpace(userAgent) || userAgent.Equals(HttpClient.USER_AGENT))
Value = HttpClient.USER_AGENT;
else
Value = string.Format(@"{0} {1}", userAgent, HttpClient.USER_AGENT);
}
}
}

@ -0,0 +1,118 @@
using Hamakaze.Headers;
using System;
using System.Collections.Generic;
namespace Hamakaze {
public class HttpClient : IDisposable {
public const string PRODUCT_STRING = @"HMKZ";
public const string VERSION_MAJOR = @"1";
public const string VERSION_MINOR = @"0";
public const string USER_AGENT = PRODUCT_STRING + @"/" + VERSION_MAJOR + @"." + VERSION_MINOR;
private static HttpClient InstanceValue { get; set; }
public static HttpClient Instance {
get {
if(InstanceValue == null)
InstanceValue = new HttpClient();
return InstanceValue;
}
}
private HttpConnectionManager Connections { get; }
private HttpTaskManager Tasks { get; }
public string DefaultUserAgent { get; set; } = USER_AGENT;
public bool ReuseConnections { get; set; } = true;
public IEnumerable<HttpEncoding> AcceptedEncodings { get; set; } = new[] { HttpEncoding.GZip, HttpEncoding.Deflate, HttpEncoding.Brotli };
public HttpClient() {
Connections = new HttpConnectionManager();
Tasks = new HttpTaskManager();
}
public HttpTask CreateTask(
HttpRequestMessage request,
Action<HttpTask, HttpResponseMessage> onComplete = null,
Action<HttpTask, Exception> onError = null,
Action<HttpTask> onCancel = null,
Action<HttpTask, long, long> onDownloadProgress = null,
Action<HttpTask, long, long> onUploadProgress = null,
Action<HttpTask, HttpTask.TaskState> onStateChange = null,
bool disposeRequest = true,
bool disposeResponse = true
) {
if(request == null)
throw new ArgumentNullException(nameof(request));
if(string.IsNullOrWhiteSpace(request.UserAgent))
request.UserAgent = DefaultUserAgent;
if(!request.HasHeader(HttpAcceptEncodingHeader.NAME))
request.AcceptedEncodings = AcceptedEncodings;
request.Connection = ReuseConnections ? HttpConnectionHeader.KEEP_ALIVE : HttpConnectionHeader.CLOSE;
HttpTask task = new(Connections, request, disposeRequest, disposeResponse);
if(onComplete != null)
task.OnComplete += onComplete;
if(onError != null)
task.OnError += onError;
if(onCancel != null)
task.OnCancel += onCancel;
if(onDownloadProgress != null)
task.OnDownloadProgress += onDownloadProgress;
if(onUploadProgress != null)
task.OnUploadProgress += onUploadProgress;
if(onStateChange != null)
task.OnStateChange += onStateChange;
return task;
}
public void RunTask(HttpTask task) {
Tasks.RunTask(task);
}
public void SendRequest(
HttpRequestMessage request,
Action<HttpTask, HttpResponseMessage> onComplete = null,
Action<HttpTask, Exception> onError = null,
Action<HttpTask> onCancel = null,
Action<HttpTask, long, long> onDownloadProgress = null,
Action<HttpTask, long, long> onUploadProgress = null,
Action<HttpTask, HttpTask.TaskState> onStateChange = null,
bool disposeRequest = true,
bool disposeResponse = true
) {
RunTask(CreateTask(request, onComplete, onError, onCancel, onDownloadProgress, onUploadProgress, onStateChange, disposeRequest, disposeResponse));
}
public static void Send(
HttpRequestMessage request,
Action<HttpTask, HttpResponseMessage> onComplete = null,
Action<HttpTask, Exception> onError = null,
Action<HttpTask> onCancel = null,
Action<HttpTask, long, long> onDownloadProgress = null,
Action<HttpTask, long, long> onUploadProgress = null,
Action<HttpTask, HttpTask.TaskState> onStateChange = null,
bool disposeRequest = true,
bool disposeResponse = true
) {
Instance.SendRequest(request, onComplete, onError, onCancel, onDownloadProgress, onUploadProgress, onStateChange, disposeRequest, disposeResponse);
}
private bool IsDisposed;
~HttpClient()
=> DoDispose();
public void Dispose() {
DoDispose();
GC.SuppressFinalize(this);
}
private void DoDispose() {
if(IsDisposed)
return;
IsDisposed = true;
Tasks.Dispose();
Connections.Dispose();
}
}
}

@ -0,0 +1,81 @@
using System;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Threading;
namespace Hamakaze {
public class HttpConnection : IDisposable {
public IPEndPoint EndPoint { get; }
public Stream Stream { get; }
public Socket Socket { get; }
public NetworkStream NetworkStream { get; }
public SslStream SslStream { get; }
public string Host { get; }
public bool IsSecure { get; }
public bool HasTimedOut => MaxRequests == 0 || (DateTimeOffset.Now - LastOperation) > MaxIdle;
public int MaxRequests { get; set; } = -1;
public TimeSpan MaxIdle { get; set; } = TimeSpan.MaxValue;
public DateTimeOffset LastOperation { get; private set; } = DateTimeOffset.Now;
public bool InUse { get; private set; }
public HttpConnection(string host, IPEndPoint endPoint, bool secure) {
Host = host ?? throw new ArgumentNullException(nameof(host));
EndPoint = endPoint ?? throw new ArgumentNullException(nameof(endPoint));
IsSecure = secure;
if(endPoint.AddressFamily is not AddressFamily.InterNetwork and not AddressFamily.InterNetworkV6)
throw new ArgumentException(@"Address must be an IPv4 or IPv6 address.", nameof(endPoint));
Socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp) {
NoDelay = true,
Blocking = true,
};
Socket.Connect(endPoint);
NetworkStream = new NetworkStream(Socket, true);
if(IsSecure) {
SslStream = new SslStream(NetworkStream, false, (s, ce, ch, e) => e == SslPolicyErrors.None, null);
Stream = SslStream;
SslStream.AuthenticateAsClient(Host, null, SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13, true);
} else
Stream = NetworkStream;
}
public void MarkUsed() {
LastOperation = DateTimeOffset.Now;
if(MaxRequests > 0)
--MaxRequests;
}
public bool Acquire() {
return !InUse && (InUse = true);
}
public void Release() {
InUse = false;
}
private bool IsDisposed;
~HttpConnection()
=> DoDispose();
public void Dispose() {
DoDispose();
GC.SuppressFinalize(this);
}
private void DoDispose() {
if(IsDisposed)
return;
IsDisposed = true;
Stream.Dispose();
}
}
}

@ -0,0 +1,122 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading;
namespace Hamakaze {
public class HttpConnectionManager : IDisposable {
private List<HttpConnection> Connections { get; } = new();
private Mutex Lock { get; } = new();
public HttpConnectionManager() {
}
private void AcquireLock() {
if(!Lock.WaitOne(10000))
throw new HttpConnectionManagerLockException();
}
private void ReleaseLock() {
Lock.ReleaseMutex();
}
public HttpConnection CreateConnection(string host, IPEndPoint endPoint, bool secure) {
if(host == null)
throw new ArgumentNullException(nameof(host));
if(endPoint == null)
throw new ArgumentNullException(nameof(endPoint));
HttpConnection conn = null;
AcquireLock();
try {
conn = CreateConnectionInternal(host, endPoint, secure);
} finally {
ReleaseLock();
}
return conn;
}
private HttpConnection CreateConnectionInternal(string host, IPEndPoint endPoint, bool secure) {
HttpConnection conn = new(host, endPoint, secure);
Connections.Add(conn);
return conn;
}
public HttpConnection GetConnection(string host, IPEndPoint endPoint, bool secure) {
if(host == null)
throw new ArgumentNullException(nameof(host));
if(endPoint == null)
throw new ArgumentNullException(nameof(endPoint));
HttpConnection conn = null;
AcquireLock();
try {
conn = GetConnectionInternal(host, endPoint, secure);
} finally {
ReleaseLock();
}
return conn;
}
private HttpConnection GetConnectionInternal(string host, IPEndPoint endPoint, bool secure) {
CleanConnectionsInternal();
HttpConnection conn = Connections.FirstOrDefault(c => host.Equals(c.Host) && endPoint.Equals(c.EndPoint) && c.IsSecure == secure && c.Acquire());
if(conn == null) {
conn = CreateConnectionInternal(host, endPoint, secure);
conn.Acquire();
}
return conn;
}
public void EndConnection(HttpConnection conn) {
if(conn == null)
throw new ArgumentNullException(nameof(conn));
AcquireLock();
try {
EndConnectionInternal(conn);
} finally {
ReleaseLock();
}
}
private void EndConnectionInternal(HttpConnection conn) {
Connections.Remove(conn);
conn.Dispose();
}
public void CleanConnection() {
AcquireLock();
try {
CleanConnectionsInternal();
} finally {
ReleaseLock();
}
}
private void CleanConnectionsInternal() {
IEnumerable<HttpConnection> conns = Connections.Where(x => x.HasTimedOut).ToArray();
foreach(HttpConnection conn in conns) {
Connections.Remove(conn);
conn.Dispose();
}
}
private bool IsDisposed;
~HttpConnectionManager()
=> DoDispose();
public void Dispose() {
DoDispose();
GC.SuppressFinalize(this);
}
private void DoDispose() {
if(IsDisposed)
return;
IsDisposed = true;
Lock.Dispose();
foreach(HttpConnection conn in Connections)
conn.Dispose();
Connections.Clear();
}
}
}

@ -0,0 +1,69 @@
using System;
using System.Globalization;
using System.Text;
namespace Hamakaze {
public readonly struct HttpEncoding : IComparable<HttpEncoding?>, IEquatable<HttpEncoding?> {
public const string DEFLATE = @"deflate";
public const string GZIP = @"gzip";
public const string XGZIP = @"x-gzip";
public const string BROTLI = @"br";
public const string IDENTITY = @"identity";
public const string CHUNKED = @"chunked";
public const string ANY = @"*";
public static readonly HttpEncoding Any = new(ANY);
public static readonly HttpEncoding None = new(ANY, 0f);
public static readonly HttpEncoding Deflate = new(DEFLATE);
public static readonly HttpEncoding GZip = new(GZIP);
public static readonly HttpEncoding Brotli = new(BROTLI);
public static readonly HttpEncoding Identity = new(IDENTITY);
public string Name { get; }
public float Quality { get; }
public HttpEncoding(string name, float quality = 1f) {
Name = name ?? throw new ArgumentNullException(nameof(name));
Quality = quality;
}
public HttpEncoding WithQuality(float quality) {
return new HttpEncoding(Name, quality);
}
public static HttpEncoding Parse(string encoding) {
string[] parts = encoding.Split(';', StringSplitOptions.TrimEntries);
float quality = 1f;
encoding = parts[0];
for(int i = 1; i < parts.Length; ++i)
if(parts[i].StartsWith(@"q=")) {
if(!float.TryParse(parts[i], out quality))
quality = 1f;
break;
}
return new HttpEncoding(encoding, quality);
}
public override string ToString() {
StringBuilder sb = new();
sb.Append(Name);
if(Quality is >= 0f and < 1f)
sb.AppendFormat(CultureInfo.InvariantCulture, @";q={0:0.0}", Quality);
return sb.ToString();
}
public int CompareTo(HttpEncoding? other) {
if(!other.HasValue || other.Value.Quality < Quality)
return -1;
if(other.Value.Quality > Quality)
return 1;
return 0;
}
public bool Equals(HttpEncoding? other) {
return other.HasValue && Name.Equals(other.Value.Name) && Quality.Equals(other.Value.Quality);
}
}
}

@ -0,0 +1,40 @@
using System;
namespace Hamakaze {
public class HttpException : Exception {
public HttpException(string message) : base(message) { }
}
public class HttpConnectionManagerException : HttpException {
public HttpConnectionManagerException(string message) : base(message) { }
}
public class HttpConnectionManagerLockException : HttpConnectionManagerException {
public HttpConnectionManagerLockException() : base(@"Failed to lock the connection manager in time.") { }
}
public class HttpTaskException : HttpException {
public HttpTaskException(string message) : base(message) { }
}
public class HttpTaskAlreadyStartedException : HttpTaskException {
public HttpTaskAlreadyStartedException() : base(@"Task has already started.") { }
}
public class HttpTaskInvalidStateException : HttpTaskException {
public HttpTaskInvalidStateException() : base(@"Task has ended up in an invalid state.") { }
}
public class HttpTaskNoAddressesException : HttpTaskException {
public HttpTaskNoAddressesException() : base(@"Could not find any addresses for this host.") { }
}
public class HttpTaskNoConnectionException : HttpTaskException {
public HttpTaskNoConnectionException() : base(@"Was unable to create a connection with this host.") { }
}
public class HttpTaskRequestFailedException : HttpTaskException {
public HttpTaskRequestFailedException() : base(@"Request failed for unknown reasons.") { }
}
public class HttpTaskManagerException : HttpException {
public HttpTaskManagerException(string message) : base(message) { }
}
public class HttpTaskManagerLockException : HttpTaskManagerException {
public HttpTaskManagerLockException() : base(@"Failed to reserve a thread.") { }
}
}

@ -0,0 +1,159 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Hamakaze {
public readonly struct HttpMediaType : IComparable<HttpMediaType?>, IEquatable<HttpMediaType?> {
public const string TYPE_APPLICATION = @"application";
public const string TYPE_AUDIO = @"audio";
public const string TYPE_IMAGE = @"image";
public const string TYPE_MESSAGE = @"message";
public const string TYPE_MULTIPART = @"multipart";
public const string TYPE_TEXT = @"text";
public const string TYPE_VIDEO = @"video";
public static readonly HttpMediaType OctetStream = new(TYPE_APPLICATION, @"octet-stream");
public static readonly HttpMediaType FWIF = new(TYPE_APPLICATION, @"x.fwif");
public static readonly HttpMediaType JSON = new(TYPE_APPLICATION, @"json");
public static readonly HttpMediaType HTML = new(TYPE_TEXT, @"html", args: new[] { Param.UTF8 });
public string Type { get; }
public string Subtype { get; }
public string Suffix { get; }
public IEnumerable<Param> Params { get; }
public HttpMediaType(string type, string subtype, string suffix = null, IEnumerable<Param> args = null) {
Type = type ?? throw new ArgumentNullException(nameof(type));
Subtype = subtype ?? throw new ArgumentNullException(nameof(subtype));
Suffix = suffix ?? string.Empty;
Params = args ?? Enumerable.Empty<Param>();
}
public string GetParamValue(string name) {
foreach(Param param in Params)
if(param.Name.ToLowerInvariant() == name)
return param.Value;
return null;
}
public static explicit operator HttpMediaType(string mediaTypeString) => Parse(mediaTypeString);
public static HttpMediaType Parse(string mediaTypeString) {
if(mediaTypeString == null)
throw new ArgumentNullException(nameof(mediaTypeString));
int slashIndex = mediaTypeString.IndexOf('/');
if(slashIndex == -1)
return OctetStream;
string type = mediaTypeString[..slashIndex];
string subtype = mediaTypeString[(slashIndex + 1)..];
string suffix = null;
IEnumerable<Param> args = null;
int paramIndex = subtype.IndexOf(';');
if(paramIndex != -1) {
args = subtype[(paramIndex + 1)..]
.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(Param.Parse);
subtype = subtype[..paramIndex];
}
int suffixIndex = subtype.IndexOf('+');
if(suffixIndex != -1) {
suffix = subtype[(suffixIndex + 1)..];
subtype = subtype[..suffixIndex];
}
return new HttpMediaType(type, subtype, suffix, args);
}
public override string ToString() {
StringBuilder sb = new();
sb.AppendFormat(@"{0}/{1}", Type, Subtype);
if(!string.IsNullOrWhiteSpace(Suffix))
sb.AppendFormat(@"+{0}", Suffix);
if(Params.Any())
sb.AppendFormat(@";{0}", string.Join(';', Params));
return sb.ToString();
}
public int CompareTo(HttpMediaType? other) {
if(!other.HasValue)
return -1;
int type = Type.CompareTo(other.Value.Type);
if(type != 0)
return type;
int subtype = Subtype.CompareTo(other.Value.Subtype);
if(subtype != 0)
return subtype;
int suffix = Suffix.CompareTo(other.Value.Suffix);
if(suffix != 0)
return suffix;
int paramCount = Params.Count();
int args = paramCount - other.Value.Params.Count();
if(args != 0)
return args;
for(int i = 0; i < paramCount; ++i) {
args = Params.ElementAt(i).CompareTo(other.Value.Params.ElementAt(i));
if(args != 0)
return args;
}
return 0;
}
public bool Equals(HttpMediaType? other) {
if(!other.HasValue)
return false;
if(!Type.Equals(other.Value.Type) || !Subtype.Equals(other.Value.Subtype) || !Suffix.Equals(other.Value.Suffix))
return false;
int paramCount = Params.Count();
if(paramCount != other.Value.Params.Count())
return false;
for(int i = 0; i < paramCount; ++i)
if(!Params.ElementAt(i).Equals(other.Value.Params.ElementAt(i)))
return false;
return true;
}
public readonly struct Param : IComparable<Param?>, IEquatable<Param?> {
public const string CHARSET = @"charset";
public static readonly Param ASCII = new(CHARSET, @"us-ascii");
public static readonly Param UTF8 = new(CHARSET, @"utf-8");
public string Name { get; }
public string Value { get; }
public Param(string name, string value) {
Name = name ?? throw new ArgumentNullException(nameof(name));
Value = value ?? throw new ArgumentNullException(nameof(name));
}
public override string ToString() {
return string.Format(@"{0}={1}", Name, Value);
}
public static explicit operator Param(string paramStr) => Parse(paramStr);
public static Param Parse(string paramStr) {
string[] parts = (paramStr ?? throw new ArgumentNullException(nameof(paramStr))).Split('=', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
return new Param(parts[0], parts[1]);
}
public int CompareTo(Param? other) {
if(!other.HasValue)
return -1;
int name = Name.CompareTo(other.Value.Name);
return name != 0
? name
: Value.CompareTo(other.Value.Value);
}
public bool Equals(Param? other) {
return other.HasValue && Name.Equals(other.Value.Name) && Value.Equals(other.Value.Value);
}
}
}
}

@ -0,0 +1,46 @@
using Hamakaze.Headers;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Hamakaze {
public abstract class HttpMessage : IDisposable {
public abstract string ProtocolVersion { get; }
public abstract IEnumerable<HttpHeader> Headers { get; }
public abstract Stream Body { get; }
public virtual bool HasBody => Body != null;
protected bool OwnsBodyStream { get; set; }
public virtual IEnumerable<HttpHeader> GetHeader(string header) {
header = HttpHeader.NormaliseName(header);
return Headers.Where(h => h.Name == header);
}
public virtual bool HasHeader(string header) {
header = HttpHeader.NormaliseName(header);
return Headers.Any(h => h.Name == header);
}
public virtual string GetHeaderLine(string header) {
return string.Join(@", ", GetHeader(header).Select(h => h.Value));
}
private bool IsDisposed;
~HttpMessage()
=> DoDispose();
public void Dispose() {
DoDispose();
GC.SuppressFinalize(this);
}
protected void DoDispose() {
if(IsDisposed)
return;
IsDisposed = true;
if(OwnsBodyStream && Body != null)
Body.Dispose();
}
}
}

@ -0,0 +1,190 @@
using Hamakaze.Headers;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace Hamakaze {
public class HttpRequestMessage : HttpMessage {
public const string GET = @"GET";
public const string PUT = @"PUT";
public const string HEAD = @"HEAD";
public const string POST = @"POST";
public const string DELETE = @"DELETE";
public override string ProtocolVersion => @"1.1";
public string Method { get; }
public string RequestTarget { get; }
public bool IsSecure { get; }
public string Host { get; }
public ushort Port { get; }
public bool IsDefaultPort { get; }
public override IEnumerable<HttpHeader> Headers => HeaderList;
private List<HttpHeader> HeaderList { get; } = new();
private Stream BodyStream { get; set; }
public override Stream Body {
get {
if(BodyStream == null) {
OwnsBodyStream = true;
SetBody(new MemoryStream());
}
return BodyStream;
}
}
private static readonly string[] HEADERS_READONLY = new[] {
HttpHostHeader.NAME, HttpContentLengthHeader.NAME,
};
private static readonly string[] HEADERS_SINGLE = new[] {
HttpUserAgentHeader.NAME, HttpConnectionHeader.NAME, HttpAcceptEncodingHeader.NAME,
};
public IEnumerable<HttpEncoding> AcceptedEncodings {
get => HeaderList.Where(x => x.Name == HttpAcceptEncodingHeader.NAME).Cast<HttpAcceptEncodingHeader>().FirstOrDefault()?.Encodings
?? Enumerable.Empty<HttpEncoding>();
set {
HeaderList.RemoveAll(x => x.Name == HttpAcceptEncodingHeader.NAME);
HeaderList.Add(new HttpAcceptEncodingHeader(value));
}
}
public string UserAgent {
get => HeaderList.FirstOrDefault(x => x.Name == HttpUserAgentHeader.NAME)?.Value.ToString()
?? string.Empty;
set {
HeaderList.RemoveAll(x => x.Name == HttpUserAgentHeader.NAME);
HeaderList.Add(new HttpUserAgentHeader(value));
}
}
public string Connection {
get => HeaderList.FirstOrDefault(x => x.Name == HttpConnectionHeader.NAME)?.Value.ToString()
?? string.Empty;
set {
HeaderList.RemoveAll(x => x.Name == HttpConnectionHeader.NAME);
HeaderList.Add(new HttpConnectionHeader(value));
}
}
public HttpMediaType ContentType {
get => HeaderList.Where(x => x.Name == HttpContentTypeHeader.NAME).Cast<HttpContentTypeHeader>().FirstOrDefault()?.MediaType
?? HttpMediaType.OctetStream;
set {
HeaderList.RemoveAll(x => x.Name == HttpContentTypeHeader.NAME);
HeaderList.Add(new HttpContentTypeHeader(value));
}
}
public HttpRequestMessage(string method, string uri) : this(
method, new Uri(uri)
) {}
public const ushort HTTP = 80;
public const ushort HTTPS = 443;
public HttpRequestMessage(string method, Uri uri) {
Method = method ?? throw new ArgumentNullException(nameof(method));
RequestTarget = uri.PathAndQuery;
IsSecure = uri.Scheme.Equals(@"https", StringComparison.InvariantCultureIgnoreCase);
Host = uri.Host;