using System;
using System.Text;
using System.Security.Cryptography;
using System.Collections.Generic;
using System.Threading;
namespace NetCoreServer
{
///
/// WebSocket utility class
///
public class WebSocket : IWebSocket
{
private readonly IWebSocket _wsHandler;
///
/// Initialize a new WebSocket
///
/// WebSocket handler
public WebSocket(IWebSocket wsHandler) { _wsHandler = wsHandler; ClearWsBuffers(); InitWsNonce(); }
///
/// Final frame
///
public const byte WS_FIN = 0x80;
///
/// Text frame
///
public const byte WS_TEXT = 0x01;
///
/// Binary frame
///
public const byte WS_BINARY = 0x02;
///
/// Close frame
///
public const byte WS_CLOSE = 0x08;
///
/// Ping frame
///
public const byte WS_PING = 0x09;
///
/// Pong frame
///
public const byte WS_PONG = 0x0A;
///
/// Perform WebSocket client upgrade
///
/// WebSocket upgrade HTTP response
/// WebSocket client Id
/// 'true' if the WebSocket was successfully upgrade, 'false' if the WebSocket was not upgrade
public bool PerformClientUpgrade(HttpResponse response, Guid id)
{
if (response.Status != 101)
return false;
bool error = false;
bool accept = false;
bool connection = false;
bool upgrade = false;
// Validate WebSocket handshake headers
for (int i = 0; i < response.Headers; i++)
{
var header = response.Header(i);
var key = header.Item1;
var value = header.Item2;
if (string.Compare(key, "Connection", StringComparison.OrdinalIgnoreCase) == 0)
{
if (string.Compare(value, "Upgrade", StringComparison.OrdinalIgnoreCase) != 0)
{
error = true;
_wsHandler.OnWsError("Invalid WebSocket handshaked response: 'Connection' header value must be 'Upgrade'");
break;
}
connection = true;
}
else if (string.Compare(key, "Upgrade", StringComparison.OrdinalIgnoreCase) == 0)
{
if (string.Compare(value, "websocket", StringComparison.OrdinalIgnoreCase) != 0)
{
error = true;
_wsHandler.OnWsError("Invalid WebSocket handshaked response: 'Upgrade' header value must be 'websocket'");
break;
}
upgrade = true;
}
else if (string.Compare(key, "Sec-WebSocket-Accept", StringComparison.OrdinalIgnoreCase) == 0)
{
// Calculate the original WebSocket hash
string wskey = Convert.ToBase64String(WsNonce) + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
string wshash;
using (SHA1 sha1 = SHA1.Create())
{
wshash = Encoding.UTF8.GetString(sha1.ComputeHash(Encoding.UTF8.GetBytes(wskey)));
}
// Get the received WebSocket hash
wskey = Encoding.UTF8.GetString(Convert.FromBase64String(value));
// Compare original and received hashes
if (string.Compare(wskey, wshash, StringComparison.InvariantCulture) != 0)
{
error = true;
_wsHandler.OnWsError("Invalid WebSocket handshaked response: 'Sec-WebSocket-Accept' value validation failed");
break;
}
accept = true;
}
}
// Failed to perform WebSocket handshake
if (!accept || !connection || !upgrade)
{
if (!error)
_wsHandler.OnWsError("Invalid WebSocket response");
return false;
}
// WebSocket successfully handshaked!
WsHandshaked = true;
WsRandom.NextBytes(WsSendMask);
_wsHandler.OnWsConnected(response);
return true;
}
///
/// Perform WebSocket server upgrade
///
/// WebSocket upgrade HTTP request
/// WebSocket upgrade HTTP response
/// 'true' if the WebSocket was successfully upgrade, 'false' if the WebSocket was not upgrade
public bool PerformServerUpgrade(HttpRequest request, HttpResponse response)
{
if (request.Method != "GET")
return false;
bool error = false;
bool connection = false;
bool upgrade = false;
bool wsKey = false;
bool wsVersion = false;
string accept = "";
// Validate WebSocket handshake headers
for (int i = 0; i < request.Headers; i++)
{
var header = request.Header(i);
var key = header.Item1;
var value = header.Item2;
if (string.Compare(key, "Connection", StringComparison.OrdinalIgnoreCase) == 0)
{
if ((string.Compare(value, "Upgrade", StringComparison.OrdinalIgnoreCase) != 0) && (string.Compare(value, "keep-alive, Upgrade", StringComparison.OrdinalIgnoreCase) != 0))
{
error = true;
response.MakeErrorResponse(400, "Invalid WebSocket handshaked request: 'Connection' header value must be 'Upgrade' or 'keep-alive, Upgrade'");
break;
}
connection = true;
}
else if (string.Compare(key, "Upgrade", StringComparison.OrdinalIgnoreCase) == 0)
{
if (string.Compare(value, "websocket", StringComparison.OrdinalIgnoreCase) != 0)
{
error = true;
response.MakeErrorResponse(400, "Invalid WebSocket handshaked request: 'Upgrade' header value must be 'websocket'");
break;
}
upgrade = true;
}
else if (string.Compare(key, "Sec-WebSocket-Key", StringComparison.OrdinalIgnoreCase) == 0)
{
if (string.IsNullOrEmpty(value))
{
error = true;
response.MakeErrorResponse(400, "Invalid WebSocket handshaked request: 'Sec-WebSocket-Key' header value must be non empty");
break;
}
// Calculate the original WebSocket hash
string wskey = value + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
byte[] wshash;
using (SHA1 sha1 = SHA1.Create())
{
wshash = sha1.ComputeHash(Encoding.UTF8.GetBytes(wskey));
}
accept = Convert.ToBase64String(wshash);
wsKey = true;
}
else if (string.Compare(key, "Sec-WebSocket-Version", StringComparison.OrdinalIgnoreCase) == 0)
{
if (string.Compare(value, "13", StringComparison.OrdinalIgnoreCase) != 0)
{
error = true;
response.MakeErrorResponse(400, "Invalid WebSocket handshaked request: 'Sec-WebSocket-Version' header value must be '13'");
break;
}
wsVersion = true;
}
}
// Filter out non WebSocket handshake requests
if (!connection && !upgrade && !wsKey && !wsVersion)
return false;
// Failed to perform WebSocket handshake
if (!connection || !upgrade || !wsKey || !wsVersion)
{
if (!error)
response.MakeErrorResponse(400, "Invalid WebSocket response");
_wsHandler.SendUpgrade(response);
return false;
}
// Prepare WebSocket upgrade success response
response.Clear();
response.SetBegin(101);
response.SetHeader("Connection", "Upgrade");
response.SetHeader("Upgrade", "websocket");
response.SetHeader("Sec-WebSocket-Accept", accept);
response.SetBody();
// Validate WebSocket upgrade request and response
if (!_wsHandler.OnWsConnecting(request, response))
return false;
// Send WebSocket upgrade response
_wsHandler.SendUpgrade(response);
// WebSocket successfully handshaked!
WsHandshaked = true;
Array.Fill(WsSendMask, (byte)0);
_wsHandler.OnWsConnected(request);
return true;
}
///
/// Prepare WebSocket send frame
///
/// WebSocket opcode
/// WebSocket mask
/// Buffer to send as a span of bytes
/// WebSocket status (default is 0)
public void PrepareSendFrame(byte opcode, bool mask, ReadOnlySpan buffer, int status = 0)
{
// Check if we need to store additional 2 bytes of close status frame
bool storeStatus = ((opcode & WS_CLOSE) == WS_CLOSE) && ((buffer.Length > 0) || (status != 0));
long size = storeStatus ? (buffer.Length + 2) : buffer.Length;
// Clear the previous WebSocket send buffer
WsSendBuffer.Clear();
// Append WebSocket frame opcode
WsSendBuffer.Append(opcode);
// Append WebSocket frame size
if (size <= 125)
WsSendBuffer.Append((byte)(((int)size & 0xFF) | (mask ? 0x80 : 0)));
else if (size <= 65535)
{
WsSendBuffer.Append((byte)(126 | (mask ? 0x80 : 0)));
WsSendBuffer.Append((byte)((size >> 8) & 0xFF));
WsSendBuffer.Append((byte)(size & 0xFF));
}
else
{
WsSendBuffer.Append((byte)(127 | (mask ? 0x80 : 0)));
for (int i = 7; i >= 0; i--)
WsSendBuffer.Append((byte)((size >> (8 * i)) & 0xFF));
}
if (mask)
{
// Append WebSocket frame mask
WsSendBuffer.Append(WsSendMask);
}
// Resize WebSocket frame buffer
long offset = WsSendBuffer.Size;
WsSendBuffer.Resize(WsSendBuffer.Size + size);
int index = 0;
// Append WebSocket close status
// RFC 6455: If there is a body, the first two bytes of the body MUST
// be a 2-byte unsigned integer (in network byte order) representing
// a status code with value code.
if (storeStatus)
{
index += 2;
WsSendBuffer.Data[offset + 0] = (byte)(((status >> 8) & 0xFF) ^ WsSendMask[0]);
WsSendBuffer.Data[offset + 1] = (byte)((status & 0xFF) ^ WsSendMask[1]);
}
// Mask WebSocket frame content
for (int i = index; i < size; i++)
WsSendBuffer.Data[offset + i] = (byte)(buffer[i - index] ^ WsSendMask[i % 4]);
}
///
/// Prepare WebSocket send frame
///
/// Buffer to send
/// Buffer offset
/// Buffer size
public void PrepareReceiveFrame(byte[] buffer, long offset, long size)
{
lock (WsReceiveLock)
{
int index = 0;
// Clear received data after WebSocket frame was processed
if (WsFrameReceived)
{
WsFrameReceived = false;
WsHeaderSize = 0;
WsPayloadSize = 0;
WsReceiveFrameBuffer.Clear();
Array.Clear(WsReceiveMask, 0, WsReceiveMask.Length);
}
if (WsFinalReceived)
{
WsFinalReceived = false;
WsReceiveFinalBuffer.Clear();
}
while (size > 0)
{
// Clear received data after WebSocket frame was processed
if (WsFrameReceived)
{
WsFrameReceived = false;
WsHeaderSize = 0;
WsPayloadSize = 0;
WsReceiveFrameBuffer.Clear();
Array.Clear(WsReceiveMask, 0, WsReceiveMask.Length);
}
if (WsFinalReceived)
{
WsFinalReceived = false;
WsReceiveFinalBuffer.Clear();
}
// Prepare WebSocket frame opcode and mask flag
if (WsReceiveFrameBuffer.Size < 2)
{
for (long i = 0; i < 2; i++, index++, size--)
{
if (size == 0)
return;
WsReceiveFrameBuffer.Append(buffer[offset + index]);
}
}
byte opcode = (byte)(WsReceiveFrameBuffer[0] & 0x0F);
bool fin = ((WsReceiveFrameBuffer[0] >> 7) & 0x01) != 0;
bool mask = ((WsReceiveFrameBuffer[1] >> 7) & 0x01) != 0;
long payload = WsReceiveFrameBuffer[1] & (~0x80);
// Prepare WebSocket opcode
WsOpcode = (opcode != 0) ? opcode : WsOpcode;
// Prepare WebSocket frame size
if (payload <= 125)
{
WsHeaderSize = 2 + (mask ? 4 : 0);
WsPayloadSize = payload;
}
else if (payload == 126)
{
if (WsReceiveFrameBuffer.Size < 4)
{
for (long i = 0; i < 2; i++, index++, size--)
{
if (size == 0)
return;
WsReceiveFrameBuffer.Append(buffer[offset + index]);
}
}
payload = ((WsReceiveFrameBuffer[2] << 8) | (WsReceiveFrameBuffer[3] << 0));
WsHeaderSize = 4 + (mask ? 4 : 0);
WsPayloadSize = payload;
}
else if (payload == 127)
{
if (WsReceiveFrameBuffer.Size < 10)
{
for (long i = 0; i < 8; i++, index++, size--)
{
if (size == 0)
return;
WsReceiveFrameBuffer.Append(buffer[offset + index]);
}
}
payload = ((WsReceiveFrameBuffer[2] << 56) | (WsReceiveFrameBuffer[3] << 48) | (WsReceiveFrameBuffer[4] << 40) | (WsReceiveFrameBuffer[5] << 32) | (WsReceiveFrameBuffer[6] << 24) | (WsReceiveFrameBuffer[7] << 16) | (WsReceiveFrameBuffer[8] << 8) | (WsReceiveFrameBuffer[9] << 0));
WsHeaderSize = 10 + (mask ? 4 : 0);
WsPayloadSize = payload;
}
// Prepare WebSocket frame mask
if (mask)
{
if (WsReceiveFrameBuffer.Size < WsHeaderSize)
{
for (long i = 0; i < 4; i++, index++, size--)
{
if (size == 0)
return;
WsReceiveFrameBuffer.Append(buffer[offset + index]);
WsReceiveMask[i] = buffer[offset + index];
}
}
}
long total = WsHeaderSize + WsPayloadSize;
long length = Math.Min(total - WsReceiveFrameBuffer.Size, size);
// Prepare WebSocket frame payload
WsReceiveFrameBuffer.Append(buffer[((int)offset + index)..((int)offset + index + (int)length)]);
index += (int)length;
size -= length;
// Process WebSocket frame
if (WsReceiveFrameBuffer.Size == total)
{
// Unmask WebSocket frame content
if (mask)
{
for (long i = 0; i < WsPayloadSize; i++)
WsReceiveFinalBuffer.Append((byte)(WsReceiveFrameBuffer[WsHeaderSize + i] ^ WsReceiveMask[i % 4]));
}
else
WsReceiveFinalBuffer.Append(WsReceiveFrameBuffer.AsSpan().Slice((int)WsHeaderSize, (int)WsPayloadSize));
WsFrameReceived = true;
// Finalize WebSocket frame
if (fin)
{
WsFinalReceived = true;
switch (WsOpcode)
{
case WS_PING:
{
// Call the WebSocket ping handler
_wsHandler.OnWsPing(WsReceiveFinalBuffer.Data, 0, WsReceiveFinalBuffer.Size);
break;
}
case WS_PONG:
{
// Call the WebSocket pong handler
_wsHandler.OnWsPong(WsReceiveFinalBuffer.Data, 0, WsReceiveFinalBuffer.Size);
break;
}
case WS_CLOSE:
{
int sindex = 0;
int status = 1000;
// Read WebSocket close status
if (WsReceiveFinalBuffer.Size >= 2)
{
sindex += 2;
status = ((WsReceiveFinalBuffer[0] << 8) | (WsReceiveFinalBuffer[1] << 0));
}
// Call the WebSocket close handler
_wsHandler.OnWsClose(WsReceiveFinalBuffer.Data, sindex, WsReceiveFinalBuffer.Size - sindex, status);
break;
}
case WS_BINARY:
case WS_TEXT:
{
// Call the WebSocket received handler
_wsHandler.OnWsReceived(WsReceiveFinalBuffer.Data, 0, WsReceiveFinalBuffer.Size);
break;
}
}
}
}
}
}
}
///
/// Required WebSocket receive frame size
///
public long RequiredReceiveFrameSize()
{
lock (WsReceiveLock)
{
if (WsFrameReceived)
return 0;
// Required WebSocket frame opcode and mask flag
if (WsReceiveFrameBuffer.Size < 2)
return 2 - WsReceiveFrameBuffer.Size;
bool mask = ((WsReceiveFrameBuffer[1] >> 7) & 0x01) != 0;
long payload = WsReceiveFrameBuffer[1] & (~0x80);
// Required WebSocket frame size
if ((payload == 126) && (WsReceiveFrameBuffer.Size < 4))
return 4 - WsReceiveFrameBuffer.Size;
if ((payload == 127) && (WsReceiveFrameBuffer.Size < 10))
return 10 - WsReceiveFrameBuffer.Size;
// Required WebSocket frame mask
if ((mask) && (WsReceiveFrameBuffer.Size < WsHeaderSize))
return WsHeaderSize - WsReceiveFrameBuffer.Size;
// Required WebSocket frame payload
return WsHeaderSize + WsPayloadSize - WsReceiveFrameBuffer.Size;
}
}
///
/// Clear WebSocket send/receive buffers
///
public void ClearWsBuffers()
{
// Clear the receive buffer
bool acquiredReceiveLock = false;
try
{
// Sometimes on disconnect the receive lock could be taken by receive thread.
// In this case we'll skip the receive buffer clearing. It will happen on
// re-connect then or in GC.
Monitor.TryEnter(WsReceiveLock, ref acquiredReceiveLock);
if (acquiredReceiveLock)
{
WsFrameReceived = false;
WsFinalReceived = false;
WsHeaderSize = 0;
WsPayloadSize = 0;
WsReceiveFrameBuffer.Clear();
WsReceiveFinalBuffer.Clear();
Array.Clear(WsReceiveMask, 0, WsReceiveMask.Length);
}
}
finally
{
if (acquiredReceiveLock)
Monitor.Exit(WsReceiveLock);
}
// Clear the send buffer
lock (WsSendLock)
{
WsSendBuffer.Clear();
Array.Clear(WsSendMask, 0, WsSendMask.Length);
}
}
///
/// Initialize WebSocket random nonce
///
public void InitWsNonce() => WsRandom.NextBytes(WsNonce);
///
/// Handshaked flag
///
internal bool WsHandshaked;
///
/// Received frame flag
///
internal bool WsFrameReceived;
///
/// Received final flag
///
internal bool WsFinalReceived;
///
/// Received frame opcode
///
internal byte WsOpcode;
///
/// Received frame header size
///
internal long WsHeaderSize;
///
/// Received frame payload size
///
internal long WsPayloadSize;
///
/// Receive buffer lock
///
internal readonly object WsReceiveLock = new object();
///
/// Receive frame buffer
///
internal readonly Buffer WsReceiveFrameBuffer = new Buffer();
///
/// Receive final buffer
///
internal readonly Buffer WsReceiveFinalBuffer = new Buffer();
///
/// Receive mask
///
internal readonly byte[] WsReceiveMask = new byte[4];
///
/// Send buffer lock
///
internal readonly object WsSendLock = new object();
///
/// Send buffer
///
internal readonly Buffer WsSendBuffer = new Buffer();
///
/// Send mask
///
internal readonly byte[] WsSendMask = new byte[4];
///
/// WebSocket random generator
///
internal readonly Random WsRandom = new Random();
///
/// WebSocket random nonce of 16 bytes
///
internal readonly byte[] WsNonce = new byte[16];
}
}