#region License /* * WebSocket.cs * * 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-2016 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 #region Contributors /* * Contributors: * - Frank Razenberg * - David Wood * - Liryna */ #endregion using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.IO; using System.Net.Security; using System.Net.Sockets; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; using WebSocketSharp.Net; using WebSocketSharp.Net.WebSockets; namespace WebSocketSharp { /// /// Implements the WebSocket interface. /// /// /// /// This class provides a set of methods and properties for two-way /// communication using the WebSocket protocol. /// /// /// The WebSocket protocol is defined in /// RFC 6455. /// /// public class WebSocket : IDisposable { #region Private Fields private AuthenticationChallenge _authChallenge; private string _base64Key; private bool _client; private Action _closeContext; private CompressionMethod _compression; private WebSocketContext _context; private CookieCollection _cookies; private NetworkCredential _credentials; private bool _emitOnPing; private bool _enableRedirection; private string _extensions; private bool _extensionsRequested; private object _forMessageEventQueue; private object _forPing; private object _forSend; private object _forState; private MemoryStream _fragmentsBuffer; private bool _fragmentsCompressed; private Opcode _fragmentsOpcode; private const string _guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; private Func _handshakeRequestChecker; private bool _ignoreExtensions; private bool _inContinuation; private volatile bool _inMessage; private volatile Logger _logger; private static readonly int _maxRetryCountForConnect; private Action _message; private Queue _messageEventQueue; private uint _nonceCount; private string _origin; private ManualResetEvent _pongReceived; private bool _preAuth; private string _protocol; private string[] _protocols; private bool _protocolsRequested; private NetworkCredential _proxyCredentials; private Uri _proxyUri; private volatile WebSocketState _readyState; private ManualResetEvent _receivingExited; private int _retryCountForConnect; private bool _secure; private ClientSslConfiguration _sslConfig; private Stream _stream; private TcpClient _tcpClient; private Uri _uri; private const string _version = "13"; private TimeSpan _waitTime; #endregion #region Internal Fields /// /// Represents the empty array of used internally. /// internal static readonly byte[] EmptyBytes; /// /// Represents the length used to determine whether the data should be fragmented in sending. /// /// /// /// The data will be fragmented if that length is greater than the value of this field. /// /// /// If you would like to change the value, you must set it to a value between 125 and /// Int32.MaxValue - 14 inclusive. /// /// internal static readonly int FragmentLength; /// /// Represents the random number generator used internally. /// internal static readonly RandomNumberGenerator RandomNumber; #endregion #region Static Constructor static WebSocket () { _maxRetryCountForConnect = 10; EmptyBytes = new byte[0]; FragmentLength = 1016; RandomNumber = new RNGCryptoServiceProvider (); } #endregion #region Internal Constructors // As server internal WebSocket (HttpListenerWebSocketContext context, string protocol) { _context = context; _protocol = protocol; _closeContext = context.Close; _logger = context.Log; _message = messages; _secure = context.IsSecureConnection; _stream = context.Stream; _waitTime = TimeSpan.FromSeconds (1); init (); } // As server internal WebSocket (TcpListenerWebSocketContext context, string protocol) { _context = context; _protocol = protocol; _closeContext = context.Close; _logger = context.Log; _message = messages; _secure = context.IsSecureConnection; _stream = context.Stream; _waitTime = TimeSpan.FromSeconds (1); init (); } #endregion #region Public Constructors /// /// Initializes a new instance of the class with /// and optionally . /// /// /// /// A that specifies the URL to which to connect. /// /// /// The scheme of the URL must be ws or wss. /// /// /// The new instance uses a secure connection if the scheme is wss. /// /// /// /// /// An array of that specifies the names of /// the subprotocols if necessary. /// /// /// Each value of the array must be a token defined in /// /// RFC 2616. /// /// /// /// is . /// /// /// /// is an empty string. /// /// /// -or- /// /// /// is an invalid WebSocket URL string. /// /// /// -or- /// /// /// contains a value that is not a token. /// /// /// -or- /// /// /// contains a value twice. /// /// public WebSocket (string url, params string[] protocols) { if (url == null) throw new ArgumentNullException ("url"); if (url.Length == 0) throw new ArgumentException ("An empty string.", "url"); string msg; if (!url.TryCreateWebSocketUri (out _uri, out msg)) throw new ArgumentException (msg, "url"); if (protocols != null && protocols.Length > 0) { if (!checkProtocols (protocols, out msg)) throw new ArgumentException (msg, "protocols"); _protocols = protocols; } _base64Key = CreateBase64Key (); _client = true; _logger = new Logger (); _message = messagec; _secure = _uri.Scheme == "wss"; _waitTime = TimeSpan.FromSeconds (5); init (); } #endregion #region Internal Properties internal CookieCollection CookieCollection { get { return _cookies; } } // As server internal Func CustomHandshakeRequestChecker { get { return _handshakeRequestChecker; } set { _handshakeRequestChecker = value; } } internal bool HasMessage { get { lock (_forMessageEventQueue) return _messageEventQueue.Count > 0; } } // As server internal bool IgnoreExtensions { get { return _ignoreExtensions; } set { _ignoreExtensions = 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 a message. /// /// /// The set operation does nothing if the connection has already been /// established or it is closing. /// /// /// /// One of the enum values. /// /// /// It specifies the compression method used to compress a message. /// /// /// The default value is . /// /// /// /// The set operation is not available if this instance is not a client. /// public CompressionMethod Compression { get { return _compression; } set { string msg = null; if (!_client) { msg = "This instance is not a client."; throw new InvalidOperationException (msg); } if (!canSet (out msg)) { _logger.Warn (msg); return; } lock (_forState) { if (!canSet (out msg)) { _logger.Warn (msg); return; } _compression = value; } } } /// /// Gets the HTTP cookies included in the handshake request/response. /// /// /// /// An /// instance. /// /// /// It 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 /// used to authenticate the client. /// /// /// The default value is . /// /// public NetworkCredential Credentials { get { return _credentials; } } /// /// Gets or sets a value indicating whether a event /// is emitted when a ping is received. /// /// /// /// true if this instance emits a event /// when receives a ping; otherwise, false. /// /// /// The default value is false. /// /// public bool EmitOnPing { get { return _emitOnPing; } set { _emitOnPing = value; } } /// /// Gets or sets a value indicating whether the URL redirection for /// the handshake request is allowed. /// /// /// The set operation does nothing if the connection has already been /// established or it is closing. /// /// /// /// true if this instance allows the URL redirection for /// the handshake request; otherwise, false. /// /// /// The default value is false. /// /// /// /// The set operation is not available if this instance is not a client. /// public bool EnableRedirection { get { return _enableRedirection; } set { string msg = null; if (!_client) { msg = "This instance is not a client."; throw new InvalidOperationException (msg); } if (!canSet (out msg)) { _logger.Warn (msg); return; } lock (_forState) { if (!canSet (out msg)) { _logger.Warn (msg); return; } _enableRedirection = value; } } } /// /// Gets the extensions selected by server. /// /// /// A that will be a list of the extensions /// negotiated between client and server, or an empty string if /// not specified or selected. /// public string Extensions { get { return _extensions ?? String.Empty; } } /// /// Gets a value indicating whether the connection is alive. /// /// /// The get operation returns the value by using a ping/pong /// if the current state of the connection is Open. /// /// /// true if the connection is alive; otherwise, false. /// public bool IsAlive { get { return ping (EmptyBytes); } } /// /// Gets a value indicating whether a secure connection is used. /// /// /// true if this instance uses a secure connection; otherwise, /// false. /// public bool IsSecure { get { return _secure; } } /// /// Gets the logging function. /// /// /// The default logging level is . /// /// /// A that provides the logging function. /// public Logger Log { get { return _logger; } internal set { _logger = value; } } /// /// Gets or sets the value of the HTTP Origin header to send with /// the handshake request. /// /// /// /// The HTTP Origin header is defined in /// /// Section 7 of RFC 6454. /// /// /// This instance sends the Origin header if this property has any. /// /// /// The set operation does nothing if the connection has already been /// established or it is closing. /// /// /// /// /// A that represents the value of the Origin /// header to send. /// /// /// The syntax is <scheme>://<host>[:<port>]. /// /// /// The default value is . /// /// /// /// The set operation is not available if this instance is not a client. /// /// /// /// The value specified for a set operation is not an absolute URI string. /// /// /// -or- /// /// /// The value specified for a set operation includes the path segments. /// /// public string Origin { get { return _origin; } set { string msg = null; if (!_client) { msg = "This instance is not a client."; throw new InvalidOperationException (msg); } if (!value.IsNullOrEmpty ()) { Uri uri; if (!Uri.TryCreate (value, UriKind.Absolute, out uri)) { msg = "Not an absolute URI string."; throw new ArgumentException (msg, "value"); } if (uri.Segments.Length > 1) { msg = "It includes the path segments."; throw new ArgumentException (msg, "value"); } } if (!canSet (out msg)) { _logger.Warn (msg); return; } lock (_forState) { if (!canSet (out msg)) { _logger.Warn (msg); return; } _origin = !value.IsNullOrEmpty () ? value.TrimEnd ('/') : value; } } } /// /// Gets the name of subprotocol selected by the server. /// /// /// /// A that will be one of the names of /// subprotocols specified by client. /// /// /// An empty string if not specified or selected. /// /// public string Protocol { get { return _protocol ?? String.Empty; } internal set { _protocol = value; } } /// /// Gets the current state of the connection. /// /// /// /// One of the enum values. /// /// /// It indicates the current state of the connection. /// /// /// The default value is . /// /// public WebSocketState ReadyState { get { return _readyState; } } /// /// Gets the configuration for secure connection. /// /// /// This configuration will be referenced when attempts to connect, /// so it must be configured before any connect method is called. /// /// /// A that represents /// the configuration used to establish a secure connection. /// /// /// /// This instance is not a client. /// /// /// This instance does not use a secure connection. /// /// public ClientSslConfiguration SslConfiguration { get { if (!_client) { var msg = "This instance is not a client."; throw new InvalidOperationException (msg); } if (!_secure) { var msg = "This instance does not use a secure connection."; throw new InvalidOperationException (msg); } return getSslConfiguration (); } } /// /// Gets the URL to which to connect. /// /// /// A that represents the URL to which to connect. /// public Uri Url { get { return _client ? _uri : _context.RequestUri; } } /// /// Gets or sets the time to wait for the response to the ping or close. /// /// /// The set operation does nothing if the connection has already been /// established or it is closing. /// /// /// /// A to wait for the response. /// /// /// The default value is the same as 5 seconds if this instance is /// a client. /// /// /// /// The value specified for a set operation is zero or less. /// public TimeSpan WaitTime { get { return _waitTime; } set { if (value <= TimeSpan.Zero) throw new ArgumentOutOfRangeException ("value", "Zero or less."); string msg; if (!canSet (out msg)) { _logger.Warn (msg); return; } lock (_forState) { if (!canSet (out msg)) { _logger.Warn (msg); return; } _waitTime = 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 // As server private bool accept () { if (_readyState == WebSocketState.Open) { var msg = "The handshake request has already been accepted."; _logger.Warn (msg); return false; } lock (_forState) { if (_readyState == WebSocketState.Open) { var msg = "The handshake request has already been accepted."; _logger.Warn (msg); return false; } if (_readyState == WebSocketState.Closing) { var msg = "The close process has set in."; _logger.Error (msg); msg = "An interruption has occurred while attempting to accept."; error (msg, null); return false; } if (_readyState == WebSocketState.Closed) { var msg = "The connection has been closed."; _logger.Error (msg); msg = "An interruption has occurred while attempting to accept."; error (msg, null); return false; } try { if (!acceptHandshake ()) return false; } catch (Exception ex) { _logger.Fatal (ex.Message); _logger.Debug (ex.ToString ()); var msg = "An exception has occurred while attempting to accept."; fatal (msg, ex); return false; } _readyState = WebSocketState.Open; return true; } } // As server private bool acceptHandshake () { _logger.Debug ( String.Format ( "A handshake request from {0}:\n{1}", _context.UserEndPoint, _context ) ); string msg; if (!checkHandshakeRequest (_context, out msg)) { _logger.Error (msg); refuseHandshake ( CloseStatusCode.ProtocolError, "A handshake error has occurred while attempting to accept." ); return false; } if (!customCheckHandshakeRequest (_context, out msg)) { _logger.Error (msg); refuseHandshake ( CloseStatusCode.PolicyViolation, "A handshake error has occurred while attempting to accept." ); return false; } _base64Key = _context.Headers["Sec-WebSocket-Key"]; if (_protocol != null) { var vals = _context.SecWebSocketProtocols; processSecWebSocketProtocolClientHeader (vals); } if (!_ignoreExtensions) { var val = _context.Headers["Sec-WebSocket-Extensions"]; processSecWebSocketExtensionsClientHeader (val); } return sendHttpResponse (createHandshakeResponse ()); } private bool canSet (out string message) { message = null; if (_readyState == WebSocketState.Open) { message = "The connection has already been established."; return false; } if (_readyState == WebSocketState.Closing) { message = "The connection is closing."; return false; } return true; } // As server private bool checkHandshakeRequest ( WebSocketContext context, out string message ) { message = null; if (!context.IsWebSocketRequest) { message = "Not a handshake request."; return false; } if (context.RequestUri == null) { message = "It specifies an invalid Request-URI."; return false; } var headers = context.Headers; var key = headers["Sec-WebSocket-Key"]; if (key == null) { message = "It includes no Sec-WebSocket-Key header."; return false; } if (key.Length == 0) { message = "It includes an invalid Sec-WebSocket-Key header."; return false; } var version = headers["Sec-WebSocket-Version"]; if (version == null) { message = "It includes no Sec-WebSocket-Version header."; return false; } if (version != _version) { message = "It includes an invalid Sec-WebSocket-Version header."; return false; } var protocol = headers["Sec-WebSocket-Protocol"]; if (protocol != null && protocol.Length == 0) { message = "It includes an invalid Sec-WebSocket-Protocol header."; return false; } if (!_ignoreExtensions) { var extensions = headers["Sec-WebSocket-Extensions"]; if (extensions != null && extensions.Length == 0) { message = "It includes an invalid Sec-WebSocket-Extensions header."; return false; } } return true; } // As client private bool checkHandshakeResponse (HttpResponse response, out string message) { message = null; if (response.IsRedirect) { message = "Indicates the redirection."; return false; } if (response.IsUnauthorized) { message = "Requires the authentication."; return false; } if (!response.IsWebSocketResponse) { message = "Not a WebSocket handshake response."; return false; } var headers = response.Headers; if (!validateSecWebSocketAcceptHeader (headers["Sec-WebSocket-Accept"])) { message = "Includes no Sec-WebSocket-Accept header, or it has an invalid value."; return false; } if (!validateSecWebSocketProtocolServerHeader (headers["Sec-WebSocket-Protocol"])) { message = "Includes no Sec-WebSocket-Protocol header, or it has an invalid value."; return false; } if (!validateSecWebSocketExtensionsServerHeader (headers["Sec-WebSocket-Extensions"])) { message = "Includes an invalid Sec-WebSocket-Extensions header."; return false; } if (!validateSecWebSocketVersionServerHeader (headers["Sec-WebSocket-Version"])) { message = "Includes an invalid Sec-WebSocket-Version header."; return false; } return true; } private static bool checkProtocols (string[] protocols, out string message) { message = null; Func cond = protocol => protocol.IsNullOrEmpty () || !protocol.IsToken (); if (protocols.Contains (cond)) { message = "It contains a value that is not a token."; return false; } if (protocols.ContainsTwice ()) { message = "It contains a value twice."; return false; } return true; } private bool checkReceivedFrame (WebSocketFrame frame, out string message) { message = null; var masked = frame.IsMasked; if (_client && masked) { message = "A frame from the server is masked."; return false; } if (!_client && !masked) { message = "A frame from a client is not masked."; return false; } if (_inContinuation && frame.IsData) { message = "A data frame has been received while receiving continuation frames."; return false; } if (frame.IsCompressed && _compression == CompressionMethod.None) { message = "A compressed frame has been received without any agreement for it."; return false; } if (frame.Rsv2 == Rsv.On) { message = "The RSV2 of a frame is non-zero without any negotiation for it."; return false; } if (frame.Rsv3 == Rsv.On) { message = "The RSV3 of a frame is non-zero without any negotiation for it."; return false; } return true; } private void close (ushort code, string reason) { if (_readyState == WebSocketState.Closing) { _logger.Info ("The closing is already in progress."); return; } if (_readyState == WebSocketState.Closed) { _logger.Info ("The connection has already been closed."); return; } if (code == 1005) { // == no status close (PayloadData.Empty, true, true, false); return; } var send = !code.IsReserved (); close (new PayloadData (code, reason), send, send, false); } private void close ( PayloadData payloadData, bool send, bool receive, bool received ) { lock (_forState) { if (_readyState == WebSocketState.Closing) { _logger.Info ("The closing is already in progress."); return; } if (_readyState == WebSocketState.Closed) { _logger.Info ("The connection has already been closed."); return; } send = send && _readyState == WebSocketState.Open; receive = send && receive; _readyState = WebSocketState.Closing; } _logger.Trace ("Begin closing the connection."); var res = closeHandshake (payloadData, send, receive, received); releaseResources (); _logger.Trace ("End closing the connection."); _readyState = WebSocketState.Closed; var e = new CloseEventArgs (payloadData, res); try { OnClose.Emit (this, e); } catch (Exception ex) { _logger.Error (ex.Message); _logger.Debug (ex.ToString ()); } } private void closeAsync (ushort code, string reason) { if (_readyState == WebSocketState.Closing) { _logger.Info ("The closing is already in progress."); return; } if (_readyState == WebSocketState.Closed) { _logger.Info ("The connection has already been closed."); return; } if (code == 1005) { // == no status closeAsync (PayloadData.Empty, true, true, false); return; } var send = !code.IsReserved (); closeAsync (new PayloadData (code, reason), send, send, false); } private void closeAsync ( PayloadData payloadData, bool send, bool receive, bool received ) { Action closer = close; closer.BeginInvoke ( payloadData, send, receive, received, ar => closer.EndInvoke (ar), null ); } private bool closeHandshake (byte[] frameAsBytes, bool receive, bool received) { var sent = frameAsBytes != null && sendBytes (frameAsBytes); var wait = !received && sent && receive && _receivingExited != null; if (wait) received = _receivingExited.WaitOne (_waitTime); var ret = sent && received; _logger.Debug ( String.Format ( "Was clean?: {0}\n sent: {1}\n received: {2}", ret, sent, received ) ); return ret; } private bool closeHandshake ( PayloadData payloadData, bool send, bool receive, bool received ) { var sent = false; if (send) { var frame = WebSocketFrame.CreateCloseFrame (payloadData, _client); sent = sendBytes (frame.ToArray ()); if (_client) frame.Unmask (); } var wait = !received && sent && receive && _receivingExited != null; if (wait) received = _receivingExited.WaitOne (_waitTime); var ret = sent && received; _logger.Debug ( String.Format ( "Was clean?: {0}\n sent: {1}\n received: {2}", ret, sent, received ) ); return ret; } // As client private bool connect () { if (_readyState == WebSocketState.Open) { var msg = "The connection has already been established."; _logger.Warn (msg); return false; } lock (_forState) { if (_readyState == WebSocketState.Open) { var msg = "The connection has already been established."; _logger.Warn (msg); return false; } if (_readyState == WebSocketState.Closing) { var msg = "The close process has set in."; _logger.Error (msg); msg = "An interruption has occurred while attempting to connect."; error (msg, null); return false; } if (_retryCountForConnect > _maxRetryCountForConnect) { var msg = "An opportunity for reconnecting has been lost."; _logger.Error (msg); msg = "An interruption has occurred while attempting to connect."; error (msg, null); return false; } _readyState = WebSocketState.Connecting; try { doHandshake (); } catch (Exception ex) { _retryCountForConnect++; _logger.Fatal (ex.Message); _logger.Debug (ex.ToString ()); var msg = "An exception has occurred while attempting to connect."; fatal (msg, ex); return false; } _retryCountForConnect = 1; _readyState = WebSocketState.Open; return true; } } // As client private string createExtensions () { var buff = new StringBuilder (80); if (_compression != CompressionMethod.None) { var str = _compression.ToExtensionString ( "server_no_context_takeover", "client_no_context_takeover"); buff.AppendFormat ("{0}, ", str); } var len = buff.Length; if (len > 2) { buff.Length = len - 2; return buff.ToString (); } return null; } // As server private HttpResponse createHandshakeFailureResponse (HttpStatusCode code) { var ret = HttpResponse.CreateCloseResponse (code); ret.Headers["Sec-WebSocket-Version"] = _version; return ret; } // As client private HttpRequest createHandshakeRequest () { var ret = HttpRequest.CreateWebSocketRequest (_uri); var headers = ret.Headers; if (!_origin.IsNullOrEmpty ()) headers["Origin"] = _origin; headers["Sec-WebSocket-Key"] = _base64Key; _protocolsRequested = _protocols != null; if (_protocolsRequested) headers["Sec-WebSocket-Protocol"] = _protocols.ToString (", "); _extensionsRequested = _compression != CompressionMethod.None; if (_extensionsRequested) headers["Sec-WebSocket-Extensions"] = createExtensions (); 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) ret.SetCookies (_cookies); return ret; } // As server private HttpResponse createHandshakeResponse () { var ret = HttpResponse.CreateWebSocketResponse (); var headers = ret.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) ret.SetCookies (_cookies); return ret; } // As server private bool customCheckHandshakeRequest ( WebSocketContext context, out string message ) { message = null; if (_handshakeRequestChecker == null) return true; message = _handshakeRequestChecker (context); return message == null; } private MessageEventArgs dequeueFromMessageEventQueue () { lock (_forMessageEventQueue) return _messageEventQueue.Count > 0 ? _messageEventQueue.Dequeue () : null; } // As client private void doHandshake () { setClientStream (); var res = sendHandshakeRequest (); string msg; if (!checkHandshakeResponse (res, out msg)) throw new WebSocketException (CloseStatusCode.ProtocolError, msg); if (_protocolsRequested) _protocol = res.Headers["Sec-WebSocket-Protocol"]; if (_extensionsRequested) processSecWebSocketExtensionsServerHeader (res.Headers["Sec-WebSocket-Extensions"]); processCookies (res.Cookies); } private void enqueueToMessageEventQueue (MessageEventArgs e) { lock (_forMessageEventQueue) _messageEventQueue.Enqueue (e); } private void error (string message, Exception exception) { try { OnError.Emit (this, new ErrorEventArgs (message, exception)); } catch (Exception ex) { _logger.Error (ex.Message); _logger.Debug (ex.ToString ()); } } private void fatal (string message, Exception exception) { var code = exception is WebSocketException ? ((WebSocketException) exception).Code : CloseStatusCode.Abnormal; fatal (message, (ushort) code); } private void fatal (string message, ushort code) { var payload = new PayloadData (code, message); close (payload, !code.IsReserved (), false, false); } private void fatal (string message, CloseStatusCode code) { fatal (message, (ushort) code); } private ClientSslConfiguration getSslConfiguration () { if (_sslConfig == null) _sslConfig = new ClientSslConfiguration (_uri.DnsSafeHost); return _sslConfig; } private void init () { _compression = CompressionMethod.None; _cookies = new CookieCollection (); _forPing = new object (); _forSend = new object (); _forState = new object (); _messageEventQueue = new Queue (); _forMessageEventQueue = ((ICollection) _messageEventQueue).SyncRoot; _readyState = WebSocketState.Connecting; } private void message () { MessageEventArgs e = null; lock (_forMessageEventQueue) { if (_inMessage || _messageEventQueue.Count == 0 || _readyState != WebSocketState.Open) return; _inMessage = true; e = _messageEventQueue.Dequeue (); } _message (e); } private void messagec (MessageEventArgs e) { do { try { OnMessage.Emit (this, e); } catch (Exception ex) { _logger.Error (ex.ToString ()); error ("An error has occurred during an OnMessage event.", ex); } lock (_forMessageEventQueue) { if (_messageEventQueue.Count == 0 || _readyState != WebSocketState.Open) { _inMessage = false; break; } e = _messageEventQueue.Dequeue (); } } while (true); } private void messages (MessageEventArgs e) { try { OnMessage.Emit (this, e); } catch (Exception ex) { _logger.Error (ex.ToString ()); error ("An error has occurred during an OnMessage event.", ex); } lock (_forMessageEventQueue) { if (_messageEventQueue.Count == 0 || _readyState != WebSocketState.Open) { _inMessage = false; return; } e = _messageEventQueue.Dequeue (); } ThreadPool.QueueUserWorkItem (state => messages (e)); } private void open () { _inMessage = true; startReceiving (); try { OnOpen.Emit (this, EventArgs.Empty); } catch (Exception ex) { _logger.Error (ex.ToString ()); error ("An error has occurred during the OnOpen event.", ex); } MessageEventArgs e = null; lock (_forMessageEventQueue) { if (_messageEventQueue.Count == 0 || _readyState != WebSocketState.Open) { _inMessage = false; return; } e = _messageEventQueue.Dequeue (); } Task.Run(() => _message.Invoke(e)); } private bool ping (byte[] data) { if (_readyState != WebSocketState.Open) return false; var pongReceived = _pongReceived; if (pongReceived == null) return false; lock (_forPing) { try { pongReceived.Reset (); if (!send (Fin.Final, Opcode.Ping, data, false)) return false; return pongReceived.WaitOne (_waitTime); } catch (ObjectDisposedException) { return false; } } } private bool processCloseFrame (WebSocketFrame frame) { var payload = frame.PayloadData; close (payload, !payload.HasReservedCode, false, true); return false; } // As client private void processCookies (CookieCollection cookies) { if (cookies.Count == 0) return; _cookies.SetOrRemove (cookies); } private bool processDataFrame (WebSocketFrame frame) { enqueueToMessageEventQueue ( frame.IsCompressed ? new MessageEventArgs ( frame.Opcode, frame.PayloadData.ApplicationData.Decompress (_compression)) : new MessageEventArgs (frame)); return true; } private bool processFragmentFrame (WebSocketFrame frame) { if (!_inContinuation) { // Must process first fragment. if (frame.IsContinuation) return true; _fragmentsOpcode = frame.Opcode; _fragmentsCompressed = frame.IsCompressed; _fragmentsBuffer = new MemoryStream (); _inContinuation = true; } _fragmentsBuffer.WriteBytes (frame.PayloadData.ApplicationData, 1024); if (frame.IsFinal) { using (_fragmentsBuffer) { var data = _fragmentsCompressed ? _fragmentsBuffer.DecompressToArray (_compression) : _fragmentsBuffer.ToArray (); enqueueToMessageEventQueue (new MessageEventArgs (_fragmentsOpcode, data)); } _fragmentsBuffer = null; _inContinuation = false; } return true; } private bool processPingFrame (WebSocketFrame frame) { _logger.Trace ("A ping was received."); var pong = WebSocketFrame.CreatePongFrame (frame.PayloadData, _client); lock (_forState) { if (_readyState != WebSocketState.Open) { _logger.Error ("The connection is closing."); return true; } if (!sendBytes (pong.ToArray ())) return false; } _logger.Trace ("A pong to this ping has been sent."); if (_emitOnPing) { if (_client) pong.Unmask (); enqueueToMessageEventQueue (new MessageEventArgs (frame)); } return true; } private bool processPongFrame (WebSocketFrame frame) { _logger.Trace ("A pong was received."); try { _pongReceived.Set (); } catch (NullReferenceException ex) { _logger.Error (ex.Message); _logger.Debug (ex.ToString ()); return false; } catch (ObjectDisposedException ex) { _logger.Error (ex.Message); _logger.Debug (ex.ToString ()); return false; } _logger.Trace ("It has been signaled."); return true; } private bool processReceivedFrame (WebSocketFrame frame) { string msg; if (!checkReceivedFrame (frame, out msg)) throw new WebSocketException (CloseStatusCode.ProtocolError, msg); frame.Unmask (); return frame.IsFragment ? processFragmentFrame (frame) : frame.IsData ? processDataFrame (frame) : frame.IsPing ? processPingFrame (frame) : frame.IsPong ? processPongFrame (frame) : frame.IsClose ? processCloseFrame (frame) : processUnsupportedFrame (frame); } // As server private void processSecWebSocketExtensionsClientHeader (string value) { if (value == null) return; var buff = new StringBuilder (80); var comp = false; foreach (var elm in value.SplitHeaderValue (',')) { var extension = elm.Trim (); if (extension.Length == 0) continue; if (!comp) { if (extension.IsCompressionExtension (CompressionMethod.Deflate)) { _compression = CompressionMethod.Deflate; buff.AppendFormat ( "{0}, ", _compression.ToExtensionString ( "client_no_context_takeover", "server_no_context_takeover" ) ); comp = true; } } } var len = buff.Length; if (len <= 2) return; buff.Length = len - 2; _extensions = buff.ToString (); } // As client private void processSecWebSocketExtensionsServerHeader (string value) { if (value == null) { _compression = CompressionMethod.None; return; } _extensions = value; } // As server private void processSecWebSocketProtocolClientHeader ( IEnumerable values ) { if (values.Contains (val => val == _protocol)) return; _protocol = null; } private bool processUnsupportedFrame (WebSocketFrame frame) { _logger.Fatal ("An unsupported frame:" + frame.PrintToString (false)); fatal ("There is no way to handle it.", CloseStatusCode.PolicyViolation); return false; } // As server private void refuseHandshake (CloseStatusCode code, string reason) { _readyState = WebSocketState.Closing; var res = createHandshakeFailureResponse (HttpStatusCode.BadRequest); sendHttpResponse (res); releaseServerResources (); _readyState = WebSocketState.Closed; var e = new CloseEventArgs ((ushort) code, reason, false); try { OnClose.Emit (this, e); } catch (Exception ex) { _logger.Error (ex.Message); _logger.Debug (ex.ToString ()); } } // As client private void releaseClientResources () { if (_stream != null) { _stream.Dispose (); _stream = null; } if (_tcpClient != null) { _tcpClient.Close (); _tcpClient = null; } } private void releaseCommonResources () { if (_fragmentsBuffer != null) { _fragmentsBuffer.Dispose (); _fragmentsBuffer = null; _inContinuation = false; } if (_pongReceived != null) { _pongReceived.Close (); _pongReceived = null; } if (_receivingExited != null) { _receivingExited.Close (); _receivingExited = null; } } private void releaseResources () { if (_client) releaseClientResources (); else releaseServerResources (); releaseCommonResources (); } // As server private void releaseServerResources () { if (_closeContext == null) return; _closeContext (); _closeContext = null; _stream = null; _context = null; } private bool send (Opcode opcode, Stream stream) { lock (_forSend) { var src = stream; var compressed = false; var sent = false; try { if (_compression != CompressionMethod.None) { stream = stream.Compress (_compression); compressed = true; } sent = send (opcode, stream, compressed); if (!sent) error ("A send has been interrupted.", null); } catch (Exception ex) { _logger.Error (ex.ToString ()); error ("An error has occurred during a send.", ex); } finally { if (compressed) stream.Dispose (); src.Dispose (); } return sent; } } private bool send (Opcode opcode, Stream stream, bool compressed) { var len = stream.Length; if (len == 0) return send (Fin.Final, opcode, EmptyBytes, false); var quo = len / FragmentLength; var rem = (int) (len % FragmentLength); byte[] buff = null; if (quo == 0) { buff = new byte[rem]; return stream.Read (buff, 0, rem) == rem && send (Fin.Final, opcode, buff, compressed); } if (quo == 1 && rem == 0) { buff = new byte[FragmentLength]; return stream.Read (buff, 0, FragmentLength) == FragmentLength && send (Fin.Final, opcode, buff, compressed); } /* Send fragments */ // Begin buff = new byte[FragmentLength]; var sent = stream.Read (buff, 0, FragmentLength) == FragmentLength && send (Fin.More, opcode, buff, compressed); if (!sent) return false; var n = rem == 0 ? quo - 2 : quo - 1; for (long i = 0; i < n; i++) { sent = stream.Read (buff, 0, FragmentLength) == FragmentLength && send (Fin.More, Opcode.Cont, buff, false); if (!sent) return false; } // End if (rem == 0) rem = FragmentLength; else buff = new byte[rem]; return stream.Read (buff, 0, rem) == rem && send (Fin.Final, Opcode.Cont, buff, false); } private bool send (Fin fin, Opcode opcode, byte[] data, bool compressed) { lock (_forState) { if (_readyState != WebSocketState.Open) { _logger.Error ("The connection is closing."); return false; } var frame = new WebSocketFrame (fin, opcode, data, compressed, _client); return sendBytes (frame.ToArray ()); } } 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.Error (ex.ToString ()); error ( "An error has occurred during the callback for an async send.", ex ); } }, null ); } private bool sendBytes (byte[] bytes) { try { _stream.Write (bytes, 0, bytes.Length); } catch (Exception ex) { _logger.Error (ex.Message); _logger.Debug (ex.ToString ()); return false; } return true; } // As client private HttpResponse sendHandshakeRequest () { var req = createHandshakeRequest (); var res = sendHttpRequest (req, 90000); if (res.IsUnauthorized) { var chal = res.Headers["WWW-Authenticate"]; _logger.Warn (String.Format ("Received an authentication requirement for '{0}'.", chal)); if (chal.IsNullOrEmpty ()) { _logger.Error ("No authentication challenge is specified."); return res; } _authChallenge = AuthenticationChallenge.Parse (chal); if (_authChallenge == null) { _logger.Error ("An invalid authentication challenge is specified."); return res; } if (_credentials != null && (!_preAuth || _authChallenge.Scheme == AuthenticationSchemes.Digest)) { if (res.HasConnectionClose) { releaseClientResources (); setClientStream (); } var authRes = new AuthenticationResponse (_authChallenge, _credentials, _nonceCount); _nonceCount = authRes.NonceCount; req.Headers["Authorization"] = authRes.ToString (); res = sendHttpRequest (req, 15000); } } if (res.IsRedirect) { var url = res.Headers["Location"]; _logger.Warn (String.Format ("Received a redirection to '{0}'.", url)); if (_enableRedirection) { if (url.IsNullOrEmpty ()) { _logger.Error ("No url to redirect is located."); return res; } Uri uri; string msg; if (!url.TryCreateWebSocketUri (out uri, out msg)) { _logger.Error ("An invalid url to redirect is located: " + msg); return res; } releaseClientResources (); _uri = uri; _secure = uri.Scheme == "wss"; setClientStream (); return sendHandshakeRequest (); } } return res; } // As client private HttpResponse sendHttpRequest (HttpRequest request, int millisecondsTimeout) { _logger.Debug ("A request to the server:\n" + request.ToString ()); var res = request.GetResponse (_stream, millisecondsTimeout); _logger.Debug ("A response to this request:\n" + res.ToString ()); return res; } // As server private bool sendHttpResponse (HttpResponse response) { _logger.Debug ( String.Format ( "A response to {0}:\n{1}", _context.UserEndPoint, response ) ); return sendBytes (response.ToByteArray ()); } // As client private void sendProxyConnectRequest () { var req = HttpRequest.CreateConnectRequest (_uri); var res = sendHttpRequest (req, 90000); if (res.IsProxyAuthenticationRequired) { var chal = res.Headers["Proxy-Authenticate"]; _logger.Warn ( String.Format ("Received a proxy authentication requirement for '{0}'.", chal)); if (chal.IsNullOrEmpty ()) throw new WebSocketException ("No proxy authentication challenge is specified."); var authChal = AuthenticationChallenge.Parse (chal); if (authChal == null) throw new WebSocketException ("An invalid proxy authentication challenge is specified."); if (_proxyCredentials != null) { if (res.HasConnectionClose) { releaseClientResources (); _tcpClient = new TcpClient (_proxyUri.DnsSafeHost, _proxyUri.Port); _stream = _tcpClient.GetStream (); } var authRes = new AuthenticationResponse (authChal, _proxyCredentials, 0); req.Headers["Proxy-Authorization"] = authRes.ToString (); res = sendHttpRequest (req, 15000); } if (res.IsProxyAuthenticationRequired) throw new WebSocketException ("A proxy authentication is required."); } if (res.StatusCode[0] != '2') throw new WebSocketException ( "The proxy has failed a connection to the requested host and port."); } // As client private void setClientStream () { if (_proxyUri != null) { _tcpClient = new TcpClient (_proxyUri.DnsSafeHost, _proxyUri.Port); _stream = _tcpClient.GetStream (); sendProxyConnectRequest (); } else { _tcpClient = new TcpClient (_uri.DnsSafeHost, _uri.Port); _stream = _tcpClient.GetStream (); } if (_secure) { var conf = getSslConfiguration (); var host = conf.TargetHost; if (host != _uri.DnsSafeHost) throw new WebSocketException ( CloseStatusCode.TlsHandshakeFailure, "An invalid host name is specified."); try { var sslStream = new SslStream ( _stream, false, conf.ServerCertificateValidationCallback, conf.ClientCertificateSelectionCallback); sslStream.AuthenticateAsClient ( host, conf.ClientCertificates, conf.EnabledSslProtocols, conf.CheckCertificateRevocation); _stream = sslStream; } catch (Exception ex) { throw new WebSocketException (CloseStatusCode.TlsHandshakeFailure, ex); } } } private void startReceiving () { if (_messageEventQueue.Count > 0) _messageEventQueue.Clear (); _pongReceived = new ManualResetEvent (false); _receivingExited = new ManualResetEvent (false); Action receive = null; receive = () => WebSocketFrame.ReadFrameAsync ( _stream, false, frame => { if (!processReceivedFrame (frame) || _readyState == WebSocketState.Closed) { var exited = _receivingExited; if (exited != null) exited.Set (); return; } // Receive next asap because the Ping or Close needs a response to it. receive (); if (_inMessage || !HasMessage || _readyState != WebSocketState.Open) return; message (); }, ex => { _logger.Fatal (ex.ToString ()); fatal ("An exception has occurred while receiving.", ex); } ); receive (); } // As client private bool validateSecWebSocketAcceptHeader (string value) { return value != null && value == CreateResponseKey (_base64Key); } // As client private bool validateSecWebSocketExtensionsServerHeader (string value) { if (value == null) return true; if (value.Length == 0) return false; if (!_extensionsRequested) return false; var comp = _compression != CompressionMethod.None; foreach (var e in value.SplitHeaderValue (',')) { var ext = e.Trim (); if (comp && ext.IsCompressionExtension (_compression)) { if (!ext.Contains ("server_no_context_takeover")) { _logger.Error ("The server hasn't sent back 'server_no_context_takeover'."); return false; } if (!ext.Contains ("client_no_context_takeover")) _logger.Warn ("The server hasn't sent back 'client_no_context_takeover'."); var method = _compression.ToExtensionString (); var invalid = ext.SplitHeaderValue (';').Contains ( t => { t = t.Trim (); return t != method && t != "server_no_context_takeover" && t != "client_no_context_takeover"; } ); if (invalid) return false; } else { return false; } } return true; } // As client private bool validateSecWebSocketProtocolServerHeader (string value) { if (value == null) return !_protocolsRequested; if (value.Length == 0) return false; return _protocolsRequested && _protocols.Contains (p => p == value); } // As client private bool validateSecWebSocketVersionServerHeader (string value) { return value == null || value == _version; } #endregion #region Internal Methods // As server internal void Close (HttpResponse response) { _readyState = WebSocketState.Closing; sendHttpResponse (response); releaseServerResources (); _readyState = WebSocketState.Closed; } // As server internal void Close (HttpStatusCode code) { Close (createHandshakeFailureResponse (code)); } // As server internal void Close (PayloadData payloadData, byte[] frameAsBytes) { lock (_forState) { if (_readyState == WebSocketState.Closing) { _logger.Info ("The closing is already in progress."); return; } if (_readyState == WebSocketState.Closed) { _logger.Info ("The connection has already been closed."); return; } _readyState = WebSocketState.Closing; } _logger.Trace ("Begin closing the connection."); var sent = frameAsBytes != null && sendBytes (frameAsBytes); var received = sent && _receivingExited != null ? _receivingExited.WaitOne (_waitTime) : false; var res = sent && received; _logger.Debug ( String.Format ( "Was clean?: {0}\n sent: {1}\n received: {2}", res, sent, received ) ); releaseServerResources (); releaseCommonResources (); _logger.Trace ("End closing the connection."); _readyState = WebSocketState.Closed; var e = new CloseEventArgs (payloadData, res); try { OnClose.Emit (this, e); } catch (Exception ex) { _logger.Error (ex.Message); _logger.Debug (ex.ToString ()); } } // As client internal static string CreateBase64Key () { var src = new byte[16]; RandomNumber.GetBytes (src); return Convert.ToBase64String (src); } internal static string CreateResponseKey (string base64Key) { var buff = new StringBuilder (base64Key, 64); buff.Append (_guid); SHA1 sha1 = new SHA1CryptoServiceProvider (); var src = sha1.ComputeHash (buff.ToString ().GetUTF8EncodedBytes ()); return Convert.ToBase64String (src); } // As server internal void InternalAccept () { try { if (!acceptHandshake ()) return; } catch (Exception ex) { _logger.Fatal (ex.Message); _logger.Debug (ex.ToString ()); var msg = "An exception has occurred while attempting to accept."; fatal (msg, ex); return; } _readyState = WebSocketState.Open; open (); } // As server internal bool Ping (byte[] frameAsBytes, TimeSpan timeout) { if (_readyState != WebSocketState.Open) return false; var pongReceived = _pongReceived; if (pongReceived == null) return false; lock (_forPing) { try { pongReceived.Reset (); lock (_forState) { if (_readyState != WebSocketState.Open) return false; if (!sendBytes (frameAsBytes)) return false; } return pongReceived.WaitOne (timeout); } catch (ObjectDisposedException) { return false; } } } // As server internal void Send ( Opcode opcode, byte[] data, Dictionary cache ) { lock (_forSend) { lock (_forState) { if (_readyState != WebSocketState.Open) { _logger.Error ("The connection is closing."); return; } byte[] found; if (!cache.TryGetValue (_compression, out found)) { found = new WebSocketFrame ( Fin.Final, opcode, data.Compress (_compression), _compression != CompressionMethod.None, false ) .ToArray (); cache.Add (_compression, found); } sendBytes (found); } } } // As server internal void Send ( Opcode opcode, Stream stream, Dictionary cache ) { lock (_forSend) { Stream found; if (!cache.TryGetValue (_compression, out found)) { found = stream.Compress (_compression); cache.Add (_compression, found); } else { found.Position = 0; } send (opcode, found, _compression != CompressionMethod.None); } } #endregion #region Public Methods /// /// Accepts the handshake request. /// /// /// This method does nothing if the handshake request has already been /// accepted. /// /// /// /// This instance is a client. /// /// /// -or- /// /// /// The close process is in progress. /// /// /// -or- /// /// /// The connection has already been closed. /// /// public void Accept () { if (_client) { var msg = "This instance is a client."; throw new InvalidOperationException (msg); } if (_readyState == WebSocketState.Closing) { var msg = "The close process is in progress."; throw new InvalidOperationException (msg); } if (_readyState == WebSocketState.Closed) { var msg = "The connection has already been closed."; throw new InvalidOperationException (msg); } if (accept ()) open (); } /// /// Accepts the handshake request asynchronously. /// /// /// /// This method does not wait for the accept process to be complete. /// /// /// This method does nothing if the handshake request has already been /// accepted. /// /// /// /// /// This instance is a client. /// /// /// -or- /// /// /// The close process is in progress. /// /// /// -or- /// /// /// The connection has already been closed. /// /// public void AcceptAsync () { if (_client) { var msg = "This instance is a client."; throw new InvalidOperationException (msg); } if (_readyState == WebSocketState.Closing) { var msg = "The close process is in progress."; throw new InvalidOperationException (msg); } if (_readyState == WebSocketState.Closed) { var msg = "The connection has already been closed."; throw new InvalidOperationException (msg); } Func acceptor = accept; acceptor.BeginInvoke ( ar => { if (acceptor.EndInvoke (ar)) open (); }, null ); } /// /// Closes the connection. /// /// /// This method does nothing if the current state of the connection is /// Closing or Closed. /// public void Close () { close (1005, String.Empty); } /// /// Closes the connection with the specified code. /// /// /// This method does nothing if the current state of the connection is /// Closing or Closed. /// /// /// /// A that represents the status code indicating /// the reason for the close. /// /// /// The status codes are defined in /// /// Section 7.4 of RFC 6455. /// /// /// /// is less than 1000 or greater than 4999. /// /// /// /// is 1011 (server error). /// It cannot be used by clients. /// /// /// -or- /// /// /// is 1010 (mandatory extension). /// It cannot be used by servers. /// /// public void Close (ushort code) { if (!code.IsCloseStatusCode ()) { var msg = "Less than 1000 or greater than 4999."; throw new ArgumentOutOfRangeException ("code", msg); } if (_client && code == 1011) { var msg = "1011 cannot be used."; throw new ArgumentException (msg, "code"); } if (!_client && code == 1010) { var msg = "1010 cannot be used."; throw new ArgumentException (msg, "code"); } close (code, String.Empty); } /// /// Closes the connection with the specified code. /// /// /// This method does nothing if the current state of the connection is /// Closing or Closed. /// /// /// /// One of the enum values. /// /// /// It represents the status code indicating the reason for the close. /// /// /// /// /// is /// . /// It cannot be used by clients. /// /// /// -or- /// /// /// is /// . /// It cannot be used by servers. /// /// public void Close (CloseStatusCode code) { if (_client && code == CloseStatusCode.ServerError) { var msg = "ServerError cannot be used."; throw new ArgumentException (msg, "code"); } if (!_client && code == CloseStatusCode.MandatoryExtension) { var msg = "MandatoryExtension cannot be used."; throw new ArgumentException (msg, "code"); } close ((ushort) code, String.Empty); } /// /// Closes the connection with the specified code and reason. /// /// /// This method does nothing if the current state of the connection is /// Closing or Closed. /// /// /// /// A that represents the status code indicating /// the reason for the close. /// /// /// The status codes are defined in /// /// Section 7.4 of RFC 6455. /// /// /// /// /// A that represents the reason for the close. /// /// /// The size must be 123 bytes or less in UTF-8. /// /// /// /// /// is less than 1000 or greater than 4999. /// /// /// -or- /// /// /// The size of is greater than 123 bytes. /// /// /// /// /// is 1011 (server error). /// It cannot be used by clients. /// /// /// -or- /// /// /// is 1010 (mandatory extension). /// It cannot be used by servers. /// /// /// -or- /// /// /// is 1005 (no status) and there is reason. /// /// /// -or- /// /// /// could not be UTF-8-encoded. /// /// public void Close (ushort code, string reason) { if (!code.IsCloseStatusCode ()) { var msg = "Less than 1000 or greater than 4999."; throw new ArgumentOutOfRangeException ("code", msg); } if (_client && code == 1011) { var msg = "1011 cannot be used."; throw new ArgumentException (msg, "code"); } if (!_client && code == 1010) { var msg = "1010 cannot be used."; throw new ArgumentException (msg, "code"); } if (reason.IsNullOrEmpty ()) { close (code, String.Empty); return; } if (code == 1005) { var msg = "1005 cannot be used."; throw new ArgumentException (msg, "code"); } byte[] bytes; if (!reason.TryGetUTF8EncodedBytes (out bytes)) { var msg = "It could not be UTF-8-encoded."; throw new ArgumentException (msg, "reason"); } if (bytes.Length > 123) { var msg = "Its size is greater than 123 bytes."; throw new ArgumentOutOfRangeException ("reason", msg); } close (code, reason); } /// /// Closes the connection with the specified code and reason. /// /// /// This method does nothing if the current state of the connection is /// Closing or Closed. /// /// /// /// One of the enum values. /// /// /// It represents the status code indicating the reason for the close. /// /// /// /// /// A that represents the reason for the close. /// /// /// The size must be 123 bytes or less in UTF-8. /// /// /// /// /// is /// . /// It cannot be used by clients. /// /// /// -or- /// /// /// is /// . /// It cannot be used by servers. /// /// /// -or- /// /// /// is /// and there is reason. /// /// /// -or- /// /// /// could not be UTF-8-encoded. /// /// /// /// The size of is greater than 123 bytes. /// public void Close (CloseStatusCode code, string reason) { if (_client && code == CloseStatusCode.ServerError) { var msg = "ServerError cannot be used."; throw new ArgumentException (msg, "code"); } if (!_client && code == CloseStatusCode.MandatoryExtension) { var msg = "MandatoryExtension cannot be used."; throw new ArgumentException (msg, "code"); } if (reason.IsNullOrEmpty ()) { close ((ushort) code, String.Empty); return; } if (code == CloseStatusCode.NoStatus) { var msg = "NoStatus cannot be used."; throw new ArgumentException (msg, "code"); } byte[] bytes; if (!reason.TryGetUTF8EncodedBytes (out bytes)) { var msg = "It could not be UTF-8-encoded."; throw new ArgumentException (msg, "reason"); } if (bytes.Length > 123) { var msg = "Its size is greater than 123 bytes."; throw new ArgumentOutOfRangeException ("reason", msg); } close ((ushort) code, reason); } /// /// Closes the connection asynchronously. /// /// /// /// This method does not wait for the close to be complete. /// /// /// This method does nothing if the current state of the connection is /// Closing or Closed. /// /// public void CloseAsync () { closeAsync (1005, String.Empty); } /// /// Closes the connection asynchronously with the specified code. /// /// /// /// This method does not wait for the close to be complete. /// /// /// This method does nothing if the current state of the connection is /// Closing or Closed. /// /// /// /// /// A that represents the status code indicating /// the reason for the close. /// /// /// The status codes are defined in /// /// Section 7.4 of RFC 6455. /// /// /// /// is less than 1000 or greater than 4999. /// /// /// /// is 1011 (server error). /// It cannot be used by clients. /// /// /// -or- /// /// /// is 1010 (mandatory extension). /// It cannot be used by servers. /// /// public void CloseAsync (ushort code) { if (!code.IsCloseStatusCode ()) { var msg = "Less than 1000 or greater than 4999."; throw new ArgumentOutOfRangeException ("code", msg); } if (_client && code == 1011) { var msg = "1011 cannot be used."; throw new ArgumentException (msg, "code"); } if (!_client && code == 1010) { var msg = "1010 cannot be used."; throw new ArgumentException (msg, "code"); } closeAsync (code, String.Empty); } /// /// Closes the connection asynchronously with the specified code. /// /// /// /// This method does not wait for the close to be complete. /// /// /// This method does nothing if the current state of the connection is /// Closing or Closed. /// /// /// /// /// One of the enum values. /// /// /// It represents the status code indicating the reason for the close. /// /// /// /// /// is /// . /// It cannot be used by clients. /// /// /// -or- /// /// /// is /// . /// It cannot be used by servers. /// /// public void CloseAsync (CloseStatusCode code) { if (_client && code == CloseStatusCode.ServerError) { var msg = "ServerError cannot be used."; throw new ArgumentException (msg, "code"); } if (!_client && code == CloseStatusCode.MandatoryExtension) { var msg = "MandatoryExtension cannot be used."; throw new ArgumentException (msg, "code"); } closeAsync ((ushort) code, String.Empty); } /// /// Closes the connection asynchronously with the specified code and reason. /// /// /// /// This method does not wait for the close to be complete. /// /// /// This method does nothing if the current state of the connection is /// Closing or Closed. /// /// /// /// /// A that represents the status code indicating /// the reason for the close. /// /// /// The status codes are defined in /// /// Section 7.4 of RFC 6455. /// /// /// /// /// A that represents the reason for the close. /// /// /// The size must be 123 bytes or less in UTF-8. /// /// /// /// /// is less than 1000 or greater than 4999. /// /// /// -or- /// /// /// The size of is greater than 123 bytes. /// /// /// /// /// is 1011 (server error). /// It cannot be used by clients. /// /// /// -or- /// /// /// is 1010 (mandatory extension). /// It cannot be used by servers. /// /// /// -or- /// /// /// is 1005 (no status) and there is reason. /// /// /// -or- /// /// /// could not be UTF-8-encoded. /// /// public void CloseAsync (ushort code, string reason) { if (!code.IsCloseStatusCode ()) { var msg = "Less than 1000 or greater than 4999."; throw new ArgumentOutOfRangeException ("code", msg); } if (_client && code == 1011) { var msg = "1011 cannot be used."; throw new ArgumentException (msg, "code"); } if (!_client && code == 1010) { var msg = "1010 cannot be used."; throw new ArgumentException (msg, "code"); } if (reason.IsNullOrEmpty ()) { closeAsync (code, String.Empty); return; } if (code == 1005) { var msg = "1005 cannot be used."; throw new ArgumentException (msg, "code"); } byte[] bytes; if (!reason.TryGetUTF8EncodedBytes (out bytes)) { var msg = "It could not be UTF-8-encoded."; throw new ArgumentException (msg, "reason"); } if (bytes.Length > 123) { var msg = "Its size is greater than 123 bytes."; throw new ArgumentOutOfRangeException ("reason", msg); } closeAsync (code, reason); } /// /// Closes the connection asynchronously with the specified code and reason. /// /// /// /// This method does not wait for the close to be complete. /// /// /// This method does nothing if the current state of the connection is /// Closing or Closed. /// /// /// /// /// One of the enum values. /// /// /// It represents the status code indicating the reason for the close. /// /// /// /// /// A that represents the reason for the close. /// /// /// The size must be 123 bytes or less in UTF-8. /// /// /// /// /// is /// . /// It cannot be used by clients. /// /// /// -or- /// /// /// is /// . /// It cannot be used by servers. /// /// /// -or- /// /// /// is /// and there is reason. /// /// /// -or- /// /// /// could not be UTF-8-encoded. /// /// /// /// The size of is greater than 123 bytes. /// public void CloseAsync (CloseStatusCode code, string reason) { if (_client && code == CloseStatusCode.ServerError) { var msg = "ServerError cannot be used."; throw new ArgumentException (msg, "code"); } if (!_client && code == CloseStatusCode.MandatoryExtension) { var msg = "MandatoryExtension cannot be used."; throw new ArgumentException (msg, "code"); } if (reason.IsNullOrEmpty ()) { closeAsync ((ushort) code, String.Empty); return; } if (code == CloseStatusCode.NoStatus) { var msg = "NoStatus cannot be used."; throw new ArgumentException (msg, "code"); } byte[] bytes; if (!reason.TryGetUTF8EncodedBytes (out bytes)) { var msg = "It could not be UTF-8-encoded."; throw new ArgumentException (msg, "reason"); } if (bytes.Length > 123) { var msg = "Its size is greater than 123 bytes."; throw new ArgumentOutOfRangeException ("reason", msg); } closeAsync ((ushort) code, reason); } /// /// Establishes a connection. /// /// /// This method does nothing if the connection has already been established. /// /// /// /// This instance is not a client. /// /// /// -or- /// /// /// The close process is in progress. /// /// /// -or- /// /// /// A series of reconnecting has failed. /// /// public void Connect () { if (!_client) { var msg = "This instance is not a client."; throw new InvalidOperationException (msg); } if (_readyState == WebSocketState.Closing) { var msg = "The close process is in progress."; throw new InvalidOperationException (msg); } if (_retryCountForConnect > _maxRetryCountForConnect) { var msg = "A series of reconnecting has failed."; throw new InvalidOperationException (msg); } if (connect ()) open (); } /// /// Establishes a connection asynchronously. /// /// /// /// This method does not wait for the connect process to be complete. /// /// /// This method does nothing if the connection has already been /// established. /// /// /// /// /// This instance is not a client. /// /// /// -or- /// /// /// The close process is in progress. /// /// /// -or- /// /// /// A series of reconnecting has failed. /// /// public void ConnectAsync () { if (!_client) { var msg = "This instance is not a client."; throw new InvalidOperationException (msg); } if (_readyState == WebSocketState.Closing) { var msg = "The close process is in progress."; throw new InvalidOperationException (msg); } if (_retryCountForConnect > _maxRetryCountForConnect) { var msg = "A series of reconnecting has failed."; throw new InvalidOperationException (msg); } Func connector = connect; connector.BeginInvoke ( ar => { if (connector.EndInvoke (ar)) open (); }, null ); } /// /// Sends a ping using the WebSocket connection. /// /// /// true if the send has done with no error and a pong has been /// received within a time; otherwise, false. /// public bool Ping () { return ping (EmptyBytes); } /// /// Sends a ping with using the WebSocket /// connection. /// /// /// true if the send has done with no error and a pong has been /// received within a time; otherwise, false. /// /// /// /// A that represents the message to send. /// /// /// The size must be 125 bytes or less in UTF-8. /// /// /// /// could not be UTF-8-encoded. /// /// /// The size of is greater than 125 bytes. /// public bool Ping (string message) { if (message.IsNullOrEmpty ()) return ping (EmptyBytes); byte[] bytes; if (!message.TryGetUTF8EncodedBytes (out bytes)) { var msg = "It could not be UTF-8-encoded."; throw new ArgumentException (msg, "message"); } if (bytes.Length > 125) { var msg = "Its size is greater than 125 bytes."; throw new ArgumentOutOfRangeException ("message", msg); } return ping (bytes); } /// /// Sends the specified data using the WebSocket connection. /// /// /// An array of that represents the binary data to send. /// /// /// The current state of the connection is not Open. /// /// /// is . /// public void Send (byte[] data) { if (_readyState != WebSocketState.Open) { var msg = "The current state of the connection is not Open."; throw new InvalidOperationException (msg); } if (data == null) throw new ArgumentNullException ("data"); send (Opcode.Binary, new MemoryStream (data)); } /// /// Sends the specified file using the WebSocket connection. /// /// /// /// A that specifies the file to send. /// /// /// The file is sent as the binary data. /// /// /// /// The current state of the connection is not Open. /// /// /// is . /// /// /// /// The file does not exist. /// /// /// -or- /// /// /// The file could not be opened. /// /// public void Send (FileInfo fileInfo) { if (_readyState != WebSocketState.Open) { var msg = "The current state of the connection is not Open."; throw new InvalidOperationException (msg); } if (fileInfo == null) throw new ArgumentNullException ("fileInfo"); if (!fileInfo.Exists) { var msg = "The file does not exist."; throw new ArgumentException (msg, "fileInfo"); } FileStream stream; if (!fileInfo.TryOpenRead (out stream)) { var msg = "The file could not be opened."; throw new ArgumentException (msg, "fileInfo"); } send (Opcode.Binary, stream); } /// /// Sends the specified data using the WebSocket connection. /// /// /// A that represents the text data to send. /// /// /// The current state of the connection is not Open. /// /// /// is . /// /// /// could not be UTF-8-encoded. /// public void Send (string data) { if (_readyState != WebSocketState.Open) { var msg = "The current state of the connection is not Open."; throw new InvalidOperationException (msg); } if (data == null) throw new ArgumentNullException ("data"); byte[] bytes; if (!data.TryGetUTF8EncodedBytes (out bytes)) { var msg = "It could not be UTF-8-encoded."; throw new ArgumentException (msg, "data"); } send (Opcode.Text, new MemoryStream (bytes)); } /// /// Sends the data from the specified stream using the WebSocket connection. /// /// /// /// A instance from which to read the data to send. /// /// /// The data is sent as the binary data. /// /// /// /// An that specifies the number of bytes to send. /// /// /// The current state of the connection is not Open. /// /// /// is . /// /// /// /// cannot be read. /// /// /// -or- /// /// /// is less than 1. /// /// /// -or- /// /// /// No data could be read from . /// /// public void Send (Stream stream, int length) { if (_readyState != WebSocketState.Open) { var msg = "The current state of the connection is not Open."; throw new InvalidOperationException (msg); } if (stream == null) throw new ArgumentNullException ("stream"); if (!stream.CanRead) { var msg = "It cannot be read."; throw new ArgumentException (msg, "stream"); } if (length < 1) { var msg = "Less than 1."; throw new ArgumentException (msg, "length"); } var bytes = stream.ReadBytes (length); var len = bytes.Length; if (len == 0) { var msg = "No data could be read from it."; throw new ArgumentException (msg, "stream"); } if (len < length) { _logger.Warn ( String.Format ( "Only {0} byte(s) of data could be read from the stream.", len ) ); } send (Opcode.Binary, new MemoryStream (bytes)); } /// /// Sends the specified data asynchronously using the WebSocket connection. /// /// /// This method does not wait for the send to be complete. /// /// /// An array of that represents the binary data to send. /// /// /// /// An Action<bool> delegate or /// if not needed. /// /// /// The delegate invokes the method called when the send is complete. /// /// /// true is passed to the method if the send has done with /// no error; otherwise, false. /// /// /// /// The current state of the connection is not Open. /// /// /// is . /// public void SendAsync (byte[] data, Action completed) { if (_readyState != WebSocketState.Open) { var msg = "The current state of the connection is not Open."; throw new InvalidOperationException (msg); } if (data == null) throw new ArgumentNullException ("data"); sendAsync (Opcode.Binary, new MemoryStream (data), completed); } /// /// Sends the specified file asynchronously using the WebSocket connection. /// /// /// This method does not wait for the send to be complete. /// /// /// /// A that specifies the file to send. /// /// /// The file is sent as the binary data. /// /// /// /// /// An Action<bool> delegate or /// if not needed. /// /// /// The delegate invokes the method called when the send is complete. /// /// /// true is passed to the method if the send has done with /// no error; otherwise, false. /// /// /// /// The current state of the connection is not Open. /// /// /// is . /// /// /// /// The file does not exist. /// /// /// -or- /// /// /// The file could not be opened. /// /// public void SendAsync (FileInfo fileInfo, Action completed) { if (_readyState != WebSocketState.Open) { var msg = "The current state of the connection is not Open."; throw new InvalidOperationException (msg); } if (fileInfo == null) throw new ArgumentNullException ("fileInfo"); if (!fileInfo.Exists) { var msg = "The file does not exist."; throw new ArgumentException (msg, "fileInfo"); } FileStream stream; if (!fileInfo.TryOpenRead (out stream)) { var msg = "The file could not be opened."; throw new ArgumentException (msg, "fileInfo"); } sendAsync (Opcode.Binary, stream, completed); } /// /// Sends the specified data asynchronously using the WebSocket connection. /// /// /// This method does not wait for the send to be complete. /// /// /// A that represents the text data to send. /// /// /// /// An Action<bool> delegate or /// if not needed. /// /// /// The delegate invokes the method called when the send is complete. /// /// /// true is passed to the method if the send has done with /// no error; otherwise, false. /// /// /// /// The current state of the connection is not Open. /// /// /// is . /// /// /// could not be UTF-8-encoded. /// public void SendAsync (string data, Action completed) { if (_readyState != WebSocketState.Open) { var msg = "The current state of the connection is not Open."; throw new InvalidOperationException (msg); } if (data == null) throw new ArgumentNullException ("data"); byte[] bytes; if (!data.TryGetUTF8EncodedBytes (out bytes)) { var msg = "It could not be UTF-8-encoded."; throw new ArgumentException (msg, "data"); } sendAsync (Opcode.Text, new MemoryStream (bytes), completed); } /// /// Sends the data from the specified stream asynchronously using /// the WebSocket connection. /// /// /// This method does not wait for the send to be complete. /// /// /// /// A instance from which to read the data to send. /// /// /// The data is sent as the binary data. /// /// /// /// An that specifies the number of bytes to send. /// /// /// /// An Action<bool> delegate or /// if not needed. /// /// /// The delegate invokes the method called when the send is complete. /// /// /// true is passed to the method if the send has done with /// no error; otherwise, false. /// /// /// /// The current state of the connection is not Open. /// /// /// is . /// /// /// /// cannot be read. /// /// /// -or- /// /// /// is less than 1. /// /// /// -or- /// /// /// No data could be read from . /// /// public void SendAsync (Stream stream, int length, Action completed) { if (_readyState != WebSocketState.Open) { var msg = "The current state of the connection is not Open."; throw new InvalidOperationException (msg); } if (stream == null) throw new ArgumentNullException ("stream"); if (!stream.CanRead) { var msg = "It cannot be read."; throw new ArgumentException (msg, "stream"); } if (length < 1) { var msg = "Less than 1."; throw new ArgumentException (msg, "length"); } var bytes = stream.ReadBytes (length); var len = bytes.Length; if (len == 0) { var msg = "No data could be read from it."; throw new ArgumentException (msg, "stream"); } if (len < length) { _logger.Warn ( String.Format ( "Only {0} byte(s) of data could be read from the stream.", len ) ); } sendAsync (Opcode.Binary, new MemoryStream (bytes), completed); } /// /// Sets an HTTP cookie to send with the handshake request. /// /// /// This method does nothing if the connection has already been /// established or it is closing. /// /// /// A that represents the cookie to send. /// /// /// This instance is not a client. /// /// /// is . /// public void SetCookie (Cookie cookie) { string msg = null; if (!_client) { msg = "This instance is not a client."; throw new InvalidOperationException (msg); } if (cookie == null) throw new ArgumentNullException ("cookie"); if (!canSet (out msg)) { _logger.Warn (msg); return; } lock (_forState) { if (!canSet (out msg)) { _logger.Warn (msg); return; } lock (_cookies.SyncRoot) _cookies.SetOrRemove (cookie); } } /// /// Sets the credentials for the HTTP authentication (Basic/Digest). /// /// /// This method does nothing if the connection has already been /// established or it is closing. /// /// /// /// A that represents the username associated with /// the credentials. /// /// /// or an empty string if initializes /// the credentials. /// /// /// /// /// A that represents the password for the username /// associated with the credentials. /// /// /// or an empty string if not necessary. /// /// /// /// true if sends the credentials for the Basic authentication in /// advance with the first handshake request; otherwise, false. /// /// /// This instance is not a client. /// /// /// /// contains an invalid character. /// /// /// -or- /// /// /// contains an invalid character. /// /// public void SetCredentials (string username, string password, bool preAuth) { string msg = null; if (!_client) { msg = "This instance is not a client."; throw new InvalidOperationException (msg); } if (!username.IsNullOrEmpty ()) { if (username.Contains (':') || !username.IsText ()) { msg = "It contains an invalid character."; throw new ArgumentException (msg, "username"); } } if (!password.IsNullOrEmpty ()) { if (!password.IsText ()) { msg = "It contains an invalid character."; throw new ArgumentException (msg, "password"); } } if (!canSet (out msg)) { _logger.Warn (msg); return; } lock (_forState) { if (!canSet (out msg)) { _logger.Warn (msg); return; } if (username.IsNullOrEmpty ()) { _credentials = null; _preAuth = false; return; } _credentials = new NetworkCredential ( username, password, _uri.PathAndQuery ); _preAuth = preAuth; } } /// /// Sets the URL of the HTTP proxy server through which to connect and /// the credentials for the HTTP proxy authentication (Basic/Digest). /// /// /// This method does nothing if the connection has already been /// established or it is closing. /// /// /// /// A that represents the URL of the proxy server /// through which to connect. /// /// /// The syntax is http://<host>[:<port>]. /// /// /// or an empty string if initializes the URL and /// the credentials. /// /// /// /// /// A that represents the username associated with /// the credentials. /// /// /// or an empty string if the credentials are not /// necessary. /// /// /// /// /// A that represents the password for the username /// associated with the credentials. /// /// /// or an empty string if not necessary. /// /// /// /// This instance is not a client. /// /// /// /// is not an absolute URI string. /// /// /// -or- /// /// /// The scheme of is not http. /// /// /// -or- /// /// /// includes the path segments. /// /// /// -or- /// /// /// contains an invalid character. /// /// /// -or- /// /// /// contains an invalid character. /// /// public void SetProxy (string url, string username, string password) { string msg = null; if (!_client) { msg = "This instance is not a client."; throw new InvalidOperationException (msg); } Uri uri = null; if (!url.IsNullOrEmpty ()) { if (!Uri.TryCreate (url, UriKind.Absolute, out uri)) { msg = "Not an absolute URI string."; throw new ArgumentException (msg, "url"); } if (uri.Scheme != "http") { msg = "The scheme part is not http."; throw new ArgumentException (msg, "url"); } if (uri.Segments.Length > 1) { msg = "It includes the path segments."; throw new ArgumentException (msg, "url"); } } if (!username.IsNullOrEmpty ()) { if (username.Contains (':') || !username.IsText ()) { msg = "It contains an invalid character."; throw new ArgumentException (msg, "username"); } } if (!password.IsNullOrEmpty ()) { if (!password.IsText ()) { msg = "It contains an invalid character."; throw new ArgumentException (msg, "password"); } } if (!canSet (out msg)) { _logger.Warn (msg); return; } lock (_forState) { if (!canSet (out msg)) { _logger.Warn (msg); return; } if (url.IsNullOrEmpty ()) { _proxyUri = null; _proxyCredentials = null; return; } _proxyUri = uri; _proxyCredentials = !username.IsNullOrEmpty () ? new NetworkCredential ( username, password, String.Format ( "{0}:{1}", _uri.DnsSafeHost, _uri.Port ) ) : null; } } #endregion #region Explicit Interface Implementations /// /// Closes the connection and releases all associated resources. /// /// /// /// This method closes the connection with close status 1001 (going away). /// /// /// And this method does nothing if the current state of the connection is /// Closing or Closed. /// /// void IDisposable.Dispose () { close (1001, String.Empty); } #endregion } }