From e08eaad4434137b4212270ef1222d94e9801ab19 Mon Sep 17 00:00:00 2001 From: Michal Dobrodenka Date: Tue, 25 Sep 2018 15:01:46 +0200 Subject: [PATCH 1/5] - Adding ConnectTimeout and ReadWriteTimeout to WebSocket. - Adding try/catch around stream.Dispose() because with SSL streams in Mono stream can be sometimes already disposed and Dispose throws exception, crashing the app --- websocket-sharp/Net/HttpConnection.cs | 5 +- websocket-sharp/WebSocket.cs | 91 ++++++++++++++++++++++++--- 2 files changed, 87 insertions(+), 9 deletions(-) diff --git a/websocket-sharp/Net/HttpConnection.cs b/websocket-sharp/Net/HttpConnection.cs index 572d785c2..a318e43f2 100644 --- a/websocket-sharp/Net/HttpConnection.cs +++ b/websocket-sharp/Net/HttpConnection.cs @@ -221,7 +221,10 @@ private void disposeStream () _inputStream = null; _outputStream = null; - _stream.Dispose (); + try { + _stream.Dispose(); + } + catch { } _stream = null; } diff --git a/websocket-sharp/WebSocket.cs b/websocket-sharp/WebSocket.cs index 93ed5bf4e..849f5bf73 100644 --- a/websocket-sharp/WebSocket.cs +++ b/websocket-sharp/WebSocket.cs @@ -119,6 +119,8 @@ public class WebSocket : IDisposable private Uri _uri; private const string _version = "13"; private TimeSpan _waitTime; + private int _connectTimeout; + private int _readWriteTimeout; #endregion @@ -330,6 +332,37 @@ internal bool IsConnected { #region Public Properties + /// + /// Gets or sets underlying socket connect timeout. + /// + public int ConnectTimeout { + get { + return _connectTimeout; + } + + set { + _connectTimeout = value; + } + } + + /// + /// Gets or sets underlying socket read or write timeout. + /// + public int ReadWriteTimeout { + get { + return _readWriteTimeout; + } + + set { + _readWriteTimeout = value; + + if (_tcpClient!= null) { + _tcpClient.ReceiveTimeout = value; + _tcpClient.SendTimeout = value; + } + } + } + /// /// Gets or sets the compression method used to compress a message. /// @@ -1804,7 +1837,9 @@ private void refuseHandshake (CloseStatusCode code, string reason) private void releaseClientResources () { if (_stream != null) { - _stream.Dispose (); + try { + _stream.Dispose(); + } catch { } _stream = null; } @@ -1876,9 +1911,13 @@ private bool send (Opcode opcode, Stream stream) error ("An error has occurred during a send.", ex); } finally { - if (compressed) - stream.Dispose (); - + if (compressed) { + try { + stream.Dispose (); + } + catch { } + } + src.Dispose (); } @@ -2093,8 +2132,11 @@ private void sendProxyConnectRequest () if (_proxyCredentials != null) { if (res.HasConnectionClose) { releaseClientResources (); - _tcpClient = new TcpClient (_proxyUri.DnsSafeHost, _proxyUri.Port); - _stream = _tcpClient.GetStream (); + //_tcpClient = new TcpClient (_proxyUri.DnsSafeHost, _proxyUri.Port); + _tcpClient = connectTcpClient(_proxyUri.DnsSafeHost, _proxyUri.Port, _connectTimeout); + _tcpClient.ReceiveTimeout = _readWriteTimeout; + _tcpClient.SendTimeout = _readWriteTimeout; + _stream = _tcpClient.GetStream(); } var authRes = new AuthenticationResponse (authChal, _proxyCredentials, 0); @@ -2115,12 +2157,18 @@ private void sendProxyConnectRequest () private void setClientStream () { if (_proxyUri != null) { - _tcpClient = new TcpClient (_proxyUri.DnsSafeHost, _proxyUri.Port); + //_tcpClient = new TcpClient (_proxyUri.DnsSafeHost, _proxyUri.Port); + _tcpClient = connectTcpClient(_proxyUri.DnsSafeHost, _proxyUri.Port, _connectTimeout); + _tcpClient.ReceiveTimeout = _readWriteTimeout; + _tcpClient.SendTimeout = _readWriteTimeout; _stream = _tcpClient.GetStream (); sendProxyConnectRequest (); } else { - _tcpClient = new TcpClient (_uri.DnsSafeHost, _uri.Port); + //_tcpClient = new TcpClient (_uri.DnsSafeHost, _uri.Port); + _tcpClient = connectTcpClient(_uri.DnsSafeHost, _uri.Port, _connectTimeout); + _tcpClient.ReceiveTimeout = _readWriteTimeout; + _tcpClient.SendTimeout = _readWriteTimeout; _stream = _tcpClient.GetStream (); } @@ -2152,6 +2200,33 @@ private void setClientStream () } } + private static TcpClient connectTcpClient(string hostname, int port, int connectTimeout) { + var client = new TcpClient(); + var result = client.BeginConnect(hostname, port, onEndConnect, client); + bool success = result.AsyncWaitHandle.WaitOne(connectTimeout, true); + + if (!client.Connected) { + client.Close(); + throw new TimeoutException("Failed to connect server."); + } + + return client; + } + + private static void onEndConnect(IAsyncResult asyncResult) { + TcpClient client = (TcpClient)asyncResult.AsyncState; + + try { + client.EndConnect(asyncResult); + } + catch { } + + try { + asyncResult.AsyncWaitHandle.Close(); + } + catch { } + } + private void startReceiving () { if (_messageEventQueue.Count > 0) From ab9672ed34cb200764fb5f60df7770397b1d2be6 Mon Sep 17 00:00:00 2001 From: Michal Dobrodenka Date: Tue, 25 Sep 2018 15:53:33 +0200 Subject: [PATCH 2/5] Setting default timeouts to 5 seconds --- websocket-sharp/WebSocket.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/websocket-sharp/WebSocket.cs b/websocket-sharp/WebSocket.cs index 849f5bf73..9be4aede9 100644 --- a/websocket-sharp/WebSocket.cs +++ b/websocket-sharp/WebSocket.cs @@ -119,8 +119,8 @@ public class WebSocket : IDisposable private Uri _uri; private const string _version = "13"; private TimeSpan _waitTime; - private int _connectTimeout; - private int _readWriteTimeout; + private int _connectTimeout = 5000; + private int _readWriteTimeout = 5000; #endregion @@ -1911,8 +1911,8 @@ private bool send (Opcode opcode, Stream stream) error ("An error has occurred during a send.", ex); } finally { - if (compressed) { - try { + if (compressed) { + try { stream.Dispose (); } catch { } From b7969b15907256dbf8d24b8d62d7c865d742377d Mon Sep 17 00:00:00 2001 From: Michal Dobrodenka Date: Thu, 29 Nov 2018 12:34:17 +0100 Subject: [PATCH 3/5] Adding support for IPv6 --- websocket-sharp/WebSocket.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websocket-sharp/WebSocket.cs b/websocket-sharp/WebSocket.cs index 9be4aede9..ad4da5a6d 100644 --- a/websocket-sharp/WebSocket.cs +++ b/websocket-sharp/WebSocket.cs @@ -2201,7 +2201,7 @@ private void setClientStream () } private static TcpClient connectTcpClient(string hostname, int port, int connectTimeout) { - var client = new TcpClient(); + var client = new TcpClient(AddressFamily.InterNetworkV6); var result = client.BeginConnect(hostname, port, onEndConnect, client); bool success = result.AsyncWaitHandle.WaitOne(connectTimeout, true); From cc920e635b989f899780c2fd620ce322a35c8327 Mon Sep 17 00:00:00 2001 From: Michal Dobrodenka Date: Thu, 14 Jul 2022 14:56:56 +0200 Subject: [PATCH 4/5] Some refactoring & bug fixes --- Example/Example.csproj | 19 +- Example/Notifier.cs | 16 +- Example/Program.cs | 63 +- Example1/AudioStreamer.cs | 171 +- Example1/Example1.csproj | 21 +- Example1/Notifier.cs | 16 +- Example1/Program.cs | 6 +- Example1/TextMessage.cs | 29 +- Example2/App.config | 2 +- Example2/Example2.csproj | 16 +- Example2/Program.cs | 115 +- Example3/App.config | 4 +- Example3/Example3.csproj | 21 +- Example3/Program.cs | 169 +- LICENSE.txt | 2 +- README.md | 294 +- .../websocket-sharp-netstandard.csproj | 105 + websocket-sharp.sln | 68 +- websocket-sharp/ClientWebSocket.cs | 1569 ++++ websocket-sharp/CloseEventArgs.cs | 10 + websocket-sharp/Ext.cs | 62 +- websocket-sharp/LogData.cs | 31 +- websocket-sharp/Logger.cs | 75 +- websocket-sharp/Net/ChunkStream.cs | 3 +- websocket-sharp/Net/HttpConnection.cs | 2 +- .../Net/HttpListenerAsyncResult.cs | 3 +- websocket-sharp/Net/HttpListenerRequest.cs | 18 +- websocket-sharp/Net/HttpStreamAsyncResult.cs | 22 +- websocket-sharp/Net/ResponseStream.cs | 8 +- websocket-sharp/Net/SslConfiguration.cs | 172 + .../HttpListenerWebSocketContext.cs | 8 +- .../WebSockets/TcpListenerWebSocketContext.cs | 6 +- .../Net/WebSockets/WebSocketContext.cs | 2 +- websocket-sharp/PayloadData.cs | 25 +- websocket-sharp/Server/HttpServer.cs | 66 +- websocket-sharp/Server/WebSocketBehavior.cs | 10 +- websocket-sharp/Server/WebSocketServer.cs | 8 +- .../Server/WebSocketServiceManager.cs | 9 +- .../Server/WebSocketSessionManager.cs | 11 +- websocket-sharp/ServerExt.cs | 122 + websocket-sharp/ServerWebSocket.cs | 839 +++ websocket-sharp/WebSocket.cs | 6553 ++++++----------- websocket-sharp/WebSocketException.cs | 14 + websocket-sharp/websocket-sharp.csproj | 49 +- 44 files changed, 5983 insertions(+), 4851 deletions(-) create mode 100644 websocket-sharp-netstandard/websocket-sharp-netstandard.csproj create mode 100644 websocket-sharp/ClientWebSocket.cs create mode 100644 websocket-sharp/Net/SslConfiguration.cs create mode 100644 websocket-sharp/ServerExt.cs create mode 100644 websocket-sharp/ServerWebSocket.cs diff --git a/Example/Example.csproj b/Example/Example.csproj index 38c5b4200..477aa33be 100644 --- a/Example/Example.csproj +++ b/Example/Example.csproj @@ -1,5 +1,5 @@ - - + + Debug AnyCPU @@ -9,7 +9,13 @@ Exe Example example - v3.5 + v4.6.1 + + + + + 3.5 + true @@ -20,6 +26,7 @@ prompt 4 true + false none @@ -28,6 +35,7 @@ prompt 4 true + false true @@ -38,6 +46,7 @@ prompt 4 true + false none @@ -47,6 +56,7 @@ prompt 4 true + false @@ -68,4 +78,7 @@ + + + \ No newline at end of file diff --git a/Example/Notifier.cs b/Example/Notifier.cs index 5371c37a4..f21eec32c 100644 --- a/Example/Notifier.cs +++ b/Example/Notifier.cs @@ -11,16 +11,16 @@ namespace Example internal class Notifier : IDisposable { private volatile bool _enabled; - private ManualResetEvent _exited; private Queue _queue; private object _sync; + private ManualResetEvent _waitHandle; public Notifier () { _enabled = true; - _exited = new ManualResetEvent (false); _queue = new Queue (); _sync = ((ICollection) _queue).SyncRoot; + _waitHandle = new ManualResetEvent (false); ThreadPool.QueueUserWorkItem ( state => { @@ -40,9 +40,8 @@ public Notifier () } } - _exited.Set (); - } - ); + _waitHandle.Set (); + }); } public int Count { @@ -61,16 +60,15 @@ private NotificationMessage dequeue () public void Close () { _enabled = false; - _exited.WaitOne (); - _exited.Close (); + _waitHandle.WaitOne (); + _waitHandle.Close (); } public void Notify (NotificationMessage message) { - lock (_sync) { + lock (_sync) if (_enabled) _queue.Enqueue (message); - } } void IDisposable.Dispose () diff --git a/Example/Program.cs b/Example/Program.cs index d414bb1e5..4ab4bad54 100644 --- a/Example/Program.cs +++ b/Example/Program.cs @@ -16,74 +16,67 @@ public static void Main (string[] args) // close status 1001 (going away) when the control leaves the using block. // // If you would like to connect to the server with the secure connection, - // you should create a new instance with a wss scheme WebSocket URL. + // you should create the instance with the wss scheme WebSocket URL. using (var nf = new Notifier ()) - using (var ws = new WebSocket ("ws://echo.websocket.org")) + using (var ws = new ClientWebSocket ("ws://echo.websocket.org")) //using (var ws = new WebSocket ("wss://echo.websocket.org")) //using (var ws = new WebSocket ("ws://localhost:4649/Echo")) - //using (var ws = new WebSocket ("wss://localhost:5963/Echo")) //using (var ws = new WebSocket ("ws://localhost:4649/Echo?name=nobita")) - //using (var ws = new WebSocket ("wss://localhost:5963/Echo?name=nobita")) + //using (var ws = new WebSocket ("wss://localhost:4649/Echo")) //using (var ws = new WebSocket ("ws://localhost:4649/Chat")) - //using (var ws = new WebSocket ("wss://localhost:5963/Chat")) //using (var ws = new WebSocket ("ws://localhost:4649/Chat?name=nobita")) - //using (var ws = new WebSocket ("wss://localhost:5963/Chat?name=nobita")) + //using (var ws = new WebSocket ("wss://localhost:4649/Chat")) { // Set the WebSocket events. ws.OnOpen += (sender, e) => ws.Send ("Hi, there!"); ws.OnMessage += (sender, e) => - nf.Notify ( - new NotificationMessage { - Summary = "WebSocket Message", - Body = !e.IsPing ? e.Data : "Received a ping.", - Icon = "notification-message-im" - } - ); + nf.Notify ( + new NotificationMessage { + Summary = "WebSocket Message", + Body = !e.IsPing ? e.Data : "Received a ping.", + Icon = "notification-message-im" + }); ws.OnError += (sender, e) => - nf.Notify ( - new NotificationMessage { - Summary = "WebSocket Error", - Body = e.Message, - Icon = "notification-message-im" - } - ); + nf.Notify ( + new NotificationMessage { + Summary = "WebSocket Error", + Body = e.Message, + Icon = "notification-message-im" + }); ws.OnClose += (sender, e) => - nf.Notify ( - new NotificationMessage { - Summary = String.Format ("WebSocket Close ({0})", e.Code), - Body = e.Reason, - Icon = "notification-message-im" - } - ); + nf.Notify ( + new NotificationMessage { + Summary = String.Format ("WebSocket Close ({0})", e.Code), + Body = e.Reason, + Icon = "notification-message-im" + }); + #if DEBUG // To change the logging level. ws.Log.Level = LogLevel.Trace; // To change the wait time for the response to the Ping or Close. - //ws.WaitTime = TimeSpan.FromSeconds (10); + ws.WaitTime = TimeSpan.FromSeconds (10); // To emit a WebSocket.OnMessage event when receives a ping. - //ws.EmitOnPing = true; + ws.EmitOnPing = true; #endif // To enable the Per-message Compression extension. //ws.Compression = CompressionMethod.Deflate; - // To validate the server certificate. - /* + /* To validate the server certificate. ws.SslConfiguration.ServerCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => { ws.Log.Debug ( String.Format ( "Certificate:\n- Issuer: {0}\n- Subject: {1}", certificate.Issuer, - certificate.Subject - ) - ); + certificate.Subject)); return true; // If the server certificate is valid. }; @@ -95,7 +88,7 @@ public static void Main (string[] args) // To send the Origin header. //ws.Origin = "http://localhost:4649"; - // To send the cookies. + // To send the Cookies. //ws.SetCookie (new Cookie ("name", "nobita")); //ws.SetCookie (new Cookie ("roles", "\"idiot, gunfighter\"")); diff --git a/Example1/AudioStreamer.cs b/Example1/AudioStreamer.cs index 711694e9a..669d5c527 100644 --- a/Example1/AudioStreamer.cs +++ b/Example1/AudioStreamer.cs @@ -12,10 +12,10 @@ namespace Example1 internal class AudioStreamer : IDisposable { private Dictionary _audioBox; + private Timer _heartbeatTimer; private uint? _id; private string _name; private Notifier _notifier; - private Timer _timer; private WebSocket _websocket; public AudioStreamer (string url) @@ -23,9 +23,9 @@ public AudioStreamer (string url) _websocket = new WebSocket (url); _audioBox = new Dictionary (); + _heartbeatTimer = new Timer (sendHeartbeat, null, -1, -1); _id = null; _notifier = new Notifier (); - _timer = new Timer (sendHeartbeat, null, -1, -1); configure (); } @@ -36,81 +36,69 @@ private void configure () _websocket.Log.Level = LogLevel.Trace; #endif _websocket.OnOpen += (sender, e) => - _websocket.Send (createTextMessage ("connection", String.Empty)); + _websocket.Send (createTextMessage ("connection", String.Empty)); _websocket.OnMessage += (sender, e) => { - if (e.IsText) { - _notifier.Notify (processTextMessage (e.Data)); + if (e.IsText) { + _notifier.Notify (convertTextMessage (e.Data)); + } + else { + var msg = convertBinaryMessage (e.RawData); + if (msg.user_id == _id) return; - } - if (e.IsBinary) { - processBinaryMessage (e.RawData); + if (_audioBox.ContainsKey (msg.user_id)) { + _audioBox[msg.user_id].Enqueue (msg.buffer_array); return; } - }; + + var queue = Queue.Synchronized (new Queue ()); + queue.Enqueue (msg.buffer_array); + _audioBox.Add (msg.user_id, queue); + } + }; _websocket.OnError += (sender, e) => - _notifier.Notify ( - new NotificationMessage { - Summary = "AudioStreamer (error)", - Body = e.Message, - Icon = "notification-message-im" - } - ); + _notifier.Notify ( + new NotificationMessage { + Summary = "AudioStreamer (error)", + Body = e.Message, + Icon = "notification-message-im" + }); _websocket.OnClose += (sender, e) => - _notifier.Notify ( - new NotificationMessage { - Summary = "AudioStreamer (disconnect)", - Body = String.Format ("code: {0} reason: {1}", e.Code, e.Reason), - Icon = "notification-message-im" - } - ); - } - - private byte[] createBinaryMessage (float[,] bufferArray) - { - return new BinaryMessage { - UserID = (uint) _id, - ChannelNumber = (byte) bufferArray.GetLength (0), - BufferLength = (uint) bufferArray.GetLength (1), - BufferArray = bufferArray - } - .ToArray (); - } - - private string createTextMessage (string type, string message) - { - return new TextMessage { - UserID = _id, - Name = _name, - Type = type, - Message = message - } - .ToString (); + _notifier.Notify ( + new NotificationMessage { + Summary = "AudioStreamer (disconnect)", + Body = String.Format ("code: {0} reason: {1}", e.Code, e.Reason), + Icon = "notification-message-im" + }); } - private void processBinaryMessage (byte[] data) + private AudioMessage convertBinaryMessage (byte[] data) { - var msg = BinaryMessage.Parse (data); - - var id = msg.UserID; - if (id == _id) - return; - - Queue queue; - if (_audioBox.TryGetValue (id, out queue)) { - queue.Enqueue (msg.BufferArray); - return; - } - - queue = Queue.Synchronized (new Queue ()); - queue.Enqueue (msg.BufferArray); - _audioBox.Add (id, queue); + var id = data.SubArray (0, 4).To (ByteOrder.Big); + var chNum = data.SubArray (4, 1)[0]; + var buffLen = data.SubArray (5, 4).To (ByteOrder.Big); + var buffArr = new float[chNum, buffLen]; + + var offset = 9; + ((int) chNum).Times ( + i => buffLen.Times ( + j => { + buffArr[i, j] = data.SubArray (offset, 4).To (ByteOrder.Big); + offset += 4; + })); + + return new AudioMessage { + user_id = id, + ch_num = chNum, + buffer_length = buffLen, + buffer_array = buffArr + }; } - private NotificationMessage processTextMessage (string data) + private NotificationMessage convertTextMessage (string data) { var json = JObject.Parse (data); var id = (uint) json["user_id"]; @@ -127,18 +115,15 @@ private NotificationMessage processTextMessage (string data) else if (type == "connection") { var users = (JArray) json["message"]; var buff = new StringBuilder ("Now keeping connections:"); - foreach (JToken user in users) { + foreach (JToken user in users) buff.AppendFormat ( - "\n- user_id: {0} name: {1}", (uint) user["user_id"], (string) user["name"] - ); - } + "\n- user_id: {0} name: {1}", (uint) user["user_id"], (string) user["name"]); body = buff.ToString (); } else if (type == "connected") { _id = id; - _timer.Change (30000, 30000); - + _heartbeatTimer.Change (30000, 30000); body = String.Format ("user_id: {0} name: {1}", id, name); } else { @@ -146,22 +131,45 @@ private NotificationMessage processTextMessage (string data) } return new NotificationMessage { - Summary = String.Format ("AudioStreamer ({0})", type), - Body = body, - Icon = "notification-message-im" - }; + Summary = String.Format ("AudioStreamer ({0})", type), + Body = body, + Icon = "notification-message-im" + }; } - private void sendHeartbeat (object state) + private byte[] createBinaryMessage (float[,] bufferArray) { - _websocket.Send (createTextMessage ("heartbeat", String.Empty)); + var msg = new List (); + + var id = (uint) _id; + var chNum = bufferArray.GetLength (0); + var buffLen = bufferArray.GetLength (1); + + msg.AddRange (id.ToByteArray (ByteOrder.Big)); + msg.Add ((byte) chNum); + msg.AddRange (((uint) buffLen).ToByteArray (ByteOrder.Big)); + + chNum.Times ( + i => buffLen.Times ( + j => msg.AddRange (bufferArray[i, j].ToByteArray (ByteOrder.Big)))); + + return msg.ToArray (); } - public void Close () + private string createTextMessage (string type, string message) { - Disconnect (); - _timer.Dispose (); - _notifier.Close (); + return JsonConvert.SerializeObject ( + new TextMessage { + user_id = _id, + name = _name, + type = type, + message = message + }); + } + + private void sendHeartbeat (object state) + { + _websocket.Send (createTextMessage ("heartbeat", String.Empty)); } public void Connect (string username) @@ -172,7 +180,7 @@ public void Connect (string username) public void Disconnect () { - _timer.Change (-1, -1); + _heartbeatTimer.Change (-1, -1); _websocket.Close (CloseStatusCode.Away); _audioBox.Clear (); _id = null; @@ -186,7 +194,10 @@ public void Write (string message) void IDisposable.Dispose () { - Close (); + Disconnect (); + + _heartbeatTimer.Dispose (); + _notifier.Close (); } } } diff --git a/Example1/Example1.csproj b/Example1/Example1.csproj index 81c52eff2..7847a56cc 100644 --- a/Example1/Example1.csproj +++ b/Example1/Example1.csproj @@ -1,5 +1,5 @@ - - + + Debug AnyCPU @@ -9,7 +9,13 @@ Exe Example example1 - v3.5 + v4.6.1 + + + + + 3.5 + true @@ -20,6 +26,7 @@ prompt 4 true + false none @@ -28,6 +35,7 @@ prompt 4 true + false true @@ -38,6 +46,7 @@ prompt 4 true + false none @@ -47,6 +56,7 @@ 4 true UBUNTU + false @@ -62,10 +72,10 @@ + - @@ -74,4 +84,7 @@ websocket-sharp + + + \ No newline at end of file diff --git a/Example1/Notifier.cs b/Example1/Notifier.cs index adf53ec9a..5bcbb16ac 100644 --- a/Example1/Notifier.cs +++ b/Example1/Notifier.cs @@ -11,16 +11,16 @@ namespace Example1 internal class Notifier : IDisposable { private volatile bool _enabled; - private ManualResetEvent _exited; private Queue _queue; private object _sync; + private ManualResetEvent _waitHandle; public Notifier () { _enabled = true; - _exited = new ManualResetEvent (false); _queue = new Queue (); _sync = ((ICollection) _queue).SyncRoot; + _waitHandle = new ManualResetEvent (false); ThreadPool.QueueUserWorkItem ( state => { @@ -40,9 +40,8 @@ public Notifier () } } - _exited.Set (); - } - ); + _waitHandle.Set (); + }); } public int Count { @@ -61,16 +60,15 @@ private NotificationMessage dequeue () public void Close () { _enabled = false; - _exited.WaitOne (); - _exited.Close (); + _waitHandle.WaitOne (); + _waitHandle.Close (); } public void Notify (NotificationMessage message) { - lock (_sync) { + lock (_sync) if (_enabled) _queue.Enqueue (message); - } } void IDisposable.Dispose () diff --git a/Example1/Program.cs b/Example1/Program.cs index 88c0bedfe..7b936da93 100644 --- a/Example1/Program.cs +++ b/Example1/Program.cs @@ -7,10 +7,8 @@ public class Program { public static void Main (string[] args) { - // The AudioStreamer class provides a client (chat) for AudioStreamer - // (https://github.com/agektmr/AudioStreamer). - - using (var streamer = new AudioStreamer ("ws://localhost:3000/socket")) + using (var streamer = new AudioStreamer ("ws://agektmr.node-ninja.com:3000/socket")) + //using (var streamer = new AudioStreamer ("ws://localhost:3000/socket")) { string name; do { diff --git a/Example1/TextMessage.cs b/Example1/TextMessage.cs index 2b177d845..5eab648f9 100644 --- a/Example1/TextMessage.cs +++ b/Example1/TextMessage.cs @@ -1,33 +1,12 @@ -using Newtonsoft.Json; using System; namespace Example1 { internal class TextMessage { - [JsonProperty ("user_id")] - public uint? UserID { - get; set; - } - - [JsonProperty ("name")] - public string Name { - get; set; - } - - [JsonProperty ("type")] - public string Type { - get; set; - } - - [JsonProperty ("message")] - public string Message { - get; set; - } - - public override string ToString () - { - return JsonConvert.SerializeObject (this); - } + public uint? user_id; + public string name; + public string type; + public string message; } } diff --git a/Example2/App.config b/Example2/App.config index 3a02690ea..a38803f79 100644 --- a/Example2/App.config +++ b/Example2/App.config @@ -4,4 +4,4 @@ - + diff --git a/Example2/Example2.csproj b/Example2/Example2.csproj index 685a1ef6d..fc1b124e9 100644 --- a/Example2/Example2.csproj +++ b/Example2/Example2.csproj @@ -1,5 +1,5 @@ - - + + Debug AnyCPU @@ -9,7 +9,13 @@ Exe Example2 example2 - v3.5 + v4.6.1 + + + + + 3.5 + true @@ -20,6 +26,7 @@ prompt 4 true + false none @@ -28,6 +35,7 @@ prompt 4 true + false true @@ -38,6 +46,7 @@ prompt 4 true + false none @@ -46,6 +55,7 @@ prompt 4 true + false diff --git a/Example2/Program.cs b/Example2/Program.cs index c9bd7ef3d..950ffde85 100644 --- a/Example2/Program.cs +++ b/Example2/Program.cs @@ -13,70 +13,44 @@ public static void Main (string[] args) { // Create a new instance of the WebSocketServer class. // - // If you would like to provide the secure connection, you should - // create a new instance with the 'secure' parameter set to true, - // or a wss scheme WebSocket URL. + // If you would like to provide the secure connection, you should create the instance with + // the 'secure' parameter set to true, or the wss scheme WebSocket URL. var wssv = new WebSocketServer (4649); //var wssv = new WebSocketServer (5963, true); - - //var wssv = new WebSocketServer (System.Net.IPAddress.Any, 4649); - //var wssv = new WebSocketServer (System.Net.IPAddress.Any, 5963, true); - - //var wssv = new WebSocketServer (System.Net.IPAddress.IPv6Any, 4649); - //var wssv = new WebSocketServer (System.Net.IPAddress.IPv6Any, 5963, true); - - //var wssv = new WebSocketServer ("ws://0.0.0.0:4649"); - //var wssv = new WebSocketServer ("wss://0.0.0.0:5963"); - - //var wssv = new WebSocketServer ("ws://[::0]:4649"); - //var wssv = new WebSocketServer ("wss://[::0]:5963"); - - //var wssv = new WebSocketServer (System.Net.IPAddress.Loopback, 4649); - //var wssv = new WebSocketServer (System.Net.IPAddress.Loopback, 5963, true); - - //var wssv = new WebSocketServer (System.Net.IPAddress.IPv6Loopback, 4649); - //var wssv = new WebSocketServer (System.Net.IPAddress.IPv6Loopback, 5963, true); - + //var wssv = new WebSocketServer (System.Net.IPAddress.Parse ("127.0.0.1"), 4649); + //var wssv = new WebSocketServer (System.Net.IPAddress.Parse ("127.0.0.1"), 5963, true); //var wssv = new WebSocketServer ("ws://localhost:4649"); //var wssv = new WebSocketServer ("wss://localhost:5963"); - - //var wssv = new WebSocketServer ("ws://127.0.0.1:4649"); - //var wssv = new WebSocketServer ("wss://127.0.0.1:5963"); - - //var wssv = new WebSocketServer ("ws://[::1]:4649"); - //var wssv = new WebSocketServer ("wss://[::1]:5963"); #if DEBUG // To change the logging level. wssv.Log.Level = LogLevel.Trace; // To change the wait time for the response to the WebSocket Ping or Close. - //wssv.WaitTime = TimeSpan.FromSeconds (2); - - // Not to remove the inactive sessions periodically. - //wssv.KeepClean = false; + wssv.WaitTime = TimeSpan.FromSeconds (2); #endif - // To provide the secure connection. - /* + /* To provide the secure connection. var cert = ConfigurationManager.AppSettings["ServerCertFile"]; var passwd = ConfigurationManager.AppSettings["CertFilePassword"]; wssv.SslConfiguration.ServerCertificate = new X509Certificate2 (cert, passwd); */ - // To provide the HTTP Authentication (Basic/Digest). - /* + /* To provide the HTTP Authentication (Basic/Digest). wssv.AuthenticationSchemes = AuthenticationSchemes.Basic; wssv.Realm = "WebSocket Test"; wssv.UserCredentialsFinder = id => { - var name = id.Name; + var name = id.Name; - // Return user name, password, and roles. - return name == "nobita" - ? new NetworkCredential (name, "password", "gunfighter") - : null; // If the user credentials aren't found. - }; + // Return user name, password, and roles. + return name == "nobita" + ? new NetworkCredential (name, "password", "gunfighter") + : null; // If the user credentials aren't found. + }; */ + // Not to remove the inactive sessions periodically. + //wssv.KeepClean = false; + // To resolve to wait for socket in TIME_WAIT state. //wssv.ReuseAddress = true; @@ -84,39 +58,36 @@ public static void Main (string[] args) wssv.AddWebSocketService ("/Echo"); wssv.AddWebSocketService ("/Chat"); - // Add the WebSocket service with initializing. - /* + /* Add the WebSocket service with initializing. wssv.AddWebSocketService ( "/Chat", - () => - new Chat ("Anon#") { - // To send the Sec-WebSocket-Protocol header that has a subprotocol name. - Protocol = "chat", - // To ignore the Sec-WebSocket-Extensions header. - IgnoreExtensions = true, - // To emit a WebSocket.OnMessage event when receives a ping. - EmitOnPing = true, - // To validate the Origin header. - OriginValidator = val => { - // Check the value of the Origin header, and return true if valid. - Uri origin; - return !val.IsNullOrEmpty () - && Uri.TryCreate (val, UriKind.Absolute, out origin) - && origin.Host == "localhost"; - }, - // To validate the cookies. - CookiesValidator = (req, res) => { - // Check the cookies in 'req', and set the cookies to send to - // the client with 'res' if necessary. - foreach (Cookie cookie in req) { - cookie.Expired = true; - res.Add (cookie); - } - - return true; // If valid. - } + () => new Chat ("Anon#") { + // To send the Sec-WebSocket-Protocol header that has a subprotocol name. + Protocol = "chat", + // To emit a WebSocket.OnMessage event when receives a ping. + EmitOnPing = true, + // To ignore the Sec-WebSocket-Extensions header. + IgnoreExtensions = true, + // To validate the Origin header. + OriginValidator = val => { + // Check the value of the Origin header, and return true if valid. + Uri origin; + return !val.IsNullOrEmpty () && + Uri.TryCreate (val, UriKind.Absolute, out origin) && + origin.Host == "localhost"; + }, + // To validate the Cookies. + CookiesValidator = (req, res) => { + // Check the Cookies in 'req', and set the Cookies to send to the client with 'res' + // if necessary. + foreach (Cookie cookie in req) { + cookie.Expired = true; + res.Add (cookie); + } + + return true; // If valid. } - ); + }); */ wssv.Start (); diff --git a/Example3/App.config b/Example3/App.config index fa624b42b..03d5bdafc 100644 --- a/Example3/App.config +++ b/Example3/App.config @@ -2,7 +2,7 @@ - + - + diff --git a/Example3/Example3.csproj b/Example3/Example3.csproj index ce4fe265c..1cedde126 100644 --- a/Example3/Example3.csproj +++ b/Example3/Example3.csproj @@ -1,5 +1,5 @@ - - + + Debug AnyCPU @@ -9,7 +9,13 @@ Exe Example3 example3 - v3.5 + v4.6.1 + + + + + 3.5 + true @@ -20,6 +26,7 @@ prompt 4 true + false none @@ -28,6 +35,7 @@ prompt 4 true + false true @@ -38,6 +46,7 @@ prompt 4 true + false none @@ -46,6 +55,7 @@ prompt 4 true + false @@ -69,8 +79,5 @@ - - - - + \ No newline at end of file diff --git a/Example3/Program.cs b/Example3/Program.cs index 939bfed89..7a9e04361 100644 --- a/Example3/Program.cs +++ b/Example3/Program.cs @@ -14,140 +14,117 @@ public static void Main (string[] args) { // Create a new instance of the HttpServer class. // - // If you would like to provide the secure connection, you should - // create a new instance with the 'secure' parameter set to true, - // or an https scheme HTTP URL. + // If you would like to provide the secure connection, you should create the instance with + // the 'secure' parameter set to true, or the https scheme HTTP URL. var httpsv = new HttpServer (4649); //var httpsv = new HttpServer (5963, true); - - //var httpsv = new HttpServer (System.Net.IPAddress.Any, 4649); - //var httpsv = new HttpServer (System.Net.IPAddress.Any, 5963, true); - - //var httpsv = new HttpServer (System.Net.IPAddress.IPv6Any, 4649); - //var httpsv = new HttpServer (System.Net.IPAddress.IPv6Any, 5963, true); - - //var httpsv = new HttpServer ("http://0.0.0.0:4649"); - //var httpsv = new HttpServer ("https://0.0.0.0:5963"); - - //var httpsv = new HttpServer ("http://[::0]:4649"); - //var httpsv = new HttpServer ("https://[::0]:5963"); - - //var httpsv = new HttpServer (System.Net.IPAddress.Loopback, 4649); - //var httpsv = new HttpServer (System.Net.IPAddress.Loopback, 5963, true); - - //var httpsv = new HttpServer (System.Net.IPAddress.IPv6Loopback, 4649); - //var httpsv = new HttpServer (System.Net.IPAddress.IPv6Loopback, 5963, true); - + //var httpsv = new HttpServer (System.Net.IPAddress.Parse ("127.0.0.1"), 4649); + //var httpsv = new HttpServer (System.Net.IPAddress.Parse ("127.0.0.1"), 5963, true); //var httpsv = new HttpServer ("http://localhost:4649"); //var httpsv = new HttpServer ("https://localhost:5963"); - - //var httpsv = new HttpServer ("http://127.0.0.1:4649"); - //var httpsv = new HttpServer ("https://127.0.0.1:5963"); - - //var httpsv = new HttpServer ("http://[::1]:4649"); - //var httpsv = new HttpServer ("https://[::1]:5963"); #if DEBUG // To change the logging level. httpsv.Log.Level = LogLevel.Trace; // To change the wait time for the response to the WebSocket Ping or Close. - //httpsv.WaitTime = TimeSpan.FromSeconds (2); - - // Not to remove the inactive WebSocket sessions periodically. - //httpsv.KeepClean = false; + httpsv.WaitTime = TimeSpan.FromSeconds (2); #endif - // To provide the secure connection. - /* + /* To provide the secure connection. var cert = ConfigurationManager.AppSettings["ServerCertFile"]; var passwd = ConfigurationManager.AppSettings["CertFilePassword"]; httpsv.SslConfiguration.ServerCertificate = new X509Certificate2 (cert, passwd); */ - // To provide the HTTP Authentication (Basic/Digest). - /* + /* To provide the HTTP Authentication (Basic/Digest). httpsv.AuthenticationSchemes = AuthenticationSchemes.Basic; httpsv.Realm = "WebSocket Test"; httpsv.UserCredentialsFinder = id => { - var name = id.Name; + var name = id.Name; - // Return user name, password, and roles. - return name == "nobita" - ? new NetworkCredential (name, "password", "gunfighter") - : null; // If the user credentials aren't found. - }; + // Return user name, password, and roles. + return name == "nobita" + ? new NetworkCredential (name, "password", "gunfighter") + : null; // If the user credentials aren't found. + }; */ - // To resolve to wait for socket in TIME_WAIT state. - //httpsv.ReuseAddress = true; - // Set the document root path. - httpsv.DocumentRootPath = ConfigurationManager.AppSettings["DocumentRootPath"]; + // httpsv.RootPath = ConfigurationManager.AppSettings["RootPath"]; // Set the HTTP GET request event. httpsv.OnGet += (sender, e) => { + var req = e.Request; + var res = e.Response; + + var path = req.RawUrl; + if (path == "/") + path += "index.html"; + + var content = httpsv.GetFile (path); + if (content == null) { + res.StatusCode = (int) HttpStatusCode.NotFound; + return; + } + + if (path.EndsWith (".html")) { + res.ContentType = "text/html"; + res.ContentEncoding = Encoding.UTF8; + } + else if (path.EndsWith (".js")) { + res.ContentType = "application/javascript"; + res.ContentEncoding = Encoding.UTF8; + } + + res.WriteContent (content); + }; + + httpsv.OnPost += (sender, e) => + { var req = e.Request; var res = e.Response; + }; - var path = req.RawUrl; - if (path == "/") - path += "index.html"; - - byte[] contents; - if (!e.TryReadFile (path, out contents)) { - res.StatusCode = (int) HttpStatusCode.NotFound; - return; - } - - if (path.EndsWith (".html")) { - res.ContentType = "text/html"; - res.ContentEncoding = Encoding.UTF8; - } - else if (path.EndsWith (".js")) { - res.ContentType = "application/javascript"; - res.ContentEncoding = Encoding.UTF8; - } + // Not to remove the inactive WebSocket sessions periodically. + //httpsv.KeepClean = false; - res.WriteContent (contents); - }; + // To resolve to wait for socket in TIME_WAIT state. + //httpsv.ReuseAddress = true; // Add the WebSocket services. httpsv.AddWebSocketService ("/Echo"); httpsv.AddWebSocketService ("/Chat"); - // Add the WebSocket service with initializing. - /* + /* Add the WebSocket service with initializing. httpsv.AddWebSocketService ( "/Chat", - () => - new Chat ("Anon#") { - // To send the Sec-WebSocket-Protocol header that has a subprotocol name. - Protocol = "chat", - // To ignore the Sec-WebSocket-Extensions header. - IgnoreExtensions = true, - // To emit a WebSocket.OnMessage event when receives a ping. - EmitOnPing = true, - // To validate the Origin header. - OriginValidator = val => { - // Check the value of the Origin header, and return true if valid. - Uri origin; - return !val.IsNullOrEmpty () - && Uri.TryCreate (val, UriKind.Absolute, out origin) - && origin.Host == "localhost"; - }, - // To validate the cookies. - CookiesValidator = (req, res) => { - // Check the cookies in 'req', and set the cookies to send to - // the client with 'res' if necessary. - foreach (Cookie cookie in req) { - cookie.Expired = true; - res.Add (cookie); - } - - return true; // If valid. - } + () => new Chat ("Anon#") { + // To send the Sec-WebSocket-Protocol header that has a subprotocol name. + Protocol = "chat", + // To emit a WebSocket.OnMessage event when receives a ping. + EmitOnPing = true, + // To ignore the Sec-WebSocket-Extensions header. + IgnoreExtensions = true, + // To validate the Origin header. + OriginValidator = val => { + // Check the value of the Origin header, and return true if valid. + Uri origin; + return !val.IsNullOrEmpty () && + Uri.TryCreate (val, UriKind.Absolute, out origin) && + origin.Host == "localhost"; + }, + // To validate the Cookies. + CookiesValidator = (req, res) => { + // Check the Cookies in 'req', and set the Cookies to send to the client with 'res' + // if necessary. + foreach (Cookie cookie in req) { + cookie.Expired = true; + res.Add (cookie); + } + + return true; // If valid. } - ); + }); */ httpsv.Start (); diff --git a/LICENSE.txt b/LICENSE.txt index c53829dc8..b9c30e643 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2010-2018 sta.blockhead +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 diff --git a/README.md b/README.md index e7049b224..a8e0f3a66 100644 --- a/README.md +++ b/README.md @@ -2,65 +2,64 @@ ## Welcome to websocket-sharp! ## -websocket-sharp supports: +**websocket-sharp** supports: -- [RFC 6455](#supported-websocket-specifications) -- [WebSocket Client](#websocket-client) and [Server](#websocket-server) -- [Per-message Compression](#per-message-compression) extension -- [Secure Connection](#secure-connection) -- [HTTP Authentication](#http-authentication) -- [Query string, Origin header, and Cookies](#query-string-origin-header-and-cookies) -- [Connecting through the HTTP proxy server](#connecting-through-the-http-proxy-server) -- .NET Framework **3.5** or later (includes compatible environment such as [Mono]) +- **[RFC 6455](#supported-websocket-specifications)** +- **[WebSocket Client](#websocket-client)** and **[Server](#websocket-server)** +- **[Per-message Compression](#per-message-compression)** extension +- **[Secure Connection](#secure-connection)** +- **[HTTP Authentication](#http-authentication)** +- **[Query String, Origin header and Cookies](#query-string-origin-header-and-cookies)** +- **[Connecting through the HTTP Proxy server](#connecting-through-the-http-proxy-server)** +- .NET **3.5** or later (includes compatible) ## Branches ## -- [master] for production releases. -- [hybi-00] for older [draft-ietf-hybi-thewebsocketprotocol-00]. No longer maintained. -- [draft75] for even more old [draft-hixie-thewebsocketprotocol-75]. No longer maintained. +- **[master]** for production releases. +- **[hybi-00]** for older [draft-ietf-hybi-thewebsocketprotocol-00]. No longer maintained. +- **[draft75]** for even more old [draft-hixie-thewebsocketprotocol-75]. No longer maintained. ## Build ## websocket-sharp is built as a single assembly, **websocket-sharp.dll**. -websocket-sharp is developed with [MonoDevelop]. So a simple way to build is to open **websocket-sharp.sln** and run build for **websocket-sharp project** with any of the build configurations (e.g. `Debug`) in MonoDevelop. +websocket-sharp is developed with **[MonoDevelop]**. So the simple way to build is to open **websocket-sharp.sln** and run build for **websocket-sharp project** with any of the build configurations (e.g. `Debug`) in MonoDevelop. ## Install ## ### Self Build ### -You should add your websocket-sharp.dll (e.g. `/path/to/websocket-sharp/bin/Debug/websocket-sharp.dll`) to the library references of your project. +You should add your **websocket-sharp.dll** (e.g. `/path/to/websocket-sharp/bin/Debug/websocket-sharp.dll`) to the library references of your project. -If you would like to use that dll in your [Unity] project, you should add it to any folder of your project (e.g. `Assets/Plugins`) in the **Unity Editor**. +If you would like to use that dll in your **[Unity]** project, you should add it to any folder of your project (e.g. `Assets/Plugins`) in **Unity Editor**. ### NuGet Gallery ### -websocket-sharp is available on the [NuGet Gallery], as still a **prerelease** version. +websocket-sharp is available on the **[NuGet Gallery]**, as still a **prerelease** version. -- [NuGet Gallery: websocket-sharp] +- **[NuGet Gallery: websocket-sharp]** -You can add websocket-sharp to your project with the NuGet Package Manager, by using the following command in the Package Manager Console. +You can add websocket-sharp to your project with the **NuGet Package Manager**, by using the following command in the **Package Manager Console**. PM> Install-Package WebSocketSharp -Pre ### Unity Asset Store ### -websocket-sharp is available on the Unity Asset Store (Sorry, Not available now). +websocket-sharp is available on the **Unity Asset Store**. -- [WebSocket-Sharp for Unity] +- **[WebSocket-Sharp for Unity]** It works with **Unity Free**, but there are some limitations: -- [Security Sandbox of the Webplayer] (The server is not available in Web Player) -- [WebGL Networking] (Not available in WebGL) -- Incompatible platform (Not available for such UWP) -- Lack of dll for the System.IO.Compression (The compression extension is not available on Windows) -- .NET Socket Support for iOS/Android (iOS/Android Pro is required if your Unity is earlier than Unity 5) -- .NET API 2.0 compatibility level for iOS/Android +- **[Security Sandbox of the Webplayer]** (The server isn't available in Web Player) +- **[WebGL Networking]** (Not available in WebGL) +- **Weak Support for the System.IO.Compression** (The compression extension isn't available on Windows) +- **.NET Socket Support for iOS/Android** (It requires iOS/Android Pro if your Unity is earlier than Unity 5) +- **.NET API 2.0 compatibility level for iOS/Android** -.NET API 2.0 compatibility level for iOS/Android may require to fix lack of some features for later than .NET Framework 2.0, such as the `System.Func<...>` delegates (so i have added them in the asset package). +**.NET API 2.0 compatibility level for iOS/Android** may require to fix lack of some features for later than .NET 2.0, such as the `System.Func<...>` delegates (so i've fixed it in the asset package). -And it is priced at **US$15**. I believe your $15 makes this project more better, **Thank you!** +And it's priced at **US$15**. I think your $15 makes this project more better and accelerated, **Thank you!** ## Usage ## @@ -78,7 +77,7 @@ namespace Example { using (var ws = new WebSocket ("ws://dragonsnest.far/Laputa")) { ws.OnMessage += (sender, e) => - Console.WriteLine ("Laputa says: " + e.Data); + Console.WriteLine ("Laputa says: " + e.Data); ws.Connect (); ws.Send ("BALUS"); @@ -103,19 +102,13 @@ The `WebSocket` class exists in the `WebSocketSharp` namespace. Creating a new instance of the `WebSocket` class with the WebSocket URL to connect. -```csharp -var ws = new WebSocket ("ws://example.com"); -``` - -The `WebSocket` class inherits the `System.IDisposable` interface, so you can create it with the `using` statement. - ```csharp using (var ws = new WebSocket ("ws://example.com")) { ... } ``` -This will **close** the WebSocket connection with status code `1001` (going away) when the control leaves the `using` block. +The `WebSocket` class inherits the `System.IDisposable` interface, so you can use the `using` statement. And the WebSocket connection will be closed with close status `1001` (going away) when the control leaves the `using` block. #### Step 3 #### @@ -123,33 +116,35 @@ Setting the `WebSocket` events. ##### WebSocket.OnOpen Event ##### -This event occurs when the WebSocket connection has been established. +A `WebSocket.OnOpen` event occurs when the WebSocket connection has been established. ```csharp ws.OnOpen += (sender, e) => { - ... - }; + ... +}; ``` -`System.EventArgs.Empty` is passed as `e`, so you do not need to use it. +`e` has passed as the `System.EventArgs.Empty`, so you don't need to use it. ##### WebSocket.OnMessage Event ##### -This event occurs when the `WebSocket` receives a message. +A `WebSocket.OnMessage` event occurs when the `WebSocket` receives a message. ```csharp ws.OnMessage += (sender, e) => { - ... - }; + ... +}; ``` -A `WebSocketSharp.MessageEventArgs` instance is passed as `e`. +`e` has passed as a `WebSocketSharp.MessageEventArgs`. If you would like to get the message data, you should access `e.Data` or `e.RawData` property. -`e.Data` property returns a `string`, so it is mainly used to get the **text** message data. +And you can determine which property you should access by checking `e.IsText` or `e.IsBinary` property. -`e.RawData` property returns a `byte[]`, so it is mainly used to get the **binary** message data. +If `e.IsText` is `true`, you should access `e.Data` that returns a `string` (represents a **text** message). + +Or if `e.IsBinary` is `true`, you should access `e.RawData` that returns a `byte[]` (represents a **binary** message). ```csharp if (e.IsText) { @@ -167,55 +162,49 @@ if (e.IsBinary) { } ``` -And if you would like to notify that a **ping** has been received, via this event, you should set the `WebSocket.EmitOnPing` property to `true`. +And if you would like to notify that a **ping** has been received, via this event, you should set the `WebSocket.EmitOnPing` property to `true`, such as the following. ```csharp ws.EmitOnPing = true; ws.OnMessage += (sender, e) => { - if (e.IsPing) { - // Do something to notify that a ping has been received. - ... + if (e.IsPing) { + // Do something to notify that a ping has been received. + ... - return; - } - }; + return; + } +}; ``` ##### WebSocket.OnError Event ##### -This event occurs when the `WebSocket` gets an error. +A `WebSocket.OnError` event occurs when the `WebSocket` gets an error. ```csharp ws.OnError += (sender, e) => { - ... - }; + ... +}; ``` -A `WebSocketSharp.ErrorEventArgs` instance is passed as `e`. - -If you would like to get the error message, you should access `e.Message` property. +`e` has passed as a `WebSocketSharp.ErrorEventArgs`. `e.Message` property returns a `string` that represents the error message. -And `e.Exception` property returns a `System.Exception` instance that represents the cause of the error if it is due to an exception. +If the error is due to an exception, `e.Exception` property returns a `System.Exception` instance that caused the error. ##### WebSocket.OnClose Event ##### -This event occurs when the WebSocket connection has been closed. +A `WebSocket.OnClose` event occurs when the WebSocket connection has been closed. ```csharp ws.OnClose += (sender, e) => { - ... - }; + ... +}; ``` -A `WebSocketSharp.CloseEventArgs` instance is passed as `e`. - -If you would like to get the reason for the close, you should access `e.Code` or `e.Reason` property. +`e` has passed as a `WebSocketSharp.CloseEventArgs`. -`e.Code` property returns a `ushort` that represents the status code for the close. - -`e.Reason` property returns a `string` that represents the reason for the close. +`e.Code` property returns a `ushort` that represents the status code indicating the reason for the close, and `e.Reason` property returns a `string` that represents the reason for the close. #### Step 4 #### @@ -358,15 +347,15 @@ public class Chat : WebSocketBehavior You can define the behavior of any WebSocket service by creating the class that inherits the `WebSocketBehavior` class. -If you override the `WebSocketBehavior.OnMessage (MessageEventArgs)` method, it will be called when the `WebSocket` used in a session in the service receives a message. +If you override the `WebSocketBehavior.OnMessage (MessageEventArgs)` method, it's called when the `WebSocket` used in a session in the service receives a message. -And if you override the `WebSocketBehavior.OnOpen ()`, `WebSocketBehavior.OnError (ErrorEventArgs)`, and `WebSocketBehavior.OnClose (CloseEventArgs)` methods, each of them will be called when each of the `WebSocket` events (`OnOpen`, `OnError`, and `OnClose`) occurs. +And if you override the `WebSocketBehavior.OnOpen ()`, `WebSocketBehavior.OnError (ErrorEventArgs)`, and `WebSocketBehavior.OnClose (CloseEventArgs)` methods, each of them is called when each event of the `WebSocket` (the `OnOpen`, `OnError`, and `OnClose` events) occurs. -The `WebSocketBehavior.Send` method can send data to the client on a session in the service. +The `WebSocketBehavior.Send` method sends data to the client on a session in the service. If you would like to get the sessions in the service, you should access the `WebSocketBehavior.Sessions` property (returns a `WebSocketSharp.Server.WebSocketSessionManager`). -The `WebSocketBehavior.Sessions.Broadcast` method can send data to every client in the service. +The `WebSocketBehavior.Sessions.Broadcast` method sends data to every client in the service. #### Step 3 #### @@ -379,15 +368,15 @@ wssv.AddWebSocketService ("/Chat"); wssv.AddWebSocketService ("/ChatWithNyan", () => new Chat (" Nyan!")); ``` -You can add any WebSocket service to your `WebSocketServer` with the specified behavior and absolute path to the service, by using the `WebSocketServer.AddWebSocketService (string)` or `WebSocketServer.AddWebSocketService (string, Func)` method. +You can add any WebSocket service to your `WebSocketServer` with the specified behavior and path to the service, by using the `WebSocketServer.AddWebSocketService (string)` or `WebSocketServer.AddWebSocketService (string, Func)` method. The type of `TBehaviorWithNew` must inherit the `WebSocketBehavior` class, and must have a public parameterless constructor. -The type of `TBehavior` must inherit the `WebSocketBehavior` class. +And also the type of `TBehavior` must inherit the `WebSocketBehavior` class. -So you can use a class in the above Step 2 to add the service. +So you can use the classes created in **Step 2** to add the service. -If you create a new instance of the `WebSocketServer` class without a port number, it sets the port number to **80**. So it is necessary to run with root permission. +If you create a instance of the `WebSocketServer` class without a port number, the `WebSocketServer` class set the port number to **80** automatically. So it's necessary to run with root permission. $ sudo mono example2.exe @@ -413,7 +402,7 @@ You can use the `WebSocketServer.Stop ()`, `WebSocketServer.Stop (ushort, string ### HTTP Server with the WebSocket ### -I have modified the `System.Net.HttpListener`, `System.Net.HttpListenerContext`, and some other classes from **[Mono]** to create an HTTP server that allows to accept the WebSocket handshake requests. +I modified the `System.Net.HttpListener`, `System.Net.HttpListenerContext`, and some other classes of **[Mono]** to create the HTTP server that allows to accept the WebSocket connection requests. So websocket-sharp provides the `WebSocketSharp.Server.HttpServer` class. @@ -432,21 +421,19 @@ For more information, would you see **[Example3]**? #### Per-message Compression #### -websocket-sharp supports the [Per-message Compression][compression] extension (but does not support it with the [context take over]). +websocket-sharp supports the **[Per-message Compression][compression]** extension (but doesn't support this extension with the [context take over]). -As a WebSocket client, if you would like to enable this extension, you should set the `WebSocket.Compression` property to a compression method before calling the connect method. +As a WebSocket client, if you would like to enable this extension, you should set such as the following. ```csharp ws.Compression = CompressionMethod.Deflate; ``` -And then the client will send the following header in the handshake request to the server. +And then your client will send the following header in the connection request to the server. Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover -If the server supports this extension, it will return the same header which has the corresponding value. - -So eventually this extension will be available when the client receives the header in the handshake response. +If the server accepts this extension, it will return the same header which has the corresponding value. And when your client receives it, this extension will be available. #### Ignoring the extensions #### @@ -463,21 +450,23 @@ wssv.AddWebSocketService ( ); ``` -If it is set to `true`, the service will not return the Sec-WebSocket-Extensions header in its handshake response. +If it's set to `true`, the server doesn't return the **Sec-WebSocket-Extensions** header in its response. I think this is useful when you get something error in connecting the server and exclude the extensions as a cause of the error. ### Secure Connection ### -websocket-sharp supports the secure connection with **SSL/TLS**. +websocket-sharp supports the **Secure Connection** with **SSL/TLS**. -As a WebSocket client, you should create a new instance of the `WebSocket` class with a **wss** scheme WebSocket URL. +As a **WebSocket Client**, you should create a new instance of the `WebSocket` class with the **wss** scheme WebSocket URL. ```csharp -var ws = new WebSocket ("wss://example.com"); +using (var ws = new WebSocket ("wss://example.com")) { + ... +} ``` -If you would like to set a custom validation for the server certificate, you should set the `WebSocket.SslConfiguration.ServerCertificateValidationCallback` property to a callback for it. +And if you would like to use the custom validation for the server certificate, you should set the `WebSocket.SslConfiguration.ServerCertificateValidationCallback` property. ```csharp ws.SslConfiguration.ServerCertificateValidationCallback = @@ -489,9 +478,9 @@ ws.SslConfiguration.ServerCertificateValidationCallback = }; ``` -The default callback always returns `true`. +If you set this property to nothing, the validation does nothing with the server certificate, and returns `true`. -As a WebSocket server, you should create a new instance of the `WebSocketServer` or `HttpServer` class with some settings for the secure connection, such as the following. +As a **WebSocket Server**, you should create a new instance of the `WebSocketServer` or `HttpServer` class with some settings for secure connection, such as the following. ```csharp var wssv = new WebSocketServer (5963, true); @@ -501,31 +490,31 @@ wssv.SslConfiguration.ServerCertificate = ### HTTP Authentication ### -websocket-sharp supports the [HTTP Authentication (Basic/Digest)][rfc2617]. +websocket-sharp supports the **[HTTP Authentication (Basic/Digest)][rfc2617]**. -As a WebSocket client, you should set a pair of user name and password for the HTTP authentication, by using the `WebSocket.SetCredentials (string, string, bool)` method before calling the connect method. +As a **WebSocket Client**, you should set a pair of user name and password for the HTTP authentication, by using the `WebSocket.SetCredentials (string, string, bool)` method before connecting. ```csharp ws.SetCredentials ("nobita", "password", preAuth); ``` -If `preAuth` is `true`, the client will send the credentials for the Basic authentication in the first handshake request to the server. +If `preAuth` is `true`, the `WebSocket` sends the Basic authentication credentials with the first connection request to the server. -Otherwise, it will send the credentials for either the Basic or Digest (determined by the unauthorized response to the first handshake request) authentication in the second handshake request to the server. +Or if `preAuth` is `false`, the `WebSocket` sends either the Basic or Digest (determined by the unauthorized response to the first connection request) authentication credentials with the second connection request to the server. -As a WebSocket server, you should set an HTTP authentication scheme, a realm, and any function to find the user credentials before calling the start method, such as the following. +As a **WebSocket Server**, you should set an HTTP authentication scheme, a realm, and any function to find the user credentials before starting, such as the following. ```csharp wssv.AuthenticationSchemes = AuthenticationSchemes.Basic; wssv.Realm = "WebSocket Test"; wssv.UserCredentialsFinder = id => { - var name = id.Name; + var name = id.Name; - // Return user name, password, and roles. - return name == "nobita" - ? new NetworkCredential (name, "password", "gunfighter") - : null; // If the user credentials are not found. - }; + // Return user name, password, and roles. + return name == "nobita" + ? new NetworkCredential (name, "password", "gunfighter") + : null; // If the user credentials aren't found. +}; ``` If you would like to provide the Digest authentication, you should set such as the following. @@ -534,27 +523,29 @@ If you would like to provide the Digest authentication, you should set such as t wssv.AuthenticationSchemes = AuthenticationSchemes.Digest; ``` -### Query string, Origin header, and Cookies ### +### Query String, Origin header and Cookies ### -As a WebSocket client, if you would like to send the query string in the handshake request, you should create a new instance of the `WebSocket` class with a WebSocket URL that includes the [Query] string parameters. +As a **WebSocket Client**, if you would like to send the **Query String** with the WebSocket connection request to the server, you should create a new instance of the `WebSocket` class with the WebSocket URL that includes the [Query] string parameters. ```csharp -var ws = new WebSocket ("ws://example.com/?name=nobita"); +using (var ws = new WebSocket ("ws://example.com/?name=nobita")) { + ... +} ``` -If you would like to send the Origin header in the handshake request, you should set the `WebSocket.Origin` property to an allowable value as the [Origin] header before calling the connect method. +And if you would like to send the **Origin** header with the WebSocket connection request to the server, you should set the `WebSocket.Origin` property to an allowable value as the [Origin] header before connecting, such as the following. ```csharp ws.Origin = "http://example.com"; ``` -And if you would like to send the cookies in the handshake request, you should set any cookie by using the `WebSocket.SetCookie (WebSocketSharp.Net.Cookie)` method before calling the connect method. +And also if you would like to send the **Cookies** with the WebSocket connection request to the server, you should set any cookie by using the `WebSocket.SetCookie (WebSocketSharp.Net.Cookie)` method before connecting, such as the following. ```csharp ws.SetCookie (new Cookie ("name", "nobita")); ``` -As a WebSocket server, if you would like to get the query string included in a handshake request, you should access the `WebSocketBehavior.Context.QueryString` property, such as the following. +As a **WebSocket Server**, if you would like to get the **Query String** included in a WebSocket connection request, you should access the `WebSocketBehavior.Context.QueryString` property, such as the following. ```csharp public class Chat : WebSocketBehavior @@ -571,50 +562,46 @@ public class Chat : WebSocketBehavior } ``` -If you would like to get the value of the Origin header included in a handshake request, you should access the `WebSocketBehavior.Context.Origin` property. - -If you would like to get the cookies included in a handshake request, you should access the `WebSocketBehavior.Context.CookieCollection` property. - -And if you would like to validate the Origin header, cookies, or both, you should set each validation for it with your `WebSocketBehavior`, for example, by using the `WebSocketServer.AddWebSocketService (string, Func)` method with initializing, such as the following. +And if you would like to validate the **Origin** header, **Cookies**, or both included in a WebSocket connection request, you should set each validation with your `WebSocketBehavior`, for example, by using the `AddWebSocketService (string, Func)` method with initializing, such as the following. ```csharp wssv.AddWebSocketService ( "/Chat", - () => - new Chat () { - OriginValidator = val => { - // Check the value of the Origin header, and return true if valid. - Uri origin; - return !val.IsNullOrEmpty () - && Uri.TryCreate (val, UriKind.Absolute, out origin) - && origin.Host == "example.com"; - }, - CookiesValidator = (req, res) => { - // Check the cookies in 'req', and set the cookies to send to - // the client with 'res' if necessary. - foreach (Cookie cookie in req) { - cookie.Expired = true; - res.Add (cookie); - } - - return true; // If valid. - } + () => new Chat () { + OriginValidator = val => { + // Check the value of the Origin header, and return true if valid. + Uri origin; + return !val.IsNullOrEmpty () && + Uri.TryCreate (val, UriKind.Absolute, out origin) && + origin.Host == "example.com"; + }, + CookiesValidator = (req, res) => { + // Check the Cookies in 'req', and set the Cookies to send to the client with 'res' + // if necessary. + foreach (Cookie cookie in req) { + cookie.Expired = true; + res.Add (cookie); + } + + return true; // If valid. } -); + }); ``` -### Connecting through the HTTP proxy server ### +And also if you would like to get each value of the Origin header and cookies, you should access each of the `WebSocketBehavior.Context.Origin` and `WebSocketBehavior.Context.CookieCollection` properties. + +### Connecting through the HTTP Proxy server ### -websocket-sharp supports to connect through the HTTP proxy server. +websocket-sharp supports to connect through the **HTTP Proxy** server. -If you would like to connect to a WebSocket server through the HTTP proxy server, you should set the proxy server URL, and if necessary, a pair of user name and password for the proxy server authentication (Basic/Digest), by using the `WebSocket.SetProxy (string, string, string)` method before calling the connect method. +If you would like to connect to a WebSocket server through the HTTP Proxy server, you should set the proxy server URL, and if necessary, a pair of user name and password for the proxy server authentication (Basic/Digest), by using the `WebSocket.SetProxy (string, string, string)` method before connecting. ```csharp var ws = new WebSocket ("ws://example.com"); ws.SetProxy ("http://localhost:3128", "nobita", "password"); ``` -I have tested this with **[Squid]**. It is necessary to disable the following option in **squid.conf** (e.g. `/etc/squid/squid.conf`). +I tested this with the [Squid]. And it's necessary to disable the following configuration option in **squid.conf** (e.g. `/etc/squid/squid.conf`). ``` # Deny CONNECT to other than SSL ports @@ -623,7 +610,7 @@ I have tested this with **[Squid]**. It is necessary to disable the following op ### Logging ### -The `WebSocket` class has the own logging function. +The `WebSocket` class includes the own logging function. You can use it with the `WebSocket.Log` property (returns a `WebSocketSharp.Logger`). @@ -641,7 +628,7 @@ And if you would like to output a log, you should use any of the output methods. ws.Log.Debug ("This is a debug message."); ``` -The `WebSocketServer` and `HttpServer` classes have the same logging function. +The `WebSocketServer` and `HttpServer` classes include the same logging function. ## Examples ## @@ -649,40 +636,49 @@ Examples using websocket-sharp. ### Example ### -[Example] connects to the [Echo server]. +**[Example]** connects to the **[Echo server]** with the WebSocket. + +### Example1 ### + +**[Example1]** connects to the **[Audio Data delivery server]** with the WebSocket. (But it's only implemented the chat feature, still unfinished.) + +And Example1 uses **[Json.NET]**. ### Example2 ### -[Example2] starts a WebSocket server. +**[Example2]** starts a WebSocket server. ### Example3 ### -[Example3] starts an HTTP server that allows to accept the WebSocket handshake requests. +**[Example3]** starts an HTTP server that allows to accept the WebSocket connection requests. Would you access to [http://localhost:4649](http://localhost:4649) to do **WebSocket Echo Test** with your web browser while Example3 is running? ## Supported WebSocket Specifications ## -websocket-sharp supports **RFC 6455**, and it is based on the following references: +websocket-sharp supports **[RFC 6455][rfc6455]**, and it's based on the following WebSocket references: -- [The WebSocket Protocol][rfc6455] -- [The WebSocket API][api] -- [Compression Extensions for WebSocket][compression] +- **[The WebSocket Protocol][rfc6455]** +- **[The WebSocket API][api]** +- **[Compression Extensions for WebSocket][compression]** Thanks for translating to japanese. -- [The WebSocket Protocol 日本語訳][rfc6455_ja] -- [The WebSocket API 日本語訳][api_ja] +- **[The WebSocket Protocol 日本語訳][rfc6455_ja]** +- **[The WebSocket API 日本語訳][api_ja]** ## License ## -websocket-sharp is provided under [The MIT License]. +websocket-sharp is provided under **[The MIT License]**. +[Audio Data delivery server]: http://agektmr.node-ninja.com:3000 [Echo server]: http://www.websocket.org/echo.html [Example]: https://github.com/sta/websocket-sharp/tree/master/Example +[Example1]: https://github.com/sta/websocket-sharp/tree/master/Example1 [Example2]: https://github.com/sta/websocket-sharp/tree/master/Example2 [Example3]: https://github.com/sta/websocket-sharp/tree/master/Example3 +[Json.NET]: http://james.newtonking.com/projects/json-net.aspx [Mono]: http://www.mono-project.com [MonoDevelop]: http://monodevelop.com [NuGet Gallery]: http://www.nuget.org diff --git a/websocket-sharp-netstandard/websocket-sharp-netstandard.csproj b/websocket-sharp-netstandard/websocket-sharp-netstandard.csproj new file mode 100644 index 000000000..f7d0d93f6 --- /dev/null +++ b/websocket-sharp-netstandard/websocket-sharp-netstandard.csproj @@ -0,0 +1,105 @@ + + + + netstandard2.0 + websocket_sharp_core + + + + TRACE;NET_CORE + + + + TRACE;NET_CORE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/websocket-sharp.sln b/websocket-sharp.sln index 3c20e06a0..1d5be149c 100644 --- a/websocket-sharp.sln +++ b/websocket-sharp.sln @@ -1,6 +1,8 @@  -Microsoft Visual Studio Solution File, Format Version 10.00 -# Visual Studio 2008 +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29721.120 +MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "websocket-sharp", "websocket-sharp\websocket-sharp.csproj", "{B357BAC7-529E-4D81-A0D2-71041B19C8DE}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example", "Example\Example.csproj", "{52805AEC-EFB1-4F42-BB8E-3ED4E692C568}" @@ -11,54 +13,56 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example2", "Example2\Exampl EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example3", "Example3\Example3.csproj", "{C648BA25-77E5-4A40-A97F-D0AA37B9FB26}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "websocket-sharp-android", "websocket-sharp-android\websocket-sharp-android.csproj", "{DDEDE2F7-F845-4862-9BD2-05D8A1F413F9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "websocket-sharp-ios", "websocket-sharp-ios\websocket-sharp-ios.csproj", "{CCE46F6C-0A64-4438-A2D9-B3BD746228F6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "websocket-sharp-netstandard", "websocket-sharp-netstandard\websocket-sharp-netstandard.csproj", "{AFF6F166-7DA7-4B2B-96E8-092AE8EF32B9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU - Debug_Ubuntu|Any CPU = Debug_Ubuntu|Any CPU - Release_Ubuntu|Any CPU = Release_Ubuntu|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {390E2568-57B7-4D17-91E5-C29336368CCF}.Debug_Ubuntu|Any CPU.ActiveCfg = Debug_Ubuntu|Any CPU - {390E2568-57B7-4D17-91E5-C29336368CCF}.Debug_Ubuntu|Any CPU.Build.0 = Debug_Ubuntu|Any CPU - {390E2568-57B7-4D17-91E5-C29336368CCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {390E2568-57B7-4D17-91E5-C29336368CCF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {390E2568-57B7-4D17-91E5-C29336368CCF}.Release_Ubuntu|Any CPU.ActiveCfg = Release_Ubuntu|Any CPU - {390E2568-57B7-4D17-91E5-C29336368CCF}.Release_Ubuntu|Any CPU.Build.0 = Release_Ubuntu|Any CPU - {390E2568-57B7-4D17-91E5-C29336368CCF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {390E2568-57B7-4D17-91E5-C29336368CCF}.Release|Any CPU.Build.0 = Release|Any CPU - {52805AEC-EFB1-4F42-BB8E-3ED4E692C568}.Debug_Ubuntu|Any CPU.ActiveCfg = Debug_Ubuntu|Any CPU - {52805AEC-EFB1-4F42-BB8E-3ED4E692C568}.Debug_Ubuntu|Any CPU.Build.0 = Debug_Ubuntu|Any CPU - {52805AEC-EFB1-4F42-BB8E-3ED4E692C568}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {52805AEC-EFB1-4F42-BB8E-3ED4E692C568}.Debug|Any CPU.Build.0 = Debug|Any CPU - {52805AEC-EFB1-4F42-BB8E-3ED4E692C568}.Release_Ubuntu|Any CPU.ActiveCfg = Release_Ubuntu|Any CPU - {52805AEC-EFB1-4F42-BB8E-3ED4E692C568}.Release_Ubuntu|Any CPU.Build.0 = Release_Ubuntu|Any CPU - {52805AEC-EFB1-4F42-BB8E-3ED4E692C568}.Release|Any CPU.ActiveCfg = Release|Any CPU - {52805AEC-EFB1-4F42-BB8E-3ED4E692C568}.Release|Any CPU.Build.0 = Release|Any CPU - {B357BAC7-529E-4D81-A0D2-71041B19C8DE}.Debug_Ubuntu|Any CPU.ActiveCfg = Debug_Ubuntu|Any CPU - {B357BAC7-529E-4D81-A0D2-71041B19C8DE}.Debug_Ubuntu|Any CPU.Build.0 = Debug_Ubuntu|Any CPU {B357BAC7-529E-4D81-A0D2-71041B19C8DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B357BAC7-529E-4D81-A0D2-71041B19C8DE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B357BAC7-529E-4D81-A0D2-71041B19C8DE}.Release_Ubuntu|Any CPU.ActiveCfg = Release_Ubuntu|Any CPU - {B357BAC7-529E-4D81-A0D2-71041B19C8DE}.Release_Ubuntu|Any CPU.Build.0 = Release_Ubuntu|Any CPU {B357BAC7-529E-4D81-A0D2-71041B19C8DE}.Release|Any CPU.ActiveCfg = Release|Any CPU {B357BAC7-529E-4D81-A0D2-71041B19C8DE}.Release|Any CPU.Build.0 = Release|Any CPU - {B81A24C8-25BB-42B2-AF99-1E1EACCE74C7}.Debug_Ubuntu|Any CPU.ActiveCfg = Debug_Ubuntu|Any CPU - {B81A24C8-25BB-42B2-AF99-1E1EACCE74C7}.Debug_Ubuntu|Any CPU.Build.0 = Debug_Ubuntu|Any CPU + {52805AEC-EFB1-4F42-BB8E-3ED4E692C568}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {52805AEC-EFB1-4F42-BB8E-3ED4E692C568}.Debug|Any CPU.Build.0 = Debug|Any CPU + {52805AEC-EFB1-4F42-BB8E-3ED4E692C568}.Release|Any CPU.ActiveCfg = Release|Any CPU + {52805AEC-EFB1-4F42-BB8E-3ED4E692C568}.Release|Any CPU.Build.0 = Release|Any CPU + {390E2568-57B7-4D17-91E5-C29336368CCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {390E2568-57B7-4D17-91E5-C29336368CCF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {390E2568-57B7-4D17-91E5-C29336368CCF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {390E2568-57B7-4D17-91E5-C29336368CCF}.Release|Any CPU.Build.0 = Release|Any CPU {B81A24C8-25BB-42B2-AF99-1E1EACCE74C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B81A24C8-25BB-42B2-AF99-1E1EACCE74C7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B81A24C8-25BB-42B2-AF99-1E1EACCE74C7}.Release_Ubuntu|Any CPU.ActiveCfg = Release_Ubuntu|Any CPU - {B81A24C8-25BB-42B2-AF99-1E1EACCE74C7}.Release_Ubuntu|Any CPU.Build.0 = Release_Ubuntu|Any CPU {B81A24C8-25BB-42B2-AF99-1E1EACCE74C7}.Release|Any CPU.ActiveCfg = Release|Any CPU {B81A24C8-25BB-42B2-AF99-1E1EACCE74C7}.Release|Any CPU.Build.0 = Release|Any CPU - {C648BA25-77E5-4A40-A97F-D0AA37B9FB26}.Debug_Ubuntu|Any CPU.ActiveCfg = Debug_Ubuntu|Any CPU - {C648BA25-77E5-4A40-A97F-D0AA37B9FB26}.Debug_Ubuntu|Any CPU.Build.0 = Debug_Ubuntu|Any CPU {C648BA25-77E5-4A40-A97F-D0AA37B9FB26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C648BA25-77E5-4A40-A97F-D0AA37B9FB26}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C648BA25-77E5-4A40-A97F-D0AA37B9FB26}.Release_Ubuntu|Any CPU.ActiveCfg = Release_Ubuntu|Any CPU - {C648BA25-77E5-4A40-A97F-D0AA37B9FB26}.Release_Ubuntu|Any CPU.Build.0 = Release_Ubuntu|Any CPU {C648BA25-77E5-4A40-A97F-D0AA37B9FB26}.Release|Any CPU.ActiveCfg = Release|Any CPU {C648BA25-77E5-4A40-A97F-D0AA37B9FB26}.Release|Any CPU.Build.0 = Release|Any CPU + {DDEDE2F7-F845-4862-9BD2-05D8A1F413F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DDEDE2F7-F845-4862-9BD2-05D8A1F413F9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DDEDE2F7-F845-4862-9BD2-05D8A1F413F9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DDEDE2F7-F845-4862-9BD2-05D8A1F413F9}.Release|Any CPU.Build.0 = Release|Any CPU + {CCE46F6C-0A64-4438-A2D9-B3BD746228F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CCE46F6C-0A64-4438-A2D9-B3BD746228F6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CCE46F6C-0A64-4438-A2D9-B3BD746228F6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CCE46F6C-0A64-4438-A2D9-B3BD746228F6}.Release|Any CPU.Build.0 = Release|Any CPU + {AFF6F166-7DA7-4B2B-96E8-092AE8EF32B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AFF6F166-7DA7-4B2B-96E8-092AE8EF32B9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AFF6F166-7DA7-4B2B-96E8-092AE8EF32B9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AFF6F166-7DA7-4B2B-96E8-092AE8EF32B9}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {A36643F8-A6CC-48E6-9791-626A074FE485} EndGlobalSection GlobalSection(MonoDevelopProperties) = preSolution StartupItem = websocket-sharp\websocket-sharp.csproj diff --git a/websocket-sharp/ClientWebSocket.cs b/websocket-sharp/ClientWebSocket.cs new file mode 100644 index 000000000..e7c5c4c1b --- /dev/null +++ b/websocket-sharp/ClientWebSocket.cs @@ -0,0 +1,1569 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Net.Security; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using WebSocketSharp.Net; + +// ReSharper disable UnusedMember.Global + +namespace WebSocketSharp +{ + public sealed class ClientWebSocket : WebSocket + { + private const int maxRetryCountForConnect = 10; + private readonly string base64Key; + + private readonly string[] protocols; + + private Uri uri; + private AuthenticationChallenge authChallenge; + private NetworkCredential credentials; + private bool enableRedirection; + private bool extensionsRequested; + + private int insideHandshakeBlock; + private uint nonceCount; + private string origin; + private bool preAuth; + private bool protocolsRequested; + private NetworkCredential proxyCredentials; + private Uri proxyUri; + private int retryCountForConnect; + private bool secure; + private ClientSslConfiguration sslConfig; + + private TcpClient tcpClient; + + + /// + /// 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 ClientWebSocket(string url, params string[] protocols) + : base(TimeSpan.FromSeconds(5)) + { + if (url == null) + throw new ArgumentNullException(nameof(url)); + + if (url.Length == 0) + throw new ArgumentException("An empty string.", nameof(url)); + + string msg; + if (!url.TryCreateWebSocketUri(out uri, out msg)) + throw new ArgumentException(msg, nameof(url)); + + if (protocols != null && protocols.Length > 0) + { + if (!CheckProtocols(protocols, out msg)) + throw new ArgumentException(msg, nameof(protocols)); + + this.protocols = protocols; + } + + base64Key = CreateBase64Key(); + logger = new Logger(); + secure = uri.Scheme == "wss"; // can be changed later !? + } + + + public override bool IsSecure + { + get + { + return secure; + } + } + + + /// + /// Gets or sets underlying socket read or write timeout. + /// + public override int ReadWriteTimeout + { + get + { + return base.ReadWriteTimeout; + } + + set + { + base.ReadWriteTimeout = value; + +#if !XAMARIN + if (tcpClient != null) + { + tcpClient.ReceiveTimeout = value; + tcpClient.SendTimeout = value; + } +#endif + } + } + + + /// + /// 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 (!secure) + { + throw new InvalidOperationException("This instance does not use a secure connection."); + } + + return GetSslConfiguration(); + } + } + + /// + /// Gets the URL to which to connect. + /// + /// + /// A that represents the URL to which to connect. + /// + public override Uri Url + { + get + { + return uri; + } + } + + + /// + /// 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 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 + { + if (compression == value) + return; + + lock (forState) + { + if (!CanModifyConnectionProperties(out var msg)) + { + logger.Warn(msg); + return; + } + + compression = 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 + { + lock (forState) + { + if (!CanModifyConnectionProperties(out var msg)) + { + logger.Warn(msg); + return; + } + + enableRedirection = 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 + { + if (!value.IsNullOrEmpty()) + { + if (!Uri.TryCreate(value, UriKind.Absolute, out var result)) + { + throw new ArgumentException("Not an absolute URI string.", nameof(value)); + } + + if (result.Segments.Length > 1) + { + throw new ArgumentException("It includes the path segments.", nameof(value)); + } + } + + lock (forState) + { + if (!CanModifyConnectionProperties(out var msg)) + { + logger.Warn(msg); + return; + } + + origin = !value.IsNullOrEmpty() ? value.TrimEnd('/') : value; + } + } + } + + + private static bool CheckProtocols(string[] protocols, out string message) + { + message = null; + + if (protocols.Contains(protocol => protocol.IsNullOrEmpty() || !protocol.IsToken())) + { + 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; + } + + + protected override void MessageHandler(MessageEventArgs e) + { + for (; ; ) + { + CallOnMessage(e); + + e = DequeueNextMessage(); + if (e == null) + break; + } + } + + + 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; + } + + + // As client + private bool PerformConnectSequence() + { + bool TryEnterHandshakeBlock() + { + // if (insideHandshakeBlock == 0) insideHandshakeBlock = 1 + // returns previous value + return Interlocked.CompareExchange(ref insideHandshakeBlock, 1, 0) > 0; + } + + lock (forState) + { + if (readyState == WebSocketState.Open) + { + logger.Warn("The connection has already been established."); + + return false; + } + + if (readyState == WebSocketState.Closing) + { + logger.Error("The close process has set in."); + + Error("An interruption has occurred while attempting to connect.", null); + + return false; + } + + if (retryCountForConnect > maxRetryCountForConnect) + { + logger.Error("An opportunity for reconnecting has been lost."); + + Error("An interruption has occurred while attempting to connect.", null); + + return false; + } + + readyState = WebSocketState.Connecting; + } // lock + + if (TryEnterHandshakeBlock()) + { + // alredy in the handshake.. What does it do here twice at all. + Fatal("Connect - doHandshake doing it twice!", null); + return false; + } + + try + { + // this acquires send lock + // i'll release _forState lock and then acquire it after + // and protect for double parallel call of doHandshake with interlocked + + DoHandshake(); + } + catch (Exception ex) + { + lock (forState) + { + retryCountForConnect++; + } + + logger.Fatal(ex.Message); + logger.Debug(ex.ToString()); + + Fatal("An exception has occurred while attempting to connect.", ex); + + return false; + } + finally + { + insideHandshakeBlock = 0; + } + + lock (forState) + { + if (readyState != WebSocketState.Connecting) + { + Fatal($"Socket state error, expected Connecting, was: {readyState}", null); + + return false; + } + + retryCountForConnect = 1; + readyState = WebSocketState.Open; + return true; + } // lock + } + + + // As client + private string CreateExtensions() + { + var buff = new StringBuilder(80); + + var compressionMethod = compression; + + if (compressionMethod != CompressionMethod.None) + { + var str = compressionMethod.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 client + private void DoHandshake() + { + SetClientStream(); + var res = SendHandshakeRequest(); + + string msg; + if (!CheckHandshakeResponse(res, out msg)) + throw new WebSocketException(CloseStatusCode.ProtocolError, msg); + + if (protocolsRequested) + { + var resHeader = res.Headers["Sec-WebSocket-Protocol"]; + protocol = resHeader; + } + + if (extensionsRequested) + { + var resHeader = res.Headers["Sec-WebSocket-Extensions"]; + + if (resHeader != null) + { + extensions = resHeader; + } + else + { + compression = CompressionMethod.None; + } + } + + AssignCookieCollection(res.Cookies); + } + + + // 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(); + + SetRequestCookies(ret); + + return ret; + } + + + // 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($"Received a proxy authentication requirement for '{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(true); + tcpClient = ConnectTcpClient(proxyUri.DnsSafeHost, proxyUri.Port, ConnectTimeout, ReadWriteTimeout); + socketStream = 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 = ConnectTcpClient(proxyUri.DnsSafeHost, proxyUri.Port, ConnectTimeout, ReadWriteTimeout); + socketStream = tcpClient.GetStream(); + SendProxyConnectRequest(); + } + else + { + tcpClient = ConnectTcpClient(uri.DnsSafeHost, uri.Port, ConnectTimeout, ReadWriteTimeout); + socketStream = 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( + socketStream, + false, + conf.ServerCertificateValidationCallback, + conf.ClientCertificateSelectionCallback); + + sslStream.AuthenticateAsClient( + host, + conf.ClientCertificates, + conf.EnabledSslProtocols, + conf.CheckCertificateRevocation); + + socketStream = sslStream; + } + catch (Exception ex) + { + throw new WebSocketException(CloseStatusCode.TlsHandshakeFailure, ex); + } + } + } + + + // As client + + + // As client + private void ReleaseClientResources(bool dispose) + { + try + { + if (dispose) + socketStream?.Dispose(); + } + catch + { + } + + socketStream = null; + + try + { + if (dispose) + tcpClient?.Close(); + } + catch + { + } + + tcpClient = null; + } + + + private static void OnEndConnect(IAsyncResult asyncResult) + { + var state = (TcpClientAsyncState)asyncResult.AsyncState; + + try + { + state.Client?.EndConnect(asyncResult); + } + catch (ObjectDisposedException) + { + } + catch (Exception e) + { + // this catches for example DNS lookup failures + state.Exception = e; + } + + try + { + asyncResult.AsyncWaitHandle.Close(); + } + catch + { + } + + try + { + state.EndConnectSignal.Set(); + } + catch + { + // could be disposed already + } + } + + + // ReSharper disable once UnusedParameter.Local + private static TcpClient ConnectTcpClient(string hostname, int port, int connectTimeout, int readWriteTimeout) + { +#if XAMARIN + var client = new TcpClient(AddressFamily.InterNetworkV6); +#else + var client = new TcpClient(); +#endif + using (var endConnectSignal = new ManualResetEvent(false)) + { + var state = new TcpClientAsyncState + { + Client = client, + EndConnectSignal = endConnectSignal + }; + + var result = client.BeginConnect(hostname, port, OnEndConnect, state); + // this one: + // bool success = result.AsyncWaitHandle.WaitOne(connectTimeout, true); + // does not work reliably, because + // result.AsyncWaitHandle is signalled sooner than EndConnect is + // on Mono, MD reported it is set even before connected = true; + + // the solution below is neither modern nor exciting but it should work + // and not lose exception messages in endconnect which help us with troubleshooting on location + + try + { + var sw = new Stopwatch(); + sw.Restart(); + + var waitOk = result.CompletedSynchronously || endConnectSignal.WaitOne(connectTimeout, true); + endConnectSignal.Close(); + sw.Stop(); + + // waitOk does not mean it is connected.. + // it means that the wait completed before timeout, meaning there was maybe an exception in EndConnect + + if (client.Connected && state.Exception == null && waitOk) + { + // Debug.Print($"Connection looks good! {hostname}:{port}"); + } + else + { + var spent = sw.ElapsedMilliseconds; + + try + { + client.Close(); // can this throw? + } + catch + { + } + + if (state.Exception != null) // there was an exception in endconnect.... I did not want to put it into inner exception, logging then takes more effort and space + throw state.Exception; + else if (!waitOk) + throw new TimeoutException($"Failed to connect to server {hostname}:{port} timeout={connectTimeout} spent={spent}ms"); + else + throw new TimeoutException($"Failed to connect to server {hostname}:{port} not connected (!) spent={spent}ms"); + } + } + catch (ObjectDisposedException) + { + // toto: log + } + } // using + +#if !XAMARIN + client.ReceiveTimeout = readWriteTimeout; + client.SendTimeout = readWriteTimeout; +#endif + + return client; + } + + + // 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 compressionMethod = compression; + var comp = compressionMethod != CompressionMethod.None; + foreach (var e in value.SplitHeaderValue(',')) + { + var ext = e.Trim(); + if (comp && ext.IsCompressionExtension(compressionMethod)) + { + 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 = compressionMethod.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; + } + + + // As client + private HttpResponse SendHandshakeRequest() + { + var req = CreateHandshakeRequest(); + var res = SendHttpRequest(req, 90000); + if (res.IsUnauthorized) + { + var chal = res.Headers["WWW-Authenticate"]; + logger.Warn($"Received an authentication requirement for '{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(true); + 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($"Received a redirection to '{url}'."); + if (enableRedirection) + { + if (url.IsNullOrEmpty()) + { + logger.Error("No url to redirect is located."); + return res; + } + + if (!url.TryCreateWebSocketUri(out var result, out var msg)) + { + logger.Error("An invalid url to redirect is located: " + msg); + return res; + } + + ReleaseClientResources(true); + + this.uri = result; + secure = result.Scheme == "wss"; + + SetClientStream(); + return SendHandshakeRequest(); + } + } + + return res; + } + + + // As client + private HttpResponse SendHttpRequest(HttpRequest request, int millisecondsTimeout) + { + logger.Debug($"A request to the server: {request}"); + var res = request.GetResponse(socketStream, millisecondsTimeout); + logger.Debug($"A response to this request: {res}"); + + return res; + } + + + private ClientSslConfiguration GetSslConfiguration() + { + if (sslConfig == null) + sslConfig = new ClientSslConfiguration(uri.DnsSafeHost); + + return sslConfig; + } + + + private protected override void PerformCloseSequence(PayloadData payloadData, bool send, bool receive, bool received) + { + Stream streamForLater; + ManualResetEvent receivingExitedForLater; + TcpClient tcpClientForLater; + + bool DoClosingHandshake() + { + var clean = false; + + try + { + clean = CloseHandshake(streamForLater, receivingExitedForLater, payloadData, send, receive, received); + } + catch + { + } + + try + { + streamForLater?.Dispose(); + } + catch + { + } + + try + { + tcpClientForLater?.Close(); + } + catch + { + } + + try + { + receivingExitedForLater?.Dispose(); + } + catch + { + } + + return clean; + } + + 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; + + streamForLater = socketStream; + tcpClientForLater = tcpClient; + receivingExitedForLater = receivingExitedEvent; + + ReleaseClientResources(false); // no disposal + + ReleaseCommonResources(false); // no disposal of _receivingExited + + readyState = WebSocketState.Closed; + } // lock + + logger.Trace("Begin closing the connection."); + + // call outside lock + var wasClean = DoClosingHandshake(); + + logger.Trace("End closing the connection."); + + CallOnClose(new CloseEventArgs(payloadData) + { + WasClean = wasClean + }); + } + + + internal static string CreateBase64Key() + { + var src = new byte[16]; + RandomNumber.GetBytes(src); + + return Convert.ToBase64String(src); + } + + + /// + /// 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 (readyState == WebSocketState.Closing) + { + throw new InvalidOperationException("The close process is in progress."); + } + + if (retryCountForConnect > maxRetryCountForConnect) + { + throw new InvalidOperationException("A series of reconnecting has failed."); + } + + if (PerformConnectSequence()) + 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 (readyState == WebSocketState.Closing) + { + throw new InvalidOperationException("The close process is in progress."); + } + + if (retryCountForConnect > maxRetryCountForConnect) + { + throw new InvalidOperationException("A series of reconnecting has failed."); + } + +#if NET_CORE + var task = System.Threading.Tasks.Task.Factory.StartNew(PerformConnectSequence); + + task.ContinueWith((t) => + { + if (!t.IsFaulted && t.Exception == null && t.Result) + { + open(); + } + else + { + PerformCloseSequence(1006, "could not open"); + } + }); +#else + Func connector = PerformConnectSequence; + + connector.BeginInvoke( + ar => + { + if (connector.EndInvoke(ar)) + open(); + }, + null + ); +#endif + } + + + /// + /// 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 isPreAuth) + { + if (!username.IsNullOrEmpty()) + { + if (username.Contains(':') || !username.IsText()) + { + throw new ArgumentException("It contains an invalid character.", nameof(username)); + } + } + + if (!password.IsNullOrEmpty()) + { + if (!password.IsText()) + { + throw new ArgumentException("It contains an invalid character.", nameof(password)); + } + } + + if (!CanModifyConnectionProperties(out var msg)) + { + logger.Warn(msg); + return; + } + + lock (forState) + { + if (!CanModifyConnectionProperties(out msg)) + { + logger.Warn(msg); + return; + } + + if (username.IsNullOrEmpty()) + { + credentials = null; + this.preAuth = false; + + return; + } + + credentials = new NetworkCredential( + username, password, uri.PathAndQuery + ); + + this.preAuth = isPreAuth; + } + } + + + /// + /// 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; + + Uri theUri = null; + + if (!url.IsNullOrEmpty()) + { + if (!Uri.TryCreate(url, UriKind.Absolute, out theUri)) + { + throw new ArgumentException("Not an absolute URI string.", nameof(url)); + } + + if (theUri.Scheme != "http") + { + throw new ArgumentException("The scheme part is not http.", nameof(url)); + } + + if (theUri.Segments.Length > 1) + { + throw new ArgumentException("It includes the path segments.", nameof(url)); + } + } + + if (!username.IsNullOrEmpty()) + { + if (username.Contains(':') || !username.IsText()) + { + throw new ArgumentException("It contains an invalid character.", nameof(username)); + } + } + + if (!password.IsNullOrEmpty()) + { + if (!password.IsText()) + { + throw new ArgumentException("It contains an invalid character.", nameof(password)); + } + } + + if (!CanModifyConnectionProperties(out msg)) + { + logger.Warn(msg); + return; + } + + lock (forState) + { + if (!CanModifyConnectionProperties(out msg)) + { + logger.Warn(msg); + return; + } + + if (url.IsNullOrEmpty()) + { + proxyUri = null; + proxyCredentials = null; + + return; + } + + proxyUri = theUri; + proxyCredentials = !username.IsNullOrEmpty() ? new NetworkCredential(username, password, $"{this.uri.DnsSafeHost}:{this.uri.Port}") : null; + } + } + + + private protected override WebSocketFrame CreateCloseFrame(PayloadData payloadData) + { + return WebSocketFrame.CreateCloseFrame(payloadData, true); + } + + + private protected override WebSocketFrame CreatePongFrame(PayloadData payloadData) + { + return WebSocketFrame.CreatePongFrame(payloadData, true); + } + + + private protected override WebSocketFrame CreateFrame(Fin fin, Opcode opcode, byte[] data, bool compressed) + { + return new WebSocketFrame(fin, opcode, data, compressed, true); + } + + + private protected override void CheckCode(ushort code) + { + if (code == 1011) + { + throw new ArgumentException("1011 cannot be used.", nameof(code)); + } + } + + + private protected override void CheckCloseStatus(CloseStatusCode code) + { + if (code == CloseStatusCode.ServerError) + { + throw new ArgumentException("ServerError cannot be used.", nameof(code)); + } + } + + + private protected override string CheckFrameMask(WebSocketFrame frame) + { + if (frame.IsMasked) + { + return "A frame from the server is masked."; + } + + return null; + } + + + private protected override void UnmaskFrame(WebSocketFrame frame) + { + frame.Unmask(); + } + + + private class TcpClientAsyncState + { + public TcpClient Client; + public ManualResetEvent EndConnectSignal; + public Exception Exception; + } + } +} diff --git a/websocket-sharp/CloseEventArgs.cs b/websocket-sharp/CloseEventArgs.cs index c665ccde9..51aa4a2bf 100644 --- a/websocket-sharp/CloseEventArgs.cs +++ b/websocket-sharp/CloseEventArgs.cs @@ -137,6 +137,16 @@ internal set { } } + public Exception Exception + { + get => _payloadData.Exception; + } + + public string CallerDbgInfo + { + get => _payloadData.CallerDbgInfo; + } + #endregion } } diff --git a/websocket-sharp/Ext.cs b/websocket-sharp/Ext.cs index 5e42b235c..6837dcd90 100644 --- a/websocket-sharp/Ext.cs +++ b/websocket-sharp/Ext.cs @@ -51,11 +51,8 @@ using System.IO; using System.IO.Compression; using System.Net.Sockets; -using System.Security.Cryptography.X509Certificates; using System.Text; using WebSocketSharp.Net; -using WebSocketSharp.Net.WebSockets; -using WebSocketSharp.Server; namespace WebSocketSharp { @@ -182,18 +179,6 @@ internal static byte[] Append (this ushort code, string reason) return ret; } - internal static void Close (this HttpListenerResponse response, HttpStatusCode code) - { - response.StatusCode = (int) code; - response.OutputStream.Close (); - } - - internal static void CloseWithAuthChallenge ( - this HttpListenerResponse response, string challenge) - { - response.Headers.InternalSet ("WWW-Authenticate", challenge, true); - response.Close (HttpStatusCode.Unauthorized); - } internal static byte[] Compress (this byte[] data, CompressionMethod method) { @@ -678,6 +663,8 @@ internal static bool KeepsAlive ( this NameValueCollection headers, Version version ) { + if (version == null) + return false; var comparison = StringComparison.OrdinalIgnoreCase; return version < HttpVersion.Version11 ? headers.Contains ("Connection", "keep-alive", comparison) @@ -2000,51 +1987,6 @@ public static string UrlEncode (this string value) : value; } - /// - /// Writes and sends the specified data with the specified - /// . - /// - /// - /// A that represents the HTTP response used to - /// send the content data. - /// - /// - /// An array of that represents the content data to send. - /// - /// - /// - /// is . - /// - /// - /// -or- - /// - /// - /// is . - /// - /// - public static void WriteContent (this HttpListenerResponse response, byte[] content) - { - if (response == null) - throw new ArgumentNullException ("response"); - - if (content == null) - throw new ArgumentNullException ("content"); - - var len = content.LongLength; - if (len == 0) { - response.Close (); - return; - } - - response.ContentLength64 = len; - var output = response.OutputStream; - if (len <= Int32.MaxValue) - output.Write (content, 0, (int) len); - else - output.WriteBytes (content, 1024); - - output.Close (); - } #endregion } diff --git a/websocket-sharp/LogData.cs b/websocket-sharp/LogData.cs index 9c0843093..92cf7ff15 100644 --- a/websocket-sharp/LogData.cs +++ b/websocket-sharp/LogData.cs @@ -28,7 +28,6 @@ using System; using System.Diagnostics; -using System.Text; namespace WebSocketSharp { @@ -39,7 +38,7 @@ public class LogData { #region Private Fields - private StackFrame _caller; + private string _caller; private DateTime _date; private LogLevel _level; private string _message; @@ -48,7 +47,7 @@ public class LogData #region Internal Constructors - internal LogData (LogLevel level, StackFrame caller, string message) + internal LogData (LogLevel level, string caller, string message) { _level = level; _caller = caller; @@ -66,7 +65,7 @@ internal LogData (LogLevel level, StackFrame caller, string message) /// /// A that provides the information of the logging method caller. /// - public StackFrame Caller { + public string Caller { get { return _caller; } @@ -120,28 +119,8 @@ public string Message { /// public override string ToString () { - var header = String.Format ("{0}|{1,-5}|", _date, _level); - var method = _caller.GetMethod (); - var type = method.DeclaringType; -#if DEBUG - var lineNum = _caller.GetFileLineNumber (); - var headerAndCaller = - String.Format ("{0}{1}.{2}:{3}|", header, type.Name, method.Name, lineNum); -#else - var headerAndCaller = String.Format ("{0}{1}.{2}|", header, type.Name, method.Name); -#endif - var msgs = _message.Replace ("\r\n", "\n").TrimEnd ('\n').Split ('\n'); - if (msgs.Length <= 1) - return String.Format ("{0}{1}", headerAndCaller, _message); - - var buff = new StringBuilder (String.Format ("{0}{1}\n", headerAndCaller, msgs[0]), 64); - - var fmt = String.Format ("{{0,{0}}}{{1}}\n", header.Length); - for (var i = 1; i < msgs.Length; i++) - buff.AppendFormat (fmt, "", msgs[i]); - - buff.Length--; - return buff.ToString (); + var msgs = string.IsNullOrEmpty(_message) ? string.Empty : _message.Replace("\r\n", "; ").Replace("\n", "; ").Trim(); + return $"{_level} {msgs} caller={_caller}"; } #endregion diff --git a/websocket-sharp/Logger.cs b/websocket-sharp/Logger.cs index 17850e67e..26e7e70c9 100644 --- a/websocket-sharp/Logger.cs +++ b/websocket-sharp/Logger.cs @@ -27,8 +27,8 @@ #endregion using System; -using System.Diagnostics; using System.IO; +using System.Runtime.CompilerServices; namespace WebSocketSharp { @@ -57,7 +57,7 @@ public class Logger private volatile string _file; private volatile LogLevel _level; private Action _output; - private object _sync; + private readonly object _sync; #endregion @@ -187,28 +187,27 @@ public Action Output { #region Private Methods - private static void defaultOutput (LogData data, string path) + private static void defaultOutput(LogData data, string path) { - var log = data.ToString (); - Console.WriteLine (log); - if (path != null && path.Length > 0) - writeToFile (log, path); + // do not write to console, it pollutes linux stdout + if (string.IsNullOrEmpty(path)) + return; + + var log = data.ToString(); + writeToFile(log, path); } - private void output (string message, LogLevel level) + private void output (string message, LogLevel level, string caller) { lock (_sync) { if (_level > level) return; - LogData data = null; try { - data = new LogData (level, new StackFrame (2, true), message); + var data = new LogData (level, caller, message); _output (data, _file); } - catch (Exception ex) { - data = new LogData (LogLevel.Fatal, new StackFrame (0, true), ex.Message); - Console.WriteLine (data.ToString ()); + catch { } } } @@ -222,6 +221,13 @@ private static void writeToFile (string value, string path) #endregion + + private static string BuildCaller(string caller, string callerFile, int callerLine) + { + return $"fn={caller} in {callerFile}:{callerLine}"; + } + + #region Public Methods /// @@ -234,12 +240,15 @@ private static void writeToFile (string value, string path) /// /// A that represents the message to output as a log. /// - public void Debug (string message) + /// + /// + /// + public void Debug (string message, [CallerMemberName]string caller = null, [CallerFilePath] string callerFile = null, [CallerLineNumber] int callerLine = 0) { if (_level > LogLevel.Debug) return; - output (message, LogLevel.Debug); + output (message, LogLevel.Debug, BuildCaller(caller, callerFile, callerLine)); } /// @@ -252,12 +261,15 @@ public void Debug (string message) /// /// A that represents the message to output as a log. /// - public void Error (string message) + /// + /// + /// + public void Error (string message, [CallerMemberName]string caller = null, [CallerFilePath] string callerFile = null, [CallerLineNumber] int callerLine = 0) { if (_level > LogLevel.Error) return; - output (message, LogLevel.Error); + output (message, LogLevel.Error, BuildCaller(caller, callerFile, callerLine)); } /// @@ -266,9 +278,12 @@ public void Error (string message) /// /// A that represents the message to output as a log. /// - public void Fatal (string message) + /// + /// + /// + public void Fatal (string message, [CallerMemberName]string caller = null, [CallerFilePath] string callerFile = null, [CallerLineNumber] int callerLine = 0) { - output (message, LogLevel.Fatal); + output (message, LogLevel.Fatal, BuildCaller(caller, callerFile, callerLine)); } /// @@ -281,12 +296,15 @@ public void Fatal (string message) /// /// A that represents the message to output as a log. /// - public void Info (string message) + /// + /// + /// + public void Info (string message, [CallerMemberName]string caller = null, [CallerFilePath] string callerFile = null, [CallerLineNumber] int callerLine = 0) { if (_level > LogLevel.Info) return; - output (message, LogLevel.Info); + output (message, LogLevel.Info, BuildCaller(caller, callerFile, callerLine)); } /// @@ -299,14 +317,18 @@ public void Info (string message) /// /// A that represents the message to output as a log. /// - public void Trace (string message) + /// + /// + /// + public void Trace (string message, [CallerMemberName]string caller = null, [CallerFilePath] string callerFile = null, [CallerLineNumber] int callerLine = 0) { if (_level > LogLevel.Trace) return; - output (message, LogLevel.Trace); + output (message, LogLevel.Trace, BuildCaller(caller, callerFile, callerLine)); } + /// /// Outputs as a log with . /// @@ -317,12 +339,15 @@ public void Trace (string message) /// /// A that represents the message to output as a log. /// - public void Warn (string message) + /// + /// + /// + public void Warn (string message, [CallerMemberName]string caller = null, [CallerFilePath] string callerFile = null, [CallerLineNumber] int callerLine = 0) { if (_level > LogLevel.Warn) return; - output (message, LogLevel.Warn); + output (message, LogLevel.Warn, BuildCaller(caller, callerFile, callerLine)); } #endregion diff --git a/websocket-sharp/Net/ChunkStream.cs b/websocket-sharp/Net/ChunkStream.cs index a5271b573..6b57eabda 100644 --- a/websocket-sharp/Net/ChunkStream.cs +++ b/websocket-sharp/Net/ChunkStream.cs @@ -41,7 +41,6 @@ using System.Collections.Generic; using System.Globalization; using System.IO; -using System.Net; using System.Text; namespace WebSocketSharp.Net @@ -257,7 +256,7 @@ private InputChunkState setTrailer (byte[] buffer, ref int offset, int length) private static void throwProtocolViolation (string message) { - throw new WebException (message, null, WebExceptionStatus.ServerProtocolViolation, null); + throw new WebSocketProtocolViolationException (message); } private void write (byte[] buffer, ref int offset, int length) diff --git a/websocket-sharp/Net/HttpConnection.cs b/websocket-sharp/Net/HttpConnection.cs index a318e43f2..f848db25d 100644 --- a/websocket-sharp/Net/HttpConnection.cs +++ b/websocket-sharp/Net/HttpConnection.cs @@ -294,7 +294,7 @@ private static void onRead (IAsyncResult asyncResult) if (conn.processInput (conn._requestBuffer.GetBuffer (), len)) { if (!conn._context.HasError) - conn._context.Request.FinishInitialization (); + conn._context.Request.FinishInitialization (len - conn._position); if (conn._context.HasError) { conn.SendError (); diff --git a/websocket-sharp/Net/HttpListenerAsyncResult.cs b/websocket-sharp/Net/HttpListenerAsyncResult.cs index a1c737421..de311b7ff 100644 --- a/websocket-sharp/Net/HttpListenerAsyncResult.cs +++ b/websocket-sharp/Net/HttpListenerAsyncResult.cs @@ -46,6 +46,7 @@ using System; using System.Threading; +using System.Threading.Tasks; namespace WebSocketSharp.Net { @@ -147,7 +148,7 @@ private static void complete (HttpListenerAsyncResult asyncResult) if (callback == null) return; - ThreadPool.QueueUserWorkItem ( + Task.Factory.StartNew ( state => { try { callback (asyncResult); diff --git a/websocket-sharp/Net/HttpListenerRequest.cs b/websocket-sharp/Net/HttpListenerRequest.cs index 953c9b956..0fa1f62af 100644 --- a/websocket-sharp/Net/HttpListenerRequest.cs +++ b/websocket-sharp/Net/HttpListenerRequest.cs @@ -687,7 +687,7 @@ internal void AddHeader (string headerField) } } - internal void FinishInitialization () + internal void FinishInitialization (int contentLength) { if (_protocolVersion == HttpVersion.Version10) { finishInitialization10 (); @@ -714,10 +714,18 @@ internal void FinishInitialization () if (_httpMethod == "POST" || _httpMethod == "PUT") { if (_contentLength <= 0 && !_chunked) { - _context.ErrorMessage = String.Empty; - _context.ErrorStatus = 411; - - return; + if (contentLength >= 0) + { + // we cannot reject a request if it does not have content-length provided + _contentLength = contentLength; + } + else + { + _context.ErrorMessage = String.Empty; + _context.ErrorStatus = 411; + } + + return; } } diff --git a/websocket-sharp/Net/HttpStreamAsyncResult.cs b/websocket-sharp/Net/HttpStreamAsyncResult.cs index 44189303c..f7456c44e 100644 --- a/websocket-sharp/Net/HttpStreamAsyncResult.cs +++ b/websocket-sharp/Net/HttpStreamAsyncResult.cs @@ -39,6 +39,7 @@ using System; using System.Threading; +using System.Threading.Tasks; namespace WebSocketSharp.Net { @@ -168,8 +169,25 @@ internal void Complete () if (_waitHandle != null) _waitHandle.Set (); - if (_callback != null) - _callback.BeginInvoke (this, ar => _callback.EndInvoke (ar), null); + if (_callback == null) + return; + +#if NET_CORE + void TheTask() + { + try + { + _callback(this); + } + catch + { + } + } + + _ = Task.Factory.StartNew(TheTask); +#else + _callback.BeginInvoke (this, ar => _callback.EndInvoke (ar), null); +#endif } } diff --git a/websocket-sharp/Net/ResponseStream.cs b/websocket-sharp/Net/ResponseStream.cs index 85059a407..a3e7e86be 100644 --- a/websocket-sharp/Net/ResponseStream.cs +++ b/websocket-sharp/Net/ResponseStream.cs @@ -293,8 +293,12 @@ public override void EndWrite (IAsyncResult asyncResult) public override void Flush () { - if (!_disposed && (_sendChunked || _response.SendChunked)) - flush (false); + // he won't send it if ContentLength64 is 0 although body is not empty... so help him + if (!_sendChunked && !_response.HeadersSent && _body != null && _body.Length > 0 && _response.ContentLength64 < _body.Length) + _response.ContentLength64 = _body.Length; + + if (!_disposed && (_sendChunked || _response.SendChunked)) + flush (false); } public override int Read (byte[] buffer, int offset, int count) diff --git a/websocket-sharp/Net/SslConfiguration.cs b/websocket-sharp/Net/SslConfiguration.cs new file mode 100644 index 000000000..bfd3e5ac0 --- /dev/null +++ b/websocket-sharp/Net/SslConfiguration.cs @@ -0,0 +1,172 @@ +#region License +/* + * SslConfiguration.cs + * + * This code is derived from ClientSslConfiguration.cs. + * + * The MIT License + * + * Copyright (c) 2014 liryna + * Copyright (c) 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 Authors +/* + * Authors: + * - Liryna + */ +#endregion + +using System.Net.Security; +using System.Security.Authentication; + +namespace WebSocketSharp.Net +{ + /// + /// Stores the parameters used to configure a instance. + /// + /// + /// The SslConfiguration class is an abstract class. + /// + public abstract class SslConfiguration + { + #region Private Fields + + private LocalCertificateSelectionCallback _certSelectionCallback; + private RemoteCertificateValidationCallback _certValidationCallback; + private bool _checkCertRevocation; + private SslProtocols _enabledProtocols; + + #endregion + + #region Protected Constructors + + /// + /// Initializes a new instance of the class with + /// the specified and + /// . + /// + /// + /// The enum value that represents the protocols used for + /// authentication. + /// + /// + /// true if the certificate revocation list is checked during authentication; + /// otherwise, false. + /// + protected SslConfiguration (SslProtocols enabledSslProtocols, bool checkCertificateRevocation) + { + _enabledProtocols = enabledSslProtocols; + _checkCertRevocation = checkCertificateRevocation; + } + + #endregion + + #region Protected Properties + + /// + /// Gets or sets the callback used to select a certificate to supply to the remote party. + /// + /// + /// If this callback returns , no certificate will be supplied. + /// + /// + /// A delegate that references the method + /// used to select a certificate. The default value is a function that only returns + /// . + /// + protected LocalCertificateSelectionCallback CertificateSelectionCallback { + get { + return _certSelectionCallback ?? + (_certSelectionCallback = + (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => + null); + } + + set { + _certSelectionCallback = value; + } + } + + /// + /// Gets or sets the callback used to validate the certificate supplied by the remote party. + /// + /// + /// If this callback returns true, the certificate will be valid. + /// + /// + /// A delegate that references the method + /// used to validate the certificate. The default value is a function that only returns + /// true. + /// + protected RemoteCertificateValidationCallback CertificateValidationCallback { + get { + return _certValidationCallback ?? + (_certValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true); + } + + set { + _certValidationCallback = value; + } + } + + #endregion + + #region Public Properties + + /// + /// Gets or sets a value indicating whether the certificate revocation list is checked + /// during authentication. + /// + /// + /// true if the certificate revocation list is checked; otherwise, false. + /// + public bool CheckCertificateRevocation { + get { + return _checkCertRevocation; + } + + set { + _checkCertRevocation = value; + } + } + + /// + /// Gets or sets the SSL protocols used for authentication. + /// + /// + /// The enum value that represents the protocols used for + /// authentication. + /// + public SslProtocols EnabledSslProtocols { + get { + return _enabledProtocols; + } + + set { + _enabledProtocols = value; + } + } + + #endregion + } +} diff --git a/websocket-sharp/Net/WebSockets/HttpListenerWebSocketContext.cs b/websocket-sharp/Net/WebSockets/HttpListenerWebSocketContext.cs index eed49ce1c..6e84d825b 100644 --- a/websocket-sharp/Net/WebSockets/HttpListenerWebSocketContext.cs +++ b/websocket-sharp/Net/WebSockets/HttpListenerWebSocketContext.cs @@ -42,8 +42,8 @@ public class HttpListenerWebSocketContext : WebSocketContext { #region Private Fields - private HttpListenerContext _context; - private WebSocket _websocket; + private readonly HttpListenerContext _context; + private readonly ServerWebSocket _websocket; #endregion @@ -54,7 +54,7 @@ internal HttpListenerWebSocketContext ( ) { _context = context; - _websocket = new WebSocket (this, protocol); + _websocket = new ServerWebSocket (this, protocol); } #endregion @@ -353,7 +353,7 @@ public override System.Net.IPEndPoint UserEndPoint { /// /// A . /// - public override WebSocket WebSocket { + public override ServerWebSocket WebSocket { get { return _websocket; } diff --git a/websocket-sharp/Net/WebSockets/TcpListenerWebSocketContext.cs b/websocket-sharp/Net/WebSockets/TcpListenerWebSocketContext.cs index 519da7896..a76248470 100644 --- a/websocket-sharp/Net/WebSockets/TcpListenerWebSocketContext.cs +++ b/websocket-sharp/Net/WebSockets/TcpListenerWebSocketContext.cs @@ -62,7 +62,7 @@ internal class TcpListenerWebSocketContext : WebSocketContext private TcpClient _tcpClient; private IPrincipal _user; private System.Net.EndPoint _userEndPoint; - private WebSocket _websocket; + private ServerWebSocket _websocket; #endregion @@ -106,7 +106,7 @@ Logger log _userEndPoint = sock.RemoteEndPoint; _request = HttpRequest.Read (_stream, 90000); - _websocket = new WebSocket (this, protocol); + _websocket = new ServerWebSocket (this, protocol); } #endregion @@ -422,7 +422,7 @@ public override System.Net.IPEndPoint UserEndPoint { /// /// A . /// - public override WebSocket WebSocket { + public override ServerWebSocket WebSocket { get { return _websocket; } diff --git a/websocket-sharp/Net/WebSockets/WebSocketContext.cs b/websocket-sharp/Net/WebSockets/WebSocketContext.cs index 6921891f7..d22e6c5b6 100644 --- a/websocket-sharp/Net/WebSockets/WebSocketContext.cs +++ b/websocket-sharp/Net/WebSockets/WebSocketContext.cs @@ -217,7 +217,7 @@ protected WebSocketContext () /// /// A . /// - public abstract WebSocket WebSocket { get; } + public abstract ServerWebSocket WebSocket { get; } #endregion } diff --git a/websocket-sharp/PayloadData.cs b/websocket-sharp/PayloadData.cs index 4e629d88c..c83254556 100644 --- a/websocket-sharp/PayloadData.cs +++ b/websocket-sharp/PayloadData.cs @@ -29,6 +29,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Runtime.CompilerServices; namespace WebSocketSharp { @@ -104,7 +105,11 @@ internal PayloadData (byte[] data, long length) _length = length; } - internal PayloadData (ushort code, string reason) + internal PayloadData (ushort code, string reason, + Exception exception = null, + [CallerMemberName] string function = null, + [CallerFilePath] string sourceFilePath = "", + [CallerLineNumber] int sourceLineNumber = 0) { _code = code; _reason = reason ?? String.Empty; @@ -114,6 +119,9 @@ internal PayloadData (ushort code, string reason) _codeSet = true; _reasonSet = true; + + Exception = exception; + CallerDbgInfo = FormatCaller(function, sourceFilePath, sourceLineNumber); } #endregion @@ -190,6 +198,10 @@ public ulong Length { } } + public Exception Exception { get; } + + public string CallerDbgInfo { get; } + #endregion #region Internal Methods @@ -230,5 +242,16 @@ IEnumerator IEnumerable.GetEnumerator () } #endregion + + private static readonly char[] pathDelims = new[] { '\\', '/' }; + private static string FormatCaller(string function, string sourceFilePath, int sourceLineNumber) + { + // the path is stored as windows path and on linux, system.io.getfilename won't work + var ix = sourceFilePath.LastIndexOfAny(pathDelims); + if (ix >= 0 && ix < sourceFilePath.Length - 1) + sourceFilePath = sourceFilePath.Substring(ix + 1); + + return $"{function} in {sourceFilePath}:{sourceLineNumber}"; + } } } diff --git a/websocket-sharp/Server/HttpServer.cs b/websocket-sharp/Server/HttpServer.cs index 7c6bdf2b6..d749ed14e 100644 --- a/websocket-sharp/Server/HttpServer.cs +++ b/websocket-sharp/Server/HttpServer.cs @@ -46,11 +46,17 @@ using System.Security.Principal; using System.Text; using System.Threading; +using System.Threading.Tasks; using WebSocketSharp.Net; using WebSocketSharp.Net.WebSockets; namespace WebSocketSharp.Server { + public interface IHttpServerRequestHandler + { + Task HandleHttpRequest(HttpListenerContext context); + } + /// /// Provides a simple HTTP server that allows to accept /// WebSocket handshake requests. @@ -736,6 +742,10 @@ public WebSocketServiceManager WebSocketServices { } } + + public IHttpServerRequestHandler RequestHandler { get; set; } + + #endregion #region Public Events @@ -882,25 +892,42 @@ private void init ( _sync = new object (); } - private void processRequest (HttpListenerContext context) + private async Task processRequest (HttpListenerContext context) { - var method = context.Request.HttpMethod; - var evt = method == "GET" - ? OnGet - : method == "HEAD" - ? OnHead - : method == "POST" + var handler = RequestHandler; + if (handler != null) + { + try + { + await handler.HandleHttpRequest(context); + } + catch + { + } + + context.Response.Close(); // it's closed. but let's close it twice to be sure + + return; // if there is the handler, he always handles it. exit, done. + } + + // the legacy stuff + var method = context.Request.HttpMethod; + var evt = method == "GET" + ? OnGet + : method == "HEAD" + ? OnHead + : method == "POST" ? OnPost : method == "PUT" - ? OnPut - : method == "DELETE" - ? OnDelete - : method == "CONNECT" - ? OnConnect - : method == "OPTIONS" - ? OnOptions - : method == "TRACE" - ? OnTrace + ? OnPut + : method == "DELETE" + ? OnDelete + : method == "CONNECT" + ? OnConnect + : method == "OPTIONS" + ? OnOptions + : method == "TRACE" + ? OnTrace : null; if (evt != null) @@ -908,7 +935,7 @@ private void processRequest (HttpListenerContext context) else context.Response.StatusCode = 501; // Not Implemented - context.Response.Close (); + context.Response.Close (); } private void processRequest (HttpListenerWebSocketContext context) @@ -936,15 +963,14 @@ private void receiveRequest () HttpListenerContext ctx = null; try { ctx = _listener.GetContext (); - ThreadPool.QueueUserWorkItem ( - state => { + Task.Factory.StartNew (async () => { try { if (ctx.Request.IsUpgradeRequest ("websocket")) { processRequest (ctx.AcceptWebSocket (null)); return; } - processRequest (ctx); + await processRequest (ctx); } catch (Exception ex) { _log.Fatal (ex.Message); diff --git a/websocket-sharp/Server/WebSocketBehavior.cs b/websocket-sharp/Server/WebSocketBehavior.cs index b5e8ffeb7..edb205b30 100644 --- a/websocket-sharp/Server/WebSocketBehavior.cs +++ b/websocket-sharp/Server/WebSocketBehavior.cs @@ -27,6 +27,7 @@ #endregion using System; +using System.Collections.Generic; using System.Collections.Specialized; using System.IO; using WebSocketSharp.Net; @@ -47,7 +48,7 @@ public abstract class WebSocketBehavior : IWebSocketSession #region Private Fields private WebSocketContext _context; - private Func _cookiesValidator; + private Func, bool> _cookiesValidator; private bool _emitOnPing; private string _id; private bool _ignoreExtensions; @@ -55,7 +56,7 @@ public abstract class WebSocketBehavior : IWebSocketSession private string _protocol; private WebSocketSessionManager _sessions; private DateTime _startTime; - private WebSocket _websocket; + private ServerWebSocket _websocket; #endregion @@ -220,7 +221,7 @@ public WebSocketContext Context { /// The default value is . /// /// - public Func CookiesValidator { + public Func, bool> CookiesValidator { get { return _cookiesValidator; } @@ -412,8 +413,7 @@ private string checkHandshakeRequest (WebSocketContext context) if (_cookiesValidator != null) { var req = context.CookieCollection; - var res = context.WebSocket.CookieCollection; - if (!_cookiesValidator (req, res)) + if (!_cookiesValidator (req, context.WebSocket.Cookies)) return "It includes no cookie or an invalid one."; } diff --git a/websocket-sharp/Server/WebSocketServer.cs b/websocket-sharp/Server/WebSocketServer.cs index b1b7bf027..a6df0b57a 100644 --- a/websocket-sharp/Server/WebSocketServer.cs +++ b/websocket-sharp/Server/WebSocketServer.cs @@ -43,6 +43,7 @@ using System.Security.Principal; using System.Text; using System.Threading; +using System.Threading.Tasks; using WebSocketSharp.Net; using WebSocketSharp.Net.WebSockets; @@ -844,8 +845,8 @@ private void receiveRequest () TcpClient cl = null; try { cl = _listener.AcceptTcpClient (); - ThreadPool.QueueUserWorkItem ( - state => { + Task.Factory.StartNew ( + () => { try { var ctx = new TcpListenerWebSocketContext ( cl, null, _secure, _sslConfigInUse, _log @@ -854,7 +855,7 @@ private void receiveRequest () processRequest (ctx); } catch (Exception ex) { - _log.Error (ex.Message); + _log.Error (ex.ToString()); _log.Debug (ex.ToString ()); cl.Close (); @@ -867,7 +868,6 @@ private void receiveRequest () _log.Info ("The underlying listener is stopped."); break; } - _log.Fatal (ex.Message); _log.Debug (ex.ToString ()); diff --git a/websocket-sharp/Server/WebSocketServiceManager.cs b/websocket-sharp/Server/WebSocketServiceManager.cs index 8706f58fe..f26262414 100644 --- a/websocket-sharp/Server/WebSocketServiceManager.cs +++ b/websocket-sharp/Server/WebSocketServiceManager.cs @@ -32,6 +32,7 @@ using System.IO; using System.Text; using System.Threading; +using System.Threading.Tasks; using WebSocketSharp.Net; namespace WebSocketSharp.Server @@ -354,15 +355,15 @@ private void broadcast (Opcode opcode, Stream stream, Action completed) private void broadcastAsync (Opcode opcode, byte[] data, Action completed) { - ThreadPool.QueueUserWorkItem ( - state => broadcast (opcode, data, completed) + Task.Factory.StartNew ( + () => broadcast (opcode, data, completed) ); } private void broadcastAsync (Opcode opcode, Stream stream, Action completed) { - ThreadPool.QueueUserWorkItem ( - state => broadcast (opcode, stream, completed) + Task.Factory.StartNew ( + () => broadcast (opcode, stream, completed) ); } diff --git a/websocket-sharp/Server/WebSocketSessionManager.cs b/websocket-sharp/Server/WebSocketSessionManager.cs index f7144b0ce..8c1f5f7ed 100644 --- a/websocket-sharp/Server/WebSocketSessionManager.cs +++ b/websocket-sharp/Server/WebSocketSessionManager.cs @@ -33,6 +33,7 @@ using System.Linq; using System.Text; using System.Threading; +using System.Threading.Tasks; using System.Timers; namespace WebSocketSharp.Server @@ -370,15 +371,15 @@ private void broadcast (Opcode opcode, Stream stream, Action completed) private void broadcastAsync (Opcode opcode, byte[] data, Action completed) { - ThreadPool.QueueUserWorkItem ( - state => broadcast (opcode, data, completed) + Task.Factory.StartNew ( + () => broadcast (opcode, data, completed) ); } private void broadcastAsync (Opcode opcode, Stream stream, Action completed) { - ThreadPool.QueueUserWorkItem ( - state => broadcast (opcode, stream, completed) + Task.Factory.StartNew ( + () => broadcast (opcode, stream, completed) ); } @@ -438,7 +439,7 @@ private void stop (PayloadData payloadData, bool send) _sweepTimer.Enabled = false; foreach (var session in _sessions.Values.ToList ()) - session.Context.WebSocket.Close (payloadData, bytes); + session.Context.WebSocket.PerformCloseSessionSequence (payloadData, bytes); _state = ServerState.Stop; } diff --git a/websocket-sharp/ServerExt.cs b/websocket-sharp/ServerExt.cs new file mode 100644 index 000000000..d713ef4b0 --- /dev/null +++ b/websocket-sharp/ServerExt.cs @@ -0,0 +1,122 @@ +#region License +/* + * Ext.cs + * + * Some parts of this code are derived from Mono (http://www.mono-project.com): + * - GetStatusDescription is derived from HttpListenerResponse.cs (System.Net) + * - IsPredefinedScheme is derived from Uri.cs (System) + * - MaybeUri is derived from Uri.cs (System) + * + * The MIT License + * + * Copyright (c) 2001 Garrett Rooney + * Copyright (c) 2003 Ian MacLean + * Copyright (c) 2003 Ben Maurer + * Copyright (c) 2003, 2005, 2009 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2009 Stephane Delcroix + * 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: + * - Liryna + * - Nikola Kovacevic + * - Chris Swiedler + */ +#endregion + +using System; +using WebSocketSharp.Net; + +namespace WebSocketSharp +{ + /// + /// Provides a set of static methods for websocket-sharp. + /// + public static class ServerExt + { + + internal static void Close(this HttpListenerResponse response, HttpStatusCode code) + { + response.StatusCode = (int)code; + response.OutputStream.Close(); + } + + internal static void CloseWithAuthChallenge( + this HttpListenerResponse response, string challenge) + { + response.Headers.InternalSet("WWW-Authenticate", challenge, true); + response.Close(HttpStatusCode.Unauthorized); + } + + + + /// + /// Writes and sends the specified data with the specified + /// . + /// + /// + /// A that represents the HTTP response used to + /// send the content data. + /// + /// + /// An array of that represents the content data to send. + /// + /// + /// + /// is . + /// + /// + /// -or- + /// + /// + /// is . + /// + /// + public static void WriteContent(this HttpListenerResponse response, byte[] content) + { + if (response == null) + throw new ArgumentNullException("response"); + + if (content == null) + throw new ArgumentNullException("content"); + + var len = content.LongLength; + if (len == 0) + { + response.Close(); + return; + } + + response.ContentLength64 = len; + var output = response.OutputStream; + if (len <= Int32.MaxValue) + output.Write(content, 0, (int)len); + else + output.WriteBytes(content, 1024); + + output.Close(); + } + + } +} diff --git a/websocket-sharp/ServerWebSocket.cs b/websocket-sharp/ServerWebSocket.cs new file mode 100644 index 000000000..3fa07f32b --- /dev/null +++ b/websocket-sharp/ServerWebSocket.cs @@ -0,0 +1,839 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using WebSocketSharp.Net; +using WebSocketSharp.Net.WebSockets; + +// ReSharper disable UnusedMember.Global + +namespace WebSocketSharp +{ + public sealed class ServerWebSocket : WebSocket + { + private string base64Key; + private Action closeContext; + private Func handshakeRequestChecker; + private bool ignoreExtensions; + private WebSocketContext socketContext; + + + // As server + internal ServerWebSocket(HttpListenerWebSocketContext context, string protocol) + : base(TimeSpan.FromSeconds(1)) + { + this.socketContext = context; + this.protocol = protocol; + + closeContext = context.Close; + logger = context.Log; + IsSecure = context.IsSecureConnection; + socketStream = context.Stream; + } + + + // As server + internal ServerWebSocket(TcpListenerWebSocketContext context, string protocol) + : base(TimeSpan.FromSeconds(1)) + { + this.socketContext = context; + this.protocol = protocol; + + closeContext = context.Close; + logger = context.Log; + IsSecure = context.IsSecureConnection; + socketStream = context.Stream; + } + + + public override bool IsSecure { get; } + + /* + //internal CookieCollection CookieCollection { + // get => _cookies; + //}*/ + + // As server + internal Func CustomHandshakeRequestChecker + { + get + { + return handshakeRequestChecker; + } + + set + { + handshakeRequestChecker = value; + } + } + + // As server + internal bool IgnoreExtensions + { + get + { + return ignoreExtensions; + } + + set + { + ignoreExtensions = value; + } + } + + + /// + /// Gets the URL to which to connect. + /// + /// + /// A that represents the URL to which to connect. + /// + public override Uri Url + { + get + { + return socketContext?.RequestUri; + } + } + + + // As server + private bool AcceptInternal() + { + // this is server code.. the chance for cross thread call here is relatively low + + var webSocketState = readyState; + + if (webSocketState == WebSocketState.Open) + { + logger.Warn("The handshake request has already been accepted."); + + return false; + } + + if (webSocketState == WebSocketState.Closing) + { + logger.Error("The close process has set in."); + + Error("An interruption has occurred while attempting to accept.", null); + + return false; + } + + if (webSocketState == WebSocketState.Closed) + { + logger.Error("The connection has been closed."); + + Error("An interruption has occurred while attempting to accept.", null); + + return false; + } + + try + { + // this does send inside and acquires locks + // I really doubt accept can becalled in parallel, ifi it is, it is bad design and should fail setting _readyState + // and most probably it is never called. AcceptInternal() is + + if (!AcceptHandshake()) + return false; + } + catch (Exception ex) + { + logger.Fatal(ex.Message); + logger.Debug(ex.ToString()); + + Fatal("An exception has occurred while attempting to accept.", ex); + + return false; + } + + lock (forState) + { + if (readyState != WebSocketState.Connecting) + { + Fatal($"Socket state error, expected Connecting, was: {readyState}", null); + + return false; + } + + readyState = WebSocketState.Open; + return true; + } // lock + } + + + // As server + private bool AcceptHandshake() + { + logger.Debug($"A handshake request from {socketContext.UserEndPoint}: {socketContext}"); + + if (!CheckHandshakeRequest(socketContext, out var msg)) + { + logger.Error(msg); + + RefuseHandshake( + CloseStatusCode.ProtocolError, + "A handshake error has occurred while attempting to accept." + ); + + return false; + } + + var customCheck = CustomCheckHandshakeRequest(socketContext, out msg); + if (!customCheck) + { + logger.Error(msg); + + RefuseHandshake( + CloseStatusCode.PolicyViolation, + "A handshake error has occurred while attempting to accept." + ); + + return false; + } + + base64Key = socketContext.Headers["Sec-WebSocket-Key"]; + + if (protocol != null) + { + var vals = socketContext.SecWebSocketProtocols; + if (!vals.Contains(val => val == protocol)) + protocol = null; + } + + if (!ignoreExtensions) + { + var val = socketContext.Headers["Sec-WebSocket-Extensions"]; + if (val != null) + { + var buff = new StringBuilder(80); + + foreach (var elm in val.SplitHeaderValue(',')) + { + var extension = elm.Trim(); + if (extension.Length == 0) + continue; + + if (extension.IsCompressionExtension(CompressionMethod.Deflate)) + { + var compressionMethod = CompressionMethod.Deflate; + + buff.AppendFormat("{0}, ", compressionMethod.ToExtensionString("client_no_context_takeover", "server_no_context_takeover")); + + compression = compressionMethod; + + break; + } + } + + var len = buff.Length; + if (len > 2) + { + buff.Length = len - 2; + extensions = buff.ToString(); + } + } + } + + return SendHttpResponse(CreateHandshakeResponse()); + } + + + // 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 versionString = headers["Sec-WebSocket-Version"]; + if (versionString == null) + { + message = "It includes no Sec-WebSocket-Version header."; + return false; + } + + if (versionString != version) + { + message = "It includes an invalid Sec-WebSocket-Version header."; + return false; + } + + var protocolString = headers["Sec-WebSocket-Protocol"]; + if (protocolString != null && protocolString.Length == 0) + { + message = "It includes an invalid Sec-WebSocket-Protocol header."; + return false; + } + + if (!ignoreExtensions) + { + var extensionsString = headers["Sec-WebSocket-Extensions"]; + if (extensionsString != null && extensionsString.Length == 0) + { + message = "It includes an invalid Sec-WebSocket-Extensions header."; + return false; + } + } + + return true; + } + + + // As server + private void RefuseHandshake(CloseStatusCode code, string reason) + { + readyState = WebSocketState.Closing; + + var res = CreateHandshakeFailureResponse(HttpStatusCode.BadRequest); + SendHttpResponse(res); + + ReleaseServerResources(); + + readyState = WebSocketState.Closed; + + CallOnClose(new CloseEventArgs(code, reason)); + } + + + // 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; + + SetResponseCookies(ret); + + 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; + } + + + // As server + private HttpResponse CreateHandshakeFailureResponse(HttpStatusCode code) + { + var ret = HttpResponse.CreateCloseResponse(code); + ret.Headers["Sec-WebSocket-Version"] = version; + + return ret; + } + + + // As server + private void ReleaseServerResources() + { + if (closeContext == null) + return; + + closeContext(); + closeContext = null; + socketStream = null; + socketContext = null; + } + + + // As server + private bool SendHttpResponse(HttpResponse response) + { + logger.Debug($"A response to {socketContext.UserEndPoint}: {response}"); + + var stream = socketStream; + if (stream == null) + return false; + + lock (forSend) + { + return sendBytesInternal(stream, response.ToByteArray()); + } + } + + + private protected override void PerformCloseSequence(PayloadData payloadData, bool send, bool receive, bool received) + { + Stream streamForLater; + ManualResetEvent receivingExitedForLater; + + bool DoClosingHandshake() + { + var clean = false; + + try + { + clean = CloseHandshake(streamForLater, receivingExitedForLater, payloadData, send, receive, received); + } + catch + { + } + + try + { + receivingExitedForLater?.Dispose(); + } + catch + { + } + + return clean; + } + + 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; + + streamForLater = socketStream; + receivingExitedForLater = receivingExitedEvent; + + ReleaseServerResources(); + ReleaseCommonResources(false); // no disposal of _receivingExited + + readyState = WebSocketState.Closed; + } // lock + + logger.Trace("Begin closing the connection."); + + // call outside lock + var wasClean = DoClosingHandshake(); + + logger.Trace("End closing the connection."); + + CallOnClose(new CloseEventArgs(payloadData) + { + WasClean = wasClean + }); + } + + + protected override void MessageHandler(MessageEventArgs e) + { + CallOnMessage(e); + + e = DequeueNextMessage(); + if (e == null) + return; + + // process next message + Task.Factory.StartNew(() => MessageHandler(e)); + } + + + // As server + internal void CloseResponse(HttpResponse response) + { + readyState = WebSocketState.Closing; + + SendHttpResponse(response); + ReleaseServerResources(); + + readyState = WebSocketState.Closed; + } + + + // As server + internal void Close(HttpStatusCode code) + { + CloseResponse(CreateHandshakeFailureResponse(code)); + } + + + // As server + internal void PerformCloseSessionSequence(PayloadData payloadData, byte[] frameAsBytes) + { + Stream streamForLater; + ManualResetEvent receivingExitedForLater; + + bool SendClosingBytes() + { + var clean = false; + + try + { + if (frameAsBytes != null && streamForLater != null) + { + bool sent; + + lock (forSend) + { + sent = sendBytesInternal(streamForLater, frameAsBytes); + } + + var received = sent && receivingExitedForLater != null && receivingExitedForLater.WaitOne(WaitTime, true); + + clean = sent && received; + + logger.Debug($"SendClosingBytes: Was clean?: {clean} sent: {sent} received: {received}"); + } + } + catch + { + } + + // stream is not disposed on server + + try + { + receivingExitedForLater?.Dispose(); + } + catch + { + } + + return clean; + } + + 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; + + streamForLater = socketStream; + receivingExitedForLater = receivingExitedEvent; + + ReleaseServerResources(); + ReleaseCommonResources(false); + + readyState = WebSocketState.Closed; + } // lock + + logger.Trace("Begin closing the connection."); + + // call outside lock + var wasClean = SendClosingBytes(); + + logger.Trace("End closing the connection."); + + CallOnClose(new CloseEventArgs(payloadData) + { + WasClean = wasClean + }); + } + + + // As server + internal void InternalAccept() + { + // called from websocket behavior + + try + { + if (!AcceptHandshake()) + return; + } + catch (Exception ex) + { + logger.Fatal(ex.Message); + logger.Debug(ex.ToString()); + + Fatal("An exception has occurred while attempting to accept.", ex); + + return; + } + + readyState = WebSocketState.Open; + + open(); + } + + + // As server + internal bool Ping(byte[] frameAsBytes, TimeSpan timeout) + { + return HandlePing(frameAsBytes, timeout); + } + + + // As server + internal void Send(Opcode opcode, byte[] data, Dictionary cache) + { + if (readyState != WebSocketState.Open) + { + logger.Error("The connection is closing."); + return; + } + + var compressionMethod = compression; + + if (!cache.TryGetValue(compressionMethod, out var found)) + { + found = CreateFrame(Fin.Final, opcode, data.Compress(compressionMethod), compressionMethod != CompressionMethod.None).ToArray(); + + cache.Add(compressionMethod, found); + } + + var stream = socketStream; + if (stream == null) + { + logger.Error("The stream is null."); + return; + } + + lock (forSend) + { + sendBytesInternal(stream, found); + } + } + + + // As server + internal void Send(Opcode opcode, Stream stream, Dictionary cache) + { + var compressionMethod = compression; + + lock (forSend) + { + Stream found; + if (!cache.TryGetValue(compressionMethod, out found)) + { + found = stream.Compress(compressionMethod); + cache.Add(compressionMethod, found); + } + else + { + found.Position = 0; + } + + SendFragmentedInternal(opcode, found, compressionMethod != CompressionMethod.None); + } + } + + + /// + /// 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 (readyState == WebSocketState.Closing) + { + throw new InvalidOperationException("The close process is in progress."); + } + + if (readyState == WebSocketState.Closed) + { + throw new InvalidOperationException("The connection has already been closed."); + } + + if (AcceptInternal()) + 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 (readyState == WebSocketState.Closing) + { + throw new InvalidOperationException("The close process is in progress."); + } + + if (readyState == WebSocketState.Closed) + { + throw new InvalidOperationException("The connection has already been closed."); + } + +#if NET_CORE + var task = Task.Factory.StartNew(AcceptInternal); + + task.ContinueWith((t) => + { + if (!t.IsFaulted && t.Exception == null && t.Result) + { + open(); + } + else + { + //close(1006, "could not open"); // untested + } + }); +#else + Func acceptor = AcceptInternal; + + acceptor.BeginInvoke( + ar => + { + if (acceptor.EndInvoke(ar)) + open(); + }, + null + ); +#endif + } + + + private protected override WebSocketFrame CreateCloseFrame(PayloadData payloadData) + { + return WebSocketFrame.CreateCloseFrame(payloadData, false); + } + + + private protected override WebSocketFrame CreatePongFrame(PayloadData payloadData) + { + return WebSocketFrame.CreatePongFrame(payloadData, false); + } + + + private protected override WebSocketFrame CreateFrame(Fin fin, Opcode opcode, byte[] data, bool compressed) + { + return new WebSocketFrame(fin, opcode, data, compressed, false); + } + + + private protected override void CheckCode(ushort code) + { + if (code == 1010) + { + throw new ArgumentException("1010 cannot be used.", nameof(code)); + } + } + + + private protected override void CheckCloseStatus(CloseStatusCode code) + { + if (code == CloseStatusCode.MandatoryExtension) + { + throw new ArgumentException("MandatoryExtension cannot be used.", nameof(code)); + } + } + + + private protected override string CheckFrameMask(WebSocketFrame frame) + { + if (!frame.IsMasked) + { + return "A frame from a client is not masked."; + } + + return null; + } + + + private protected override void UnmaskFrame(WebSocketFrame frame) + { + } + } +} diff --git a/websocket-sharp/WebSocket.cs b/websocket-sharp/WebSocket.cs index ad4da5a6d..d3de036df 100644 --- a/websocket-sharp/WebSocket.cs +++ b/websocket-sharp/WebSocket.cs @@ -1,4 +1,5 @@ #region License + /* * WebSocket.cs * @@ -28,4142 +29,2464 @@ * 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 WebSocketSharp.Net; -using WebSocketSharp.Net.WebSockets; + +// ReSharper disable UnusedMember.Global 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; - private int _connectTimeout = 5000; - private int _readWriteTimeout = 5000; - - #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 underlying socket connect timeout. - /// - public int ConnectTimeout { - get { - return _connectTimeout; - } - - set { - _connectTimeout = value; - } - } - - /// - /// Gets or sets underlying socket read or write timeout. - /// - public int ReadWriteTimeout { - get { - return _readWriteTimeout; - } - - set { - _readWriteTimeout = value; - - if (_tcpClient!= null) { - _tcpClient.ReceiveTimeout = value; - _tcpClient.SendTimeout = value; - } - } - } - - /// - /// 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); - e.WasClean = res; - - try { - OnClose.Emit (this, e); - } - catch (Exception ex) { - _logger.Error (ex.ToString ()); - error ("An error has occurred during the OnClose event.", ex); - } - } - - 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; + /// + /// 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 abstract class WebSocket : IDisposable + { + protected const string version = "13"; + private const string guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + + /// + /// 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; + + protected readonly object forSend = new object(); + protected readonly object forState = new object(); // locks _readyState, _retryCountForConnect + + protected CompressionMethod compression = CompressionMethod.None; + protected string extensions; + protected string protocol; + protected volatile WebSocketState readyState = WebSocketState.Connecting; + protected ManualResetEvent receivingExitedEvent; // receiving completely stopped (when socket closes) + + protected Stream socketStream; + + protected volatile Logger logger; + + private readonly CookieCollection cookies = new CookieCollection(); // the cookies that are put into response + + private readonly Queue messageEventQueue = new Queue(); + + private MemoryStream fragmentsBuffer; + private bool fragmentsCompressed; + private Opcode fragmentsOpcode; + private bool inContinuation; + private volatile bool inMessage; + private int insidePingBlock; + + private ManualResetEvent pongReceivedEvent; + private TimeSpan waitTime; + + + static WebSocket() + { +#if NET_CORE + EmptyBytes = Array.Empty(); +#else + EmptyBytes = new byte[0]; +#endif + FragmentLength = 1016; + RandomNumber = new RNGCryptoServiceProvider(); + } + + + protected WebSocket(TimeSpan waitTime) + { + this.waitTime = waitTime; + } + + + /// + /// 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; + } + } + } + + internal bool HasMessage + { + get + { + lock (messageEventQueue) + return messageEventQueue.Count > 0; + } + } + + internal bool IsConnected + { + get + { + var webSocketState = readyState; + return webSocketState == WebSocketState.Open || webSocketState == WebSocketState.Closing; + } + } + + + /// + /// Gets or sets underlying socket connect timeout. + /// + public int ConnectTimeout { get; set; } = 5000; + + /// + /// Gets or sets underlying socket read or write timeout. + /// + public virtual int ReadWriteTimeout { get; set; } = 5000; + + + public abstract Uri Url { get; } + + /// + /// 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; set; } + + + /// + /// 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 PingInternal(EmptyBytes); + } + } + + /// + /// Gets a value indicating whether a secure connection is used. + /// + /// + /// true if this instance uses a secure connection; otherwise, + /// false. + /// + // ReSharper disable once UnusedMemberInSuper.Global + public abstract bool IsSecure { get; } + + /// + /// Gets the logging function. + /// + /// + /// The default logging level is . + /// + /// + /// A that provides the logging function. + /// + public Logger Log + { + get + { + // note: can be called from inside lock! + return logger; + } + + internal set + { + logger = 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 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(nameof(value), "Zero or less."); + + lock (forState) + { + if (!CanModifyConnectionProperties(out var msg)) + { + logger.Warn(msg); + return; + } + + waitTime = value; + } + } + } + + + /// + /// 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() + { + PerformCloseSequence(1001, String.Empty); + } + + + protected abstract void MessageHandler(MessageEventArgs e); + + + private protected MessageEventArgs DequeueNextMessage() + { + lock (messageEventQueue) + { + MessageEventArgs e; + + if (messageEventQueue.Count == 0 || readyState != WebSocketState.Open) + e = null; + else + e = messageEventQueue.Dequeue(); + + if (e == null) + inMessage = false; + + return e; + } + } + + + //internal CookieCollection CookieCollection { + // get => _cookies; + //} + + + /// + /// 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) + { + if (cookie == null) + throw new ArgumentNullException(nameof(cookie)); + + lock (forState) + { + if (!CanModifyConnectionProperties(out var msg)) + { + logger.Warn(msg); + return; + } + } + + // this should be in the lock above but better not. no lock nesting + lock (cookies.SyncRoot) + { + cookies.SetOrRemove(cookie); + } + } + + + private protected void SetResponseCookies(HttpResponse ret) + { + lock (cookies.SyncRoot) + { + if (cookies.Count > 0) + ret.SetCookies(cookies); + } + } + + + private protected void SetRequestCookies(HttpRequest ret) + { + lock (cookies.SyncRoot) + { + if (cookies.Count > 0) + ret.SetCookies(cookies); + } + } + + + private protected void AssignCookieCollection(CookieCollection cookieCollection) + { + if (cookieCollection == null) + return; + + lock (cookies.SyncRoot) + { + cookies.SetOrRemove(cookieCollection); + } + } + + + /// + /// 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; + + + protected bool CanModifyConnectionProperties(out string message) + { + var webSocketState = readyState; + + message = null; + + if (webSocketState == WebSocketState.Open) + { + message = "The connection has already been established."; + return false; + } + + if (webSocketState == WebSocketState.Closing) + { + message = "The connection is closing."; + return false; + } + + return true; + } + + + private bool CheckReceivedFrame(WebSocketFrame frame, out string message) + { + message = CheckFrameMask(frame); + if (!string.IsNullOrEmpty(message)) + 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; + } + + + protected void PerformCloseSequence(ushort code, string reason) + { + var webSocketState = readyState; + + if (webSocketState == WebSocketState.Closing) + { + logger.Info("The closing is already in progress."); + return; + } + + if (webSocketState == WebSocketState.Closed) + { + logger.Info("The connection has already been closed."); + return; + } + + if (code == 1005) + { + // == no status + PerformCloseSequence(PayloadData.Empty, true, true, false); + return; + } + + var send = !code.IsReserved(); + PerformCloseSequence(new PayloadData(code, reason), send, send, false); + } + + + private protected abstract void PerformCloseSequence(PayloadData payloadData, bool send, bool receive, bool received); + - AuthenticationResponse authRes = null; - if (_authChallenge != null && _credentials != null) { - authRes = new AuthenticationResponse (_authChallenge, _credentials, _nonceCount); - _nonceCount = authRes.NonceCount; - } - else if (_preAuth) { - authRes = new AuthenticationResponse (_credentials); - } + private void StartCloseAsyncTask(ushort code, string reason) + { + if (readyState == WebSocketState.Closing) + { + logger.Info("The closing is already in progress."); + return; + } - 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 (); - } - - _message.BeginInvoke (e, ar => _message.EndInvoke (ar), null); - } - - 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 (code, reason); - - try { - OnClose.Emit (this, e); - } - catch (Exception ex) { - _logger.Error (ex.Message); - _logger.Debug (ex.ToString ()); - } - } - - // As client - private void releaseClientResources () - { - if (_stream != null) { - try { - _stream.Dispose(); - } catch { } - _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) { - try { - stream.Dispose (); - } - catch { } - } - - 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); - _tcpClient = connectTcpClient(_proxyUri.DnsSafeHost, _proxyUri.Port, _connectTimeout); - _tcpClient.ReceiveTimeout = _readWriteTimeout; - _tcpClient.SendTimeout = _readWriteTimeout; - _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); - _tcpClient = connectTcpClient(_proxyUri.DnsSafeHost, _proxyUri.Port, _connectTimeout); - _tcpClient.ReceiveTimeout = _readWriteTimeout; - _tcpClient.SendTimeout = _readWriteTimeout; - _stream = _tcpClient.GetStream (); - sendProxyConnectRequest (); - } - else { - //_tcpClient = new TcpClient (_uri.DnsSafeHost, _uri.Port); - _tcpClient = connectTcpClient(_uri.DnsSafeHost, _uri.Port, _connectTimeout); - _tcpClient.ReceiveTimeout = _readWriteTimeout; - _tcpClient.SendTimeout = _readWriteTimeout; - _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 static TcpClient connectTcpClient(string hostname, int port, int connectTimeout) { - var client = new TcpClient(AddressFamily.InterNetworkV6); - var result = client.BeginConnect(hostname, port, onEndConnect, client); - bool success = result.AsyncWaitHandle.WaitOne(connectTimeout, true); - - if (!client.Connected) { - client.Close(); - throw new TimeoutException("Failed to connect server."); - } - - return client; - } - - private static void onEndConnect(IAsyncResult asyncResult) { - TcpClient client = (TcpClient)asyncResult.AsyncState; - - try { - client.EndConnect(asyncResult); - } - catch { } - - try { - asyncResult.AsyncWaitHandle.Close(); - } - catch { } - } - - 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); - e.WasClean = res; - - try { - OnClose.Emit (this, e); - } - catch (Exception ex) { - _logger.Error (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 ().UTF8Encode ()); - - 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 - } + if (readyState == WebSocketState.Closed) + { + logger.Info("The connection has already been closed."); + return; + } + + if (code == 1005) + { + // == no status + StartCloseAsyncTask(PayloadData.Empty, true, true, false); + return; + } + + var send = !code.IsReserved(); + StartCloseAsyncTask(new PayloadData(code, reason), send, send, false); + } + + + private void StartCloseAsyncTask( + PayloadData payloadData, bool send, bool receive, bool received + ) + { +#if NET_CORE + _ = System.Threading.Tasks.Task.Factory.StartNew(() => PerformCloseSequence(payloadData, send, receive, received)); +#else + Action closer = PerformCloseSequence; + + closer.BeginInvoke( + payloadData, send, receive, received, ar => closer.EndInvoke(ar), null + ); +#endif + } + + + //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} sent: {1} received: {2}", ret, sent, received + // ) + // ); + + // return ret; + //} + + + private protected bool CloseHandshake(Stream stream, ManualResetEvent receivingExited, PayloadData payloadData, bool send, bool receive, bool received) + { + var sent = false; + + if (send) + { + if (stream != null) + { + var frame = CreateCloseFrame(payloadData); + + lock (forSend) + { + sent = sendBytesInternal(stream, frame.ToArray()); + } + + UnmaskFrame(frame); + } + } + + var wait = !received && sent && receive && receivingExited != null; + if (wait) + received = receivingExited.WaitOne(waitTime, true); + + var ret = sent && received; + + logger.Debug($"Was clean?: {ret} sent: {sent} received: {received}"); + + return ret; + } + + + //private MessageEventArgs dequeueFromMessageEventQueue () + //{ + // lock (_messageEventQueue) + // return _messageEventQueue.Count > 0 ? _messageEventQueue.Dequeue () : null; + //} + + + private void EnqueueToMessageEventQueue(MessageEventArgs e) + { + lock (messageEventQueue) + messageEventQueue.Enqueue(e); + } + + + protected 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()); + } + } + + + protected void Fatal(string message, Exception exception) + { + try + { + var code = exception is WebSocketException + ? ((WebSocketException)exception).Code + : CloseStatusCode.Abnormal; + + var payload = new PayloadData((ushort)code, message, exception); + + PerformCloseSequence(payload, !code.IsReserved(), false, false); + } + catch + { + } + } + + + protected void Fatal(string message, CloseStatusCode code) + { + try + { + var payload = new PayloadData((ushort)code, message); + PerformCloseSequence(payload, !code.IsReserved(), false, false); + } + catch + { + } + } + + + private void message() + { + MessageEventArgs e; + lock (messageEventQueue) + { + if (inMessage || messageEventQueue.Count == 0 || readyState != WebSocketState.Open) + return; + + inMessage = true; + e = messageEventQueue.Dequeue(); + } + + MessageHandler(e); + } + + + protected 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; + lock (messageEventQueue) + { + if (messageEventQueue.Count == 0 || readyState != WebSocketState.Open) + { + inMessage = false; + return; + } + + e = messageEventQueue.Dequeue(); + } + +#if NET_CORE + _ = System.Threading.Tasks.Task.Factory.StartNew(() => + { + MessageHandler(e); + }); +#else + Action handler = MessageHandler; + + handler.BeginInvoke(e, ar => handler.EndInvoke(ar), null); +#endif + } + + + private bool PingInternal(byte[] data) + { + // client ping + + var frame = CreateFrame(Fin.Final, Opcode.Ping, data, false); + + return HandlePing(frame.ToArray(), waitTime); + } + + + private bool ProcessCloseFrame(WebSocketFrame frame) + { + // if there are unprocessed messages, process them + while (HasMessage) + message(); + + var payload = frame.PayloadData; + PerformCloseSequence(payload, !payload.HasReservedCode, false, true); + + return false; + } + + + 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."); + + if (readyState != WebSocketState.Open) + { + logger.Error("The connection is closing."); + return true; + } + + var pong = CreatePongFrame(frame.PayloadData); + var stream = this.socketStream; + if (stream == null) + return false; + + lock (forSend) + { + if (!sendBytesInternal(stream, pong.ToArray())) + return false; + } + + logger.Trace("A pong to this ping has been sent."); + + if (EmitOnPing) + { + UnmaskFrame(pong); + + EnqueueToMessageEventQueue(new MessageEventArgs(frame)); + } + + return true; + } + + + private bool ProcessPongFrame() + { + logger.Trace("A pong was received."); + + try + { + pongReceivedEvent?.Set(); + + logger.Trace("Pong has been signaled."); + + return true; + } + catch (Exception ex) + { + logger.Error(ex.Message); + logger.Debug(ex.ToString()); + + return false; + } + } + + + 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.IsClose + ? ProcessCloseFrame(frame) + : ProcessUnsupportedFrame(frame); + } + + + 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; + } + + + protected void ReleaseCommonResources(bool disposeReceivingExited) + { + try + { + fragmentsBuffer?.Dispose(); + } + catch + { + } + + fragmentsBuffer = null; + inContinuation = false; + + DisposePongReceived(); + + DisposeReceivingExited(disposeReceivingExited); + } + + + private bool SendCompressFragmented(Opcode opcode, Stream stream) + { + lock (forSend) + { + var src = stream; + var compressed = false; + var sent = false; + try + { + var compressionMethod = compression; + + if (compressionMethod != CompressionMethod.None) + { + stream = stream.Compress(compressionMethod); + compressed = true; + } + + sent = SendFragmentedInternal(opcode, stream, compressed); + if (!sent) + { + Error($"Send failed. {opcode}", null); + } + } + catch (Exception ex) + { + logger.Error(ex.ToString()); + Error("An error has occurred during a send.", ex); + } + finally + { + if (compressed) + { + try + { + stream.Dispose(); + } + catch + { + } + } + + src.Dispose(); + } + + return sent; + } + } + + + protected static int ReadFromStream(Stream stream, byte[] buff, int length) + { + var done = 0; + + while (done < length) + { + var reallyRead = stream.Read(buff, done, length - done); + if (reallyRead <= 0) + break; // ?! eof + + done += reallyRead; + } + + return done; + } + + + private protected bool SendFragmentedInternal(Opcode opcode, Stream inputStream, bool compressed) + { + // caller locks + + var outputStream = socketStream; + + // returns false if send failed. there should be no other reason + var len = inputStream.Length; + if (len == 0) + return SendSingleFragmentInternal(outputStream, Fin.Final, opcode, EmptyBytes, false); // returns false if not sent + + var quo = len / FragmentLength; + var rem = (int)(len % FragmentLength); + + byte[] buff; + if (quo == 0) + { + buff = new byte[rem]; + return ReadFromStream(inputStream, buff, rem) == rem + && SendSingleFragmentInternal(outputStream, Fin.Final, opcode, buff, compressed); + } + + if (quo == 1 && rem == 0) + { + buff = new byte[FragmentLength]; + return ReadFromStream(inputStream, buff, FragmentLength) == FragmentLength + && SendSingleFragmentInternal(outputStream, Fin.Final, opcode, buff, compressed); + } + + /* Send fragments */ + + // Begin + buff = new byte[FragmentLength]; + var sent = ReadFromStream(inputStream, buff, FragmentLength) == FragmentLength + && SendSingleFragmentInternal(outputStream, 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 = ReadFromStream(inputStream, buff, FragmentLength) == FragmentLength + && SendSingleFragmentInternal(outputStream, Fin.More, Opcode.Cont, buff, false); + + if (!sent) + return false; + } + + // End + if (rem == 0) + rem = FragmentLength; + else + buff = new byte[rem]; + + return ReadFromStream(inputStream, buff, rem) == rem + && SendSingleFragmentInternal(outputStream, Fin.Final, Opcode.Cont, buff, false); + } + + + private bool SendSingleFragmentInternal(Stream stream, Fin fin, Opcode opcode, byte[] data, bool compressed) + { + // caller locks + + if (readyState != WebSocketState.Open) + { + logger.Error("The connection is closing."); + return false; + } + + if (stream == null) + { + logger.Error("The stream is null."); + return false; + } + + var frame = CreateFrame(fin, opcode, data, compressed); + + return sendBytesInternal(stream, frame.ToArray()); + } + + + private void SendCompressFragmentedAsync(Opcode opcode, Stream stream, Action completed) + { +#if NET_CORE + var task = System.Threading.Tasks.Task.Factory.StartNew(() => + { + var s = SendCompressFragmented(opcode, stream); + return s; + }); + + task.ContinueWith((t) => + { + if (!t.IsFaulted && t.Exception == null && t.Result) + { + if (completed != null) + completed(t.Result); + } + else + { + logger.Error(t.Exception?.ToString()); + Error("An error has occurred during the callback for an async send.", t.Exception == null ? null : t.Exception); + } + }); +#else + Func sender = SendCompressFragmented; + + 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 + ); +#endif + } + + + protected bool sendBytesInternal(Stream stream, byte[] bytes) + { + // caller locks + + try + { + stream.Write(bytes, 0, bytes.Length); + } + catch (Exception ex) + { + logger.Error(ex.Message); + logger.Debug(ex.ToString()); + + return false; + } + + return true; + } + + + private void startReceiving() + { + lock (messageEventQueue) + { + if (messageEventQueue.Count > 0) + messageEventQueue.Clear(); + } + + DisposePongReceived(); + DisposeReceivingExited(true); + + pongReceivedEvent = new ManualResetEvent(false); + receivingExitedEvent = new ManualResetEvent(false); + + ReceiveLoop(); + } + + + private void ReceiveLoop() + { + void OnReadCompleted(WebSocketFrame frame) + { + var receivedFrameResult = ProcessReceivedFrame(frame); + var closed = readyState == WebSocketState.Closed; + + if (!receivedFrameResult || closed) + { + logger.Info($"ReceiveLoop exit closed={closed} receivedFrameResult={receivedFrameResult}"); + + receivingExitedEvent?.Set(); + return; + } + + // Receive next asap because the Ping or Close needs a response to it. + ReceiveLoop(); + + if (inMessage || !HasMessage || readyState != WebSocketState.Open) + return; + + message(); + } + + void OnReadFailed(Exception ex) + { + logger.Fatal(ex.ToString()); + Fatal("An exception has occurred while receiving.", ex); + } + + WebSocketFrame.ReadFrameAsync(socketStream, false, OnReadCompleted, OnReadFailed); + } + + + private void DisposeReceivingExited(bool disposeReceivingExited) + { + try + { + if (disposeReceivingExited) + receivingExitedEvent?.Dispose(); + } + catch + { + } + + receivingExitedEvent = null; + } + + + private void DisposePongReceived() + { + try + { + pongReceivedEvent?.Dispose(); + } + catch + { + } + + pongReceivedEvent = null; + } + + + private protected void CallOnMessage(MessageEventArgs args) + { + try + { + OnMessage.Emit(this, args); + } + catch (Exception ex) + { + logger.Error(ex.ToString()); + Error("An error has occurred during an OnMessage event.", ex); + } + } + + + private protected void CallOnClose(CloseEventArgs args) + { + try + { + OnClose.Emit(this, args); + } + catch (Exception ex) + { + logger.Error(ex.ToString()); + Error("An error has occurred during the OnClose event.", ex); + } + } + + + 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().UTF8Encode()); + + return Convert.ToBase64String(src); + } + + + protected bool HandlePing(byte[] frameAsBytes, TimeSpan timeout) + { + bool TryEnterPingBlock() + { + // if (insidePingBlock == 0) insidePingBlock = 1 + // returns previous value + return Interlocked.CompareExchange(ref insidePingBlock, 1, 0) > 0; + } + + if (readyState != WebSocketState.Open) + return false; + + var pongReceived = this.pongReceivedEvent; + if (pongReceived == null) + return false; + + if (TryEnterPingBlock()) + { + // already in ping.. wait for result + + try + { + return pongReceived.WaitOne(timeout, true); + } + catch (Exception ex) + { + logger.Fatal($"HandlePing (a) {ex.Message}"); + + return false; + } + } + else + { + // send request and wait for reply + + try + { + pongReceived.Reset(); + + if (readyState != WebSocketState.Open) + return false; + + var stream = this.socketStream; + if (stream == null) + return false; + + lock (forSend) + { + if (!sendBytesInternal(stream, frameAsBytes)) + return false; + } + + return pongReceived.WaitOne(timeout, true); + } + catch (Exception ex) + { + logger.Fatal($"HandlePing (r) {ex.Message}"); + + return false; + } + finally + { + insidePingBlock = 0; + } + } + } + + + /// + /// Closes the connection. + /// + /// + /// This method does nothing if the current state of the connection is + /// Closing or Closed. + /// + public void Close() + { + PerformCloseSequence(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()) + { + throw new ArgumentOutOfRangeException(nameof(code), "Less than 1000 or greater than 4999."); + } + + CheckCode(code); + + PerformCloseSequence(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) + { + CheckCloseStatus(code); + + PerformCloseSequence((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()) + { + throw new ArgumentOutOfRangeException(nameof(code), "Less than 1000 or greater than 4999."); + } + + CheckCode(code); + + if (reason.IsNullOrEmpty()) + { + PerformCloseSequence(code, String.Empty); + return; + } + + if (code == 1005) + { + throw new ArgumentException("1005 cannot be used.", nameof(code)); + } + + byte[] bytes; + if (!reason.TryGetUTF8EncodedBytes(out bytes)) + { + throw new ArgumentException("It could not be UTF-8-encoded.", nameof(reason)); + } + + if (bytes.Length > 123) + { + throw new ArgumentOutOfRangeException(nameof(reason), "Its size is greater than 123 bytes."); + } + + PerformCloseSequence(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) + { + CheckCloseStatus(code); + + if (reason.IsNullOrEmpty()) + { + PerformCloseSequence((ushort)code, String.Empty); + return; + } + + if (code == CloseStatusCode.NoStatus) + { + throw new ArgumentException("NoStatus cannot be used.", nameof(code)); + } + + byte[] bytes; + if (!reason.TryGetUTF8EncodedBytes(out bytes)) + { + throw new ArgumentException("It could not be UTF-8-encoded.", nameof(reason)); + } + + if (bytes.Length > 123) + { + throw new ArgumentOutOfRangeException(nameof(reason), "Its size is greater than 123 bytes."); + } + + PerformCloseSequence((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() + { + StartCloseAsyncTask(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()) + { + throw new ArgumentOutOfRangeException(nameof(code), "Less than 1000 or greater than 4999."); + } + + CheckCode(code); + + StartCloseAsyncTask(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) + { + CheckCloseStatus(code); + + StartCloseAsyncTask((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()) + { + throw new ArgumentOutOfRangeException(nameof(code), "Less than 1000 or greater than 4999."); + } + + CheckCode(code); + + if (reason.IsNullOrEmpty()) + { + StartCloseAsyncTask(code, String.Empty); + return; + } + + if (code == 1005) + { + throw new ArgumentException("1005 cannot be used.", nameof(code)); + } + + byte[] bytes; + if (!reason.TryGetUTF8EncodedBytes(out bytes)) + { + throw new ArgumentException("It could not be UTF-8-encoded.", nameof(reason)); + } + + if (bytes.Length > 123) + { + throw new ArgumentOutOfRangeException(nameof(reason), "Its size is greater than 123 bytes."); + } + + StartCloseAsyncTask(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) + { + CheckCloseStatus(code); + + if (reason.IsNullOrEmpty()) + { + StartCloseAsyncTask((ushort)code, String.Empty); + return; + } + + if (code == CloseStatusCode.NoStatus) + { + throw new ArgumentException("NoStatus cannot be used.", nameof(code)); + } + + byte[] bytes; + if (!reason.TryGetUTF8EncodedBytes(out bytes)) + { + throw new ArgumentException("It could not be UTF-8-encoded.", nameof(reason)); + } + + if (bytes.Length > 123) + { + throw new ArgumentOutOfRangeException(nameof(reason), "Its size is greater than 123 bytes."); + } + + StartCloseAsyncTask((ushort)code, reason); + } + + + /// + /// 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 PingInternal(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 PingInternal(EmptyBytes); + + byte[] bytes; + if (!message.TryGetUTF8EncodedBytes(out bytes)) + { + throw new ArgumentException("It could not be UTF-8-encoded.", nameof(message)); + } + + if (bytes.Length > 125) + { + throw new ArgumentOutOfRangeException(nameof(message), "Its size is greater than 125 bytes."); + } + + return PingInternal(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 bool Send(byte[] data) + { + if (readyState != WebSocketState.Open) + { + throw new InvalidOperationException("The current state of the connection is not Open."); + } + + if (data == null) + throw new ArgumentNullException(nameof(data)); + + return SendCompressFragmented(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 bool Send(FileInfo fileInfo) + { + if (readyState != WebSocketState.Open) + { + throw new InvalidOperationException("The current state of the connection is not Open."); + } + + if (fileInfo == null) + throw new ArgumentNullException(nameof(fileInfo)); + + if (!fileInfo.Exists) + { + throw new ArgumentException("The file does not exist.", nameof(fileInfo)); + } + + FileStream stream; + if (!fileInfo.TryOpenRead(out stream)) + { + throw new ArgumentException("The file could not be opened.", nameof(fileInfo)); + } + + return SendCompressFragmented(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 bool Send(string data) + { + if (readyState != WebSocketState.Open) + { + throw new InvalidOperationException("The current state of the connection is not Open."); + } + + if (data == null) + throw new ArgumentNullException(nameof(data)); + + byte[] bytes; + if (!data.TryGetUTF8EncodedBytes(out bytes)) + { + throw new ArgumentException("It could not be UTF-8-encoded.", nameof(data)); + } + + return SendCompressFragmented(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 bool Send(Stream stream, int length) + { + if (readyState != WebSocketState.Open) + { + throw new InvalidOperationException("The current state of the connection is not Open."); + } + + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + if (!stream.CanRead) + { + throw new ArgumentException("It cannot be read.", nameof(stream)); + } + + if (length < 1) + { + throw new ArgumentException("Less than 1.", nameof(length)); + } + + var bytes = stream.ReadBytes(length); + + var len = bytes.Length; + if (len == 0) + { + throw new ArgumentException("No data could be read from it.", nameof(stream)); + } + + if (len < length) + { + logger.Warn($"Only {len} byte(s) of data could be read from the stream."); + } + + return SendCompressFragmented(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) + { + throw new InvalidOperationException("The current state of the connection is not Open."); + } + + if (data == null) + throw new ArgumentNullException(nameof(data)); + + SendCompressFragmentedAsync(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) + { + throw new InvalidOperationException("The current state of the connection is not Open."); + } + + if (fileInfo == null) + throw new ArgumentNullException(nameof(fileInfo)); + + if (!fileInfo.Exists) + { + throw new ArgumentException("The file does not exist.", nameof(fileInfo)); + } + + FileStream stream; + if (!fileInfo.TryOpenRead(out stream)) + { + throw new ArgumentException("The file could not be opened.", nameof(fileInfo)); + } + + SendCompressFragmentedAsync(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) + { + throw new InvalidOperationException("The current state of the connection is not Open."); + } + + if (data == null) + throw new ArgumentNullException(nameof(data)); + + byte[] bytes; + if (!data.TryGetUTF8EncodedBytes(out bytes)) + { + throw new ArgumentException("It could not be UTF-8-encoded.", nameof(data)); + } + + SendCompressFragmentedAsync(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) + { + throw new InvalidOperationException("The current state of the connection is not Open."); + } + + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + if (!stream.CanRead) + { + throw new ArgumentException("It cannot be read.", nameof(stream)); + } + + if (length < 1) + { + throw new ArgumentException("Less than 1.", nameof(length)); + } + + var bytes = stream.ReadBytes(length); + + var len = bytes.Length; + if (len == 0) + { + throw new ArgumentException("No data could be read from it.", nameof(stream)); + } + + if (len < length) + { + logger.Warn($"Only {len} byte(s) of data could be read from the stream."); + } + + SendCompressFragmentedAsync(Opcode.Binary, new MemoryStream(bytes), completed); + } + + + private protected abstract WebSocketFrame CreateCloseFrame(PayloadData payloadData); + + + private protected abstract WebSocketFrame CreatePongFrame(PayloadData payloadData); + + + private protected abstract WebSocketFrame CreateFrame(Fin fin, Opcode opcode, byte[] data, bool compressed); + + + private protected abstract void CheckCode(ushort code); + + + private protected abstract void CheckCloseStatus(CloseStatusCode code); + + + private protected abstract string CheckFrameMask(WebSocketFrame frame); + + + private protected abstract void UnmaskFrame(WebSocketFrame frame); + } } diff --git a/websocket-sharp/WebSocketException.cs b/websocket-sharp/WebSocketException.cs index 81d7c8081..5e118b187 100644 --- a/websocket-sharp/WebSocketException.cs +++ b/websocket-sharp/WebSocketException.cs @@ -106,4 +106,18 @@ public CloseStatusCode Code { #endregion } + + public class WebSocketProtocolViolationException : WebSocketException + { + internal WebSocketProtocolViolationException (Exception innerException) + : base (innerException) + { + } + + + internal WebSocketProtocolViolationException(string message) + : base(message) + { + } + } } diff --git a/websocket-sharp/websocket-sharp.csproj b/websocket-sharp/websocket-sharp.csproj index 0860c0313..5a8314165 100644 --- a/websocket-sharp/websocket-sharp.csproj +++ b/websocket-sharp/websocket-sharp.csproj @@ -1,5 +1,5 @@ - - + + Debug AnyCPU @@ -9,9 +9,15 @@ Library WebSocketSharp websocket-sharp - v3.5 + v4.5 true websocket-sharp.snk + + + + + 3.5 + true @@ -22,6 +28,7 @@ prompt 4 false + false none @@ -30,43 +37,22 @@ prompt 4 false - - - true - full - false - bin\Debug_Ubuntu - DEBUG - prompt - 4 - false - - - none - false - bin\Release_Ubuntu - prompt - 4 - false - true - - - - - + false - + + + @@ -135,15 +121,12 @@ + - - - - - + \ No newline at end of file From bdbd9aaeb71e09f746e05ddfef0e52d0e985942a Mon Sep 17 00:00:00 2001 From: Michal Dobrodenka Date: Wed, 17 May 2023 11:26:15 +0200 Subject: [PATCH 5/5] Getting rid of BeginRead in reading, as it caused problems on some older and weaker android devices (probably code was run synchronously and code around is not really ready for it). Some other fixes in server code --- websocket-sharp/ClientWebSocket.cs | 50 +++---- websocket-sharp/Ext.cs | 213 +++++++++++++++-------------- websocket-sharp/ServerWebSocket.cs | 4 +- websocket-sharp/WebSocket.cs | 90 +++++++----- websocket-sharp/WebSocketFrame.cs | 2 +- 5 files changed, 190 insertions(+), 169 deletions(-) diff --git a/websocket-sharp/ClientWebSocket.cs b/websocket-sharp/ClientWebSocket.cs index e7c5c4c1b..908000437 100644 --- a/websocket-sharp/ClientWebSocket.cs +++ b/websocket-sharp/ClientWebSocket.cs @@ -486,35 +486,37 @@ bool TryEnterHandshakeBlock() return Interlocked.CompareExchange(ref insideHandshakeBlock, 1, 0) > 0; } - lock (forState) { - if (readyState == WebSocketState.Open) - { - logger.Warn("The connection has already been established."); - - return false; - } + var errorAction = 0; - if (readyState == WebSocketState.Closing) + lock (forState) { - logger.Error("The close process has set in."); - - Error("An interruption has occurred while attempting to connect.", null); - - return false; - } - - if (retryCountForConnect > maxRetryCountForConnect) + if (readyState == WebSocketState.Open) + errorAction = 1; + else if (readyState == WebSocketState.Closing) + errorAction = 2; + else if (retryCountForConnect > maxRetryCountForConnect) + errorAction = 3; + + readyState = WebSocketState.Connecting; + } // lock + + // do this outside lock + switch (errorAction) { - logger.Error("An opportunity for reconnecting has been lost."); - - Error("An interruption has occurred while attempting to connect.", null); - - return false; + case 1: + logger.Warn("The connection has already been established."); + return false; + case 2: + logger.Error("The close process has set in."); + CallOnError("An interruption has occurred while attempting to connect.", null); + return false; + case 3: + logger.Error("An opportunity for reconnecting has been lost."); + CallOnError("An interruption has occurred while attempting to connect.", null); + return false; } - - readyState = WebSocketState.Connecting; - } // lock + } if (TryEnterHandshakeBlock()) { diff --git a/websocket-sharp/Ext.cs b/websocket-sharp/Ext.cs index 6837dcd90..6178cfe76 100644 --- a/websocket-sharp/Ext.cs +++ b/websocket-sharp/Ext.cs @@ -48,10 +48,12 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; +//using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Net.Sockets; using System.Text; +using System.Threading.Tasks; using WebSocketSharp.Net; namespace WebSocketSharp @@ -723,56 +725,65 @@ internal static byte[] ReadBytes (this Stream stream, long length, int bufferLen } } - internal static void ReadBytesAsync ( - this Stream stream, int length, Action completed, Action error - ) - { - var buff = new byte[length]; - var offset = 0; - var retry = 0; - - AsyncCallback callback = null; - callback = - ar => { - try { - var nread = stream.EndRead (ar); - if (nread == 0 && retry < _retry) { - retry++; - stream.BeginRead (buff, offset, length, callback, null); - - return; - } - - if (nread == 0 || nread == length) { - if (completed != null) - completed (buff.SubArray (0, offset + nread)); - - return; - } - - retry = 0; - - offset += nread; - length -= nread; - - stream.BeginRead (buff, offset, length, callback, null); - } - catch (Exception ex) { - if (error != null) - error (ex); - } - }; + internal static void ReadBytesAsync (this Stream stream, int length, Action completed, Action error, bool isHeader = false) + { + + Task.Factory.StartNew(() => + { + var buff = new byte[length]; + var offset = 0; + int retries = 0; + + while (length > 0) + { + try + { + //Debug.WriteLine($"ReadBytesAsync - {DateTime.Now} - {length}"); + + if (offset == 0 && isHeader) + stream.ReadTimeout = Int32.MaxValue; + else + stream.ReadTimeout = 5000; // todo: should be value from WebSocket class + + var read = stream.Read(buff, offset, length); + + if (read <= 0) + { + if (retries >= _retry) + { + completed?.Invoke(buff.SubArray(0, offset)); + return; + } + + retries++; + } + + length -= read; + offset += read; + } + catch (Exception e) + { + //// it was BeginRead before, which has no timeout! + //// dirty hack, on timeout, continue reading + //if (offset == 0 + //&& e is IOException + //&& e.InnerException is SocketException + //&& e.InnerException.Message == "A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.") + //{ + // continue; + //} + + //Debug.WriteLine($"ReadBytesAsync Exception - {DateTime.Now} - {length} - {e} - {e.InnerException}"); + error?.Invoke(e); + return; + } + } - try { - stream.BeginRead (buff, offset, length, callback, null); - } - catch (Exception ex) { - if (error != null) - error (ex); - } - } + completed?.Invoke(buff); + }); + } - internal static void ReadBytesAsync ( + internal static void ReadBytesAsync ( this Stream stream, long length, int bufferLength, @@ -780,65 +791,57 @@ internal static void ReadBytesAsync ( Action error ) { - var dest = new MemoryStream (); - var buff = new byte[bufferLength]; - var retry = 0; - - Action read = null; - read = - len => { - if (len < bufferLength) - bufferLength = (int) len; - - stream.BeginRead ( - buff, - 0, - bufferLength, - ar => { - try { - var nread = stream.EndRead (ar); - if (nread > 0) - dest.Write (buff, 0, nread); - - if (nread == 0 && retry < _retry) { - retry++; - read (len); - - return; - } - - if (nread == 0 || nread == len) { - if (completed != null) { - dest.Close (); - completed (dest.ToArray ()); - } - - dest.Dispose (); - return; + Task.Factory.StartNew(() => + { + var buff = new byte[length]; + var offset = 0; + int retries = 0; + + while (length > 0) + { + try + { + int bytesToRead = bufferLength < length ? (int)bufferLength : (int)length; + + //Debug.WriteLine($"ReadBytesAsync2 - {DateTime.Now} - {bytesToRead}"); + + var read = stream.Read(buff, offset, bytesToRead); + + if (read <= 0) + { + if (retries >= _retry) + { + completed?.Invoke(buff.SubArray(0, offset)); + return; + } + + retries++; + } + + length -= read; + offset += read; + } + catch (Exception e) + { + //// it was BeginRead before, which has no timeout! + //// dirty hack, on timeout, continue reading + //if (offset == 0 + //&& e is IOException + //&& e.InnerException is SocketException + //&& e.InnerException.Message == "A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.") + //{ + // continue; + //} + + + error?.Invoke(e); + return; + } } - retry = 0; - read (len - nread); - } - catch (Exception ex) { - dest.Dispose (); - if (error != null) - error (ex); - } - }, - null - ); - }; - - try { - read (length); - } - catch (Exception ex) { - dest.Dispose (); - if (error != null) - error (ex); - } - } + completed?.Invoke(buff); + }); + } internal static T[] Reverse (this T[] array) { diff --git a/websocket-sharp/ServerWebSocket.cs b/websocket-sharp/ServerWebSocket.cs index 3fa07f32b..659294303 100644 --- a/websocket-sharp/ServerWebSocket.cs +++ b/websocket-sharp/ServerWebSocket.cs @@ -117,7 +117,7 @@ private bool AcceptInternal() { logger.Error("The close process has set in."); - Error("An interruption has occurred while attempting to accept.", null); + CallOnError("An interruption has occurred while attempting to accept.", null); return false; } @@ -126,7 +126,7 @@ private bool AcceptInternal() { logger.Error("The connection has been closed."); - Error("An interruption has occurred while attempting to accept.", null); + CallOnError("An interruption has occurred while attempting to accept.", null); return false; } diff --git a/websocket-sharp/WebSocket.cs b/websocket-sharp/WebSocket.cs index d3de036df..a6ed911e6 100644 --- a/websocket-sharp/WebSocket.cs +++ b/websocket-sharp/WebSocket.cs @@ -707,7 +707,7 @@ private void EnqueueToMessageEventQueue(MessageEventArgs e) } - protected void Error(string message, Exception exception) + protected void CallOnError(string message, Exception exception) { try { @@ -779,7 +779,7 @@ protected void open() catch (Exception ex) { logger.Error(ex.ToString()); - Error("An error has occurred during the OnOpen event.", ex); + CallOnError("An error has occurred during the OnOpen event.", ex); } MessageEventArgs e; @@ -983,49 +983,65 @@ protected void ReleaseCommonResources(bool disposeReceivingExited) private bool SendCompressFragmented(Opcode opcode, Stream stream) { - lock (forSend) + string onErrorMessage = null; + Exception onErrorException = null; + + try { - var src = stream; - var compressed = false; - var sent = false; - try + lock (forSend) { - var compressionMethod = compression; - - if (compressionMethod != CompressionMethod.None) + var src = stream; + var compressed = false; + var sent = false; + try { - stream = stream.Compress(compressionMethod); - compressed = true; - } + var compressionMethod = compression; - sent = SendFragmentedInternal(opcode, stream, compressed); - if (!sent) + if (compressionMethod != CompressionMethod.None) + { + stream = stream.Compress(compressionMethod); + compressed = true; + } + + sent = SendFragmentedInternal(opcode, stream, compressed); + if (!sent) + { + onErrorMessage = $"Send failed. {opcode}"; + } + } + catch (Exception ex) { - Error($"Send failed. {opcode}", null); + onErrorMessage = "An error has occurred during a send."; + onErrorException = ex; } - } - catch (Exception ex) - { - logger.Error(ex.ToString()); - Error("An error has occurred during a send.", ex); - } - finally - { - if (compressed) + finally { - try - { - stream.Dispose(); - } - catch + if (compressed) { + try + { + stream.Dispose(); + } + catch + { + } } + + src.Dispose(); } - src.Dispose(); - } + return sent; + } // lock + } + finally + { + // call outside lock + if (onErrorException != null) + logger.Error(onErrorException.ToString()); + + if (!string.IsNullOrEmpty(onErrorMessage)) + CallOnError(onErrorMessage, onErrorException); - return sent; } } @@ -1148,7 +1164,7 @@ private void SendCompressFragmentedAsync(Opcode opcode, Stream stream, Action completed, Action error) { - stream.ReadBytesAsync (2, bytes => completed (processHeader (bytes)), error); + stream.ReadBytesAsync (2, bytes => completed (processHeader (bytes)), error, true); } private static WebSocketFrame readMaskingKey (Stream stream, WebSocketFrame frame)