#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. /// /// /// 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 used in the WebSocket connection request and /// response. /// /// /// An IEnumerable<Cookie> interface that provides an enumerator which /// supports the iteration over the collection of cookies. /// public IEnumerable Cookies { get { lock (_cookies.SyncRoot) { foreach (Cookie cookie in _cookies) yield return 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 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 . 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; } } /// /// 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 data frame. /// 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.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 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.INCORRECT_DATA, "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.POLICY_VIOLATION, null); } // As server private bool acceptHandshake () { _logger.Debug ( String.Format ( "A WebSocket connection request from {0}:\n{1}", _context.UserEndPoint, _context)); var err = checkIfValidHandshakeRequest (_context); if (err != null) { _logger.Error (err); error ("An error has occurred while connecting."); 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 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; } 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 checkIfCanClose (Func checkParams) { return _readyState.CheckIfClosable () ?? checkParams (); } private string checkIfCanConnect () { return !_client && _readyState == WebSocketState.CLOSED ? "Connect isn't available to reconnect as a server." : _readyState.CheckIfConnectable (); } private string checkIfCanSend (Func checkParams) { return _readyState.CheckIfOpen () ?? checkParams (); } // As server private string checkIfValidHandshakeRequest (WebSocketContext context) { string key, version; return !context.IsWebSocketRequest ? "Not WebSocket connection request." : !validateHostHeader (context.Host) ? "Invalid Host header." : (key = context.SecWebSocketKey) == null || key.Length == 0 ? "Invalid Sec-WebSocket-Key header." : (version = context.SecWebSocketVersion) == null || version != _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." : !validateSecWebSocketVersionHeader ( 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 [] frameAsBytes, int timeOut, Action release) { var sent = frameAsBytes != null && _stream.Write (frameAsBytes); 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) { var frame = _stream.ReadFrame (); // MORE & CONT if (!frame.IsFinal && frame.IsContinuation) { dest.WriteBytes (frame.PayloadData.ApplicationData); return concatenateFragmentsInto (dest); } // FINAL & CONT if (frame.IsFinal && frame.IsContinuation) { dest.WriteBytes (frame.PayloadData.ApplicationData); return true; } // FINAL & PING if (frame.IsFinal && frame.IsPing) { acceptPingFrame (frame); return concatenateFragmentsInto (dest); } // FINAL & PONG if (frame.IsFinal && frame.IsPong) { acceptPongFrame (frame); return concatenateFragmentsInto (dest); } // FINAL & CLOSE if (frame.IsFinal && frame.IsClose) return acceptCloseFrame (frame); // ? return acceptUnsupportedFrame ( frame, CloseStatusCode.INCORRECT_DATA, null); } 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 (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 != null) headers ["Sec-WebSocket-Protocol"] = _protocols.ToString (", "); 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 (_base64Key); 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; } // 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; } 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)); } private void init () { _compression = CompressionMethod.NONE; _cookies = new CookieCollection (); _extensions = String.Empty; _forConn = new object (); _forSend = new object (); _protocol = String.Empty; _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 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; } // 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) { var msg = "The WebSocket connection isn't available."; _logger.Error (msg); error (msg); 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) { var msg = "The WebSocket connection isn't available."; _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; } 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; 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 (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 validateSecWebSocketProtocolHeader (string value) { if (value == null) return _protocols == null; if (_protocols == null || !_protocols.Contains (protocol => protocol == value)) return false; _protocol = value; return true; } // As client private bool validateSecWebSocketVersionHeader (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 [] frameAsBytes, int waitTimeOut) { 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 ( frameAsBytes, waitTimeOut, 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 millisecondsTimeout) { return send (frame) && _receivePong.WaitOne (millisecondsTimeout); } // 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 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; 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 that indicates 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 that indicates 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 that indicates the /// reason for closure. /// /// /// A that represents the reason for closure. /// public void Close (ushort code, string reason) { byte [] data = null; var msg = checkIfCanClose ( () => 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 that indicates the reason for closure. /// /// /// A that represents the reason for closure. /// public void Close (CloseStatusCode code, string reason) { byte [] data = null; var msg = checkIfCanClose ( () => (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 that indicates 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 that indicates 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 that indicates the /// reason for closure. /// /// /// A that represents the reason for closure. /// public void CloseAsync (ushort code, string reason) { byte [] data = null; var msg = checkIfCanClose ( () => 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 that indicates the reason for closure. /// /// /// A that represents the reason for closure. /// public void CloseAsync (CloseStatusCode code, string reason) { byte [] data = null; var msg = checkIfCanClose ( () => (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 the /// . /// public void Dispose () { Close (CloseStatusCode.AWAY, null); } /// /// Sends a Ping using the WebSocket connection. /// /// /// true if the instance receives the 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 instance receives the 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 = checkIfCanSend (() => 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 = checkIfCanSend (() => 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 = checkIfCanSend (() => 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 = checkIfCanSend (() => 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 = checkIfCanSend (() => 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 = checkIfCanSend (() => 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 = checkIfCanSend ( () => 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 HTTP 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 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 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 } }