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..908000437 --- /dev/null +++ b/websocket-sharp/ClientWebSocket.cs @@ -0,0 +1,1571 @@ +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; + } + + { + var errorAction = 0; + + lock (forState) + { + 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) + { + 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; + } + } + + 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..6178cfe76 100644 --- a/websocket-sharp/Ext.cs +++ b/websocket-sharp/Ext.cs @@ -48,14 +48,13 @@ 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.Security.Cryptography.X509Certificates; using System.Text; +using System.Threading.Tasks; using WebSocketSharp.Net; -using WebSocketSharp.Net.WebSockets; -using WebSocketSharp.Server; namespace WebSocketSharp { @@ -182,18 +181,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 +665,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) @@ -736,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, @@ -793,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; + 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; + } } - if (nread == 0 || nread == len) { - if (completed != null) { - dest.Close (); - completed (dest.ToArray ()); - } - - dest.Dispose (); - 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) { @@ -2000,51 +1990,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 572d785c2..f848db25d 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; } @@ -291,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..659294303 --- /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."); + + CallOnError("An interruption has occurred while attempting to accept.", null); + + return false; + } + + if (webSocketState == WebSocketState.Closed) + { + logger.Error("The connection has been closed."); + + CallOnError("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 93ed5bf4e..a6ed911e6 100644 --- a/websocket-sharp/WebSocket.cs +++ b/websocket-sharp/WebSocket.cs @@ -1,4 +1,5 @@ #region License + /* * WebSocket.cs * @@ -28,4067 +29,2480 @@ * 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; - - #endregion - - #region Internal Fields - - /// - /// Represents the empty array of used internally. - /// - internal static readonly byte[] EmptyBytes; - - /// - /// Represents the length used to determine whether the data should be fragmented in sending. - /// - /// - /// - /// The data will be fragmented if that length is greater than the value of this field. - /// - /// - /// If you would like to change the value, you must set it to a value between 125 and - /// Int32.MaxValue - 14 inclusive. - /// - /// - internal static readonly int FragmentLength; - - /// - /// Represents the random number generator used internally. - /// - internal static readonly RandomNumberGenerator RandomNumber; - - #endregion - - #region Static Constructor - - static WebSocket () - { - _maxRetryCountForConnect = 10; - EmptyBytes = new byte[0]; - FragmentLength = 1016; - RandomNumber = new RNGCryptoServiceProvider (); - } - - #endregion - - #region Internal Constructors - - // As server - internal WebSocket (HttpListenerWebSocketContext context, string protocol) - { - _context = context; - _protocol = protocol; - - _closeContext = context.Close; - _logger = context.Log; - _message = messages; - _secure = context.IsSecureConnection; - _stream = context.Stream; - _waitTime = TimeSpan.FromSeconds (1); - - init (); - } - - // As server - internal WebSocket (TcpListenerWebSocketContext context, string protocol) - { - _context = context; - _protocol = protocol; - - _closeContext = context.Close; - _logger = context.Log; - _message = messages; - _secure = context.IsSecureConnection; - _stream = context.Stream; - _waitTime = TimeSpan.FromSeconds (1); - - init (); - } - - #endregion - - #region Public Constructors - - /// - /// Initializes a new instance of the class with - /// and optionally . - /// - /// - /// - /// A that specifies the URL to which to connect. - /// - /// - /// The scheme of the URL must be ws or wss. - /// - /// - /// The new instance uses a secure connection if the scheme is wss. - /// - /// - /// - /// - /// An array of that specifies the names of - /// the subprotocols if necessary. - /// - /// - /// Each value of the array must be a token defined in - /// - /// RFC 2616. - /// - /// - /// - /// is . - /// - /// - /// - /// is an empty string. - /// - /// - /// -or- - /// - /// - /// is an invalid WebSocket URL string. - /// - /// - /// -or- - /// - /// - /// contains a value that is not a token. - /// - /// - /// -or- - /// - /// - /// contains a value twice. - /// - /// - public WebSocket (string url, params string[] protocols) - { - if (url == null) - throw new ArgumentNullException ("url"); - - if (url.Length == 0) - throw new ArgumentException ("An empty string.", "url"); - - string msg; - if (!url.TryCreateWebSocketUri (out _uri, out msg)) - throw new ArgumentException (msg, "url"); - - if (protocols != null && protocols.Length > 0) { - if (!checkProtocols (protocols, out msg)) - throw new ArgumentException (msg, "protocols"); - - _protocols = protocols; - } - - _base64Key = CreateBase64Key (); - _client = true; - _logger = new Logger (); - _message = messagec; - _secure = _uri.Scheme == "wss"; - _waitTime = TimeSpan.FromSeconds (5); - - init (); - } - - #endregion - - #region Internal Properties - - internal CookieCollection CookieCollection { - get { - return _cookies; - } - } - - // As server - internal Func CustomHandshakeRequestChecker { - get { - return _handshakeRequestChecker; - } - - set { - _handshakeRequestChecker = value; - } - } - - internal bool HasMessage { - get { - lock (_forMessageEventQueue) - return _messageEventQueue.Count > 0; - } - } - - // As server - internal bool IgnoreExtensions { - get { - return _ignoreExtensions; - } - - set { - _ignoreExtensions = value; - } - } - - internal bool IsConnected { - get { - return _readyState == WebSocketState.Open || _readyState == WebSocketState.Closing; - } - } - - #endregion - - #region Public Properties - - /// - /// Gets or sets the compression method used to compress a message. - /// - /// - /// The set operation does nothing if the connection has already been - /// established or it is closing. - /// - /// - /// - /// One of the enum values. - /// - /// - /// It specifies the compression method used to compress a message. - /// - /// - /// The default value is . - /// - /// - /// - /// The set operation is not available if this instance is not a client. - /// - public CompressionMethod Compression { - get { - return _compression; - } - - set { - string msg = null; - - if (!_client) { - msg = "This instance is not a client."; - throw new InvalidOperationException (msg); - } - - if (!canSet (out msg)) { - _logger.Warn (msg); - return; - } - - lock (_forState) { - if (!canSet (out msg)) { - _logger.Warn (msg); - return; - } - - _compression = value; - } - } - } - - /// - /// Gets the HTTP cookies included in the handshake request/response. - /// - /// - /// - /// An - /// instance. - /// - /// - /// It provides an enumerator which supports the iteration over - /// the collection of the cookies. - /// - /// - public IEnumerable Cookies { - get { - lock (_cookies.SyncRoot) { - foreach (Cookie cookie in _cookies) - yield return cookie; - } - } - } - - /// - /// Gets the credentials for the HTTP authentication (Basic/Digest). - /// - /// - /// - /// A that represents the credentials - /// used to authenticate the client. - /// - /// - /// The default value is . - /// - /// - public NetworkCredential Credentials { - get { - return _credentials; - } - } - - /// - /// Gets or sets a value indicating whether a event - /// is emitted when a ping is received. - /// - /// - /// - /// true if this instance emits a event - /// when receives a ping; otherwise, false. - /// - /// - /// The default value is false. - /// - /// - public bool EmitOnPing { - get { - return _emitOnPing; - } - - set { - _emitOnPing = value; - } - } - - /// - /// Gets or sets a value indicating whether the URL redirection for - /// the handshake request is allowed. - /// - /// - /// The set operation does nothing if the connection has already been - /// established or it is closing. - /// - /// - /// - /// true if this instance allows the URL redirection for - /// the handshake request; otherwise, false. - /// - /// - /// The default value is false. - /// - /// - /// - /// The set operation is not available if this instance is not a client. - /// - public bool EnableRedirection { - get { - return _enableRedirection; - } - - set { - string msg = null; - - if (!_client) { - msg = "This instance is not a client."; - throw new InvalidOperationException (msg); - } - - if (!canSet (out msg)) { - _logger.Warn (msg); - return; - } - - lock (_forState) { - if (!canSet (out msg)) { - _logger.Warn (msg); - return; - } - - _enableRedirection = value; - } - } - } - - /// - /// Gets the extensions selected by server. - /// - /// - /// A that will be a list of the extensions - /// negotiated between client and server, or an empty string if - /// not specified or selected. - /// - public string Extensions { - get { - return _extensions ?? String.Empty; - } - } - - /// - /// Gets a value indicating whether the connection is alive. - /// - /// - /// The get operation returns the value by using a ping/pong - /// if the current state of the connection is Open. - /// - /// - /// true if the connection is alive; otherwise, false. - /// - public bool IsAlive { - get { - return ping (EmptyBytes); - } - } - - /// - /// Gets a value indicating whether a secure connection is used. - /// - /// - /// true if this instance uses a secure connection; otherwise, - /// false. - /// - public bool IsSecure { - get { - return _secure; - } - } - - /// - /// Gets the logging function. - /// - /// - /// The default logging level is . - /// - /// - /// A that provides the logging function. - /// - public Logger Log { - get { - return _logger; - } - - internal set { - _logger = value; - } - } - - /// - /// Gets or sets the value of the HTTP Origin header to send with - /// the handshake request. - /// - /// - /// - /// The HTTP Origin header is defined in - /// - /// Section 7 of RFC 6454. - /// - /// - /// This instance sends the Origin header if this property has any. - /// - /// - /// The set operation does nothing if the connection has already been - /// established or it is closing. - /// - /// - /// - /// - /// A that represents the value of the Origin - /// header to send. - /// - /// - /// The syntax is <scheme>://<host>[:<port>]. - /// - /// - /// The default value is . - /// - /// - /// - /// The set operation is not available if this instance is not a client. - /// - /// - /// - /// The value specified for a set operation is not an absolute URI string. - /// - /// - /// -or- - /// - /// - /// The value specified for a set operation includes the path segments. - /// - /// - public string Origin { - get { - return _origin; - } - - set { - string msg = null; - - if (!_client) { - msg = "This instance is not a client."; - throw new InvalidOperationException (msg); - } - - if (!value.IsNullOrEmpty ()) { - Uri uri; - if (!Uri.TryCreate (value, UriKind.Absolute, out uri)) { - msg = "Not an absolute URI string."; - throw new ArgumentException (msg, "value"); - } - - if (uri.Segments.Length > 1) { - msg = "It includes the path segments."; - throw new ArgumentException (msg, "value"); - } - } - - if (!canSet (out msg)) { - _logger.Warn (msg); - return; - } - - lock (_forState) { - if (!canSet (out msg)) { - _logger.Warn (msg); - return; - } - - _origin = !value.IsNullOrEmpty () ? value.TrimEnd ('/') : value; - } - } - } - - /// - /// Gets the name of subprotocol selected by the server. - /// - /// - /// - /// A that will be one of the names of - /// subprotocols specified by client. - /// - /// - /// An empty string if not specified or selected. - /// - /// - public string Protocol { - get { - return _protocol ?? String.Empty; - } - - internal set { - _protocol = value; - } - } - - /// - /// Gets the current state of the connection. - /// - /// - /// - /// One of the enum values. - /// - /// - /// It indicates the current state of the connection. - /// - /// - /// The default value is . - /// - /// - public WebSocketState ReadyState { - get { - return _readyState; - } - } - - /// - /// Gets the configuration for secure connection. - /// - /// - /// This configuration will be referenced when attempts to connect, - /// so it must be configured before any connect method is called. - /// - /// - /// A that represents - /// the configuration used to establish a secure connection. - /// - /// - /// - /// This instance is not a client. - /// - /// - /// This instance does not use a secure connection. - /// - /// - public ClientSslConfiguration SslConfiguration { - get { - if (!_client) { - var msg = "This instance is not a client."; - throw new InvalidOperationException (msg); - } - - if (!_secure) { - var msg = "This instance does not use a secure connection."; - throw new InvalidOperationException (msg); - } - - return getSslConfiguration (); - } - } - - /// - /// Gets the URL to which to connect. - /// - /// - /// A that represents the URL to which to connect. - /// - public Uri Url { - get { - return _client ? _uri : _context.RequestUri; - } - } - - /// - /// Gets or sets the time to wait for the response to the ping or close. - /// - /// - /// The set operation does nothing if the connection has already been - /// established or it is closing. - /// - /// - /// - /// A to wait for the response. - /// - /// - /// The default value is the same as 5 seconds if this instance is - /// a client. - /// - /// - /// - /// The value specified for a set operation is zero or less. - /// - public TimeSpan WaitTime { - get { - return _waitTime; - } - - set { - if (value <= TimeSpan.Zero) - throw new ArgumentOutOfRangeException ("value", "Zero or less."); - - string msg; - if (!canSet (out msg)) { - _logger.Warn (msg); - return; - } - - lock (_forState) { - if (!canSet (out msg)) { - _logger.Warn (msg); - return; - } - - _waitTime = value; - } - } - } - - #endregion - - #region Public Events - - /// - /// Occurs when the WebSocket connection has been closed. - /// - public event EventHandler OnClose; - - /// - /// Occurs when the gets an error. - /// - public event EventHandler OnError; - - /// - /// Occurs when the receives a message. - /// - public event EventHandler OnMessage; - - /// - /// Occurs when the WebSocket connection has been established. - /// - public event EventHandler OnOpen; - - #endregion - - #region Private Methods - - // As server - private bool accept () - { - if (_readyState == WebSocketState.Open) { - var msg = "The handshake request has already been accepted."; - _logger.Warn (msg); - - return false; - } - - lock (_forState) { - if (_readyState == WebSocketState.Open) { - var msg = "The handshake request has already been accepted."; - _logger.Warn (msg); - - return false; - } - - if (_readyState == WebSocketState.Closing) { - var msg = "The close process has set in."; - _logger.Error (msg); - - msg = "An interruption has occurred while attempting to accept."; - error (msg, null); - - return false; - } - - if (_readyState == WebSocketState.Closed) { - var msg = "The connection has been closed."; - _logger.Error (msg); - - msg = "An interruption has occurred while attempting to accept."; - error (msg, null); - - return false; - } - - try { - if (!acceptHandshake ()) - return false; - } - catch (Exception ex) { - _logger.Fatal (ex.Message); - _logger.Debug (ex.ToString ()); - - var msg = "An exception has occurred while attempting to accept."; - fatal (msg, ex); - - return false; - } - - _readyState = WebSocketState.Open; - return true; - } - } - - // As server - private bool acceptHandshake () - { - _logger.Debug ( - String.Format ( - "A handshake request from {0}:\n{1}", _context.UserEndPoint, _context - ) - ); - - string msg; - if (!checkHandshakeRequest (_context, out msg)) { - _logger.Error (msg); - - refuseHandshake ( - CloseStatusCode.ProtocolError, - "A handshake error has occurred while attempting to accept." - ); - - return false; - } - - if (!customCheckHandshakeRequest (_context, out msg)) { - _logger.Error (msg); - - refuseHandshake ( - CloseStatusCode.PolicyViolation, - "A handshake error has occurred while attempting to accept." - ); - - return false; - } - - _base64Key = _context.Headers["Sec-WebSocket-Key"]; - - if (_protocol != null) { - var vals = _context.SecWebSocketProtocols; - processSecWebSocketProtocolClientHeader (vals); - } - - if (!_ignoreExtensions) { - var val = _context.Headers["Sec-WebSocket-Extensions"]; - processSecWebSocketExtensionsClientHeader (val); - } - - return sendHttpResponse (createHandshakeResponse ()); - } - - private bool canSet (out string message) - { - message = null; - - if (_readyState == WebSocketState.Open) { - message = "The connection has already been established."; - return false; - } - - if (_readyState == WebSocketState.Closing) { - message = "The connection is closing."; - return false; - } - - return true; - } - - // As server - private bool checkHandshakeRequest ( - WebSocketContext context, out string message - ) - { - message = null; - - if (!context.IsWebSocketRequest) { - message = "Not a handshake request."; - return false; - } - - if (context.RequestUri == null) { - message = "It specifies an invalid Request-URI."; - return false; - } - - var headers = context.Headers; - - var key = headers["Sec-WebSocket-Key"]; - if (key == null) { - message = "It includes no Sec-WebSocket-Key header."; - return false; - } - - if (key.Length == 0) { - message = "It includes an invalid Sec-WebSocket-Key header."; - return false; - } - - var version = headers["Sec-WebSocket-Version"]; - if (version == null) { - message = "It includes no Sec-WebSocket-Version header."; - return false; - } - - if (version != _version) { - message = "It includes an invalid Sec-WebSocket-Version header."; - return false; - } - - var protocol = headers["Sec-WebSocket-Protocol"]; - if (protocol != null && protocol.Length == 0) { - message = "It includes an invalid Sec-WebSocket-Protocol header."; - return false; - } - - if (!_ignoreExtensions) { - var extensions = headers["Sec-WebSocket-Extensions"]; - if (extensions != null && extensions.Length == 0) { - message = "It includes an invalid Sec-WebSocket-Extensions header."; - return false; - } - } - - return true; - } - - // As client - private bool checkHandshakeResponse (HttpResponse response, out string message) - { - message = null; - - if (response.IsRedirect) { - message = "Indicates the redirection."; - return false; - } - - if (response.IsUnauthorized) { - message = "Requires the authentication."; - return false; - } - - if (!response.IsWebSocketResponse) { - message = "Not a WebSocket handshake response."; - return false; - } - - var headers = response.Headers; - if (!validateSecWebSocketAcceptHeader (headers["Sec-WebSocket-Accept"])) { - message = "Includes no Sec-WebSocket-Accept header, or it has an invalid value."; - return false; - } - - if (!validateSecWebSocketProtocolServerHeader (headers["Sec-WebSocket-Protocol"])) { - message = "Includes no Sec-WebSocket-Protocol header, or it has an invalid value."; - return false; - } - - if (!validateSecWebSocketExtensionsServerHeader (headers["Sec-WebSocket-Extensions"])) { - message = "Includes an invalid Sec-WebSocket-Extensions header."; - return false; - } - - if (!validateSecWebSocketVersionServerHeader (headers["Sec-WebSocket-Version"])) { - message = "Includes an invalid Sec-WebSocket-Version header."; - return false; - } - - return true; - } - - private static bool checkProtocols (string[] protocols, out string message) - { - message = null; - - Func cond = protocol => protocol.IsNullOrEmpty () - || !protocol.IsToken (); - - if (protocols.Contains (cond)) { - message = "It contains a value that is not a token."; - return false; - } - - if (protocols.ContainsTwice ()) { - message = "It contains a value twice."; - return false; - } - - return true; - } - - private bool checkReceivedFrame (WebSocketFrame frame, out string message) - { - message = null; - - var masked = frame.IsMasked; - if (_client && masked) { - message = "A frame from the server is masked."; - return false; - } - - if (!_client && !masked) { - message = "A frame from a client is not masked."; - return false; - } - - if (_inContinuation && frame.IsData) { - message = "A data frame has been received while receiving continuation frames."; - return false; - } - - if (frame.IsCompressed && _compression == CompressionMethod.None) { - message = "A compressed frame has been received without any agreement for it."; - return false; - } - - if (frame.Rsv2 == Rsv.On) { - message = "The RSV2 of a frame is non-zero without any negotiation for it."; - return false; - } - - if (frame.Rsv3 == Rsv.On) { - message = "The RSV3 of a frame is non-zero without any negotiation for it."; - return false; - } - - return true; - } - - private void close (ushort code, string reason) - { - if (_readyState == WebSocketState.Closing) { - _logger.Info ("The closing is already in progress."); - return; - } - - if (_readyState == WebSocketState.Closed) { - _logger.Info ("The connection has already been closed."); - return; - } - - if (code == 1005) { // == no status - close (PayloadData.Empty, true, true, false); - return; - } - - var send = !code.IsReserved (); - close (new PayloadData (code, reason), send, send, false); - } - - private void close ( - PayloadData payloadData, bool send, bool receive, bool received - ) - { - lock (_forState) { - if (_readyState == WebSocketState.Closing) { - _logger.Info ("The closing is already in progress."); - return; - } - - if (_readyState == WebSocketState.Closed) { - _logger.Info ("The connection has already been closed."); - return; - } - - send = send && _readyState == WebSocketState.Open; - receive = send && receive; - - _readyState = WebSocketState.Closing; - } - - _logger.Trace ("Begin closing the connection."); - - var res = closeHandshake (payloadData, send, receive, received); - releaseResources (); - - _logger.Trace ("End closing the connection."); - - _readyState = WebSocketState.Closed; - - var e = new CloseEventArgs (payloadData); - 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; + /// + /// 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); + - _protocolsRequested = _protocols != null; - if (_protocolsRequested) - headers["Sec-WebSocket-Protocol"] = _protocols.ToString (", "); + private void StartCloseAsyncTask(ushort code, string reason) + { + if (readyState == WebSocketState.Closing) + { + logger.Info("The closing is already in progress."); + return; + } - _extensionsRequested = _compression != CompressionMethod.None; - if (_extensionsRequested) - headers["Sec-WebSocket-Extensions"] = createExtensions (); - - headers["Sec-WebSocket-Version"] = _version; + 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 CallOnError(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()); + CallOnError("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; + } - AuthenticationResponse authRes = null; - if (_authChallenge != null && _credentials != null) { - authRes = new AuthenticationResponse (_authChallenge, _credentials, _nonceCount); - _nonceCount = authRes.NonceCount; - } - else if (_preAuth) { - authRes = new AuthenticationResponse (_credentials); - } + var pong = CreatePongFrame(frame.PayloadData); + var stream = this.socketStream; + if (stream == null) + return false; - 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) { - _stream.Dispose (); - _stream = null; - } - - if (_tcpClient != null) { - _tcpClient.Close (); - _tcpClient = null; - } - } - - private void releaseCommonResources () - { - if (_fragmentsBuffer != null) { - _fragmentsBuffer.Dispose (); - _fragmentsBuffer = null; - _inContinuation = false; - } - - if (_pongReceived != null) { - _pongReceived.Close (); - _pongReceived = null; - } - - if (_receivingExited != null) { - _receivingExited.Close (); - _receivingExited = null; - } - } - - private void releaseResources () - { - if (_client) - releaseClientResources (); - else - releaseServerResources (); - - releaseCommonResources (); - } - - // As server - private void releaseServerResources () - { - if (_closeContext == null) - return; - - _closeContext (); - _closeContext = null; - _stream = null; - _context = null; - } - - private bool send (Opcode opcode, Stream stream) - { - lock (_forSend) { - var src = stream; - var compressed = false; - var sent = false; - try { - if (_compression != CompressionMethod.None) { - stream = stream.Compress (_compression); - compressed = true; - } - - sent = send (opcode, stream, compressed); - if (!sent) - error ("A send has been interrupted.", null); - } - catch (Exception ex) { - _logger.Error (ex.ToString ()); - error ("An error has occurred during a send.", ex); - } - finally { - if (compressed) - stream.Dispose (); - - src.Dispose (); - } - - return sent; - } - } - - private bool send (Opcode opcode, Stream stream, bool compressed) - { - var len = stream.Length; - if (len == 0) - return send (Fin.Final, opcode, EmptyBytes, false); - - var quo = len / FragmentLength; - var rem = (int) (len % FragmentLength); - - byte[] buff = null; - if (quo == 0) { - buff = new byte[rem]; - return stream.Read (buff, 0, rem) == rem - && send (Fin.Final, opcode, buff, compressed); - } - - if (quo == 1 && rem == 0) { - buff = new byte[FragmentLength]; - return stream.Read (buff, 0, FragmentLength) == FragmentLength - && send (Fin.Final, opcode, buff, compressed); - } - - /* Send fragments */ - - // Begin - buff = new byte[FragmentLength]; - var sent = stream.Read (buff, 0, FragmentLength) == FragmentLength - && send (Fin.More, opcode, buff, compressed); - - if (!sent) - return false; - - var n = rem == 0 ? quo - 2 : quo - 1; - for (long i = 0; i < n; i++) { - sent = stream.Read (buff, 0, FragmentLength) == FragmentLength - && send (Fin.More, Opcode.Cont, buff, false); - - if (!sent) - return false; - } - - // End - if (rem == 0) - rem = FragmentLength; - else - buff = new byte[rem]; - - return stream.Read (buff, 0, rem) == rem - && send (Fin.Final, Opcode.Cont, buff, false); - } - - private bool send (Fin fin, Opcode opcode, byte[] data, bool compressed) - { - lock (_forState) { - if (_readyState != WebSocketState.Open) { - _logger.Error ("The connection is closing."); - return false; - } - - var frame = new WebSocketFrame (fin, opcode, data, compressed, _client); - return sendBytes (frame.ToArray ()); - } - } - - private void sendAsync (Opcode opcode, Stream stream, Action completed) - { - Func sender = send; - sender.BeginInvoke ( - opcode, - stream, - ar => { - try { - var sent = sender.EndInvoke (ar); - if (completed != null) - completed (sent); - } - catch (Exception ex) { - _logger.Error (ex.ToString ()); - error ( - "An error has occurred during the callback for an async send.", - ex - ); - } - }, - null - ); - } - - private bool sendBytes (byte[] bytes) - { - try { - _stream.Write (bytes, 0, bytes.Length); - } - catch (Exception ex) { - _logger.Error (ex.Message); - _logger.Debug (ex.ToString ()); - - return false; - } - - return true; - } - - // As client - private HttpResponse sendHandshakeRequest () - { - var req = createHandshakeRequest (); - var res = sendHttpRequest (req, 90000); - if (res.IsUnauthorized) { - var chal = res.Headers["WWW-Authenticate"]; - _logger.Warn (String.Format ("Received an authentication requirement for '{0}'.", chal)); - if (chal.IsNullOrEmpty ()) { - _logger.Error ("No authentication challenge is specified."); - return res; - } - - _authChallenge = AuthenticationChallenge.Parse (chal); - if (_authChallenge == null) { - _logger.Error ("An invalid authentication challenge is specified."); - return res; - } - - if (_credentials != null && - (!_preAuth || _authChallenge.Scheme == AuthenticationSchemes.Digest)) { - if (res.HasConnectionClose) { - releaseClientResources (); - setClientStream (); - } - - var authRes = new AuthenticationResponse (_authChallenge, _credentials, _nonceCount); - _nonceCount = authRes.NonceCount; - req.Headers["Authorization"] = authRes.ToString (); - res = sendHttpRequest (req, 15000); - } - } - - if (res.IsRedirect) { - var url = res.Headers["Location"]; - _logger.Warn (String.Format ("Received a redirection to '{0}'.", url)); - if (_enableRedirection) { - if (url.IsNullOrEmpty ()) { - _logger.Error ("No url to redirect is located."); - return res; - } - - Uri uri; - string msg; - if (!url.TryCreateWebSocketUri (out uri, out msg)) { - _logger.Error ("An invalid url to redirect is located: " + msg); - return res; - } - - releaseClientResources (); - - _uri = uri; - _secure = uri.Scheme == "wss"; - - setClientStream (); - return sendHandshakeRequest (); - } - } - - return res; - } - - // As client - private HttpResponse sendHttpRequest (HttpRequest request, int millisecondsTimeout) - { - _logger.Debug ("A request to the server:\n" + request.ToString ()); - var res = request.GetResponse (_stream, millisecondsTimeout); - _logger.Debug ("A response to this request:\n" + res.ToString ()); - - return res; - } - - // As server - private bool sendHttpResponse (HttpResponse response) - { - _logger.Debug ( - String.Format ( - "A response to {0}:\n{1}", _context.UserEndPoint, response - ) - ); - - return sendBytes (response.ToByteArray ()); - } - - // As client - private void sendProxyConnectRequest () - { - var req = HttpRequest.CreateConnectRequest (_uri); - var res = sendHttpRequest (req, 90000); - if (res.IsProxyAuthenticationRequired) { - var chal = res.Headers["Proxy-Authenticate"]; - _logger.Warn ( - String.Format ("Received a proxy authentication requirement for '{0}'.", chal)); - - if (chal.IsNullOrEmpty ()) - throw new WebSocketException ("No proxy authentication challenge is specified."); - - var authChal = AuthenticationChallenge.Parse (chal); - if (authChal == null) - throw new WebSocketException ("An invalid proxy authentication challenge is specified."); - - if (_proxyCredentials != null) { - if (res.HasConnectionClose) { - releaseClientResources (); - _tcpClient = new TcpClient (_proxyUri.DnsSafeHost, _proxyUri.Port); - _stream = _tcpClient.GetStream (); - } - - var authRes = new AuthenticationResponse (authChal, _proxyCredentials, 0); - req.Headers["Proxy-Authorization"] = authRes.ToString (); - res = sendHttpRequest (req, 15000); - } - - if (res.IsProxyAuthenticationRequired) - throw new WebSocketException ("A proxy authentication is required."); - } - - if (res.StatusCode[0] != '2') - throw new WebSocketException ( - "The proxy has failed a connection to the requested host and port."); - } - - // As client - private void setClientStream () - { - if (_proxyUri != null) { - _tcpClient = new TcpClient (_proxyUri.DnsSafeHost, _proxyUri.Port); - _stream = _tcpClient.GetStream (); - sendProxyConnectRequest (); - } - else { - _tcpClient = new TcpClient (_uri.DnsSafeHost, _uri.Port); - _stream = _tcpClient.GetStream (); - } - - if (_secure) { - var conf = getSslConfiguration (); - var host = conf.TargetHost; - if (host != _uri.DnsSafeHost) - throw new WebSocketException ( - CloseStatusCode.TlsHandshakeFailure, "An invalid host name is specified."); - - try { - var sslStream = new SslStream ( - _stream, - false, - conf.ServerCertificateValidationCallback, - conf.ClientCertificateSelectionCallback); - - sslStream.AuthenticateAsClient ( - host, - conf.ClientCertificates, - conf.EnabledSslProtocols, - conf.CheckCertificateRevocation); - - _stream = sslStream; - } - catch (Exception ex) { - throw new WebSocketException (CloseStatusCode.TlsHandshakeFailure, ex); - } - } - } - - private void startReceiving () - { - if (_messageEventQueue.Count > 0) - _messageEventQueue.Clear (); - - _pongReceived = new ManualResetEvent (false); - _receivingExited = new ManualResetEvent (false); - - Action receive = null; - receive = - () => - WebSocketFrame.ReadFrameAsync ( - _stream, - false, - frame => { - if (!processReceivedFrame (frame) || _readyState == WebSocketState.Closed) { - var exited = _receivingExited; - if (exited != null) - exited.Set (); - - return; - } - - // Receive next asap because the Ping or Close needs a response to it. - receive (); - - if (_inMessage || !HasMessage || _readyState != WebSocketState.Open) - return; - - message (); - }, - ex => { - _logger.Fatal (ex.ToString ()); - fatal ("An exception has occurred while receiving.", ex); - } - ); - - receive (); - } - - // As client - private bool validateSecWebSocketAcceptHeader (string value) - { - return value != null && value == CreateResponseKey (_base64Key); - } - - // As client - private bool validateSecWebSocketExtensionsServerHeader (string value) - { - if (value == null) - return true; - - if (value.Length == 0) - return false; - - if (!_extensionsRequested) - return false; - - var comp = _compression != CompressionMethod.None; - foreach (var e in value.SplitHeaderValue (',')) { - var ext = e.Trim (); - if (comp && ext.IsCompressionExtension (_compression)) { - if (!ext.Contains ("server_no_context_takeover")) { - _logger.Error ("The server hasn't sent back 'server_no_context_takeover'."); - return false; - } - - if (!ext.Contains ("client_no_context_takeover")) - _logger.Warn ("The server hasn't sent back 'client_no_context_takeover'."); - - var method = _compression.ToExtensionString (); - var invalid = - ext.SplitHeaderValue (';').Contains ( - t => { - t = t.Trim (); - return t != method - && t != "server_no_context_takeover" - && t != "client_no_context_takeover"; - } - ); - - if (invalid) - return false; - } - else { - return false; - } - } - - return true; - } - - // As client - private bool validateSecWebSocketProtocolServerHeader (string value) - { - if (value == null) - return !_protocolsRequested; - - if (value.Length == 0) - return false; - - return _protocolsRequested && _protocols.Contains (p => p == value); - } - - // As client - private bool validateSecWebSocketVersionServerHeader (string value) - { - return value == null || value == _version; - } - - #endregion - - #region Internal Methods - - // As server - internal void Close (HttpResponse response) - { - _readyState = WebSocketState.Closing; - - sendHttpResponse (response); - releaseServerResources (); - - _readyState = WebSocketState.Closed; - } - - // As server - internal void Close (HttpStatusCode code) - { - Close (createHandshakeFailureResponse (code)); - } - - // As server - internal void Close (PayloadData payloadData, byte[] frameAsBytes) - { - lock (_forState) { - if (_readyState == WebSocketState.Closing) { - _logger.Info ("The closing is already in progress."); - return; - } - - if (_readyState == WebSocketState.Closed) { - _logger.Info ("The connection has already been closed."); - return; - } - - _readyState = WebSocketState.Closing; - } - - _logger.Trace ("Begin closing the connection."); - - var sent = frameAsBytes != null && sendBytes (frameAsBytes); - var received = sent && _receivingExited != null - ? _receivingExited.WaitOne (_waitTime) - : false; - - var res = sent && received; - - _logger.Debug ( - String.Format ( - "Was clean?: {0}\n sent: {1}\n received: {2}", res, sent, received - ) - ); - - releaseServerResources (); - releaseCommonResources (); - - _logger.Trace ("End closing the connection."); - - _readyState = WebSocketState.Closed; - - var e = new CloseEventArgs (payloadData); - 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 - } + 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) + { + string onErrorMessage = null; + Exception onErrorException = null; + + try + { + 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) + { + onErrorMessage = $"Send failed. {opcode}"; + } + } + catch (Exception ex) + { + onErrorMessage = "An error has occurred during a send."; + onErrorException = ex; + } + finally + { + if (compressed) + { + try + { + stream.Dispose(); + } + catch + { + } + } + + src.Dispose(); + } + + return sent; + } // lock + } + finally + { + // call outside lock + if (onErrorException != null) + logger.Error(onErrorException.ToString()); + + if (!string.IsNullOrEmpty(onErrorMessage)) + CallOnError(onErrorMessage, onErrorException); + + } + } + + + 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()); + CallOnError("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()); + CallOnError("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()); + CallOnError("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()); + CallOnError("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/WebSocketFrame.cs b/websocket-sharp/WebSocketFrame.cs index 3be6300fe..e2207973a 100644 --- a/websocket-sharp/WebSocketFrame.cs +++ b/websocket-sharp/WebSocketFrame.cs @@ -526,7 +526,7 @@ private static WebSocketFrame readHeader (Stream stream) private static void readHeaderAsync ( 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) 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