#region License /* * WebSocket.cs * * A C# implementation of the WebSocket interface. * * This code is derived from WebSocket.java * (http://github.com/adamac/Java-WebSocket-client). * * The MIT License * * Copyright (c) 2009 Adam MacBeth * Copyright (c) 2010-2014 sta.blockhead * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #endregion using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.IO; using System.Linq; using System.Net.Sockets; using System.Net.Security; using System.Security.Cryptography; using System.Text; using System.Threading; using WebSocketSharp.Net; using WebSocketSharp.Net.WebSockets; namespace WebSocketSharp { /// /// Implements the WebSocket interface. /// /// /// The WebSocket class provides a set of methods and properties for two-way /// communication using the WebSocket protocol /// (RFC 6455). /// public class WebSocket : IDisposable { #region Private Const Fields private const string _guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; private const string _version = "13"; #endregion #region Private Fields private AuthenticationChallenge _authChallenge; private string _base64key; private RemoteCertificateValidationCallback _certValidationCallback; private bool _client; private Action _closeContext; private CompressionMethod _compression; private WebSocketContext _context; private CookieCollection _cookies; private Func _cookiesValidation; private NetworkCredential _credentials; private string _extensions; private AutoResetEvent _exitReceiving; private object _forClose; private object _forSend; private volatile Logger _logger; private uint _nonceCount; private string _origin; private bool _preAuth; private string _protocol; private string _protocols; private volatile WebSocketState _readyState; private AutoResetEvent _receivePong; private bool _secure; private WsStream _stream; private TcpClient _tcpClient; private Uri _uri; #endregion #region Internal Const Fields internal const int FragmentLength = 1016; // Max value is int.MaxValue - 14. #endregion #region Private Constructors private WebSocket () { _compression = CompressionMethod.NONE; _cookies = new CookieCollection (); _extensions = String.Empty; _forClose = new object (); _forSend = new object (); _protocol = String.Empty; _readyState = WebSocketState.CONNECTING; } #endregion #region Internal Constructors internal WebSocket (HttpListenerWebSocketContext context, Logger logger) : this () { _stream = context.Stream; _closeContext = context.Close; init (context, logger); } internal WebSocket (TcpListenerWebSocketContext context, Logger logger) : this () { _stream = context.Stream; _closeContext = context.Close; init (context, logger); } #endregion #region Public Constructors /// /// Initializes a new instance of the class with the /// specified WebSocket URL and subprotocols. /// /// /// A that contains a WebSocket URL to connect. /// /// /// An array of that contains the WebSocket subprotocols /// if any. /// /// /// is . /// /// /// is invalid. /// public WebSocket (string url, params string[] protocols) : this () { if (url == null) throw new ArgumentNullException ("url"); string msg; if (!url.TryCreateWebSocketUri (out _uri, out msg)) throw new ArgumentException (msg, "url"); _protocols = protocols.ToString (", "); _secure = _uri.Scheme == "wss" ? true : false; _client = true; _base64key = createBase64Key (); _logger = new Logger (); } /// /// Initializes a new instance of the class with the /// specified WebSocket URL, OnOpen, OnMessage, OnError, OnClose event /// handlers and subprotocols. /// /// /// This constructor initializes a new instance of the /// class and establishes a WebSocket connection. /// /// /// A that contains a WebSocket URL to connect. /// /// /// An event handler. /// /// /// An event handler. /// /// /// An event handler. /// /// /// An event handler. /// /// /// An array of that contains the WebSocket subprotocols /// if any. /// /// /// is . /// /// /// is invalid. /// public WebSocket ( string url, EventHandler onOpen, EventHandler onMessage, EventHandler onError, EventHandler onClose, params string [] protocols) : this (url, protocols) { OnOpen = onOpen; OnMessage = onMessage; OnError = onError; OnClose = onClose; Connect (); } #endregion #region Internal Properties internal Func CookiesValidation { get { return _cookiesValidation; } set { _cookiesValidation = value; } } internal bool IsOpened { get { return _readyState == WebSocketState.OPEN || _readyState == WebSocketState.CLOSING; } } #endregion #region Public Properties /// /// Gets or sets the compression method used to compress the payload data of /// the WebSocket Data frame. /// /// /// One of the values that represents the /// compression method to use. /// The default value is . /// public CompressionMethod Compression { get { return _compression; } set { var msg = !_client ? "Set operation of Compression isn't available as a server." : IsOpened ? "A WebSocket connection has already been established." : null; if (msg != null) { _logger.Error (msg); error (msg); return; } _compression = value; } } /// /// Gets the cookies used in the WebSocket connection request. /// /// /// An IEnumerable<Cookie> interface that provides an enumerator which /// supports the iteration over the collection of cookies. /// public IEnumerable Cookies { get { lock (_cookies.SyncRoot) { return from Cookie cookie in _cookies select cookie; } } } /// /// Gets the credentials for HTTP authentication (Basic/Digest). /// /// /// A that represents the credentials for /// HTTP authentication. The default value is . /// public NetworkCredential Credentials { get { return _credentials; } } /// /// Gets the WebSocket extensions selected by the server. /// /// /// A that represents the WebSocket extensions if any. /// The default value is . /// public string Extensions { get { return _extensions; } } /// /// Gets a value indicating whether the WebSocket connection is alive. /// /// /// true if the connection is alive; otherwise, false. /// public bool IsAlive { get { return Ping (); } } /// /// Gets a value indicating whether the WebSocket connection is secure. /// /// /// true if the connection is secure; otherwise, false. /// public bool IsSecure { get { return _secure; } } /// /// Gets the logging functions. /// /// /// The default logging level is the . If you /// change the current logging level, you set the Log.Level property /// to any of the values. /// /// /// A that provides the logging functions. /// public Logger Log { get { return _logger; } internal set { if (value == null) return; _logger = value; } } /// /// Gets or sets the value of the Origin header used in the WebSocket /// connection request. /// /// /// The sends the Origin header if this property has /// any. /// /// /// /// A that represents the value of the /// HTTP Origin /// header to send. The default value is . /// /// /// The Origin header has the following syntax: /// <scheme>://<host>[:<port>] /// /// public string Origin { get { return _origin; } set { string msg = null; if (!_client) msg = "Set operation of Origin isn't available as a server."; else if (IsOpened) msg = "A WebSocket connection has already been established."; else if (value.IsNullOrEmpty ()) { _origin = value; return; } else { Uri origin; if (!Uri.TryCreate (value, UriKind.Absolute, out origin) || origin.Segments.Length > 1) msg = "The syntax of Origin must be '://[:]'."; } if (msg != null) { _logger.Error (msg); error (msg); return; } _origin = value.TrimEnd ('/'); } } /// /// Gets the WebSocket subprotocol selected by the server. /// /// /// A that represents the subprotocol if any. /// The default value is . /// public string Protocol { get { return _protocol; } } /// /// Gets the state of the WebSocket connection. /// /// /// One of the values. /// The default value is . /// public WebSocketState ReadyState { get { return _readyState; } } /// /// Gets or sets the callback used to validate the certificate supplied by /// the server. /// /// /// If the value of this property is , the validation /// does nothing with the server certificate, always returns valid. /// /// /// A delegate that /// references the method(s) used to validate the server certificate. /// The default value is . /// public RemoteCertificateValidationCallback ServerCertificateValidationCallback { get { return _certValidationCallback; } set { var msg = !_client ? "Set operation of ServerCertificateValidationCallback isn't available as a server." : IsOpened ? "A WebSocket connection has already been established." : null; if (msg != null) { _logger.Error (msg); error (msg); return; } _certValidationCallback = value; } } /// /// Gets the WebSocket URL to connect. /// /// /// A that represents the WebSocket URL to connect. /// public Uri Url { get { return _uri; } internal set { _uri = value; } } #endregion #region Public Events /// /// Occurs when the WebSocket connection has been closed. /// public event EventHandler OnClose; /// /// Occurs when the gets an error. /// public event EventHandler OnError; /// /// Occurs when the receives a data frame. /// public event EventHandler OnMessage; /// /// Occurs when the WebSocket connection has been established. /// public event EventHandler OnOpen; #endregion #region Private Methods // As server private bool acceptHandshake () { _logger.Debug (String.Format ( "A WebSocket connection request from {0}:\n{1}", _context.UserEndPoint, _context)); if (!validateConnectionRequest (_context)) { _logger.Error ("An invalid WebSocket connection request."); error ("An error has occurred while handshaking."); Close (HttpStatusCode.BadRequest); return false; } _base64key = _context.SecWebSocketKey; if (_protocol.Length > 0 && !_context.Headers.Contains ("Sec-WebSocket-Protocol", _protocol)) _protocol = String.Empty; var extensions = _context.Headers ["Sec-WebSocket-Extensions"]; if (extensions != null && extensions.Length > 0) processRequestedExtensions (extensions); return send (createHandshakeResponse ()); } private void close (CloseStatusCode code, string reason, bool wait) { close (new PayloadData (((ushort) code).Append (reason)), !code.IsReserved (), wait); } private void close (PayloadData payload, bool send, bool wait) { lock (_forClose) { if (_readyState == WebSocketState.CLOSING || _readyState == WebSocketState.CLOSED) return; _readyState = WebSocketState.CLOSING; } _logger.Trace ("Start closing handshake."); var args = new CloseEventArgs (payload); args.WasClean = _client ? close ( send ? WsFrame.CreateCloseFrame (Mask.MASK, payload).ToByteArray () : null, wait ? 5000 : 0, closeClientResources) : close ( send ? WsFrame.CreateCloseFrame (Mask.UNMASK, payload).ToByteArray () : null, wait ? 1000 : 0, closeServerResources); _readyState = WebSocketState.CLOSED; OnClose.Emit (this, args); _logger.Trace ("End closing handshake."); } private bool close (byte [] frameAsBytes, int timeOut, Func release) { var sent = frameAsBytes != null && _stream.Write (frameAsBytes); var received = timeOut == 0 || (sent && _exitReceiving.WaitOne (timeOut)); var released = release (); var result = sent && received && released; _logger.Debug (String.Format ( "Was clean?: {0}\nsent: {1} received: {2} released: {3}", result, sent, received, released)); return result; } // As client private bool closeClientResources () { try { if (_stream != null) { _stream.Dispose (); _stream = null; } if (_tcpClient != null) { _tcpClient.Close (); _tcpClient = null; } return true; } catch (Exception ex) { _logger.Fatal (ex.ToString ()); error ("An exception has occurred."); return false; } } // As server private bool closeServerResources () { try { if (_closeContext != null) _closeContext (); _stream = null; _context = null; return true; } catch (Exception ex) { _logger.Fatal (ex.ToString ()); error ("An exception has occurred."); return false; } } private bool concatenateFragmentsInto (Stream dest) { while (true) { var frame = _stream.ReadFrame (); // MORE & CONT if (!frame.IsFinal && frame.IsContinuation) { dest.WriteBytes (frame.PayloadData.ApplicationData); continue; } // FINAL & CONT if (frame.IsFinal && frame.IsContinuation) { dest.WriteBytes (frame.PayloadData.ApplicationData); break; } // FINAL & PING if (frame.IsFinal && frame.IsPing) { processPingFrame (frame); continue; } // FINAL & PONG if (frame.IsFinal && frame.IsPong) { processPongFrame (); continue; } // FINAL & CLOSE if (frame.IsFinal && frame.IsClose) return processCloseFrame (frame); // ? return processUnsupportedFrame (frame, CloseStatusCode.INCORRECT_DATA, null); } return true; } private bool connect () { return _client ? doHandshake () : acceptHandshake (); } // As client private static string createBase64Key () { var src = new byte [16]; var rand = new Random (); rand.NextBytes (src); return Convert.ToBase64String (src); } // As client private string createExtensionsRequest () { var extensions = new StringBuilder (64); if (_compression != CompressionMethod.NONE) extensions.Append (_compression.ToCompressionExtension ()); return extensions.Length > 0 ? extensions.ToString () : String.Empty; } // As client private HandshakeRequest createHandshakeRequest () { var path = _uri.PathAndQuery; var host = _uri.Port == 80 ? _uri.DnsSafeHost : _uri.Authority; var req = new HandshakeRequest (path); var headers = req.Headers; headers ["Host"] = host; if (!_origin.IsNullOrEmpty ()) headers ["Origin"] = _origin; headers ["Sec-WebSocket-Key"] = _base64key; if (!_protocols.IsNullOrEmpty ()) headers ["Sec-WebSocket-Protocol"] = _protocols; var extensions = createExtensionsRequest (); if (extensions.Length > 0) headers ["Sec-WebSocket-Extensions"] = extensions; headers ["Sec-WebSocket-Version"] = _version; AuthenticationResponse authRes = null; if (_authChallenge != null && _credentials != null) { authRes = new AuthenticationResponse ( _authChallenge, _credentials, _nonceCount); _nonceCount = authRes.NonceCount; } else if (_preAuth) authRes = new AuthenticationResponse (_credentials); if (authRes != null) headers ["Authorization"] = authRes.ToString (); if (_cookies.Count > 0) req.SetCookies (_cookies); return req; } // As server private HandshakeResponse createHandshakeResponse () { var res = new HandshakeResponse (HttpStatusCode.SwitchingProtocols); var headers = res.Headers; headers ["Sec-WebSocket-Accept"] = createResponseKey (); if (_protocol.Length > 0) headers ["Sec-WebSocket-Protocol"] = _protocol; if (_extensions.Length > 0) headers ["Sec-WebSocket-Extensions"] = _extensions; if (_cookies.Count > 0) res.SetCookies (_cookies); return res; } // As server private HandshakeResponse createHandshakeResponse (HttpStatusCode code) { var res = HandshakeResponse.CreateCloseResponse (code); res.Headers ["Sec-WebSocket-Version"] = _version; return res; } private string createResponseKey () { var buffer = new StringBuilder (_base64key, 64); buffer.Append (_guid); SHA1 sha1 = new SHA1CryptoServiceProvider (); var src = sha1.ComputeHash (Encoding.UTF8.GetBytes (buffer.ToString ())); return Convert.ToBase64String (src); } // As client private bool doHandshake () { setClientStream (); var res = sendHandshakeRequest (); var err = res.IsUnauthorized ? String.Format ("An HTTP {0} authorization is required.", res.AuthChallenge.Scheme) : !validateConnectionResponse (res) ? "An invalid response to this WebSocket connection request." : null; if (err != null) { _logger.Error (err); var msg = "An error has occurred while handshaking."; error (msg); close (CloseStatusCode.ABNORMAL, msg, false); return false; } var protocol = res.Headers ["Sec-WebSocket-Protocol"]; if (!protocol.IsNullOrEmpty ()) _protocol = protocol; processRespondedExtensions (res.Headers ["Sec-WebSocket-Extensions"]); var cookies = res.Cookies; if (cookies.Count > 0) _cookies.SetOrRemove (cookies); return true; } private void error (string message) { OnError.Emit (this, new ErrorEventArgs (message)); } // As server private void init (WebSocketContext context, Logger logger) { _context = context; _logger = logger; _uri = context.RequestUri; _secure = context.IsSecureConnection; } private void open () { _readyState = WebSocketState.OPEN; OnOpen.Emit (this, EventArgs.Empty); startReceiving (); } private bool processCloseFrame (WsFrame frame) { var payload = frame.PayloadData; close (payload, !payload.ContainsReservedCloseStatusCode, false); return false; } private bool processDataFrame (WsFrame frame) { var args = frame.IsCompressed ? new MessageEventArgs ( frame.Opcode, frame.PayloadData.ApplicationData.Decompress (_compression)) : new MessageEventArgs (frame.Opcode, frame.PayloadData); OnMessage.Emit (this, args); return true; } private void processException (Exception exception, string reason) { var code = CloseStatusCode.ABNORMAL; var msg = reason; if (exception.GetType () == typeof (WebSocketException)) { var wsex = (WebSocketException) exception; code = wsex.Code; reason = wsex.Message; } if (code == CloseStatusCode.ABNORMAL || code == CloseStatusCode.TLS_HANDSHAKE_FAILURE) { _logger.Fatal (exception.ToString ()); reason = msg; } else { _logger.Error (reason); msg = null; } error (msg ?? code.GetMessage ()); if (_readyState == WebSocketState.CONNECTING && !_client) Close (HttpStatusCode.BadRequest); else close (code, reason ?? code.GetMessage (), false); } private bool processFragmentedFrame (WsFrame frame) { return frame.IsContinuation // Not first fragment ? true : processFragments (frame); } private bool processFragments (WsFrame first) { using (var concatenated = new MemoryStream ()) { concatenated.WriteBytes (first.PayloadData.ApplicationData); if (!concatenateFragmentsInto (concatenated)) return false; byte [] data; if (_compression != CompressionMethod.NONE) { data = concatenated.DecompressToArray (_compression); } else { concatenated.Close (); data = concatenated.ToArray (); } OnMessage.Emit (this, new MessageEventArgs (first.Opcode, data)); return true; } } private bool processFrame (WsFrame frame) { return frame.IsCompressed && _compression == CompressionMethod.NONE ? processUnsupportedFrame ( frame, CloseStatusCode.INCORRECT_DATA, "A compressed data has been received without available decompression method.") : frame.IsFragmented ? processFragmentedFrame (frame) : frame.IsData ? processDataFrame (frame) : frame.IsPing ? processPingFrame (frame) : frame.IsPong ? processPongFrame () : frame.IsClose ? processCloseFrame (frame) : processUnsupportedFrame (frame, CloseStatusCode.POLICY_VIOLATION, null); } private bool processPingFrame (WsFrame frame) { if (send (WsFrame.CreatePongFrame (_client ? Mask.MASK : Mask.UNMASK, frame.PayloadData))) _logger.Trace ("Returned Pong."); return true; } private bool processPongFrame () { _receivePong.Set (); _logger.Trace ("Received Pong."); return true; } // As server private void processRequestedExtensions (string extensions) { var comp = false; var buffer = new List (); foreach (var e in extensions.SplitHeaderValue (',')) { var extension = e.Trim (); var tmp = extension.RemovePrefix ("x-webkit-"); if (!comp && tmp.IsCompressionExtension ()) { var method = tmp.ToCompressionMethod (); if (method != CompressionMethod.NONE) { _compression = method; comp = true; buffer.Add (extension); } } } if (buffer.Count > 0) _extensions = buffer.ToArray ().ToString (", "); } // As client private void processRespondedExtensions (string extensions) { var comp = _compression != CompressionMethod.NONE ? true : false; var hasComp = false; if (extensions != null && extensions.Length > 0) { foreach (var e in extensions.SplitHeaderValue (',')) { var extension = e.Trim (); if (comp && !hasComp && extension.Equals (_compression)) hasComp = true; } _extensions = extensions; } if (comp && !hasComp) _compression = CompressionMethod.NONE; } private bool processUnsupportedFrame (WsFrame frame, CloseStatusCode code, string reason) { _logger.Debug ("Unsupported frame:\n" + frame.PrintToString (false)); processException (new WebSocketException (code, reason), null); return false; } // As client private HandshakeResponse receiveHandshakeResponse () { var res = HandshakeResponse.Parse (_stream.ReadHandshake ()); _logger.Debug ("A response to this WebSocket connection request:\n" + res.ToString ()); return res; } private bool send (byte [] frameAsBytes) { if (_readyState != WebSocketState.OPEN) { var msg = "A WebSocket connection isn't established or has been closed."; _logger.Error (msg); error (msg); return false; } return _stream.Write (frameAsBytes); } // As client private void send (HandshakeRequest request) { _logger.Debug (String.Format ( "A WebSocket connection request to {0}:\n{1}", _uri, request)); _stream.WriteHandshake (request); } // As server private bool send (HandshakeResponse response) { _logger.Debug ("A response to a WebSocket connection request:\n" + response.ToString ()); return _stream.WriteHandshake (response); } private bool send (WsFrame frame) { if (_readyState != WebSocketState.OPEN) { var msg = "A WebSocket connection isn't established or has been closed."; _logger.Error (msg); error (msg); return false; } return _stream.Write (frame.ToByteArray ()); } private bool send (Opcode opcode, byte [] data) { lock (_forSend) { var sent = false; try { var compressed = false; if (_compression != CompressionMethod.NONE) { data = data.Compress (_compression); compressed = true; } sent = send (WsFrame.CreateFrame ( Fin.FINAL, opcode, _client ? Mask.MASK : Mask.UNMASK, data, compressed)); } catch (Exception ex) { _logger.Fatal (ex.ToString ()); error ("An exception has occurred."); } return sent; } } private bool send (Opcode opcode, Stream stream) { lock (_forSend) { var sent = false; var src = stream; var compressed = false; try { if (_compression != CompressionMethod.NONE) { stream = stream.Compress (_compression); compressed = true; } sent = sendFragmented (opcode, stream, _client ? Mask.MASK : Mask.UNMASK, compressed); } catch (Exception ex) { _logger.Fatal (ex.ToString ()); error ("An exception has occurred."); } finally { if (compressed) stream.Dispose (); src.Dispose (); } return sent; } } private void send (Opcode opcode, byte [] data, Action completed) { Func sender = send; AsyncCallback callback = ar => { try { var sent = sender.EndInvoke (ar); if (completed != null) completed (sent); } catch (Exception ex) { _logger.Fatal (ex.ToString ()); error ("An exception has occurred."); } }; sender.BeginInvoke (opcode, data, callback, null); } private void send (Opcode opcode, Stream stream, Action completed) { Func sender = send; AsyncCallback callback = ar => { try { var sent = sender.EndInvoke (ar); if (completed != null) completed (sent); } catch (Exception ex) { _logger.Fatal (ex.ToString ()); error ("An exception has occurred."); } }; sender.BeginInvoke (opcode, stream, callback, null); } private bool sendFragmented (Opcode opcode, Stream stream, Mask mask, bool compressed) { var len = stream.Length; if (sendFragmented (opcode, stream, len, mask, compressed) == len) return true; var msg = "Sending fragmented data is interrupted."; _logger.Error (msg); error (msg); close (CloseStatusCode.ABNORMAL, msg, false); return false; } private long sendFragmented ( Opcode opcode, Stream stream, long length, Mask mask, bool compressed) { var quo = length / FragmentLength; var rem = (int) (length % FragmentLength); var count = rem == 0 ? quo - 2 : quo - 1; long sentLen = 0; int readLen = 0; byte [] buffer = null; // Not fragment if (quo == 0) { buffer = new byte [rem]; readLen = stream.Read (buffer, 0, rem); if (readLen == rem && send (WsFrame.CreateFrame (Fin.FINAL, opcode, mask, buffer, compressed))) sentLen = readLen; return sentLen; } buffer = new byte [FragmentLength]; // First readLen = stream.Read (buffer, 0, FragmentLength); if (readLen == FragmentLength && send (WsFrame.CreateFrame (Fin.MORE, opcode, mask, buffer, compressed))) sentLen = readLen; else return sentLen; // Mid for (long i = 0; i < count; i++) { readLen = stream.Read (buffer, 0, FragmentLength); if (readLen == FragmentLength && send (WsFrame.CreateFrame (Fin.MORE, Opcode.CONT, mask, buffer, compressed))) sentLen += readLen; else return sentLen; } // Final var tmpLen = FragmentLength; if (rem != 0) buffer = new byte [tmpLen = rem]; readLen = stream.Read (buffer, 0, tmpLen); if (readLen == tmpLen && send (WsFrame.CreateFrame (Fin.FINAL, Opcode.CONT, mask, buffer, compressed))) sentLen += readLen; return sentLen; } // As client private HandshakeResponse sendHandshakeRequest () { var req = createHandshakeRequest (); var res = sendHandshakeRequest (req); if (res.IsUnauthorized) { _authChallenge = res.AuthChallenge; if (_credentials != null && (!_preAuth || _authChallenge.Scheme == "digest")) { if (res.Headers.Contains ("Connection", "close")) { closeClientResources (); setClientStream (); } var authRes = new AuthenticationResponse ( _authChallenge, _credentials, _nonceCount); _nonceCount = authRes.NonceCount; req.Headers ["Authorization"] = authRes.ToString (); res = sendHandshakeRequest (req); } } return res; } // As client private HandshakeResponse sendHandshakeRequest (HandshakeRequest request) { send (request); return receiveHandshakeResponse (); } // As client private void setClientStream () { var host = _uri.DnsSafeHost; var port = _uri.Port; _tcpClient = new TcpClient (host, port); _stream = WsStream.CreateClientStream (_tcpClient, _secure, host, _certValidationCallback); } private void startReceiving () { _exitReceiving = new AutoResetEvent (false); _receivePong = new AutoResetEvent (false); Action receive = null; receive = () => _stream.ReadFrameAsync ( frame => { if (processFrame (frame)) receive (); else _exitReceiving.Set (); }, ex => processException ( ex, "An exception has occurred while receiving a message.")); receive (); } // As server private bool validateConnectionRequest (WebSocketContext context) { string version; return context.IsWebSocketRequest && validateHostHeader (context.Host) && !context.SecWebSocketKey.IsNullOrEmpty () && ((version = context.SecWebSocketVersion) != null && version == _version) && validateCookies (context.CookieCollection, _cookies); } // As client private bool validateConnectionResponse (HandshakeResponse response) { string accept, version; return response.IsWebSocketResponse && ((accept = response.Headers ["Sec-WebSocket-Accept"]) != null && accept == createResponseKey ()) && ((version = response.Headers ["Sec-WebSocket-Version"]) == null || version == _version); } // As server private bool validateCookies (CookieCollection request, CookieCollection response) { return _cookiesValidation != null ? _cookiesValidation (request, response) : true; } // As server private bool validateHostHeader (string value) { if (value == null || value.Length == 0) return false; if (!_uri.IsAbsoluteUri) return true; var i = value.IndexOf (':'); var host = i > 0 ? value.Substring (0, i) : value; var type = Uri.CheckHostName (host); return type != UriHostNameType.Dns || Uri.CheckHostName (_uri.DnsSafeHost) != UriHostNameType.Dns || host == _uri.DnsSafeHost; } #endregion #region Internal Methods // As server internal void Close (HandshakeResponse response) { _readyState = WebSocketState.CLOSING; send (response); closeServerResources (); _readyState = WebSocketState.CLOSED; } // As server internal void Close (HttpStatusCode code) { Close (createHandshakeResponse (code)); } // As server internal void Close (CloseEventArgs args, byte [] frameAsBytes, int waitTimeOut) { lock (_forClose) { if (_readyState == WebSocketState.CLOSING || _readyState == WebSocketState.CLOSED) return; _readyState = WebSocketState.CLOSING; } args.WasClean = close (frameAsBytes, waitTimeOut, closeServerResources); _readyState = WebSocketState.CLOSED; OnClose.Emit (this, args); } internal bool Ping (byte [] frameAsBytes, int timeOut) { return send (frameAsBytes) && _receivePong.WaitOne (timeOut); } // As server, used to broadcast internal void Send (Opcode opcode, byte [] data, Dictionary cache) { lock (_forSend) { try { byte [] cached; if (!cache.TryGetValue (_compression, out cached)) { cached = WsFrame.CreateFrame ( Fin.FINAL, opcode, Mask.UNMASK, data.Compress (_compression), _compression != CompressionMethod.NONE).ToByteArray (); cache.Add (_compression, cached); } send (cached); } catch (Exception ex) { _logger.Fatal (ex.ToString ()); error ("An exception has occurred."); } } } // As server, used to broadcast internal void Send (Opcode opcode, Stream stream, Dictionary cache) { lock (_forSend) { try { Stream cached; if (!cache.TryGetValue (_compression, out cached)) { cached = stream.Compress (_compression); cache.Add (_compression, cached); } else cached.Position = 0; sendFragmented (opcode, cached, Mask.UNMASK, _compression != CompressionMethod.NONE); } catch (Exception ex) { _logger.Fatal (ex.ToString ()); error ("An exception has occurred."); } } } #endregion #region Public Methods /// /// Closes the WebSocket connection and releases all associated resources. /// public void Close () { close (new PayloadData (), _readyState == WebSocketState.OPEN, true); } /// /// Closes the WebSocket connection with the specified , /// and releases all associated resources. /// /// /// This method emits a event if is not /// in the allowable range of the WebSocket close status code. /// /// /// A that indicates the status code for closure. /// public void Close (ushort code) { var msg = code.CheckIfValidCloseStatusCode (); if (msg != null) { _logger.Error (String.Format ("{0}\ncode: {1}", msg, code)); error (msg); return; } var send = _readyState == WebSocketState.OPEN && !code.IsReserved (); close (new PayloadData (code.ToByteArrayInternally (ByteOrder.BIG)), send, true); } /// /// Closes the WebSocket connection with the specified , /// and releases all associated resources. /// /// /// One of the values that indicate the status codes for closure. /// public void Close (CloseStatusCode code) { var send = _readyState == WebSocketState.OPEN && !code.IsReserved (); close (new PayloadData (((ushort) code).ToByteArrayInternally (ByteOrder.BIG)), send, true); } /// /// Closes the WebSocket connection with the specified and , /// and releases all associated resources. /// /// /// This method emits a event if is not /// in the allowable range of the WebSocket close status code /// or the length of is greater than 123 bytes. /// /// /// A that indicates the status code for closure. /// /// /// A that contains the reason for closure. /// public void Close (ushort code, string reason) { byte [] data = null; var msg = code.CheckIfValidCloseStatusCode () ?? (data = code.Append (reason)).CheckIfValidCloseData (); if (msg != null) { _logger.Error (String.Format ("{0}\ncode: {1}\nreason: {2}", msg, code, reason)); error (msg); return; } var send = _readyState == WebSocketState.OPEN && !code.IsReserved (); close (new PayloadData (data), send, true); } /// /// Closes the WebSocket connection with the specified and /// , and releases all associated resources. /// /// /// This method emits a event if the length of /// is greater than 123 bytes. /// /// /// One of the values that indicate the status codes for closure. /// /// /// A that contains the reason for closure. /// public void Close (CloseStatusCode code, string reason) { var data = ((ushort) code).Append (reason); var msg = data.CheckIfValidCloseData (); if (msg != null) { _logger.Error (String.Format ("{0}\nreason: {1}", msg, reason)); error (msg); return; } var send = _readyState == WebSocketState.OPEN && !code.IsReserved (); close (new PayloadData (data), send, true); } /// /// Establishes a WebSocket connection. /// public void Connect () { if (IsOpened) { var msg = "A WebSocket connection has already been established."; _logger.Error (msg); error (msg); return; } try { if (connect ()) open (); } catch (Exception ex) { processException (ex, "An exception has occurred while connecting or opening."); } } /// /// Closes the WebSocket connection and releases all associated resources. /// /// /// This method closes the WebSocket connection with the . /// public void Dispose () { Close (CloseStatusCode.AWAY); } /// /// Sends a Ping using the WebSocket connection. /// /// /// true if the instance receives a Pong in a time; /// otherwise, false. /// public bool Ping () { return _client ? Ping (WsFrame.CreatePingFrame (Mask.MASK).ToByteArray (), 5000) : Ping (WsFrame.EmptyUnmaskPingData, 1000); } /// /// Sends a Ping with the specified using the WebSocket connection. /// /// /// A that contains a message to send. /// /// /// true if the instance receives a Pong in a time; /// otherwise, false. /// public bool Ping (string message) { if (message == null || message.Length == 0) return Ping (); var data = Encoding.UTF8.GetBytes (message); var msg = data.CheckIfValidPingData (); if (msg != null) { _logger.Error (msg); error (msg); return false; } return _client ? Ping (WsFrame.CreatePingFrame (Mask.MASK, data).ToByteArray (), 5000) : Ping (WsFrame.CreatePingFrame (Mask.UNMASK, data).ToByteArray (), 1000); } /// /// Sends a binary using the WebSocket connection. /// /// /// This method does not wait for the send to be complete. /// /// /// An array of that contains a binary data to send. /// public void Send (byte[] data) { Send (data, null); } /// /// Sends a binary data from the specified /// using the WebSocket connection. /// /// /// This method does not wait for the send to be complete. /// /// /// A from which contains a binary data to send. /// public void Send (FileInfo file) { Send (file, null); } /// /// Sends a text using the WebSocket connection. /// /// /// This method does not wait for the send to be complete. /// /// /// A that contains a text data to send. /// public void Send (string data) { Send (data, null); } /// /// Sends a binary using the WebSocket connection. /// /// /// This method does not wait for the send to be complete. /// /// /// An array of that contains a binary data to send. /// /// /// An Action<bool> delegate that references the method(s) called when /// the send is complete. /// A passed to this delegate is true if the send is complete /// successfully; otherwise, false. /// public void Send (byte [] data, Action completed) { var msg = _readyState.CheckIfOpen () ?? data.CheckIfValidSendData (); if (msg != null) { _logger.Error (msg); error (msg); return; } var len = data.LongLength; if (len <= FragmentLength) send ( Opcode.BINARY, len > 0 && _client && _compression == CompressionMethod.NONE ? data.Copy (len) : data, completed); else send (Opcode.BINARY, new MemoryStream (data), completed); } /// /// Sends a binary data from the specified /// using the WebSocket connection. /// /// /// This method does not wait for the send to be complete. /// /// /// A from which contains a binary data to send. /// /// /// An Action<bool> delegate that references the method(s) called when /// the send is complete. /// A passed to this delegate is true if the send is complete /// successfully; otherwise, false. /// public void Send (FileInfo file, Action completed) { var msg = _readyState.CheckIfOpen () ?? (file == null ? "'file' must not be null." : null); if (msg != null) { _logger.Error (msg); error (msg); return; } send (Opcode.BINARY, file.OpenRead (), completed); } /// /// Sends a text using the WebSocket connection. /// /// /// This method does not wait for the send to be complete. /// /// /// A that contains a text data to send. /// /// /// An Action<bool> delegate that references the method(s) called when /// the send is complete. /// A passed to this delegate is true if the send is complete /// successfully; otherwise, false. /// public void Send (string data, Action completed) { var msg = _readyState.CheckIfOpen () ?? data.CheckIfValidSendData (); if (msg != null) { _logger.Error (msg); error (msg); return; } var rawData = Encoding.UTF8.GetBytes (data); if (rawData.LongLength <= FragmentLength) send (Opcode.TEXT, rawData, completed); else send (Opcode.TEXT, new MemoryStream (rawData), completed); } /// /// Sends a binary data from the specified /// using the WebSocket connection. /// /// /// This method does not wait for the send to be complete. /// /// /// A object from which contains a binary data to send. /// /// /// An that contains the number of bytes to send. /// public void Send (Stream stream, int length) { Send (stream, length, null); } /// /// Sends a binary data from the specified /// using the WebSocket connection. /// /// /// This method does not wait for the send to be complete. /// /// /// A object from which contains a binary data to send. /// /// /// An that contains the number of bytes to send. /// /// /// An Action<bool> delegate that references the method(s) called when /// the send is complete. /// A passed to this delegate is true if the send is /// complete successfully; otherwise, false. /// public void Send (Stream stream, int length, Action completed) { var msg = _readyState.CheckIfOpen () ?? stream.CheckIfCanRead () ?? (length < 1 ? "'length' must be greater than 0." : null); if (msg != null) { _logger.Error (msg); error (msg); return; } stream.ReadBytesAsync ( length, data => { var len = data.Length; if (len == 0) { var err = "A data cannot be read from 'stream'."; _logger.Error (err); error (err); return; } if (len < length) _logger.Warn (String.Format ( "A data with 'length' cannot be read from 'stream'.\nexpected: {0} actual: {1}", length, len)); var sent = len <= FragmentLength ? send (Opcode.BINARY, data) : send (Opcode.BINARY, new MemoryStream (data)); if (completed != null) completed (sent); }, ex => { _logger.Fatal (ex.ToString ()); error ("An exception has occurred."); }); } /// /// Sets a used in the WebSocket connection request. /// /// /// A that represents an HTTP Cookie to set. /// public void SetCookie (Cookie cookie) { var msg = !_client ? "SetCookie isn't available as a server." : IsOpened ? "A WebSocket connection has already been established." : cookie == null ? "'cookie' must not be null." : null; if (msg != null) { _logger.Error (msg); error (msg); return; } lock (_cookies.SyncRoot) { _cookies.SetOrRemove (cookie); } } /// /// Sets a pair of the and /// for HTTP authentication (Basic/Digest). /// /// /// A that represents the user name used to authenticate. /// /// /// A that represents the password for /// used to authenticate. /// /// /// true if the sends a Basic authentication /// credentials with the first connection request; otherwise, false. /// public void SetCredentials (string username, string password, bool preAuth) { string msg = null; if (!_client) msg = "SetCredentials isn't available as a server."; else if (IsOpened) msg = "A WebSocket connection has already been established."; else if (username.IsNullOrEmpty ()) { _credentials = null; _preAuth = false; _logger.Warn ("Credentials was set back to the default."); return; } else { msg = username.Contains (':') || !username.IsText () ? "'username' contains an invalid character." : !password.IsNullOrEmpty () && !password.IsText () ? "'password' contains an invalid character." : null; } if (msg != null) { _logger.Error (msg); error (msg); return; } _credentials = new NetworkCredential ( username, password, _uri.PathAndQuery); _preAuth = preAuth; } #endregion } }