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