Merge branch 'master' of git.flash.moe:flash/hamakaze
This commit is contained in:
commit
ee67efd29e
22 changed files with 1266 additions and 95 deletions
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -9,9 +9,10 @@ namespace Hamakaze.Headers {
|
||||||
|
|
||||||
public const string CLOSE = @"close";
|
public const string CLOSE = @"close";
|
||||||
public const string KEEP_ALIVE = @"keep-alive";
|
public const string KEEP_ALIVE = @"keep-alive";
|
||||||
|
public const string UPGRADE = @"upgrade";
|
||||||
|
|
||||||
public HttpConnectionHeader(string mode) {
|
public HttpConnectionHeader(string mode) {
|
||||||
Value = mode ?? throw new ArgumentNullException(nameof(mode));
|
Value = (mode ?? throw new ArgumentNullException(nameof(mode))).ToLowerInvariant();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,22 @@
|
||||||
using Hamakaze.Headers;
|
using Hamakaze.Headers;
|
||||||
|
using Hamakaze.WebSocket;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace Hamakaze {
|
namespace Hamakaze {
|
||||||
public class HttpClient : IDisposable {
|
public class HttpClient : IDisposable {
|
||||||
public const string PRODUCT_STRING = @"HMKZ";
|
public const string PRODUCT_STRING = @"HMKZ";
|
||||||
public const string VERSION_MAJOR = @"1";
|
public const string VERSION_MAJOR = @"1";
|
||||||
public const string VERSION_MINOR = @"0";
|
public const string VERSION_MINOR = @"1";
|
||||||
public const string USER_AGENT = PRODUCT_STRING + @"/" + VERSION_MAJOR + @"." + VERSION_MINOR;
|
public const string USER_AGENT = PRODUCT_STRING + @"/" + VERSION_MAJOR + @"." + VERSION_MINOR;
|
||||||
|
|
||||||
|
private const string WS_GUID = @"258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||||
|
private const string WS_PROTO = @"websocket";
|
||||||
|
private const int WS_RNG = 16;
|
||||||
|
|
||||||
private static HttpClient InstanceValue { get; set; }
|
private static HttpClient InstanceValue { get; set; }
|
||||||
public static HttpClient Instance {
|
public static HttpClient Instance {
|
||||||
get {
|
get {
|
||||||
|
@ -47,6 +55,7 @@ namespace Hamakaze {
|
||||||
request.UserAgent = DefaultUserAgent;
|
request.UserAgent = DefaultUserAgent;
|
||||||
if(!request.HasHeader(HttpAcceptEncodingHeader.NAME))
|
if(!request.HasHeader(HttpAcceptEncodingHeader.NAME))
|
||||||
request.AcceptedEncodings = AcceptedEncodings;
|
request.AcceptedEncodings = AcceptedEncodings;
|
||||||
|
if(!request.HasHeader(HttpConnectionHeader.NAME))
|
||||||
request.Connection = ReuseConnections ? HttpConnectionHeader.KEEP_ALIVE : HttpConnectionHeader.CLOSE;
|
request.Connection = ReuseConnections ? HttpConnectionHeader.KEEP_ALIVE : HttpConnectionHeader.CLOSE;
|
||||||
|
|
||||||
HttpTask task = new(Connections, request, disposeRequest, disposeResponse);
|
HttpTask task = new(Connections, request, disposeRequest, disposeResponse);
|
||||||
|
@ -85,6 +94,131 @@ namespace Hamakaze {
|
||||||
RunTask(CreateTask(request, onComplete, onError, onCancel, onDownloadProgress, onUploadProgress, onStateChange, disposeRequest, disposeResponse));
|
RunTask(CreateTask(request, onComplete, onError, onCancel, onDownloadProgress, onUploadProgress, onStateChange, disposeRequest, disposeResponse));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void CreateWsClient(
|
||||||
|
string url,
|
||||||
|
Action<WsClient> onOpen,
|
||||||
|
Action<WsMessage> onMessage,
|
||||||
|
Action<Exception> onError,
|
||||||
|
IEnumerable<string> protocols = null,
|
||||||
|
Action<HttpResponseMessage> onResponse = null,
|
||||||
|
bool disposeRequest = true,
|
||||||
|
bool disposeResponse = true
|
||||||
|
) => CreateWsConnection(
|
||||||
|
url,
|
||||||
|
conn => onOpen(new WsClient(conn, onMessage, onError)),
|
||||||
|
onError,
|
||||||
|
protocols,
|
||||||
|
onResponse,
|
||||||
|
disposeRequest,
|
||||||
|
disposeResponse
|
||||||
|
);
|
||||||
|
|
||||||
|
public void CreateWsClient(
|
||||||
|
HttpRequestMessage request,
|
||||||
|
Action<WsClient> onOpen,
|
||||||
|
Action<WsMessage> onMessage,
|
||||||
|
Action<Exception> onError,
|
||||||
|
IEnumerable<string> protocols = null,
|
||||||
|
Action<HttpResponseMessage> onResponse = null,
|
||||||
|
bool disposeRequest = true,
|
||||||
|
bool disposeResponse = true
|
||||||
|
) => CreateWsConnection(
|
||||||
|
request,
|
||||||
|
conn => onOpen(new WsClient(conn, onMessage, onError)),
|
||||||
|
onError,
|
||||||
|
protocols,
|
||||||
|
onResponse,
|
||||||
|
disposeRequest,
|
||||||
|
disposeResponse
|
||||||
|
);
|
||||||
|
|
||||||
|
public void CreateWsConnection(
|
||||||
|
string url,
|
||||||
|
Action<WsConnection> onOpen,
|
||||||
|
Action<Exception> onError,
|
||||||
|
IEnumerable<string> protocols = null,
|
||||||
|
Action<HttpResponseMessage> onResponse = null,
|
||||||
|
bool disposeRequest = true,
|
||||||
|
bool disposeResponse = true
|
||||||
|
) => CreateWsConnection(
|
||||||
|
new HttpRequestMessage(@"GET", url),
|
||||||
|
onOpen,
|
||||||
|
onError,
|
||||||
|
protocols,
|
||||||
|
onResponse,
|
||||||
|
disposeRequest,
|
||||||
|
disposeResponse
|
||||||
|
);
|
||||||
|
|
||||||
|
public void CreateWsConnection(
|
||||||
|
HttpRequestMessage request,
|
||||||
|
Action<WsConnection> onOpen,
|
||||||
|
Action<Exception> onError,
|
||||||
|
IEnumerable<string> protocols = null,
|
||||||
|
Action<HttpResponseMessage> onResponse = null,
|
||||||
|
bool disposeRequest = true,
|
||||||
|
bool disposeResponse = true
|
||||||
|
) {
|
||||||
|
string key = Convert.ToBase64String(RandomNumberGenerator.GetBytes(WS_RNG));
|
||||||
|
|
||||||
|
request.Connection = HttpConnectionHeader.UPGRADE;
|
||||||
|
request.SetHeader(@"Cache-Control", @"no-cache");
|
||||||
|
request.SetHeader(@"Upgrade", WS_PROTO);
|
||||||
|
request.SetHeader(@"Sec-WebSocket-Key", key);
|
||||||
|
request.SetHeader(@"Sec-WebSocket-Version", @"13");
|
||||||
|
|
||||||
|
if(protocols?.Any() == true)
|
||||||
|
request.SetHeader(@"Sec-WebSocket-Protocol", string.Join(@", ", protocols));
|
||||||
|
|
||||||
|
SendRequest(
|
||||||
|
request,
|
||||||
|
(t, res) => {
|
||||||
|
try {
|
||||||
|
onResponse?.Invoke(res);
|
||||||
|
|
||||||
|
if(res.ProtocolVersion.CompareTo(@"1.1") < 0)
|
||||||
|
throw new HttpUpgradeProtocolVersionException(@"1.1", res.ProtocolVersion);
|
||||||
|
|
||||||
|
if(res.StatusCode != 101)
|
||||||
|
throw new HttpUpgradeUnexpectedStatusException(res.StatusCode);
|
||||||
|
|
||||||
|
if(res.Connection != HttpConnectionHeader.UPGRADE)
|
||||||
|
throw new HttpUpgradeUnexpectedHeaderException(
|
||||||
|
@"Connection",
|
||||||
|
HttpConnectionHeader.UPGRADE,
|
||||||
|
res.Connection
|
||||||
|
);
|
||||||
|
|
||||||
|
string hUpgrade = res.GetHeaderLine(@"Upgrade");
|
||||||
|
if(hUpgrade != WS_PROTO)
|
||||||
|
throw new HttpUpgradeUnexpectedHeaderException(@"Upgrade", WS_PROTO, hUpgrade);
|
||||||
|
|
||||||
|
string serverHashStr = res.GetHeaderLine(@"Sec-WebSocket-Accept");
|
||||||
|
byte[] expectHash = SHA1.HashData(Encoding.ASCII.GetBytes(key + WS_GUID));
|
||||||
|
|
||||||
|
if(string.IsNullOrWhiteSpace(serverHashStr))
|
||||||
|
throw new HttpUpgradeUnexpectedHeaderException(
|
||||||
|
@"Sec-WebSocket-Accept",
|
||||||
|
Convert.ToBase64String(expectHash),
|
||||||
|
serverHashStr
|
||||||
|
);
|
||||||
|
|
||||||
|
byte[] givenHash = Convert.FromBase64String(serverHashStr.Trim());
|
||||||
|
|
||||||
|
if(!expectHash.SequenceEqual(givenHash))
|
||||||
|
throw new HttpUpgradeInvalidHashException(Convert.ToBase64String(expectHash), serverHashStr);
|
||||||
|
|
||||||
|
onOpen(t.Connection.ToWebSocket());
|
||||||
|
} catch(Exception ex) {
|
||||||
|
onError(ex);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(t, ex) => onError(ex),
|
||||||
|
disposeRequest: disposeRequest,
|
||||||
|
disposeResponse: disposeResponse
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public static void Send(
|
public static void Send(
|
||||||
HttpRequestMessage request,
|
HttpRequestMessage request,
|
||||||
Action<HttpTask, HttpResponseMessage> onComplete = null,
|
Action<HttpTask, HttpResponseMessage> onComplete = null,
|
||||||
|
@ -95,9 +229,57 @@ namespace Hamakaze {
|
||||||
Action<HttpTask, HttpTask.TaskState> onStateChange = null,
|
Action<HttpTask, HttpTask.TaskState> onStateChange = null,
|
||||||
bool disposeRequest = true,
|
bool disposeRequest = true,
|
||||||
bool disposeResponse = true
|
bool disposeResponse = true
|
||||||
) {
|
) => Instance.SendRequest(
|
||||||
Instance.SendRequest(request, onComplete, onError, onCancel, onDownloadProgress, onUploadProgress, onStateChange, disposeRequest, disposeResponse);
|
request,
|
||||||
}
|
onComplete,
|
||||||
|
onError,
|
||||||
|
onCancel,
|
||||||
|
onDownloadProgress,
|
||||||
|
onUploadProgress,
|
||||||
|
onStateChange,
|
||||||
|
disposeRequest,
|
||||||
|
disposeResponse
|
||||||
|
);
|
||||||
|
|
||||||
|
public static void Connect(
|
||||||
|
string url,
|
||||||
|
Action<WsClient> onOpen,
|
||||||
|
Action<WsMessage> onMessage,
|
||||||
|
Action<Exception> onError,
|
||||||
|
IEnumerable<string> protocols = null,
|
||||||
|
Action<HttpResponseMessage> onResponse = null,
|
||||||
|
bool disposeRequest = true,
|
||||||
|
bool disposeResponse = true
|
||||||
|
) => Instance.CreateWsClient(
|
||||||
|
url,
|
||||||
|
onOpen,
|
||||||
|
onMessage,
|
||||||
|
onError,
|
||||||
|
protocols,
|
||||||
|
onResponse,
|
||||||
|
disposeRequest,
|
||||||
|
disposeResponse
|
||||||
|
);
|
||||||
|
|
||||||
|
public static void Connect(
|
||||||
|
HttpRequestMessage request,
|
||||||
|
Action<WsClient> onOpen,
|
||||||
|
Action<WsMessage> onMessage,
|
||||||
|
Action<Exception> onError,
|
||||||
|
IEnumerable<string> protocols = null,
|
||||||
|
Action<HttpResponseMessage> onResponse = null,
|
||||||
|
bool disposeRequest = true,
|
||||||
|
bool disposeResponse = true
|
||||||
|
) => Instance.CreateWsClient(
|
||||||
|
request,
|
||||||
|
onOpen,
|
||||||
|
onMessage,
|
||||||
|
onError,
|
||||||
|
protocols,
|
||||||
|
onResponse,
|
||||||
|
disposeRequest,
|
||||||
|
disposeResponse
|
||||||
|
);
|
||||||
|
|
||||||
private bool IsDisposed;
|
private bool IsDisposed;
|
||||||
~HttpClient()
|
~HttpClient()
|
||||||
|
|
|
@ -4,27 +4,29 @@ using System.Net;
|
||||||
using System.Net.Security;
|
using System.Net.Security;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using System.Security.Authentication;
|
using System.Security.Authentication;
|
||||||
using System.Threading;
|
using Hamakaze.WebSocket;
|
||||||
|
|
||||||
namespace Hamakaze {
|
namespace Hamakaze {
|
||||||
public class HttpConnection : IDisposable {
|
public class HttpConnection : IDisposable {
|
||||||
public IPEndPoint EndPoint { get; }
|
public IPEndPoint EndPoint { get; }
|
||||||
public Stream Stream { get; }
|
public Stream Stream { get; }
|
||||||
|
|
||||||
public Socket Socket { get; }
|
private Socket Socket { get; }
|
||||||
public NetworkStream NetworkStream { get; }
|
|
||||||
public SslStream SslStream { get; }
|
private NetworkStream NetworkStream { get; }
|
||||||
|
private SslStream SslStream { get; }
|
||||||
|
|
||||||
public string Host { get; }
|
public string Host { get; }
|
||||||
public bool IsSecure { get; }
|
public bool IsSecure { get; }
|
||||||
|
|
||||||
public bool HasTimedOut => MaxRequests == 0 || (DateTimeOffset.Now - LastOperation) > MaxIdle;
|
public bool HasTimedOut => MaxRequests < 1 || (DateTimeOffset.Now - LastOperation) > MaxIdle;
|
||||||
|
|
||||||
public int MaxRequests { get; set; } = -1;
|
public int? MaxRequests { get; set; } = null;
|
||||||
public TimeSpan MaxIdle { get; set; } = TimeSpan.MaxValue;
|
public TimeSpan MaxIdle { get; set; } = TimeSpan.MaxValue;
|
||||||
public DateTimeOffset LastOperation { get; private set; } = DateTimeOffset.Now;
|
public DateTimeOffset LastOperation { get; private set; } = DateTimeOffset.Now;
|
||||||
|
|
||||||
public bool InUse { get; private set; }
|
public bool InUse { get; private set; }
|
||||||
|
public bool HasUpgraded { get; private set; }
|
||||||
|
|
||||||
public HttpConnection(string host, IPEndPoint endPoint, bool secure) {
|
public HttpConnection(string host, IPEndPoint endPoint, bool secure) {
|
||||||
Host = host ?? throw new ArgumentNullException(nameof(host));
|
Host = host ?? throw new ArgumentNullException(nameof(host));
|
||||||
|
@ -45,25 +47,41 @@ namespace Hamakaze {
|
||||||
if(IsSecure) {
|
if(IsSecure) {
|
||||||
SslStream = new SslStream(NetworkStream, false, (s, ce, ch, e) => e == SslPolicyErrors.None, null);
|
SslStream = new SslStream(NetworkStream, false, (s, ce, ch, e) => e == SslPolicyErrors.None, null);
|
||||||
Stream = SslStream;
|
Stream = SslStream;
|
||||||
SslStream.AuthenticateAsClient(Host, null, SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13, true);
|
SslStream.AuthenticateAsClient(
|
||||||
|
Host,
|
||||||
|
null,
|
||||||
|
SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13,
|
||||||
|
true
|
||||||
|
);
|
||||||
} else
|
} else
|
||||||
Stream = NetworkStream;
|
Stream = NetworkStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void MarkUsed() {
|
public void MarkUsed() {
|
||||||
LastOperation = DateTimeOffset.Now;
|
LastOperation = DateTimeOffset.Now;
|
||||||
if(MaxRequests > 0)
|
if(MaxRequests != null)
|
||||||
--MaxRequests;
|
--MaxRequests;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Acquire() {
|
public bool Acquire() {
|
||||||
return !InUse && (InUse = true);
|
return !HasUpgraded && !InUse && (InUse = true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Release() {
|
public void Release() {
|
||||||
InUse = false;
|
InUse = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public WsConnection ToWebSocket() {
|
||||||
|
if(HasUpgraded)
|
||||||
|
throw new HttpConnectionAlreadyUpgradedException();
|
||||||
|
HasUpgraded = true;
|
||||||
|
|
||||||
|
NetworkStream.ReadTimeout = -1;
|
||||||
|
SslStream.ReadTimeout = -1;
|
||||||
|
|
||||||
|
return new WsConnection(Stream);
|
||||||
|
}
|
||||||
|
|
||||||
private bool IsDisposed;
|
private bool IsDisposed;
|
||||||
~HttpConnection()
|
~HttpConnection()
|
||||||
=> DoDispose();
|
=> DoDispose();
|
||||||
|
@ -75,6 +93,8 @@ namespace Hamakaze {
|
||||||
if(IsDisposed)
|
if(IsDisposed)
|
||||||
return;
|
return;
|
||||||
IsDisposed = true;
|
IsDisposed = true;
|
||||||
|
|
||||||
|
if(!HasUpgraded)
|
||||||
Stream.Dispose();
|
Stream.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,32 @@ namespace Hamakaze {
|
||||||
public HttpException(string message) : base(message) { }
|
public HttpException(string message) : base(message) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class HttpUpgradeException : HttpException {
|
||||||
|
public HttpUpgradeException(string message) : base(message) { }
|
||||||
|
}
|
||||||
|
public class HttpUpgradeProtocolVersionException : HttpUpgradeException {
|
||||||
|
public HttpUpgradeProtocolVersionException(string expectedVersion, string givenVersion)
|
||||||
|
: base($@"Server HTTP version ({givenVersion}) is lower than what is expected {expectedVersion}.") { }
|
||||||
|
}
|
||||||
|
public class HttpUpgradeUnexpectedStatusException : HttpUpgradeException {
|
||||||
|
public HttpUpgradeUnexpectedStatusException(int statusCode) : base($@"Expected HTTP status code 101, got {statusCode}.") { }
|
||||||
|
}
|
||||||
|
public class HttpUpgradeUnexpectedHeaderException : HttpUpgradeException {
|
||||||
|
public HttpUpgradeUnexpectedHeaderException(string header, string expected, string given)
|
||||||
|
: base($@"Unexpected {header} header value ""{given}"", expected ""{expected}"".") { }
|
||||||
|
}
|
||||||
|
public class HttpUpgradeInvalidHashException : HttpUpgradeException {
|
||||||
|
public HttpUpgradeInvalidHashException(string expected, string given)
|
||||||
|
: base($@"Server sent invalid hash ""{given}"", expected ""{expected}"".") { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HttpConnectionException : HttpException {
|
||||||
|
public HttpConnectionException(string message) : base(message) { }
|
||||||
|
}
|
||||||
|
public class HttpConnectionAlreadyUpgradedException : HttpConnectionException {
|
||||||
|
public HttpConnectionAlreadyUpgradedException() : base(@"This connection has already been upgraded.") { }
|
||||||
|
}
|
||||||
|
|
||||||
public class HttpConnectionManagerException : HttpException {
|
public class HttpConnectionManagerException : HttpException {
|
||||||
public HttpConnectionManagerException(string message) : base(message) { }
|
public HttpConnectionManagerException(string message) : base(message) { }
|
||||||
}
|
}
|
||||||
|
@ -12,6 +38,13 @@ namespace Hamakaze {
|
||||||
public HttpConnectionManagerLockException() : base(@"Failed to lock the connection manager in time.") { }
|
public HttpConnectionManagerLockException() : base(@"Failed to lock the connection manager in time.") { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class HttpRequestMessageException : HttpException {
|
||||||
|
public HttpRequestMessageException(string message) : base(message) { }
|
||||||
|
}
|
||||||
|
public class HttpRequestMessageStreamException : HttpRequestMessageException {
|
||||||
|
public HttpRequestMessageStreamException() : base(@"Provided Stream is not writable.") { }
|
||||||
|
}
|
||||||
|
|
||||||
public class HttpTaskException : HttpException {
|
public class HttpTaskException : HttpException {
|
||||||
public HttpTaskException(string message) : base(message) { }
|
public HttpTaskException(string message) : base(message) { }
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,7 +92,8 @@ namespace Hamakaze {
|
||||||
public HttpRequestMessage(string method, Uri uri) {
|
public HttpRequestMessage(string method, Uri uri) {
|
||||||
Method = method ?? throw new ArgumentNullException(nameof(method));
|
Method = method ?? throw new ArgumentNullException(nameof(method));
|
||||||
RequestTarget = uri.PathAndQuery;
|
RequestTarget = uri.PathAndQuery;
|
||||||
IsSecure = uri.Scheme.Equals(@"https", StringComparison.InvariantCultureIgnoreCase);
|
IsSecure = uri.Scheme.Equals(@"https", StringComparison.InvariantCultureIgnoreCase)
|
||||||
|
|| uri.Scheme.Equals(@"wss", StringComparison.InvariantCultureIgnoreCase);
|
||||||
Host = uri.Host;
|
Host = uri.Host;
|
||||||
ushort defaultPort = (IsSecure ? HTTPS : HTTP);
|
ushort defaultPort = (IsSecure ? HTTPS : HTTP);
|
||||||
Port = uri.Port == -1 ? defaultPort : (ushort)uri.Port;
|
Port = uri.Port == -1 ? defaultPort : (ushort)uri.Port;
|
||||||
|
@ -157,6 +158,9 @@ namespace Hamakaze {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void WriteTo(Stream stream, Action<long, long> onProgress = null) {
|
public void WriteTo(Stream stream, Action<long, long> onProgress = null) {
|
||||||
|
if(!stream.CanWrite)
|
||||||
|
throw new HttpRequestMessageStreamException();
|
||||||
|
|
||||||
using(StreamWriter sw = new(stream, new ASCIIEncoding(), leaveOpen: true)) {
|
using(StreamWriter sw = new(stream, new ASCIIEncoding(), leaveOpen: true)) {
|
||||||
sw.NewLine = "\r\n";
|
sw.NewLine = "\r\n";
|
||||||
sw.Write(Method);
|
sw.Write(Method);
|
||||||
|
|
|
@ -124,10 +124,10 @@ namespace Hamakaze {
|
||||||
using MemoryStream ms = new();
|
using MemoryStream ms = new();
|
||||||
int byt; ushort lastTwo = 0;
|
int byt; ushort lastTwo = 0;
|
||||||
|
|
||||||
for(; ; ) {
|
for(;;) {
|
||||||
byt = stream.ReadByte();
|
byt = stream.ReadByte();
|
||||||
if(byt == -1 && ms.Length == 0)
|
if(byt == -1 && ms.Length == 0)
|
||||||
return null;
|
throw new IOException(@"readLine: There is no data.");
|
||||||
|
|
||||||
ms.WriteByte((byte)byt);
|
ms.WriteByte((byte)byt);
|
||||||
|
|
||||||
|
@ -151,7 +151,7 @@ namespace Hamakaze {
|
||||||
if(line == null)
|
if(line == null)
|
||||||
throw new IOException(@"Failed to read initial HTTP header.");
|
throw new IOException(@"Failed to read initial HTTP header.");
|
||||||
if(!line.StartsWith(@"HTTP/"))
|
if(!line.StartsWith(@"HTTP/"))
|
||||||
throw new IOException(@"Response is not a valid HTTP message.");
|
throw new IOException($@"Response is not a valid HTTP message: {line}.");
|
||||||
string[] parts = line[5..].Split(' ', 3);
|
string[] parts = line[5..].Split(' ', 3);
|
||||||
if(!int.TryParse(parts.ElementAtOrDefault(1), out int statusCode))
|
if(!int.TryParse(parts.ElementAtOrDefault(1), out int statusCode))
|
||||||
throw new IOException(@"Invalid HTTP status code format.");
|
throw new IOException(@"Invalid HTTP status code format.");
|
||||||
|
@ -238,11 +238,11 @@ namespace Hamakaze {
|
||||||
readBuffer(chunkLength);
|
readBuffer(chunkLength);
|
||||||
readLine();
|
readLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
readLine();
|
readLine();
|
||||||
} else if(contentLength != 0) {
|
} else if(contentLength != 0) {
|
||||||
body = new MemoryStream();
|
body = new MemoryStream();
|
||||||
readBuffer(contentLength);
|
readBuffer(contentLength);
|
||||||
readLine();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(body != null)
|
if(body != null)
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
using Hamakaze.Headers;
|
using Hamakaze.Headers;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
|
@ -25,7 +24,7 @@ namespace Hamakaze {
|
||||||
private HttpConnectionManager Connections { get; }
|
private HttpConnectionManager Connections { get; }
|
||||||
|
|
||||||
private IEnumerable<IPAddress> Addresses { get; set; }
|
private IEnumerable<IPAddress> Addresses { get; set; }
|
||||||
private HttpConnection Connection { get; set; }
|
public HttpConnection Connection { get; private set; }
|
||||||
|
|
||||||
public bool DisposeRequest { get; set; }
|
public bool DisposeRequest { get; set; }
|
||||||
public bool DisposeResponse { get; set; }
|
public bool DisposeResponse { get; set; }
|
||||||
|
@ -70,6 +69,7 @@ namespace Hamakaze {
|
||||||
if(IsCancelled)
|
if(IsCancelled)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
try {
|
||||||
switch(State) {
|
switch(State) {
|
||||||
case TaskState.Initial:
|
case TaskState.Initial:
|
||||||
State = TaskState.Lookup;
|
State = TaskState.Lookup;
|
||||||
|
@ -96,7 +96,10 @@ namespace Hamakaze {
|
||||||
Request?.Dispose();
|
Request?.Dispose();
|
||||||
return false;
|
return false;
|
||||||
default:
|
default:
|
||||||
Error(new HttpTaskInvalidStateException());
|
throw new HttpTaskInvalidStateException();
|
||||||
|
}
|
||||||
|
} catch(Exception ex) {
|
||||||
|
Error(ex);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,26 +107,19 @@ namespace Hamakaze {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DoLookup() {
|
private void DoLookup() {
|
||||||
try {
|
|
||||||
Addresses = Dns.GetHostAddresses(Request.Host);
|
Addresses = Dns.GetHostAddresses(Request.Host);
|
||||||
} catch(Exception ex) {
|
|
||||||
Error(ex);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!Addresses.Any())
|
if(!Addresses.Any())
|
||||||
Error(new HttpTaskNoAddressesException());
|
throw new HttpTaskNoAddressesException();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DoRequest() {
|
private void DoRequest() {
|
||||||
Exception exception = null;
|
Queue<IPAddress> addresses = new(Addresses);
|
||||||
|
|
||||||
try {
|
while(addresses.TryDequeue(out IPAddress addr)) {
|
||||||
foreach(IPAddress addr in Addresses) {
|
|
||||||
int tries = 0;
|
int tries = 0;
|
||||||
IPEndPoint endPoint = new(addr, Request.Port);
|
IPEndPoint endPoint = new(addr, Request.Port);
|
||||||
|
|
||||||
exception = null;
|
|
||||||
Connection = Connections.GetConnection(Request.Host, endPoint, Request.IsSecure);
|
Connection = Connections.GetConnection(Request.Host, endPoint, Request.IsSecure);
|
||||||
|
|
||||||
retry:
|
retry:
|
||||||
|
@ -131,41 +127,32 @@ namespace Hamakaze {
|
||||||
try {
|
try {
|
||||||
Request.WriteTo(Connection.Stream, (p, t) => OnUploadProgress?.Invoke(this, p, t));
|
Request.WriteTo(Connection.Stream, (p, t) => OnUploadProgress?.Invoke(this, p, t));
|
||||||
break;
|
break;
|
||||||
} catch(IOException ex) {
|
} catch(HttpRequestMessageStreamException) {
|
||||||
Connection.Dispose();
|
Connection.Dispose();
|
||||||
Connection = Connections.GetConnection(Request.Host, endPoint, Request.IsSecure);
|
Connection = Connections.GetConnection(Request.Host, endPoint, Request.IsSecure);
|
||||||
|
|
||||||
if(tries < 2)
|
if(tries < 2)
|
||||||
goto retry;
|
goto retry;
|
||||||
|
|
||||||
exception = ex;
|
if(!addresses.Any())
|
||||||
continue;
|
throw;
|
||||||
} finally {
|
} finally {
|
||||||
Connection.MarkUsed();
|
Connection.MarkUsed();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch(Exception ex) {
|
|
||||||
Error(ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(exception != null)
|
if(Connection == null)
|
||||||
Error(exception);
|
throw new HttpTaskNoConnectionException();
|
||||||
else if(Connection == null)
|
|
||||||
Error(new HttpTaskNoConnectionException());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DoResponse() {
|
private void DoResponse() {
|
||||||
try {
|
|
||||||
Response = HttpResponseMessage.ReadFrom(Connection.Stream, (p, t) => OnDownloadProgress?.Invoke(this, p, t));
|
Response = HttpResponseMessage.ReadFrom(Connection.Stream, (p, t) => OnDownloadProgress?.Invoke(this, p, t));
|
||||||
} catch(Exception ex) {
|
|
||||||
Error(ex);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(Response.Connection == HttpConnectionHeader.CLOSE)
|
if(Response.Connection == HttpConnectionHeader.CLOSE
|
||||||
|
|| Response.ProtocolVersion.CompareTo(@"1.1") < 0)
|
||||||
Connection.Dispose();
|
Connection.Dispose();
|
||||||
if(Response == null)
|
if(Response == null)
|
||||||
Error(new HttpTaskRequestFailedException());
|
throw new HttpTaskRequestFailedException();
|
||||||
|
|
||||||
HttpKeepAliveHeader hkah = Response.Headers.Where(x => x.Name == HttpKeepAliveHeader.NAME).Cast<HttpKeepAliveHeader>().FirstOrDefault();
|
HttpKeepAliveHeader hkah = Response.Headers.Where(x => x.Name == HttpKeepAliveHeader.NAME).Cast<HttpKeepAliveHeader>().FirstOrDefault();
|
||||||
if(hkah != null) {
|
if(hkah != null) {
|
||||||
|
|
5
Hamakaze/WebSocket/IHasBinaryData.cs
Normal file
5
Hamakaze/WebSocket/IHasBinaryData.cs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
namespace Hamakaze.WebSocket {
|
||||||
|
public interface IHasBinaryData {
|
||||||
|
byte[] Data { get; }
|
||||||
|
}
|
||||||
|
}
|
11
Hamakaze/WebSocket/WsBinaryMessage.cs
Normal file
11
Hamakaze/WebSocket/WsBinaryMessage.cs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Hamakaze.WebSocket {
|
||||||
|
public class WsBinaryMessage : WsMessage, IHasBinaryData {
|
||||||
|
public byte[] Data { get; }
|
||||||
|
|
||||||
|
public WsBinaryMessage(byte[] data) {
|
||||||
|
Data = data ?? Array.Empty<byte>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
Hamakaze/WebSocket/WsBufferedSend.cs
Normal file
36
Hamakaze/WebSocket/WsBufferedSend.cs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Hamakaze.WebSocket {
|
||||||
|
public class WsBufferedSend : IDisposable {
|
||||||
|
private WsConnection Connection { get; }
|
||||||
|
|
||||||
|
internal WsBufferedSend(WsConnection conn) {
|
||||||
|
Connection = conn ?? throw new ArgumentNullException(nameof(conn));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendPart(ReadOnlySpan<byte> data)
|
||||||
|
=> Connection.WriteFrame(WsOpcode.DataBinary, data, false);
|
||||||
|
|
||||||
|
public void SendFinalPart(ReadOnlySpan<byte> data)
|
||||||
|
=> Connection.WriteFrame(WsOpcode.DataBinary, data, true);
|
||||||
|
|
||||||
|
private bool IsDisposed;
|
||||||
|
|
||||||
|
~WsBufferedSend() {
|
||||||
|
DoDispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() {
|
||||||
|
DoDispose();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DoDispose() {
|
||||||
|
if(IsDisposed)
|
||||||
|
return;
|
||||||
|
IsDisposed = true;
|
||||||
|
|
||||||
|
Connection.EndBufferedSend();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
306
Hamakaze/WebSocket/WsClient.cs
Normal file
306
Hamakaze/WebSocket/WsClient.cs
Normal file
|
@ -0,0 +1,306 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
// todo: sending errors as fake close messages
|
||||||
|
|
||||||
|
namespace Hamakaze.WebSocket {
|
||||||
|
public class WsClient : IDisposable {
|
||||||
|
public WsConnection Connection { get; }
|
||||||
|
public bool IsRunning { get; private set; } = true;
|
||||||
|
|
||||||
|
private Thread ReadThread { get; }
|
||||||
|
private Action<WsMessage> MessageHandler { get; }
|
||||||
|
private Action<Exception> ExceptionHandler { get; }
|
||||||
|
|
||||||
|
private Mutex SendLock { get; }
|
||||||
|
private const int TIMEOUT = 60000;
|
||||||
|
|
||||||
|
public WsClient(
|
||||||
|
WsConnection connection,
|
||||||
|
Action<WsMessage> messageHandler,
|
||||||
|
Action<Exception> exceptionHandler
|
||||||
|
) {
|
||||||
|
Connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
||||||
|
MessageHandler = messageHandler ?? throw new ArgumentNullException(nameof(messageHandler));
|
||||||
|
ExceptionHandler = exceptionHandler ?? throw new ArgumentNullException(nameof(exceptionHandler));
|
||||||
|
|
||||||
|
SendLock = new();
|
||||||
|
|
||||||
|
ReadThread = new(ReadThreadBody) { IsBackground = true };
|
||||||
|
ReadThread.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReadThreadBody() {
|
||||||
|
try {
|
||||||
|
while(IsRunning)
|
||||||
|
MessageHandler(Connection.Receive());
|
||||||
|
} catch(Exception ex) {
|
||||||
|
IsRunning = false;
|
||||||
|
ExceptionHandler(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Send(string text) {
|
||||||
|
try {
|
||||||
|
if(!SendLock.WaitOne(TIMEOUT))
|
||||||
|
throw new WsClientMutexFailedException();
|
||||||
|
Connection.Send(text);
|
||||||
|
} finally {
|
||||||
|
SendLock.ReleaseMutex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Send(object obj) {
|
||||||
|
if(obj == null)
|
||||||
|
throw new ArgumentNullException(nameof(obj));
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(!SendLock.WaitOne(TIMEOUT))
|
||||||
|
throw new WsClientMutexFailedException();
|
||||||
|
Connection.Send(obj.ToString());
|
||||||
|
} finally {
|
||||||
|
SendLock.ReleaseMutex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Send(ReadOnlySpan<byte> data) {
|
||||||
|
if(data == null)
|
||||||
|
throw new ArgumentNullException(nameof(data));
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(!SendLock.WaitOne(TIMEOUT))
|
||||||
|
throw new WsClientMutexFailedException();
|
||||||
|
Connection.Send(data);
|
||||||
|
} finally {
|
||||||
|
SendLock.ReleaseMutex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Send(byte[] buffer, int offset, int count) {
|
||||||
|
if(buffer == null)
|
||||||
|
throw new ArgumentNullException(nameof(buffer));
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(!SendLock.WaitOne(TIMEOUT))
|
||||||
|
throw new WsClientMutexFailedException();
|
||||||
|
Connection.Send(buffer.AsSpan(offset, count));
|
||||||
|
} finally {
|
||||||
|
SendLock.ReleaseMutex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Send(Action<WsBufferedSend> handler) {
|
||||||
|
if(handler == null)
|
||||||
|
throw new ArgumentNullException(nameof(handler));
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(!SendLock.WaitOne(TIMEOUT))
|
||||||
|
throw new WsClientMutexFailedException();
|
||||||
|
using(WsBufferedSend bs = Connection.BeginBufferedSend())
|
||||||
|
handler(bs);
|
||||||
|
} finally {
|
||||||
|
SendLock.ReleaseMutex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Ping() {
|
||||||
|
try {
|
||||||
|
if(!SendLock.WaitOne(TIMEOUT))
|
||||||
|
throw new WsClientMutexFailedException();
|
||||||
|
Connection.Ping();
|
||||||
|
} finally {
|
||||||
|
SendLock.ReleaseMutex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Ping(ReadOnlySpan<byte> data) {
|
||||||
|
if(data == null)
|
||||||
|
throw new ArgumentNullException(nameof(data));
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(!SendLock.WaitOne(TIMEOUT))
|
||||||
|
throw new WsClientMutexFailedException();
|
||||||
|
Connection.Ping(data);
|
||||||
|
} finally {
|
||||||
|
SendLock.ReleaseMutex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Ping(byte[] buffer, int offset, int length) {
|
||||||
|
if(buffer == null)
|
||||||
|
throw new ArgumentNullException(nameof(buffer));
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(!SendLock.WaitOne(TIMEOUT))
|
||||||
|
throw new WsClientMutexFailedException();
|
||||||
|
Connection.Ping(buffer.AsSpan(offset, length));
|
||||||
|
} finally {
|
||||||
|
SendLock.ReleaseMutex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Pong() {
|
||||||
|
try {
|
||||||
|
if(!SendLock.WaitOne(TIMEOUT))
|
||||||
|
throw new WsClientMutexFailedException();
|
||||||
|
Connection.Pong();
|
||||||
|
} finally {
|
||||||
|
SendLock.ReleaseMutex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Pong(ReadOnlySpan<byte> data) {
|
||||||
|
if(data == null)
|
||||||
|
throw new ArgumentNullException(nameof(data));
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(!SendLock.WaitOne(TIMEOUT))
|
||||||
|
throw new WsClientMutexFailedException();
|
||||||
|
Connection.Pong(data);
|
||||||
|
} finally {
|
||||||
|
SendLock.ReleaseMutex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Pong(byte[] buffer, int offset, int length) {
|
||||||
|
if(buffer == null)
|
||||||
|
throw new ArgumentNullException(nameof(buffer));
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(!SendLock.WaitOne(TIMEOUT))
|
||||||
|
throw new WsClientMutexFailedException();
|
||||||
|
Connection.Pong(buffer.AsSpan(offset, length));
|
||||||
|
} finally {
|
||||||
|
SendLock.ReleaseMutex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Close() {
|
||||||
|
try {
|
||||||
|
if(!SendLock.WaitOne(TIMEOUT))
|
||||||
|
throw new WsClientMutexFailedException();
|
||||||
|
Connection.Close(WsCloseReason.NormalClosure);
|
||||||
|
} finally {
|
||||||
|
SendLock.ReleaseMutex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CloseEmpty() {
|
||||||
|
try {
|
||||||
|
if(!SendLock.WaitOne(TIMEOUT))
|
||||||
|
throw new WsClientMutexFailedException();
|
||||||
|
Connection.CloseEmpty();
|
||||||
|
} finally {
|
||||||
|
SendLock.ReleaseMutex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Close(WsCloseReason opcode) {
|
||||||
|
try {
|
||||||
|
if(!SendLock.WaitOne(TIMEOUT))
|
||||||
|
throw new WsClientMutexFailedException();
|
||||||
|
Connection.Close(opcode);
|
||||||
|
} finally {
|
||||||
|
SendLock.ReleaseMutex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Close(string reason) {
|
||||||
|
if(reason == null)
|
||||||
|
throw new ArgumentNullException(nameof(reason));
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(!SendLock.WaitOne(TIMEOUT))
|
||||||
|
throw new WsClientMutexFailedException();
|
||||||
|
Connection.Close(WsCloseReason.NormalClosure, reason);
|
||||||
|
} finally {
|
||||||
|
SendLock.ReleaseMutex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Close(WsCloseReason opcode, string reason) {
|
||||||
|
if(reason == null)
|
||||||
|
throw new ArgumentNullException(nameof(reason));
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(!SendLock.WaitOne(TIMEOUT))
|
||||||
|
throw new WsClientMutexFailedException();
|
||||||
|
Connection.Close(opcode, reason);
|
||||||
|
} finally {
|
||||||
|
SendLock.ReleaseMutex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Close(ReadOnlySpan<byte> data) {
|
||||||
|
if(data == null)
|
||||||
|
throw new ArgumentNullException(nameof(data));
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(!SendLock.WaitOne(TIMEOUT))
|
||||||
|
throw new WsClientMutexFailedException();
|
||||||
|
Connection.Close(data);
|
||||||
|
} finally {
|
||||||
|
SendLock.ReleaseMutex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Close(byte[] buffer, int offset, int length) {
|
||||||
|
if(buffer == null)
|
||||||
|
throw new ArgumentNullException(nameof(buffer));
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(!SendLock.WaitOne(TIMEOUT))
|
||||||
|
throw new WsClientMutexFailedException();
|
||||||
|
Connection.Close(buffer.AsSpan(offset, length));
|
||||||
|
} finally {
|
||||||
|
SendLock.ReleaseMutex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Close(WsCloseReason opcode, ReadOnlySpan<byte> data) {
|
||||||
|
if(data == null)
|
||||||
|
throw new ArgumentNullException(nameof(data));
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(!SendLock.WaitOne(TIMEOUT))
|
||||||
|
throw new WsClientMutexFailedException();
|
||||||
|
Connection.Close(opcode, data);
|
||||||
|
} finally {
|
||||||
|
SendLock.ReleaseMutex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Close(WsCloseReason code, byte[] buffer, int offset, int length) {
|
||||||
|
if(buffer == null)
|
||||||
|
throw new ArgumentNullException(nameof(buffer));
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(!SendLock.WaitOne(TIMEOUT))
|
||||||
|
throw new WsClientMutexFailedException();
|
||||||
|
Connection.Close(code, buffer.AsSpan(offset, length));
|
||||||
|
} finally {
|
||||||
|
SendLock.ReleaseMutex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsDisposed;
|
||||||
|
|
||||||
|
~WsClient() {
|
||||||
|
DoDispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() {
|
||||||
|
DoDispose();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DoDispose() {
|
||||||
|
if(IsDisposed)
|
||||||
|
return;
|
||||||
|
IsDisposed = true;
|
||||||
|
|
||||||
|
SendLock.Dispose();
|
||||||
|
Connection.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
Hamakaze/WebSocket/WsCloseMessage.cs
Normal file
36
Hamakaze/WebSocket/WsCloseMessage.cs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Hamakaze.WebSocket {
|
||||||
|
public class WsCloseMessage : WsMessage, IHasBinaryData {
|
||||||
|
public WsCloseReason Reason { get; }
|
||||||
|
public string ReasonPhrase { get; }
|
||||||
|
public byte[] Data { get; }
|
||||||
|
|
||||||
|
public WsCloseMessage(WsCloseReason reason) {
|
||||||
|
Reason = reason;
|
||||||
|
ReasonPhrase = string.Empty;
|
||||||
|
Data = Array.Empty<byte>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public WsCloseMessage(byte[] data) {
|
||||||
|
if(data == null) {
|
||||||
|
Reason = WsCloseReason.NoStatus;
|
||||||
|
ReasonPhrase = string.Empty;
|
||||||
|
Data = Array.Empty<byte>();
|
||||||
|
} else {
|
||||||
|
Reason = (WsCloseReason)WsUtils.ToU16(data);
|
||||||
|
Data = data;
|
||||||
|
|
||||||
|
if(data.Length > 2)
|
||||||
|
try {
|
||||||
|
ReasonPhrase = Encoding.UTF8.GetString(data, 2, data.Length - 2);
|
||||||
|
} catch {
|
||||||
|
ReasonPhrase = string.Empty;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
ReasonPhrase = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
Hamakaze/WebSocket/WsCloseReason.cs
Normal file
16
Hamakaze/WebSocket/WsCloseReason.cs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
namespace Hamakaze.WebSocket {
|
||||||
|
public enum WsCloseReason : ushort {
|
||||||
|
NormalClosure = 1000,
|
||||||
|
GoingAway = 1001,
|
||||||
|
ProtocolError = 1002,
|
||||||
|
InvalidData = 1003,
|
||||||
|
NoStatus = 1005, // virtual -> no data in close frame
|
||||||
|
AbnormalClosure = 1006, // virtual -> connection dropped
|
||||||
|
MalformedData = 1007,
|
||||||
|
PolicyViolation = 1008,
|
||||||
|
FrameTooLarge = 1009,
|
||||||
|
MissingExtension = 1010,
|
||||||
|
UnexpectedCondition = 1011,
|
||||||
|
TlsHandshakeFailed = 1015, // virtual -> obvious
|
||||||
|
}
|
||||||
|
}
|
395
Hamakaze/WebSocket/WsConnection.cs
Normal file
395
Hamakaze/WebSocket/WsConnection.cs
Normal file
|
@ -0,0 +1,395 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Security;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Hamakaze.WebSocket {
|
||||||
|
public class WsConnection : IDisposable {
|
||||||
|
public Stream Stream { get; }
|
||||||
|
|
||||||
|
public bool IsSecure { get; }
|
||||||
|
public bool IsClosed { get; private set; }
|
||||||
|
|
||||||
|
private const byte MASK_FLAG = 0x80;
|
||||||
|
private const int MASK_SIZE = 4;
|
||||||
|
|
||||||
|
private WsOpcode FragmentType = 0;
|
||||||
|
private MemoryStream FragmentStream;
|
||||||
|
|
||||||
|
private WsBufferedSend BufferedSend;
|
||||||
|
|
||||||
|
public WsConnection(Stream stream) {
|
||||||
|
Stream = stream ?? throw new ArgumentNullException(nameof(stream));
|
||||||
|
IsSecure = stream is SslStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] GenerateMask() {
|
||||||
|
return RandomNumberGenerator.GetBytes(MASK_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StrictRead(byte[] buffer, int offset, int length) {
|
||||||
|
int read = Stream.Read(buffer, offset, length);
|
||||||
|
if(read < length)
|
||||||
|
throw new Exception(@"Was unable to read the requested amount of data.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private (WsOpcode opcode, int length, bool isFinal, byte[] mask) ReadFrameHeader() {
|
||||||
|
byte[] buffer = new byte[8];
|
||||||
|
StrictRead(buffer, 0, 2);
|
||||||
|
|
||||||
|
WsOpcode opcode = (WsOpcode)(buffer[0] & 0x0F);
|
||||||
|
bool isFinal = (buffer[0] & (byte)WsOpcode.FlagFinal) > 0;
|
||||||
|
|
||||||
|
if(opcode >= WsOpcode.CtrlClose && !isFinal)
|
||||||
|
throw new WsInvalidOpcodeException((WsOpcode)buffer[0]);
|
||||||
|
|
||||||
|
bool isControl = (opcode & WsOpcode.CtrlClose) > 0;
|
||||||
|
|
||||||
|
if(isControl && !isFinal)
|
||||||
|
throw new WsInvalidControlFrameException(@"fragmented");
|
||||||
|
|
||||||
|
bool isMasked = (buffer[1] & MASK_FLAG) > 0;
|
||||||
|
|
||||||
|
// this may look stupid and you'd be correct but it's better than the stack of casts
|
||||||
|
// i'd otherwise have to do otherwise because c# converts everything back to int32
|
||||||
|
buffer[1] &= 0x7F;
|
||||||
|
long length = buffer[1];
|
||||||
|
|
||||||
|
if(length == 126) {
|
||||||
|
StrictRead(buffer, 0, 2);
|
||||||
|
length = WsUtils.ToU16(buffer);
|
||||||
|
} else if(length == 127) {
|
||||||
|
StrictRead(buffer, 0, 8);
|
||||||
|
length = WsUtils.ToI64(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isControl && length > 125)
|
||||||
|
throw new WsInvalidControlFrameException(@"too large");
|
||||||
|
|
||||||
|
// should there be a sanity check on the length of frames?
|
||||||
|
// i seriously don't understand the rationale behind both
|
||||||
|
// having a framing system but then also supporting frame lengths
|
||||||
|
// of 2^63, feels like 2^16 per frame would be a fine max.
|
||||||
|
// UPDATE: decided to put the max at 2^32-1
|
||||||
|
// it's still more than you should ever need for a single frame
|
||||||
|
// and it makes working with the number within a .NET context
|
||||||
|
// less of a bother.
|
||||||
|
if(length < 0 || length > int.MaxValue)
|
||||||
|
throw new WsInvalidFrameSizeException(length);
|
||||||
|
|
||||||
|
byte[] mask = null;
|
||||||
|
|
||||||
|
if(isMasked) {
|
||||||
|
StrictRead(buffer, 0, MASK_SIZE);
|
||||||
|
mask = buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (opcode, (int)length, isFinal, mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int ReadFrameBody(byte[] target, int length, byte[] mask, int offset = 0) {
|
||||||
|
if(target == null)
|
||||||
|
throw new ArgumentNullException(nameof(target));
|
||||||
|
|
||||||
|
bool isMasked = mask != null;
|
||||||
|
|
||||||
|
int read;
|
||||||
|
const int bufferSize = 0x1000;
|
||||||
|
int take = length > bufferSize ? bufferSize : (int)length;
|
||||||
|
|
||||||
|
while(length > 0) {
|
||||||
|
read = Stream.Read(target, offset, take);
|
||||||
|
|
||||||
|
if(isMasked)
|
||||||
|
for(int i = 0; i < read; ++i) {
|
||||||
|
int o = offset + i;
|
||||||
|
target[o] ^= mask[o % MASK_SIZE];
|
||||||
|
}
|
||||||
|
|
||||||
|
length -= read;
|
||||||
|
offset += read;
|
||||||
|
|
||||||
|
if(take > length)
|
||||||
|
take = (int)length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
private WsMessage ReadFrame() {
|
||||||
|
(WsOpcode opcode, int length, bool isFinal, byte[] mask) = ReadFrameHeader();
|
||||||
|
|
||||||
|
if(opcode is not WsOpcode.DataContinue
|
||||||
|
and not WsOpcode.DataBinary
|
||||||
|
and not WsOpcode.DataText
|
||||||
|
and not WsOpcode.CtrlClose
|
||||||
|
and not WsOpcode.CtrlPing
|
||||||
|
and not WsOpcode.CtrlPong)
|
||||||
|
throw new WsUnsupportedOpcodeException(opcode);
|
||||||
|
|
||||||
|
bool hasBody = length > 0;
|
||||||
|
bool isContinue = opcode == WsOpcode.DataContinue;
|
||||||
|
bool canFragment = (opcode & WsOpcode.CtrlClose) == 0;
|
||||||
|
|
||||||
|
byte[] body = length < 1 ? null : new byte[length];
|
||||||
|
|
||||||
|
if(hasBody) {
|
||||||
|
ReadFrameBody(body, length, mask);
|
||||||
|
|
||||||
|
if(canFragment) {
|
||||||
|
if(isContinue) {
|
||||||
|
if(FragmentType == 0)
|
||||||
|
throw new WsUnexpectedContinueException();
|
||||||
|
|
||||||
|
opcode = FragmentType;
|
||||||
|
|
||||||
|
FragmentStream ??= new();
|
||||||
|
FragmentStream.Write(body, 0, length);
|
||||||
|
} else {
|
||||||
|
if(FragmentType != 0)
|
||||||
|
throw new WsUnexpectedDataException();
|
||||||
|
|
||||||
|
if(!isFinal) {
|
||||||
|
FragmentType = opcode;
|
||||||
|
FragmentStream = new();
|
||||||
|
FragmentStream.Write(body, 0, length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WsMessage msg;
|
||||||
|
|
||||||
|
if(isFinal) {
|
||||||
|
if(canFragment && isContinue) {
|
||||||
|
FragmentType = 0;
|
||||||
|
|
||||||
|
body = FragmentStream.ToArray();
|
||||||
|
FragmentStream.Dispose();
|
||||||
|
FragmentStream = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
msg = opcode switch {
|
||||||
|
WsOpcode.DataText => new WsTextMessage(body),
|
||||||
|
WsOpcode.DataBinary => new WsBinaryMessage(body),
|
||||||
|
|
||||||
|
WsOpcode.CtrlClose => new WsCloseMessage(body),
|
||||||
|
WsOpcode.CtrlPing => new WsPingMessage(body),
|
||||||
|
WsOpcode.CtrlPong => new WsPongMessage(body),
|
||||||
|
|
||||||
|
// fallback, if we end up here something is very fucked
|
||||||
|
_ => throw new WsUnsupportedOpcodeException(opcode),
|
||||||
|
};
|
||||||
|
} else msg = null;
|
||||||
|
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public WsMessage Receive() {
|
||||||
|
WsMessage msg;
|
||||||
|
while((msg = ReadFrame()) == null);
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteFrameHeader(WsOpcode opcode, int length, bool isFinal, byte[] mask = null) {
|
||||||
|
if(length < 0 || length > int.MaxValue)
|
||||||
|
throw new WsInvalidFrameSizeException(length);
|
||||||
|
|
||||||
|
bool shouldMask = mask != null;
|
||||||
|
|
||||||
|
if(isFinal)
|
||||||
|
opcode |= WsOpcode.FlagFinal;
|
||||||
|
|
||||||
|
Stream.WriteByte((byte)opcode);
|
||||||
|
|
||||||
|
byte bLen1 = 0;
|
||||||
|
if(shouldMask)
|
||||||
|
bLen1 |= MASK_FLAG;
|
||||||
|
|
||||||
|
byte[] bLenBuff = WsUtils.FromI64(length);
|
||||||
|
if(length < 126) {
|
||||||
|
Stream.WriteByte((byte)(bLen1 | bLenBuff[7]));
|
||||||
|
} else if(length <= ushort.MaxValue) {
|
||||||
|
Stream.WriteByte((byte)(bLen1 | 126));
|
||||||
|
Stream.Write(bLenBuff, 6, 2);
|
||||||
|
} else {
|
||||||
|
Stream.WriteByte((byte)(bLen1 | 127));
|
||||||
|
Stream.Write(bLenBuff, 0, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(shouldMask)
|
||||||
|
Stream.Write(mask, 0, MASK_SIZE);
|
||||||
|
Stream.Flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int WriteFrameBody(ReadOnlySpan<byte> body, byte[] mask = null, int offset = 0) {
|
||||||
|
if(body == null)
|
||||||
|
throw new ArgumentNullException(nameof(body));
|
||||||
|
|
||||||
|
if(mask != null) {
|
||||||
|
byte[] masked = new byte[body.Length];
|
||||||
|
|
||||||
|
for(int i = 0; i < body.Length; ++i)
|
||||||
|
masked[i] = (byte)(body[i] ^ mask[offset++ % MASK_SIZE]);
|
||||||
|
|
||||||
|
body = masked;
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream.Write(body);
|
||||||
|
Stream.Flush();
|
||||||
|
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void WriteFrame(WsOpcode opcode, ReadOnlySpan<byte> body, bool isFinal) {
|
||||||
|
if(body == null)
|
||||||
|
throw new ArgumentNullException(nameof(body));
|
||||||
|
|
||||||
|
byte[] mask = GenerateMask();
|
||||||
|
WriteFrameHeader(opcode, body.Length, isFinal, mask);
|
||||||
|
if(body.Length > 0)
|
||||||
|
WriteFrameBody(body, mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteData(WsOpcode opcode, ReadOnlySpan<byte> body) {
|
||||||
|
if(body == null)
|
||||||
|
throw new ArgumentNullException(nameof(body));
|
||||||
|
if(BufferedSend != null)
|
||||||
|
throw new WsBufferedSendInSessionException();
|
||||||
|
|
||||||
|
if(body.Length > ushort.MaxValue) {
|
||||||
|
WriteFrame(opcode, body.Slice(0, ushort.MaxValue), false);
|
||||||
|
body = body.Slice(ushort.MaxValue);
|
||||||
|
|
||||||
|
while(body.Length > ushort.MaxValue) {
|
||||||
|
WriteFrame(WsOpcode.DataContinue, body.Slice(0, ushort.MaxValue), false);
|
||||||
|
body = body.Slice(ushort.MaxValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteFrame(WsOpcode.DataContinue, body, true);
|
||||||
|
} else
|
||||||
|
WriteFrame(opcode, body, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Send(string text)
|
||||||
|
=> WriteData(WsOpcode.DataText, Encoding.UTF8.GetBytes(text));
|
||||||
|
|
||||||
|
public void Send(ReadOnlySpan<byte> buffer)
|
||||||
|
=> WriteData(WsOpcode.DataBinary, buffer);
|
||||||
|
|
||||||
|
public WsBufferedSend BeginBufferedSend() {
|
||||||
|
if(BufferedSend != null)
|
||||||
|
throw new WsBufferedSendAlreadyActiveException();
|
||||||
|
return BufferedSend = new(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// this method should only be called from within WsBufferedSend.Dispose
|
||||||
|
internal void EndBufferedSend() {
|
||||||
|
BufferedSend = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteControl(WsOpcode opcode)
|
||||||
|
=> WriteFrameHeader(opcode, 0, true, GenerateMask());
|
||||||
|
|
||||||
|
private void WriteControl(WsOpcode opcode, ReadOnlySpan<byte> buffer) {
|
||||||
|
if(buffer == null)
|
||||||
|
throw new ArgumentNullException(nameof(buffer));
|
||||||
|
if(buffer.Length > 125)
|
||||||
|
throw new ArgumentException(@"Data may not be more than 125 bytes.", nameof(buffer));
|
||||||
|
|
||||||
|
byte[] mask = GenerateMask();
|
||||||
|
WriteFrameHeader(opcode, buffer.Length, true, mask);
|
||||||
|
WriteFrameBody(buffer, mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Ping()
|
||||||
|
=> WriteControl(WsOpcode.CtrlPing);
|
||||||
|
|
||||||
|
public void Ping(ReadOnlySpan<byte> buffer)
|
||||||
|
=> WriteControl(WsOpcode.CtrlPing, buffer);
|
||||||
|
|
||||||
|
public void Pong()
|
||||||
|
=> WriteControl(WsOpcode.CtrlPong);
|
||||||
|
|
||||||
|
public void Pong(ReadOnlySpan<byte> buffer)
|
||||||
|
=> WriteControl(WsOpcode.CtrlPong, buffer);
|
||||||
|
|
||||||
|
public void CloseEmpty() {
|
||||||
|
if(IsClosed)
|
||||||
|
return;
|
||||||
|
IsClosed = true;
|
||||||
|
|
||||||
|
WriteControl(WsOpcode.CtrlClose);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Close(ReadOnlySpan<byte> buffer) {
|
||||||
|
if(buffer == null)
|
||||||
|
throw new ArgumentNullException(nameof(buffer));
|
||||||
|
|
||||||
|
if(IsClosed)
|
||||||
|
return;
|
||||||
|
IsClosed = true;
|
||||||
|
|
||||||
|
WriteControl(WsOpcode.CtrlClose, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Close(WsCloseReason code)
|
||||||
|
=> Close(WsUtils.FromU16((ushort)code));
|
||||||
|
|
||||||
|
public void Close(WsCloseReason code, ReadOnlySpan<byte> reason) {
|
||||||
|
if(reason == null)
|
||||||
|
throw new ArgumentNullException(nameof(reason));
|
||||||
|
if(reason.Length > 123)
|
||||||
|
throw new ArgumentException(@"Reason may not be more than 123 bytes.", nameof(reason));
|
||||||
|
|
||||||
|
if(IsClosed)
|
||||||
|
return;
|
||||||
|
IsClosed = true;
|
||||||
|
|
||||||
|
byte[] mask = GenerateMask();
|
||||||
|
WriteFrameHeader(WsOpcode.CtrlClose, 2 + reason.Length, true, mask);
|
||||||
|
WriteFrameBody(WsUtils.FromU16((ushort)code), mask);
|
||||||
|
WriteFrameBody(reason, mask, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Close(WsCloseReason code, string reason) {
|
||||||
|
if(reason == null)
|
||||||
|
throw new ArgumentNullException(nameof(reason));
|
||||||
|
|
||||||
|
int length = Encoding.UTF8.GetByteCount(reason);
|
||||||
|
if(length > 123)
|
||||||
|
throw new ArgumentException(@"Reason string may not exceed 123 bytes in length.", nameof(reason));
|
||||||
|
|
||||||
|
if(IsClosed)
|
||||||
|
return;
|
||||||
|
IsClosed = true;
|
||||||
|
|
||||||
|
byte[] mask = GenerateMask();
|
||||||
|
WriteFrameHeader(WsOpcode.CtrlClose, 2 + reason.Length, true, mask);
|
||||||
|
WriteFrameBody(WsUtils.FromU16((ushort)code), mask);
|
||||||
|
WriteFrameBody(Encoding.UTF8.GetBytes(reason), mask, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsDisposed;
|
||||||
|
|
||||||
|
~WsConnection() {
|
||||||
|
DoDispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() {
|
||||||
|
DoDispose();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DoDispose() {
|
||||||
|
if(IsDisposed)
|
||||||
|
return;
|
||||||
|
IsDisposed = true;
|
||||||
|
|
||||||
|
BufferedSend?.Dispose();
|
||||||
|
FragmentStream?.Dispose();
|
||||||
|
Stream.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
Hamakaze/WebSocket/WsException.cs
Normal file
41
Hamakaze/WebSocket/WsException.cs
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
namespace Hamakaze.WebSocket {
|
||||||
|
public class WsException : HttpException {
|
||||||
|
public WsException(string message) : base(message) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WsInvalidOpcodeException : WsException {
|
||||||
|
public WsInvalidOpcodeException(WsOpcode opcode) : base($@"An invalid WebSocket opcode was encountered: {opcode}.") { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WsUnsupportedOpcodeException : WsException {
|
||||||
|
public WsUnsupportedOpcodeException(WsOpcode opcode) : base($@"An unsupported WebSocket opcode was encountered: {opcode}.") { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WsInvalidFrameSizeException : WsException {
|
||||||
|
public WsInvalidFrameSizeException(long size) : base($@"WebSocket frame size is too large: {size} bytes.") { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WsUnexpectedContinueException : WsException {
|
||||||
|
public WsUnexpectedContinueException() : base(@"A WebSocket continue frame was issued but there is nothing to continue.") { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WsUnexpectedDataException : WsException {
|
||||||
|
public WsUnexpectedDataException() : base(@"A WebSocket data frame was issued while a fragmented frame is being constructed.") { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WsInvalidControlFrameException : WsException {
|
||||||
|
public WsInvalidControlFrameException(string variant) : base($@"An invalid WebSocket control frame was encountered: {variant}") { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WsClientMutexFailedException : WsException {
|
||||||
|
public WsClientMutexFailedException() : base(@"Failed to acquire send mutex.") { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WsBufferedSendAlreadyActiveException : WsException {
|
||||||
|
public WsBufferedSendAlreadyActiveException() : base(@"A buffered websocket send is already in session.") { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WsBufferedSendInSessionException : WsException {
|
||||||
|
public WsBufferedSendInSessionException() : base(@"Cannot send data while a buffered send is in session.") { }
|
||||||
|
}
|
||||||
|
}
|
5
Hamakaze/WebSocket/WsMessage.cs
Normal file
5
Hamakaze/WebSocket/WsMessage.cs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
namespace Hamakaze.WebSocket {
|
||||||
|
public abstract class WsMessage {
|
||||||
|
// nothing, lol
|
||||||
|
}
|
||||||
|
}
|
13
Hamakaze/WebSocket/WsOpcode.cs
Normal file
13
Hamakaze/WebSocket/WsOpcode.cs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
namespace Hamakaze.WebSocket {
|
||||||
|
public enum WsOpcode : byte {
|
||||||
|
DataContinue = 0x00,
|
||||||
|
DataText = 0x01,
|
||||||
|
DataBinary = 0x02,
|
||||||
|
|
||||||
|
CtrlClose = 0x08,
|
||||||
|
CtrlPing = 0x09,
|
||||||
|
CtrlPong = 0x0A,
|
||||||
|
|
||||||
|
FlagFinal = 0x80,
|
||||||
|
}
|
||||||
|
}
|
11
Hamakaze/WebSocket/WsPingMessage.cs
Normal file
11
Hamakaze/WebSocket/WsPingMessage.cs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Hamakaze.WebSocket {
|
||||||
|
public class WsPingMessage : WsMessage, IHasBinaryData {
|
||||||
|
public byte[] Data { get; }
|
||||||
|
|
||||||
|
public WsPingMessage(byte[] data) {
|
||||||
|
Data = data ?? Array.Empty<byte>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
Hamakaze/WebSocket/WsPongMessage.cs
Normal file
11
Hamakaze/WebSocket/WsPongMessage.cs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Hamakaze.WebSocket {
|
||||||
|
public class WsPongMessage : WsMessage, IHasBinaryData {
|
||||||
|
public byte[] Data { get; }
|
||||||
|
|
||||||
|
public WsPongMessage(byte[] data) {
|
||||||
|
Data = data ?? Array.Empty<byte>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
Hamakaze/WebSocket/WsTextMessage.cs
Normal file
20
Hamakaze/WebSocket/WsTextMessage.cs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Hamakaze.WebSocket {
|
||||||
|
public class WsTextMessage : WsMessage {
|
||||||
|
public string Text { get; }
|
||||||
|
|
||||||
|
public WsTextMessage(byte[] data) {
|
||||||
|
if(data?.Length > 0)
|
||||||
|
Text = Encoding.UTF8.GetString(data);
|
||||||
|
else
|
||||||
|
Text = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static implicit operator string(WsTextMessage msg) => msg.Text;
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return Text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
38
Hamakaze/WebSocket/WsUtils.cs
Normal file
38
Hamakaze/WebSocket/WsUtils.cs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Hamakaze.WebSocket {
|
||||||
|
internal static class WsUtils {
|
||||||
|
public static byte[] FromU16(ushort num) {
|
||||||
|
byte[] buff = BitConverter.GetBytes(num);
|
||||||
|
if(BitConverter.IsLittleEndian)
|
||||||
|
Array.Reverse(buff);
|
||||||
|
return buff;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ushort ToU16(ReadOnlySpan<byte> buffer) {
|
||||||
|
if(BitConverter.IsLittleEndian)
|
||||||
|
buffer = new byte[2] {
|
||||||
|
buffer[1], buffer[0],
|
||||||
|
};
|
||||||
|
|
||||||
|
return BitConverter.ToUInt16(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] FromI64(long num) {
|
||||||
|
byte[] buff = BitConverter.GetBytes(num);
|
||||||
|
if(BitConverter.IsLittleEndian)
|
||||||
|
Array.Reverse(buff);
|
||||||
|
return buff;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long ToI64(ReadOnlySpan<byte> buffer) {
|
||||||
|
if(BitConverter.IsLittleEndian)
|
||||||
|
buffer = new byte[8] {
|
||||||
|
buffer[7], buffer[6], buffer[5], buffer[4],
|
||||||
|
buffer[3], buffer[2], buffer[1], buffer[0],
|
||||||
|
};
|
||||||
|
|
||||||
|
return BitConverter.ToInt64(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in a new issue