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:
James Frowen 2020-10-16 16:46:04 +01:00 committed by GitHub
parent a2b64a4d1d
commit f3f0403005
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
83 changed files with 0 additions and 4615 deletions

View File

@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 75b15785adf2d4b7c8d779f5ba6a6326
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 8f69ff0981a33445a89b5e37b245806f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 278e08c90f2324e2e80a0fe8984b0590
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,15 +0,0 @@
{
"name": "Mirror.Websocket",
"references": [
"Mirror",
"Ninja.WebSockets"
],
"optionalUnityReferences": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": []
}

View File

@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 2d3598656ef1d914f91550a81c0e91cb
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 100fd42034e0d46db8980db4cc0cd178
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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);
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: e362254f82b2a4627bfd83394d093715
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: ca217a8fd870444d0ae623d3905d603f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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)
{
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 7ef87d4a0822c4d2da3e8daa392e61d3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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)
{
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 2c2d7303a1a324f0ebe15cb3faf9b0d5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1 +0,0 @@
Make sure that exceptions follow the microsoft standards

View File

@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 3488f8d8c73d64a12bc24930c0210a41
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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)
{
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 66d99fe90f4cb4471bf01c6f391ffc29
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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)
{
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 9b188b3dddee84decadd5913a535746b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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)
{
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 3c3188abcb6c441759011da01fa342f4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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)
{
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 7dd72ca0a28054d258c1d1ad8254a16e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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)
{
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: dc12d4b9cfe854aeeaab3c9f2f3d165e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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);
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 3c90b5378af58402987e8a2938d763f2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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();
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 894bb86ff9cbe4179a6764904f9dd742
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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);
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 1432676d0074e4a7ca123228797fe0ac
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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));
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 290e337adc73544809ef6db4d09ec5bc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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));
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 33e062311740342db82c2f6e53fbce73
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 58fcf4fbb7c9b47eeaf267adb27810d3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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);
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 2379de83fde9446689e7fd94d8cefca1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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);
}
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 7713ae9d113e145389c0a175d58090be
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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;
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: a4e8a4f1bcb0c49d7a82e5f430c85e2d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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]);
}
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: adbf6c11b66794c0494acf9c713a6759
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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;
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 092a4b88aabbc4d3b91f6a82c40b47e3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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);
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: df337f1f7ba5245f7acadbe2e44b86bd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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());
}
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 087dfa54efe9345b390e0758d42e52cf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,12 +0,0 @@
namespace Ninja.WebSockets.Internal
{
internal enum WebSocketOpCode
{
ContinuationFrame = 0,
TextFrame = 1,
BinaryFrame = 2,
ConnectionClose = 8,
Ping = 9,
Pong = 10
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 5f2106e8022034b9180513e27b488b4c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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.

View File

@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 681505da4aa4b48b8b6251521a495d64
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,3 +0,0 @@
{
"name": "Ninja.WebSockets"
}

View File

@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: f314da276aede4b96a9d5130f4833dd2
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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);
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: e69d99db8e5024dda9afb8bb5d494260
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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;
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 57068e417e3ea4201a8de864e9a223a5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: c1e6c099dd56f4b30b02f4258c38ff05
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 52998228355394f00bf8c88cfad8e362
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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>

View File

@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 5e1a5037feecd49deb7892a8b0b755c6
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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);
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 38af07c5cfa1940f2bd0b981bccea293
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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;
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: f2911aa44c1154bf88781555542a9de3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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;
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: efbbd1ab9e45e4dd3b88faaca1048c7f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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;
}
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: c5fcfcd10538542edb4842d81798f4dd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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;
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 7f8d9d315d665461a94bc139e46cbfce
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: b64fa4674492a4cf1857ecee9f73fcb1
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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);

View File

@ -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:

View File

@ -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 "";
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: b4bf9040513294fa4939bb9f2f0cda4e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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 "";
}
}
}

View File

@ -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: