From 4867a7cbbf7302845fff031db5eae6fbf93bf26b Mon Sep 17 00:00:00 2001 From: teravus Date: Thu, 7 Feb 2013 10:26:48 -0500 Subject: [PATCH] This is the final commit that enables the Websocket handler --- .../Servers/HttpServer/BaseHttpServer.cs | 12 +- .../HttpServer/WebsocketServerHandler.cs | 1085 +++++++++++++++++ bin/HttpServer_OpenSim.dll | Bin 116224 -> 116224 bytes bin/HttpServer_OpenSim.pdb | Bin 302592 -> 343552 bytes 4 files changed, 1095 insertions(+), 2 deletions(-) create mode 100644 OpenSim/Framework/Servers/HttpServer/WebsocketServerHandler.cs diff --git a/OpenSim/Framework/Servers/HttpServer/BaseHttpServer.cs b/OpenSim/Framework/Servers/HttpServer/BaseHttpServer.cs index dcfe99a070..70c531c105 100644 --- a/OpenSim/Framework/Servers/HttpServer/BaseHttpServer.cs +++ b/OpenSim/Framework/Servers/HttpServer/BaseHttpServer.cs @@ -54,7 +54,15 @@ namespace OpenSim.Framework.Servers.HttpServer private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); private HttpServerLogWriter httpserverlog = new HttpServerLogWriter(); - public delegate void WebSocketRequestDelegate(string servicepath, WebSocketHTTPServerHandler handler); + + /// + /// This is a pending websocket request before it got an sucessful upgrade response. + /// The consumer must call handler.HandshakeAndUpgrade() to signal to the handler to + /// start the connection and optionally provide an origin authentication method. + /// + /// + /// + public delegate void WebSocketRequestDelegate(string servicepath, WebSocketHttpServerHandler handler); /// /// Gets or sets the debug level. @@ -440,7 +448,7 @@ namespace OpenSim.Framework.Servers.HttpServer } if (dWebSocketRequestDelegate != null) { - dWebSocketRequestDelegate(req.Url.AbsolutePath, new WebSocketHTTPServerHandler(req, context, 16384)); + dWebSocketRequestDelegate(req.Url.AbsolutePath, new WebSocketHttpServerHandler(req, context, 8192)); return; } diff --git a/OpenSim/Framework/Servers/HttpServer/WebsocketServerHandler.cs b/OpenSim/Framework/Servers/HttpServer/WebsocketServerHandler.cs new file mode 100644 index 0000000000..cfb1605001 --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/WebsocketServerHandler.cs @@ -0,0 +1,1085 @@ +/* + * 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.Security.Cryptography; +using System.Text; +using HttpServer; + +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 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 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, IHttpClientContext pContext, int bufferlen) + : base(preq.HttpMethod, preq.Url.OriginalString) + { + _request = preq; + _networkContext = pContext.GiveMeTheNetworkStreamIKnowWhatImDoing(); + _clientContext = pContext; + _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(); + } + + /// + /// 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(OSHttpStatusCode.ClientErrorBadRequest, "no upgrade request submitted"); + } + + string connectionheader = _request.Headers["upgrade"]; + if (connectionheader.ToLower() != "websocket") + { + FailUpgrade(OSHttpStatusCode.ClientErrorBadRequest, "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(OSHttpStatusCode.ClientErrorForbidden, "Origin Validation Failed"); + } + } + else + { + acceptKey = GenerateAcceptKey(websocketKey); + string rawaccept = string.Format(HandshakeAcceptText, acceptKey); + SendUpgradeSuccess(rawaccept); + } + } + + /// + /// 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 + { + + // 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()); + } + catch (IOException fail) + { + Close(string.Empty); + } + catch (ObjectDisposedException fail) + { + 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(OSHttpStatusCode 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 > 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 fail) + { + Close(string.Empty); + } + catch (ObjectDisposedException fail) + { + Close(string.Empty); + } + } + + /// + /// Sends a string to the other side + /// + /// the string message that is to be sent + public void SendMessage(string message) + { + 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) + { + 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() + { + WebSocketFrame pingFrame = new WebSocketFrame() { Header = WebsocketFrameHeader.HeaderDefault(), WebSocketPayload = new byte[0] }; + 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 (_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); + } + + 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 = new byte[0]}; + 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 (_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 = new byte[0]}; + 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 + { + + } + + +} diff --git a/bin/HttpServer_OpenSim.dll b/bin/HttpServer_OpenSim.dll index 9cd1e0857b42badaad81e63a72ebb2c26effeb8a..fd7ad742d03a28238308f402e729fdaa007443e2 100755 GIT binary patch delta 4156 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 diff --git a/bin/HttpServer_OpenSim.pdb b/bin/HttpServer_OpenSim.pdb index d20a0c5074c5c0c5d35a409c4667a0e275481e51..f56e891d4c4e1e674ee26e75c59ec4c5bc7ebbe7 100644 GIT binary patch delta 14884 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 x_JFnc+TK(Bl4>fH6vvHTYMP>+M