/* * Copyright (c) Contributors, http://opensimulator.org/ * See CONTRIBUTORS.TXT for a full list of copyright holders. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the OpenSimulator Project nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Security.Cryptography; using System.Text; using System.Threading; using OSHttpServer; namespace OpenSim.Framework.Servers.HttpServer { // Sealed class. If you're going to unseal it, implement IDisposable. /// /// This class implements websockets. It grabs the network context from C#Webserver and utilizes it directly as a tcp streaming service /// public sealed class WebSocketHttpServerHandler : BaseRequestHandler { private class WebSocketState { public List ReceivedBytes; public int ExpectedBytes; public WebsocketFrameHeader Header; public bool FrameComplete; public WebSocketFrame ContinuationFrame; } /// /// Binary Data will trigger this event /// public event DataDelegate OnData; /// /// Textual Data will trigger this event /// public event TextDelegate OnText; /// /// A ping request form the other side will trigger this event. /// This class responds to the ping automatically. You shouldn't send a pong. /// it's informational. /// public event PingDelegate OnPing; /// /// This is a response to a ping you sent. /// public event PongDelegate OnPong; /// /// This is a regular HTTP Request... This may be removed in the future. /// // public event RegularHttpRequestDelegate OnRegularHttpRequest; /// /// When the upgrade from a HTTP request to a Websocket is completed, this will be fired /// public event UpgradeCompletedDelegate OnUpgradeCompleted; /// /// If the upgrade failed, this will be fired /// public event UpgradeFailedDelegate OnUpgradeFailed; /// /// When the websocket is closed, this will be fired. /// public event CloseDelegate OnClose; /// /// Set this delegate to allow your module to validate the origin of the /// Websocket request. Primary line of defense against cross site scripting /// public ValidateHandshake HandshakeValidateMethodOverride = null; private ManualResetEvent _receiveDone = new ManualResetEvent(false); private OSHttpRequest _request; private HTTPNetworkContext _networkContext; private IHttpClientContext _clientContext; private int _pingtime = 0; private byte[] _buffer; private int _bufferPosition; private int _bufferLength; private bool _closing; private bool _upgraded; private int _maxPayloadBytes = 41943040; private int _initialMsgTimeout = 0; private int _defaultReadTimeout = 10000; private const string HandshakeAcceptText = "HTTP/1.1 101 Switching Protocols\r\n" + "upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "sec-websocket-accept: {0}\r\n\r\n";// + //"{1}"; private const string HandshakeDeclineText = "HTTP/1.1 {0} {1}\r\n" + "Connection: close\r\n\r\n"; /// /// Mysterious constant defined in RFC6455 to append to the client provided security key /// private const string WebsocketHandshakeAcceptHashConstant = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; public WebSocketHttpServerHandler(OSHttpRequest preq, int bufferlen) : base(preq.HttpMethod, preq.Url.OriginalString) { _request = preq; _networkContext = preq.IHttpClientContext.GiveMeTheNetworkStreamIKnowWhatImDoing(); _networkContext.Stream.ReadTimeout = _defaultReadTimeout; _clientContext = preq.IHttpClientContext; _bufferLength = bufferlen; _buffer = new byte[_bufferLength]; } // Sealed class implments destructor and an internal dispose method. complies with C# unmanaged resource best practices. ~WebSocketHttpServerHandler() { Dispose(); } /// /// Sets the length of the stream buffer /// /// Byte length. public void SetChunksize(int pChunk) { if (!_upgraded) { _buffer = new byte[pChunk]; } else { throw new InvalidOperationException("You must set the chunksize before the connection is upgraded"); } } /// /// This is the famous nagle. /// public bool NoDelay_TCP_Nagle { get { if (_networkContext != null && _networkContext.Socket != null) { return _networkContext.Socket.NoDelay; } else { throw new InvalidOperationException("The socket has been shutdown"); } } set { if (_networkContext != null && _networkContext.Socket != null) _networkContext.Socket.NoDelay = value; else { throw new InvalidOperationException("The socket has been shutdown"); } } } /// /// This triggers the websocket to start the upgrade process... /// This is a Generalized Networking 'common sense' helper method. Some people expect to call Start() instead /// of the more context appropriate HandshakeAndUpgrade() /// public void Start() { HandshakeAndUpgrade(); } /// /// Max Payload Size in bytes. Defaults to 40MB, but could be set upon connection before calling handshake and upgrade. /// public int MaxPayloadSize { get { return _maxPayloadBytes; } set { _maxPayloadBytes = value; } } /// /// Set this to the maximum amount of milliseconds to wait for the first complete message to be sent or received on the websocket after upgrading /// Default, it waits forever. Use this when your Websocket consuming code opens a connection and waits for a message from the other side to avoid a Denial of Service vector. /// public int InitialMsgTimeout { get { return _initialMsgTimeout; } set { _initialMsgTimeout = value; } } /// /// This triggers the websocket start the upgrade process /// public void HandshakeAndUpgrade() { string webOrigin = string.Empty; string websocketKey = string.Empty; string acceptKey = string.Empty; string accepthost = string.Empty; if (!string.IsNullOrEmpty(_request.Headers["origin"])) webOrigin = _request.Headers["origin"]; if (!string.IsNullOrEmpty(_request.Headers["sec-websocket-key"])) websocketKey = _request.Headers["sec-websocket-key"]; if (!string.IsNullOrEmpty(_request.Headers["host"])) accepthost = _request.Headers["host"]; if (string.IsNullOrEmpty(_request.Headers["upgrade"])) { FailUpgrade(HttpStatusCode.BadRequest, "no upgrade request submitted"); } string connectionheader = _request.Headers["upgrade"]; if (connectionheader.ToLower() != "websocket") { FailUpgrade(HttpStatusCode.BadRequest, "no connection upgrade request submitted"); } // If the object consumer provided a method to validate the origin, we should call it and give the client a success or fail. // If not.. we should accept any. The assumption here is that there would be no Websocket handlers registered in baseHTTPServer unless // Something asked for it... if (HandshakeValidateMethodOverride != null) { if (HandshakeValidateMethodOverride(webOrigin, websocketKey, accepthost)) { acceptKey = GenerateAcceptKey(websocketKey); string rawaccept = string.Format(HandshakeAcceptText, acceptKey); SendUpgradeSuccess(rawaccept); } else { FailUpgrade(HttpStatusCode.Forbidden, "Origin Validation Failed"); } } else { acceptKey = GenerateAcceptKey(websocketKey); string rawaccept = string.Format(HandshakeAcceptText, acceptKey); SendUpgradeSuccess(rawaccept); } } public IPEndPoint GetRemoteIPEndpoint() { return _request.RemoteIPEndPoint; } /// /// Generates a handshake response key string based on the client's /// provided key to prove to the client that we're allowing the Websocket /// upgrade of our own free will and we were not coerced into doing it. /// /// Client provided security key /// private static string GenerateAcceptKey(string key) { if (string.IsNullOrEmpty(key)) return string.Empty; string acceptkey = key + WebsocketHandshakeAcceptHashConstant; SHA1 hashobj = SHA1.Create(); string ret = Convert.ToBase64String(hashobj.ComputeHash(Encoding.UTF8.GetBytes(acceptkey))); hashobj.Clear(); return ret; } /// /// Informs the otherside that we accepted their upgrade request /// /// The HTTP 1.1 101 response that says Yay \o/ private void SendUpgradeSuccess(string pHandshakeResponse) { // Create a new websocket state so we can keep track of data in between network reads. WebSocketState socketState = new WebSocketState() { ReceivedBytes = new List(), Header = WebsocketFrameHeader.HeaderDefault(), FrameComplete = true}; byte[] bhandshakeResponse = Encoding.UTF8.GetBytes(pHandshakeResponse); try { if (_initialMsgTimeout > 0) { _receiveDone.Reset(); } // Begin reading the TCP stream before writing the Upgrade success message to the other side of the stream. _networkContext.Stream.BeginRead(_buffer, 0, _bufferLength, OnReceive, socketState); // Write the upgrade handshake success message _networkContext.Stream.Write(bhandshakeResponse, 0, bhandshakeResponse.Length); _networkContext.Stream.Flush(); _upgraded = true; UpgradeCompletedDelegate d = OnUpgradeCompleted; if (d != null) d(this, new UpgradeCompletedEventArgs()); if (_initialMsgTimeout > 0) { if (!_receiveDone.WaitOne(TimeSpan.FromMilliseconds(_initialMsgTimeout))) Close(string.Empty); } } catch (IOException) { Close(string.Empty); } catch (ObjectDisposedException) { Close(string.Empty); } } /// /// The server has decided not to allow the upgrade to a websocket for some reason. The Http 1.1 response that says Nay >:( /// /// HTTP Status reflecting the reason why /// Textual reason for the upgrade fail private void FailUpgrade(HttpStatusCode pCode, string pMessage ) { string handshakeResponse = string.Format(HandshakeDeclineText, (int)pCode, pMessage.Replace("\n", string.Empty).Replace("\r", string.Empty)); byte[] bhandshakeResponse = Encoding.UTF8.GetBytes(handshakeResponse); _networkContext.Stream.Write(bhandshakeResponse, 0, bhandshakeResponse.Length); _networkContext.Stream.Flush(); _networkContext.Stream.Dispose(); UpgradeFailedDelegate d = OnUpgradeFailed; if (d != null) d(this,new UpgradeFailedEventArgs()); } /// /// This is our ugly Async OnReceive event handler. /// This chunks the input stream based on the length of the provided buffer and processes out /// as many frames as it can. It then moves the unprocessed data to the beginning of the buffer. /// /// Our Async State from beginread private void OnReceive(IAsyncResult ar) { WebSocketState _socketState = ar.AsyncState as WebSocketState; try { int bytesRead = _networkContext.Stream.EndRead(ar); if (bytesRead == 0) { // Do Disconnect _networkContext.Stream.Dispose(); _networkContext = null; return; } _bufferPosition += bytesRead; if (_bufferPosition > _bufferLength) { // Message too big for chunksize.. not sure how this happened... //Close(string.Empty); } int offset = 0; bool headerread = true; int headerforwardposition = 0; while (headerread && offset < bytesRead) { if (_socketState.FrameComplete) { WebsocketFrameHeader pheader = WebsocketFrameHeader.ZeroHeader; headerread = WebSocketReader.TryReadHeader(_buffer, offset, _bufferPosition - offset, out pheader, out headerforwardposition); offset += headerforwardposition; if (headerread) { _socketState.FrameComplete = false; if (pheader.PayloadLen > (ulong) _maxPayloadBytes) { Close("Invalid Payload size"); return; } if (pheader.PayloadLen > 0) { if ((int) pheader.PayloadLen > _bufferPosition - offset) { byte[] writebytes = new byte[_bufferPosition - offset]; Buffer.BlockCopy(_buffer, offset, writebytes, 0, (int) _bufferPosition - offset); _socketState.ExpectedBytes = (int) pheader.PayloadLen; _socketState.ReceivedBytes.AddRange(writebytes); _socketState.Header = pheader; // We need to add the header so that we can unmask it offset += (int) _bufferPosition - offset; } else { byte[] writebytes = new byte[pheader.PayloadLen]; Buffer.BlockCopy(_buffer, offset, writebytes, 0, (int) pheader.PayloadLen); WebSocketReader.Mask(pheader.Mask, writebytes); pheader.IsMasked = false; _socketState.FrameComplete = true; _socketState.ReceivedBytes.AddRange(writebytes); _socketState.Header = pheader; offset += (int) pheader.PayloadLen; } } else { pheader.Mask = 0; _socketState.FrameComplete = true; _socketState.Header = pheader; } if (_socketState.FrameComplete) { ProcessFrame(_socketState); _socketState.Header.SetDefault(); _socketState.ReceivedBytes.Clear(); _socketState.ExpectedBytes = 0; } } } else { WebsocketFrameHeader frameHeader = _socketState.Header; int bytesleft = _socketState.ExpectedBytes - _socketState.ReceivedBytes.Count; if (bytesleft > _bufferPosition) { byte[] writebytes = new byte[_bufferPosition]; Buffer.BlockCopy(_buffer, offset, writebytes, 0, (int) _bufferPosition); _socketState.ReceivedBytes.AddRange(writebytes); _socketState.Header = frameHeader; // We need to add the header so that we can unmask it offset += (int) _bufferPosition; } else { byte[] writebytes = new byte[_bufferPosition]; Buffer.BlockCopy(_buffer, offset, writebytes, 0, (int) _bufferPosition); _socketState.FrameComplete = true; _socketState.ReceivedBytes.AddRange(writebytes); _socketState.Header = frameHeader; offset += (int) _bufferPosition; } if (_socketState.FrameComplete) { ProcessFrame(_socketState); _socketState.Header.SetDefault(); _socketState.ReceivedBytes.Clear(); _socketState.ExpectedBytes = 0; // do some processing } } } if (offset > 0) { // If the buffer is maxed out.. we can just move the cursor. Nothing to move to the beginning. if (offset <_buffer.Length) Buffer.BlockCopy(_buffer, offset, _buffer, 0, _bufferPosition - offset); _bufferPosition -= offset; } if (_networkContext.Stream != null && _networkContext.Stream.CanRead && !_closing) { _networkContext.Stream.BeginRead(_buffer, _bufferPosition, _bufferLength - _bufferPosition, OnReceive, _socketState); } else { // We can't read the stream anymore... } } catch (IOException) { Close(string.Empty); } catch (ObjectDisposedException) { Close(string.Empty); } } /// /// Sends a string to the other side /// /// the string message that is to be sent public void SendMessage(string message) { if (_initialMsgTimeout > 0) { _receiveDone.Set(); _initialMsgTimeout = 0; } byte[] messagedata = Encoding.UTF8.GetBytes(message); WebSocketFrame textMessageFrame = new WebSocketFrame() { Header = WebsocketFrameHeader.HeaderDefault(), WebSocketPayload = messagedata }; textMessageFrame.Header.Opcode = WebSocketReader.OpCode.Text; textMessageFrame.Header.IsEnd = true; SendSocket(textMessageFrame.ToBytes()); } public void SendData(byte[] data) { if (_initialMsgTimeout > 0) { _receiveDone.Set(); _initialMsgTimeout = 0; } WebSocketFrame dataMessageFrame = new WebSocketFrame() { Header = WebsocketFrameHeader.HeaderDefault(), WebSocketPayload = data}; dataMessageFrame.Header.IsEnd = true; dataMessageFrame.Header.Opcode = WebSocketReader.OpCode.Binary; SendSocket(dataMessageFrame.ToBytes()); } /// /// Writes raw bytes to the websocket. Unframed data will cause disconnection /// /// private void SendSocket(byte[] data) { if (!_closing) { try { _networkContext.Stream.Write(data, 0, data.Length); } catch (IOException) { } } } /// /// Sends a Ping check to the other side. The other side SHOULD respond as soon as possible with a pong frame. This interleaves with incoming fragmented frames. /// public void SendPingCheck() { if (_initialMsgTimeout > 0) { _receiveDone.Set(); _initialMsgTimeout = 0; } WebSocketFrame pingFrame = new WebSocketFrame() { Header = WebsocketFrameHeader.HeaderDefault(), WebSocketPayload = Array.Empty() }; pingFrame.Header.Opcode = WebSocketReader.OpCode.Ping; pingFrame.Header.IsEnd = true; _pingtime = Util.EnvironmentTickCount(); SendSocket(pingFrame.ToBytes()); } /// /// Closes the websocket connection. Sends a close message to the other side if it hasn't already done so. /// /// public void Close(string message) { if (_initialMsgTimeout > 0) { _receiveDone.Set(); _initialMsgTimeout = 0; } if (_networkContext == null) return; if (_networkContext.Stream != null) { if (_networkContext.Stream.CanWrite) { byte[] messagedata = Encoding.UTF8.GetBytes(message); WebSocketFrame closeResponseFrame = new WebSocketFrame() { Header = WebsocketFrameHeader.HeaderDefault(), WebSocketPayload = messagedata }; closeResponseFrame.Header.Opcode = WebSocketReader.OpCode.Close; closeResponseFrame.Header.PayloadLen = (ulong) messagedata.Length; closeResponseFrame.Header.IsEnd = true; SendSocket(closeResponseFrame.ToBytes()); } } CloseDelegate closeD = OnClose; if (closeD != null) { closeD(this, new CloseEventArgs()); } _closing = true; } /// /// Processes a websocket frame and triggers consumer events /// /// We need to modify the websocket state here depending on the frame private void ProcessFrame(WebSocketState psocketState) { if (psocketState.Header.IsMasked) { byte[] unmask = psocketState.ReceivedBytes.ToArray(); WebSocketReader.Mask(psocketState.Header.Mask, unmask); psocketState.ReceivedBytes = new List(unmask); } if (psocketState.Header.Opcode != WebSocketReader.OpCode.Continue && _initialMsgTimeout > 0) { _receiveDone.Set(); _initialMsgTimeout = 0; } switch (psocketState.Header.Opcode) { case WebSocketReader.OpCode.Ping: PingDelegate pingD = OnPing; if (pingD != null) { pingD(this, new PingEventArgs()); } WebSocketFrame pongFrame = new WebSocketFrame(){Header = WebsocketFrameHeader.HeaderDefault(),WebSocketPayload = Array.Empty()}; pongFrame.Header.Opcode = WebSocketReader.OpCode.Pong; pongFrame.Header.IsEnd = true; SendSocket(pongFrame.ToBytes()); break; case WebSocketReader.OpCode.Pong: PongDelegate pongD = OnPong; if (pongD != null) { pongD(this, new PongEventArgs(){PingResponseMS = Util.EnvironmentTickCountSubtract(Util.EnvironmentTickCount(),_pingtime)}); } break; case WebSocketReader.OpCode.Binary: if (!psocketState.Header.IsEnd) // Not done, so we need to store this and wait for the end frame. { psocketState.ContinuationFrame = new WebSocketFrame { Header = psocketState.Header, WebSocketPayload = psocketState.ReceivedBytes.ToArray() }; } else { // Send Done Event! DataDelegate dataD = OnData; if (dataD != null) { dataD(this,new WebsocketDataEventArgs(){Data = psocketState.ReceivedBytes.ToArray()}); } } break; case WebSocketReader.OpCode.Text: if (!psocketState.Header.IsEnd) // Not done, so we need to store this and wait for the end frame. { psocketState.ContinuationFrame = new WebSocketFrame { Header = psocketState.Header, WebSocketPayload = psocketState.ReceivedBytes.ToArray() }; } else { TextDelegate textD = OnText; if (textD != null) { textD(this, new WebsocketTextEventArgs() { Data = Encoding.UTF8.GetString(psocketState.ReceivedBytes.ToArray()) }); } // Send Done Event! } break; case WebSocketReader.OpCode.Continue: // Continuation. Multiple frames worth of data for one message. Only valid when not using Control Opcodes //Console.WriteLine("currhead " + psocketState.Header.IsEnd); //Console.WriteLine("Continuation! " + psocketState.ContinuationFrame.Header.IsEnd); byte[] combineddata = new byte[psocketState.ReceivedBytes.Count+psocketState.ContinuationFrame.WebSocketPayload.Length]; byte[] newdata = psocketState.ReceivedBytes.ToArray(); Buffer.BlockCopy(psocketState.ContinuationFrame.WebSocketPayload, 0, combineddata, 0, psocketState.ContinuationFrame.WebSocketPayload.Length); Buffer.BlockCopy(newdata, 0, combineddata, psocketState.ContinuationFrame.WebSocketPayload.Length, newdata.Length); psocketState.ContinuationFrame.WebSocketPayload = combineddata; psocketState.Header.PayloadLen = (ulong)combineddata.Length; if (psocketState.Header.IsEnd) { if (psocketState.ContinuationFrame.Header.Opcode == WebSocketReader.OpCode.Text) { // Send Done event TextDelegate textD = OnText; if (textD != null) { textD(this, new WebsocketTextEventArgs() { Data = Encoding.UTF8.GetString(combineddata) }); } } else if (psocketState.ContinuationFrame.Header.Opcode == WebSocketReader.OpCode.Binary) { // Send Done event DataDelegate dataD = OnData; if (dataD != null) { dataD(this, new WebsocketDataEventArgs() { Data = combineddata }); } } else { // protocol violation } psocketState.ContinuationFrame = null; } break; case WebSocketReader.OpCode.Close: Close(string.Empty); break; } psocketState.Header.SetDefault(); psocketState.ReceivedBytes.Clear(); psocketState.ExpectedBytes = 0; } public void Dispose() { if (_initialMsgTimeout > 0) { _receiveDone.Set(); _initialMsgTimeout = 0; } if (_networkContext != null && _networkContext.Stream != null) { if (_networkContext.Stream.CanWrite) _networkContext.Stream.Flush(); _networkContext.Stream.Close(); _networkContext.Stream.Dispose(); _networkContext.Stream = null; } if (_request != null && _request.InputStream != null) { _request.InputStream.Close(); _request.InputStream.Dispose(); _request = null; } if (_clientContext != null) { _clientContext.Close(); _clientContext = null; } } } /// /// Reads a byte stream and returns Websocket frames. /// public class WebSocketReader { /// /// Bit to determine if the frame read on the stream is the last frame in a sequence of fragmented frames /// private const byte EndBit = 0x80; /// /// These are the Frame Opcodes /// public enum OpCode { // Data Opcodes Continue = 0x0, Text = 0x1, Binary = 0x2, // Control flow Opcodes Close = 0x8, Ping = 0x9, Pong = 0xA } /// /// Masks and Unmasks data using the frame mask. Mask is applied per octal /// Note: Frames from clients MUST be masked /// Note: Frames from servers MUST NOT be masked /// /// Int representing 32 bytes of mask data. Mask is applied per octal /// public static void Mask(int pMask, byte[] pBuffer) { byte[] maskKey = BitConverter.GetBytes(pMask); int currentMaskIndex = 0; for (int i = 0; i < pBuffer.Length; i++) { pBuffer[i] = (byte)(pBuffer[i] ^ maskKey[currentMaskIndex]); if (currentMaskIndex == 3) { currentMaskIndex = 0; } else { currentMaskIndex++; } } } /// /// Attempts to read a header off the provided buffer. Returns true, exports a WebSocketFrameheader, /// and an int to move the buffer forward when it reads a header. False when it can't read a header /// /// Bytes read from the stream /// Starting place in the stream to begin trying to read from /// Lenth in the stream to try and read from. Provided for cases where the /// buffer's length is larger then the data in it /// Outputs the read WebSocket frame header /// Informs the calling stream to move the buffer forward /// True if it got a header, False if it didn't get a header public static bool TryReadHeader(byte[] pBuffer, int pOffset, int length, out WebsocketFrameHeader oHeader, out int moveBuffer) { oHeader = WebsocketFrameHeader.ZeroHeader; int minumheadersize = 2; if (length > pBuffer.Length - pOffset) throw new ArgumentOutOfRangeException("The Length specified was larger the byte array supplied"); if (length < minumheadersize) { moveBuffer = 0; return false; } byte nibble1 = (byte)(pBuffer[pOffset] & 0xF0); //FIN/RSV1/RSV2/RSV3 byte nibble2 = (byte)(pBuffer[pOffset] & 0x0F); // Opcode block oHeader = new WebsocketFrameHeader(); oHeader.SetDefault(); if ((nibble1 & WebSocketReader.EndBit) == WebSocketReader.EndBit) { oHeader.IsEnd = true; } else { oHeader.IsEnd = false; } //Opcode oHeader.Opcode = (WebSocketReader.OpCode)nibble2; //Mask oHeader.IsMasked = Convert.ToBoolean((pBuffer[pOffset + 1] & 0x80) >> 7); // Payload length oHeader.PayloadLen = (byte)(pBuffer[pOffset + 1] & 0x7F); int index = 2; // LargerPayload length starts at byte 3 switch (oHeader.PayloadLen) { case 126: minumheadersize += 2; if (length < minumheadersize) { moveBuffer = 0; return false; } Array.Reverse(pBuffer, pOffset + index, 2); // two bytes oHeader.PayloadLen = BitConverter.ToUInt16(pBuffer, pOffset + index); index += 2; break; case 127: // we got more this is a bigger frame // 8 bytes - uint64 - most significant bit 0 network byte order minumheadersize += 8; if (length < minumheadersize) { moveBuffer = 0; return false; } Array.Reverse(pBuffer, pOffset + index, 8); oHeader.PayloadLen = BitConverter.ToUInt64(pBuffer, pOffset + index); index += 8; break; } //oHeader.PayloadLeft = oHeader.PayloadLen; // Start the count in case it's chunked over the network. This is different then frame fragmentation if (oHeader.IsMasked) { minumheadersize += 4; if (length < minumheadersize) { moveBuffer = 0; return false; } oHeader.Mask = BitConverter.ToInt32(pBuffer, pOffset + index); index += 4; } moveBuffer = index; return true; } } /// /// RFC6455 Websocket Frame /// public class WebSocketFrame { /* * RFC6455 nib 0 1 2 3 4 5 6 7 byt 0 1 2 3 dec 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) + |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | + +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | |Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - + : Payload Data continued ... : + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Payload Data continued ... | +---------------------------------------------------------------+ * When reading these, the frames are possibly fragmented and interleaved with control frames * the fragmented frames are not interleaved with data frames. Just control frames */ public static readonly WebSocketFrame DefaultFrame = new WebSocketFrame(){Header = new WebsocketFrameHeader(),WebSocketPayload = Array.Empty()}; public WebsocketFrameHeader Header; public byte[] WebSocketPayload; public byte[] ToBytes() { Header.PayloadLen = (ulong)WebSocketPayload.Length; return Header.ToBytes(WebSocketPayload); } } public struct WebsocketFrameHeader { //public byte CurrentMaskIndex; /// /// The last frame in a sequence of fragmented frames or the one and only frame for this message. /// public bool IsEnd; /// /// Returns whether the payload data is masked or not. Data from Clients MUST be masked, Data from Servers MUST NOT be masked /// public bool IsMasked; /// /// A set of cryptologically sound random bytes XoR-ed against the payload octally. Looped /// public int Mask; /* byt 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +---------------+---------------+---------------+---------------+ | Octal 1 | Octal 2 | Octal 3 | Octal 4 | +---------------+---------------+---------------+---------------+ */ public WebSocketReader.OpCode Opcode; public UInt64 PayloadLen; //public UInt64 PayloadLeft; // Payload is X + Y //public UInt64 ExtensionDataLength; //public UInt64 ApplicationDataLength; public static readonly WebsocketFrameHeader ZeroHeader = WebsocketFrameHeader.HeaderDefault(); public void SetDefault() { //CurrentMaskIndex = 0; IsEnd = true; IsMasked = true; Mask = 0; Opcode = WebSocketReader.OpCode.Close; // PayloadLeft = 0; PayloadLen = 0; // ExtensionDataLength = 0; // ApplicationDataLength = 0; } /// /// Returns a byte array representing the Frame header /// /// This is the frame data payload. The header describes the size of the payload. /// If payload is null, a Zero sized payload is assumed /// Returns a byte array representing the frame header public byte[] ToBytes(byte[] payload) { List result = new List(); // Squeeze in our opcode and our ending bit. result.Add((byte)((byte)Opcode | (IsEnd?0x80:0x00) )); // Again with the three different byte interpretations of size.. //bytesize if (PayloadLen <= 125) { result.Add((byte) PayloadLen); } //Uint16 else if (PayloadLen <= ushort.MaxValue) { result.Add(126); byte[] payloadLengthByte = BitConverter.GetBytes(Convert.ToUInt16(PayloadLen)); Array.Reverse(payloadLengthByte); result.AddRange(payloadLengthByte); } //UInt64 else { result.Add(127); byte[] payloadLengthByte = BitConverter.GetBytes(PayloadLen); Array.Reverse(payloadLengthByte); result.AddRange(payloadLengthByte); } // Only add a payload if it's not null if (payload != null) { result.AddRange(payload); } return result.ToArray(); } /// /// A Helper method to define the defaults /// /// public static WebsocketFrameHeader HeaderDefault() { return new WebsocketFrameHeader { //CurrentMaskIndex = 0, IsEnd = false, IsMasked = true, Mask = 0, Opcode = WebSocketReader.OpCode.Close, //PayloadLeft = 0, PayloadLen = 0, // ExtensionDataLength = 0, // ApplicationDataLength = 0 }; } } public delegate void DataDelegate(object sender, WebsocketDataEventArgs data); public delegate void TextDelegate(object sender, WebsocketTextEventArgs text); public delegate void PingDelegate(object sender, PingEventArgs pingdata); public delegate void PongDelegate(object sender, PongEventArgs pongdata); public delegate void RegularHttpRequestDelegate(object sender, RegularHttpRequestEvnetArgs request); public delegate void UpgradeCompletedDelegate(object sender, UpgradeCompletedEventArgs completeddata); public delegate void UpgradeFailedDelegate(object sender, UpgradeFailedEventArgs faileddata); public delegate void CloseDelegate(object sender, CloseEventArgs closedata); public delegate bool ValidateHandshake(string pWebOrigin, string pWebSocketKey, string pHost); public class WebsocketDataEventArgs : EventArgs { public byte[] Data; } public class WebsocketTextEventArgs : EventArgs { public string Data; } public class PingEventArgs : EventArgs { /// /// The ping event can arbitrarily contain data /// public byte[] Data; } public class PongEventArgs : EventArgs { /// /// The pong event can arbitrarily contain data /// public byte[] Data; public int PingResponseMS; } public class RegularHttpRequestEvnetArgs : EventArgs { } public class UpgradeCompletedEventArgs : EventArgs { } public class UpgradeFailedEventArgs : EventArgs { } public class CloseEventArgs : EventArgs { } }