mirror of
https://github.com/MirrorNetworking/Mirror.git
synced 2024-11-18 11:00:32 +00:00
Removing websockets from mirror repo (#2268)
* Adding new websocket transport soon that doesn't depend on library * Code for old transport can now be found here https://github.com/MirrorNetworking/NinjaWebSocketsTransport * We shouldn't need to replace with empty files from store as having this in project should not break things
This commit is contained in:
parent
a2b64a4d1d
commit
f3f0403005
@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 75b15785adf2d4b7c8d779f5ba6a6326
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,207 +0,0 @@
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using System.Net.Sockets;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Ninja.WebSockets;
|
||||
|
||||
namespace Mirror.Websocket
|
||||
{
|
||||
|
||||
public class Client
|
||||
{
|
||||
public event Action Connected;
|
||||
public event Action<ArraySegment<byte>> ReceivedData;
|
||||
public event Action Disconnected;
|
||||
public event Action<Exception> ReceivedError;
|
||||
|
||||
const int MaxMessageSize = 1024 * 256;
|
||||
WebSocket webSocket;
|
||||
CancellationTokenSource cancellation;
|
||||
|
||||
public bool NoDelay = true;
|
||||
|
||||
public bool Connecting { get; set; }
|
||||
public bool IsConnected { get; set; }
|
||||
|
||||
Uri uri;
|
||||
|
||||
public async void Connect(Uri uri)
|
||||
{
|
||||
// not if already started
|
||||
if (webSocket != null)
|
||||
{
|
||||
// paul: exceptions are better than silence
|
||||
ReceivedError?.Invoke(new Exception("Client already connected"));
|
||||
return;
|
||||
}
|
||||
this.uri = uri;
|
||||
// We are connecting from now until Connect succeeds or fails
|
||||
Connecting = true;
|
||||
|
||||
WebSocketClientOptions options = new WebSocketClientOptions()
|
||||
{
|
||||
NoDelay = true,
|
||||
KeepAliveInterval = TimeSpan.Zero,
|
||||
SecWebSocketProtocol = "binary"
|
||||
};
|
||||
|
||||
cancellation = new CancellationTokenSource();
|
||||
|
||||
WebSocketClientFactory clientFactory = new WebSocketClientFactory();
|
||||
|
||||
try
|
||||
{
|
||||
using (webSocket = await clientFactory.ConnectAsync(uri, options, cancellation.Token))
|
||||
{
|
||||
CancellationToken token = cancellation.Token;
|
||||
IsConnected = true;
|
||||
Connecting = false;
|
||||
Connected?.Invoke();
|
||||
|
||||
await ReceiveLoop(webSocket, token);
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// No error, the client got closed
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ReceivedError?.Invoke(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Disconnect();
|
||||
Disconnected?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public bool enabled;
|
||||
|
||||
async Task ReceiveLoop(WebSocket webSocket, CancellationToken token)
|
||||
{
|
||||
byte[] buffer = new byte[MaxMessageSize];
|
||||
|
||||
while (true)
|
||||
{
|
||||
WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), token);
|
||||
|
||||
if (!enabled)
|
||||
{
|
||||
await WaitForEnabledAsync();
|
||||
}
|
||||
|
||||
if (result == null)
|
||||
break;
|
||||
if (result.MessageType == WebSocketMessageType.Close)
|
||||
break;
|
||||
|
||||
// we got a text or binary message, need the full message
|
||||
ArraySegment<byte> data = await ReadFrames(result, webSocket, buffer);
|
||||
|
||||
if (data.Count == 0)
|
||||
break;
|
||||
|
||||
try
|
||||
{
|
||||
ReceivedData?.Invoke(data);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ReceivedError?.Invoke(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async Task WaitForEnabledAsync()
|
||||
{
|
||||
while (!enabled)
|
||||
{
|
||||
await Task.Delay(10);
|
||||
}
|
||||
}
|
||||
|
||||
public bool ProcessClientMessage()
|
||||
{
|
||||
// message in standalone client don't use queue to process
|
||||
return false;
|
||||
}
|
||||
|
||||
// a message might come splitted in multiple frames
|
||||
// collect all frames
|
||||
async Task<ArraySegment<byte>> ReadFrames(WebSocketReceiveResult result, WebSocket webSocket, byte[] buffer)
|
||||
{
|
||||
int count = result.Count;
|
||||
|
||||
while (!result.EndOfMessage)
|
||||
{
|
||||
if (count >= MaxMessageSize)
|
||||
{
|
||||
string closeMessage = string.Format("Maximum message size: {0} bytes.", MaxMessageSize);
|
||||
await webSocket.CloseAsync(WebSocketCloseStatus.MessageTooBig, closeMessage, CancellationToken.None);
|
||||
ReceivedError?.Invoke(new WebSocketException(WebSocketError.HeaderError));
|
||||
return new ArraySegment<byte>();
|
||||
}
|
||||
|
||||
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer, count, MaxMessageSize - count), CancellationToken.None);
|
||||
count += result.Count;
|
||||
|
||||
}
|
||||
return new ArraySegment<byte>(buffer, 0, count);
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
cancellation?.Cancel();
|
||||
|
||||
// only if started
|
||||
if (webSocket != null)
|
||||
{
|
||||
// close client
|
||||
webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
|
||||
webSocket = null;
|
||||
Connecting = false;
|
||||
IsConnected = false;
|
||||
}
|
||||
}
|
||||
|
||||
// send the data or throw exception
|
||||
public async void Send(ArraySegment<byte> segment)
|
||||
{
|
||||
if (webSocket == null)
|
||||
{
|
||||
ReceivedError?.Invoke(new SocketException((int)SocketError.NotConnected));
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await webSocket.SendAsync(segment, WebSocketMessageType.Binary, true, cancellation.Token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Disconnect();
|
||||
ReceivedError?.Invoke(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (IsConnected)
|
||||
{
|
||||
return $"Websocket connected to {uri}";
|
||||
}
|
||||
if (Connecting)
|
||||
{
|
||||
return $"Websocket connecting to {uri}";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8f69ff0981a33445a89b5e37b245806f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,127 +0,0 @@
|
||||
#if UNITY_WEBGL && !UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using AOT;
|
||||
|
||||
namespace Mirror.Websocket
|
||||
{
|
||||
// this is the client implementation used by browsers
|
||||
public class Client
|
||||
{
|
||||
static int idGenerator = 0;
|
||||
static readonly Dictionary<int, Client> clients = new Dictionary<int, Client>();
|
||||
|
||||
public bool NoDelay = true;
|
||||
|
||||
public event Action Connected;
|
||||
public event Action<ArraySegment<byte>> ReceivedData;
|
||||
public event Action Disconnected;
|
||||
#pragma warning disable CS0067 // The event is never used.
|
||||
public event Action<Exception> ReceivedError;
|
||||
#pragma warning restore CS0067 // The event is never used.
|
||||
|
||||
readonly ConcurrentQueue<byte[]> receivedQueue = new ConcurrentQueue<byte[]>();
|
||||
|
||||
public bool enabled;
|
||||
public bool Connecting { get; set; }
|
||||
public bool IsConnected
|
||||
{
|
||||
get
|
||||
{
|
||||
return SocketState(nativeRef) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
int nativeRef = 0;
|
||||
readonly int id;
|
||||
|
||||
public Client()
|
||||
{
|
||||
id = Interlocked.Increment(ref idGenerator);
|
||||
}
|
||||
|
||||
public void Connect(Uri uri)
|
||||
{
|
||||
clients[id] = this;
|
||||
|
||||
Connecting = true;
|
||||
|
||||
nativeRef = SocketCreate(uri.ToString(), id, OnOpen, OnData, OnClose);
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
SocketClose(nativeRef);
|
||||
}
|
||||
|
||||
// send the data or throw exception
|
||||
public void Send(ArraySegment<byte> segment)
|
||||
{
|
||||
SocketSend(nativeRef, segment.Array, segment.Count);
|
||||
}
|
||||
|
||||
public bool ProcessClientMessage()
|
||||
{
|
||||
if (receivedQueue.TryDequeue(out byte[] data))
|
||||
{
|
||||
clients[id].ReceivedData(new ArraySegment<byte>(data));
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#region Javascript native functions
|
||||
[DllImport("__Internal")]
|
||||
static extern int SocketCreate(
|
||||
string url,
|
||||
int id,
|
||||
Action<int> onpen,
|
||||
Action<int, IntPtr, int> ondata,
|
||||
Action<int> onclose);
|
||||
|
||||
[DllImport("__Internal")]
|
||||
static extern int SocketState(int socketInstance);
|
||||
|
||||
[DllImport("__Internal")]
|
||||
static extern void SocketSend(int socketInstance, byte[] ptr, int length);
|
||||
|
||||
[DllImport("__Internal")]
|
||||
static extern void SocketClose(int socketInstance);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Javascript callbacks
|
||||
|
||||
[MonoPInvokeCallback(typeof(Action))]
|
||||
public static void OnOpen(int id)
|
||||
{
|
||||
clients[id].Connecting = false;
|
||||
clients[id].Connected?.Invoke();
|
||||
}
|
||||
|
||||
[MonoPInvokeCallback(typeof(Action))]
|
||||
public static void OnClose(int id)
|
||||
{
|
||||
clients[id].Connecting = false;
|
||||
clients[id].Disconnected?.Invoke();
|
||||
clients.Remove(id);
|
||||
}
|
||||
|
||||
[MonoPInvokeCallback(typeof(Action))]
|
||||
public static void OnData(int id, IntPtr ptr, int length)
|
||||
{
|
||||
byte[] data = new byte[length];
|
||||
Marshal.Copy(ptr, data, 0, length);
|
||||
|
||||
clients[id].receivedQueue.Enqueue(data);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 278e08c90f2324e2e80a0fe8984b0590
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,15 +0,0 @@
|
||||
{
|
||||
"name": "Mirror.Websocket",
|
||||
"references": [
|
||||
"Mirror",
|
||||
"Ninja.WebSockets"
|
||||
],
|
||||
"optionalUnityReferences": [],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": []
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2d3598656ef1d914f91550a81c0e91cb
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 100fd42034e0d46db8980db4cc0cd178
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,249 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ninja.WebSockets
|
||||
{
|
||||
/// <summary>
|
||||
/// This buffer pool is instance thread safe
|
||||
/// Use GetBuffer to get a MemoryStream (with a publically accessible buffer)
|
||||
/// Calling Close on this MemoryStream will clear its internal buffer and return the buffer to the pool for reuse
|
||||
/// MemoryStreams can grow larger than the DEFAULT_BUFFER_SIZE (or whatever you passed in)
|
||||
/// and the underlying buffers will be returned to the pool at their larger sizes
|
||||
/// </summary>
|
||||
public class BufferPool : IBufferPool
|
||||
{
|
||||
const int DEFAULT_BUFFER_SIZE = 16384;
|
||||
readonly ConcurrentStack<byte[]> _bufferPoolStack;
|
||||
readonly int _bufferSize;
|
||||
|
||||
public BufferPool() : this(DEFAULT_BUFFER_SIZE)
|
||||
{
|
||||
}
|
||||
|
||||
public BufferPool(int bufferSize)
|
||||
{
|
||||
_bufferSize = bufferSize;
|
||||
_bufferPoolStack = new ConcurrentStack<byte[]>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This memory stream is not instance thread safe (not to be confused with the BufferPool which is instance thread safe)
|
||||
/// </summary>
|
||||
protected class PublicBufferMemoryStream : MemoryStream
|
||||
{
|
||||
readonly BufferPool _bufferPoolInternal;
|
||||
byte[] _buffer;
|
||||
MemoryStream _ms;
|
||||
|
||||
public PublicBufferMemoryStream(byte[] buffer, BufferPool bufferPool) : base(new byte[0])
|
||||
{
|
||||
_bufferPoolInternal = bufferPool;
|
||||
_buffer = buffer;
|
||||
_ms = new MemoryStream(buffer, 0, buffer.Length, true, true);
|
||||
}
|
||||
|
||||
public override long Length => base.Length;
|
||||
|
||||
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
|
||||
{
|
||||
return _ms.BeginRead(buffer, offset, count, callback, state);
|
||||
}
|
||||
|
||||
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
|
||||
{
|
||||
return _ms.BeginWrite(buffer, offset, count, callback, state);
|
||||
}
|
||||
|
||||
public override bool CanRead => _ms.CanRead;
|
||||
public override bool CanSeek => _ms.CanSeek;
|
||||
public override bool CanTimeout => _ms.CanTimeout;
|
||||
public override bool CanWrite => _ms.CanWrite;
|
||||
public override int Capacity
|
||||
{
|
||||
get { return _ms.Capacity; }
|
||||
set { _ms.Capacity = value; }
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
// clear the buffer - we only need to clear up to the number of bytes we have already written
|
||||
Array.Clear(_buffer, 0, (int)_ms.Position);
|
||||
|
||||
_ms.Close();
|
||||
|
||||
// return the buffer to the pool
|
||||
_bufferPoolInternal.ReturnBuffer(_buffer);
|
||||
}
|
||||
|
||||
public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
|
||||
{
|
||||
return _ms.CopyToAsync(destination, bufferSize, cancellationToken);
|
||||
}
|
||||
|
||||
public override int EndRead(IAsyncResult asyncResult)
|
||||
{
|
||||
return _ms.EndRead(asyncResult);
|
||||
}
|
||||
|
||||
public override void EndWrite(IAsyncResult asyncResult)
|
||||
{
|
||||
_ms.EndWrite(asyncResult);
|
||||
}
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
_ms.Flush();
|
||||
}
|
||||
|
||||
public override Task FlushAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return _ms.FlushAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override byte[] GetBuffer()
|
||||
{
|
||||
return _buffer;
|
||||
}
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get { return _ms.Position; }
|
||||
set { _ms.Position = value; }
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
return _ms.Read(buffer, offset, count);
|
||||
}
|
||||
|
||||
void EnlargeBufferIfRequired(int count)
|
||||
{
|
||||
// we cannot fit the data into the existing buffer, time for a new buffer
|
||||
if (count > (_buffer.Length - _ms.Position))
|
||||
{
|
||||
int position = (int)_ms.Position;
|
||||
|
||||
// double the buffer size
|
||||
int newSize = _buffer.Length * 2;
|
||||
|
||||
// make sure the new size is big enough
|
||||
int requiredSize = count + _buffer.Length - position;
|
||||
if (requiredSize > newSize)
|
||||
{
|
||||
// compute the power of two larger than requiredSize. so 40000 => 65536
|
||||
newSize = (int)Math.Pow(2, Math.Ceiling(Math.Log(requiredSize) / Math.Log(2)));
|
||||
}
|
||||
|
||||
byte[] newBuffer = new byte[newSize];
|
||||
Buffer.BlockCopy(_buffer, 0, newBuffer, 0, position);
|
||||
_ms = new MemoryStream(newBuffer, 0, newBuffer.Length, true, true)
|
||||
{
|
||||
Position = position
|
||||
};
|
||||
|
||||
_buffer = newBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteByte(byte value)
|
||||
{
|
||||
EnlargeBufferIfRequired(1);
|
||||
_ms.WriteByte(value);
|
||||
}
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
EnlargeBufferIfRequired(count);
|
||||
_ms.Write(buffer, offset, count);
|
||||
}
|
||||
|
||||
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
EnlargeBufferIfRequired(count);
|
||||
return _ms.WriteAsync(buffer, offset, count);
|
||||
}
|
||||
|
||||
public override object InitializeLifetimeService()
|
||||
{
|
||||
return _ms.InitializeLifetimeService();
|
||||
}
|
||||
|
||||
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
return _ms.ReadAsync(buffer, offset, count, cancellationToken);
|
||||
}
|
||||
|
||||
public override int ReadByte()
|
||||
{
|
||||
return _ms.ReadByte();
|
||||
}
|
||||
|
||||
public override int ReadTimeout
|
||||
{
|
||||
get { return _ms.ReadTimeout; }
|
||||
set { _ms.ReadTimeout = value; }
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin loc)
|
||||
{
|
||||
return _ms.Seek(offset, loc);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Note: This will not make the MemoryStream any smaller, only larger
|
||||
/// </summary>
|
||||
public override void SetLength(long value)
|
||||
{
|
||||
EnlargeBufferIfRequired((int)value);
|
||||
}
|
||||
|
||||
public override byte[] ToArray()
|
||||
{
|
||||
// you should never call this
|
||||
return _ms.ToArray();
|
||||
}
|
||||
|
||||
public override int WriteTimeout
|
||||
{
|
||||
get { return _ms.WriteTimeout; }
|
||||
set { _ms.WriteTimeout = value; }
|
||||
}
|
||||
|
||||
#if !NET45
|
||||
public override bool TryGetBuffer(out ArraySegment<byte> buffer)
|
||||
{
|
||||
buffer = new ArraySegment<byte>(_buffer, 0, (int)_ms.Position);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
public override void WriteTo(Stream stream)
|
||||
{
|
||||
_ms.WriteTo(stream);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a MemoryStream built from a buffer plucked from a thread safe pool
|
||||
/// The pool grows automatically.
|
||||
/// Closing the memory stream clears the buffer and returns it to the pool
|
||||
/// </summary>
|
||||
public MemoryStream GetBuffer()
|
||||
{
|
||||
if (!_bufferPoolStack.TryPop(out byte[] buffer))
|
||||
{
|
||||
buffer = new byte[_bufferSize];
|
||||
}
|
||||
|
||||
return new PublicBufferMemoryStream(buffer, this);
|
||||
}
|
||||
|
||||
protected void ReturnBuffer(byte[] buffer)
|
||||
{
|
||||
_bufferPoolStack.Push(buffer);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e362254f82b2a4627bfd83394d093715
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ca217a8fd870444d0ae623d3905d603f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,26 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Ninja.WebSockets.Exceptions
|
||||
{
|
||||
[Serializable]
|
||||
public class EntityTooLargeException : Exception
|
||||
{
|
||||
public EntityTooLargeException() : base()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Http header too large to fit in buffer
|
||||
/// </summary>
|
||||
public EntityTooLargeException(string message) : base(message)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public EntityTooLargeException(string message, Exception inner) : base(message, inner)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7ef87d4a0822c4d2da3e8daa392e61d3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,33 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Ninja.WebSockets.Exceptions
|
||||
{
|
||||
[Serializable]
|
||||
public class InvalidHttpResponseCodeException : Exception
|
||||
{
|
||||
public string ResponseCode { get; private set; }
|
||||
|
||||
public string ResponseHeader { get; private set; }
|
||||
|
||||
public string ResponseDetails { get; private set; }
|
||||
|
||||
public InvalidHttpResponseCodeException() : base()
|
||||
{
|
||||
}
|
||||
|
||||
public InvalidHttpResponseCodeException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public InvalidHttpResponseCodeException(string responseCode, string responseDetails, string responseHeader) : base(responseCode)
|
||||
{
|
||||
ResponseCode = responseCode;
|
||||
ResponseDetails = responseDetails;
|
||||
ResponseHeader = responseHeader;
|
||||
}
|
||||
|
||||
public InvalidHttpResponseCodeException(string message, Exception inner) : base(message, inner)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2c2d7303a1a324f0ebe15cb3faf9b0d5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1 +0,0 @@
|
||||
Make sure that exceptions follow the microsoft standards
|
@ -1,7 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3488f8d8c73d64a12bc24930c0210a41
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,23 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Ninja.WebSockets.Exceptions
|
||||
{
|
||||
[Serializable]
|
||||
public class SecWebSocketKeyMissingException : Exception
|
||||
{
|
||||
public SecWebSocketKeyMissingException() : base()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public SecWebSocketKeyMissingException(string message) : base(message)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public SecWebSocketKeyMissingException(string message, Exception inner) : base(message, inner)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 66d99fe90f4cb4471bf01c6f391ffc29
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,20 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Ninja.WebSockets.Exceptions
|
||||
{
|
||||
[Serializable]
|
||||
public class ServerListenerSocketException : Exception
|
||||
{
|
||||
public ServerListenerSocketException() : base()
|
||||
{
|
||||
}
|
||||
|
||||
public ServerListenerSocketException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public ServerListenerSocketException(string message, Exception inner) : base(message, inner)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9b188b3dddee84decadd5913a535746b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,20 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Ninja.WebSockets.Exceptions
|
||||
{
|
||||
[Serializable]
|
||||
public class WebSocketBufferOverflowException : Exception
|
||||
{
|
||||
public WebSocketBufferOverflowException() : base()
|
||||
{
|
||||
}
|
||||
|
||||
public WebSocketBufferOverflowException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public WebSocketBufferOverflowException(string message, Exception inner) : base(message, inner)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3c3188abcb6c441759011da01fa342f4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,20 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Ninja.WebSockets.Exceptions
|
||||
{
|
||||
[Serializable]
|
||||
public class WebSocketHandshakeFailedException : Exception
|
||||
{
|
||||
public WebSocketHandshakeFailedException() : base()
|
||||
{
|
||||
}
|
||||
|
||||
public WebSocketHandshakeFailedException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public WebSocketHandshakeFailedException(string message, Exception inner) : base(message, inner)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7dd72ca0a28054d258c1d1ad8254a16e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,20 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Ninja.WebSockets.Exceptions
|
||||
{
|
||||
[Serializable]
|
||||
public class WebSocketVersionNotSupportedException : Exception
|
||||
{
|
||||
public WebSocketVersionNotSupportedException() : base()
|
||||
{
|
||||
}
|
||||
|
||||
public WebSocketVersionNotSupportedException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public WebSocketVersionNotSupportedException(string message, Exception inner) : base(message, inner)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dc12d4b9cfe854aeeaab3c9f2f3d165e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,202 +0,0 @@
|
||||
// ---------------------------------------------------------------------
|
||||
// Copyright 2018 David Haig
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Ninja.WebSockets.Exceptions;
|
||||
|
||||
namespace Ninja.WebSockets
|
||||
{
|
||||
public class HttpHelper
|
||||
{
|
||||
const string HTTP_GET_HEADER_REGEX = @"^GET(.*)HTTP\/1\.1";
|
||||
|
||||
/// <summary>
|
||||
/// Calculates a random WebSocket key that can be used to initiate a WebSocket handshake
|
||||
/// </summary>
|
||||
/// <returns>A random websocket key</returns>
|
||||
public static string CalculateWebSocketKey()
|
||||
{
|
||||
// this is not used for cryptography so doing something simple like he code below is op
|
||||
Random rand = new Random((int)DateTime.Now.Ticks);
|
||||
byte[] keyAsBytes = new byte[16];
|
||||
rand.NextBytes(keyAsBytes);
|
||||
return Convert.ToBase64String(keyAsBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a WebSocket accept string from a given key
|
||||
/// </summary>
|
||||
/// <param name="secWebSocketKey">The web socket key to base the accept string on</param>
|
||||
/// <returns>A web socket accept string</returns>
|
||||
public static string ComputeSocketAcceptString(string secWebSocketKey)
|
||||
{
|
||||
// this is a guid as per the web socket spec
|
||||
const string webSocketGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
string concatenated = secWebSocketKey + webSocketGuid;
|
||||
byte[] concatenatedAsBytes = Encoding.UTF8.GetBytes(concatenated);
|
||||
|
||||
// note an instance of SHA1 is not threadsafe so we have to create a new one every time here
|
||||
byte[] sha1Hash = SHA1.Create().ComputeHash(concatenatedAsBytes);
|
||||
string secWebSocketAccept = Convert.ToBase64String(sha1Hash);
|
||||
return secWebSocketAccept;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads an http header as per the HTTP spec
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to read UTF8 text from</param>
|
||||
/// <param name="token">The cancellation token</param>
|
||||
/// <returns>The HTTP header</returns>
|
||||
public static async Task<string> ReadHttpHeaderAsync(Stream stream, CancellationToken token)
|
||||
{
|
||||
// 16KB buffer more than enough for http header
|
||||
int length = 1024 * 16;
|
||||
byte[] buffer = new byte[length];
|
||||
int offset = 0;
|
||||
int bytesRead = 0;
|
||||
|
||||
do
|
||||
{
|
||||
if (offset >= length)
|
||||
{
|
||||
throw new EntityTooLargeException("Http header message too large to fit in buffer (16KB)");
|
||||
}
|
||||
|
||||
bytesRead = await stream.ReadAsync(buffer, offset, length - offset, token);
|
||||
offset += bytesRead;
|
||||
string header = Encoding.UTF8.GetString(buffer, 0, offset);
|
||||
|
||||
// as per http specification, all headers should end this this
|
||||
if (header.Contains("\r\n\r\n"))
|
||||
{
|
||||
return header;
|
||||
}
|
||||
|
||||
} while (bytesRead > 0);
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decodes the header to detect is this is a web socket upgrade response
|
||||
/// </summary>
|
||||
/// <param name="header">The HTTP header</param>
|
||||
/// <returns>True if this is an http WebSocket upgrade response</returns>
|
||||
public static bool IsWebSocketUpgradeRequest(String header)
|
||||
{
|
||||
Regex getRegex = new Regex(HTTP_GET_HEADER_REGEX, RegexOptions.IgnoreCase);
|
||||
Match getRegexMatch = getRegex.Match(header);
|
||||
|
||||
if (getRegexMatch.Success)
|
||||
{
|
||||
// check if this is a web socket upgrade request
|
||||
Regex webSocketUpgradeRegex = new Regex("Upgrade: websocket", RegexOptions.IgnoreCase);
|
||||
Match webSocketUpgradeRegexMatch = webSocketUpgradeRegex.Match(header);
|
||||
return webSocketUpgradeRegexMatch.Success;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path from the HTTP header
|
||||
/// </summary>
|
||||
/// <param name="httpHeader">The HTTP header to read</param>
|
||||
/// <returns>The path</returns>
|
||||
public static string GetPathFromHeader(string httpHeader)
|
||||
{
|
||||
Regex getRegex = new Regex(HTTP_GET_HEADER_REGEX, RegexOptions.IgnoreCase);
|
||||
Match getRegexMatch = getRegex.Match(httpHeader);
|
||||
|
||||
if (getRegexMatch.Success)
|
||||
{
|
||||
// extract the path attribute from the first line of the header
|
||||
return getRegexMatch.Groups[1].Value.Trim();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static IList<string> GetSubProtocols(string httpHeader)
|
||||
{
|
||||
Regex regex = new Regex(@"Sec-WebSocket-Protocol:(?<protocols>.+)", RegexOptions.IgnoreCase);
|
||||
Match match = regex.Match(httpHeader);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
const int MAX_LEN = 2048;
|
||||
if (match.Length > MAX_LEN)
|
||||
{
|
||||
throw new EntityTooLargeException($"Sec-WebSocket-Protocol exceeded the maximum of length of {MAX_LEN}");
|
||||
}
|
||||
|
||||
// extract a csv list of sub protocols (in order of highest preference first)
|
||||
string csv = match.Groups["protocols"].Value.Trim();
|
||||
return csv.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(x => x.Trim())
|
||||
.ToList();
|
||||
}
|
||||
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the HTTP response code from the http response string
|
||||
/// </summary>
|
||||
/// <param name="response">The response string</param>
|
||||
/// <returns>the response code</returns>
|
||||
public static string ReadHttpResponseCode(string response)
|
||||
{
|
||||
Regex getRegex = new Regex(@"HTTP\/1\.1 (.*)", RegexOptions.IgnoreCase);
|
||||
Match getRegexMatch = getRegex.Match(response);
|
||||
|
||||
if (getRegexMatch.Success)
|
||||
{
|
||||
// extract the path attribute from the first line of the header
|
||||
return getRegexMatch.Groups[1].Value.Trim();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes an HTTP response string to the stream
|
||||
/// </summary>
|
||||
/// <param name="response">The response (without the new line characters)</param>
|
||||
/// <param name="stream">The stream to write to</param>
|
||||
/// <param name="token">The cancellation token</param>
|
||||
public static async Task WriteHttpHeaderAsync(string response, Stream stream, CancellationToken token)
|
||||
{
|
||||
response = response.Trim() + "\r\n\r\n";
|
||||
Byte[] bytes = Encoding.UTF8.GetBytes(response);
|
||||
await stream.WriteAsync(bytes, 0, bytes.Length, token);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3c90b5378af58402987e8a2938d763f2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,14 +0,0 @@
|
||||
using System.IO;
|
||||
|
||||
namespace Ninja.WebSockets
|
||||
{
|
||||
public interface IBufferPool
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a MemoryStream built from a buffer plucked from a thread safe pool
|
||||
/// The pool grows automatically.
|
||||
/// Closing the memory stream clears the buffer and returns it to the pool
|
||||
/// </summary>
|
||||
MemoryStream GetBuffer();
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 894bb86ff9cbe4179a6764904f9dd742
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,24 +0,0 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ninja.WebSockets
|
||||
{
|
||||
/// <summary>
|
||||
/// Ping Pong Manager used to facilitate ping pong WebSocket messages
|
||||
/// </summary>
|
||||
interface IPingPongManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Raised when a Pong frame is received
|
||||
/// </summary>
|
||||
event EventHandler<PongEventArgs> Pong;
|
||||
|
||||
/// <summary>
|
||||
/// Sends a ping frame
|
||||
/// </summary>
|
||||
/// <param name="payload">The payload (must be 125 bytes of less)</param>
|
||||
/// <param name="cancellation">The cancellation token</param>
|
||||
Task SendPing(ArraySegment<byte> payload, CancellationToken cancellation);
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1432676d0074e4a7ca123228797fe0ac
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,45 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ninja.WebSockets
|
||||
{
|
||||
/// <summary>
|
||||
/// Web socket client factory used to open web socket client connections
|
||||
/// </summary>
|
||||
public interface IWebSocketClientFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Connect with default options
|
||||
/// </summary>
|
||||
/// <param name="uri">The WebSocket uri to connect to (e.g. ws://example.com or wss://example.com for SSL)</param>
|
||||
/// <param name="token">The optional cancellation token</param>
|
||||
/// <returns>A connected web socket instance</returns>
|
||||
Task<WebSocket> ConnectAsync(Uri uri, CancellationToken token = default(CancellationToken));
|
||||
|
||||
/// <summary>
|
||||
/// Connect with options specified
|
||||
/// </summary>
|
||||
/// <param name="uri">The WebSocket uri to connect to (e.g. ws://example.com or wss://example.com for SSL)</param>
|
||||
/// <param name="options">The WebSocket client options</param>
|
||||
/// <param name="token">The optional cancellation token</param>
|
||||
/// <returns>A connected web socket instance</returns>
|
||||
Task<WebSocket> ConnectAsync(Uri uri, WebSocketClientOptions options, CancellationToken token = default(CancellationToken));
|
||||
|
||||
/// <summary>
|
||||
/// Connect with a stream that has already been opened and HTTP websocket upgrade request sent
|
||||
/// This function will check the handshake response from the server and proceed if successful
|
||||
/// Use this function if you have specific requirements to open a conenction like using special http headers and cookies
|
||||
/// You will have to build your own HTTP websocket upgrade request
|
||||
/// You may not even choose to use TCP/IP and this function will allow you to do that
|
||||
/// </summary>
|
||||
/// <param name="responseStream">The full duplex response stream from the server</param>
|
||||
/// <param name="secWebSocketKey">The secWebSocketKey you used in the handshake request</param>
|
||||
/// <param name="options">The WebSocket client options</param>
|
||||
/// <param name="token">The optional cancellation token</param>
|
||||
/// <returns></returns>
|
||||
Task<WebSocket> ConnectAsync(Stream responseStream, string secWebSocketKey, WebSocketClientOptions options, CancellationToken token = default(CancellationToken));
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 290e337adc73544809ef6db4d09ec5bc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,41 +0,0 @@
|
||||
using System.IO;
|
||||
using System.Net.Sockets;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ninja.WebSockets
|
||||
{
|
||||
/// <summary>
|
||||
/// Web socket server factory used to open web socket server connections
|
||||
/// </summary>
|
||||
public interface IWebSocketServerFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads a http header information from a stream and decodes the parts relating to the WebSocket protocot upgrade
|
||||
/// </summary>
|
||||
/// <param name="stream">The network stream</param>
|
||||
/// <param name="token">The optional cancellation token</param>
|
||||
/// <returns>Http data read from the stream</returns>
|
||||
Task<WebSocketHttpContext> ReadHttpHeaderFromStreamAsync(TcpClient client, Stream stream, CancellationToken token = default(CancellationToken));
|
||||
|
||||
/// <summary>
|
||||
/// Accept web socket with default options
|
||||
/// Call ReadHttpHeaderFromStreamAsync first to get WebSocketHttpContext
|
||||
/// </summary>
|
||||
/// <param name="context">The http context used to initiate this web socket request</param>
|
||||
/// <param name="token">The optional cancellation token</param>
|
||||
/// <returns>A connected web socket</returns>
|
||||
Task<WebSocket> AcceptWebSocketAsync(WebSocketHttpContext context, CancellationToken token = default(CancellationToken));
|
||||
|
||||
/// <summary>
|
||||
/// Accept web socket with options specified
|
||||
/// Call ReadHttpHeaderFromStreamAsync first to get WebSocketHttpContext
|
||||
/// </summary>
|
||||
/// <param name="context">The http context used to initiate this web socket request</param>
|
||||
/// <param name="options">The web socket options</param>
|
||||
/// <param name="token">The optional cancellation token</param>
|
||||
/// <returns>A connected web socket</returns>
|
||||
Task<WebSocket> AcceptWebSocketAsync(WebSocketHttpContext context, WebSocketServerOptions options, CancellationToken token = default(CancellationToken));
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 33e062311740342db82c2f6e53fbce73
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 58fcf4fbb7c9b47eeaf267adb27810d3
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,152 +0,0 @@
|
||||
// ---------------------------------------------------------------------
|
||||
// Copyright 2018 David Haig
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ninja.WebSockets.Internal
|
||||
{
|
||||
internal class BinaryReaderWriter
|
||||
{
|
||||
public static async Task ReadExactly(int length, Stream stream, ArraySegment<byte> buffer, CancellationToken cancellationToken)
|
||||
{
|
||||
if (buffer.Count < length)
|
||||
{
|
||||
// This will happen if the calling function supplied a buffer that was too small to fit the payload of the websocket frame.
|
||||
// Note that this can happen on the close handshake where the message size can be larger than the regular payload
|
||||
throw new InternalBufferOverflowException($"Unable to read {length} bytes into buffer (offset: {buffer.Offset} size: {buffer.Count}). Use a larger read buffer");
|
||||
}
|
||||
|
||||
int offset = 0;
|
||||
while (offset < length)
|
||||
{
|
||||
int bytesRead = 0;
|
||||
|
||||
NetworkStream networkStream = stream as NetworkStream;
|
||||
if (networkStream != null && networkStream.DataAvailable)
|
||||
{
|
||||
// paul: if data is available read it immediatelly.
|
||||
// in my tests this performed a lot better, because ReadAsync always waited until
|
||||
// the next frame.
|
||||
bytesRead = stream.Read(buffer.Array, buffer.Offset + offset, length - offset);
|
||||
}
|
||||
else
|
||||
{
|
||||
bytesRead = await stream.ReadAsync(buffer.Array, buffer.Offset + offset, length - offset, cancellationToken);
|
||||
}
|
||||
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
throw new EndOfStreamException(string.Format("Unexpected end of stream encountered whilst attempting to read {0:#,##0} bytes", length));
|
||||
}
|
||||
|
||||
offset += bytesRead;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<ushort> ReadUShortExactly(Stream stream, bool isLittleEndian, ArraySegment<byte> buffer, CancellationToken cancellationToken)
|
||||
{
|
||||
await ReadExactly(2, stream, buffer, cancellationToken);
|
||||
|
||||
if (!isLittleEndian)
|
||||
{
|
||||
// big endian
|
||||
Array.Reverse(buffer.Array, buffer.Offset, 2);
|
||||
}
|
||||
|
||||
return BitConverter.ToUInt16(buffer.Array, buffer.Offset);
|
||||
}
|
||||
|
||||
public static async Task<ulong> ReadULongExactly(Stream stream, bool isLittleEndian, ArraySegment<byte> buffer, CancellationToken cancellationToken)
|
||||
{
|
||||
await ReadExactly(8, stream, buffer, cancellationToken);
|
||||
|
||||
if (!isLittleEndian)
|
||||
{
|
||||
// big endian
|
||||
Array.Reverse(buffer.Array, buffer.Offset, 8);
|
||||
}
|
||||
|
||||
return BitConverter.ToUInt64(buffer.Array, buffer.Offset);
|
||||
}
|
||||
|
||||
public static async Task<long> ReadLongExactly(Stream stream, bool isLittleEndian, ArraySegment<byte> buffer, CancellationToken cancellationToken)
|
||||
{
|
||||
await ReadExactly(8, stream, buffer, cancellationToken);
|
||||
|
||||
if (!isLittleEndian)
|
||||
{
|
||||
// big endian
|
||||
Array.Reverse(buffer.Array, buffer.Offset, 8);
|
||||
}
|
||||
|
||||
return BitConverter.ToInt64(buffer.Array, buffer.Offset);
|
||||
}
|
||||
|
||||
public static void WriteInt(int value, Stream stream, bool isLittleEndian)
|
||||
{
|
||||
byte[] buffer = BitConverter.GetBytes(value);
|
||||
if (BitConverter.IsLittleEndian && !isLittleEndian)
|
||||
{
|
||||
Array.Reverse(buffer);
|
||||
}
|
||||
|
||||
stream.Write(buffer, 0, buffer.Length);
|
||||
}
|
||||
|
||||
public static void WriteULong(ulong value, Stream stream, bool isLittleEndian)
|
||||
{
|
||||
byte[] buffer = BitConverter.GetBytes(value);
|
||||
if (BitConverter.IsLittleEndian && !isLittleEndian)
|
||||
{
|
||||
Array.Reverse(buffer);
|
||||
}
|
||||
|
||||
stream.Write(buffer, 0, buffer.Length);
|
||||
}
|
||||
|
||||
public static void WriteLong(long value, Stream stream, bool isLittleEndian)
|
||||
{
|
||||
byte[] buffer = BitConverter.GetBytes(value);
|
||||
if (BitConverter.IsLittleEndian && !isLittleEndian)
|
||||
{
|
||||
Array.Reverse(buffer);
|
||||
}
|
||||
|
||||
stream.Write(buffer, 0, buffer.Length);
|
||||
}
|
||||
|
||||
public static void WriteUShort(ushort value, Stream stream, bool isLittleEndian)
|
||||
{
|
||||
byte[] buffer = BitConverter.GetBytes(value);
|
||||
if (BitConverter.IsLittleEndian && !isLittleEndian)
|
||||
{
|
||||
Array.Reverse(buffer);
|
||||
}
|
||||
|
||||
stream.Write(buffer, 0, buffer.Length);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2379de83fde9446689e7fd94d8cefca1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,393 +0,0 @@
|
||||
// ---------------------------------------------------------------------
|
||||
// Copyright 2018 David Haig
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.Tracing;
|
||||
using System.Net.Security;
|
||||
using System.Net.WebSockets;
|
||||
|
||||
namespace Ninja.WebSockets.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// Use the Guid to locate this EventSource in PerfView using the Additional Providers box (without wildcard characters)
|
||||
/// </summary>
|
||||
[EventSource(Name = "Ninja-WebSockets", Guid = "7DE1A071-4F85-4DBD-8FB1-EE8D3845E087")]
|
||||
internal sealed class Events : EventSource
|
||||
{
|
||||
public static Events Log = new Events();
|
||||
|
||||
[Event(1, Level = EventLevel.Informational)]
|
||||
public void ClientConnectingToIpAddress(Guid guid, string ipAddress, int port)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(1, guid, ipAddress, port);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(2, Level = EventLevel.Informational)]
|
||||
public void ClientConnectingToHost(Guid guid, string host, int port)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(2, guid, host, port);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(3, Level = EventLevel.Informational)]
|
||||
public void AttemtingToSecureSslConnection(Guid guid)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(3, guid);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(4, Level = EventLevel.Informational)]
|
||||
public void ConnectionSecured(Guid guid)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(4, guid);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(5, Level = EventLevel.Informational)]
|
||||
public void ConnectionNotSecure(Guid guid)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(5, guid);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(6, Level = EventLevel.Error)]
|
||||
public void SslCertificateError(SslPolicyErrors sslPolicyErrors)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(6, sslPolicyErrors);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(7, Level = EventLevel.Informational)]
|
||||
public void HandshakeSent(Guid guid, string httpHeader)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(7, guid, httpHeader ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(8, Level = EventLevel.Informational)]
|
||||
public void ReadingHttpResponse(Guid guid)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(8, guid);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(9, Level = EventLevel.Error)]
|
||||
public void ReadHttpResponseError(Guid guid, string exception)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(9, guid, exception ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(10, Level = EventLevel.Warning)]
|
||||
public void InvalidHttpResponseCode(Guid guid, string response)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(10, guid, response ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(11, Level = EventLevel.Error)]
|
||||
public void HandshakeFailure(Guid guid, string message)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(11, guid, message ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(12, Level = EventLevel.Informational)]
|
||||
public void ClientHandshakeSuccess(Guid guid)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(12, guid);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(13, Level = EventLevel.Informational)]
|
||||
public void ServerHandshakeSuccess(Guid guid)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(13, guid);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(14, Level = EventLevel.Informational)]
|
||||
public void AcceptWebSocketStarted(Guid guid)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(14, guid);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(15, Level = EventLevel.Informational)]
|
||||
public void SendingHandshakeResponse(Guid guid, string response)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(15, guid, response ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(16, Level = EventLevel.Error)]
|
||||
public void WebSocketVersionNotSupported(Guid guid, string exception)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(16, guid, exception ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(17, Level = EventLevel.Error)]
|
||||
public void BadRequest(Guid guid, string exception)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(17, guid, exception ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(18, Level = EventLevel.Informational)]
|
||||
public void UsePerMessageDeflate(Guid guid)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(18, guid);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(19, Level = EventLevel.Informational)]
|
||||
public void NoMessageCompression(Guid guid)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(19, guid);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(20, Level = EventLevel.Informational)]
|
||||
public void KeepAliveIntervalZero(Guid guid)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(20, guid);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(21, Level = EventLevel.Informational)]
|
||||
public void PingPongManagerStarted(Guid guid, int keepAliveIntervalSeconds)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(21, guid, keepAliveIntervalSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(22, Level = EventLevel.Informational)]
|
||||
public void PingPongManagerEnded(Guid guid)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(22, guid);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(23, Level = EventLevel.Warning)]
|
||||
public void KeepAliveIntervalExpired(Guid guid, int keepAliveIntervalSeconds)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(23, guid, keepAliveIntervalSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(24, Level = EventLevel.Warning)]
|
||||
public void CloseOutputAutoTimeout(Guid guid, WebSocketCloseStatus closeStatus, string statusDescription, string exception)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(24, guid, closeStatus, statusDescription ?? string.Empty, exception ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(25, Level = EventLevel.Error)]
|
||||
public void CloseOutputAutoTimeoutCancelled(Guid guid, int timeoutSeconds, WebSocketCloseStatus closeStatus, string statusDescription, string exception)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(25, guid, timeoutSeconds, closeStatus, statusDescription ?? string.Empty, exception ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(26, Level = EventLevel.Error)]
|
||||
public void CloseOutputAutoTimeoutError(Guid guid, string closeException, WebSocketCloseStatus closeStatus, string statusDescription, string exception)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(26, guid, closeException ?? string.Empty, closeStatus, statusDescription ?? string.Empty, exception ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(27, Level = EventLevel.Warning)]
|
||||
public void TryGetBufferNotSupported(Guid guid, string streamType)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(27, guid, streamType ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(28, Level = EventLevel.Verbose)]
|
||||
public void SendingFrame(Guid guid, WebSocketOpCode webSocketOpCode, bool isFinBitSet, int numBytes, bool isPayloadCompressed)
|
||||
{
|
||||
if (IsEnabled(EventLevel.Verbose, EventKeywords.None))
|
||||
{
|
||||
WriteEvent(28, guid, webSocketOpCode, isFinBitSet, numBytes, isPayloadCompressed);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(29, Level = EventLevel.Verbose)]
|
||||
public void ReceivedFrame(Guid guid, WebSocketOpCode webSocketOpCode, bool isFinBitSet, int numBytes)
|
||||
{
|
||||
if (IsEnabled(EventLevel.Verbose, EventKeywords.None))
|
||||
{
|
||||
WriteEvent(29, guid, webSocketOpCode, isFinBitSet, numBytes);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(30, Level = EventLevel.Informational)]
|
||||
public void CloseOutputNoHandshake(Guid guid, WebSocketCloseStatus? closeStatus, string statusDescription)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
string closeStatusDesc = $"{closeStatus}";
|
||||
WriteEvent(30, guid, closeStatusDesc, statusDescription ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(31, Level = EventLevel.Informational)]
|
||||
public void CloseHandshakeStarted(Guid guid, WebSocketCloseStatus? closeStatus, string statusDescription)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
string closeStatusDesc = $"{closeStatus}";
|
||||
WriteEvent(31, guid, closeStatusDesc, statusDescription ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(32, Level = EventLevel.Informational)]
|
||||
public void CloseHandshakeRespond(Guid guid, WebSocketCloseStatus? closeStatus, string statusDescription)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
string closeStatusDesc = $"{closeStatus}";
|
||||
WriteEvent(32, guid, closeStatusDesc, statusDescription ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(33, Level = EventLevel.Informational)]
|
||||
public void CloseHandshakeComplete(Guid guid)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(33, guid);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(34, Level = EventLevel.Warning)]
|
||||
public void CloseFrameReceivedInUnexpectedState(Guid guid, WebSocketState webSocketState, WebSocketCloseStatus? closeStatus, string statusDescription)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
string closeStatusDesc = $"{closeStatus}";
|
||||
WriteEvent(34, guid, webSocketState, closeStatusDesc, statusDescription ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(35, Level = EventLevel.Informational)]
|
||||
public void WebSocketDispose(Guid guid, WebSocketState webSocketState)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(35, guid, webSocketState);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(36, Level = EventLevel.Warning)]
|
||||
public void WebSocketDisposeCloseTimeout(Guid guid, WebSocketState webSocketState)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(36, guid, webSocketState);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(37, Level = EventLevel.Error)]
|
||||
public void WebSocketDisposeError(Guid guid, WebSocketState webSocketState, string exception)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(37, guid, webSocketState, exception ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(38, Level = EventLevel.Warning)]
|
||||
public void InvalidStateBeforeClose(Guid guid, WebSocketState webSocketState)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(38, guid, webSocketState);
|
||||
}
|
||||
}
|
||||
|
||||
[Event(39, Level = EventLevel.Warning)]
|
||||
public void InvalidStateBeforeCloseOutput(Guid guid, WebSocketState webSocketState)
|
||||
{
|
||||
if (IsEnabled())
|
||||
{
|
||||
WriteEvent(39, guid, webSocketState);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7713ae9d113e145389c0a175d58090be
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,31 +0,0 @@
|
||||
using System.Net.WebSockets;
|
||||
|
||||
namespace Ninja.WebSockets.Internal
|
||||
{
|
||||
internal class WebSocketFrame
|
||||
{
|
||||
public bool IsFinBitSet { get; private set; }
|
||||
|
||||
public WebSocketOpCode OpCode { get; private set; }
|
||||
|
||||
public int Count { get; private set; }
|
||||
|
||||
public WebSocketCloseStatus? CloseStatus { get; private set; }
|
||||
|
||||
public string CloseStatusDescription { get; private set; }
|
||||
|
||||
public WebSocketFrame(bool isFinBitSet, WebSocketOpCode webSocketOpCode, int count)
|
||||
{
|
||||
IsFinBitSet = isFinBitSet;
|
||||
OpCode = webSocketOpCode;
|
||||
Count = count;
|
||||
}
|
||||
|
||||
public WebSocketFrame(bool isFinBitSet, WebSocketOpCode webSocketOpCode, int count, WebSocketCloseStatus closeStatus, string closeStatusDescription) : this(isFinBitSet, webSocketOpCode, count)
|
||||
{
|
||||
CloseStatus = closeStatus;
|
||||
CloseStatusDescription = closeStatusDescription;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a4e8a4f1bcb0c49d7a82e5f430c85e2d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,63 +0,0 @@
|
||||
// ---------------------------------------------------------------------
|
||||
// Copyright 2018 David Haig
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
|
||||
namespace Ninja.WebSockets.Internal
|
||||
{
|
||||
internal static class WebSocketFrameCommon
|
||||
{
|
||||
public const int MaskKeyLength = 4;
|
||||
|
||||
/// <summary>
|
||||
/// Mutate payload with the mask key
|
||||
/// This is a reversible process
|
||||
/// If you apply this to masked data it will be unmasked and visa versa
|
||||
/// </summary>
|
||||
/// <param name="maskKey">The 4 byte mask key</param>
|
||||
/// <param name="payload">The payload to mutate</param>
|
||||
public static void ToggleMask(ArraySegment<byte> maskKey, ArraySegment<byte> payload)
|
||||
{
|
||||
if (maskKey.Count != MaskKeyLength)
|
||||
{
|
||||
throw new Exception($"MaskKey key must be {MaskKeyLength} bytes");
|
||||
}
|
||||
|
||||
byte[] buffer = payload.Array;
|
||||
byte[] maskKeyArray = maskKey.Array;
|
||||
int payloadOffset = payload.Offset;
|
||||
int payloadCount = payload.Count;
|
||||
int maskKeyOffset = maskKey.Offset;
|
||||
|
||||
// apply the mask key (this is a reversible process so no need to copy the payload)
|
||||
// NOTE: this is a hot function
|
||||
// TODO: make this faster
|
||||
for (int i = payloadOffset; i < payloadCount; i++)
|
||||
{
|
||||
// index should start at zero
|
||||
int payloadIndex = i - payloadOffset;
|
||||
int maskKeyIndex = maskKeyOffset + (payloadIndex % MaskKeyLength);
|
||||
buffer[i] = (Byte)(buffer[i] ^ maskKeyArray[maskKeyIndex]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: adbf6c11b66794c0494acf9c713a6759
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,170 +0,0 @@
|
||||
// ---------------------------------------------------------------------
|
||||
// Copyright 2018 David Haig
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ninja.WebSockets.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads a WebSocket frame
|
||||
/// see http://tools.ietf.org/html/rfc6455 for specification
|
||||
/// </summary>
|
||||
internal static class WebSocketFrameReader
|
||||
{
|
||||
/// <summary>
|
||||
/// Read a WebSocket frame from the stream
|
||||
/// </summary>
|
||||
/// <param name="fromStream">The stream to read from</param>
|
||||
/// <param name="intoBuffer">The buffer to read into</param>
|
||||
/// <param name="cancellationToken">the cancellation token</param>
|
||||
/// <returns>A websocket frame</returns>
|
||||
public static async Task<WebSocketFrame> ReadAsync(Stream fromStream, ArraySegment<byte> intoBuffer, CancellationToken cancellationToken)
|
||||
{
|
||||
// allocate a small buffer to read small chunks of data from the stream
|
||||
ArraySegment<byte> smallBuffer = new ArraySegment<byte>(new byte[8]);
|
||||
|
||||
await BinaryReaderWriter.ReadExactly(2, fromStream, smallBuffer, cancellationToken);
|
||||
byte byte1 = smallBuffer.Array[0];
|
||||
byte byte2 = smallBuffer.Array[1];
|
||||
|
||||
// process first byte
|
||||
byte finBitFlag = 0x80;
|
||||
byte opCodeFlag = 0x0F;
|
||||
bool isFinBitSet = (byte1 & finBitFlag) == finBitFlag;
|
||||
WebSocketOpCode opCode = (WebSocketOpCode)(byte1 & opCodeFlag);
|
||||
|
||||
// read and process second byte
|
||||
byte maskFlag = 0x80;
|
||||
bool isMaskBitSet = (byte2 & maskFlag) == maskFlag;
|
||||
uint len = await ReadLength(byte2, smallBuffer, fromStream, cancellationToken);
|
||||
int count = (int)len;
|
||||
|
||||
try
|
||||
{
|
||||
// use the masking key to decode the data if needed
|
||||
if (isMaskBitSet)
|
||||
{
|
||||
ArraySegment<byte> maskKey = new ArraySegment<byte>(smallBuffer.Array, 0, WebSocketFrameCommon.MaskKeyLength);
|
||||
await BinaryReaderWriter.ReadExactly(maskKey.Count, fromStream, maskKey, cancellationToken);
|
||||
await BinaryReaderWriter.ReadExactly(count, fromStream, intoBuffer, cancellationToken);
|
||||
ArraySegment<byte> payloadToMask = new ArraySegment<byte>(intoBuffer.Array, intoBuffer.Offset, count);
|
||||
WebSocketFrameCommon.ToggleMask(maskKey, payloadToMask);
|
||||
}
|
||||
else
|
||||
{
|
||||
await BinaryReaderWriter.ReadExactly(count, fromStream, intoBuffer, cancellationToken);
|
||||
}
|
||||
}
|
||||
catch (InternalBufferOverflowException e)
|
||||
{
|
||||
throw new InternalBufferOverflowException($"Supplied buffer too small to read {0} bytes from {Enum.GetName(typeof(WebSocketOpCode), opCode)} frame", e);
|
||||
}
|
||||
|
||||
if (opCode == WebSocketOpCode.ConnectionClose)
|
||||
{
|
||||
return DecodeCloseFrame(isFinBitSet, opCode, count, intoBuffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
// note that by this point the payload will be populated
|
||||
return new WebSocketFrame(isFinBitSet, opCode, count);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts close status and close description information from the web socket frame
|
||||
/// </summary>
|
||||
static WebSocketFrame DecodeCloseFrame(bool isFinBitSet, WebSocketOpCode opCode, int count, ArraySegment<byte> buffer)
|
||||
{
|
||||
WebSocketCloseStatus closeStatus;
|
||||
string closeStatusDescription;
|
||||
|
||||
if (count >= 2)
|
||||
{
|
||||
// network byte order
|
||||
Array.Reverse(buffer.Array, buffer.Offset, 2);
|
||||
int closeStatusCode = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
|
||||
if (Enum.IsDefined(typeof(WebSocketCloseStatus), closeStatusCode))
|
||||
{
|
||||
closeStatus = (WebSocketCloseStatus)closeStatusCode;
|
||||
}
|
||||
else
|
||||
{
|
||||
closeStatus = WebSocketCloseStatus.Empty;
|
||||
}
|
||||
|
||||
int offset = buffer.Offset + 2;
|
||||
int descCount = count - 2;
|
||||
|
||||
if (descCount > 0)
|
||||
{
|
||||
closeStatusDescription = Encoding.UTF8.GetString(buffer.Array, offset, descCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
closeStatusDescription = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
closeStatus = WebSocketCloseStatus.Empty;
|
||||
closeStatusDescription = null;
|
||||
}
|
||||
|
||||
return new WebSocketFrame(isFinBitSet, opCode, count, closeStatus, closeStatusDescription);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the length of the payload according to the contents of byte2
|
||||
/// </summary>
|
||||
static async Task<uint> ReadLength(byte byte2, ArraySegment<byte> smallBuffer, Stream fromStream, CancellationToken cancellationToken)
|
||||
{
|
||||
byte payloadLenFlag = 0x7F;
|
||||
uint len = (uint)(byte2 & payloadLenFlag);
|
||||
|
||||
// read a short length or a long length depending on the value of len
|
||||
if (len == 126)
|
||||
{
|
||||
len = await BinaryReaderWriter.ReadUShortExactly(fromStream, false, smallBuffer, cancellationToken);
|
||||
}
|
||||
else if (len == 127)
|
||||
{
|
||||
len = (uint)await BinaryReaderWriter.ReadULongExactly(fromStream, false, smallBuffer, cancellationToken);
|
||||
// 2GB - not part of the spec but just a precaution. Send large volumes of data in smaller frames.
|
||||
const uint maxLen = 2147483648;
|
||||
|
||||
// protect ourselves against bad data
|
||||
if (len > maxLen || len < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException($"Payload length out of range. Min 0 max 2GB. Actual {len:#,##0} bytes.");
|
||||
}
|
||||
}
|
||||
|
||||
return len;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 092a4b88aabbc4d3b91f6a82c40b47e3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,98 +0,0 @@
|
||||
// ---------------------------------------------------------------------
|
||||
// Copyright 2018 David Haig
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Ninja.WebSockets.Internal
|
||||
{
|
||||
// see http://tools.ietf.org/html/rfc6455 for specification
|
||||
// see fragmentation section for sending multi part messages
|
||||
// EXAMPLE: For a text message sent as three fragments,
|
||||
// the first fragment would have an opcode of TextFrame and isLastFrame false,
|
||||
// the second fragment would have an opcode of ContinuationFrame and isLastFrame false,
|
||||
// the third fragment would have an opcode of ContinuationFrame and isLastFrame true.
|
||||
internal static class WebSocketFrameWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// This is used for data masking so that web proxies don't cache the data
|
||||
/// Therefore, there are no cryptographic concerns
|
||||
/// </summary>
|
||||
static readonly Random _random;
|
||||
|
||||
static WebSocketFrameWriter()
|
||||
{
|
||||
_random = new Random((int)DateTime.Now.Ticks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// No async await stuff here because we are dealing with a memory stream
|
||||
/// </summary>
|
||||
/// <param name="opCode">The web socket opcode</param>
|
||||
/// <param name="fromPayload">Array segment to get payload data from</param>
|
||||
/// <param name="toStream">Stream to write to</param>
|
||||
/// <param name="isLastFrame">True is this is the last frame in this message (usually true)</param>
|
||||
public static void Write(WebSocketOpCode opCode, ArraySegment<byte> fromPayload, MemoryStream toStream, bool isLastFrame, bool isClient)
|
||||
{
|
||||
MemoryStream memoryStream = toStream;
|
||||
byte finBitSetAsByte = isLastFrame ? (byte)0x80 : (byte)0x00;
|
||||
byte byte1 = (byte)(finBitSetAsByte | (byte)opCode);
|
||||
memoryStream.WriteByte(byte1);
|
||||
|
||||
// NB, set the mask flag if we are constructing a client frame
|
||||
byte maskBitSetAsByte = isClient ? (byte)0x80 : (byte)0x00;
|
||||
|
||||
// depending on the size of the length we want to write it as a byte, ushort or ulong
|
||||
if (fromPayload.Count < 126)
|
||||
{
|
||||
byte byte2 = (byte)(maskBitSetAsByte | (byte)fromPayload.Count);
|
||||
memoryStream.WriteByte(byte2);
|
||||
}
|
||||
else if (fromPayload.Count <= ushort.MaxValue)
|
||||
{
|
||||
byte byte2 = (byte)(maskBitSetAsByte | 126);
|
||||
memoryStream.WriteByte(byte2);
|
||||
BinaryReaderWriter.WriteUShort((ushort)fromPayload.Count, memoryStream, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
byte byte2 = (byte)(maskBitSetAsByte | 127);
|
||||
memoryStream.WriteByte(byte2);
|
||||
BinaryReaderWriter.WriteULong((ulong)fromPayload.Count, memoryStream, false);
|
||||
}
|
||||
|
||||
// if we are creating a client frame then we MUST mack the payload as per the spec
|
||||
if (isClient)
|
||||
{
|
||||
byte[] maskKey = new byte[WebSocketFrameCommon.MaskKeyLength];
|
||||
_random.NextBytes(maskKey);
|
||||
memoryStream.Write(maskKey, 0, maskKey.Length);
|
||||
|
||||
// mask the payload
|
||||
ArraySegment<byte> maskKeyArraySegment = new ArraySegment<byte>(maskKey, 0, maskKey.Length);
|
||||
WebSocketFrameCommon.ToggleMask(maskKeyArraySegment, fromPayload);
|
||||
}
|
||||
|
||||
memoryStream.Write(fromPayload.Array, fromPayload.Offset, fromPayload.Count);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: df337f1f7ba5245f7acadbe2e44b86bd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,612 +0,0 @@
|
||||
// ---------------------------------------------------------------------
|
||||
// Copyright 2018 David Haig
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Net.WebSockets;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#if RELEASESIGNED
|
||||
[assembly: InternalsVisibleTo("Ninja.WebSockets.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1707056f4761b7846ed503642fcde97fc350c939f78026211304a56ba51e094c9cefde77fadce5b83c0a621c17f032c37c520b6d9ab2da8291a21472175d9caad55bf67bab4bffb46a96f864ea441cf695edc854296e02a44062245a4e09ccd9a77ef6146ecf941ce1d9da078add54bc2d4008decdac2fa2b388e17794ee6a6")]
|
||||
#else
|
||||
[assembly: InternalsVisibleTo("Ninja.WebSockets.UnitTests")]
|
||||
#endif
|
||||
|
||||
namespace Ninja.WebSockets.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// Main implementation of the WebSocket abstract class
|
||||
/// </summary>
|
||||
public class WebSocketImplementation : WebSocket
|
||||
{
|
||||
readonly Guid _guid;
|
||||
readonly Func<MemoryStream> _recycledStreamFactory;
|
||||
readonly Stream _stream;
|
||||
readonly bool _includeExceptionInCloseResponse;
|
||||
readonly bool _isClient;
|
||||
readonly string _subProtocol;
|
||||
CancellationTokenSource _internalReadCts;
|
||||
WebSocketState _state;
|
||||
bool _isContinuationFrame;
|
||||
WebSocketMessageType _continuationFrameMessageType = WebSocketMessageType.Binary;
|
||||
readonly bool _usePerMessageDeflate = false;
|
||||
bool _tryGetBufferFailureLogged = false;
|
||||
const int MAX_PING_PONG_PAYLOAD_LEN = 125;
|
||||
WebSocketCloseStatus? _closeStatus;
|
||||
string _closeStatusDescription;
|
||||
|
||||
public event EventHandler<PongEventArgs> Pong;
|
||||
|
||||
public WebSocketHttpContext Context { get; set; }
|
||||
|
||||
internal WebSocketImplementation(Guid guid, Func<MemoryStream> recycledStreamFactory, Stream stream, TimeSpan keepAliveInterval, string secWebSocketExtensions, bool includeExceptionInCloseResponse, bool isClient, string subProtocol)
|
||||
{
|
||||
_guid = guid;
|
||||
_recycledStreamFactory = recycledStreamFactory;
|
||||
_stream = stream;
|
||||
_isClient = isClient;
|
||||
_subProtocol = subProtocol;
|
||||
_internalReadCts = new CancellationTokenSource();
|
||||
_state = WebSocketState.Open;
|
||||
|
||||
if (secWebSocketExtensions?.IndexOf("permessage-deflate") >= 0)
|
||||
{
|
||||
_usePerMessageDeflate = true;
|
||||
Events.Log.UsePerMessageDeflate(guid);
|
||||
}
|
||||
else
|
||||
{
|
||||
Events.Log.NoMessageCompression(guid);
|
||||
}
|
||||
|
||||
KeepAliveInterval = keepAliveInterval;
|
||||
_includeExceptionInCloseResponse = includeExceptionInCloseResponse;
|
||||
if (keepAliveInterval.Ticks < 0)
|
||||
{
|
||||
throw new InvalidOperationException("KeepAliveInterval must be Zero or positive");
|
||||
}
|
||||
|
||||
if (keepAliveInterval == TimeSpan.Zero)
|
||||
{
|
||||
Events.Log.KeepAliveIntervalZero(guid);
|
||||
}
|
||||
else
|
||||
{
|
||||
// the ping pong manager starts a task
|
||||
// but we don't have to keep a reference to it
|
||||
_ = new PingPongManager(guid, this, keepAliveInterval, _internalReadCts.Token);
|
||||
}
|
||||
}
|
||||
|
||||
public override WebSocketCloseStatus? CloseStatus => _closeStatus;
|
||||
|
||||
public override string CloseStatusDescription => _closeStatusDescription;
|
||||
|
||||
public override WebSocketState State { get { return _state; } }
|
||||
|
||||
public override string SubProtocol => _subProtocol;
|
||||
|
||||
public TimeSpan KeepAliveInterval { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Receive web socket result
|
||||
/// </summary>
|
||||
/// <param name="buffer">The buffer to copy data into</param>
|
||||
/// <param name="cancellationToken">The cancellation token</param>
|
||||
/// <returns>The web socket result details</returns>
|
||||
public override async Task<WebSocketReceiveResult> ReceiveAsync(ArraySegment<byte> buffer, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
// we may receive control frames so reading needs to happen in an infinite loop
|
||||
while (true)
|
||||
{
|
||||
// allow this operation to be cancelled from iniside OR outside this instance
|
||||
using (CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_internalReadCts.Token, cancellationToken))
|
||||
{
|
||||
WebSocketFrame frame = null;
|
||||
try
|
||||
{
|
||||
frame = await WebSocketFrameReader.ReadAsync(_stream, buffer, linkedCts.Token);
|
||||
Events.Log.ReceivedFrame(_guid, frame.OpCode, frame.IsFinBitSet, frame.Count);
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
// do nothing, the socket has been disconnected
|
||||
}
|
||||
catch (InternalBufferOverflowException ex)
|
||||
{
|
||||
await CloseOutputAutoTimeoutAsync(WebSocketCloseStatus.MessageTooBig, "Frame too large to fit in buffer. Use message fragmentation", ex);
|
||||
throw;
|
||||
}
|
||||
catch (ArgumentOutOfRangeException ex)
|
||||
{
|
||||
await CloseOutputAutoTimeoutAsync(WebSocketCloseStatus.ProtocolError, "Payload length out of range", ex);
|
||||
throw;
|
||||
}
|
||||
catch (EndOfStreamException ex)
|
||||
{
|
||||
await CloseOutputAutoTimeoutAsync(WebSocketCloseStatus.InvalidPayloadData, "Unexpected end of stream encountered", ex);
|
||||
throw;
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
await CloseOutputAutoTimeoutAsync(WebSocketCloseStatus.EndpointUnavailable, "Operation cancelled", ex);
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await CloseOutputAutoTimeoutAsync(WebSocketCloseStatus.InternalServerError, "Error reading WebSocket frame", ex);
|
||||
throw;
|
||||
}
|
||||
|
||||
switch (frame.OpCode)
|
||||
{
|
||||
case WebSocketOpCode.ConnectionClose:
|
||||
return await RespondToCloseFrame(frame, buffer, linkedCts.Token);
|
||||
case WebSocketOpCode.Ping:
|
||||
ArraySegment<byte> pingPayload = new ArraySegment<byte>(buffer.Array, buffer.Offset, frame.Count);
|
||||
await SendPongAsync(pingPayload, linkedCts.Token);
|
||||
break;
|
||||
case WebSocketOpCode.Pong:
|
||||
ArraySegment<byte> pongBuffer = new ArraySegment<byte>(buffer.Array, frame.Count, buffer.Offset);
|
||||
Pong?.Invoke(this, new PongEventArgs(pongBuffer));
|
||||
break;
|
||||
case WebSocketOpCode.TextFrame:
|
||||
if (!frame.IsFinBitSet)
|
||||
{
|
||||
// continuation frames will follow, record the message type Text
|
||||
_continuationFrameMessageType = WebSocketMessageType.Text;
|
||||
}
|
||||
return new WebSocketReceiveResult(frame.Count, WebSocketMessageType.Text, frame.IsFinBitSet);
|
||||
case WebSocketOpCode.BinaryFrame:
|
||||
if (!frame.IsFinBitSet)
|
||||
{
|
||||
// continuation frames will follow, record the message type Binary
|
||||
_continuationFrameMessageType = WebSocketMessageType.Binary;
|
||||
}
|
||||
return new WebSocketReceiveResult(frame.Count, WebSocketMessageType.Binary, frame.IsFinBitSet);
|
||||
case WebSocketOpCode.ContinuationFrame:
|
||||
return new WebSocketReceiveResult(frame.Count, _continuationFrameMessageType, frame.IsFinBitSet);
|
||||
default:
|
||||
Exception ex = new NotSupportedException($"Unknown WebSocket opcode {frame.OpCode}");
|
||||
await CloseOutputAutoTimeoutAsync(WebSocketCloseStatus.ProtocolError, ex.Message, ex);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception catchAll)
|
||||
{
|
||||
// Most exceptions will be caught closer to their source to send an appropriate close message (and set the WebSocketState)
|
||||
// However, if an unhandled exception is encountered and a close message not sent then send one here
|
||||
if (_state == WebSocketState.Open)
|
||||
{
|
||||
await CloseOutputAutoTimeoutAsync(WebSocketCloseStatus.InternalServerError, "Unexpected error reading from WebSocket", catchAll);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send data to the web socket
|
||||
/// </summary>
|
||||
/// <param name="buffer">the buffer containing data to send</param>
|
||||
/// <param name="messageType">The message type. Can be Text or Binary</param>
|
||||
/// <param name="endOfMessage">True if this message is a standalone message (this is the norm)
|
||||
/// If it is a multi-part message then false (and true for the last message)</param>
|
||||
/// <param name="cancellationToken">the cancellation token</param>
|
||||
public override async Task SendAsync(ArraySegment<byte> buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken)
|
||||
{
|
||||
using (MemoryStream stream = _recycledStreamFactory())
|
||||
{
|
||||
WebSocketOpCode opCode = GetOppCode(messageType);
|
||||
|
||||
if (_usePerMessageDeflate)
|
||||
{
|
||||
// NOTE: Compression is currently work in progress and should NOT be used in this library.
|
||||
// The code below is very inefficient for small messages. Ideally we would like to have some sort of moving window
|
||||
// of data to get the best compression. And we don't want to create new buffers which is bad for GC.
|
||||
using (MemoryStream temp = new MemoryStream())
|
||||
{
|
||||
DeflateStream deflateStream = new DeflateStream(temp, CompressionMode.Compress);
|
||||
deflateStream.Write(buffer.Array, buffer.Offset, buffer.Count);
|
||||
deflateStream.Flush();
|
||||
ArraySegment<byte> compressedBuffer = new ArraySegment<byte>(temp.ToArray());
|
||||
WebSocketFrameWriter.Write(opCode, compressedBuffer, stream, endOfMessage, _isClient);
|
||||
Events.Log.SendingFrame(_guid, opCode, endOfMessage, compressedBuffer.Count, true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
WebSocketFrameWriter.Write(opCode, buffer, stream, endOfMessage, _isClient);
|
||||
Events.Log.SendingFrame(_guid, opCode, endOfMessage, buffer.Count, false);
|
||||
}
|
||||
|
||||
await WriteStreamToNetwork(stream, cancellationToken);
|
||||
// TODO: is this correct??
|
||||
_isContinuationFrame = !endOfMessage;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call this automatically from server side each keepAliveInterval period
|
||||
/// NOTE: ping payload must be 125 bytes or less
|
||||
/// </summary>
|
||||
public async Task SendPingAsync(ArraySegment<byte> payload, CancellationToken cancellationToken)
|
||||
{
|
||||
if (payload.Count > MAX_PING_PONG_PAYLOAD_LEN)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot send Ping: Max ping message size {MAX_PING_PONG_PAYLOAD_LEN} exceeded: {payload.Count}");
|
||||
}
|
||||
|
||||
if (_state == WebSocketState.Open)
|
||||
{
|
||||
using (MemoryStream stream = _recycledStreamFactory())
|
||||
{
|
||||
WebSocketFrameWriter.Write(WebSocketOpCode.Ping, payload, stream, true, _isClient);
|
||||
Events.Log.SendingFrame(_guid, WebSocketOpCode.Ping, true, payload.Count, false);
|
||||
await WriteStreamToNetwork(stream, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aborts the WebSocket without sending a Close frame
|
||||
/// </summary>
|
||||
public override void Abort()
|
||||
{
|
||||
_state = WebSocketState.Aborted;
|
||||
_internalReadCts.Cancel();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Polite close (use the close handshake)
|
||||
/// </summary>
|
||||
public override async Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_state == WebSocketState.Open)
|
||||
{
|
||||
using (MemoryStream stream = _recycledStreamFactory())
|
||||
{
|
||||
ArraySegment<byte> buffer = BuildClosePayload(closeStatus, statusDescription);
|
||||
WebSocketFrameWriter.Write(WebSocketOpCode.ConnectionClose, buffer, stream, true, _isClient);
|
||||
Events.Log.CloseHandshakeStarted(_guid, closeStatus, statusDescription);
|
||||
Events.Log.SendingFrame(_guid, WebSocketOpCode.ConnectionClose, true, buffer.Count, true);
|
||||
await WriteStreamToNetwork(stream, cancellationToken);
|
||||
_state = WebSocketState.CloseSent;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Events.Log.InvalidStateBeforeClose(_guid, _state);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fire and forget close
|
||||
/// </summary>
|
||||
public override async Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_state == WebSocketState.Open)
|
||||
{
|
||||
// set this before we write to the network because the write may fail
|
||||
_state = WebSocketState.Closed;
|
||||
|
||||
using (MemoryStream stream = _recycledStreamFactory())
|
||||
{
|
||||
ArraySegment<byte> buffer = BuildClosePayload(closeStatus, statusDescription);
|
||||
WebSocketFrameWriter.Write(WebSocketOpCode.ConnectionClose, buffer, stream, true, _isClient);
|
||||
Events.Log.CloseOutputNoHandshake(_guid, closeStatus, statusDescription);
|
||||
Events.Log.SendingFrame(_guid, WebSocketOpCode.ConnectionClose, true, buffer.Count, true);
|
||||
await WriteStreamToNetwork(stream, cancellationToken);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Events.Log.InvalidStateBeforeCloseOutput(_guid, _state);
|
||||
}
|
||||
|
||||
// cancel pending reads
|
||||
_internalReadCts.Cancel();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose will send a close frame if the connection is still open
|
||||
/// </summary>
|
||||
public override void Dispose()
|
||||
{
|
||||
Events.Log.WebSocketDispose(_guid, _state);
|
||||
|
||||
try
|
||||
{
|
||||
if (_state == WebSocketState.Open)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||
try
|
||||
{
|
||||
CloseOutputAsync(WebSocketCloseStatus.EndpointUnavailable, "Service is Disposed", cts.Token).Wait();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// log don't throw
|
||||
Events.Log.WebSocketDisposeCloseTimeout(_guid, _state);
|
||||
}
|
||||
}
|
||||
|
||||
// cancel pending reads - usually does nothing
|
||||
_internalReadCts.Cancel();
|
||||
_stream.Close();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// log dont throw
|
||||
Events.Log.WebSocketDisposeError(_guid, _state, ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a Pong frame is received
|
||||
/// </summary>
|
||||
/// <param name="e"></param>
|
||||
protected virtual void OnPong(PongEventArgs e)
|
||||
{
|
||||
Pong?.Invoke(this, e);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// As per the spec, write the close status followed by the close reason
|
||||
/// </summary>
|
||||
/// <param name="closeStatus">The close status</param>
|
||||
/// <param name="statusDescription">Optional extra close details</param>
|
||||
/// <returns>The payload to sent in the close frame</returns>
|
||||
ArraySegment<byte> BuildClosePayload(WebSocketCloseStatus closeStatus, string statusDescription)
|
||||
{
|
||||
byte[] statusBuffer = BitConverter.GetBytes((ushort)closeStatus);
|
||||
// network byte order (big endian)
|
||||
Array.Reverse(statusBuffer);
|
||||
|
||||
if (statusDescription == null)
|
||||
{
|
||||
return new ArraySegment<byte>(statusBuffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
byte[] descBuffer = Encoding.UTF8.GetBytes(statusDescription);
|
||||
byte[] payload = new byte[statusBuffer.Length + descBuffer.Length];
|
||||
Buffer.BlockCopy(statusBuffer, 0, payload, 0, statusBuffer.Length);
|
||||
Buffer.BlockCopy(descBuffer, 0, payload, statusBuffer.Length, descBuffer.Length);
|
||||
return new ArraySegment<byte>(payload);
|
||||
}
|
||||
}
|
||||
|
||||
/// NOTE: pong payload must be 125 bytes or less
|
||||
/// Pong should contain the same payload as the ping
|
||||
async Task SendPongAsync(ArraySegment<byte> payload, CancellationToken cancellationToken)
|
||||
{
|
||||
// as per websocket spec
|
||||
if (payload.Count > MAX_PING_PONG_PAYLOAD_LEN)
|
||||
{
|
||||
Exception ex = new InvalidOperationException($"Max ping message size {MAX_PING_PONG_PAYLOAD_LEN} exceeded: {payload.Count}");
|
||||
await CloseOutputAutoTimeoutAsync(WebSocketCloseStatus.ProtocolError, ex.Message, ex);
|
||||
throw ex;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_state == WebSocketState.Open)
|
||||
{
|
||||
using (MemoryStream stream = _recycledStreamFactory())
|
||||
{
|
||||
WebSocketFrameWriter.Write(WebSocketOpCode.Pong, payload, stream, true, _isClient);
|
||||
Events.Log.SendingFrame(_guid, WebSocketOpCode.Pong, true, payload.Count, false);
|
||||
await WriteStreamToNetwork(stream, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await CloseOutputAutoTimeoutAsync(WebSocketCloseStatus.EndpointUnavailable, "Unable to send Pong response", ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a Close frame is received
|
||||
/// Send a response close frame if applicable
|
||||
/// </summary>
|
||||
async Task<WebSocketReceiveResult> RespondToCloseFrame(WebSocketFrame frame, ArraySegment<byte> buffer, CancellationToken token)
|
||||
{
|
||||
_closeStatus = frame.CloseStatus;
|
||||
_closeStatusDescription = frame.CloseStatusDescription;
|
||||
|
||||
if (_state == WebSocketState.CloseSent)
|
||||
{
|
||||
// this is a response to close handshake initiated by this instance
|
||||
_state = WebSocketState.Closed;
|
||||
Events.Log.CloseHandshakeComplete(_guid);
|
||||
}
|
||||
else if (_state == WebSocketState.Open)
|
||||
{
|
||||
// do not echo the close payload back to the client, there is no requirement for it in the spec.
|
||||
// However, the same CloseStatus as recieved should be sent back.
|
||||
ArraySegment<byte> closePayload = new ArraySegment<byte>(new byte[0], 0, 0);
|
||||
_state = WebSocketState.CloseReceived;
|
||||
Events.Log.CloseHandshakeRespond(_guid, frame.CloseStatus, frame.CloseStatusDescription);
|
||||
|
||||
using (MemoryStream stream = _recycledStreamFactory())
|
||||
{
|
||||
WebSocketFrameWriter.Write(WebSocketOpCode.ConnectionClose, closePayload, stream, true, _isClient);
|
||||
Events.Log.SendingFrame(_guid, WebSocketOpCode.ConnectionClose, true, closePayload.Count, false);
|
||||
await WriteStreamToNetwork(stream, token);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Events.Log.CloseFrameReceivedInUnexpectedState(_guid, _state, frame.CloseStatus, frame.CloseStatusDescription);
|
||||
}
|
||||
|
||||
return new WebSocketReceiveResult(frame.Count, WebSocketMessageType.Close, frame.IsFinBitSet, frame.CloseStatus, frame.CloseStatusDescription);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Note that the way in which the stream buffer is accessed can lead to significant performance problems
|
||||
/// You want to avoid a call to stream.ToArray to avoid extra memory allocation
|
||||
/// MemoryStream can be configured to have its internal buffer accessible.
|
||||
/// </summary>
|
||||
ArraySegment<byte> GetBuffer(MemoryStream stream)
|
||||
{
|
||||
#if NET45
|
||||
// NET45 does not have a TryGetBuffer function on Stream
|
||||
if (_tryGetBufferFailureLogged)
|
||||
{
|
||||
return new ArraySegment<byte>(stream.ToArray(), 0, (int)stream.Position);
|
||||
}
|
||||
|
||||
// note that a MemoryStream will throw an UnuthorizedAccessException if the internal buffer is not public. Set publiclyVisible = true
|
||||
try
|
||||
{
|
||||
return new ArraySegment<byte>(stream.GetBuffer(), 0, (int)stream.Position);
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
Events.Log.TryGetBufferNotSupported(_guid, stream?.GetType()?.ToString());
|
||||
_tryGetBufferFailureLogged = true;
|
||||
return new ArraySegment<byte>(stream.ToArray(), 0, (int)stream.Position);
|
||||
}
|
||||
#else
|
||||
// Avoid calling ToArray on the MemoryStream because it allocates a new byte array on tha heap
|
||||
// We avaoid this by attempting to access the internal memory stream buffer
|
||||
// This works with supported streams like the recyclable memory stream and writable memory streams
|
||||
if (!stream.TryGetBuffer(out ArraySegment<byte> buffer))
|
||||
{
|
||||
if (!_tryGetBufferFailureLogged)
|
||||
{
|
||||
Events.Log.TryGetBufferNotSupported(_guid, stream?.GetType()?.ToString());
|
||||
_tryGetBufferFailureLogged = true;
|
||||
}
|
||||
|
||||
// internal buffer not suppoted, fall back to ToArray()
|
||||
byte[] array = stream.ToArray();
|
||||
buffer = new ArraySegment<byte>(array, 0, array.Length);
|
||||
}
|
||||
|
||||
return new ArraySegment<byte>(buffer.Array, buffer.Offset, (int)stream.Position);
|
||||
#endif
|
||||
}
|
||||
|
||||
Task writeTask = Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Puts data on the wire
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to read data from</param>
|
||||
async Task WriteStreamToNetwork(MemoryStream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
ArraySegment<byte> buffer = GetBuffer(stream);
|
||||
if (_stream is SslStream)
|
||||
{
|
||||
if (writeTask.IsCompleted)
|
||||
{
|
||||
writeTask = _stream.WriteAsync(buffer.Array, buffer.Offset, buffer.Count, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
writeTask = writeTask.ContinueWith((prevTask) =>
|
||||
_stream.WriteAsync(buffer.Array, buffer.Offset, buffer.Count, cancellationToken));
|
||||
}
|
||||
await writeTask;
|
||||
await _stream.FlushAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
await _stream.WriteAsync(buffer.Array, buffer.Offset, buffer.Count, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Turns a spec websocket frame opcode into a WebSocketMessageType
|
||||
/// </summary>
|
||||
WebSocketOpCode GetOppCode(WebSocketMessageType messageType)
|
||||
{
|
||||
if (_isContinuationFrame)
|
||||
{
|
||||
return WebSocketOpCode.ContinuationFrame;
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (messageType)
|
||||
{
|
||||
case WebSocketMessageType.Binary:
|
||||
return WebSocketOpCode.BinaryFrame;
|
||||
case WebSocketMessageType.Text:
|
||||
return WebSocketOpCode.TextFrame;
|
||||
case WebSocketMessageType.Close:
|
||||
throw new NotSupportedException("Cannot use Send function to send a close frame. Use Close function.");
|
||||
default:
|
||||
throw new NotSupportedException($"MessageType {messageType} not supported");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Automatic WebSocket close in response to some invalid data from the remote websocket host
|
||||
/// </summary>
|
||||
/// <param name="closeStatus">The close status to use</param>
|
||||
/// <param name="statusDescription">A description of why we are closing</param>
|
||||
/// <param name="ex">The exception (for logging)</param>
|
||||
async Task CloseOutputAutoTimeoutAsync(WebSocketCloseStatus closeStatus, string statusDescription, Exception ex)
|
||||
{
|
||||
TimeSpan timeSpan = TimeSpan.FromSeconds(5);
|
||||
Events.Log.CloseOutputAutoTimeout(_guid, closeStatus, statusDescription, ex.ToString());
|
||||
|
||||
try
|
||||
{
|
||||
// we may not want to send sensitive information to the client / server
|
||||
if (_includeExceptionInCloseResponse)
|
||||
{
|
||||
statusDescription = statusDescription + "\r\n\r\n" + ex.ToString();
|
||||
}
|
||||
|
||||
CancellationTokenSource autoCancel = new CancellationTokenSource(timeSpan);
|
||||
await CloseOutputAsync(closeStatus, statusDescription, autoCancel.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// do not throw an exception because that will mask the original exception
|
||||
Events.Log.CloseOutputAutoTimeoutCancelled(_guid, (int)timeSpan.TotalSeconds, closeStatus, statusDescription, ex.ToString());
|
||||
}
|
||||
catch (Exception closeException)
|
||||
{
|
||||
// do not throw an exception because that will mask the original exception
|
||||
Events.Log.CloseOutputAutoTimeoutError(_guid, closeException.ToString(), closeStatus, statusDescription, ex.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 087dfa54efe9345b390e0758d42e52cf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,12 +0,0 @@
|
||||
namespace Ninja.WebSockets.Internal
|
||||
{
|
||||
internal enum WebSocketOpCode
|
||||
{
|
||||
ContinuationFrame = 0,
|
||||
TextFrame = 1,
|
||||
BinaryFrame = 2,
|
||||
ConnectionClose = 8,
|
||||
Ping = 9,
|
||||
Pong = 10
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5f2106e8022034b9180513e27b488b4c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,21 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright 2018 David Haig
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
@ -1,7 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 681505da4aa4b48b8b6251521a495d64
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"name": "Ninja.WebSockets"
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f314da276aede4b96a9d5130f4833dd2
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,139 +0,0 @@
|
||||
// ---------------------------------------------------------------------
|
||||
// Copyright 2018 David Haig
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Ninja.WebSockets.Internal;
|
||||
|
||||
namespace Ninja.WebSockets
|
||||
{
|
||||
/// <summary>
|
||||
/// Ping Pong Manager used to facilitate ping pong WebSocket messages
|
||||
/// </summary>
|
||||
public class PingPongManager : IPingPongManager
|
||||
{
|
||||
readonly WebSocketImplementation _webSocket;
|
||||
readonly Guid _guid;
|
||||
readonly TimeSpan _keepAliveInterval;
|
||||
readonly Task _pingTask;
|
||||
readonly CancellationToken _cancellationToken;
|
||||
Stopwatch _stopwatch;
|
||||
long _pingSentTicks;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when a Pong frame is received
|
||||
/// </summary>
|
||||
public event EventHandler<PongEventArgs> Pong;
|
||||
|
||||
/// <summary>
|
||||
/// Initialises a new instance of the PingPongManager to facilitate ping pong WebSocket messages.
|
||||
/// If you are manually creating an instance of this class then it is advisable to set keepAliveInterval to
|
||||
/// TimeSpan.Zero when you create the WebSocket instance (using a factory) otherwise you may be automatically
|
||||
/// be sending duplicate Ping messages (see keepAliveInterval below)
|
||||
/// </summary>
|
||||
/// <param name="webSocket">The web socket used to listen to ping messages and send pong messages</param>
|
||||
/// <param name="keepAliveInterval">The time between automatically sending ping messages.
|
||||
/// Set this to TimeSpan.Zero if you with to manually control sending ping messages.
|
||||
/// </param>
|
||||
/// <param name="cancellationToken">The token used to cancel a pending ping send AND the automatic sending of ping messages
|
||||
/// if keepAliveInterval is positive</param>
|
||||
public PingPongManager(Guid guid, WebSocket webSocket, TimeSpan keepAliveInterval, CancellationToken cancellationToken)
|
||||
{
|
||||
WebSocketImplementation webSocketImpl = webSocket as WebSocketImplementation;
|
||||
_webSocket = webSocketImpl;
|
||||
if (_webSocket == null)
|
||||
throw new InvalidCastException("Cannot cast WebSocket to an instance of WebSocketImplementation. Please use the web socket factories to create a web socket");
|
||||
_guid = guid;
|
||||
_keepAliveInterval = keepAliveInterval;
|
||||
_cancellationToken = cancellationToken;
|
||||
webSocketImpl.Pong += WebSocketImpl_Pong;
|
||||
_stopwatch = Stopwatch.StartNew();
|
||||
|
||||
if (keepAliveInterval != TimeSpan.Zero)
|
||||
{
|
||||
Task.Run(PingForever, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a ping frame
|
||||
/// </summary>
|
||||
/// <param name="payload">The payload (must be 125 bytes of less)</param>
|
||||
/// <param name="cancellation">The cancellation token</param>
|
||||
public async Task SendPing(ArraySegment<byte> payload, CancellationToken cancellation)
|
||||
{
|
||||
await _webSocket.SendPingAsync(payload, cancellation);
|
||||
}
|
||||
|
||||
protected virtual void OnPong(PongEventArgs e)
|
||||
{
|
||||
Pong?.Invoke(this, e);
|
||||
}
|
||||
|
||||
async Task PingForever()
|
||||
{
|
||||
Events.Log.PingPongManagerStarted(_guid, (int)_keepAliveInterval.TotalSeconds);
|
||||
|
||||
try
|
||||
{
|
||||
while (!_cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(_keepAliveInterval, _cancellationToken);
|
||||
|
||||
if (_webSocket.State != WebSocketState.Open)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (_pingSentTicks != 0)
|
||||
{
|
||||
Events.Log.KeepAliveIntervalExpired(_guid, (int)_keepAliveInterval.TotalSeconds);
|
||||
await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, $"No Pong message received in response to a Ping after KeepAliveInterval {_keepAliveInterval}", _cancellationToken);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!_cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
_pingSentTicks = _stopwatch.Elapsed.Ticks;
|
||||
ArraySegment<byte> buffer = new ArraySegment<byte>(BitConverter.GetBytes(_pingSentTicks));
|
||||
await SendPing(buffer, _cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// normal, do nothing
|
||||
}
|
||||
|
||||
Events.Log.PingPongManagerEnded(_guid);
|
||||
}
|
||||
|
||||
void WebSocketImpl_Pong(object sender, PongEventArgs e)
|
||||
{
|
||||
_pingSentTicks = 0;
|
||||
OnPong(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e69d99db8e5024dda9afb8bb5d494260
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,24 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Ninja.WebSockets
|
||||
{
|
||||
/// <summary>
|
||||
/// Pong EventArgs
|
||||
/// </summary>
|
||||
public class PongEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The data extracted from a Pong WebSocket frame
|
||||
/// </summary>
|
||||
public ArraySegment<byte> Payload { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initialises a new instance of the PongEventArgs class
|
||||
/// </summary>
|
||||
/// <param name="payload">The pong payload must be 125 bytes or less (can be zero bytes)</param>
|
||||
public PongEventArgs(ArraySegment<byte> payload)
|
||||
{
|
||||
Payload = payload;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 57068e417e3ea4201a8de864e9a223a5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c1e6c099dd56f4b30b02f4258c38ff05
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 52998228355394f00bf8c88cfad8e362
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<Configuration>ReleaseSigned</Configuration>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<PublishDir>bin\ReleaseSigned\PublishOutput</PublishDir>
|
||||
<Platform>Any CPU</Platform>
|
||||
</PropertyGroup>
|
||||
</Project>
|
@ -1,7 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e1a5037feecd49deb7892a8b0b755c6
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,288 +0,0 @@
|
||||
// ---------------------------------------------------------------------
|
||||
// Copyright 2018 David Haig
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Net.WebSockets;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Ninja.WebSockets.Exceptions;
|
||||
using Ninja.WebSockets.Internal;
|
||||
|
||||
namespace Ninja.WebSockets
|
||||
{
|
||||
/// <summary>
|
||||
/// Web socket client factory used to open web socket client connections
|
||||
/// </summary>
|
||||
public class WebSocketClientFactory : IWebSocketClientFactory
|
||||
{
|
||||
readonly Func<MemoryStream> _bufferFactory;
|
||||
readonly IBufferPool _bufferPool;
|
||||
|
||||
/// <summary>
|
||||
/// Initialises a new instance of the WebSocketClientFactory class without caring about internal buffers
|
||||
/// </summary>
|
||||
public WebSocketClientFactory()
|
||||
{
|
||||
_bufferPool = new BufferPool();
|
||||
_bufferFactory = _bufferPool.GetBuffer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialises a new instance of the WebSocketClientFactory class with control over internal buffer creation
|
||||
/// </summary>
|
||||
/// <param name="bufferFactory">Used to get a memory stream. Feel free to implement your own buffer pool. MemoryStreams will be disposed when no longer needed and can be returned to the pool.</param>
|
||||
public WebSocketClientFactory(Func<MemoryStream> bufferFactory)
|
||||
{
|
||||
_bufferFactory = bufferFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connect with default options
|
||||
/// </summary>
|
||||
/// <param name="uri">The WebSocket uri to connect to (e.g. ws://example.com or wss://example.com for SSL)</param>
|
||||
/// <param name="token">The optional cancellation token</param>
|
||||
/// <returns>A connected web socket instance</returns>
|
||||
public async Task<WebSocket> ConnectAsync(Uri uri, CancellationToken token = default(CancellationToken))
|
||||
{
|
||||
return await ConnectAsync(uri, new WebSocketClientOptions(), token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connect with options specified
|
||||
/// </summary>
|
||||
/// <param name="uri">The WebSocket uri to connect to (e.g. ws://example.com or wss://example.com for SSL)</param>
|
||||
/// <param name="options">The WebSocket client options</param>
|
||||
/// <param name="token">The optional cancellation token</param>
|
||||
/// <returns>A connected web socket instance</returns>
|
||||
public async Task<WebSocket> ConnectAsync(Uri uri, WebSocketClientOptions options, CancellationToken token = default(CancellationToken))
|
||||
{
|
||||
Guid guid = Guid.NewGuid();
|
||||
string host = uri.Host;
|
||||
int port = uri.Port;
|
||||
TcpClient tcpClient = new TcpClient(AddressFamily.InterNetworkV6);
|
||||
tcpClient.NoDelay = options.NoDelay;
|
||||
tcpClient.Client.DualMode = true;
|
||||
string uriScheme = uri.Scheme.ToLower();
|
||||
bool useSsl = uriScheme == "wss" || uriScheme == "https";
|
||||
if (IPAddress.TryParse(host, out IPAddress ipAddress))
|
||||
{
|
||||
Events.Log.ClientConnectingToIpAddress(guid, ipAddress.ToString(), port);
|
||||
await tcpClient.ConnectAsync(ipAddress, port);
|
||||
}
|
||||
else
|
||||
{
|
||||
Events.Log.ClientConnectingToHost(guid, host, port);
|
||||
await tcpClient.ConnectAsync(host, port);
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
Stream stream = GetStream(guid, tcpClient, useSsl, host);
|
||||
return await PerformHandshake(guid, uri, stream, options, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connect with a stream that has already been opened and HTTP websocket upgrade request sent
|
||||
/// This function will check the handshake response from the server and proceed if successful
|
||||
/// Use this function if you have specific requirements to open a conenction like using special http headers and cookies
|
||||
/// You will have to build your own HTTP websocket upgrade request
|
||||
/// You may not even choose to use TCP/IP and this function will allow you to do that
|
||||
/// </summary>
|
||||
/// <param name="responseStream">The full duplex response stream from the server</param>
|
||||
/// <param name="secWebSocketKey">The secWebSocketKey you used in the handshake request</param>
|
||||
/// <param name="options">The WebSocket client options</param>
|
||||
/// <param name="token">The optional cancellation token</param>
|
||||
/// <returns></returns>
|
||||
public async Task<WebSocket> ConnectAsync(Stream responseStream, string secWebSocketKey, WebSocketClientOptions options, CancellationToken token = default(CancellationToken))
|
||||
{
|
||||
Guid guid = Guid.NewGuid();
|
||||
return await ConnectAsync(guid, responseStream, secWebSocketKey, options.KeepAliveInterval, options.SecWebSocketExtensions, options.IncludeExceptionInCloseResponse, token);
|
||||
}
|
||||
|
||||
async Task<WebSocket> ConnectAsync(Guid guid, Stream responseStream, string secWebSocketKey, TimeSpan keepAliveInterval, string secWebSocketExtensions, bool includeExceptionInCloseResponse, CancellationToken token)
|
||||
{
|
||||
Events.Log.ReadingHttpResponse(guid);
|
||||
string response = string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
response = await HttpHelper.ReadHttpHeaderAsync(responseStream, token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Events.Log.ReadHttpResponseError(guid, ex.ToString());
|
||||
throw new WebSocketHandshakeFailedException("Handshake unexpected failure", ex);
|
||||
}
|
||||
|
||||
ThrowIfInvalidResponseCode(response);
|
||||
ThrowIfInvalidAcceptString(guid, response, secWebSocketKey);
|
||||
string subProtocol = GetSubProtocolFromHeader(response);
|
||||
return new WebSocketImplementation(guid, _bufferFactory, responseStream, keepAliveInterval, secWebSocketExtensions, includeExceptionInCloseResponse, true, subProtocol);
|
||||
}
|
||||
|
||||
string GetSubProtocolFromHeader(string response)
|
||||
{
|
||||
// make sure we escape the accept string which could contain special regex characters
|
||||
string regexPattern = "Sec-WebSocket-Protocol: (.*)";
|
||||
Regex regex = new Regex(regexPattern, RegexOptions.IgnoreCase);
|
||||
Match match = regex.Match(response);
|
||||
if (match.Success)
|
||||
{
|
||||
return match.Groups[1].Value.Trim();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
void ThrowIfInvalidAcceptString(Guid guid, string response, string secWebSocketKey)
|
||||
{
|
||||
// make sure we escape the accept string which could contain special regex characters
|
||||
string regexPattern = "Sec-WebSocket-Accept: (.*)";
|
||||
Regex regex = new Regex(regexPattern, RegexOptions.IgnoreCase);
|
||||
string actualAcceptString = regex.Match(response).Groups[1].Value.Trim();
|
||||
|
||||
// check the accept string
|
||||
string expectedAcceptString = HttpHelper.ComputeSocketAcceptString(secWebSocketKey);
|
||||
if (expectedAcceptString != actualAcceptString)
|
||||
{
|
||||
string warning = string.Format($"Handshake failed because the accept string from the server '{expectedAcceptString}' was not the expected string '{actualAcceptString}'");
|
||||
Events.Log.HandshakeFailure(guid, warning);
|
||||
throw new WebSocketHandshakeFailedException(warning);
|
||||
}
|
||||
else
|
||||
{
|
||||
Events.Log.ClientHandshakeSuccess(guid);
|
||||
}
|
||||
}
|
||||
|
||||
void ThrowIfInvalidResponseCode(string responseHeader)
|
||||
{
|
||||
string responseCode = HttpHelper.ReadHttpResponseCode(responseHeader);
|
||||
if (!string.Equals(responseCode, "101 Switching Protocols", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
string[] lines = responseHeader.Split(new string[] { "\r\n" }, StringSplitOptions.None);
|
||||
|
||||
for (int i = 0; i < lines.Length; i++)
|
||||
{
|
||||
// if there is more to the message than just the header
|
||||
if (string.IsNullOrWhiteSpace(lines[i]))
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int j = i + 1; j < lines.Length - 1; j++)
|
||||
{
|
||||
builder.AppendLine(lines[j]);
|
||||
}
|
||||
|
||||
string responseDetails = builder.ToString();
|
||||
throw new InvalidHttpResponseCodeException(responseCode, responseDetails, responseHeader);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Stream GetStream(Guid guid, TcpClient tcpClient, bool isSecure, string host)
|
||||
{
|
||||
Stream stream = tcpClient.GetStream();
|
||||
|
||||
if (isSecure)
|
||||
{
|
||||
SslStream sslStream = new SslStream(stream, false, new RemoteCertificateValidationCallback(ValidateServerCertificate), null);
|
||||
Events.Log.AttemtingToSecureSslConnection(guid);
|
||||
|
||||
// This will throw an AuthenticationException if the certificate is not valid
|
||||
sslStream.AuthenticateAsClient(host);
|
||||
Events.Log.ConnectionSecured(guid);
|
||||
return sslStream;
|
||||
}
|
||||
else
|
||||
{
|
||||
Events.Log.ConnectionNotSecure(guid);
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked by the RemoteCertificateValidationDelegate
|
||||
/// If you want to ignore certificate errors (for debugging) then return true
|
||||
/// </summary>
|
||||
static bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
|
||||
{
|
||||
if (sslPolicyErrors == SslPolicyErrors.None)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Events.Log.SslCertificateError(sslPolicyErrors);
|
||||
|
||||
// Do not allow this client to communicate with unauthenticated servers.
|
||||
return false;
|
||||
}
|
||||
|
||||
static string GetAdditionalHeaders(Dictionary<string, string> additionalHeaders)
|
||||
{
|
||||
if (additionalHeaders == null || additionalHeaders.Count == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
foreach (KeyValuePair<string, string> pair in additionalHeaders)
|
||||
{
|
||||
builder.Append($"{pair.Key}: {pair.Value}\r\n");
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
async Task<WebSocket> PerformHandshake(Guid guid, Uri uri, Stream stream, WebSocketClientOptions options, CancellationToken token)
|
||||
{
|
||||
Random rand = new Random();
|
||||
byte[] keyAsBytes = new byte[16];
|
||||
rand.NextBytes(keyAsBytes);
|
||||
string secWebSocketKey = Convert.ToBase64String(keyAsBytes);
|
||||
string additionalHeaders = GetAdditionalHeaders(options.AdditionalHttpHeaders);
|
||||
string handshakeHttpRequest = $"GET {uri.PathAndQuery} HTTP/1.1\r\n" +
|
||||
$"Host: {uri.Host}:{uri.Port}\r\n" +
|
||||
"Upgrade: websocket\r\n" +
|
||||
"Connection: Upgrade\r\n" +
|
||||
$"Sec-WebSocket-Key: {secWebSocketKey}\r\n" +
|
||||
$"Origin: http://{uri.Host}:{uri.Port}\r\n" +
|
||||
$"Sec-WebSocket-Protocol: {options.SecWebSocketProtocol}\r\n" +
|
||||
additionalHeaders +
|
||||
"Sec-WebSocket-Version: 13\r\n\r\n";
|
||||
|
||||
byte[] httpRequest = Encoding.UTF8.GetBytes(handshakeHttpRequest);
|
||||
stream.Write(httpRequest, 0, httpRequest.Length);
|
||||
Events.Log.HandshakeSent(guid, handshakeHttpRequest);
|
||||
return await ConnectAsync(stream, secWebSocketKey, options, token);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 38af07c5cfa1940f2bd0b981bccea293
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,65 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ninja.WebSockets
|
||||
{
|
||||
/// <summary>
|
||||
/// Client WebSocket init options
|
||||
/// </summary>
|
||||
public class WebSocketClientOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// How often to send ping requests to the Server
|
||||
/// This is done to prevent proxy servers from closing your connection
|
||||
/// The default is TimeSpan.Zero meaning that it is disabled.
|
||||
/// WebSocket servers usually send ping messages so it is not normally necessary for the client to send them (hence the TimeSpan.Zero default)
|
||||
/// You can manually control ping pong messages using the PingPongManager class.
|
||||
/// If you do that it is advisible to set this KeepAliveInterval to zero for the WebSocketClientFactory
|
||||
/// </summary>
|
||||
public TimeSpan KeepAliveInterval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set to true to send a message immediately with the least amount of latency (typical usage for chat)
|
||||
/// This will disable Nagle's algorithm which can cause high tcp latency for small packets sent infrequently
|
||||
/// However, if you are streaming large packets or sending large numbers of small packets frequently it is advisable to set NoDelay to false
|
||||
/// This way data will be bundled into larger packets for better throughput
|
||||
/// </summary>
|
||||
public bool NoDelay { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Add any additional http headers to this dictionary
|
||||
/// </summary>
|
||||
public Dictionary<string, string> AdditionalHttpHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Include the full exception (with stack trace) in the close response
|
||||
/// when an exception is encountered and the WebSocket connection is closed
|
||||
/// The default is false
|
||||
/// </summary>
|
||||
public bool IncludeExceptionInCloseResponse { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// WebSocket Extensions as an HTTP header value
|
||||
/// </summary>
|
||||
public string SecWebSocketExtensions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A comma separated list of sub protocols in preference order (first one being the most preferred)
|
||||
/// The server will return the first supported sub protocol (or none if none are supported)
|
||||
/// Can be null
|
||||
/// </summary>
|
||||
public string SecWebSocketProtocol { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initialises a new instance of the WebSocketClientOptions class
|
||||
/// </summary>
|
||||
public WebSocketClientOptions()
|
||||
{
|
||||
KeepAliveInterval = TimeSpan.Zero;
|
||||
NoDelay = true;
|
||||
AdditionalHttpHeaders = new Dictionary<string, string>();
|
||||
IncludeExceptionInCloseResponse = false;
|
||||
SecWebSocketProtocol = null;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f2911aa44c1154bf88781555542a9de3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,56 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace Ninja.WebSockets
|
||||
{
|
||||
/// <summary>
|
||||
/// The WebSocket HTTP Context used to initiate a WebSocket handshake
|
||||
/// </summary>
|
||||
public class WebSocketHttpContext
|
||||
{
|
||||
/// <summary>
|
||||
/// True if this is a valid WebSocket request
|
||||
/// </summary>
|
||||
public bool IsWebSocketRequest { get; private set; }
|
||||
|
||||
public IList<string> WebSocketRequestedProtocols { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The raw http header extracted from the stream
|
||||
/// </summary>
|
||||
public string HttpHeader { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Path extracted from the http header
|
||||
/// </summary>
|
||||
public string Path { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The stream AFTER the header has already been read
|
||||
/// </summary>
|
||||
public Stream Stream { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The tcp connection we are using
|
||||
/// </summary>
|
||||
public TcpClient Client { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initialises a new instance of the WebSocketHttpContext class
|
||||
/// </summary>
|
||||
/// <param name="isWebSocketRequest">True if this is a valid WebSocket request</param>
|
||||
/// <param name="httpHeader">The raw http header extracted from the stream</param>
|
||||
/// <param name="path">The Path extracted from the http header</param>
|
||||
/// <param name="stream">The stream AFTER the header has already been read</param>
|
||||
public WebSocketHttpContext(bool isWebSocketRequest, IList<string> webSocketRequestedProtocols, string httpHeader, string path, TcpClient client, Stream stream)
|
||||
{
|
||||
IsWebSocketRequest = isWebSocketRequest;
|
||||
WebSocketRequestedProtocols = webSocketRequestedProtocols;
|
||||
HttpHeader = httpHeader;
|
||||
Path = path;
|
||||
Client = client;
|
||||
Stream = stream;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: efbbd1ab9e45e4dd3b88faaca1048c7f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,172 +0,0 @@
|
||||
// ---------------------------------------------------------------------
|
||||
// Copyright 2018 David Haig
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Sockets;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Ninja.WebSockets.Exceptions;
|
||||
using Ninja.WebSockets.Internal;
|
||||
|
||||
namespace Ninja.WebSockets
|
||||
{
|
||||
/// <summary>
|
||||
/// Web socket server factory used to open web socket server connections
|
||||
/// </summary>
|
||||
public class WebSocketServerFactory : IWebSocketServerFactory
|
||||
{
|
||||
readonly Func<MemoryStream> _bufferFactory;
|
||||
readonly IBufferPool _bufferPool;
|
||||
|
||||
/// <summary>
|
||||
/// Initialises a new instance of the WebSocketServerFactory class without caring about internal buffers
|
||||
/// </summary>
|
||||
public WebSocketServerFactory()
|
||||
{
|
||||
_bufferPool = new BufferPool();
|
||||
_bufferFactory = _bufferPool.GetBuffer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialises a new instance of the WebSocketClientFactory class with control over internal buffer creation
|
||||
/// </summary>
|
||||
/// <param name="bufferFactory">Used to get a memory stream. Feel free to implement your own buffer pool. MemoryStreams will be disposed when no longer needed and can be returned to the pool.</param>
|
||||
public WebSocketServerFactory(Func<MemoryStream> bufferFactory)
|
||||
{
|
||||
_bufferFactory = bufferFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a http header information from a stream and decodes the parts relating to the WebSocket protocot upgrade
|
||||
/// </summary>
|
||||
/// <param name="stream">The network stream</param>
|
||||
/// <param name="token">The optional cancellation token</param>
|
||||
/// <returns>Http data read from the stream</returns>
|
||||
public async Task<WebSocketHttpContext> ReadHttpHeaderFromStreamAsync(TcpClient client, Stream stream, CancellationToken token = default(CancellationToken))
|
||||
{
|
||||
string header = await HttpHelper.ReadHttpHeaderAsync(stream, token);
|
||||
string path = HttpHelper.GetPathFromHeader(header);
|
||||
bool isWebSocketRequest = HttpHelper.IsWebSocketUpgradeRequest(header);
|
||||
IList<string> subProtocols = HttpHelper.GetSubProtocols(header);
|
||||
return new WebSocketHttpContext(isWebSocketRequest, subProtocols, header, path, client, stream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Accept web socket with default options
|
||||
/// Call ReadHttpHeaderFromStreamAsync first to get WebSocketHttpContext
|
||||
/// </summary>
|
||||
/// <param name="context">The http context used to initiate this web socket request</param>
|
||||
/// <param name="token">The optional cancellation token</param>
|
||||
/// <returns>A connected web socket</returns>
|
||||
public async Task<WebSocket> AcceptWebSocketAsync(WebSocketHttpContext context, CancellationToken token = default(CancellationToken))
|
||||
{
|
||||
return await AcceptWebSocketAsync(context, new WebSocketServerOptions(), token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Accept web socket with options specified
|
||||
/// Call ReadHttpHeaderFromStreamAsync first to get WebSocketHttpContext
|
||||
/// </summary>
|
||||
/// <param name="context">The http context used to initiate this web socket request</param>
|
||||
/// <param name="options">The web socket options</param>
|
||||
/// <param name="token">The optional cancellation token</param>
|
||||
/// <returns>A connected web socket</returns>
|
||||
public async Task<WebSocket> AcceptWebSocketAsync(WebSocketHttpContext context, WebSocketServerOptions options, CancellationToken token = default(CancellationToken))
|
||||
{
|
||||
Guid guid = Guid.NewGuid();
|
||||
Events.Log.AcceptWebSocketStarted(guid);
|
||||
await PerformHandshakeAsync(guid, context.HttpHeader, options.SubProtocol, context.Stream, token);
|
||||
Events.Log.ServerHandshakeSuccess(guid);
|
||||
string secWebSocketExtensions = null;
|
||||
return new WebSocketImplementation(guid, _bufferFactory, context.Stream, options.KeepAliveInterval, secWebSocketExtensions, options.IncludeExceptionInCloseResponse, false, options.SubProtocol)
|
||||
{
|
||||
Context = context
|
||||
};
|
||||
}
|
||||
|
||||
static void CheckWebSocketVersion(string httpHeader)
|
||||
{
|
||||
Regex webSocketVersionRegex = new Regex("Sec-WebSocket-Version: (.*)", RegexOptions.IgnoreCase);
|
||||
|
||||
// check the version. Support version 13 and above
|
||||
const int WebSocketVersion = 13;
|
||||
Match match = webSocketVersionRegex.Match(httpHeader);
|
||||
if (match.Success)
|
||||
{
|
||||
int secWebSocketVersion = Convert.ToInt32(match.Groups[1].Value.Trim());
|
||||
if (secWebSocketVersion < WebSocketVersion)
|
||||
{
|
||||
throw new WebSocketVersionNotSupportedException(string.Format("WebSocket Version {0} not suported. Must be {1} or above", secWebSocketVersion, WebSocketVersion));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new WebSocketVersionNotSupportedException("Cannot find \"Sec-WebSocket-Version\" in http header");
|
||||
}
|
||||
}
|
||||
|
||||
static async Task PerformHandshakeAsync(Guid guid, String httpHeader, string subProtocol, Stream stream, CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
Regex webSocketKeyRegex = new Regex("Sec-WebSocket-Key: (.*)", RegexOptions.IgnoreCase);
|
||||
CheckWebSocketVersion(httpHeader);
|
||||
|
||||
Match match = webSocketKeyRegex.Match(httpHeader);
|
||||
if (match.Success)
|
||||
{
|
||||
string secWebSocketKey = match.Groups[1].Value.Trim();
|
||||
string setWebSocketAccept = HttpHelper.ComputeSocketAcceptString(secWebSocketKey);
|
||||
string response = ("HTTP/1.1 101 Switching Protocols\r\n"
|
||||
+ "Connection: Upgrade\r\n"
|
||||
+ "Upgrade: websocket\r\n"
|
||||
+ (subProtocol != null ? $"Sec-WebSocket-Protocol: {subProtocol}\r\n" : "")
|
||||
+ $"Sec-WebSocket-Accept: {setWebSocketAccept}");
|
||||
|
||||
Events.Log.SendingHandshakeResponse(guid, response);
|
||||
await HttpHelper.WriteHttpHeaderAsync(response, stream, token);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new SecWebSocketKeyMissingException("Unable to read \"Sec-WebSocket-Key\" from http header");
|
||||
}
|
||||
}
|
||||
catch (WebSocketVersionNotSupportedException ex)
|
||||
{
|
||||
Events.Log.WebSocketVersionNotSupported(guid, ex.ToString());
|
||||
string response = "HTTP/1.1 426 Upgrade Required\r\nSec-WebSocket-Version: 13" + ex.Message;
|
||||
await HttpHelper.WriteHttpHeaderAsync(response, stream, token);
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Events.Log.BadRequest(guid, ex.ToString());
|
||||
await HttpHelper.WriteHttpHeaderAsync("HTTP/1.1 400 Bad Request", stream, token);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c5fcfcd10538542edb4842d81798f4dd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,45 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Ninja.WebSockets
|
||||
{
|
||||
/// <summary>
|
||||
/// Server WebSocket init options
|
||||
/// </summary>
|
||||
public class WebSocketServerOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// How often to send ping requests to the Client
|
||||
/// The default is 60 seconds
|
||||
/// This is done to prevent proxy servers from closing your connection
|
||||
/// A timespan of zero will disable the automatic ping pong mechanism
|
||||
/// You can manually control ping pong messages using the PingPongManager class.
|
||||
/// If you do that it is advisible to set this KeepAliveInterval to zero in the WebSocketServerFactory
|
||||
/// </summary>
|
||||
public TimeSpan KeepAliveInterval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Include the full exception (with stack trace) in the close response
|
||||
/// when an exception is encountered and the WebSocket connection is closed
|
||||
/// The default is false
|
||||
/// </summary>
|
||||
public bool IncludeExceptionInCloseResponse { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the sub protocol to send back to the client in the opening handshake
|
||||
/// Can be null (the most common use case)
|
||||
/// The client can specify multiple preferred protocols in the opening handshake header
|
||||
/// The server should use the first supported one or set this to null if none of the requested sub protocols are supported
|
||||
/// </summary>
|
||||
public string SubProtocol { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initialises a new instance of the WebSocketServerOptions class
|
||||
/// </summary>
|
||||
public WebSocketServerOptions()
|
||||
{
|
||||
KeepAliveInterval = TimeSpan.FromSeconds(60);
|
||||
IncludeExceptionInCloseResponse = false;
|
||||
SubProtocol = null;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7f8d9d315d665461a94bc139e46cbfce
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b64fa4674492a4cf1857ecee9f73fcb1
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,112 +0,0 @@
|
||||
var LibraryWebSockets = {
|
||||
$webSocketInstances: [],
|
||||
|
||||
SocketCreate: function(url, id, onopen, ondata, onclose)
|
||||
{
|
||||
var str = Pointer_stringify(url);
|
||||
|
||||
var socket = new WebSocket(str, "binary");
|
||||
|
||||
socket.binaryType = 'arraybuffer';
|
||||
|
||||
socket.onopen = function(e) {
|
||||
Runtime.dynCall('vi', onopen, [id]);
|
||||
}
|
||||
|
||||
socket.onerror = function(e) {
|
||||
console.log("websocket error " + JSON.stringify(e));
|
||||
}
|
||||
|
||||
socket.onmessage = function (e) {
|
||||
// Todo: handle other data types?
|
||||
if (e.data instanceof Blob)
|
||||
{
|
||||
var reader = new FileReader();
|
||||
reader.addEventListener("loadend", function() {
|
||||
var array = new Uint8Array(reader.result);
|
||||
});
|
||||
reader.readAsArrayBuffer(e.data);
|
||||
}
|
||||
else if (e.data instanceof ArrayBuffer)
|
||||
{
|
||||
var array = new Uint8Array(e.data);
|
||||
var ptr = _malloc(array.length);
|
||||
var dataHeap = new Uint8Array(HEAPU8.buffer, ptr, array.length);
|
||||
dataHeap.set(array);
|
||||
Runtime.dynCall('viii', ondata, [id, ptr, array.length]);
|
||||
_free(ptr);
|
||||
}
|
||||
else if(typeof e.data === "string") {
|
||||
var reader = new FileReader();
|
||||
reader.addEventListener("loadend", function() {
|
||||
var array = new Uint8Array(reader.result);
|
||||
});
|
||||
var blob = new Blob([e.data]);
|
||||
reader.readAsArrayBuffer(blob);
|
||||
}
|
||||
};
|
||||
|
||||
socket.onclose = function (e) {
|
||||
Runtime.dynCall('vi', onclose, [id]);
|
||||
|
||||
if (e.code != 1000)
|
||||
{
|
||||
if (e.reason != null && e.reason.length > 0)
|
||||
socket.error = e.reason;
|
||||
else
|
||||
{
|
||||
switch (e.code)
|
||||
{
|
||||
case 1001:
|
||||
socket.error = "Endpoint going away.";
|
||||
break;
|
||||
case 1002:
|
||||
socket.error = "Protocol error.";
|
||||
break;
|
||||
case 1003:
|
||||
socket.error = "Unsupported message.";
|
||||
break;
|
||||
case 1005:
|
||||
socket.error = "No status.";
|
||||
break;
|
||||
case 1006:
|
||||
socket.error = "Abnormal disconnection.";
|
||||
break;
|
||||
case 1009:
|
||||
socket.error = "Data frame too large.";
|
||||
break;
|
||||
default:
|
||||
socket.error = "Error "+e.code;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var instance = webSocketInstances.push(socket) - 1;
|
||||
return instance;
|
||||
},
|
||||
|
||||
SocketState: function (socketInstance)
|
||||
{
|
||||
var socket = webSocketInstances[socketInstance];
|
||||
|
||||
if(socket)
|
||||
return socket.readyState;
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
SocketSend: function (socketInstance, ptr, length)
|
||||
{
|
||||
var socket = webSocketInstances[socketInstance];
|
||||
socket.send (HEAPU8.buffer.slice(ptr, ptr+length));
|
||||
},
|
||||
|
||||
SocketClose: function (socketInstance)
|
||||
{
|
||||
var socket = webSocketInstances[socketInstance];
|
||||
socket.close();
|
||||
}
|
||||
};
|
||||
|
||||
autoAddDeps(LibraryWebSockets, '$webSocketInstances');
|
||||
mergeInto(LibraryManager.library, LibraryWebSockets);
|
@ -1,34 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3fba16b22ae274c729f6e8f91c425355
|
||||
PluginImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
iconMap: {}
|
||||
executionOrder: {}
|
||||
isPreloaded: 0
|
||||
isOverridable: 0
|
||||
platformData:
|
||||
- first:
|
||||
Any:
|
||||
second:
|
||||
enabled: 0
|
||||
settings: {}
|
||||
- first:
|
||||
Editor: Editor
|
||||
second:
|
||||
enabled: 0
|
||||
settings:
|
||||
DefaultValueInitialized: true
|
||||
- first:
|
||||
Facebook: WebGL
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
- first:
|
||||
WebGL: WebGL
|
||||
second:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,363 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Net.WebSockets;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Ninja.WebSockets;
|
||||
using Ninja.WebSockets.Internal;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.Websocket
|
||||
{
|
||||
public class Server
|
||||
{
|
||||
public event Action<int> Connected;
|
||||
public event Action<int, ArraySegment<byte>> ReceivedData;
|
||||
public event Action<int> Disconnected;
|
||||
public event Action<int, Exception> ReceivedError;
|
||||
|
||||
const int MaxMessageSize = 256 * 1024;
|
||||
|
||||
// listener
|
||||
TcpListener listener;
|
||||
readonly IWebSocketServerFactory webSocketServerFactory = new WebSocketServerFactory();
|
||||
|
||||
CancellationTokenSource cancellation;
|
||||
|
||||
// clients with <connectionId, TcpClient>
|
||||
Dictionary<int, WebSocket> clients = new Dictionary<int, WebSocket>();
|
||||
|
||||
public bool NoDelay = true;
|
||||
|
||||
// connectionId counter
|
||||
// (right now we only use it from one listener thread, but we might have
|
||||
// multiple threads later in case of WebSockets etc.)
|
||||
// -> static so that another server instance doesn't start at 0 again.
|
||||
static int counter = 0;
|
||||
|
||||
// public next id function in case someone needs to reserve an id
|
||||
// (e.g. if hostMode should always have 0 connection and external
|
||||
// connections should start at 1, etc.)
|
||||
public static int NextConnectionId()
|
||||
{
|
||||
int id = Interlocked.Increment(ref counter);
|
||||
|
||||
// it's very unlikely that we reach the uint limit of 2 billion.
|
||||
// even with 1 new connection per second, this would take 68 years.
|
||||
// -> but if it happens, then we should throw an exception because
|
||||
// the caller probably should stop accepting clients.
|
||||
// -> it's hardly worth using 'bool Next(out id)' for that case
|
||||
// because it's just so unlikely.
|
||||
if (id == int.MaxValue)
|
||||
{
|
||||
throw new Exception("connection id limit reached: " + id);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
// check if the server is running
|
||||
public bool Active
|
||||
{
|
||||
get { return listener != null; }
|
||||
}
|
||||
|
||||
public WebSocket GetClient(int connectionId)
|
||||
{
|
||||
// paul: null is evil, throw exception if not found
|
||||
return clients[connectionId];
|
||||
}
|
||||
|
||||
public bool _secure = false;
|
||||
|
||||
public SslConfiguration _sslConfig;
|
||||
|
||||
public class SslConfiguration
|
||||
{
|
||||
public System.Security.Cryptography.X509Certificates.X509Certificate2 Certificate;
|
||||
public bool ClientCertificateRequired;
|
||||
public System.Security.Authentication.SslProtocols EnabledSslProtocols;
|
||||
public bool CheckCertificateRevocation;
|
||||
}
|
||||
|
||||
public async Task Listen(int port)
|
||||
{
|
||||
try
|
||||
{
|
||||
cancellation = new CancellationTokenSource();
|
||||
|
||||
listener = TcpListener.Create(port);
|
||||
listener.Server.NoDelay = NoDelay;
|
||||
listener.Start();
|
||||
Debug.Log($"Websocket server started listening on port {port}");
|
||||
while (true)
|
||||
{
|
||||
TcpClient tcpClient = await listener.AcceptTcpClientAsync();
|
||||
_ = ProcessTcpClient(tcpClient, cancellation.Token);
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// do nothing. This will be thrown if the Listener has been stopped
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ReceivedError?.Invoke(0, ex);
|
||||
}
|
||||
}
|
||||
|
||||
async Task ProcessTcpClient(TcpClient tcpClient, CancellationToken token)
|
||||
{
|
||||
|
||||
try
|
||||
{
|
||||
// this worker thread stays alive until either of the following happens:
|
||||
// Client sends a close conection request OR
|
||||
// An unhandled exception is thrown OR
|
||||
// The server is disposed
|
||||
|
||||
// get a secure or insecure stream
|
||||
Stream stream = tcpClient.GetStream();
|
||||
if (_secure)
|
||||
{
|
||||
SslStream sslStream = new SslStream(stream, false, CertVerificationCallback);
|
||||
sslStream.AuthenticateAsServer(_sslConfig.Certificate, _sslConfig.ClientCertificateRequired, _sslConfig.EnabledSslProtocols, _sslConfig.CheckCertificateRevocation);
|
||||
stream = sslStream;
|
||||
}
|
||||
WebSocketHttpContext context = await webSocketServerFactory.ReadHttpHeaderFromStreamAsync(tcpClient, stream, token);
|
||||
if (context.IsWebSocketRequest)
|
||||
{
|
||||
// Force KeepAliveInterval to Zero, otherwise the transport is unstable and causes random disconnects.
|
||||
WebSocketServerOptions options = new WebSocketServerOptions() { KeepAliveInterval = TimeSpan.Zero, SubProtocol = "binary" };
|
||||
|
||||
WebSocket webSocket = await webSocketServerFactory.AcceptWebSocketAsync(context, options);
|
||||
|
||||
await ReceiveLoopAsync(webSocket, token);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log("Http header contains no web socket upgrade request. Ignoring");
|
||||
}
|
||||
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// do nothing. This will be thrown if the transport is closed
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// do nothing. This will be thrown if the Listener has been stopped
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ReceivedError?.Invoke(0, ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
tcpClient.Client.Close();
|
||||
tcpClient.Close();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ReceivedError?.Invoke(0, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool CertVerificationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
|
||||
{
|
||||
// Much research has been done on this. When this is initiated from a HTTPS/WSS stream,
|
||||
// the certificate is null and the SslPolicyErrors is RemoteCertificateNotAvailable.
|
||||
// Meaning we CAN'T verify this and this is all we can do.
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool enabled;
|
||||
|
||||
async Task ReceiveLoopAsync(WebSocket webSocket, CancellationToken token)
|
||||
{
|
||||
int connectionId = NextConnectionId();
|
||||
clients.Add(connectionId, webSocket);
|
||||
|
||||
byte[] buffer = new byte[MaxMessageSize];
|
||||
|
||||
try
|
||||
{
|
||||
// someone connected, raise event
|
||||
Connected?.Invoke(connectionId);
|
||||
|
||||
while (true)
|
||||
{
|
||||
WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), token);
|
||||
|
||||
if (!enabled)
|
||||
{
|
||||
await WaitForEnabledAsync();
|
||||
}
|
||||
|
||||
if (result.MessageType == WebSocketMessageType.Close)
|
||||
{
|
||||
Debug.Log($"Client initiated close. Status: {result.CloseStatus} Description: {result.CloseStatusDescription}");
|
||||
break;
|
||||
}
|
||||
|
||||
ArraySegment<byte> data = await ReadFrames(connectionId, result, webSocket, buffer, token);
|
||||
|
||||
if (data.Count == 0)
|
||||
break;
|
||||
|
||||
try
|
||||
{
|
||||
// we received some data, raise event
|
||||
ReceivedData?.Invoke(connectionId, data);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ReceivedError?.Invoke(connectionId, exception);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ReceivedError?.Invoke(connectionId, exception);
|
||||
}
|
||||
finally
|
||||
{
|
||||
clients.Remove(connectionId);
|
||||
Disconnected?.Invoke(connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
async Task WaitForEnabledAsync()
|
||||
{
|
||||
while (!enabled)
|
||||
{
|
||||
await Task.Delay(10);
|
||||
}
|
||||
}
|
||||
|
||||
// a message might come splitted in multiple frames
|
||||
// collect all frames
|
||||
async Task<ArraySegment<byte>> ReadFrames(int connectionId, WebSocketReceiveResult result, WebSocket webSocket, byte[] buffer, CancellationToken token)
|
||||
{
|
||||
int count = result.Count;
|
||||
|
||||
while (!result.EndOfMessage)
|
||||
{
|
||||
if (count >= MaxMessageSize)
|
||||
{
|
||||
string closeMessage = string.Format("Maximum message size: {0} bytes.", MaxMessageSize);
|
||||
await webSocket.CloseAsync(WebSocketCloseStatus.MessageTooBig, closeMessage, CancellationToken.None);
|
||||
ReceivedError?.Invoke(connectionId, new WebSocketException(WebSocketError.HeaderError));
|
||||
return new ArraySegment<byte>();
|
||||
}
|
||||
|
||||
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer, count, MaxMessageSize - count), CancellationToken.None);
|
||||
count += result.Count;
|
||||
|
||||
}
|
||||
return new ArraySegment<byte>(buffer, 0, count);
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
// only if started
|
||||
if (!Active)
|
||||
return;
|
||||
|
||||
Debug.Log("Server: stopping...");
|
||||
cancellation.Cancel();
|
||||
|
||||
// stop listening to connections so that no one can connect while we
|
||||
// close the client connections
|
||||
listener.Stop();
|
||||
|
||||
// clear clients list
|
||||
clients.Clear();
|
||||
listener = null;
|
||||
}
|
||||
|
||||
// send message to client using socket connection or throws exception
|
||||
public async void Send(int connectionId, ArraySegment<byte> segment)
|
||||
{
|
||||
// find the connection
|
||||
if (clients.TryGetValue(connectionId, out WebSocket client))
|
||||
{
|
||||
try
|
||||
{
|
||||
await client.SendAsync(segment, WebSocketMessageType.Binary, true, cancellation.Token);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// connection has been closed, swallow exception
|
||||
Disconnect(connectionId);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
if (clients.ContainsKey(connectionId))
|
||||
{
|
||||
// paul: If someone unplugs their internet
|
||||
// we can potentially get hundreds of errors here all at once
|
||||
// because all the WriteAsync wake up at once and throw exceptions
|
||||
|
||||
// by hiding inside this if, I ensure that we only report the first error
|
||||
// all other errors are swallowed.
|
||||
// this prevents a log storm that freezes the server for several seconds
|
||||
ReceivedError?.Invoke(connectionId, exception);
|
||||
}
|
||||
|
||||
Disconnect(connectionId);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ReceivedError?.Invoke(connectionId, new SocketException((int)SocketError.NotConnected));
|
||||
}
|
||||
}
|
||||
|
||||
// get connection info in case it's needed (IP etc.)
|
||||
// (we should never pass the TcpClient to the outside)
|
||||
public string GetClientAddress(int connectionId)
|
||||
{
|
||||
// find the connection
|
||||
if (clients.TryGetValue(connectionId, out WebSocket client))
|
||||
{
|
||||
WebSocketImplementation wsClient = client as WebSocketImplementation;
|
||||
return wsClient.Context.Client.Client.RemoteEndPoint.ToString();
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// disconnect (kick) a client
|
||||
public bool Disconnect(int connectionId)
|
||||
{
|
||||
// find the connection
|
||||
if (clients.TryGetValue(connectionId, out WebSocket client))
|
||||
{
|
||||
clients.Remove(connectionId);
|
||||
// just close it. client thread will take care of the rest.
|
||||
client.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
|
||||
Debug.Log("Server.Disconnect connectionId:" + connectionId);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (Active)
|
||||
{
|
||||
return $"Websocket server {listener.LocalEndpoint}";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b4bf9040513294fa4939bb9f2f0cda4e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,219 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Security.Authentication;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.Websocket
|
||||
{
|
||||
[HelpURL("https://mirror-networking.com/docs/Transports/WebSockets.html")]
|
||||
public class WebsocketTransport : Transport
|
||||
{
|
||||
public const string Scheme = "ws";
|
||||
public const string SecureScheme = "wss";
|
||||
|
||||
protected Client client = new Client();
|
||||
protected Server server = new Server();
|
||||
|
||||
[Header("Transport Settings")]
|
||||
|
||||
[Tooltip("Connection Port.")]
|
||||
public int port = 7778;
|
||||
|
||||
[Tooltip("Nagle Algorithm can be disabled by enabling NoDelay.")]
|
||||
public bool NoDelay = true;
|
||||
|
||||
[Header("Secure Sockets (SSL/WSS).")]
|
||||
|
||||
[Tooltip("Indicates if SSL/WSS protocol will be used with the PFX Certificate file below.")]
|
||||
public bool Secure;
|
||||
|
||||
[Tooltip("Full path and filename to PFX Certificate file generated from web hosting environment.")]
|
||||
public string CertificatePath;
|
||||
|
||||
[Tooltip("Password for PFX Certificate file above.")]
|
||||
public string CertificatePassword;
|
||||
|
||||
[Tooltip("SSL and TLS Protocols")]
|
||||
public SslProtocols EnabledSslProtocols = SslProtocols.Default;
|
||||
|
||||
public WebsocketTransport()
|
||||
{
|
||||
// dispatch the events from the server
|
||||
server.Connected += (connectionId) => OnServerConnected.Invoke(connectionId);
|
||||
server.Disconnected += (connectionId) => OnServerDisconnected.Invoke(connectionId);
|
||||
server.ReceivedData += (connectionId, data) => OnServerDataReceived.Invoke(connectionId, data, Channels.DefaultReliable);
|
||||
server.ReceivedError += (connectionId, error) => OnServerError.Invoke(connectionId, error);
|
||||
|
||||
// dispatch events from the client
|
||||
client.Connected += () => OnClientConnected.Invoke();
|
||||
client.Disconnected += () => OnClientDisconnected.Invoke();
|
||||
client.ReceivedData += (data) => OnClientDataReceived.Invoke(data, Channels.DefaultReliable);
|
||||
client.ReceivedError += (error) => OnClientError.Invoke(error);
|
||||
|
||||
// configure
|
||||
client.NoDelay = NoDelay;
|
||||
server.NoDelay = NoDelay;
|
||||
|
||||
Debug.Log("Websocket transport initialized!");
|
||||
}
|
||||
|
||||
public override bool Available()
|
||||
{
|
||||
// WebSockets should be available on all platforms, including WebGL (automatically) using our included JSLIB code
|
||||
return true;
|
||||
}
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
server.enabled = true;
|
||||
client.enabled = true;
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
server.enabled = false;
|
||||
client.enabled = false;
|
||||
}
|
||||
|
||||
void LateUpdate()
|
||||
{
|
||||
// note: we need to check enabled in case we set it to false
|
||||
// when LateUpdate already started.
|
||||
// (https://github.com/vis2k/Mirror/pull/379)
|
||||
if (!enabled)
|
||||
return;
|
||||
|
||||
// process a maximum amount of client messages per tick
|
||||
// TODO add clientMaxReceivesPerTick same as telepathy
|
||||
while (true)
|
||||
{
|
||||
// stop when there is no more message
|
||||
if (!client.ProcessClientMessage())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Some messages can disable transport
|
||||
// If this is disabled stop processing message in queue
|
||||
if (!enabled)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// client
|
||||
public override bool ClientConnected() => client.IsConnected;
|
||||
|
||||
public override void ClientConnect(string host)
|
||||
{
|
||||
if (Secure)
|
||||
{
|
||||
client.Connect(new Uri($"wss://{host}:{port}"));
|
||||
}
|
||||
else
|
||||
{
|
||||
client.Connect(new Uri($"ws://{host}:{port}"));
|
||||
}
|
||||
}
|
||||
|
||||
public override void ClientConnect(Uri uri)
|
||||
{
|
||||
if (uri.Scheme != Scheme && uri.Scheme != SecureScheme)
|
||||
throw new ArgumentException($"Invalid url {uri}, use {Scheme}://host:port or {SecureScheme}://host:port instead", nameof(uri));
|
||||
|
||||
if (uri.IsDefaultPort)
|
||||
{
|
||||
UriBuilder uriBuilder = new UriBuilder(uri);
|
||||
uriBuilder.Port = port;
|
||||
uri = uriBuilder.Uri;
|
||||
}
|
||||
|
||||
client.Connect(uri);
|
||||
}
|
||||
|
||||
public override bool ClientSend(int channelId, ArraySegment<byte> segment)
|
||||
{
|
||||
client.Send(segment);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void ClientDisconnect() => client.Disconnect();
|
||||
|
||||
public override Uri ServerUri()
|
||||
{
|
||||
UriBuilder builder = new UriBuilder();
|
||||
builder.Scheme = Secure ? SecureScheme : Scheme;
|
||||
builder.Host = Dns.GetHostName();
|
||||
builder.Port = port;
|
||||
return builder.Uri;
|
||||
}
|
||||
|
||||
|
||||
// server
|
||||
public override bool ServerActive() => server.Active;
|
||||
|
||||
public override void ServerStart()
|
||||
{
|
||||
server._secure = Secure;
|
||||
if (Secure)
|
||||
{
|
||||
server._secure = Secure;
|
||||
server._sslConfig = new Server.SslConfiguration
|
||||
{
|
||||
Certificate = new System.Security.Cryptography.X509Certificates.X509Certificate2(CertificatePath, CertificatePassword),
|
||||
ClientCertificateRequired = false,
|
||||
CheckCertificateRevocation = false,
|
||||
EnabledSslProtocols = EnabledSslProtocols
|
||||
};
|
||||
}
|
||||
_ = server.Listen(port);
|
||||
}
|
||||
|
||||
public override bool ServerSend(List<int> connectionIds, int channelId, ArraySegment<byte> segment)
|
||||
{
|
||||
// send to all
|
||||
foreach (int connectionId in connectionIds)
|
||||
server.Send(connectionId, segment);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool ServerDisconnect(int connectionId)
|
||||
{
|
||||
return server.Disconnect(connectionId);
|
||||
}
|
||||
|
||||
public override string ServerGetClientAddress(int connectionId)
|
||||
{
|
||||
return server.GetClientAddress(connectionId);
|
||||
}
|
||||
public override void ServerStop() => server.Stop();
|
||||
|
||||
// common
|
||||
public override void Shutdown()
|
||||
{
|
||||
client.Disconnect();
|
||||
server.Stop();
|
||||
}
|
||||
|
||||
public override int GetMaxPacketSize(int channelId)
|
||||
{
|
||||
// Telepathy's limit is Array.Length, which is int
|
||||
return int.MaxValue;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (client.Connecting || client.IsConnected)
|
||||
{
|
||||
return client.ToString();
|
||||
}
|
||||
if (server.Active)
|
||||
{
|
||||
return server.ToString();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5f039183eda8945448b822a77e2a9d0e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
Loading…
Reference in New Issue
Block a user