websocket-sharp/websocket-sharp/WebSocket.cs
2014-08-21 20:48:37 +09:00

2220 lines
65 KiB
C#

#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
#region Contributors
/*
* Contributors:
* - Frank Razenberg <frank@zzattack.org>
*/
#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 WebSocketSharp.Net;
using WebSocketSharp.Net.WebSockets;
namespace WebSocketSharp
{
/// <summary>
/// Implements the WebSocket interface.
/// </summary>
/// <remarks>
/// The WebSocket class provides a set of methods and properties for two-way communication using
/// the WebSocket protocol (<see href="http://tools.ietf.org/html/rfc6455">RFC 6455</see>).
/// </remarks>
public class WebSocket : IDisposable
{
#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 NetworkCredential _credentials;
private string _extensions;
private AutoResetEvent _exitReceiving;
private object _forConn;
private object _forEvent;
private object _forMessageEventQueue;
private object _forSend;
private const string _guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
private Func<WebSocketContext, string>
_handshakeRequestChecker;
private volatile Logger _logger;
private Queue<MessageEventArgs> _messageEventQueue;
private uint _nonceCount;
private string _origin;
private bool _preAuth;
private string _protocol;
private string[] _protocols;
private NetworkCredential _proxyCredentials;
private Uri _proxyUri;
private volatile WebSocketState _readyState;
private AutoResetEvent _receivePong;
private bool _secure;
private Stream _stream;
private TcpClient _tcpClient;
private Uri _uri;
private const string _version = "13";
#endregion
#region Internal Fields
internal const int FragmentLength = 1016; // Max value is int.MaxValue - 14.
#endregion
#region Internal Constructors
// As server
internal WebSocket (HttpListenerWebSocketContext context, string protocol, Logger logger)
{
_context = context;
_protocol = protocol;
_logger = logger;
_closeContext = context.Close;
_secure = context.IsSecureConnection;
_stream = context.Stream;
init ();
}
// As server
internal WebSocket (TcpListenerWebSocketContext context, string protocol, Logger logger)
{
_context = context;
_protocol = protocol;
_logger = logger;
_closeContext = context.Close;
_secure = context.IsSecureConnection;
_stream = context.Stream;
init ();
}
#endregion
#region Public Constructors
/// <summary>
/// Initializes a new instance of the <see cref="WebSocket"/> class with the specified
/// WebSocket URL and subprotocols.
/// </summary>
/// <param name="url">
/// A <see cref="string"/> that represents the WebSocket URL to connect.
/// </param>
/// <param name="protocols">
/// An array of <see cref="string"/> that contains the WebSocket subprotocols if any.
/// Each value of <paramref name="protocols"/> must be a token defined in
/// <see href="http://tools.ietf.org/html/rfc2616#section-2.2">RFC 2616</see>.
/// </param>
/// <exception cref="ArgumentException">
/// <para>
/// <paramref name="url"/> is invalid.
/// </para>
/// <para>
/// -or-
/// </para>
/// <para>
/// <paramref name="protocols"/> is invalid.
/// </para>
/// </exception>
/// <exception cref="ArgumentNullException">
/// <paramref name="url"/> is <see langword="null"/>.
/// </exception>
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 CookieCollection CookieCollection {
get {
return _cookies;
}
}
// As server
internal Func<WebSocketContext, string> CustomHandshakeRequestChecker {
get {
return _handshakeRequestChecker ?? (context => null);
}
set {
_handshakeRequestChecker = value;
}
}
internal bool IsConnected {
get {
return _readyState == WebSocketState.Open || _readyState == WebSocketState.Closing;
}
}
#endregion
#region Public Properties
/// <summary>
/// Gets or sets the compression method used to compress the message on the WebSocket
/// connection.
/// </summary>
/// <value>
/// One of the <see cref="CompressionMethod"/> enum values, indicates the compression method
/// used to compress the message. The default value is <see cref="CompressionMethod.None"/>.
/// </value>
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 ("An error has occurred while setting the compression.");
return;
}
_compression = value;
}
}
}
/// <summary>
/// Gets the HTTP cookies included in the WebSocket connection request and response.
/// </summary>
/// <value>
/// An IEnumerable&lt;Cookie&gt; instance that provides an enumerator which supports the
/// iteration over the collection of the cookies.
/// </value>
public IEnumerable<Cookie> Cookies {
get {
lock (_cookies.SyncRoot)
foreach (Cookie cookie in _cookies)
yield return cookie;
}
}
/// <summary>
/// Gets the credentials for the HTTP authentication (Basic/Digest).
/// </summary>
/// <value>
/// A <see cref="NetworkCredential"/> that represents the credentials for the HTTP
/// authentication. The default value is <see langword="null"/>.
/// </value>
public NetworkCredential Credentials {
get {
return _credentials;
}
}
/// <summary>
/// Gets the WebSocket extensions selected by the server.
/// </summary>
/// <value>
/// A <see cref="string"/> that represents the extensions if any. The default value is
/// <see cref="String.Empty"/>.
/// </value>
public string Extensions {
get {
return _extensions ?? String.Empty;
}
}
/// <summary>
/// Gets a value indicating whether the WebSocket connection is alive.
/// </summary>
/// <value>
/// <c>true</c> if the connection is alive; otherwise, <c>false</c>.
/// </value>
public bool IsAlive {
get {
return Ping ();
}
}
/// <summary>
/// Gets a value indicating whether the WebSocket connection is secure.
/// </summary>
/// <value>
/// <c>true</c> if the connection is secure; otherwise, <c>false</c>.
/// </value>
public bool IsSecure {
get {
return _secure;
}
}
/// <summary>
/// Gets the logging functions.
/// </summary>
/// <remarks>
/// The default logging level is <see cref="LogLevel.Error"/>. If you would like to change it,
/// you should set the <c>Log.Level</c> property to any of the <see cref="LogLevel"/> enum
/// values.
/// </remarks>
/// <value>
/// A <see cref="Logger"/> that provides the logging functions.
/// </value>
public Logger Log {
get {
return _logger;
}
internal set {
_logger = value;
}
}
/// <summary>
/// Gets or sets the value of the Origin header to send with the WebSocket connection request
/// to the server.
/// </summary>
/// <remarks>
/// The <see cref="WebSocket"/> sends the Origin header if this property has any.
/// </remarks>
/// <value>
/// <para>
/// A <see cref="string"/> that represents the value of the
/// <see href="http://tools.ietf.org/html/rfc6454#section-7">HTTP Origin
/// header</see> to send. The default value is <see langword="null"/>.
/// </para>
/// <para>
/// The Origin header has the following syntax:
/// <c>&lt;scheme&gt;://&lt;host&gt;[:&lt;port&gt;]</c>
/// </para>
/// </value>
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 '<scheme>://<host>[:<port>]'.";
}
if (msg != null) {
_logger.Error (msg);
error ("An error has occurred while setting the origin.");
return;
}
_origin = value.TrimEnd ('/');
}
}
}
/// <summary>
/// Gets the WebSocket subprotocol selected by the server.
/// </summary>
/// <value>
/// A <see cref="string"/> that represents the subprotocol if any. The default value is
/// <see cref="String.Empty"/>.
/// </value>
public string Protocol {
get {
return _protocol ?? String.Empty;
}
internal set {
_protocol = value;
}
}
/// <summary>
/// Gets the state of the WebSocket connection.
/// </summary>
/// <value>
/// One of the <see cref="WebSocketState"/> enum values, indicates the state of the WebSocket
/// connection. The default value is <see cref="WebSocketState.Connecting"/>.
/// </value>
public WebSocketState ReadyState {
get {
return _readyState;
}
}
/// <summary>
/// Gets or sets the callback used to validate the certificate supplied by the server.
/// </summary>
/// <remarks>
/// If the value of this property is <see langword="null"/>, the validation does nothing with
/// the server certificate, always returns valid.
/// </remarks>
/// <value>
/// A <see cref="RemoteCertificateValidationCallback"/> delegate that references the method(s)
/// used to validate the server certificate. The default value is <see langword="null"/>.
/// </value>
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 (
"An error has occurred while setting the server certificate validation callback.");
return;
}
_certValidationCallback = value;
}
}
}
/// <summary>
/// Gets the WebSocket URL to connect.
/// </summary>
/// <value>
/// A <see cref="Uri"/> that represents the WebSocket URL to connect.
/// </value>
public Uri Url {
get {
return _client
? _uri
: _context.RequestUri;
}
}
#endregion
#region Public Events
/// <summary>
/// Occurs when the WebSocket connection has been closed.
/// </summary>
public event EventHandler<CloseEventArgs> OnClose;
/// <summary>
/// Occurs when the <see cref="WebSocket"/> gets an error.
/// </summary>
public event EventHandler<ErrorEventArgs> OnError;
/// <summary>
/// Occurs when the <see cref="WebSocket"/> receives a message.
/// </summary>
public event EventHandler<MessageEventArgs> OnMessage;
/// <summary>
/// Occurs when the WebSocket connection has been established.
/// </summary>
public event EventHandler OnOpen;
#endregion
#region Private Methods
// As server
private bool acceptHandshake ()
{
_logger.Debug (
String.Format (
"A WebSocket connection request from {0}:\n{1}", _context.UserEndPoint, _context));
var msg = checkIfValidHandshakeRequest (_context);
if (msg != null) {
_logger.Error (msg);
error ("An error has occurred while connecting.");
Close (HttpStatusCode.BadRequest);
return false;
}
if (_protocol != null &&
!_context.SecWebSocketProtocols.Contains (protocol => protocol == _protocol))
_protocol = null;
var extensions = _context.Headers["Sec-WebSocket-Extensions"];
if (extensions != null && extensions.Length > 0)
processSecWebSocketExtensionsHeader (extensions);
return sendHttpResponse (createHandshakeResponse ());
}
private string checkIfAvailable (
string operation, bool availableAsServer, bool availableAsConnected)
{
return !_client && !availableAsServer
? operation + " isn't available as a server."
: !availableAsConnected
? _readyState.CheckIfConnectable ()
: null;
}
private string checkIfCanConnect ()
{
return !_client && _readyState == WebSocketState.Closed
? "Connect isn't available to reconnect as a server."
: _readyState.CheckIfConnectable ();
}
// As server
private string checkIfValidHandshakeRequest (WebSocketContext context)
{
var headers = context.Headers;
return context.RequestUri == null
? "Invalid request url."
: !context.IsWebSocketRequest
? "Not WebSocket connection request."
: !validateSecWebSocketKeyHeader (headers["Sec-WebSocket-Key"])
? "Invalid Sec-WebSocket-Key header."
: !validateSecWebSocketVersionClientHeader (headers["Sec-WebSocket-Version"])
? "Invalid Sec-WebSocket-Version header."
: CustomHandshakeRequestChecker (context);
}
// As client
private string checkIfValidHandshakeResponse (HttpResponse response)
{
var headers = response.Headers;
return response.IsUnauthorized
? "HTTP authentication is required."
: !response.IsWebSocketResponse
? "Not WebSocket connection response."
: !validateSecWebSocketAcceptHeader (headers["Sec-WebSocket-Accept"])
? "Invalid Sec-WebSocket-Accept header."
: !validateSecWebSocketProtocolHeader (headers["Sec-WebSocket-Protocol"])
? "Invalid Sec-WebSocket-Protocol header."
: !validateSecWebSocketExtensionsHeader (headers["Sec-WebSocket-Extensions"])
? "Invalid Sec-WebSocket-Extensions header."
: !validateSecWebSocketVersionServerHeader (headers["Sec-WebSocket-Version"])
? "Invalid Sec-WebSocket-Version header."
: null;
}
private void close (CloseStatusCode code, string reason, bool wait)
{
close (new PayloadData (((ushort) code).Append (reason)), !code.IsReserved (), wait);
}
private void close (PayloadData payload, bool send, bool wait)
{
lock (_forConn) {
if (_readyState == WebSocketState.Closing || _readyState == WebSocketState.Closed) {
_logger.Info ("Closing the WebSocket connection has already been done.");
return;
}
_readyState = WebSocketState.Closing;
}
_logger.Trace ("Start closing handshake.");
var e = new CloseEventArgs (payload);
e.WasClean =
_client
? closeHandshake (
send ? WebSocketFrame.CreateCloseFrame (Mask.Mask, payload).ToByteArray () : null,
wait ? 5000 : 0,
closeClientResources)
: closeHandshake (
send ? WebSocketFrame.CreateCloseFrame (Mask.Unmask, payload).ToByteArray () : null,
wait ? 1000 : 0,
closeServerResources);
_logger.Trace ("End closing handshake.");
_readyState = WebSocketState.Closed;
try {
OnClose.Emit (this, e);
}
catch (Exception ex) {
_logger.Fatal (ex.ToString ());
error ("An exception has occurred while OnClose.", ex);
}
}
private void closeAsync (PayloadData payload, bool send, bool wait)
{
Action<PayloadData, bool, bool> 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 millisecondsTimeout, Action release)
{
var sent = frameAsBytes != null && sendBytes (frameAsBytes);
var received =
millisecondsTimeout == 0 ||
(sent && _exitReceiving != null && _exitReceiving.WaitOne (millisecondsTimeout));
release ();
if (_receivePong != null) {
_receivePong.Close ();
_receivePong = null;
}
if (_exitReceiving != null) {
_exitReceiving.Close ();
_exitReceiving = null;
}
var result = sent && received;
_logger.Debug (
String.Format ("Was clean?: {0}\nsent: {1} received: {2}", result, sent, received));
return result;
}
// As server
private void closeServerResources ()
{
if (_closeContext == null)
return;
_closeContext ();
_closeContext = null;
_stream = null;
_context = null;
}
private bool concatenateFragmentsInto (Stream dest)
{
while (true) {
var frame = WebSocketFrame.Read (_stream, true);
if (frame.IsFinal) {
/* FINAL */
// CONT
if (frame.IsContinuation) {
dest.WriteBytes (frame.PayloadData.ApplicationData);
break;
}
// PING
if (frame.IsPing) {
processPingFrame (frame);
continue;
}
// PONG
if (frame.IsPong) {
processPongFrame (frame);
continue;
}
// CLOSE
if (frame.IsClose)
return processCloseFrame (frame);
}
else {
/* MORE */
// CONT
if (frame.IsContinuation) {
dest.WriteBytes (frame.PayloadData.ApplicationData);
continue;
}
}
// ?
return processUnsupportedFrame (
frame,
CloseStatusCode.IncorrectData,
"An incorrect data has been received while receiving fragmented data.");
}
return true;
}
private bool connect ()
{
lock (_forConn) {
var msg = _readyState.CheckIfConnectable ();
if (msg != null) {
_logger.Error (msg);
error (msg);
return false;
}
try {
if (_client ? doHandshake () : acceptHandshake ()) {
_readyState = WebSocketState.Open;
return true;
}
}
catch (Exception ex) {
processException (ex, "An exception has occurred while connecting.");
}
return false;
}
}
// As client
private string createExtensions ()
{
var buff = new StringBuilder (32);
if (_compression != CompressionMethod.None)
buff.Append (_compression.ToExtensionString ());
return buff.Length > 0
? buff.ToString ()
: null;
}
// As server
private HttpResponse createHandshakeCloseResponse (HttpStatusCode code)
{
var res = HttpResponse.CreateCloseResponse (code);
res.Headers["Sec-WebSocket-Version"] = _version;
return res;
}
// As client
private HttpRequest createHandshakeRequest ()
{
var req = HttpRequest.CreateWebSocketRequest (_uri);
var headers = req.Headers;
if (!_origin.IsNullOrEmpty ())
headers["Origin"] = _origin;
headers["Sec-WebSocket-Key"] = _base64Key;
if (_protocols != null)
headers["Sec-WebSocket-Protocol"] = _protocols.ToString (", ");
var extensions = createExtensions ();
if (extensions != null)
headers["Sec-WebSocket-Extensions"] = extensions;
headers["Sec-WebSocket-Version"] = _version;
AuthenticationResponse authRes = null;
if (_authChallenge != null && _credentials != null) {
authRes = new AuthenticationResponse (_authChallenge, _credentials, _nonceCount);
_nonceCount = authRes.NonceCount;
}
else if (_preAuth) {
authRes = new AuthenticationResponse (_credentials);
}
if (authRes != null)
headers["Authorization"] = authRes.ToString ();
if (_cookies.Count > 0)
req.SetCookies (_cookies);
return req;
}
// As server
private HttpResponse createHandshakeResponse ()
{
var res = HttpResponse.CreateWebSocketResponse ();
var headers = res.Headers;
headers["Sec-WebSocket-Accept"] = CreateResponseKey (_base64Key);
if (_protocol != null)
headers["Sec-WebSocket-Protocol"] = _protocol;
if (_extensions != null)
headers["Sec-WebSocket-Extensions"] = _extensions;
if (_cookies.Count > 0)
res.SetCookies (_cookies);
return res;
}
private MessageEventArgs dequeueFromMessageEventQueue ()
{
lock (_forMessageEventQueue)
return _messageEventQueue.Count > 0
? _messageEventQueue.Dequeue ()
: null;
}
// As client
private bool doHandshake ()
{
setClientStream ();
var res = sendHandshakeRequest ();
var msg = checkIfValidHandshakeResponse (res);
if (msg != null) {
_logger.Error (msg);
msg = "An error has occurred while connecting.";
error (msg);
close (CloseStatusCode.Abnormal, msg, false);
return false;
}
var cookies = res.Cookies;
if (cookies.Count > 0)
_cookies.SetOrRemove (cookies);
return true;
}
private void enqueueToMessageEventQueue (MessageEventArgs e)
{
lock (_forMessageEventQueue)
_messageEventQueue.Enqueue (e);
}
private void error (string message)
{
error (message, null);
}
private void error (string message, Exception exception)
{
try {
OnError.Emit (this, new ErrorEventArgs (message, exception));
}
catch (Exception ex) {
_logger.Fatal ("An exception has occurred while OnError:\n" + ex.ToString ());
}
}
private void init ()
{
_compression = CompressionMethod.None;
_cookies = new CookieCollection ();
_forConn = new object ();
_forEvent = new object ();
_forSend = new object ();
_messageEventQueue = new Queue<MessageEventArgs> ();
_forMessageEventQueue = ((ICollection) _messageEventQueue).SyncRoot;
_readyState = WebSocketState.Connecting;
}
private void open ()
{
try {
startReceiving ();
lock (_forEvent) {
try {
OnOpen.Emit (this, EventArgs.Empty);
}
catch (Exception ex) {
processException (ex, "An exception has occurred while OnOpen.");
}
}
}
catch (Exception ex) {
processException (ex, "An exception has occurred while opening.");
}
}
private bool processCloseFrame (WebSocketFrame frame)
{
var payload = frame.PayloadData;
close (payload, !payload.ContainsReservedCloseStatusCode, false);
return false;
}
private bool processDataFrame (WebSocketFrame frame)
{
var e = frame.IsCompressed
? new MessageEventArgs (
frame.Opcode, frame.PayloadData.ApplicationData.Decompress (_compression))
: new MessageEventArgs (frame.Opcode, frame.PayloadData);
enqueueToMessageEventQueue (e);
return true;
}
private void processException (Exception exception, string message)
{
var code = CloseStatusCode.Abnormal;
var reason = message;
if (exception is WebSocketException) {
var wsex = (WebSocketException) exception;
code = wsex.Code;
reason = wsex.Message;
}
if (code == CloseStatusCode.Abnormal || code == CloseStatusCode.TlsHandshakeFailure)
_logger.Fatal (exception.ToString ());
else
_logger.Error (reason);
error (message ?? code.GetMessage (), exception);
if (_readyState == WebSocketState.Connecting && !_client)
Close (HttpStatusCode.BadRequest);
else
close (code, reason ?? code.GetMessage (), false);
}
private bool processFragmentedFrame (WebSocketFrame frame)
{
return frame.IsContinuation // Not first fragment
? true
: processFragments (frame);
}
private bool processFragments (WebSocketFrame first)
{
using (var buff = new MemoryStream ()) {
buff.WriteBytes (first.PayloadData.ApplicationData);
if (!concatenateFragmentsInto (buff))
return false;
byte[] data;
if (_compression != CompressionMethod.None) {
data = buff.DecompressToArray (_compression);
}
else {
buff.Close ();
data = buff.ToArray ();
}
enqueueToMessageEventQueue (new MessageEventArgs (first.Opcode, data));
return true;
}
}
private bool processPingFrame (WebSocketFrame frame)
{
var mask = _client ? Mask.Mask : Mask.Unmask;
if (send (WebSocketFrame.CreatePongFrame (mask, frame.PayloadData).ToByteArray ()))
_logger.Trace ("Returned a Pong.");
return true;
}
private bool processPongFrame (WebSocketFrame frame)
{
_receivePong.Set ();
_logger.Trace ("Received a Pong.");
return true;
}
// As server
private void processSecWebSocketExtensionsHeader (string value)
{
var buff = new StringBuilder (32);
var compress = false;
foreach (var extension in value.SplitHeaderValue (',')) {
var trimed = extension.Trim ();
var unprefixed = trimed.RemovePrefix ("x-webkit-");
if (!compress && unprefixed.IsCompressionExtension ()) {
var method = unprefixed.ToCompressionMethod ();
if (method != CompressionMethod.None) {
_compression = method;
compress = true;
buff.Append (trimed + ", ");
}
}
}
var len = buff.Length;
if (len > 0) {
buff.Length = len - 2;
_extensions = buff.ToString ();
}
}
private bool processUnsupportedFrame (WebSocketFrame frame, CloseStatusCode code, string reason)
{
_logger.Debug ("Unsupported frame:\n" + frame.PrintToString (false));
processException (new WebSocketException (code, reason), null);
return false;
}
private bool processWebSocketFrame (WebSocketFrame frame)
{
return frame.IsCompressed && _compression == CompressionMethod.None
? processUnsupportedFrame (
frame,
CloseStatusCode.IncorrectData,
"A compressed data has been received without available decompression method.")
: frame.IsFragmented
? processFragmentedFrame (frame)
: frame.IsData
? processDataFrame (frame)
: frame.IsPing
? processPingFrame (frame)
: frame.IsPong
? processPongFrame (frame)
: frame.IsClose
? processCloseFrame (frame)
: processUnsupportedFrame (frame, CloseStatusCode.PolicyViolation, null);
}
private bool send (byte[] frameAsBytes)
{
lock (_forConn) {
if (_readyState != WebSocketState.Open) {
_logger.Error ("Closing the WebSocket connection has been done.");
return false;
}
return sendBytes (frameAsBytes);
}
}
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, _client ? Mask.Mask : Mask.Unmask, stream, compressed);
if (!sent)
error ("Sending a data has been interrupted.");
}
catch (Exception ex) {
_logger.Fatal (ex.ToString ());
error ("An exception has occurred while sending a data.", ex);
}
finally {
if (compressed)
stream.Dispose ();
src.Dispose ();
}
return sent;
}
}
private bool send (Opcode opcode, Mask mask, Stream stream, bool compressed)
{
var len = stream.Length;
/* Not fragmented */
if (len == 0)
return send (Fin.Final, opcode, mask, new byte[0], compressed);
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, mask, buff, compressed);
}
buff = new byte[FragmentLength];
if (quo == 1 && rem == 0)
return stream.Read (buff, 0, FragmentLength) == FragmentLength &&
send (Fin.Final, opcode, mask, buff, compressed);
/* Send fragmented */
// Begin
if (stream.Read (buff, 0, FragmentLength) != FragmentLength ||
!send (Fin.More, opcode, mask, buff, compressed))
return false;
var n = rem == 0 ? quo - 2 : quo - 1;
for (long i = 0; i < n; i++)
if (stream.Read (buff, 0, FragmentLength) != FragmentLength ||
!send (Fin.More, Opcode.Cont, mask, buff, compressed))
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, mask, buff, compressed);
}
private bool send (Fin fin, Opcode opcode, Mask mask, byte[] data, bool compressed)
{
lock (_forConn) {
if (_readyState != WebSocketState.Open) {
_logger.Error ("Closing the WebSocket connection has been done.");
return false;
}
return sendBytes (
WebSocketFrame.CreateWebSocketFrame (fin, opcode, mask, data, compressed).ToByteArray ());
}
}
private void sendAsync (Opcode opcode, Stream stream, Action<bool> completed)
{
Func<Opcode, Stream, bool> 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.", ex);
}
},
null);
}
private bool sendBytes (byte[] data)
{
try {
_stream.Write (data, 0, data.Length);
return true;
}
catch (Exception ex) {
_logger.Fatal (ex.ToString ());
return false;
}
}
// As client
private HttpResponse sendHandshakeRequest ()
{
var req = createHandshakeRequest ();
var res = sendHttpRequest (req, 90000);
if (res.IsUnauthorized) {
_authChallenge = res.AuthenticationChallenge;
if (_credentials != null &&
(!_preAuth || _authChallenge.Scheme == AuthenticationSchemes.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 = sendHttpRequest (req, 15000);
}
}
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 ("A response to this request:\n" + response.ToString ());
return sendBytes (response.ToByteArray ());
}
// As client
private void sendProxyConnectRequest ()
{
var req = HttpRequest.CreateConnectRequest (_uri);
var res = sendHttpRequest (req, 90000);
if (res.IsProxyAuthenticationRequired) {
var authChal = res.ProxyAuthenticationChallenge;
if (authChal != null && _proxyCredentials != null) {
if (res.Headers.Contains ("Connection", "close")) {
closeClientResources ();
_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 ("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 sslStream = new SslStream (
_stream,
false,
_certValidationCallback ?? ((sender, certificate, chain, sslPolicyErrors) => true));
sslStream.AuthenticateAsClient (_uri.DnsSafeHost);
_stream = sslStream;
}
}
private void startReceiving ()
{
if (_messageEventQueue.Count > 0)
_messageEventQueue.Clear ();
_exitReceiving = new AutoResetEvent (false);
_receivePong = new AutoResetEvent (false);
Action receive = null;
receive = () => WebSocketFrame.ReadAsync (
_stream,
true,
frame => {
if (processWebSocketFrame (frame) && _readyState != WebSocketState.Closed) {
receive ();
if (!frame.IsData)
return;
lock (_forEvent) {
try {
var e = dequeueFromMessageEventQueue ();
if (e != null && _readyState == WebSocketState.Open)
OnMessage.Emit (this, e);
}
catch (Exception ex) {
processException (ex, "An exception has occurred while OnMessage.");
}
}
}
else if (_exitReceiving != null) {
_exitReceiving.Set ();
}
},
ex => processException (ex, "An exception has occurred while receiving a message."));
receive ();
}
// As client
private bool validateSecWebSocketAcceptHeader (string value)
{
return value != null && value == CreateResponseKey (_base64Key);
}
// As client
private bool validateSecWebSocketExtensionsHeader (string value)
{
var compress = _compression != CompressionMethod.None;
if (value == null || value.Length == 0) {
if (compress)
_compression = CompressionMethod.None;
return true;
}
if (!compress)
return false;
var extensions = value.SplitHeaderValue (',');
if (extensions.Contains (
extension => extension.Trim () != _compression.ToExtensionString ()))
return false;
_extensions = value;
return true;
}
// As server
private bool validateSecWebSocketKeyHeader (string value)
{
if (value == null || value.Length == 0)
return false;
_base64Key = value;
return true;
}
// As client
private bool validateSecWebSocketProtocolHeader (string value)
{
if (value == null)
return _protocols == null;
if (_protocols == null || !_protocols.Contains (protocol => protocol == value))
return false;
_protocol = value;
return true;
}
// As server
private bool validateSecWebSocketVersionClientHeader (string value)
{
return value != null && value == _version;
}
// As client
private bool validateSecWebSocketVersionServerHeader (string value)
{
return value == null || value == _version;
}
#endregion
#region Internal Methods
// As server
internal void Close (HttpResponse response)
{
_readyState = WebSocketState.Closing;
sendHttpResponse (response);
closeServerResources ();
_readyState = WebSocketState.Closed;
}
// As server
internal void Close (HttpStatusCode code)
{
Close (createHandshakeCloseResponse (code));
}
// As server
internal void Close (CloseEventArgs e, byte[] frameAsBytes, int millisecondsTimeout)
{
lock (_forConn) {
if (_readyState == WebSocketState.Closing || _readyState == WebSocketState.Closed) {
_logger.Info ("Closing the WebSocket connection has already been done.");
return;
}
_readyState = WebSocketState.Closing;
}
e.WasClean = closeHandshake (frameAsBytes, millisecondsTimeout, closeServerResources);
_readyState = WebSocketState.Closed;
try {
OnClose.Emit (this, e);
}
catch (Exception ex) {
_logger.Fatal ("An exception has occurred while OnClose:\n" + ex.ToString ());
}
}
// As server
internal void ConnectAsServer ()
{
try {
if (acceptHandshake ()) {
_readyState = WebSocketState.Open;
open ();
}
}
catch (Exception ex) {
processException (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 buff = new StringBuilder (base64Key, 64);
buff.Append (_guid);
SHA1 sha1 = new SHA1CryptoServiceProvider ();
var src = sha1.ComputeHash (Encoding.UTF8.GetBytes (buff.ToString ()));
return Convert.ToBase64String (src);
}
internal bool Ping (byte[] frameAsBytes, int millisecondsTimeout)
{
try {
AutoResetEvent pong;
return _readyState == WebSocketState.Open &&
send (frameAsBytes) &&
(pong = _receivePong) != null &&
pong.WaitOne (millisecondsTimeout);
}
catch (Exception ex) {
_logger.Fatal ("An exception has occurred while Ping:\n" + ex.ToString ());
return false;
}
}
// As server, used to broadcast
internal void Send (Opcode opcode, byte[] data, Dictionary<CompressionMethod, byte[]> cache)
{
lock (_forSend) {
lock (_forConn) {
if (_readyState != WebSocketState.Open) {
_logger.Error ("Closing the WebSocket connection has been done.");
return;
}
try {
byte[] cached;
if (!cache.TryGetValue (_compression, out cached)) {
cached = WebSocketFrame.CreateWebSocketFrame (
Fin.Final,
opcode,
Mask.Unmask,
data.Compress (_compression),
_compression != CompressionMethod.None)
.ToByteArray ();
cache.Add (_compression, cached);
}
sendBytes (cached);
}
catch (Exception ex) {
_logger.Fatal (ex.ToString ());
}
}
}
}
// As server, used to broadcast
internal void Send (Opcode opcode, Stream stream, Dictionary <CompressionMethod, Stream> cache)
{
lock (_forSend) {
try {
Stream cached;
if (!cache.TryGetValue (_compression, out cached)) {
cached = stream.Compress (_compression);
cache.Add (_compression, cached);
}
else {
cached.Position = 0;
}
send (opcode, Mask.Unmask, cached, _compression != CompressionMethod.None);
}
catch (Exception ex) {
_logger.Fatal (ex.ToString ());
}
}
}
#endregion
#region Public Methods
/// <summary>
/// Closes the WebSocket connection, and releases all associated resources.
/// </summary>
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);
}
/// <summary>
/// Closes the WebSocket connection with the specified <see cref="ushort"/>,
/// and releases all associated resources.
/// </summary>
/// <remarks>
/// This method emits a <see cref="OnError"/> event if <paramref name="code"/>
/// isn't in the allowable range of the WebSocket close status code.
/// </remarks>
/// <param name="code">
/// A <see cref="ushort"/> that represents the status code indicating the reason for the close.
/// </param>
public void Close (ushort code)
{
Close (code, null);
}
/// <summary>
/// Closes the WebSocket connection with the specified <see cref="CloseStatusCode"/>,
/// and releases all associated resources.
/// </summary>
/// <param name="code">
/// One of the <see cref="CloseStatusCode"/> enum values, represents the status code
/// indicating the reason for the close.
/// </param>
public void Close (CloseStatusCode code)
{
Close (code, null);
}
/// <summary>
/// Closes the WebSocket connection with the specified <see cref="ushort"/>
/// and <see cref="string"/>, and releases all associated resources.
/// </summary>
/// <remarks>
/// This method emits a <see cref="OnError"/> event if <paramref name="code"/>
/// isn't in the allowable range of the WebSocket close status code or the size
/// of <paramref name="reason"/> is greater than 123 bytes.
/// </remarks>
/// <param name="code">
/// A <see cref="ushort"/> that represents the status code indicating the reason for the close.
/// </param>
/// <param name="reason">
/// A <see cref="string"/> that represents the reason for the close.
/// </param>
public void Close (ushort code, string reason)
{
byte[] data = null;
var msg = _readyState.CheckIfClosable () ??
code.CheckIfValidCloseStatusCode () ??
(data = code.Append (reason)).CheckIfValidControlData ("reason");
if (msg != null) {
_logger.Error (String.Format ("{0}\ncode: {1} reason: {2}", msg, code, reason));
error (msg);
return;
}
var send = _readyState == WebSocketState.Open && !code.IsReserved ();
close (new PayloadData (data), send, send);
}
/// <summary>
/// Closes the WebSocket connection with the specified <see cref="CloseStatusCode"/>
/// and <see cref="string"/>, and releases all associated resources.
/// </summary>
/// <remarks>
/// This method emits a <see cref="OnError"/> event if the size
/// of <paramref name="reason"/> is greater than 123 bytes.
/// </remarks>
/// <param name="code">
/// One of the <see cref="CloseStatusCode"/> enum values, represents the status code
/// indicating the reason for the close.
/// </param>
/// <param name="reason">
/// A <see cref="string"/> that represents the reason for the close.
/// </param>
public void Close (CloseStatusCode code, string reason)
{
byte[] data = null;
var msg = _readyState.CheckIfClosable () ??
(data = ((ushort) code).Append (reason)).CheckIfValidControlData ("reason");
if (msg != null) {
_logger.Error (String.Format ("{0}\ncode: {1} reason: {2}", msg, code, reason));
error (msg);
return;
}
var send = _readyState == WebSocketState.Open && !code.IsReserved ();
close (new PayloadData (data), send, send);
}
/// <summary>
/// Closes the WebSocket connection asynchronously, and releases all associated resources.
/// </summary>
/// <remarks>
/// This method doesn't wait for the close to be complete.
/// </remarks>
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);
}
/// <summary>
/// Closes the WebSocket connection asynchronously with the specified <see cref="ushort"/>,
/// and releases all associated resources.
/// </summary>
/// <remarks>
/// <para>
/// This method doesn't wait for the close to be complete.
/// </para>
/// <para>
/// This method emits a <see cref="OnError"/> event if <paramref name="code"/>
/// isn't in the allowable range of the WebSocket close status code.
/// </para>
/// </remarks>
/// <param name="code">
/// A <see cref="ushort"/> that represents the status code indicating the reason for the close.
/// </param>
public void CloseAsync (ushort code)
{
CloseAsync (code, null);
}
/// <summary>
/// Closes the WebSocket connection asynchronously with the specified
/// <see cref="CloseStatusCode"/>, and releases all associated resources.
/// </summary>
/// <remarks>
/// This method doesn't wait for the close to be complete.
/// </remarks>
/// <param name="code">
/// One of the <see cref="CloseStatusCode"/> enum values, represents the status code
/// indicating the reason for the close.
/// </param>
public void CloseAsync (CloseStatusCode code)
{
CloseAsync (code, null);
}
/// <summary>
/// Closes the WebSocket connection asynchronously with the specified <see cref="ushort"/>
/// and <see cref="string"/>, and releases all associated resources.
/// </summary>
/// <remarks>
/// <para>
/// This method doesn't wait for the close to be complete.
/// </para>
/// <para>
/// This method emits a <see cref="OnError"/> event if <paramref name="code"/>
/// isn't in the allowable range of the WebSocket close status code or the size
/// of <paramref name="reason"/> is greater than 123 bytes.
/// </para>
/// </remarks>
/// <param name="code">
/// A <see cref="ushort"/> that represents the status code indicating the reason for the close.
/// </param>
/// <param name="reason">
/// A <see cref="string"/> that represents the reason for the close.
/// </param>
public void CloseAsync (ushort code, string reason)
{
byte[] data = null;
var msg = _readyState.CheckIfClosable () ??
code.CheckIfValidCloseStatusCode () ??
(data = code.Append (reason)).CheckIfValidControlData ("reason");
if (msg != null) {
_logger.Error (String.Format ("{0}\ncode: {1} reason: {2}", msg, code, reason));
error (msg);
return;
}
var send = _readyState == WebSocketState.Open && !code.IsReserved ();
closeAsync (new PayloadData (data), send, send);
}
/// <summary>
/// Closes the WebSocket connection asynchronously with the specified
/// <see cref="CloseStatusCode"/> and <see cref="string"/>, and releases
/// all associated resources.
/// </summary>
/// <remarks>
/// <para>
/// This method doesn't wait for the close to be complete.
/// </para>
/// <para>
/// This method emits a <see cref="OnError"/> event if the size
/// of <paramref name="reason"/> is greater than 123 bytes.
/// </para>
/// </remarks>
/// <param name="code">
/// One of the <see cref="CloseStatusCode"/> enum values, represents the status code
/// indicating the reason for the close.
/// </param>
/// <param name="reason">
/// A <see cref="string"/> that represents the reason for the close.
/// </param>
public void CloseAsync (CloseStatusCode code, string reason)
{
byte[] data = null;
var msg = _readyState.CheckIfClosable () ??
(data = ((ushort) code).Append (reason)).CheckIfValidControlData ("reason");
if (msg != null) {
_logger.Error (String.Format ("{0}\ncode: {1} reason: {2}", msg, code, reason));
error (msg);
return;
}
var send = _readyState == WebSocketState.Open && !code.IsReserved ();
closeAsync (new PayloadData (data), send, send);
}
/// <summary>
/// Establishes a WebSocket connection.
/// </summary>
public void Connect ()
{
var msg = checkIfCanConnect ();
if (msg != null) {
_logger.Error (msg);
error (msg);
return;
}
if (connect ())
open ();
}
/// <summary>
/// Establishes a WebSocket connection asynchronously.
/// </summary>
/// <remarks>
/// This method doesn't wait for the connect to be complete.
/// </remarks>
public void ConnectAsync ()
{
var msg = checkIfCanConnect ();
if (msg != null) {
_logger.Error (msg);
error (msg);
return;
}
Func<bool> connector = connect;
connector.BeginInvoke (
ar => {
if (connector.EndInvoke (ar))
open ();
},
null);
}
/// <summary>
/// Sends a Ping using the WebSocket connection.
/// </summary>
/// <returns>
/// <c>true</c> if the <see cref="WebSocket"/> receives a Pong to this Ping in a time;
/// otherwise, <c>false</c>.
/// </returns>
public bool Ping ()
{
return _client
? Ping (WebSocketFrame.CreatePingFrame (Mask.Mask).ToByteArray (), 5000)
: Ping (WebSocketFrame.EmptyUnmaskPingData, 1000);
}
/// <summary>
/// Sends a Ping with the specified <paramref name="message"/> using the WebSocket connection.
/// </summary>
/// <returns>
/// <c>true</c> if the <see cref="WebSocket"/> receives a Pong to this Ping in a time;
/// otherwise, <c>false</c>.
/// </returns>
/// <param name="message">
/// A <see cref="string"/> that represents the message to send.
/// </param>
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 (WebSocketFrame.CreatePingFrame (Mask.Mask, data).ToByteArray (), 5000)
: Ping (WebSocketFrame.CreatePingFrame (Mask.Unmask, data).ToByteArray (), 1000);
}
/// <summary>
/// Sends a binary <paramref name="data"/> using the WebSocket connection.
/// </summary>
/// <param name="data">
/// An array of <see cref="byte"/> that represents the binary data to send.
/// </param>
public void Send (byte[] data)
{
var msg = _readyState.CheckIfOpen () ?? data.CheckIfValidSendData ();
if (msg != null) {
_logger.Error (msg);
error (msg);
return;
}
send (Opcode.Binary, new MemoryStream (data));
}
/// <summary>
/// Sends the specified <paramref name="file"/> as a binary data
/// using the WebSocket connection.
/// </summary>
/// <param name="file">
/// A <see cref="FileInfo"/> that represents the file to send.
/// </param>
public void Send (FileInfo file)
{
var msg = _readyState.CheckIfOpen () ?? file.CheckIfValidSendData ();
if (msg != null) {
_logger.Error (msg);
error (msg);
return;
}
send (Opcode.Binary, file.OpenRead ());
}
/// <summary>
/// Sends a text <paramref name="data"/> using the WebSocket connection.
/// </summary>
/// <param name="data">
/// A <see cref="string"/> that represents the text data to send.
/// </param>
public void Send (string data)
{
var msg = _readyState.CheckIfOpen () ?? data.CheckIfValidSendData ();
if (msg != null) {
_logger.Error (msg);
error (msg);
return;
}
send (Opcode.Text, new MemoryStream (Encoding.UTF8.GetBytes (data)));
}
/// <summary>
/// Sends a binary <paramref name="data"/> asynchronously using the WebSocket connection.
/// </summary>
/// <remarks>
/// This method doesn't wait for the send to be complete.
/// </remarks>
/// <param name="data">
/// An array of <see cref="byte"/> that represents the binary data to send.
/// </param>
/// <param name="completed">
/// An Action&lt;bool&gt; delegate that references the method(s) called when the send is
/// complete. A <see cref="bool"/> passed to this delegate is <c>true</c> if the send is
/// complete successfully; otherwise, <c>false</c>.
/// </param>
public void SendAsync (byte[] data, Action<bool> completed)
{
var msg = _readyState.CheckIfOpen () ?? data.CheckIfValidSendData ();
if (msg != null) {
_logger.Error (msg);
error (msg);
return;
}
sendAsync (Opcode.Binary, new MemoryStream (data), completed);
}
/// <summary>
/// Sends the specified <paramref name="file"/> as a binary data asynchronously
/// using the WebSocket connection.
/// </summary>
/// <remarks>
/// This method doesn't wait for the send to be complete.
/// </remarks>
/// <param name="file">
/// A <see cref="FileInfo"/> that represents the file to send.
/// </param>
/// <param name="completed">
/// An Action&lt;bool&gt; delegate that references the method(s) called when the send is
/// complete. A <see cref="bool"/> passed to this delegate is <c>true</c> if the send is
/// complete successfully; otherwise, <c>false</c>.
/// </param>
public void SendAsync (FileInfo file, Action<bool> completed)
{
var msg = _readyState.CheckIfOpen () ?? file.CheckIfValidSendData ();
if (msg != null) {
_logger.Error (msg);
error (msg);
return;
}
sendAsync (Opcode.Binary, file.OpenRead (), completed);
}
/// <summary>
/// Sends a text <paramref name="data"/> asynchronously using the WebSocket connection.
/// </summary>
/// <remarks>
/// This method doesn't wait for the send to be complete.
/// </remarks>
/// <param name="data">
/// A <see cref="string"/> that represents the text data to send.
/// </param>
/// <param name="completed">
/// An Action&lt;bool&gt; delegate that references the method(s) called when the send is
/// complete. A <see cref="bool"/> passed to this delegate is <c>true</c> if the send is
/// complete successfully; otherwise, <c>false</c>.
/// </param>
public void SendAsync (string data, Action<bool> completed)
{
var msg = _readyState.CheckIfOpen () ?? data.CheckIfValidSendData ();
if (msg != null) {
_logger.Error (msg);
error (msg);
return;
}
sendAsync (Opcode.Text, new MemoryStream (Encoding.UTF8.GetBytes (data)), completed);
}
/// <summary>
/// Sends a binary data from the specified <see cref="Stream"/> asynchronously
/// using the WebSocket connection.
/// </summary>
/// <remarks>
/// This method doesn't wait for the send to be complete.
/// </remarks>
/// <param name="stream">
/// A <see cref="Stream"/> from which contains the binary data to send.
/// </param>
/// <param name="length">
/// An <see cref="int"/> that represents the number of bytes to send.
/// </param>
/// <param name="completed">
/// An Action&lt;bool&gt; delegate that references the method(s) called when the send is
/// complete. A <see cref="bool"/> passed to this delegate is <c>true</c> if the send is
/// complete successfully; otherwise, <c>false</c>.
/// </param>
public void SendAsync (Stream stream, int length, Action<bool> completed)
{
var msg = _readyState.CheckIfOpen () ??
stream.CheckIfCanRead () ??
(length < 1 ? "'length' must be greater than 0." : null);
if (msg != null) {
_logger.Error (msg);
error (msg);
return;
}
stream.ReadBytesAsync (
length,
data => {
var len = data.Length;
if (len == 0) {
msg = "A data cannot be read from 'stream'.";
_logger.Error (msg);
error (msg);
return;
}
if (len < length)
_logger.Warn (
String.Format (
"A data with 'length' cannot be read from 'stream'.\nexpected: {0} actual: {1}",
length,
len));
var sent = 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.", ex);
});
}
/// <summary>
/// Sets an HTTP <paramref name="cookie"/> to send with the WebSocket connection request
/// to the server.
/// </summary>
/// <param name="cookie">
/// A <see cref="Cookie"/> that represents the cookie to send.
/// </param>
public void SetCookie (Cookie cookie)
{
lock (_forConn) {
var msg = checkIfAvailable ("SetCookie", false, false) ??
(cookie == null ? "'cookie' is null." : null);
if (msg != null) {
_logger.Error (msg);
error ("An error has occurred while setting the cookie.");
return;
}
lock (_cookies.SyncRoot) {
_cookies.SetOrRemove (cookie);
}
}
}
/// <summary>
/// Sets a pair of <paramref name="username"/> and <paramref name="password"/> for
/// the HTTP authentication (Basic/Digest).
/// </summary>
/// <param name="username">
/// A <see cref="string"/> that represents the user name used to authenticate.
/// </param>
/// <param name="password">
/// A <see cref="string"/> that represents the password for <paramref name="username"/>
/// used to authenticate.
/// </param>
/// <param name="preAuth">
/// <c>true</c> if the <see cref="WebSocket"/> sends the Basic authentication credentials
/// with the first connection request to the server; otherwise, <c>false</c>.
/// </param>
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 ("An error has occurred while setting the credentials.");
return;
}
_credentials = new NetworkCredential (username, password, _uri.PathAndQuery);
_preAuth = preAuth;
}
}
/// <summary>
/// Sets an HTTP Proxy server URL to connect through, and if necessary, a pair of
/// <paramref name="username"/> and <paramref name="password"/> for the proxy server
/// authentication (Basic/Digest).
/// </summary>
/// <param name="url">
/// A <see cref="string"/> that represents the proxy server URL to connect through.
/// </param>
/// <param name="username">
/// A <see cref="string"/> that represents the user name used to authenticate.
/// </param>
/// <param name="password">
/// A <see cref="string"/> that represents the password for <paramref name="username"/>
/// used to authenticate.
/// </param>
public void SetProxy (string url, string username, string password)
{
lock (_forConn) {
var msg = checkIfAvailable ("SetProxy", false, false);
if (msg == null) {
if (url.IsNullOrEmpty ()) {
_proxyUri = null;
_proxyCredentials = null;
_logger.Warn ("Proxy url and credentials were set back to the default.");
return;
}
Uri uri;
if (!Uri.TryCreate (url, UriKind.Absolute, out uri) ||
uri.Scheme != "http" ||
uri.Segments.Length > 1) {
msg = "The syntax of proxy url must be 'http://<host>[:<port>]'.";
}
else {
_proxyUri = uri;
if (username.IsNullOrEmpty ()) {
_proxyCredentials = null;
_logger.Warn ("Proxy 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 ("An error has occurred while setting the proxy.");
return;
}
_proxyCredentials = new NetworkCredential (
username, password, String.Format ("{0}:{1}", _uri.DnsSafeHost, _uri.Port));
}
}
#endregion
#region Explicit Interface Implementation
/// <summary>
/// Closes the WebSocket connection, and releases all associated resources.
/// </summary>
/// <remarks>
/// This method closes the WebSocket connection with <see cref="CloseStatusCode.Away"/>.
/// </remarks>
void IDisposable.Dispose ()
{
Close (CloseStatusCode.Away, null);
}
#endregion
}
}