Got at least one data fetching method working; turns out, we can't use a patched LogicStack to get the data
This commit is contained in:
97
Vendor/EmbedIO-3.5.2/WebSockets/CloseStatusCode.cs
vendored
Normal file
97
Vendor/EmbedIO-3.5.2/WebSockets/CloseStatusCode.cs
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
namespace EmbedIO.WebSockets
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates the status code for the WebSocket connection close.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The values of this enumeration are defined in
|
||||
/// <see href="http://tools.ietf.org/html/rfc6455#section-7.4">Section 7.4</see> of RFC 6455.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// "Reserved value" must not be set as a status code in a connection close frame by
|
||||
/// an endpoint. It's designated for use in applications expecting a status code to
|
||||
/// indicate that the connection was closed due to the system grounds.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public enum CloseStatusCode : ushort
|
||||
{
|
||||
/// <summary>
|
||||
/// Equivalent to close status 1000. Indicates normal close.
|
||||
/// </summary>
|
||||
Normal = 1000,
|
||||
|
||||
/// <summary>
|
||||
/// Equivalent to close status 1001. Indicates that an endpoint is going away.
|
||||
/// </summary>
|
||||
Away = 1001,
|
||||
|
||||
/// <summary>
|
||||
/// Equivalent to close status 1002. Indicates that an endpoint is terminating
|
||||
/// the connection due to a protocol error.
|
||||
/// </summary>
|
||||
ProtocolError = 1002,
|
||||
|
||||
/// <summary>
|
||||
/// Equivalent to close status 1003. Indicates that an endpoint is terminating
|
||||
/// the connection because it has received a type of data that it cannot accept.
|
||||
/// </summary>
|
||||
UnsupportedData = 1003,
|
||||
|
||||
/// <summary>
|
||||
/// Equivalent to close status 1004. Still undefined. A Reserved value.
|
||||
/// </summary>
|
||||
Undefined = 1004,
|
||||
|
||||
/// <summary>
|
||||
/// Equivalent to close status 1005. Indicates that no status code was actually present.
|
||||
/// A Reserved value.
|
||||
/// </summary>
|
||||
NoStatus = 1005,
|
||||
|
||||
/// <summary>
|
||||
/// Equivalent to close status 1006. Indicates that the connection was closed abnormally.
|
||||
/// A Reserved value.
|
||||
/// </summary>
|
||||
Abnormal = 1006,
|
||||
|
||||
/// <summary>
|
||||
/// Equivalent to close status 1007. Indicates that an endpoint is terminating
|
||||
/// the connection because it has received a message that contains data that
|
||||
/// isn't consistent with the type of the message.
|
||||
/// </summary>
|
||||
InvalidData = 1007,
|
||||
|
||||
/// <summary>
|
||||
/// Equivalent to close status 1008. Indicates that an endpoint is terminating
|
||||
/// the connection because it has received a message that violates its policy.
|
||||
/// </summary>
|
||||
PolicyViolation = 1008,
|
||||
|
||||
/// <summary>
|
||||
/// Equivalent to close status 1009. Indicates that an endpoint is terminating
|
||||
/// the connection because it has received a message that is too big to process.
|
||||
/// </summary>
|
||||
TooBig = 1009,
|
||||
|
||||
/// <summary>
|
||||
/// Equivalent to close status 1010. Indicates that a client is terminating
|
||||
/// the connection because it has expected the server to negotiate one or more extension,
|
||||
/// but the server didn't return them in the handshake response.
|
||||
/// </summary>
|
||||
MandatoryExtension = 1010,
|
||||
|
||||
/// <summary>
|
||||
/// Equivalent to close status 1011. Indicates that a server is terminating
|
||||
/// the connection because it has encountered an unexpected condition that
|
||||
/// prevented it from fulfilling the request.
|
||||
/// </summary>
|
||||
ServerError = 1011,
|
||||
|
||||
/// <summary>
|
||||
/// Equivalent to close status 1015. Indicates that the connection was closed
|
||||
/// due to a failure to perform a TLS handshake. A Reserved value.
|
||||
/// </summary>
|
||||
TlsHandshakeFailure = 1015,
|
||||
}
|
||||
}
|
||||
54
Vendor/EmbedIO-3.5.2/WebSockets/IWebSocket.cs
vendored
Normal file
54
Vendor/EmbedIO-3.5.2/WebSockets/IWebSocket.cs
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace EmbedIO.WebSockets
|
||||
{
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Interface to create a WebSocket implementation.
|
||||
/// </summary>
|
||||
/// <seealso cref="IDisposable" />
|
||||
public interface IWebSocket : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the state.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The state.
|
||||
/// </value>
|
||||
WebSocketState State { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Sends the buffer to the web socket asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The buffer.</param>
|
||||
/// <param name="isText">if set to <c>true</c> [is text].</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>
|
||||
/// A task that represents the asynchronous of send data using websocket.
|
||||
/// </returns>
|
||||
Task SendAsync(byte[] buffer, bool isText, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Closes the web socket asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>
|
||||
/// The task object representing the asynchronous operation.
|
||||
/// </returns>
|
||||
Task CloseAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Closes the web socket asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="code">The code.</param>
|
||||
/// <param name="comment">The comment.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>
|
||||
/// The task object representing the asynchronous operation.
|
||||
/// </returns>
|
||||
Task CloseAsync(CloseStatusCode code, string? comment = null, CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
87
Vendor/EmbedIO-3.5.2/WebSockets/IWebSocketContext.cs
vendored
Normal file
87
Vendor/EmbedIO-3.5.2/WebSockets/IWebSocketContext.cs
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Net;
|
||||
using System.Security.Principal;
|
||||
using System.Threading;
|
||||
using EmbedIO.Sessions;
|
||||
|
||||
namespace EmbedIO.WebSockets
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the context of a WebSocket connection.
|
||||
/// </summary>
|
||||
public interface IWebSocketContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a unique identifier for a WebSocket context.
|
||||
/// </summary>
|
||||
string Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="CancellationToken "/> used to cancel operations.
|
||||
/// </summary>
|
||||
CancellationToken CancellationToken { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unique identifier of the opening handshake HTTP context.
|
||||
/// </summary>
|
||||
string HttpContextId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the session proxy associated with the opening handshake HTTP context.
|
||||
/// </summary>
|
||||
ISessionProxy Session { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the dictionary of data associated with the opening handshake HTTP context.
|
||||
/// </summary>
|
||||
IDictionary<object, object> Items { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the server IP address and port number to which the opening handshake request is directed.
|
||||
/// </summary>
|
||||
IPEndPoint LocalEndPoint { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the client IP address and port number from which the opening handshake request originated.
|
||||
/// </summary>
|
||||
IPEndPoint RemoteEndPoint { get; }
|
||||
|
||||
/// <summary>The URI requested by the WebSocket client.</summary>
|
||||
Uri RequestUri { get; }
|
||||
|
||||
/// <summary>The HTTP headers that were sent to the server during the opening handshake.</summary>
|
||||
NameValueCollection Headers { get; }
|
||||
|
||||
/// <summary>The value of the Origin HTTP header included in the opening handshake.</summary>
|
||||
string Origin { get; }
|
||||
|
||||
/// <summary>The value of the SecWebSocketKey HTTP header included in the opening handshake.</summary>
|
||||
string WebSocketVersion { get; }
|
||||
|
||||
/// <summary>The list of subprotocols requested by the WebSocket client.</summary>
|
||||
IEnumerable<string> RequestedProtocols { get; }
|
||||
|
||||
/// <summary>The accepted subprotocol.</summary>
|
||||
string AcceptedProtocol { get; }
|
||||
|
||||
/// <summary>The cookies that were passed to the server during the opening handshake.</summary>
|
||||
ICookieCollection Cookies { get; }
|
||||
|
||||
/// <summary>An object used to obtain identity, authentication information, and security roles for the WebSocket client.</summary>
|
||||
IPrincipal User { get; }
|
||||
|
||||
/// <summary>Whether the WebSocket client is authenticated.</summary>
|
||||
bool IsAuthenticated { get; }
|
||||
|
||||
/// <summary>Whether the WebSocket client connected from the local machine.</summary>
|
||||
bool IsLocal { get; }
|
||||
|
||||
/// <summary>Whether the WebSocket connection is secured using Secure Sockets Layer (SSL).</summary>
|
||||
bool IsSecureConnection { get; }
|
||||
|
||||
/// <summary>The <see cref="IWebSocket"/> interface used to interact with the WebSocket connection.</summary>
|
||||
IWebSocket WebSocket { get; }
|
||||
}
|
||||
}
|
||||
32
Vendor/EmbedIO-3.5.2/WebSockets/IWebSocketReceiveResult.cs
vendored
Normal file
32
Vendor/EmbedIO-3.5.2/WebSockets/IWebSocketReceiveResult.cs
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
namespace EmbedIO.WebSockets
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for WebSocket Receive Result object.
|
||||
/// </summary>
|
||||
public interface IWebSocketReceiveResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the count.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The count.
|
||||
/// </value>
|
||||
int Count { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether [end of message].
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if [end of message]; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
bool EndOfMessage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the message.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The type of the message.
|
||||
/// </value>
|
||||
int MessageType { get; }
|
||||
}
|
||||
}
|
||||
22
Vendor/EmbedIO-3.5.2/WebSockets/Internal/Fin.cs
vendored
Normal file
22
Vendor/EmbedIO-3.5.2/WebSockets/Internal/Fin.cs
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace EmbedIO.WebSockets.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates whether a WebSocket frame is the final frame of a message.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The values of this enumeration are defined in
|
||||
/// <see href="http://tools.ietf.org/html/rfc6455#section-5.2">Section 5.2</see> of RFC 6455.
|
||||
/// </remarks>
|
||||
internal enum Fin : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Equivalent to numeric value 0. Indicates more frames of a message follow.
|
||||
/// </summary>
|
||||
More = 0x0,
|
||||
|
||||
/// <summary>
|
||||
/// Equivalent to numeric value 1. Indicates the final frame of a message.
|
||||
/// </summary>
|
||||
Final = 0x1,
|
||||
}
|
||||
}
|
||||
29
Vendor/EmbedIO-3.5.2/WebSockets/Internal/FragmentBuffer.cs
vendored
Normal file
29
Vendor/EmbedIO-3.5.2/WebSockets/Internal/FragmentBuffer.cs
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace EmbedIO.WebSockets.Internal
|
||||
{
|
||||
internal class FragmentBuffer : MemoryStream
|
||||
{
|
||||
private readonly bool _fragmentsCompressed;
|
||||
private readonly Opcode _fragmentsOpcode;
|
||||
|
||||
public FragmentBuffer(Opcode frameOpcode, bool frameIsCompressed)
|
||||
{
|
||||
_fragmentsOpcode = frameOpcode;
|
||||
_fragmentsCompressed = frameIsCompressed;
|
||||
}
|
||||
|
||||
public void AddPayload(MemoryStream data) => data.CopyTo(this, 1024);
|
||||
|
||||
public async Task<MessageEventArgs> GetMessage(CompressionMethod compression)
|
||||
{
|
||||
var data = _fragmentsCompressed
|
||||
? await this.CompressAsync(compression, false, CancellationToken.None).ConfigureAwait(false)
|
||||
: this;
|
||||
|
||||
return new MessageEventArgs(_fragmentsOpcode, data.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
22
Vendor/EmbedIO-3.5.2/WebSockets/Internal/Mask.cs
vendored
Normal file
22
Vendor/EmbedIO-3.5.2/WebSockets/Internal/Mask.cs
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace EmbedIO.WebSockets.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates whether the payload data of a WebSocket frame is masked.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The values of this enumeration are defined in
|
||||
/// <see href="http://tools.ietf.org/html/rfc6455#section-5.2">Section 5.2</see> of RFC 6455.
|
||||
/// </remarks>
|
||||
internal enum Mask : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Equivalent to numeric value 0. Indicates not masked.
|
||||
/// </summary>
|
||||
Off = 0x0,
|
||||
|
||||
/// <summary>
|
||||
/// Equivalent to numeric value 1. Indicates masked.
|
||||
/// </summary>
|
||||
On = 0x1,
|
||||
}
|
||||
}
|
||||
114
Vendor/EmbedIO-3.5.2/WebSockets/Internal/MessageEventArgs.cs
vendored
Normal file
114
Vendor/EmbedIO-3.5.2/WebSockets/Internal/MessageEventArgs.cs
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
using System;
|
||||
using Swan;
|
||||
|
||||
namespace EmbedIO.WebSockets.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the event data for the <see cref="Internal.WebSocket.OnMessage"/> event.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// That event occurs when the <see cref="Internal.WebSocket"/> receives
|
||||
/// a message or a ping if the <see cref="Internal.WebSocket.EmitOnPing"/>
|
||||
/// property is set to <c>true</c>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If you would like to get the message data, you should access
|
||||
/// the <see cref="Data"/> or <see cref="RawData"/> property.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal class MessageEventArgs : EventArgs
|
||||
{
|
||||
private readonly byte[] _rawData;
|
||||
private string? _data;
|
||||
private bool _dataSet;
|
||||
|
||||
internal MessageEventArgs(WebSocketFrame frame)
|
||||
{
|
||||
Opcode = frame.Opcode;
|
||||
_rawData = frame.PayloadData.ApplicationData.ToArray();
|
||||
}
|
||||
|
||||
internal MessageEventArgs(Opcode opcode, byte[] rawData)
|
||||
{
|
||||
if ((ulong)rawData.Length > PayloadData.MaxLength)
|
||||
throw new WebSocketException(CloseStatusCode.TooBig);
|
||||
|
||||
Opcode = opcode;
|
||||
_rawData = rawData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the message data as a <see cref="string"/>.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A <see cref="string"/> that represents the message data if its type is
|
||||
/// text or ping and if decoding it to a string has successfully done;
|
||||
/// otherwise, <see langword="null"/>.
|
||||
/// </value>
|
||||
public string? Data
|
||||
{
|
||||
get
|
||||
{
|
||||
SetData();
|
||||
return _data;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the message type is binary.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if the message type is binary; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool IsBinary => Opcode == Opcode.Binary;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the message type is ping.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if the message type is ping; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool IsPing => Opcode == Opcode.Ping;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the message type is text.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if the message type is text; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool IsText => Opcode == Opcode.Text;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the message data as an array of <see cref="byte"/>.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// An array of <see cref="byte"/> that represents the message data.
|
||||
/// </value>
|
||||
public byte[] RawData
|
||||
{
|
||||
get
|
||||
{
|
||||
SetData();
|
||||
return _rawData;
|
||||
}
|
||||
}
|
||||
|
||||
internal Opcode Opcode { get; }
|
||||
|
||||
private void SetData()
|
||||
{
|
||||
if (_dataSet)
|
||||
return;
|
||||
|
||||
if (Opcode == Opcode.Binary)
|
||||
{
|
||||
_dataSet = true;
|
||||
return;
|
||||
}
|
||||
|
||||
_data = _rawData.ToText();
|
||||
_dataSet = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
74
Vendor/EmbedIO-3.5.2/WebSockets/Internal/PayloadData.cs
vendored
Normal file
74
Vendor/EmbedIO-3.5.2/WebSockets/Internal/PayloadData.cs
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Swan;
|
||||
using EmbedIO.Net.Internal;
|
||||
|
||||
namespace EmbedIO.WebSockets.Internal
|
||||
{
|
||||
internal class PayloadData
|
||||
{
|
||||
public const ulong MaxLength = long.MaxValue;
|
||||
|
||||
private readonly byte[] _data;
|
||||
private ushort? _code;
|
||||
|
||||
internal PayloadData(byte[] data)
|
||||
{
|
||||
_data = data;
|
||||
}
|
||||
|
||||
internal PayloadData(ushort code = 1005, string? reason = null)
|
||||
{
|
||||
_code = code;
|
||||
_data = code == 1005 ? Array.Empty<byte>() : Append(code, reason);
|
||||
}
|
||||
|
||||
internal MemoryStream ApplicationData => new MemoryStream(_data);
|
||||
|
||||
internal ulong Length => (ulong)_data.Length;
|
||||
|
||||
internal ushort Code
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_code.HasValue)
|
||||
{
|
||||
_code = _data.Length > 1
|
||||
? BitConverter.ToUInt16(_data.Take(2).ToArray().ToHostOrder(Endianness.Big), 0)
|
||||
: (ushort)1005;
|
||||
}
|
||||
|
||||
return _code.Value;
|
||||
}
|
||||
}
|
||||
|
||||
internal bool HasReservedCode => _data.Length > 1 && (Code == (ushort)CloseStatusCode.Undefined ||
|
||||
Code == (ushort)CloseStatusCode.NoStatus ||
|
||||
Code == (ushort)CloseStatusCode.Abnormal ||
|
||||
Code == (ushort)CloseStatusCode.TlsHandshakeFailure);
|
||||
|
||||
public override string ToString() => BitConverter.ToString(_data);
|
||||
|
||||
internal static byte[] Append(ushort code, string? reason)
|
||||
{
|
||||
var ret = code.ToByteArray(Endianness.Big);
|
||||
if (string.IsNullOrEmpty(reason)) return ret;
|
||||
|
||||
var buff = new List<byte>(ret);
|
||||
buff.AddRange(Encoding.UTF8.GetBytes(reason));
|
||||
|
||||
return buff.ToArray();
|
||||
}
|
||||
|
||||
internal void Mask(byte[] key)
|
||||
{
|
||||
for (long i = 0; i < _data.Length; i++)
|
||||
_data[i] = (byte)(_data[i] ^ key[i % 4]);
|
||||
}
|
||||
|
||||
internal byte[] ToArray() => _data;
|
||||
}
|
||||
}
|
||||
22
Vendor/EmbedIO-3.5.2/WebSockets/Internal/Rsv.cs
vendored
Normal file
22
Vendor/EmbedIO-3.5.2/WebSockets/Internal/Rsv.cs
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace EmbedIO.WebSockets.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates whether each RSV (RSV1, RSV2, and RSV3) of a WebSocket frame is non-zero.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The values of this enumeration are defined in
|
||||
/// <see href="http://tools.ietf.org/html/rfc6455#section-5.2">Section 5.2</see> of RFC 6455.
|
||||
/// </remarks>
|
||||
internal enum Rsv : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Equivalent to numeric value 0. Indicates zero.
|
||||
/// </summary>
|
||||
Off = 0x0,
|
||||
|
||||
/// <summary>
|
||||
/// Equivalent to numeric value 1. Indicates non-zero.
|
||||
/// </summary>
|
||||
On = 0x1,
|
||||
}
|
||||
}
|
||||
66
Vendor/EmbedIO-3.5.2/WebSockets/Internal/StreamExtensions.cs
vendored
Normal file
66
Vendor/EmbedIO-3.5.2/WebSockets/Internal/StreamExtensions.cs
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace EmbedIO.WebSockets.Internal
|
||||
{
|
||||
internal static class StreamExtensions
|
||||
{
|
||||
private static readonly byte[] LastByte = { 0x00 };
|
||||
|
||||
// Compresses or decompresses a stream using the specified compression method.
|
||||
public static async Task<MemoryStream> CompressAsync(
|
||||
this Stream @this,
|
||||
CompressionMethod method,
|
||||
bool compress,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@this.Position = 0;
|
||||
var targetStream = new MemoryStream();
|
||||
|
||||
switch (method)
|
||||
{
|
||||
case CompressionMethod.Deflate:
|
||||
if (compress)
|
||||
{
|
||||
using var compressor = new DeflateStream(targetStream, CompressionMode.Compress, true);
|
||||
await @this.CopyToAsync(compressor, 1024, cancellationToken).ConfigureAwait(false);
|
||||
await @this.CopyToAsync(compressor).ConfigureAwait(false);
|
||||
|
||||
// WebSocket use this
|
||||
targetStream.Write(LastByte, 0, 1);
|
||||
targetStream.Position = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
using var compressor = new DeflateStream(@this, CompressionMode.Decompress);
|
||||
await compressor.CopyToAsync(targetStream).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
break;
|
||||
case CompressionMethod.Gzip:
|
||||
if (compress)
|
||||
{
|
||||
using var compressor = new GZipStream(targetStream, CompressionMode.Compress, true);
|
||||
await @this.CopyToAsync(compressor).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
using var compressor = new GZipStream(@this, CompressionMode.Decompress);
|
||||
await compressor.CopyToAsync(targetStream).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
break;
|
||||
case CompressionMethod.None:
|
||||
await @this.CopyToAsync(targetStream).ConfigureAwait(false);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(method), method, null);
|
||||
}
|
||||
|
||||
return targetStream;
|
||||
}
|
||||
}
|
||||
}
|
||||
66
Vendor/EmbedIO-3.5.2/WebSockets/Internal/SystemWebSocket.cs
vendored
Normal file
66
Vendor/EmbedIO-3.5.2/WebSockets/Internal/SystemWebSocket.cs
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace EmbedIO.WebSockets.Internal
|
||||
{
|
||||
internal sealed class SystemWebSocket : IWebSocket
|
||||
{
|
||||
public SystemWebSocket(System.Net.WebSockets.WebSocket webSocket)
|
||||
{
|
||||
UnderlyingWebSocket = webSocket;
|
||||
}
|
||||
|
||||
~SystemWebSocket()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
public System.Net.WebSockets.WebSocket UnderlyingWebSocket { get; }
|
||||
|
||||
public WebSocketState State => UnderlyingWebSocket.State;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SendAsync(byte[] buffer, bool isText, CancellationToken cancellationToken = default)
|
||||
=> UnderlyingWebSocket.SendAsync(
|
||||
new ArraySegment<byte>(buffer),
|
||||
isText ? WebSocketMessageType.Text : WebSocketMessageType.Binary,
|
||||
true,
|
||||
cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task CloseAsync(CancellationToken cancellationToken = default) =>
|
||||
UnderlyingWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task CloseAsync(CloseStatusCode code, string? comment = null, CancellationToken cancellationToken = default)=>
|
||||
UnderlyingWebSocket.CloseAsync(MapCloseStatus(code), comment ?? string.Empty, cancellationToken);
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposing)
|
||||
return;
|
||||
|
||||
UnderlyingWebSocket.Dispose();
|
||||
}
|
||||
|
||||
private WebSocketCloseStatus MapCloseStatus(CloseStatusCode code) => code switch {
|
||||
CloseStatusCode.Normal => WebSocketCloseStatus.NormalClosure,
|
||||
CloseStatusCode.ProtocolError => WebSocketCloseStatus.ProtocolError,
|
||||
CloseStatusCode.InvalidData => WebSocketCloseStatus.InvalidPayloadData,
|
||||
CloseStatusCode.UnsupportedData => WebSocketCloseStatus.InvalidPayloadData,
|
||||
CloseStatusCode.PolicyViolation => WebSocketCloseStatus.PolicyViolation,
|
||||
CloseStatusCode.TooBig => WebSocketCloseStatus.MessageTooBig,
|
||||
CloseStatusCode.MandatoryExtension => WebSocketCloseStatus.MandatoryExtension,
|
||||
CloseStatusCode.ServerError => WebSocketCloseStatus.InternalServerError,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(code), code, null)
|
||||
};
|
||||
}
|
||||
}
|
||||
29
Vendor/EmbedIO-3.5.2/WebSockets/Internal/SystemWebSocketReceiveResult.cs
vendored
Normal file
29
Vendor/EmbedIO-3.5.2/WebSockets/Internal/SystemWebSocketReceiveResult.cs
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
namespace EmbedIO.WebSockets.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a wrapper around a regular WebSocketContext.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
internal sealed class SystemWebSocketReceiveResult : IWebSocketReceiveResult
|
||||
{
|
||||
private readonly System.Net.WebSockets.WebSocketReceiveResult _results;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SystemWebSocketReceiveResult"/> class.
|
||||
/// </summary>
|
||||
/// <param name="results">The results.</param>
|
||||
public SystemWebSocketReceiveResult(System.Net.WebSockets.WebSocketReceiveResult results)
|
||||
{
|
||||
_results = results;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Count => _results.Count;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool EndOfMessage=> _results.EndOfMessage;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int MessageType => (int) _results.MessageType;
|
||||
}
|
||||
}
|
||||
561
Vendor/EmbedIO-3.5.2/WebSockets/Internal/WebSocket.cs
vendored
Normal file
561
Vendor/EmbedIO-3.5.2/WebSockets/Internal/WebSocket.cs
vendored
Normal file
@@ -0,0 +1,561 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using System.Net.WebSockets;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using EmbedIO.Net.Internal;
|
||||
using Swan;
|
||||
using Swan.Logging;
|
||||
|
||||
namespace EmbedIO.WebSockets.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements the WebSocket interface.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The WebSocket class provides a set of methods and properties for two-way communication using
|
||||
/// the WebSocket protocol (<see href="http://tools.ietf.org/html/rfc6455">RFC 6455</see>).
|
||||
/// </remarks>
|
||||
internal sealed class WebSocket : IWebSocket
|
||||
{
|
||||
public const string SupportedVersion = "13";
|
||||
|
||||
private readonly object _stateSyncRoot = new ();
|
||||
private readonly ConcurrentQueue<MessageEventArgs> _messageEventQueue = new ();
|
||||
private readonly Action _closeConnection;
|
||||
private readonly TimeSpan _waitTime = TimeSpan.FromSeconds(1);
|
||||
|
||||
private volatile WebSocketState _readyState;
|
||||
private AutoResetEvent? _exitReceiving;
|
||||
private FragmentBuffer? _fragmentsBuffer;
|
||||
private volatile bool _inMessage;
|
||||
private AutoResetEvent? _receivePong;
|
||||
private Stream? _stream;
|
||||
|
||||
private WebSocket(HttpConnection connection)
|
||||
{
|
||||
_closeConnection = connection.ForceClose;
|
||||
_stream = connection.Stream;
|
||||
_readyState = WebSocketState.Open;
|
||||
}
|
||||
|
||||
~WebSocket()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the <see cref="WebSocket"/> receives a message.
|
||||
/// </summary>
|
||||
public event EventHandler<MessageEventArgs>? OnMessage;
|
||||
|
||||
/// <inheritdoc />
|
||||
public WebSocketState State => _readyState;
|
||||
|
||||
internal CompressionMethod Compression { get; } = CompressionMethod.None;
|
||||
|
||||
internal bool EmitOnPing { get; set; }
|
||||
|
||||
internal bool InContinuation { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SendAsync(byte[] buffer, bool isText, CancellationToken cancellationToken) => SendAsync(buffer, isText ? Opcode.Text : Opcode.Binary, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task CloseAsync(CancellationToken cancellationToken = default) => CloseAsync(CloseStatusCode.Normal, cancellationToken: cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task CloseAsync(
|
||||
CloseStatusCode code = CloseStatusCode.Undefined,
|
||||
string? reason = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
bool CheckParametersForClose()
|
||||
{
|
||||
if (code == CloseStatusCode.NoStatus && !string.IsNullOrEmpty(reason))
|
||||
{
|
||||
"'code' cannot have a reason.".Trace(nameof(WebSocket));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (code == CloseStatusCode.MandatoryExtension)
|
||||
{
|
||||
"'code' cannot be used by a server.".Trace(nameof(WebSocket));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(reason) && Encoding.UTF8.GetBytes(reason).Length > 123)
|
||||
{
|
||||
"The size of 'reason' is greater than the allowable max size.".Trace(nameof(WebSocket));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_readyState != WebSocketState.Open)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (code != CloseStatusCode.Undefined && !CheckParametersForClose())
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (code == CloseStatusCode.NoStatus)
|
||||
{
|
||||
return InternalCloseAsync(cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
var send = !IsOpcodeReserved(code);
|
||||
return InternalCloseAsync(new PayloadData((ushort)code, reason), send, send, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a ping using the WebSocket connection.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the <see cref="WebSocket"/> receives a pong to this ping in a time;
|
||||
/// otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public Task<bool> PingAsync() => PingAsync(WebSocketFrame.EmptyPingBytes, _waitTime);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a ping with the specified <paramref name="message"/> using the WebSocket connection.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the <see cref="WebSocket"/> receives a pong to this ping in a time;
|
||||
/// otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
/// <param name="message">
|
||||
/// A <see cref="string"/> that represents the message to send.
|
||||
/// </param>
|
||||
public Task<bool> PingAsync(string message)
|
||||
{
|
||||
if (string.IsNullOrEmpty(message))
|
||||
{
|
||||
return PingAsync();
|
||||
}
|
||||
|
||||
var data = Encoding.UTF8.GetBytes(message);
|
||||
|
||||
if (data.Length <= 125)
|
||||
{
|
||||
return PingAsync(WebSocketFrame.CreatePingFrame(data).ToArray(), _waitTime);
|
||||
}
|
||||
|
||||
"A message has greater than the allowable max size.".Error(nameof(PingAsync));
|
||||
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends binary <paramref name="data" /> using the WebSocket connection.
|
||||
/// </summary>
|
||||
/// <param name="data">An array of <see cref="byte" /> that represents the binary data to send.</param>
|
||||
/// <param name="opcode">The opcode.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>
|
||||
/// A task that represents the asynchronous of send
|
||||
/// binary data using websocket.
|
||||
/// </returns>
|
||||
#pragma warning disable CA1801 // Unused parameter
|
||||
public async Task SendAsync(byte[] data, Opcode opcode, CancellationToken cancellationToken = default)
|
||||
#pragma warning restore CA1801
|
||||
{
|
||||
if (_readyState != WebSocketState.Open)
|
||||
{
|
||||
throw new WebSocketException(CloseStatusCode.Normal, $"This operation isn\'t available in: {_readyState}");
|
||||
}
|
||||
|
||||
using var stream = new WebSocketStream(data, opcode, Compression);
|
||||
foreach (var frame in stream.GetFrames())
|
||||
{
|
||||
await Send(frame).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
internal static async Task<WebSocket> AcceptAsync(HttpListenerContext httpContext, string acceptedProtocol)
|
||||
{
|
||||
static string CreateResponseKey(string clientKey)
|
||||
{
|
||||
const string Guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
|
||||
var buff = new StringBuilder(clientKey, 64).Append(Guid);
|
||||
#pragma warning disable CA5350 // Do Not Use Weak Cryptographic Algorithms
|
||||
using var sha1 = SHA1.Create();
|
||||
return Convert.ToBase64String(sha1.ComputeHash(Encoding.UTF8.GetBytes(buff.ToString())));
|
||||
#pragma warning restore CA5350 // Do Not Use Weak Cryptographic Algorithms
|
||||
}
|
||||
|
||||
var requestHeaders = httpContext.Request.Headers;
|
||||
|
||||
var webSocketKey = requestHeaders[HttpHeaderNames.SecWebSocketKey];
|
||||
|
||||
if (string.IsNullOrEmpty(webSocketKey))
|
||||
{
|
||||
throw new WebSocketException(CloseStatusCode.ProtocolError, $"Includes no {HttpHeaderNames.SecWebSocketKey} header, or it has an invalid value.");
|
||||
}
|
||||
|
||||
var webSocketVersion = requestHeaders[HttpHeaderNames.SecWebSocketVersion];
|
||||
|
||||
if (webSocketVersion == null || webSocketVersion != SupportedVersion)
|
||||
{
|
||||
throw new WebSocketException(CloseStatusCode.ProtocolError, $"Includes no {HttpHeaderNames.SecWebSocketVersion} header, or it has an invalid value.");
|
||||
}
|
||||
|
||||
var handshakeResponse = new WebSocketHandshakeResponse(httpContext);
|
||||
|
||||
handshakeResponse.Headers[HttpHeaderNames.SecWebSocketAccept] = CreateResponseKey(webSocketKey);
|
||||
|
||||
if (acceptedProtocol.Length > 0)
|
||||
{
|
||||
handshakeResponse.Headers[HttpHeaderNames.SecWebSocketProtocol] = acceptedProtocol;
|
||||
}
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(handshakeResponse.ToString());
|
||||
await httpContext.Connection.Stream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
|
||||
|
||||
// Signal the original response that headers have been sent.
|
||||
httpContext.HttpListenerResponse.HeadersSent = true;
|
||||
|
||||
var socket = new WebSocket(httpContext.Connection);
|
||||
socket.Open();
|
||||
return socket;
|
||||
}
|
||||
|
||||
internal async Task<bool> PingAsync(byte[] frameAsBytes, TimeSpan timeout)
|
||||
{
|
||||
if (_readyState != WebSocketState.Open)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
await _stream.WriteAsync(frameAsBytes, 0, frameAsBytes.Length).ConfigureAwait(false);
|
||||
|
||||
return _receivePong != null && _receivePong.WaitOne(timeout);
|
||||
}
|
||||
|
||||
private static bool IsOpcodeReserved(CloseStatusCode code)
|
||||
=> code == CloseStatusCode.Undefined
|
||||
|| code == CloseStatusCode.NoStatus
|
||||
|| code == CloseStatusCode.Abnormal
|
||||
|| code == CloseStatusCode.TlsHandshakeFailure;
|
||||
|
||||
#pragma warning disable CA1801 // Unused parameter
|
||||
private void Dispose(bool disposing)
|
||||
#pragma warning restore CA1801
|
||||
{
|
||||
try
|
||||
{
|
||||
InternalCloseAsync(new PayloadData((ushort)CloseStatusCode.Away)).Await();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InternalCloseAsync(
|
||||
PayloadData? payloadData = null,
|
||||
bool send = true,
|
||||
bool receive = true,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
lock (_stateSyncRoot)
|
||||
{
|
||||
if (_readyState == WebSocketState.CloseReceived || _readyState == WebSocketState.CloseSent)
|
||||
{
|
||||
"The closing is already in progress.".Trace(nameof(InternalCloseAsync));
|
||||
return;
|
||||
}
|
||||
|
||||
if (_readyState == WebSocketState.Closed)
|
||||
{
|
||||
"The connection has been closed.".Trace(nameof(InternalCloseAsync));
|
||||
return;
|
||||
}
|
||||
|
||||
send = send && _readyState == WebSocketState.Open;
|
||||
receive = receive && send;
|
||||
|
||||
_readyState = WebSocketState.CloseSent;
|
||||
}
|
||||
|
||||
"Begin closing the connection.".Trace(nameof(InternalCloseAsync));
|
||||
|
||||
var bytes = send ? WebSocketFrame.CreateCloseFrame(payloadData).ToArray() : null;
|
||||
await CloseHandshakeAsync(bytes, receive, cancellationToken).ConfigureAwait(false);
|
||||
ReleaseResources();
|
||||
|
||||
"End closing the connection.".Trace(nameof(InternalCloseAsync));
|
||||
|
||||
lock (_stateSyncRoot)
|
||||
{
|
||||
_readyState = WebSocketState.Closed;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CloseHandshakeAsync(
|
||||
byte[]? frameAsBytes,
|
||||
bool receive,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var sent = frameAsBytes != null;
|
||||
|
||||
if (sent)
|
||||
{
|
||||
await _stream.WriteAsync(frameAsBytes, 0, frameAsBytes.Length, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (receive && sent)
|
||||
{
|
||||
_ = _exitReceiving?.WaitOne(_waitTime);
|
||||
}
|
||||
}
|
||||
|
||||
private void Fatal(string message, Exception? exception = null)
|
||||
=> Fatal(message, (exception as WebSocketException)?.Code ?? CloseStatusCode.Abnormal);
|
||||
|
||||
private void Fatal(string message, CloseStatusCode code)
|
||||
=> InternalCloseAsync(new PayloadData((ushort)code, message), !IsOpcodeReserved(code), false).Await();
|
||||
|
||||
private void Message()
|
||||
{
|
||||
if (_inMessage || _messageEventQueue.IsEmpty || _readyState != WebSocketState.Open)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_inMessage = true;
|
||||
|
||||
if (_messageEventQueue.TryDequeue(out var e))
|
||||
{
|
||||
Messages(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void Messages(MessageEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
OnMessage?.Invoke(this, e);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ex.Log(nameof(WebSocket));
|
||||
}
|
||||
|
||||
if (!_messageEventQueue.TryDequeue(out e) || _readyState != WebSocketState.Open)
|
||||
{
|
||||
_inMessage = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_ = Task.Run(() => Messages(e));
|
||||
}
|
||||
|
||||
private void Open()
|
||||
{
|
||||
_inMessage = true;
|
||||
StartReceiving();
|
||||
|
||||
if (!_messageEventQueue.TryDequeue(out var e) || _readyState != WebSocketState.Open)
|
||||
{
|
||||
_inMessage = false;
|
||||
return;
|
||||
}
|
||||
|
||||
Messages(e);
|
||||
}
|
||||
|
||||
private Task ProcessCloseFrame(WebSocketFrame frame) => InternalCloseAsync(frame.PayloadData, !frame.PayloadData.HasReservedCode, false);
|
||||
|
||||
private async Task ProcessDataFrame(WebSocketFrame frame)
|
||||
{
|
||||
if (frame.IsCompressed)
|
||||
{
|
||||
using var ms = await frame.PayloadData.ApplicationData.CompressAsync(Compression, false, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
_messageEventQueue.Enqueue(new MessageEventArgs(frame.Opcode, ms.ToArray()));
|
||||
}
|
||||
else
|
||||
{
|
||||
_messageEventQueue.Enqueue(new MessageEventArgs(frame));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessFragmentFrame(WebSocketFrame frame)
|
||||
{
|
||||
if (!InContinuation)
|
||||
{
|
||||
// Must process first fragment.
|
||||
if (frame.Opcode == Opcode.Cont)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_fragmentsBuffer = new FragmentBuffer(frame.Opcode, frame.IsCompressed);
|
||||
InContinuation = true;
|
||||
}
|
||||
|
||||
_fragmentsBuffer.AddPayload(frame.PayloadData.ApplicationData);
|
||||
|
||||
if (frame.Fin == Fin.Final)
|
||||
{
|
||||
using (_fragmentsBuffer)
|
||||
{
|
||||
_messageEventQueue.Enqueue(await _fragmentsBuffer.GetMessage(Compression).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
_fragmentsBuffer = null;
|
||||
InContinuation = false;
|
||||
}
|
||||
}
|
||||
|
||||
private Task ProcessPingFrame(WebSocketFrame frame)
|
||||
{
|
||||
if (EmitOnPing)
|
||||
{
|
||||
_messageEventQueue.Enqueue(new MessageEventArgs(frame));
|
||||
}
|
||||
|
||||
return Send(new WebSocketFrame(Opcode.Pong, frame.PayloadData));
|
||||
}
|
||||
|
||||
private void ProcessPongFrame()
|
||||
{
|
||||
_ = _receivePong?.Set();
|
||||
"Received a pong.".Trace(nameof(ProcessPongFrame));
|
||||
}
|
||||
|
||||
private async Task<bool> ProcessReceivedFrame(WebSocketFrame frame)
|
||||
{
|
||||
if (frame.IsFragment)
|
||||
{
|
||||
await ProcessFragmentFrame(frame).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (frame.Opcode)
|
||||
{
|
||||
case Opcode.Text:
|
||||
case Opcode.Binary:
|
||||
await ProcessDataFrame(frame).ConfigureAwait(false);
|
||||
break;
|
||||
case Opcode.Ping:
|
||||
await ProcessPingFrame(frame).ConfigureAwait(false);
|
||||
break;
|
||||
case Opcode.Pong:
|
||||
ProcessPongFrame();
|
||||
break;
|
||||
case Opcode.Close:
|
||||
await ProcessCloseFrame(frame).ConfigureAwait(false);
|
||||
break;
|
||||
default:
|
||||
Fatal($"Unsupported frame received: {frame.PrintToString()}", CloseStatusCode.PolicyViolation);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ReleaseResources()
|
||||
{
|
||||
_closeConnection();
|
||||
_stream = null;
|
||||
|
||||
if (_fragmentsBuffer != null)
|
||||
{
|
||||
_fragmentsBuffer.Dispose();
|
||||
_fragmentsBuffer = null;
|
||||
InContinuation = false;
|
||||
}
|
||||
|
||||
if (_receivePong != null)
|
||||
{
|
||||
_receivePong.Dispose();
|
||||
_receivePong = null;
|
||||
}
|
||||
|
||||
if (_exitReceiving == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_exitReceiving.Dispose();
|
||||
_exitReceiving = null;
|
||||
}
|
||||
|
||||
private Task Send(WebSocketFrame frame)
|
||||
{
|
||||
lock (_stateSyncRoot)
|
||||
{
|
||||
if (_readyState != WebSocketState.Open)
|
||||
{
|
||||
"The sending has been interrupted.".Error(nameof(Send));
|
||||
return Task.Delay(0);
|
||||
}
|
||||
}
|
||||
|
||||
var frameAsBytes = frame.ToArray();
|
||||
return _stream.WriteAsync(frameAsBytes, 0, frameAsBytes.Length);
|
||||
}
|
||||
|
||||
private void StartReceiving()
|
||||
{
|
||||
while (_messageEventQueue.TryDequeue(out _))
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
_exitReceiving = new AutoResetEvent(false);
|
||||
_receivePong = new AutoResetEvent(false);
|
||||
|
||||
var frameStream = new WebSocketFrameStream(_stream);
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
while (_readyState == WebSocketState.Open)
|
||||
{
|
||||
try
|
||||
{
|
||||
var frame = await frameStream.ReadFrameAsync(this).ConfigureAwait(false);
|
||||
|
||||
if (frame == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await ProcessReceivedFrame(frame).ConfigureAwait(false);
|
||||
|
||||
if (!result || _readyState == WebSocketState.Closed)
|
||||
{
|
||||
_ = _exitReceiving?.Set();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_ = Task.Run(Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Fatal("An exception has occurred while receiving.", ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
100
Vendor/EmbedIO-3.5.2/WebSockets/Internal/WebSocketContext.cs
vendored
Normal file
100
Vendor/EmbedIO-3.5.2/WebSockets/Internal/WebSocketContext.cs
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Net;
|
||||
using System.Security.Principal;
|
||||
using System.Threading;
|
||||
using EmbedIO.Sessions;
|
||||
using EmbedIO.Utilities;
|
||||
|
||||
namespace EmbedIO.WebSockets.Internal
|
||||
{
|
||||
internal sealed class WebSocketContext : IWebSocketContext
|
||||
{
|
||||
internal WebSocketContext(
|
||||
IHttpContextImpl httpContext,
|
||||
string webSocketVersion,
|
||||
IEnumerable<string> requestedProtocols,
|
||||
string acceptedProtocol,
|
||||
IWebSocket webSocket,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Id = UniqueIdGenerator.GetNext();
|
||||
CancellationToken = cancellationToken;
|
||||
HttpContextId = httpContext.Id;
|
||||
Session = httpContext.Session;
|
||||
Items = httpContext.Items;
|
||||
LocalEndPoint = httpContext.LocalEndPoint;
|
||||
RemoteEndPoint = httpContext.RemoteEndPoint;
|
||||
RequestUri = httpContext.Request.Url;
|
||||
Headers = httpContext.Request.Headers;
|
||||
Origin = Headers[HttpHeaderNames.Origin];
|
||||
RequestedProtocols = requestedProtocols;
|
||||
AcceptedProtocol = acceptedProtocol;
|
||||
WebSocketVersion = webSocketVersion;
|
||||
Cookies = httpContext.Request.Cookies;
|
||||
User = httpContext.User;
|
||||
IsAuthenticated = httpContext.User.Identity.IsAuthenticated;
|
||||
IsLocal = httpContext.Request.IsLocal;
|
||||
IsSecureConnection = httpContext.Request.IsSecureConnection;
|
||||
WebSocket = webSocket;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Id { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public CancellationToken CancellationToken { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string HttpContextId { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ISessionProxy Session { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDictionary<object, object> Items { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IPEndPoint LocalEndPoint { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IPEndPoint RemoteEndPoint { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Uri RequestUri { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public NameValueCollection Headers { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Origin { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<string> RequestedProtocols { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string AcceptedProtocol { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string WebSocketVersion { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICookieCollection Cookies { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IPrincipal User { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsAuthenticated { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsLocal { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsSecureConnection { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IWebSocket WebSocket { get; }
|
||||
}
|
||||
}
|
||||
239
Vendor/EmbedIO-3.5.2/WebSockets/Internal/WebSocketFrame.cs
vendored
Normal file
239
Vendor/EmbedIO-3.5.2/WebSockets/Internal/WebSocketFrame.cs
vendored
Normal file
@@ -0,0 +1,239 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using EmbedIO.Net.Internal;
|
||||
using Swan;
|
||||
|
||||
namespace EmbedIO.WebSockets.Internal
|
||||
{
|
||||
internal class WebSocketFrame
|
||||
{
|
||||
internal static readonly byte[] EmptyPingBytes = CreatePingFrame().ToArray();
|
||||
|
||||
internal WebSocketFrame(Opcode opcode, PayloadData payloadData)
|
||||
: this(Fin.Final, opcode, payloadData)
|
||||
{
|
||||
}
|
||||
|
||||
internal WebSocketFrame(Fin fin, Opcode opcode, byte[] data, bool compressed)
|
||||
: this(fin, opcode, new PayloadData(data), compressed)
|
||||
{
|
||||
}
|
||||
|
||||
internal WebSocketFrame(
|
||||
Fin fin,
|
||||
Opcode opcode,
|
||||
PayloadData payloadData,
|
||||
bool compressed = false)
|
||||
{
|
||||
Fin = fin;
|
||||
Rsv1 = IsOpcodeData(opcode) && compressed ? Rsv.On : Rsv.Off;
|
||||
Rsv2 = Rsv.Off;
|
||||
Rsv3 = Rsv.Off;
|
||||
Opcode = opcode;
|
||||
|
||||
var len = payloadData.Length;
|
||||
if (len < 126)
|
||||
{
|
||||
PayloadLength = (byte)len;
|
||||
ExtendedPayloadLength = Array.Empty<byte>();
|
||||
}
|
||||
else if (len < 0x010000)
|
||||
{
|
||||
PayloadLength = 126;
|
||||
ExtendedPayloadLength = ((ushort)len).ToByteArray(Endianness.Big);
|
||||
}
|
||||
else
|
||||
{
|
||||
PayloadLength = 127;
|
||||
ExtendedPayloadLength = len.ToByteArray(Endianness.Big);
|
||||
}
|
||||
|
||||
Mask = Mask.Off;
|
||||
MaskingKey = Array.Empty<byte>();
|
||||
PayloadData = payloadData;
|
||||
}
|
||||
|
||||
internal WebSocketFrame(
|
||||
Fin fin,
|
||||
Rsv rsv1,
|
||||
Rsv rsv2,
|
||||
Rsv rsv3,
|
||||
Opcode opcode,
|
||||
Mask mask,
|
||||
byte payloadLength)
|
||||
{
|
||||
Fin = fin;
|
||||
Rsv1 = rsv1;
|
||||
Rsv2 = rsv2;
|
||||
Rsv3 = rsv3;
|
||||
Opcode = opcode;
|
||||
Mask = mask;
|
||||
PayloadLength = payloadLength;
|
||||
}
|
||||
|
||||
public byte[]? ExtendedPayloadLength { get; internal set; }
|
||||
|
||||
public Fin Fin { get; internal set; }
|
||||
|
||||
public bool IsCompressed => Rsv1 == Rsv.On;
|
||||
|
||||
public bool IsFragment => Fin == Fin.More || Opcode == Opcode.Cont;
|
||||
|
||||
public bool IsMasked => Mask == Mask.On;
|
||||
|
||||
public Mask Mask { get; internal set; }
|
||||
|
||||
public byte[] MaskingKey { get; internal set; }
|
||||
|
||||
public Opcode Opcode { get; internal set; }
|
||||
|
||||
public PayloadData PayloadData { get; internal set; }
|
||||
|
||||
public byte PayloadLength { get; internal set; }
|
||||
|
||||
public Rsv Rsv1 { get; internal set; }
|
||||
|
||||
public Rsv Rsv2 { get; internal set; }
|
||||
|
||||
public Rsv Rsv3 { get; internal set; }
|
||||
|
||||
internal int ExtendedPayloadLengthCount => PayloadLength < 126 ? 0 : (PayloadLength == 126 ? 2 : 8);
|
||||
|
||||
internal ulong FullPayloadLength => PayloadLength < 126
|
||||
? PayloadLength
|
||||
: PayloadLength == 126
|
||||
? BitConverter.ToUInt16(ExtendedPayloadLength.ToHostOrder(Endianness.Big), 0)
|
||||
: BitConverter.ToUInt64(ExtendedPayloadLength.ToHostOrder(Endianness.Big), 0);
|
||||
|
||||
public IEnumerator<byte> GetEnumerator() => ((IEnumerable<byte>)ToArray()).GetEnumerator();
|
||||
|
||||
public string PrintToString()
|
||||
{
|
||||
// Payload Length
|
||||
var payloadLen = PayloadLength;
|
||||
|
||||
// Extended Payload Length
|
||||
var extPayloadLen = payloadLen > 125 ? FullPayloadLength.ToString(CultureInfo.InvariantCulture) : string.Empty;
|
||||
|
||||
// Masking Key
|
||||
var maskingKey = BitConverter.ToString(MaskingKey);
|
||||
|
||||
// Payload Data
|
||||
var payload = payloadLen == 0
|
||||
? string.Empty
|
||||
: payloadLen > 125
|
||||
? "---"
|
||||
: Opcode == Opcode.Text && !(IsFragment || IsMasked || IsCompressed)
|
||||
? PayloadData.ApplicationData.ToArray().ToText()
|
||||
: PayloadData.ToString();
|
||||
|
||||
return $@"
|
||||
FIN: {Fin}
|
||||
RSV1: {Rsv1}
|
||||
RSV2: {Rsv2}
|
||||
RSV3: {Rsv3}
|
||||
Opcode: {Opcode}
|
||||
MASK: {Mask}
|
||||
Payload Length: {payloadLen}
|
||||
Extended Payload Length: {extPayloadLen}
|
||||
Masking Key: {maskingKey}
|
||||
Payload Data: {payload}";
|
||||
}
|
||||
|
||||
public byte[] ToArray()
|
||||
{
|
||||
using var buff = new MemoryStream();
|
||||
var header = (int)Fin;
|
||||
|
||||
header = (header << 1) + (int)Rsv1;
|
||||
header = (header << 1) + (int)Rsv2;
|
||||
header = (header << 1) + (int)Rsv3;
|
||||
header = (header << 4) + (int)Opcode;
|
||||
header = (header << 1) + (int)Mask;
|
||||
header = (header << 7) + PayloadLength;
|
||||
buff.Write(((ushort)header).ToByteArray(Endianness.Big), 0, 2);
|
||||
|
||||
if (PayloadLength > 125)
|
||||
{
|
||||
buff.Write(ExtendedPayloadLength, 0, PayloadLength == 126 ? 2 : 8);
|
||||
}
|
||||
|
||||
if (Mask == Mask.On)
|
||||
{
|
||||
buff.Write(MaskingKey, 0, 4);
|
||||
}
|
||||
|
||||
if (PayloadLength > 0)
|
||||
{
|
||||
var bytes = PayloadData.ToArray();
|
||||
if (PayloadLength < 127)
|
||||
{
|
||||
buff.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
using var input = new MemoryStream(bytes);
|
||||
input.CopyTo(buff, 1024);
|
||||
}
|
||||
}
|
||||
|
||||
return buff.ToArray();
|
||||
}
|
||||
|
||||
public override string ToString() => BitConverter.ToString(ToArray());
|
||||
|
||||
internal static WebSocketFrame CreateCloseFrame(PayloadData? payloadData) => new (Fin.Final, Opcode.Close, payloadData ?? new PayloadData());
|
||||
|
||||
internal static WebSocketFrame CreatePingFrame() => new (Fin.Final, Opcode.Ping, new PayloadData());
|
||||
|
||||
internal static WebSocketFrame CreatePingFrame(byte[] data) => new (Fin.Final, Opcode.Ping, new PayloadData(data));
|
||||
|
||||
internal void Validate(WebSocket webSocket)
|
||||
{
|
||||
if (!IsMasked)
|
||||
{
|
||||
throw new WebSocketException(CloseStatusCode.ProtocolError, "A frame from a client isn't masked.");
|
||||
}
|
||||
|
||||
if (webSocket.InContinuation && (Opcode == Opcode.Text || Opcode == Opcode.Binary))
|
||||
{
|
||||
throw new WebSocketException(CloseStatusCode.ProtocolError,
|
||||
"A data frame has been received while receiving continuation frames.");
|
||||
}
|
||||
|
||||
if (IsCompressed && webSocket.Compression == CompressionMethod.None)
|
||||
{
|
||||
throw new WebSocketException(CloseStatusCode.ProtocolError,
|
||||
"A compressed frame has been received without any agreement for it.");
|
||||
}
|
||||
|
||||
if (Rsv2 == Rsv.On)
|
||||
{
|
||||
throw new WebSocketException(CloseStatusCode.ProtocolError,
|
||||
"The RSV2 of a frame is non-zero without any negotiation for it.");
|
||||
}
|
||||
|
||||
if (Rsv3 == Rsv.On)
|
||||
{
|
||||
throw new WebSocketException(CloseStatusCode.ProtocolError,
|
||||
"The RSV3 of a frame is non-zero without any negotiation for it.");
|
||||
}
|
||||
}
|
||||
|
||||
internal void Unmask()
|
||||
{
|
||||
if (Mask == Mask.Off)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Mask = Mask.Off;
|
||||
PayloadData.Mask(MaskingKey);
|
||||
MaskingKey = Array.Empty<byte>();
|
||||
}
|
||||
|
||||
private static bool IsOpcodeData(Opcode opcode) => opcode == Opcode.Text || opcode == Opcode.Binary;
|
||||
}
|
||||
}
|
||||
148
Vendor/EmbedIO-3.5.2/WebSockets/Internal/WebSocketFrameStream.cs
vendored
Normal file
148
Vendor/EmbedIO-3.5.2/WebSockets/Internal/WebSocketFrameStream.cs
vendored
Normal file
@@ -0,0 +1,148 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Swan;
|
||||
|
||||
namespace EmbedIO.WebSockets.Internal
|
||||
{
|
||||
internal class WebSocketFrameStream
|
||||
{
|
||||
private readonly bool _unmask;
|
||||
private readonly Stream? _stream;
|
||||
|
||||
public WebSocketFrameStream(Stream? stream, bool unmask = false)
|
||||
{
|
||||
_stream = stream;
|
||||
_unmask = unmask;
|
||||
}
|
||||
|
||||
internal async Task<WebSocketFrame?> ReadFrameAsync(WebSocket webSocket)
|
||||
{
|
||||
if (_stream == null) return null;
|
||||
|
||||
var frame = ProcessHeader(await _stream.ReadBytesAsync(2).ConfigureAwait(false));
|
||||
|
||||
await ReadExtendedPayloadLengthAsync(frame).ConfigureAwait(false);
|
||||
await ReadMaskingKeyAsync(frame).ConfigureAwait(false);
|
||||
await ReadPayloadDataAsync(frame).ConfigureAwait(false);
|
||||
|
||||
if (_unmask)
|
||||
frame.Unmask();
|
||||
|
||||
frame.Validate(webSocket);
|
||||
|
||||
frame.Unmask();
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
private static bool IsOpcodeData(byte opcode) => opcode == 0x1 || opcode == 0x2;
|
||||
|
||||
private static bool IsOpcodeControl(byte opcode) => opcode > 0x7 && opcode < 0x10;
|
||||
|
||||
private static WebSocketFrame ProcessHeader(byte[] header)
|
||||
{
|
||||
if (header.Length != 2)
|
||||
throw new WebSocketException("The header of a frame cannot be read from the stream.");
|
||||
|
||||
// FIN
|
||||
var fin = (header[0] & 0x80) == 0x80 ? Fin.Final : Fin.More;
|
||||
|
||||
// RSV1
|
||||
var rsv1 = (header[0] & 0x40) == 0x40 ? Rsv.On : Rsv.Off;
|
||||
|
||||
// RSV2
|
||||
var rsv2 = (header[0] & 0x20) == 0x20 ? Rsv.On : Rsv.Off;
|
||||
|
||||
// RSV3
|
||||
var rsv3 = (header[0] & 0x10) == 0x10 ? Rsv.On : Rsv.Off;
|
||||
|
||||
// Opcode
|
||||
var opcode = (byte)(header[0] & 0x0f);
|
||||
|
||||
// MASK
|
||||
var mask = (header[1] & 0x80) == 0x80 ? Mask.On : Mask.Off;
|
||||
|
||||
// Payload Length
|
||||
var payloadLen = (byte)(header[1] & 0x7f);
|
||||
|
||||
var err = !Enum.IsDefined(typeof(Opcode), opcode) ? "An unsupported opcode."
|
||||
: !IsOpcodeData(opcode) && rsv1 == Rsv.On ? "A non data frame is compressed."
|
||||
: IsOpcodeControl(opcode) && fin == Fin.More ? "A control frame is fragmented."
|
||||
: IsOpcodeControl(opcode) && payloadLen > 125 ? "A control frame has a long payload length."
|
||||
: null;
|
||||
|
||||
if (err != null)
|
||||
throw new WebSocketException(CloseStatusCode.ProtocolError, err);
|
||||
|
||||
return new WebSocketFrame(fin, rsv1, rsv2, rsv3, (Opcode)opcode, mask, payloadLen);
|
||||
}
|
||||
|
||||
private async Task ReadExtendedPayloadLengthAsync(WebSocketFrame frame)
|
||||
{
|
||||
var len = frame.ExtendedPayloadLengthCount;
|
||||
|
||||
if (len == 0)
|
||||
{
|
||||
frame.ExtendedPayloadLength = Array.Empty<byte>();
|
||||
return;
|
||||
}
|
||||
|
||||
var bytes = await _stream.ReadBytesAsync(len).ConfigureAwait(false);
|
||||
|
||||
if (bytes.Length != len)
|
||||
{
|
||||
throw new WebSocketException(
|
||||
"The extended payload length of a frame cannot be read from the stream.");
|
||||
}
|
||||
|
||||
frame.ExtendedPayloadLength = bytes;
|
||||
}
|
||||
|
||||
private async Task ReadMaskingKeyAsync(WebSocketFrame frame)
|
||||
{
|
||||
var len = frame.IsMasked ? 4 : 0;
|
||||
|
||||
if (len == 0)
|
||||
{
|
||||
frame.MaskingKey = Array.Empty<byte>();
|
||||
return;
|
||||
}
|
||||
|
||||
var bytes = await _stream.ReadBytesAsync(len).ConfigureAwait(false);
|
||||
if (bytes.Length != len)
|
||||
{
|
||||
throw new WebSocketException(
|
||||
"The masking key of a frame cannot be read from the stream.");
|
||||
}
|
||||
|
||||
frame.MaskingKey = bytes;
|
||||
}
|
||||
|
||||
private async Task ReadPayloadDataAsync(WebSocketFrame frame)
|
||||
{
|
||||
var len = frame.FullPayloadLength;
|
||||
if (len == 0)
|
||||
{
|
||||
frame.PayloadData = new PayloadData();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (len > PayloadData.MaxLength)
|
||||
throw new WebSocketException(CloseStatusCode.TooBig, "A frame has a long payload length.");
|
||||
|
||||
var bytes = frame.PayloadLength < 127
|
||||
? await _stream.ReadBytesAsync((int)len).ConfigureAwait(false)
|
||||
: await _stream.ReadBytesAsync((int)len, 1024).ConfigureAwait(false);
|
||||
|
||||
if (bytes.Length != (int)len)
|
||||
{
|
||||
throw new WebSocketException(
|
||||
"The payload data of a frame cannot be read from the stream.");
|
||||
}
|
||||
|
||||
frame.PayloadData = new PayloadData(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
29
Vendor/EmbedIO-3.5.2/WebSockets/Internal/WebSocketReceiveResult.cs
vendored
Normal file
29
Vendor/EmbedIO-3.5.2/WebSockets/Internal/WebSocketReceiveResult.cs
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
|
||||
namespace EmbedIO.WebSockets.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a WS Receive result.
|
||||
/// </summary>
|
||||
internal sealed class WebSocketReceiveResult : IWebSocketReceiveResult
|
||||
{
|
||||
internal WebSocketReceiveResult(int count, Opcode code)
|
||||
{
|
||||
if (count < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(count));
|
||||
|
||||
Count = count;
|
||||
EndOfMessage = code == Opcode.Close;
|
||||
MessageType = code == Opcode.Text ? 0 : 1;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Count { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool EndOfMessage { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public int MessageType { get; }
|
||||
}
|
||||
}
|
||||
91
Vendor/EmbedIO-3.5.2/WebSockets/Internal/WebSocketStream.cs
vendored
Normal file
91
Vendor/EmbedIO-3.5.2/WebSockets/Internal/WebSocketStream.cs
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using Swan;
|
||||
|
||||
namespace EmbedIO.WebSockets.Internal
|
||||
{
|
||||
internal class WebSocketStream : MemoryStream
|
||||
{
|
||||
internal const int FragmentLength = 1016;
|
||||
|
||||
private readonly CompressionMethod _compression;
|
||||
private readonly Opcode _opcode;
|
||||
|
||||
public WebSocketStream(byte[] data, Opcode opcode, CompressionMethod compression)
|
||||
: base(data)
|
||||
{
|
||||
_compression = compression;
|
||||
_opcode = opcode;
|
||||
}
|
||||
|
||||
public IEnumerable<WebSocketFrame> GetFrames()
|
||||
{
|
||||
var compressed = _compression != CompressionMethod.None;
|
||||
var stream = compressed
|
||||
? this.CompressAsync(_compression, true, CancellationToken.None).Await()
|
||||
: this;
|
||||
|
||||
var len = stream.Length;
|
||||
|
||||
/* Not fragmented */
|
||||
|
||||
if (len == 0)
|
||||
{
|
||||
yield return new WebSocketFrame(Fin.Final, _opcode, Array.Empty<byte>(), compressed);
|
||||
yield break;
|
||||
}
|
||||
|
||||
var quo = len / FragmentLength;
|
||||
var rem = (int)(len % FragmentLength);
|
||||
|
||||
byte[] buff;
|
||||
|
||||
if (quo == 0)
|
||||
{
|
||||
buff = new byte[rem];
|
||||
|
||||
if (stream.Read(buff, 0, rem) == rem)
|
||||
yield return new WebSocketFrame(Fin.Final, _opcode, buff, compressed);
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
buff = new byte[FragmentLength];
|
||||
if (quo == 1 && rem == 0)
|
||||
{
|
||||
if (stream.Read(buff, 0, FragmentLength) == FragmentLength)
|
||||
yield return new WebSocketFrame(Fin.Final, _opcode, buff, compressed);
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
/* Send fragmented */
|
||||
|
||||
// Begin
|
||||
if (stream.Read(buff, 0, FragmentLength) != FragmentLength)
|
||||
yield break;
|
||||
|
||||
yield return new WebSocketFrame(Fin.More, _opcode, buff, compressed);
|
||||
|
||||
var n = rem == 0 ? quo - 2 : quo - 1;
|
||||
for (var i = 0; i < n; i++)
|
||||
{
|
||||
if (stream.Read(buff, 0, FragmentLength) != FragmentLength)
|
||||
yield break;
|
||||
|
||||
yield return new WebSocketFrame(Fin.More, Opcode.Cont, buff, compressed);
|
||||
}
|
||||
|
||||
// End
|
||||
if (rem == 0)
|
||||
rem = FragmentLength;
|
||||
else
|
||||
buff = new byte[rem];
|
||||
|
||||
if (stream.Read(buff, 0, rem) == rem)
|
||||
yield return new WebSocketFrame(Fin.Final, Opcode.Cont, buff, compressed);
|
||||
}
|
||||
}
|
||||
}
|
||||
43
Vendor/EmbedIO-3.5.2/WebSockets/Opcode.cs
vendored
Normal file
43
Vendor/EmbedIO-3.5.2/WebSockets/Opcode.cs
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
namespace EmbedIO.WebSockets
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates the WebSocket frame type.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The values of this enumeration are defined in
|
||||
/// <see href="http://tools.ietf.org/html/rfc6455#section-5.2">
|
||||
/// Section 5.2</see> of RFC 6455.
|
||||
/// </remarks>
|
||||
public enum Opcode : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Equivalent to numeric value 0. Indicates continuation frame.
|
||||
/// </summary>
|
||||
Cont = 0x0,
|
||||
|
||||
/// <summary>
|
||||
/// Equivalent to numeric value 1. Indicates text frame.
|
||||
/// </summary>
|
||||
Text = 0x1,
|
||||
|
||||
/// <summary>
|
||||
/// Equivalent to numeric value 2. Indicates binary frame.
|
||||
/// </summary>
|
||||
Binary = 0x2,
|
||||
|
||||
/// <summary>
|
||||
/// Equivalent to numeric value 8. Indicates connection close frame.
|
||||
/// </summary>
|
||||
Close = 0x8,
|
||||
|
||||
/// <summary>
|
||||
/// Equivalent to numeric value 9. Indicates ping frame.
|
||||
/// </summary>
|
||||
Ping = 0x9,
|
||||
|
||||
/// <summary>
|
||||
/// Equivalent to numeric value 10. Indicates pong frame.
|
||||
/// </summary>
|
||||
Pong = 0xa,
|
||||
}
|
||||
}
|
||||
51
Vendor/EmbedIO-3.5.2/WebSockets/WebSocketException.cs
vendored
Normal file
51
Vendor/EmbedIO-3.5.2/WebSockets/WebSocketException.cs
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
|
||||
namespace EmbedIO.WebSockets
|
||||
{
|
||||
/// <summary>
|
||||
/// The exception that is thrown when a WebSocket gets a fatal error.
|
||||
/// </summary>
|
||||
#pragma warning disable CA1032 // Implement standard exception constructors - this class doesn't need public constructors.
|
||||
public class WebSocketException : Exception
|
||||
#pragma warning restore CA1032
|
||||
{
|
||||
internal WebSocketException(string? message = null)
|
||||
: this(CloseStatusCode.Abnormal, message)
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
|
||||
internal WebSocketException(CloseStatusCode code, Exception? innerException = null)
|
||||
: this(code, null, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
internal WebSocketException(CloseStatusCode code, string? message, Exception? innerException = null)
|
||||
: base(message ?? GetMessage(code), innerException)
|
||||
{
|
||||
Code = code;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the status code indicating the cause of the exception.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// One of the <see cref="CloseStatusCode"/> enum values, represents the status code
|
||||
/// indicating the cause of the exception.
|
||||
/// </value>
|
||||
public CloseStatusCode Code { get; }
|
||||
|
||||
internal static string GetMessage(CloseStatusCode code) => code switch {
|
||||
CloseStatusCode.ProtocolError => "A WebSocket protocol error has occurred.",
|
||||
CloseStatusCode.UnsupportedData => "Unsupported data has been received.",
|
||||
CloseStatusCode.Abnormal => "An exception has occurred.",
|
||||
CloseStatusCode.InvalidData => "Invalid data has been received.",
|
||||
CloseStatusCode.PolicyViolation => "A policy violation has occurred.",
|
||||
CloseStatusCode.TooBig => "A too big message has been received.",
|
||||
CloseStatusCode.MandatoryExtension => "WebSocket client didn't receive expected extension(s).",
|
||||
CloseStatusCode.ServerError => "WebSocket server got an internal error.",
|
||||
CloseStatusCode.TlsHandshakeFailure => "An error has occurred during a TLS handshake.",
|
||||
_ => string.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
640
Vendor/EmbedIO-3.5.2/WebSockets/WebSocketModule.cs
vendored
Normal file
640
Vendor/EmbedIO-3.5.2/WebSockets/WebSocketModule.cs
vendored
Normal file
@@ -0,0 +1,640 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using EmbedIO.Utilities;
|
||||
using EmbedIO.WebSockets.Internal;
|
||||
using Swan;
|
||||
using Swan.Logging;
|
||||
using Swan.Threading;
|
||||
|
||||
namespace EmbedIO.WebSockets
|
||||
{
|
||||
/// <summary>
|
||||
/// A base class for modules that handle WebSocket connections.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Each WebSocket server has a list of WebSocket subprotocols it can accept.</para>
|
||||
/// <para>When a client initiates a WebSocket opening handshake:</para>
|
||||
/// <list type="bullet">
|
||||
/// <item><description>if the list of accepted subprotocols is empty,
|
||||
/// the connection is accepted only if no <c>SecWebSocketProtocol</c>
|
||||
/// header is present in the request;</description></item>
|
||||
/// <item><description>if the list of accepted subprotocols is not empty,
|
||||
/// the connection is accepted only if one or more <c>SecWebSocketProtocol</c>
|
||||
/// headers are present in the request and one of them specifies one
|
||||
/// of the subprotocols in the list. The first subprotocol specified by the client
|
||||
/// that is also present in the module's list is then specified in the
|
||||
/// handshake response.</description></item>
|
||||
/// </list>
|
||||
/// If a connection is not accepted because of a subprotocol mismatch,
|
||||
/// a <c>400 Bad Request</c> response is sent back to the client. The response
|
||||
/// contains one or more <c>SecWebSocketProtocol</c> headers that specify
|
||||
/// the list of accepted subprotocols (if any).
|
||||
/// </remarks>
|
||||
public abstract class WebSocketModule : WebModuleBase, IDisposable
|
||||
{
|
||||
private const int ReceiveBufferSize = 2048;
|
||||
|
||||
private readonly bool _enableConnectionWatchdog;
|
||||
private readonly List<string> _protocols = new List<string>();
|
||||
private readonly ConcurrentDictionary<string, IWebSocketContext> _contexts = new ConcurrentDictionary<string, IWebSocketContext>();
|
||||
private bool _isDisposing;
|
||||
private int _maxMessageSize;
|
||||
private TimeSpan _keepAliveInterval;
|
||||
private Encoding _encoding;
|
||||
private PeriodicTask? _connectionWatchdog;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WebSocketModule" /> class.
|
||||
/// </summary>
|
||||
/// <param name="urlPath">The URL path of the WebSocket endpoint to serve.</param>
|
||||
/// <param name="enableConnectionWatchdog">If set to <see langword="true"/>,
|
||||
/// contexts representing closed connections will automatically be purged
|
||||
/// from <see cref="ActiveContexts"/> every 30 seconds..</param>
|
||||
protected WebSocketModule(string urlPath, bool enableConnectionWatchdog)
|
||||
: base(urlPath)
|
||||
{
|
||||
_enableConnectionWatchdog = enableConnectionWatchdog;
|
||||
_maxMessageSize = 0;
|
||||
_keepAliveInterval = TimeSpan.FromSeconds(30);
|
||||
_encoding = Encoding.UTF8;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed override bool IsFinalHandler => true;
|
||||
|
||||
/// <summary>
|
||||
/// <para>Gets or sets the maximum size of a received message.
|
||||
/// If a message exceeding the maximum size is received from a client,
|
||||
/// the connection is closed automatically.</para>
|
||||
/// <para>The default value is 0, which disables message size checking.</para>
|
||||
/// </summary>
|
||||
protected int MaxMessageSize
|
||||
{
|
||||
get => _maxMessageSize;
|
||||
set
|
||||
{
|
||||
EnsureConfigurationNotLocked();
|
||||
_maxMessageSize = Math.Max(value, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the keep-alive interval for the WebSocket connection.
|
||||
/// The default is 30 seconds.
|
||||
/// </summary>
|
||||
/// <exception cref="ArgumentOutOfRangeException">This property is being set to a value
|
||||
/// that is too small to be acceptable.</exception>
|
||||
protected TimeSpan KeepAliveInterval
|
||||
{
|
||||
get => _keepAliveInterval;
|
||||
set
|
||||
{
|
||||
EnsureConfigurationNotLocked();
|
||||
if (value != Timeout.InfiniteTimeSpan && value < TimeSpan.Zero)
|
||||
throw new ArgumentOutOfRangeException(nameof(value), "The specified keep-alive interval is too small.");
|
||||
|
||||
_keepAliveInterval = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="Encoding"/> used by the <see cref="SendAsync(IWebSocketContext,string)"/> method
|
||||
/// to send a string. The default is <see cref="System.Text.Encoding.UTF8"/> per the WebSocket specification.
|
||||
/// </summary>
|
||||
/// <exception cref="ArgumentNullException">This property is being set to <see langword="null"/>.</exception>
|
||||
protected Encoding Encoding
|
||||
{
|
||||
get => _encoding;
|
||||
set
|
||||
{
|
||||
EnsureConfigurationNotLocked();
|
||||
_encoding = Validate.NotNull(nameof(value), value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of <see cref="IWebSocketContext"/> interfaces
|
||||
/// representing the currently connected clients.
|
||||
/// </summary>
|
||||
protected IReadOnlyList<IWebSocketContext> ActiveContexts
|
||||
{
|
||||
get
|
||||
{
|
||||
// ConcurrentDictionary<TKey,TValue>.Values, although declared as ICollection<TValue>,
|
||||
// will probably return a ReadOnlyCollection<TValue>, which implements IReadOnlyList<TValue>:
|
||||
// https://referencesource.microsoft.com/#mscorlib/system/Collections/Concurrent/ConcurrentDictionary.cs,fe55c11912af21d2
|
||||
// https://github.com/dotnet/corefx/blob/master/src/System.Collections.Concurrent/src/System/Collections/Concurrent/ConcurrentDictionary.cs#L1990
|
||||
// https://github.com/mono/mono/blob/master/mcs/class/referencesource/mscorlib/system/collections/Concurrent/ConcurrentDictionary.cs#L1961
|
||||
// However there is no formal guarantee, so be ready to convert to a list, just in case.
|
||||
var values = _contexts.Values;
|
||||
return values is IReadOnlyList<IWebSocketContext> list
|
||||
? list
|
||||
: values.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected sealed override async Task OnRequestAsync(IHttpContext context)
|
||||
{
|
||||
// The WebSocket endpoint must match exactly, giving a RequestedPath of "/".
|
||||
// In all other cases the path is longer, so there's no need to compare strings here.
|
||||
if (context.RequestedPath.Length > 1)
|
||||
return;
|
||||
|
||||
var requestedProtocols = context.Request.Headers.GetValues(HttpHeaderNames.SecWebSocketProtocol)
|
||||
?.Select(s => s.Trim())
|
||||
.Where(s => s.Length > 0)
|
||||
.ToArray()
|
||||
?? Array.Empty<string>();
|
||||
string acceptedProtocol;
|
||||
bool acceptConnection;
|
||||
if (_protocols.Count > 0)
|
||||
{
|
||||
acceptedProtocol = requestedProtocols.FirstOrDefault(p => _protocols.Contains(p)) ?? string.Empty;
|
||||
acceptConnection = acceptedProtocol.Length > 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
acceptedProtocol = string.Empty;
|
||||
acceptConnection = requestedProtocols.Length == 0;
|
||||
}
|
||||
|
||||
if (!acceptConnection)
|
||||
{
|
||||
$"{BaseRoute} - Rejecting WebSocket connection: no subprotocol was accepted.".Debug(nameof(WebSocketModule));
|
||||
foreach (var protocol in _protocols)
|
||||
context.Response.Headers.Add(HttpHeaderNames.SecWebSocketProtocol, protocol);
|
||||
|
||||
// Not throwing a HTTP exception here because a WebSocket client
|
||||
// does not care about nice, formatted messages.
|
||||
context.Response.SetEmptyResponse((int)HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
var contextImpl = context.GetImplementation();
|
||||
$"{BaseRoute} - Accepting WebSocket connection with subprotocol \"{acceptedProtocol}\"".Debug(nameof(WebSocketModule));
|
||||
var webSocketContext = await contextImpl.AcceptWebSocketAsync(
|
||||
requestedProtocols,
|
||||
acceptedProtocol,
|
||||
ReceiveBufferSize,
|
||||
KeepAliveInterval,
|
||||
context.CancellationToken).ConfigureAwait(false);
|
||||
|
||||
PurgeDisconnectedContexts();
|
||||
_ = _contexts.TryAdd(webSocketContext.Id, webSocketContext);
|
||||
|
||||
$"{BaseRoute} - WebSocket connection accepted - There are now {_contexts.Count} sockets connected."
|
||||
.Debug(nameof(WebSocketModule));
|
||||
|
||||
await OnClientConnectedAsync(webSocketContext).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
if (webSocketContext.WebSocket is SystemWebSocket systemWebSocket)
|
||||
{
|
||||
await ProcessSystemContext(
|
||||
webSocketContext,
|
||||
systemWebSocket.UnderlyingWebSocket,
|
||||
context.CancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ProcessEmbedIOContext(webSocketContext, context.CancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ex.Log(nameof(WebSocketModule));
|
||||
}
|
||||
finally
|
||||
{
|
||||
// once the loop is completed or connection aborted, remove the WebSocket
|
||||
RemoveWebSocket(webSocketContext);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnStart(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_enableConnectionWatchdog)
|
||||
{
|
||||
_connectionWatchdog = new PeriodicTask(
|
||||
TimeSpan.FromSeconds(30),
|
||||
ct => {
|
||||
PurgeDisconnectedContexts();
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a WebSocket subprotocol to the list of protocols supported by a <see cref="WebSocketModule"/>.
|
||||
/// </summary>
|
||||
/// <param name="protocol">The protocol name to add to the list.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="protocol"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <para><paramref name="protocol"/> contains one or more invalid characters, as defined
|
||||
/// in <see href="https://tools.ietf.org/html/rfc6455#section-4.3">RFC6455, Section 4.3</see>.</para>
|
||||
/// <para>- or -</para>
|
||||
/// <para><paramref name="protocol"/> is already in the list of supported protocols.</para>
|
||||
/// </exception>
|
||||
/// <exception cref="InvalidOperationException">The <see cref="WebSocketModule"/> has already been started.</exception>
|
||||
/// <seealso cref="Validate.Rfc2616Token"/>
|
||||
/// <seealso cref="AddProtocols(IEnumerable{string})"/>
|
||||
/// <seealso cref="AddProtocols(string[])"/>
|
||||
protected void AddProtocol(string protocol)
|
||||
{
|
||||
protocol = Validate.Rfc2616Token(nameof(protocol), protocol);
|
||||
|
||||
EnsureConfigurationNotLocked();
|
||||
|
||||
if (_protocols.Contains(protocol))
|
||||
throw new ArgumentException("Duplicate WebSocket protocol name.", nameof(protocol));
|
||||
|
||||
_protocols.Add(protocol);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds one or more WebSocket subprotocols to the list of protocols supported by a <see cref="WebSocketModule"/>.
|
||||
/// </summary>
|
||||
/// <param name="protocols">The protocol names to add to the list.</param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// <para><paramref name="protocols"/> is <see langword="null"/>.</para>
|
||||
/// <para>- or -</para>
|
||||
/// <para>One or more of the strings in <paramref name="protocols"/> is <see langword="null"/>.</para>
|
||||
/// </exception>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <para>One or more of the strings in <paramref name="protocols"/>
|
||||
/// contains one or more invalid characters, as defined
|
||||
/// in <see href="https://tools.ietf.org/html/rfc6455#section-4.3">RFC6455, Section 4.3</see>.</para>
|
||||
/// <para>- or -</para>
|
||||
/// <para>One or more of the strings in <paramref name="protocols"/>
|
||||
/// is already in the list of supported protocols.</para>
|
||||
/// </exception>
|
||||
/// <exception cref="InvalidOperationException">The <see cref="WebSocketModule"/> has already been started.</exception>
|
||||
/// <remarks>
|
||||
/// <para>This method enumerates <paramref name="protocols"/> just once; hence, if an exception is thrown
|
||||
/// because one of the specified protocols is <see langword="null"/> or contains invalid characters,
|
||||
/// any preceding protocol is added to the list of supported protocols.</para>
|
||||
/// </remarks>
|
||||
/// <seealso cref="Validate.Rfc2616Token"/>
|
||||
/// <seealso cref="AddProtocol"/>
|
||||
/// <seealso cref="AddProtocols(string[])"/>
|
||||
protected void AddProtocols(IEnumerable<string> protocols)
|
||||
{
|
||||
protocols = Validate.NotNull(nameof(protocols), protocols);
|
||||
|
||||
EnsureConfigurationNotLocked();
|
||||
|
||||
foreach (var protocol in protocols.Select(p => Validate.Rfc2616Token(nameof(protocols), p)))
|
||||
{
|
||||
if (_protocols.Contains(protocol))
|
||||
throw new ArgumentException("Duplicate WebSocket protocol name.", nameof(protocols));
|
||||
|
||||
_protocols.Add(protocol);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds one or more WebSocket subprotocols to the list of protocols supported by a <see cref="WebSocketModule"/>.
|
||||
/// </summary>
|
||||
/// <param name="protocols">The protocol names to add to the list.</param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// <para><paramref name="protocols"/> is <see langword="null"/>.</para>
|
||||
/// <para>- or -</para>
|
||||
/// <para>One or more of the strings in <paramref name="protocols"/> is <see langword="null"/>.</para>
|
||||
/// </exception>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <para>One or more of the strings in <paramref name="protocols"/>
|
||||
/// contains one or more invalid characters, as defined
|
||||
/// in <see href="https://tools.ietf.org/html/rfc6455#section-4.3">RFC6455, Section 4.3</see>.</para>
|
||||
/// <para>- or -</para>
|
||||
/// <para>One or more of the strings in <paramref name="protocols"/>
|
||||
/// is already in the list of supported protocols.</para>
|
||||
/// </exception>
|
||||
/// <exception cref="InvalidOperationException">The <see cref="WebSocketModule"/> has already been started.</exception>
|
||||
/// <remarks>
|
||||
/// <para>This method performs validation checks on all specified <paramref name="protocols"/> before adding them
|
||||
/// to the list of supported protocols; hence, if an exception is thrown
|
||||
/// because one of the specified protocols is <see langword="null"/> or contains invalid characters,
|
||||
/// none of the specified protocol names are added to the list.</para>
|
||||
/// </remarks>
|
||||
/// <seealso cref="Validate.Rfc2616Token"/>
|
||||
/// <seealso cref="AddProtocol"/>
|
||||
/// <seealso cref="AddProtocols(IEnumerable{string})"/>
|
||||
protected void AddProtocols(params string[] protocols)
|
||||
{
|
||||
protocols = Validate.NotNull(nameof(protocols), protocols);
|
||||
|
||||
if (protocols.Select(p => Validate.Rfc2616Token(nameof(protocols), p)).Any(protocol => _protocols.Contains(protocol)))
|
||||
throw new ArgumentException("Duplicate WebSocket protocol name.", nameof(protocols));
|
||||
|
||||
EnsureConfigurationNotLocked();
|
||||
|
||||
_protocols.AddRange(protocols);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a text payload.
|
||||
/// </summary>
|
||||
/// <param name="context">The web socket.</param>
|
||||
/// <param name="payload">The payload.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
|
||||
protected async Task SendAsync(IWebSocketContext context, string payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
var buffer = _encoding.GetBytes(payload ?? string.Empty);
|
||||
|
||||
await context.WebSocket.SendAsync(buffer, true, context.CancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ex.Log(nameof(WebSocketModule));
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable CA1822 // Member can be declared as static - It is an instance method for API consistency.
|
||||
/// <summary>
|
||||
/// Sends a binary payload.
|
||||
/// </summary>
|
||||
/// <param name="context">The web socket.</param>
|
||||
/// <param name="payload">The payload.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
|
||||
protected async Task SendAsync(IWebSocketContext context, byte[] payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
await context.WebSocket.SendAsync(payload ?? Array.Empty<byte>(), false, context.CancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ex.Log(nameof(WebSocketModule));
|
||||
}
|
||||
}
|
||||
#pragma warning restore CA1822
|
||||
|
||||
/// <summary>
|
||||
/// Broadcasts the specified payload to all connected WebSocket clients.
|
||||
/// </summary>
|
||||
/// <param name="payload">The payload.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
|
||||
protected Task BroadcastAsync(byte[] payload)
|
||||
=> Task.WhenAll(_contexts.Values.Select(c => SendAsync(c, payload)));
|
||||
|
||||
/// <summary>
|
||||
/// Broadcasts the specified payload to selected WebSocket clients.
|
||||
/// </summary>
|
||||
/// <param name="payload">The payload.</param>
|
||||
/// <param name="selector">A callback function that must return <see langword="true"/>
|
||||
/// for each context to be included in the broadcast.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
|
||||
protected Task BroadcastAsync(byte[] payload, Func<IWebSocketContext, bool> selector)
|
||||
=> Task.WhenAll(_contexts.Values.Where(Validate.NotNull(nameof(selector), selector)).Select(c => SendAsync(c, payload)));
|
||||
|
||||
/// <summary>
|
||||
/// Broadcasts the specified payload to all connected WebSocket clients.
|
||||
/// </summary>
|
||||
/// <param name="payload">The payload.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
|
||||
protected Task BroadcastAsync(string payload)
|
||||
=> Task.WhenAll(_contexts.Values.Select(c => SendAsync(c, payload)));
|
||||
|
||||
/// <summary>
|
||||
/// Broadcasts the specified payload to selected WebSocket clients.
|
||||
/// </summary>
|
||||
/// <param name="payload">The payload.</param>
|
||||
/// <param name="selector">A callback function that must return <see langword="true"/>
|
||||
/// for each context to be included in the broadcast.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
|
||||
protected Task BroadcastAsync(string payload, Func<IWebSocketContext, bool> selector)
|
||||
=> Task.WhenAll(_contexts.Values.Where(Validate.NotNull(nameof(selector), selector)).Select(c => SendAsync(c, payload)));
|
||||
|
||||
/// <summary>
|
||||
/// Closes the specified web socket, removes it and disposes it.
|
||||
/// </summary>
|
||||
/// <param name="context">The web socket.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
|
||||
protected async Task CloseAsync(IWebSocketContext context)
|
||||
{
|
||||
if (context == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await context.WebSocket.CloseAsync(context.CancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ex.Log(nameof(WebSocketModule));
|
||||
}
|
||||
finally
|
||||
{
|
||||
RemoveWebSocket(context);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when this WebSocket server receives a full message (EndOfMessage) from a client.
|
||||
/// </summary>
|
||||
/// <param name="context">The context.</param>
|
||||
/// <param name="buffer">The buffer.</param>
|
||||
/// <param name="result">The result.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
|
||||
protected abstract Task OnMessageReceivedAsync(IWebSocketContext context, byte[] buffer, IWebSocketReceiveResult result);
|
||||
|
||||
/// <summary>
|
||||
/// Called when this WebSocket server receives a message frame regardless if the frame represents the EndOfMessage.
|
||||
/// </summary>
|
||||
/// <param name="context">The context.</param>
|
||||
/// <param name="buffer">The buffer.</param>
|
||||
/// <param name="result">The result.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
|
||||
protected virtual Task OnFrameReceivedAsync(
|
||||
IWebSocketContext context,
|
||||
byte[] buffer,
|
||||
IWebSocketReceiveResult result)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Called when this WebSocket server accepts a new client.
|
||||
/// </summary>
|
||||
/// <param name="context">The context.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
|
||||
protected virtual Task OnClientConnectedAsync(IWebSocketContext context) => Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Called when the server has removed a connected client for any reason.
|
||||
/// </summary>
|
||||
/// <param name="context">The context.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
|
||||
protected virtual Task OnClientDisconnectedAsync(IWebSocketContext context) => Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_isDisposing)
|
||||
return;
|
||||
|
||||
_isDisposing = true;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_connectionWatchdog?.Dispose();
|
||||
Task.WhenAll(_contexts.Values.Select(CloseAsync)).Await(false);
|
||||
PurgeDisconnectedContexts();
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveWebSocket(IWebSocketContext context)
|
||||
{
|
||||
if (!_contexts.TryRemove(context.Id, out _))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
context.WebSocket?.Dispose();
|
||||
|
||||
// OnClientDisconnectedAsync is better called in its own task,
|
||||
// so it may call methods that require a lock on _contextsAccess.
|
||||
// Otherwise, calling e.g. Broadcast would result in a deadlock.
|
||||
#pragma warning disable CS4014 // Call is not awaited - it is intentionally forked.
|
||||
_ = Task.Run(async () => {
|
||||
try
|
||||
{
|
||||
await OnClientDisconnectedAsync(context).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
$"[{context.Id}] OnClientDisconnectedAsync was canceled.".Debug(nameof(WebSocketModule));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
e.Log(nameof(WebSocketModule), $"[{context.Id}] Exception in OnClientDisconnectedAsync.");
|
||||
}
|
||||
});
|
||||
#pragma warning restore CS4014
|
||||
}
|
||||
|
||||
private void PurgeDisconnectedContexts()
|
||||
{
|
||||
var contexts = _contexts.Values;
|
||||
var totalCount = _contexts.Count;
|
||||
var purgedCount = 0;
|
||||
foreach (var context in contexts)
|
||||
{
|
||||
if (context.WebSocket == null || context.WebSocket.State == WebSocketState.Open)
|
||||
continue;
|
||||
|
||||
RemoveWebSocket(context);
|
||||
purgedCount++;
|
||||
}
|
||||
|
||||
$"{BaseRoute} - Purged {purgedCount} of {totalCount} sockets."
|
||||
.Debug(nameof(WebSocketModule));
|
||||
}
|
||||
|
||||
private async Task ProcessEmbedIOContext(IWebSocketContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
((Internal.WebSocket)context.WebSocket).OnMessage += async (s, e) =>
|
||||
{
|
||||
if (e.Opcode == Opcode.Close)
|
||||
{
|
||||
await context.WebSocket.CloseAsync(context.CancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await OnMessageReceivedAsync(
|
||||
context,
|
||||
e.RawData,
|
||||
new Internal.WebSocketReceiveResult(e.RawData.Length, e.Opcode))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
};
|
||||
|
||||
while (context.WebSocket.State == WebSocketState.Open
|
||||
|| context.WebSocket.State == WebSocketState.CloseReceived
|
||||
|| context.WebSocket.State == WebSocketState.CloseSent)
|
||||
{
|
||||
await Task.Delay(500, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessSystemContext(IWebSocketContext context, System.Net.WebSockets.WebSocket webSocket, CancellationToken cancellationToken)
|
||||
{
|
||||
// define a receive buffer
|
||||
var receiveBuffer = new byte[ReceiveBufferSize];
|
||||
|
||||
// define a dynamic buffer that holds multi-part receptions
|
||||
var receivedMessage = new List<byte>(receiveBuffer.Length * 2);
|
||||
|
||||
// poll the WebSocket connections for reception
|
||||
while (webSocket.State == WebSocketState.Open)
|
||||
{
|
||||
// retrieve the result (blocking)
|
||||
var receiveResult = new SystemWebSocketReceiveResult(
|
||||
await webSocket.ReceiveAsync(new ArraySegment<byte>(receiveBuffer), cancellationToken)
|
||||
.ConfigureAwait(false));
|
||||
|
||||
if (receiveResult.MessageType == (int)WebSocketMessageType.Close)
|
||||
{
|
||||
// close the connection if requested by the client
|
||||
await webSocket
|
||||
.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var frameBytes = new byte[receiveResult.Count];
|
||||
Array.Copy(receiveBuffer, frameBytes, frameBytes.Length);
|
||||
await OnFrameReceivedAsync(context, frameBytes, receiveResult).ConfigureAwait(false);
|
||||
|
||||
// add the response to the multi-part response
|
||||
receivedMessage.AddRange(frameBytes);
|
||||
|
||||
if (_maxMessageSize > 0 && receivedMessage.Count > _maxMessageSize)
|
||||
{
|
||||
// close the connection if message exceeds max length
|
||||
await webSocket.CloseAsync(
|
||||
WebSocketCloseStatus.MessageTooBig,
|
||||
$"Message too big. Maximum is {_maxMessageSize} bytes.",
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// exit the loop; we're done
|
||||
return;
|
||||
}
|
||||
|
||||
// if we're at the end of the message, process the message
|
||||
if (!receiveResult.EndOfMessage) continue;
|
||||
|
||||
await OnMessageReceivedAsync(context, receivedMessage.ToArray(), receiveResult)
|
||||
.ConfigureAwait(false);
|
||||
receivedMessage.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user