#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.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 _forConn; 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 Internal Constructors // As server internal WebSocket (HttpListenerWebSocketContext context, Logger logger) { _context = context; _logger = logger; _closeContext = context.Close; _secure = context.IsSecureConnection; _stream = context.Stream; _uri = context.Path.ToUri (); init (); } // As server internal WebSocket (TcpListenerWebSocketContext context, Logger logger) { _context = context; _logger = logger; _closeContext = context.Close; _secure = context.IsSecureConnection; _stream = context.Stream; _uri = context.Path.ToUri (); init (); } #endregion #region Public Constructors /// /// Initializes a new instance of the class with the specified /// WebSocket URL and subprotocols. /// /// /// A that represents the WebSocket URL to connect. /// /// /// An array of that contains the WebSocket subprotocols if any. /// Each value of must be a token defined in /// RFC 2616. /// /// /// /// is invalid. /// /// /// -or- /// /// /// is invalid. /// /// /// /// is . /// public WebSocket (string url, params string [] protocols) { if (url == null) throw new ArgumentNullException ("url"); string msg; if (!url.TryCreateWebSocketUri (out _uri, out msg)) throw new ArgumentException (msg, "url"); if (protocols != null && protocols.Length > 0) { msg = protocols.CheckIfValidProtocols (); if (msg != null) throw new ArgumentException (msg, "protocols"); _protocols = protocols; } _base64Key = CreateBase64Key (); _client = true; _logger = new Logger (); _secure = _uri.Scheme == "wss"; init (); } #endregion #region Internal Properties internal Func CookiesValidation { get { return _cookiesValidation; } set { _cookiesValidation = value; } } internal bool IsConnected { get { return _readyState == WebSocketState.Open || _readyState == WebSocketState.Closing; } } #endregion #region Public Properties /// /// Gets or sets the compression method used to compress the message on the WebSocket /// connection. /// /// /// One of the enum values, indicates the compression method /// used to compress the message. The default value is . /// public CompressionMethod Compression { get { return _compression; } set { lock (_forConn) { var msg = checkIfAvailable ("Set operation of Compression", false, false); if (msg != null) { _logger.Error (msg); error (msg); return; } _compression = value; } } } /// /// Gets the HTTP cookies included in the WebSocket connection request and response. /// /// /// An IEnumerable<Cookie> instance that provides an enumerator which supports the /// iteration over the collection of the cookies. /// public IEnumerable Cookies { get { lock (_cookies.SyncRoot) { foreach (Cookie cookie in _cookies) yield return cookie; } } } /// /// Gets the credentials for the HTTP authentication (Basic/Digest). /// /// /// A that represents the credentials for the HTTP /// authentication. The default value is . /// public NetworkCredential Credentials { get { return _credentials; } } /// /// Gets the WebSocket extensions selected by the server. /// /// /// A that represents the extensions if any. The default value is /// . /// public string Extensions { get { return _extensions ?? String.Empty; } } /// /// 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 . If you would like to change it, /// you should set the Log.Level property to any of the enum /// values. /// /// /// A that provides the logging functions. /// public Logger Log { get { return _logger; } internal set { _logger = value; } } /// /// Gets or sets the value of the Origin header to send with the WebSocket connection request /// to the server. /// /// /// 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 { lock (_forConn) { var msg = checkIfAvailable ("Set operation of Origin", false, false); if (msg == null) { if (value.IsNullOrEmpty ()) { _origin = value; return; } 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 ?? String.Empty; } internal set { _protocol = value; } } /// /// Gets the state of the WebSocket connection. /// /// /// One of the enum values, indicates the state of the WebSocket /// connection. 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 { lock (_forConn) { var msg = checkIfAvailable ( "Set operation of ServerCertificateValidationCallback", false, false); 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 message. /// public event EventHandler OnMessage; /// /// Occurs when the WebSocket connection has been established. /// public event EventHandler OnOpen; #endregion #region Private Methods private bool acceptCloseFrame (WsFrame frame) { var payload = frame.PayloadData; close (payload, !payload.ContainsReservedCloseStatusCode, false); return false; } private bool acceptDataFrame (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 acceptException (Exception exception, string reason) { var code = CloseStatusCode.Abnormal; var msg = reason; if (exception is WebSocketException) { var wsex = (WebSocketException) exception; code = wsex.Code; reason = wsex.Message; } if (code == CloseStatusCode.Abnormal || code == CloseStatusCode.TlsHandshakeFailure) { _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 acceptFragmentedFrame (WsFrame frame) { return frame.IsContinuation // Not first fragment ? true : acceptFragments (frame); } private bool acceptFragments (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 acceptFrame (WsFrame frame) { return frame.IsCompressed && _compression == CompressionMethod.None ? acceptUnsupportedFrame ( frame, CloseStatusCode.IncorrectData, "A compressed data has been received without available decompression method.") : frame.IsFragmented ? acceptFragmentedFrame (frame) : frame.IsData ? acceptDataFrame (frame) : frame.IsPing ? acceptPingFrame (frame) : frame.IsPong ? acceptPongFrame (frame) : frame.IsClose ? acceptCloseFrame (frame) : acceptUnsupportedFrame (frame, CloseStatusCode.PolicyViolation, null); } // As server private bool acceptHandshake () { _logger.Debug ( String.Format ( "A WebSocket connection request from {0}:\n{1}", _context.UserEndPoint, _context)); var msg = checkIfValidHandshakeRequest (_context); if (msg != null) { _logger.Error (msg); error ("An error has occurred while connecting."); Close (HttpStatusCode.BadRequest); return false; } if (_protocol != null && !_context.SecWebSocketProtocols.Contains (protocol => protocol == _protocol)) _protocol = null; var extensions = _context.Headers ["Sec-WebSocket-Extensions"]; if (extensions != null && extensions.Length > 0) acceptSecWebSocketExtensionsHeader (extensions); return send (createHandshakeResponse ()); } private bool acceptPingFrame (WsFrame frame) { var mask = _client ? Mask.Mask : Mask.Unmask; if (send (WsFrame.CreatePongFrame (mask, frame.PayloadData))) _logger.Trace ("Returned a Pong."); return true; } private bool acceptPongFrame (WsFrame frame) { _receivePong.Set (); _logger.Trace ("Received a Pong."); return true; } // As server private void acceptSecWebSocketExtensionsHeader (string value) { var extensions = new StringBuilder (32); var compress = false; foreach (var extension in value.SplitHeaderValue (',')) { var trimed = extension.Trim (); var unprefixed = trimed.RemovePrefix ("x-webkit-"); if (!compress && unprefixed.IsCompressionExtension ()) { var method = unprefixed.ToCompressionMethod (); if (method != CompressionMethod.None) { _compression = method; compress = true; extensions.Append (trimed + ", "); } } } var len = extensions.Length; if (len > 0) { extensions.Length = len - 2; _extensions = extensions.ToString (); } } private bool acceptUnsupportedFrame (WsFrame frame, CloseStatusCode code, string reason) { _logger.Debug ("Unsupported frame:\n" + frame.PrintToString (false)); acceptException (new WebSocketException (code, reason), null); return false; } private string checkIfAvailable ( string operation, bool availableAsServer, bool availableAsConnected) { return !_client && !availableAsServer ? operation + " isn't available as a server." : !availableAsConnected ? _readyState.CheckIfConnectable () : null; } private string checkIfCanConnect () { return !_client && _readyState == WebSocketState.Closed ? "Connect isn't available to reconnect as a server." : _readyState.CheckIfConnectable (); } // As server private string checkIfValidHandshakeRequest (WebSocketContext context) { var headers = context.Headers; return !context.IsWebSocketRequest ? "Not WebSocket connection request." : !validateHostHeader (headers ["Host"]) ? "Invalid Host header." : !validateSecWebSocketKeyHeader (headers ["Sec-WebSocket-Key"]) ? "Invalid Sec-WebSocket-Key header." : !validateSecWebSocketVersionClientHeader (headers ["Sec-WebSocket-Version"]) ? "Invalid Sec-WebSocket-Version header." : !validateCookies (context.CookieCollection, _cookies) ? "Invalid Cookies." : null; } // As client private string checkIfValidHandshakeResponse (HandshakeResponse response) { var headers = response.Headers; return response.IsUnauthorized ? String.Format ("HTTP {0} authorization is required.", response.AuthChallenge.Scheme) : !response.IsWebSocketResponse ? "Not WebSocket connection response." : !validateSecWebSocketAcceptHeader (headers ["Sec-WebSocket-Accept"]) ? "Invalid Sec-WebSocket-Accept header." : !validateSecWebSocketProtocolHeader (headers ["Sec-WebSocket-Protocol"]) ? "Invalid Sec-WebSocket-Protocol header." : !validateSecWebSocketExtensionsHeader (headers ["Sec-WebSocket-Extensions"]) ? "Invalid Sec-WebSocket-Extensions header." : !validateSecWebSocketVersionServerHeader (headers ["Sec-WebSocket-Version"]) ? "Invalid Sec-WebSocket-Version header." : null; } 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 (_forConn) { if (_readyState == WebSocketState.Closing || _readyState == WebSocketState.Closed) { _logger.Info ("Closing the WebSocket connection has already been done."); return; } _readyState = WebSocketState.Closing; } _logger.Trace ("Start closing handshake."); var args = new CloseEventArgs (payload); args.WasClean = _client ? closeHandshake ( send ? WsFrame.CreateCloseFrame (Mask.Mask, payload).ToByteArray () : null, wait ? 5000 : 0, closeClientResources) : closeHandshake ( send ? WsFrame.CreateCloseFrame (Mask.Unmask, payload).ToByteArray () : null, wait ? 1000 : 0, closeServerResources); _logger.Trace ("End closing handshake."); _readyState = WebSocketState.Closed; try { OnClose.Emit (this, args); } catch (Exception ex) { _logger.Fatal (ex.ToString ()); error ("An exception has occurred while OnClose."); } } private void closeAsync (PayloadData payload, bool send, bool wait) { Action closer = close; closer.BeginInvoke (payload, send, wait, ar => closer.EndInvoke (ar), null); } // As client private void closeClientResources () { if (_stream != null) { _stream.Dispose (); _stream = null; } if (_tcpClient != null) { _tcpClient.Close (); _tcpClient = null; } } private bool closeHandshake (byte [] frame, int timeout, Action release) { var sent = frame != null && _stream.Write (frame); var received = timeout == 0 || (sent && _exitReceiving != null && _exitReceiving.WaitOne (timeout)); release (); if (_receivePong != null) { _receivePong.Close (); _receivePong = null; } if (_exitReceiving != null) { _exitReceiving.Close (); _exitReceiving = null; } var result = sent && received; _logger.Debug ( String.Format ("Was clean?: {0}\nsent: {1} received: {2}", result, sent, received)); return result; } // As server private void closeServerResources () { if (_closeContext == null) return; _closeContext (); _closeContext = null; _stream = null; _context = null; } private bool concatenateFragmentsInto (Stream dest) { while (true) { var frame = _stream.ReadFrame (); if (frame.IsFinal) { // FINAL // CONT if (frame.IsContinuation) { dest.WriteBytes (frame.PayloadData.ApplicationData); break; } // PING if (frame.IsPing) { acceptPingFrame (frame); continue; } // PONG if (frame.IsPong) { acceptPongFrame (frame); continue; } // CLOSE if (frame.IsClose) return acceptCloseFrame (frame); } else { // MORE // CONT if (frame.IsContinuation) { dest.WriteBytes (frame.PayloadData.ApplicationData); continue; } } // ? return acceptUnsupportedFrame ( frame, CloseStatusCode.IncorrectData, "An incorrect data has been received while receiving fragmented data."); } return true; } private bool connect () { lock (_forConn) { var msg = _readyState.CheckIfConnectable (); if (msg != null) { _logger.Error (msg); error (msg); return false; } try { if (_client ? doHandshake () : acceptHandshake ()) { _readyState = WebSocketState.Open; return true; } } catch (Exception ex) { acceptException (ex, "An exception has occurred while connecting."); } return false; } } // As client private string createExtensionsRequest () { var extensions = new StringBuilder (32); if (_compression != CompressionMethod.None) extensions.Append (_compression.ToExtensionString ()); return extensions.Length > 0 ? extensions.ToString () : null; } // 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 != null) headers ["Sec-WebSocket-Protocol"] = _protocols.ToString (", "); var extensions = createExtensionsRequest (); if (extensions != null) 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 (_base64Key); if (_protocol != null) headers ["Sec-WebSocket-Protocol"] = _protocol; if (_extensions != null) 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; } // As client private bool doHandshake () { setClientStream (); var res = sendHandshakeRequest (); var msg = checkIfValidHandshakeResponse (res); if (msg != null) { _logger.Error (msg); msg = "An error has occurred while connecting."; error (msg); close (CloseStatusCode.Abnormal, msg, false); return false; } var cookies = res.Cookies; if (cookies.Count > 0) _cookies.SetOrRemove (cookies); return true; } private void error (string message) { OnError.Emit (this, new ErrorEventArgs (message)); } private void init () { _compression = CompressionMethod.None; _cookies = new CookieCollection (); _forConn = new object (); _forSend = new object (); _readyState = WebSocketState.Connecting; } private void open () { try { OnOpen.Emit (this, EventArgs.Empty); if (_readyState == WebSocketState.Open) startReceiving (); } catch (Exception ex) { acceptException (ex, "An exception has occurred while opening."); } } // As client private HandshakeResponse receiveHandshakeResponse () { var res = _stream.ReadHandshakeResponse (); _logger.Debug ("A response to this WebSocket connection request:\n" + res.ToString ()); return res; } private bool send (byte [] frame) { lock (_forConn) { if (_readyState != WebSocketState.Open) { _logger.Warn ("Sending has been interrupted."); return false; } return _stream.Write (frame); } } // 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 the WebSocket connection request:\n" + response.ToString ()); return _stream.WriteHandshake (response); } private bool send (WsFrame frame) { lock (_forConn) { if (_readyState != WebSocketState.Open) { _logger.Warn ("Sending has been interrupted."); 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; } var mask = _client ? Mask.Mask : Mask.Unmask; sent = send (WsFrame.CreateFrame (Fin.Final, opcode, mask, data, compressed)); } catch (Exception ex) { _logger.Fatal (ex.ToString ()); error ("An exception has occurred while sending a data."); } 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; } var mask = _client ? Mask.Mask : Mask.Unmask; sent = sendFragmented (opcode, stream, mask, compressed); } catch (Exception ex) { _logger.Fatal (ex.ToString ()); error ("An exception has occurred while sending a data."); } finally { if (compressed) stream.Dispose (); src.Dispose (); } return sent; } } private void sendAsync (Opcode opcode, byte [] data, Action completed) { Func sender = send; sender.BeginInvoke ( opcode, data, ar => { try { var sent = sender.EndInvoke (ar); if (completed != null) completed (sent); } catch (Exception ex) { _logger.Fatal (ex.ToString ()); error ("An exception has occurred while callback."); } }, null); } private void sendAsync (Opcode opcode, Stream stream, Action completed) { Func sender = send; sender.BeginInvoke ( opcode, stream, ar => { try { var sent = sender.EndInvoke (ar); if (completed != null) completed (sent); } catch (Exception ex) { _logger.Fatal (ex.ToString ()); error ("An exception has occurred while callback."); } }, null); } private bool sendFragmented (Opcode opcode, Stream stream, Mask mask, bool compressed) { var len = stream.Length; var quo = len / FragmentLength; var rem = (int) (len % FragmentLength); var times = rem == 0 ? quo - 2 : quo - 1; byte [] buffer = null; // Not fragmented if (quo == 0) { buffer = new byte [rem]; return stream.Read (buffer, 0, rem) == rem && send (WsFrame.CreateFrame (Fin.Final, opcode, mask, buffer, compressed)); } buffer = new byte [FragmentLength]; // First if (stream.Read (buffer, 0, FragmentLength) != FragmentLength || !send (WsFrame.CreateFrame (Fin.More, opcode, mask, buffer, compressed))) return false; // Mid for (long i = 0; i < times; i++) { if (stream.Read (buffer, 0, FragmentLength) != FragmentLength || !send (WsFrame.CreateFrame (Fin.More, Opcode.CONT, mask, buffer, compressed))) return false; } // Final var tmpLen = FragmentLength; if (rem != 0) buffer = new byte [tmpLen = rem]; return stream.Read (buffer, 0, tmpLen) == tmpLen && send (WsFrame.CreateFrame (Fin.Final, Opcode.CONT, mask, buffer, compressed)); } // 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 (acceptFrame (frame)) receive (); else if (_exitReceiving != null) _exitReceiving.Set (); }, ex => acceptException ( ex, "An exception has occurred while receiving a message.")); receive (); } // 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 expected = _uri.DnsSafeHost; return Uri.CheckHostName (host) != UriHostNameType.Dns || Uri.CheckHostName (expected) != UriHostNameType.Dns || host == expected; } // As client private bool validateSecWebSocketAcceptHeader (string value) { return value != null && value == CreateResponseKey (_base64Key); } // As client private bool validateSecWebSocketExtensionsHeader (string value) { var compress = _compression != CompressionMethod.None; if (value == null || value.Length == 0) { if (compress) _compression = CompressionMethod.None; return true; } if (!compress) return false; var extensions = value.SplitHeaderValue (','); if (extensions.Contains ( extension => extension.Trim () != _compression.ToExtensionString ())) return false; _extensions = value; return true; } // As server private bool validateSecWebSocketKeyHeader (string value) { if (value == null || value.Length == 0) return false; _base64Key = value; return true; } // As client private bool validateSecWebSocketProtocolHeader (string value) { if (value == null) return _protocols == null; if (_protocols == null || !_protocols.Contains (protocol => protocol == value)) return false; _protocol = value; return true; } // As server private bool validateSecWebSocketVersionClientHeader (string value) { return value != null && value == _version; } // As client private bool validateSecWebSocketVersionServerHeader (string value) { return value == null || value == _version; } #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 [] frame, int timeout) { lock (_forConn) { if (_readyState == WebSocketState.Closing || _readyState == WebSocketState.Closed) { _logger.Info ("Closing the WebSocket connection has already been done."); return; } _readyState = WebSocketState.Closing; } args.WasClean = closeHandshake (frame, timeout, closeServerResources); _readyState = WebSocketState.Closed; try { OnClose.Emit (this, args); } catch (Exception ex) { _logger.Fatal (ex.ToString ()); } } // As server internal void ConnectAsServer () { try { if (acceptHandshake ()) { _readyState = WebSocketState.Open; open (); } } catch (Exception ex) { acceptException (ex, "An exception has occurred while connecting."); } } // As client internal static string CreateBase64Key () { var src = new byte [16]; var rand = new Random (); rand.NextBytes (src); return Convert.ToBase64String (src); } internal static string CreateResponseKey (string base64Key) { 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); } internal bool Ping (byte [] frame, int timeout) { return send (frame) && _receivePong.WaitOne (timeout); } // As server, used to broadcast internal void Send (Opcode opcode, byte [] data, Dictionary cache) { lock (_forSend) { lock (_forConn) { if (_readyState != WebSocketState.Open) return; 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); } _stream.Write (cached); } catch (Exception ex) { _logger.Fatal (ex.ToString ()); error ("An exception has occurred while sending a data."); } } } } // 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; if (_readyState == WebSocketState.Open) sendFragmented (opcode, cached, Mask.Unmask, _compression != CompressionMethod.None); } catch (Exception ex) { _logger.Fatal (ex.ToString ()); error ("An exception has occurred while sending a data."); } } } #endregion #region Public Methods /// /// Closes the WebSocket connection, and releases all associated resources. /// public void Close () { var msg = _readyState.CheckIfClosable (); if (msg != null) { _logger.Error (msg); error (msg); return; } var send = _readyState == WebSocketState.Open; close (new PayloadData (), send, send); } /// /// Closes the WebSocket connection with the specified , and releases all /// associated resources. /// /// /// This method emits a event if isn't in the /// allowable range of the WebSocket close status code. /// /// /// A that represents the status code indicating the reason for closure. /// public void Close (ushort code) { Close (code, null); } /// /// Closes the WebSocket connection with the specified , and /// releases all associated resources. /// /// /// One of the enum values, represents the status code indicating /// the reason for closure. /// public void Close (CloseStatusCode code) { Close (code, null); } /// /// Closes the WebSocket connection with the specified and /// , and releases all associated resources. /// /// /// This method emits a event if isn't in the /// allowable range of the WebSocket close status code or the size of /// is greater than 123 bytes. /// /// /// A that represents the status code indicating the reason for closure. /// /// /// A that represents the reason for closure. /// public void Close (ushort code, string reason) { byte [] data = null; var msg = _readyState.CheckIfClosable () ?? code.CheckIfValidCloseStatusCode () ?? (data = code.Append (reason)).CheckIfValidControlData ("reason"); if (msg != null) { _logger.Error (String.Format ("{0}\ncode: {1} reason: {2}", msg, code, reason)); error (msg); return; } var send = _readyState == WebSocketState.Open && !code.IsReserved (); close (new PayloadData (data), send, send); } /// /// Closes the WebSocket connection with the specified and /// , and releases all associated resources. /// /// /// This method emits a event if the size of is /// greater than 123 bytes. /// /// /// One of the enum values, represents the status code indicating /// the reason for closure. /// /// /// A that represents the reason for closure. /// public void Close (CloseStatusCode code, string reason) { byte [] data = null; var msg = _readyState.CheckIfClosable () ?? (data = ((ushort) code).Append (reason)).CheckIfValidControlData ("reason"); if (msg != null) { _logger.Error (String.Format ("{0}\ncode: {1} reason: {2}", msg, code, reason)); error (msg); return; } var send = _readyState == WebSocketState.Open && !code.IsReserved (); close (new PayloadData (data), send, send); } /// /// Closes the WebSocket connection asynchronously, and releases all associated resources. /// /// /// This method doesn't wait for the close to be complete. /// public void CloseAsync () { var msg = _readyState.CheckIfClosable (); if (msg != null) { _logger.Error (msg); error (msg); return; } var send = _readyState == WebSocketState.Open; closeAsync (new PayloadData (), send, send); } /// /// Closes the WebSocket connection asynchronously with the specified , /// and releases all associated resources. /// /// /// /// This method doesn't wait for the close to be complete. /// /// /// This method emits a event if isn't in the /// allowable range of the WebSocket close status code. /// /// /// /// A that represents the status code indicating the reason for closure. /// public void CloseAsync (ushort code) { CloseAsync (code, null); } /// /// Closes the WebSocket connection asynchronously with the specified /// , and releases all associated resources. /// /// /// This method doesn't wait for the close to be complete. /// /// /// One of the enum values, represents the status code indicating /// the reason for closure. /// public void CloseAsync (CloseStatusCode code) { CloseAsync (code, null); } /// /// Closes the WebSocket connection asynchronously with the specified and /// , and releases all associated resources. /// /// /// /// This method doesn't wait for the close to be complete. /// /// /// This method emits a event if /// isn't in the allowable range of the WebSocket close status code or the size /// of is greater than 123 bytes. /// /// /// /// A that represents the status code indicating the reason for closure. /// /// /// A that represents the reason for closure. /// public void CloseAsync (ushort code, string reason) { byte [] data = null; var msg = _readyState.CheckIfClosable () ?? code.CheckIfValidCloseStatusCode () ?? (data = code.Append (reason)).CheckIfValidControlData ("reason"); if (msg != null) { _logger.Error (String.Format ("{0}\ncode: {1} reason: {2}", msg, code, reason)); error (msg); return; } var send = _readyState == WebSocketState.Open && !code.IsReserved (); closeAsync (new PayloadData (data), send, send); } /// /// Closes the WebSocket connection asynchronously with the specified /// and , and releases /// all associated resources. /// /// /// /// This method doesn't wait for the close to be complete. /// /// /// This method emits a event if the size of /// is greater than 123 bytes. /// /// /// /// One of the enum values, represents the status code indicating /// the reason for closure. /// /// /// A that represents the reason for closure. /// public void CloseAsync (CloseStatusCode code, string reason) { byte [] data = null; var msg = _readyState.CheckIfClosable () ?? (data = ((ushort) code).Append (reason)).CheckIfValidControlData ("reason"); if (msg != null) { _logger.Error (String.Format ("{0}\ncode: {1} reason: {2}", msg, code, reason)); error (msg); return; } var send = _readyState == WebSocketState.Open && !code.IsReserved (); closeAsync (new PayloadData (data), send, send); } /// /// Establishes a WebSocket connection. /// public void Connect () { var msg = checkIfCanConnect (); if (msg != null) { _logger.Error (msg); error (msg); return; } if (connect ()) open (); } /// /// Establishes a WebSocket connection asynchronously. /// /// /// This method doesn't wait for the connect to be complete. /// public void ConnectAsync () { var msg = checkIfCanConnect (); if (msg != null) { _logger.Error (msg); error (msg); return; } Func connector = connect; connector.BeginInvoke ( ar => { if (connector.EndInvoke (ar)) open (); }, null); } /// /// Closes the WebSocket connection, and releases all associated resources. /// /// /// This method closes the WebSocket connection with . /// public void Dispose () { Close (CloseStatusCode.Away, null); } /// /// Sends a Ping using the WebSocket connection. /// /// /// true if the receives a Pong to this Ping 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. /// /// /// true if the receives a Pong to this Ping in a time; /// otherwise, false. /// /// /// A that represents the message to send. /// public bool Ping (string message) { if (message == null || message.Length == 0) return Ping (); var data = Encoding.UTF8.GetBytes (message); var msg = data.CheckIfValidControlData ("message"); 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. /// /// /// An array of that represents the binary data to send. /// public void Send (byte [] data) { 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); else send (Opcode.BINARY, new MemoryStream (data)); } /// /// Sends the specified as a binary data /// using the WebSocket connection. /// /// /// A that represents the file to send. /// public void Send (FileInfo file) { var msg = _readyState.CheckIfOpen () ?? file.CheckIfValidSendData (); if (msg != null) { _logger.Error (msg); error (msg); return; } send (Opcode.BINARY, file.OpenRead ()); } /// /// Sends a text using the WebSocket connection. /// /// /// A that represents the text data to send. /// public void Send (string data) { 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); else send (Opcode.TEXT, new MemoryStream (rawData)); } /// /// Sends a binary asynchronously using the WebSocket connection. /// /// /// This method doesn't wait for the send to be complete. /// /// /// An array of that represents the 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 SendAsync (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) sendAsync ( Opcode.BINARY, len > 0 && _client && _compression == CompressionMethod.None ? data.Copy (len) : data, completed); else sendAsync (Opcode.BINARY, new MemoryStream (data), completed); } /// /// Sends the specified as a binary data asynchronously /// using the WebSocket connection. /// /// /// This method doesn't wait for the send to be complete. /// /// /// A that represents the file 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 SendAsync (FileInfo file, Action completed) { var msg = _readyState.CheckIfOpen () ?? file.CheckIfValidSendData (); if (msg != null) { _logger.Error (msg); error (msg); return; } sendAsync (Opcode.BINARY, file.OpenRead (), completed); } /// /// Sends a text asynchronously using the WebSocket connection. /// /// /// This method doesn't wait for the send to be complete. /// /// /// A that represents the 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 SendAsync (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) sendAsync (Opcode.TEXT, rawData, completed); else sendAsync (Opcode.TEXT, new MemoryStream (rawData), completed); } /// /// Sends a binary data from the specified asynchronously /// using the WebSocket connection. /// /// /// This method doesn't wait for the send to be complete. /// /// /// A from which contains the binary data to send. /// /// /// An that represents 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 SendAsync (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) { msg = "A data cannot be read from 'stream'."; _logger.Error (msg); error (msg); 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 while sending a data."); }); } /// /// Sets an HTTP to send with the WebSocket connection request /// to the server. /// /// /// A that represents the cookie to send. /// public void SetCookie (Cookie cookie) { lock (_forConn) { var msg = checkIfAvailable ("SetCookie", false, false) ?? (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 and for /// the 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 the Basic authentication credentials /// with the first connection request to the server; otherwise, false. /// public void SetCredentials (string username, string password, bool preAuth) { lock (_forConn) { var msg = checkIfAvailable ("SetCredentials", false, false); if (msg == null) { if (username.IsNullOrEmpty ()) { _credentials = null; _preAuth = false; _logger.Warn ("Credentials was set back to the default."); return; } 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 } }