using System;
using System.Text;
using System.Security.Cryptography;
using System.Collections.Generic;
using System.Threading;
using System.Linq;
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.RemoveWhiteSpace(), "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 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