diff --git a/Example/Example.csproj b/Example/Example.csproj index 38c5b4200..ee89d9f6e 100644 --- a/Example/Example.csproj +++ b/Example/Example.csproj @@ -34,7 +34,7 @@ full false bin\Debug_Ubuntu - DEBUG,UBUNTU + DEBUG prompt 4 true @@ -43,16 +43,12 @@ none false bin\Release_Ubuntu - UBUNTU prompt 4 true - - notify-sharp - @@ -65,7 +61,5 @@ - - \ No newline at end of file diff --git a/Example/NotificationMessage.cs b/Example/NotificationMessage.cs deleted file mode 100644 index fd1bd3071..000000000 --- a/Example/NotificationMessage.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; - -namespace Example -{ - internal class NotificationMessage - { - public string Body { - get; set; - } - - public string Icon { - get; set; - } - - public string Summary { - get; set; - } - - public override string ToString () - { - return String.Format ("{0}: {1}", Summary, Body); - } - } -} diff --git a/Example/Notifier.cs b/Example/Notifier.cs deleted file mode 100644 index f21eec32c..000000000 --- a/Example/Notifier.cs +++ /dev/null @@ -1,79 +0,0 @@ -#if UBUNTU -using Notifications; -#endif -using System; -using System.Collections; -using System.Collections.Generic; -using System.Threading; - -namespace Example -{ - internal class Notifier : IDisposable - { - private volatile bool _enabled; - private Queue _queue; - private object _sync; - private ManualResetEvent _waitHandle; - - public Notifier () - { - _enabled = true; - _queue = new Queue (); - _sync = ((ICollection) _queue).SyncRoot; - _waitHandle = new ManualResetEvent (false); - - ThreadPool.QueueUserWorkItem ( - state => { - while (_enabled || Count > 0) { - var msg = dequeue (); - if (msg != null) { -#if UBUNTU - var nf = new Notification (msg.Summary, msg.Body, msg.Icon); - nf.AddHint ("append", "allowed"); - nf.Show (); -#else - Console.WriteLine (msg); -#endif - } - else { - Thread.Sleep (500); - } - } - - _waitHandle.Set (); - }); - } - - public int Count { - get { - lock (_sync) - return _queue.Count; - } - } - - private NotificationMessage dequeue () - { - lock (_sync) - return _queue.Count > 0 ? _queue.Dequeue () : null; - } - - public void Close () - { - _enabled = false; - _waitHandle.WaitOne (); - _waitHandle.Close (); - } - - public void Notify (NotificationMessage message) - { - lock (_sync) - if (_enabled) - _queue.Enqueue (message); - } - - void IDisposable.Dispose () - { - Close (); - } - } -} diff --git a/Example/Program.cs b/Example/Program.cs index f3d91f6de..8d16c0794 100644 --- a/Example/Program.cs +++ b/Example/Program.cs @@ -16,87 +16,111 @@ 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 the instance with the wss scheme WebSocket URL. + // you should create a new instance with a wss scheme WebSocket URL. - using (var nf = new Notifier ()) - using (var ws = new WebSocket ("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 ("ws://localhost:4649/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/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:4649/Chat")) + //using (var ws = new WebSocket ("wss://localhost:5963/Chat?name=nobita")) { - // 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" - }); - - ws.OnError += (sender, e) => - 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" - }); - #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); - - // To emit a WebSocket.OnMessage event when receives a ping. - ws.EmitOnPing = true; -#endif // To enable the Per-message Compression extension. //ws.Compression = CompressionMethod.Deflate; - /* 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)); + // To emit a WebSocket.OnMessage event when receives a ping. + //ws.EmitOnPing = true; - return true; // If the server certificate is valid. - }; - */ + // To enable the redirection. + //ws.EnableRedirection = true; - // To send the credentials for the HTTP Authentication (Basic/Digest). - //ws.SetCredentials ("nobita", "password", false); + // To disable a delay when send or receive buffer of the underlying + // TCP socket is not full. + ws.NoDelay = true; // 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\"")); + // To send the credentials for the HTTP Authentication (Basic/Digest). + //ws.SetCredentials ("nobita", "password", false); + // To connect through the HTTP Proxy server. //ws.SetProxy ("http://localhost:3128/", "nobita", "password"); - // To enable the redirection. - //ws.EnableRedirection = true; + // To send a user header. + + var reqHeader = "RequestForID"; + var resHeader = "ID"; + + ws.SetUserHeader (reqHeader, resHeader); + + + // To validate the server certificate. + /* + ws.SslConfiguration.ServerCertificateValidationCallback = + (sender, certificate, chain, sslPolicyErrors) => { + var fmt = "Certificate:\n- Issuer: {0}\n- Subject: {1}"; + var msg = String.Format ( + fmt, + certificate.Issuer, + certificate.Subject + ); + + ws.Log.Debug (msg); + + return true; // If the server certificate is valid. + }; + */ + + // To change the wait time for the response to the Ping or Close. + //ws.WaitTime = TimeSpan.FromSeconds (10); +#endif + // Set the WebSocket events. + + ws.OnClose += + (sender, e) => { + var fmt = "[WebSocket Close ({0})] {1}"; + + Console.WriteLine (fmt, e.Code, e.Reason); + }; + + ws.OnError += + (sender, e) => { + var fmt = "[WebSocket Error] {0}"; + + Console.WriteLine (fmt, e.Message); + }; + + ws.OnMessage += + (sender, e) => { + var fmt = e.IsPing + ? "[WebSocket Ping] {0}" + : "[WebSocket Message] {0}"; + + Console.WriteLine (fmt, e.Data); + }; + + ws.OnOpen += + (sender, e) => { +#if DEBUG + var val = ws.HandshakeResponseHeaders[resHeader]; + + if (!val.IsNullOrEmpty ()) { + var fmt = "[WebSocket Open] {0}: {1}"; + + Console.WriteLine (fmt, resHeader, val); + } +#endif + ws.Send ("Hi, there!"); + }; // Connect to the server. ws.Connect (); @@ -104,11 +128,15 @@ public static void Main (string[] args) // Connect to the server asynchronously. //ws.ConnectAsync (); - Console.WriteLine ("\nType 'exit' to exit.\n"); + Console.WriteLine ("\nType \"exit\" to exit.\n"); + while (true) { Thread.Sleep (1000); + Console.Write ("> "); + var msg = Console.ReadLine (); + if (msg == "exit") break; diff --git a/Example1/AssemblyInfo.cs b/Example1/AssemblyInfo.cs deleted file mode 100644 index a78e6c6de..000000000 --- a/Example1/AssemblyInfo.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; - -// Information about this assembly is defined by the following attributes. -// Change them to the values specific to your project. - -[assembly: AssemblyTitle("Example1")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("")] -[assembly: AssemblyCopyright("sta.blockhead")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". -// The form "{Major}.{Minor}.*" will automatically update the build and revision, -// and "{Major}.{Minor}.{Build}.*" will update just the revision. - -[assembly: AssemblyVersion("1.0.*")] - -// The following attributes are used to specify the signing key for the assembly, -// if desired. See the Mono documentation for more information about signing. - -//[assembly: AssemblyDelaySign(false)] -//[assembly: AssemblyKeyFile("")] diff --git a/Example1/AudioMessage.cs b/Example1/AudioMessage.cs deleted file mode 100644 index 20793d7ba..000000000 --- a/Example1/AudioMessage.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace Example1 -{ - internal class AudioMessage - { - public uint user_id; - public byte ch_num; - public uint buffer_length; - public float[,] buffer_array; - } -} diff --git a/Example1/AudioStreamer.cs b/Example1/AudioStreamer.cs deleted file mode 100644 index 669d5c527..000000000 --- a/Example1/AudioStreamer.cs +++ /dev/null @@ -1,203 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Text; -using System.Threading; -using WebSocketSharp; - -namespace Example1 -{ - internal class AudioStreamer : IDisposable - { - private Dictionary _audioBox; - private Timer _heartbeatTimer; - private uint? _id; - private string _name; - private Notifier _notifier; - private WebSocket _websocket; - - public AudioStreamer (string url) - { - _websocket = new WebSocket (url); - - _audioBox = new Dictionary (); - _heartbeatTimer = new Timer (sendHeartbeat, null, -1, -1); - _id = null; - _notifier = new Notifier (); - - configure (); - } - - private void configure () - { -#if DEBUG - _websocket.Log.Level = LogLevel.Trace; -#endif - _websocket.OnOpen += (sender, e) => - _websocket.Send (createTextMessage ("connection", String.Empty)); - - _websocket.OnMessage += (sender, e) => { - if (e.IsText) { - _notifier.Notify (convertTextMessage (e.Data)); - } - else { - var msg = convertBinaryMessage (e.RawData); - if (msg.user_id == _id) - return; - - 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" - }); - - _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 AudioMessage convertBinaryMessage (byte[] data) - { - 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 convertTextMessage (string data) - { - var json = JObject.Parse (data); - var id = (uint) json["user_id"]; - var name = (string) json["name"]; - var type = (string) json["type"]; - - string body; - if (type == "message") { - body = String.Format ("{0}: {1}", name, (string) json["message"]); - } - else if (type == "start_music") { - body = String.Format ("{0}: Started playing music!", name); - } - else if (type == "connection") { - var users = (JArray) json["message"]; - var buff = new StringBuilder ("Now keeping connections:"); - foreach (JToken user in users) - buff.AppendFormat ( - "\n- user_id: {0} name: {1}", (uint) user["user_id"], (string) user["name"]); - - body = buff.ToString (); - } - else if (type == "connected") { - _id = id; - _heartbeatTimer.Change (30000, 30000); - body = String.Format ("user_id: {0} name: {1}", id, name); - } - else { - body = "Received unknown type message."; - } - - return new NotificationMessage { - Summary = String.Format ("AudioStreamer ({0})", type), - Body = body, - Icon = "notification-message-im" - }; - } - - private byte[] createBinaryMessage (float[,] bufferArray) - { - 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 (); - } - - private string createTextMessage (string type, string message) - { - 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) - { - _name = username; - _websocket.Connect (); - } - - public void Disconnect () - { - _heartbeatTimer.Change (-1, -1); - _websocket.Close (CloseStatusCode.Away); - _audioBox.Clear (); - _id = null; - _name = null; - } - - public void Write (string message) - { - _websocket.Send (createTextMessage ("message", message)); - } - - void IDisposable.Dispose () - { - Disconnect (); - - _heartbeatTimer.Dispose (); - _notifier.Close (); - } - } -} diff --git a/Example1/Example1.csproj b/Example1/Example1.csproj deleted file mode 100644 index 903f5b045..000000000 --- a/Example1/Example1.csproj +++ /dev/null @@ -1,77 +0,0 @@ - - - - Debug - AnyCPU - 9.0.21022 - 2.0 - {390E2568-57B7-4D17-91E5-C29336368CCF} - Exe - Example - example1 - v3.5 - - - true - full - false - bin\Debug - DEBUG; - prompt - 4 - true - - - none - false - bin\Release - prompt - 4 - true - - - true - full - false - bin\Debug_Ubuntu - DEBUG;UBUNTU - prompt - 4 - true - - - none - false - bin\Release_Ubuntu - prompt - 4 - true - UBUNTU - - - - - False - notify-sharp - - - False - - - - - - - - - - - - - - - {B357BAC7-529E-4D81-A0D2-71041B19C8DE} - websocket-sharp - - - \ No newline at end of file diff --git a/Example1/NotificationMessage.cs b/Example1/NotificationMessage.cs deleted file mode 100644 index 01f1692a8..000000000 --- a/Example1/NotificationMessage.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; - -namespace Example1 -{ - internal class NotificationMessage - { - public string Body { - get; set; - } - - public string Icon { - get; set; - } - - public string Summary { - get; set; - } - - public override string ToString () - { - return String.Format ("{0}: {1}", Summary, Body); - } - } -} diff --git a/Example1/Notifier.cs b/Example1/Notifier.cs deleted file mode 100644 index 5bcbb16ac..000000000 --- a/Example1/Notifier.cs +++ /dev/null @@ -1,79 +0,0 @@ -#if UBUNTU -using Notifications; -#endif -using System; -using System.Collections; -using System.Collections.Generic; -using System.Threading; - -namespace Example1 -{ - internal class Notifier : IDisposable - { - private volatile bool _enabled; - private Queue _queue; - private object _sync; - private ManualResetEvent _waitHandle; - - public Notifier () - { - _enabled = true; - _queue = new Queue (); - _sync = ((ICollection) _queue).SyncRoot; - _waitHandle = new ManualResetEvent (false); - - ThreadPool.QueueUserWorkItem ( - state => { - while (_enabled || Count > 0) { - var msg = dequeue (); - if (msg != null) { -#if UBUNTU - var nf = new Notification (msg.Summary, msg.Body, msg.Icon); - nf.AddHint ("append", "allowed"); - nf.Show (); -#else - Console.WriteLine (msg); -#endif - } - else { - Thread.Sleep (500); - } - } - - _waitHandle.Set (); - }); - } - - public int Count { - get { - lock (_sync) - return _queue.Count; - } - } - - private NotificationMessage dequeue () - { - lock (_sync) - return _queue.Count > 0 ? _queue.Dequeue () : null; - } - - public void Close () - { - _enabled = false; - _waitHandle.WaitOne (); - _waitHandle.Close (); - } - - public void Notify (NotificationMessage message) - { - lock (_sync) - if (_enabled) - _queue.Enqueue (message); - } - - void IDisposable.Dispose () - { - Close (); - } - } -} diff --git a/Example1/Program.cs b/Example1/Program.cs deleted file mode 100644 index 7b936da93..000000000 --- a/Example1/Program.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Threading; - -namespace Example1 -{ - public class Program - { - public static void Main (string[] args) - { - using (var streamer = new AudioStreamer ("ws://agektmr.node-ninja.com:3000/socket")) - //using (var streamer = new AudioStreamer ("ws://localhost:3000/socket")) - { - string name; - do { - Console.Write ("Input your name> "); - name = Console.ReadLine (); - } - while (name.Length == 0); - - streamer.Connect (name); - - Console.WriteLine ("\nType 'exit' to exit.\n"); - while (true) { - Thread.Sleep (1000); - Console.Write ("> "); - var msg = Console.ReadLine (); - if (msg == "exit") - break; - - streamer.Write (msg); - } - } - } - } -} diff --git a/Example1/TextMessage.cs b/Example1/TextMessage.cs deleted file mode 100644 index 5eab648f9..000000000 --- a/Example1/TextMessage.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace Example1 -{ - internal class TextMessage - { - public uint? user_id; - public string name; - public string type; - public string message; - } -} diff --git a/Example2/Chat.cs b/Example2/Chat.cs index a330ad38e..2391bf313 100644 --- a/Example2/Chat.cs +++ b/Example2/Chat.cs @@ -12,21 +12,25 @@ public class Chat : WebSocketBehavior private string _prefix; public Chat () - : this (null) { + _prefix = "anon#"; } - public Chat (string prefix) - { - _prefix = !prefix.IsNullOrEmpty () ? prefix : "anon#"; + public string Prefix { + get { + return _prefix; + } + + set { + _prefix = !value.IsNullOrEmpty () ? value : "anon#"; + } } private string getName () { - var name = Context.QueryString["name"]; - return !name.IsNullOrEmpty () - ? name - : (_prefix + getNumber ()); + var name = QueryString["name"]; + + return !name.IsNullOrEmpty () ? name : _prefix + getNumber (); } private static int getNumber () @@ -34,19 +38,33 @@ private static int getNumber () return Interlocked.Increment (ref _number); } - protected override void OnOpen () + protected override void OnClose (CloseEventArgs e) { - _name = getName (); + if (_name == null) + return; + + var fmt = "{0} got logged off..."; + var msg = String.Format (fmt, _name); + + Sessions.Broadcast (msg); } protected override void OnMessage (MessageEventArgs e) { - Sessions.Broadcast (String.Format ("{0}: {1}", _name, e.Data)); + var fmt = "{0}: {1}"; + var msg = String.Format (fmt, _name, e.Data); + + Sessions.Broadcast (msg); } - protected override void OnClose (CloseEventArgs e) + protected override void OnOpen () { - Sessions.Broadcast (String.Format ("{0} got logged off...", _name)); + _name = getName (); + + var fmt = "{0} has logged in!"; + var msg = String.Format (fmt, _name); + + Sessions.Broadcast (msg); } } } diff --git a/Example2/Echo.cs b/Example2/Echo.cs index f0a087caa..edc8872f9 100644 --- a/Example2/Echo.cs +++ b/Example2/Echo.cs @@ -8,9 +8,7 @@ public class Echo : WebSocketBehavior { protected override void OnMessage (MessageEventArgs e) { - var name = Context.QueryString["name"]; - var msg = !name.IsNullOrEmpty () ? String.Format ("'{0}' to {1}", e.Data, name) : e.Data; - Send (msg); + Send (e.Data); } } } diff --git a/Example2/Program.cs b/Example2/Program.cs index 02f003dc8..4a2e8c0bf 100644 --- a/Example2/Program.cs +++ b/Example2/Program.cs @@ -11,94 +11,161 @@ public class Program { 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 the instance with - * the 'secure' parameter set to true, or the wss scheme WebSocket URL. - */ + // 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 + // with a wss scheme WebSocket URL. + var wssv = new WebSocketServer (4649); //var wssv = new WebSocketServer (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 (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 ("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); -#endif - /* 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; - - // Return user name, password, and roles. - return name == "nobita" - ? new NetworkCredential (name, "password", "gunfighter") - : null; // If the user credentials aren't found. - }; + wssv.UserCredentialsFinder = + id => { + 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. + }; */ - // Not to remove the inactive sessions periodically. - //wssv.KeepClean = false; + // To remove the inactive sessions periodically. + //wssv.KeepClean = true; // To resolve to wait for socket in TIME_WAIT state. //wssv.ReuseAddress = true; + // To provide the secure connection. + /* + var cert = ConfigurationManager.AppSettings["ServerCertFile"]; + var passwd = ConfigurationManager.AppSettings["CertFilePassword"]; + + wssv.SslConfiguration.ServerCertificate = new X509Certificate2 ( + cert, + passwd + ); + */ + + // To change the wait time for the response to the WebSocket Ping or Close. + //wssv.WaitTime = TimeSpan.FromSeconds (2); +#endif // Add the WebSocket services. + wssv.AddWebSocketService ("/Echo"); - wssv.AddWebSocketService ("/Chat"); - /* Add the WebSocket service with initializing. + // With initializing. wssv.AddWebSocketService ( "/Chat", - () => new Chat ("Anon#") { - Protocol = "chat", - // To emit a WebSocket.OnMessage event when receives a Ping. - EmitOnPing = true, + s => { + s.Prefix = "Anon#"; +#if DEBUG + // To respond to the cookies. + /* + s.CookiesResponder = + (reqCookies, resCookies) => { + foreach (var cookie in reqCookies) { + cookie.Expired = true; + + resCookies.Add (cookie); + } + }; + */ + + // To emit a WebSocket.OnMessage event when receives a ping. + //s.EmitOnPing = true; + // To ignore the Sec-WebSocket-Extensions header. - IgnoreExtensions = true, + //s.IgnoreExtensions = true; + + // To disable a delay when send or receive buffer of the underlying + // TCP socket is not full. + s.NoDelay = 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. - } - }); - */ + /* + s.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 send the Sec-WebSocket-Protocol header that has a subprotocol + // name. + //s.Protocol = "chat"; + + // To respond to the user headers. + + s.UserHeadersResponder = + (reqHeaders, userHeaders) => { + var val = reqHeaders["RequestForID"]; + + if (!val.IsNullOrEmpty ()) + userHeaders[val] = s.ID; + }; + +#endif + } + ); + + // Start the server. wssv.Start (); + if (wssv.IsListening) { - Console.WriteLine ("Listening on port {0}, and providing WebSocket services:", wssv.Port); + var fmt = "Listening on port {0}, and providing WebSocket services:"; + + Console.WriteLine (fmt, wssv.Port); + foreach (var path in wssv.WebSocketServices.Paths) Console.WriteLine ("- {0}", path); } Console.WriteLine ("\nPress Enter key to stop the server..."); + Console.ReadLine (); + // Stop the server. wssv.Stop (); } } diff --git a/Example3/App.config b/Example3/App.config index b65960f8c..fa624b42b 100644 --- a/Example3/App.config +++ b/Example3/App.config @@ -2,7 +2,7 @@ - + diff --git a/Example3/Chat.cs b/Example3/Chat.cs index 7708a1a73..0e3f38214 100644 --- a/Example3/Chat.cs +++ b/Example3/Chat.cs @@ -12,21 +12,25 @@ public class Chat : WebSocketBehavior private string _prefix; public Chat () - : this (null) { + _prefix = "anon#"; } - public Chat (string prefix) - { - _prefix = !prefix.IsNullOrEmpty () ? prefix : "anon#"; + public string Prefix { + get { + return _prefix; + } + + set { + _prefix = !value.IsNullOrEmpty () ? value : "anon#"; + } } private string getName () { - var name = Context.QueryString["name"]; - return !name.IsNullOrEmpty () - ? name - : (_prefix + getNumber ()); + var name = QueryString["name"]; + + return !name.IsNullOrEmpty () ? name : _prefix + getNumber (); } private static int getNumber () @@ -34,19 +38,33 @@ private static int getNumber () return Interlocked.Increment (ref _number); } - protected override void OnOpen () + protected override void OnClose (CloseEventArgs e) { - _name = getName (); + if (_name == null) + return; + + var fmt = "{0} got logged off..."; + var msg = String.Format (fmt, _name); + + Sessions.Broadcast (msg); } protected override void OnMessage (MessageEventArgs e) { - Sessions.Broadcast (String.Format ("{0}: {1}", _name, e.Data)); + var fmt = "{0}: {1}"; + var msg = String.Format (fmt, _name, e.Data); + + Sessions.Broadcast (msg); } - protected override void OnClose (CloseEventArgs e) + protected override void OnOpen () { - Sessions.Broadcast (String.Format ("{0} got logged off...", _name)); + _name = getName (); + + var fmt = "{0} has logged in!"; + var msg = String.Format (fmt, _name); + + Sessions.Broadcast (msg); } } } diff --git a/Example3/Echo.cs b/Example3/Echo.cs index 1e0701914..01e2f16af 100644 --- a/Example3/Echo.cs +++ b/Example3/Echo.cs @@ -8,9 +8,7 @@ public class Echo : WebSocketBehavior { protected override void OnMessage (MessageEventArgs e) { - var name = Context.QueryString["name"]; - var msg = !name.IsNullOrEmpty () ? String.Format ("'{0}' to {1}", e.Data, name) : e.Data; - Send (msg); + Send (e.Data); } } } diff --git a/Example3/Program.cs b/Example3/Program.cs index 68c1657c4..259b5164b 100644 --- a/Example3/Program.cs +++ b/Example3/Program.cs @@ -12,120 +12,198 @@ public class Program { 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 the instance with - * the 'secure' parameter set to true, or the https scheme HTTP URL. - */ + // 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 + // with an https scheme HTTP URL. + var httpsv = new HttpServer (4649); //var httpsv = new HttpServer (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 (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://[::]:4649/"); + //var httpsv = new HttpServer ("https://[::]: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 ("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); -#endif - /* To provide the secure connection. + // To provide the HTTP Authentication (Basic/Digest). + /* + httpsv.AuthenticationSchemes = AuthenticationSchemes.Basic; + httpsv.Realm = "WebSocket Test"; + httpsv.UserCredentialsFinder = + id => { + 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. + }; + */ + + // To remove the inactive WebSocket sessions periodically. + //httpsv.KeepClean = true; + + // To resolve to wait for socket in TIME_WAIT state. + //httpsv.ReuseAddress = true; + + // 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). - httpsv.AuthenticationSchemes = AuthenticationSchemes.Basic; - httpsv.Realm = "WebSocket Test"; - httpsv.UserCredentialsFinder = id => { - 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. - }; + httpsv.SslConfiguration.ServerCertificate = new X509Certificate2 ( + cert, + passwd + ); */ - // To set the document root path. - httpsv.RootPath = ConfigurationManager.AppSettings["RootPath"]; + // To change the wait time for the response to the WebSocket Ping or Close. + //httpsv.WaitTime = TimeSpan.FromSeconds (2); +#endif + // Set the document root path. + httpsv.DocumentRootPath = ConfigurationManager + .AppSettings["DocumentRootPath"]; + + // Set the HTTP GET request event. + httpsv.OnGet += + (sender, e) => { + var req = e.Request; + var res = e.Response; - // To set the HTTP GET method event. - httpsv.OnGet += (sender, e) => { - var req = e.Request; - var res = e.Response; + var path = req.RawUrl; - var path = req.RawUrl; - if (path == "/") - path += "index.html"; + if (path == "/") + path += "index.html"; - var content = httpsv.GetFile (path); - if (content == null) { - res.StatusCode = (int) HttpStatusCode.NotFound; - return; - } + byte[] contents; - if (path.EndsWith (".html")) { - res.ContentType = "text/html"; - res.ContentEncoding = Encoding.UTF8; - } + if (!e.TryReadFile (path, out contents)) { + res.StatusCode = (int) HttpStatusCode.NotFound; - res.WriteContent (content); - }; + return; + } - // Not to remove the inactive WebSocket sessions periodically. - //httpsv.KeepClean = false; + 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; + } - // To resolve to wait for socket in TIME_WAIT state. - //httpsv.ReuseAddress = true; + res.ContentLength64 = contents.LongLength; + + res.Close (contents, true); + }; // Add the WebSocket services. + httpsv.AddWebSocketService ("/Echo"); - httpsv.AddWebSocketService ("/Chat"); - /* Add the WebSocket service with initializing. + // With initializing. httpsv.AddWebSocketService ( "/Chat", - () => new Chat ("Anon#") { - Protocol = "chat", - // To emit a WebSocket.OnMessage event when receives a Ping. - EmitOnPing = true, + s => { + s.Prefix = "Anon#"; +#if DEBUG + // To respond to the cookies. + /* + s.CookiesResponder = + (reqCookies, resCookies) => { + foreach (var cookie in reqCookies) { + cookie.Expired = true; + + resCookies.Add (cookie); + } + }; + */ + + // To emit a WebSocket.OnMessage event when receives a ping. + //s.EmitOnPing = true; + // To ignore the Sec-WebSocket-Extensions header. - IgnoreExtensions = true, + //s.IgnoreExtensions = true; + + // To disable a delay when send or receive buffer of the underlying + // TCP socket is not full. + s.NoDelay = 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. - } - }); - */ + /* + s.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 send the Sec-WebSocket-Protocol header that has a subprotocol + // name. + //s.Protocol = "chat"; + + // To respond to the user headers. + + s.UserHeadersResponder = + (reqHeaders, userHeaders) => { + var val = reqHeaders["RequestForID"]; + + if (!val.IsNullOrEmpty ()) + userHeaders[val] = s.ID; + }; + +#endif + } + ); + // Start the server. httpsv.Start (); + if (httpsv.IsListening) { - Console.WriteLine ("Listening on port {0}, and providing WebSocket services:", httpsv.Port); + var fmt = "Listening on port {0}, and providing WebSocket services:"; + + Console.WriteLine (fmt, httpsv.Port); + foreach (var path in httpsv.WebSocketServices.Paths) Console.WriteLine ("- {0}", path); } Console.WriteLine ("\nPress Enter key to stop the server..."); + Console.ReadLine (); + // Stop the server. httpsv.Stop (); } } diff --git a/Example3/Public/Js/echotest.js b/Example3/Public/Js/echotest.js index 3f99991d1..a356f0d3e 100644 --- a/Example3/Public/Js/echotest.js +++ b/Example3/Public/Js/echotest.js @@ -18,39 +18,39 @@ function init () { function doWebSocket () { websocket = new WebSocket (url); - websocket.onopen = function (evt) { - onOpen (evt) + websocket.onopen = function (e) { + onOpen (e); }; - websocket.onclose = function (evt) { - onClose (evt) + websocket.onmessage = function (e) { + onMessage (e); }; - websocket.onmessage = function (evt) { - onMessage (evt) + websocket.onerror = function (e) { + onError (e); }; - websocket.onerror = function (evt) { - onError (evt) + websocket.onclose = function (e) { + onClose (e); }; } -function onOpen (evt) { +function onOpen (event) { writeToScreen ("CONNECTED"); send ("WebSocket rocks"); } -function onClose (evt) { - writeToScreen ("DISCONNECTED"); +function onMessage (event) { + writeToScreen ('RESPONSE: ' + event.data + ''); + websocket.close (); } -function onMessage (evt) { - writeToScreen ('RESPONSE: ' + evt.data + ''); - websocket.close (); +function onError (event) { + writeToScreen ('ERROR: ' + event.data + ''); } -function onError (evt) { - writeToScreen('ERROR: ' + evt.data + ''); +function onClose (event) { + writeToScreen ("DISCONNECTED"); } function send (message) { diff --git a/LICENSE.txt b/LICENSE.txt index 39c64688b..b8fa987a2 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2010-2015 sta.blockhead +Copyright (c) 2010-2025 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 985b5863e..35f94d454 100644 --- a/README.md +++ b/README.md @@ -2,65 +2,47 @@ ## 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 **3.5** or later (includes compatible) +- [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, Cookies, and User headers](#query-string-origin-header-cookies-and-user-headers) +- [Connecting through the HTTP proxy server](#connecting-through-the-http-proxy-server) +- .NET Framework **3.5** or later versions of .NET Framework (includes compatible environment such as [Mono]) ## 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 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. +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. ## 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 **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 the **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**. - -- **[WebSocket-Sharp for Unity]** - -It works with **Unity Free**, but there are some limitations: - -- **[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 2.0, such as the `System.Func<...>` delegates (so i've fixed it in the asset package). - -And it's priced at **US$15**. I think your $15 makes this project more better and accelerated, **Thank you!** - ## Usage ## ### WebSocket Client ### @@ -77,7 +59,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"); @@ -102,13 +84,19 @@ 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")) { ... } ``` -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. +This will **close** the WebSocket connection with status code `1001` (going away) when the control leaves the `using` block. #### Step 3 #### @@ -116,43 +104,43 @@ Setting the `WebSocket` events. ##### WebSocket.OnOpen Event ##### -A `WebSocket.OnOpen` event occurs when the WebSocket connection has been established. +This event occurs when the WebSocket connection has been established. ```csharp ws.OnOpen += (sender, e) => { - ... -}; + ... + }; ``` -`e` has passed as the `System.EventArgs.Empty`, so you don't need to use it. +`System.EventArgs.Empty` is passed as `e`, so you do not need to use it. ##### WebSocket.OnMessage Event ##### -A `WebSocket.OnMessage` event occurs when the `WebSocket` receives a message. +This event occurs when the `WebSocket` instance receives a message. ```csharp ws.OnMessage += (sender, e) => { - ... -}; + ... + }; ``` -`e` has passed as a `WebSocketSharp.MessageEventArgs`. +A `WebSocketSharp.MessageEventArgs` instance is passed as `e`. -`e.Type` property returns either `WebSocketSharp.Opcode.Text` or `WebSocketSharp.Opcode.Binary` that represents the message type. So by checking it, you can determine which item you should use. +If you would like to get the message data, you should access `e.Data` or `e.RawData` property. -If it returns `Opcode.Text`, you should use `e.Data` property that returns a `string` (represents a **text** message). +`e.Data` property returns a `string`, so it is mainly used to get the **text** message data. -Or if it returns `Opcode.Binary`, you should use `e.RawData` property that returns a `byte[]` (represents a **binary** message). +`e.RawData` property returns a `byte[]`, so it is mainly used to get the **binary** message data. ```csharp -if (e.Type == Opcode.Text) { +if (e.IsText) { // Do something with e.Data. ... return; } -if (e.Type == Opcode.Binary) { +if (e.IsBinary) { // Do something with e.RawData. ... @@ -160,51 +148,55 @@ if (e.Type == Opcode.Binary) { } ``` -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. +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`. ```csharp ws.EmitOnPing = true; ws.OnMessage += (sender, e) => { - if (e.Type == Opcode.Ping) { - // 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 ##### -A `WebSocket.OnError` event occurs when the `WebSocket` gets an error. +This event occurs when the `WebSocket` instance gets an error. ```csharp ws.OnError += (sender, e) => { - ... -}; + ... + }; ``` -`e` has passed as a `WebSocketSharp.ErrorEventArgs`. +A `WebSocketSharp.ErrorEventArgs` instance is passed as `e`. + +If you would like to get the error message, you should access `e.Message` property. `e.Message` property returns a `string` that represents the error message. -If the error is due to an exception, `e.Exception` property returns a `System.Exception` instance that caused the error. +And `e.Exception` property returns a `System.Exception` instance that represents the cause of the error if it is due to an exception. ##### WebSocket.OnClose Event ##### -A `WebSocket.OnClose` event occurs when the WebSocket connection has been closed. +This event occurs when the WebSocket connection has been closed. ```csharp ws.OnClose += (sender, e) => { - ... -}; + ... + }; ``` -`e` has passed as a `WebSocketSharp.CloseEventArgs`. +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.Code` property returns a `ushort` that represents the status code 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. +`e.Reason` property returns a `string` that represents the reason for the close. #### Step 4 #### @@ -226,7 +218,7 @@ ws.Send (data); The `WebSocket.Send` method is overloaded. -You can use the `WebSocket.Send (string)`, `WebSocket.Send (byte[])`, or `WebSocket.Send (System.IO.FileInfo)` method to send the data. +You can use the `WebSocket.Send (string)`, `WebSocket.Send (byte[])`, `WebSocket.Send (System.IO.FileInfo)`, or `WebSocket.Send (System.IO.Stream, int)` method to send the data. If you would like to send the data asynchronously, you should use the `WebSocket.SendAsync` method. @@ -266,7 +258,7 @@ namespace Example protected override void OnMessage (MessageEventArgs e) { var msg = e.Data == "BALUS" - ? "I've been balused already..." + ? "Are you kidding?" : "I'm not available now."; Send (msg); @@ -278,6 +270,7 @@ namespace Example public static void Main (string[] args) { var wssv = new WebSocketServer ("ws://dragonsnest.far"); + wssv.AddWebSocketService ("/Laputa"); wssv.Start (); Console.ReadKey (true); @@ -329,13 +322,18 @@ public class Chat : WebSocketBehavior private string _suffix; public Chat () - : this (null) { + _suffix = String.Empty; } - public Chat (string suffix) - { - _suffix = suffix ?? String.Empty; + public string Suffix { + get { + return _suffix; + } + + set { + _suffix = value ?? String.Empty; + } } protected override void OnMessage (MessageEventArgs e) @@ -347,15 +345,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's called when the `WebSocket` used in a session in the service receives a message. +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. -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. +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. -The `WebSocketBehavior.Send` method sends data to the client on a session in the service. +The `WebSocketBehavior.Send` method can send data to the client on a session in the service. -If you would like to access the sessions in the service, you should use the `WebSocketBehavior.Sessions` property (returns a `WebSocketSharp.Server.WebSocketSessionManager`). +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 sends data to every client in the service. +The `WebSocketBehavior.Sessions.Broadcast` method can send data to every client in the service. #### Step 3 #### @@ -363,20 +361,19 @@ Creating a new instance of the `WebSocketServer` class. ```csharp var wssv = new WebSocketServer (4649); + wssv.AddWebSocketService ("/Echo"); wssv.AddWebSocketService ("/Chat"); -wssv.AddWebSocketService ("/ChatWithNyan", () => new Chat (" Nyan!")); +wssv.AddWebSocketService ("/ChatWithNyan", s => s.Suffix = " Nyan!"); ``` -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. +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, Action)` 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 must have a public parameterless constructor. -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 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. +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. $ sudo mono example2.exe @@ -393,26 +390,23 @@ wssv.Start (); Stopping the WebSocket server. ```csharp -wssv.Stop (code, reason); +wssv.Stop (); ``` -The `WebSocketServer.Stop` method is overloaded. - -You can use the `WebSocketServer.Stop ()`, `WebSocketServer.Stop (ushort, string)`, or `WebSocketServer.Stop (WebSocketSharp.CloseStatusCode, string)` method to stop the server. - ### HTTP Server with the WebSocket ### -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. +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. So websocket-sharp provides the `WebSocketSharp.Server.HttpServer` class. -You can add any WebSocket service to your `HttpServer` with the specified behavior and path to the service, by using the `HttpServer.AddWebSocketService (string)` or `HttpServer.AddWebSocketService (string, Func)` method. +You can add any WebSocket service to your `HttpServer` with the specified behavior and path to the service, by using the `HttpServer.AddWebSocketService (string)` or `HttpServer.AddWebSocketService (string, Action)` method. ```csharp var httpsv = new HttpServer (4649); + httpsv.AddWebSocketService ("/Echo"); httpsv.AddWebSocketService ("/Chat"); -httpsv.AddWebSocketService ("/ChatWithNyan", () => new Chat (" Nyan!")); +httpsv.AddWebSocketService ("/ChatWithNyan", s => s.Suffix = " Nyan!"); ``` For more information, would you see **[Example3]**? @@ -421,19 +415,21 @@ For more information, would you see **[Example3]**? #### Per-message Compression #### -websocket-sharp supports the **[Per-message Compression][compression]** extension (but doesn't support this extension with the [context take over]). +websocket-sharp supports the [Per-message Compression][rfc7692] extension (but does not support it with the [context take over]). -As a WebSocket client, if you would like to enable this extension, you should set such as the following. +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. ```csharp ws.Compression = CompressionMethod.Deflate; ``` -And then your client will send the following header in the connection request to the server. +And then the client will send the following header in the handshake request to the server. Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover -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. +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. #### Ignoring the extensions #### @@ -442,29 +438,25 @@ As a WebSocket server, if you would like to ignore the extensions requested from ```csharp wssv.AddWebSocketService ( "/Chat", - () => new Chat () { - // To ignore the extensions requested from a client. - IgnoreExtensions = true - }); + s => s.IgnoreExtensions = true // To ignore the extensions requested from a client. +); ``` -If it's set to `true`, the server doesn't return the **Sec-WebSocket-Extensions header** in the connection response. +If it is set to `true`, the service will not return the Sec-WebSocket-Extensions header in its handshake 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 the **wss** scheme WebSocket URL. +As a WebSocket client, you should create a new instance of the `WebSocket` class with a **wss** scheme WebSocket URL. ```csharp -using (var ws = new WebSocket ("wss://example.com")) { - ... -} +var ws = new WebSocket ("wss://example.com"); ``` -And if you would like to use the custom validation for the server certificate, you should set the `WebSocket.SslConfiguration.ServerCertificateValidationCallback` property. +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. ```csharp ws.SslConfiguration.ServerCertificateValidationCallback = @@ -476,43 +468,44 @@ ws.SslConfiguration.ServerCertificateValidationCallback = }; ``` -If you set this property to nothing, the validation does nothing with the server certificate, and returns `true`. +The default callback always returns `true`. -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. +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. ```csharp var wssv = new WebSocketServer (5963, true); -wssv.SslConfiguration.ServerCertificate = - new X509Certificate2 ("/path/to/cert.pfx", "password for cert.pfx"); +wssv.SslConfiguration.ServerCertificate = new X509Certificate2 ( + "/path/to/cert.pfx", "password for cert.pfx" + ); ``` ### 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 connecting. +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. ```csharp ws.SetCredentials ("nobita", "password", preAuth); ``` -If `preAuth` is `true`, the `WebSocket` sends the Basic authentication credentials with the first connection request to the server. +If `preAuth` is `true`, the client will send the credentials for the Basic authentication in the first 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. +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. -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. +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. ```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 aren't found. -}; + // Return user name, password, and roles. + return name == "nobita" + ? new NetworkCredential (name, "password", "gunfighter") + : null; // If the user credentials are not found. + }; ``` If you would like to provide the Digest authentication, you should set such as the following. @@ -521,29 +514,17 @@ 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, Cookies, and User headers ### -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. +#### Query string #### -```csharp -using (var ws = new WebSocket ("ws://example.com/?name=nobita")) { - ... -} -``` - -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. +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. ```csharp -ws.Origin = "http://example.com/"; +var ws = new WebSocket ("ws://example.com/?name=nobita"); ``` -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 WebSocket connection 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 handshake request, you should access the `WebSocketBehavior.QueryString` property, such as the following. ```csharp public class Chat : WebSocketBehavior @@ -553,53 +534,110 @@ public class Chat : WebSocketBehavior protected override void OnOpen () { - _name = Context.QueryString["name"]; + _name = QueryString["name"]; } ... } ``` -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. +#### Origin header #### + +As a WebSocket client, 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. + +```csharp +ws.Origin = "http://example.com/"; +``` + +As a WebSocket server, if you would like to validate the Origin header, you should set a validation for it with your `WebSocketBehavior`, for example, by using the `WebSocketServer.AddWebSocketService (string, Action)` 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); - } + s => { + s.OriginValidator = + val => { + // Check the value of the Origin header, and return true if valid. - return true; // If valid. - } - }); + Uri origin; + + return !val.IsNullOrEmpty () + && Uri.TryCreate (val, UriKind.Absolute, out origin) + && origin.Host == "example.com"; + }; + } +); +``` + +#### Cookies #### + +As a WebSocket client, 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. + +```csharp +ws.SetCookie (new Cookie ("name", "nobita")); +``` + +As a WebSocket server, if you would like to respond to the cookies, you should set a response action for it with your `WebSocketBehavior`, for example, by using the `WebSocketServer.AddWebSocketService (string, Action)` method with initializing, such as the following. + +```csharp +wssv.AddWebSocketService ( + "/Chat", + s => { + s.CookiesResponder = + (reqCookies, resCookies) => { + foreach (var cookie in reqCookies) { + cookie.Expired = true; + + resCookies.Add (cookie); + } + }; + } +); +``` + +#### User headers #### + +As a WebSocket client, if you would like to send the user headers in the handshake request, you should set any user defined header by using the `WebSocket.SetUserHeader (string, string)` method before calling the connect method. + +```csharp +ws.SetUserHeader ("RequestForID", "ID"); +``` + +And if you would like to get the user headers included in the handshake response, you should access the `WebSocket.HandshakeResponseHeaders` property after the handshake is done. + +```csharp +var id = ws.HandshakeResponseHeaders["ID"]; ``` -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. +As a WebSocket server, if you would like to respond to the user headers, you should set a response action for it with your `WebSocketBehavior`, for example, by using the `WebSocketServer.AddWebSocketService (string, Action)` method with initializing, such as the following. -### Connecting through the HTTP Proxy server ### +```csharp +wssv.AddWebSocketService ( + "/Chat", + s => { + s.UserHeadersResponder = + (reqHeaders, userHeaders) => { + var val = reqHeaders["RequestForID"]; + + if (!val.IsNullOrEmpty ()) + userHeaders[val] = s.ID; + }; + } +); +``` + +### 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 connecting. +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. ```csharp var ws = new WebSocket ("ws://example.com"); ws.SetProxy ("http://localhost:3128/", "nobita", "password"); ``` -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`). +I have tested this with **[Squid]**. It is necessary to disable the following option in **squid.conf** (e.g. `/etc/squid/squid.conf`). ``` # Deny CONNECT to other than SSL ports @@ -608,7 +646,7 @@ I tested this with the [Squid]. And it's necessary to disable the following conf ### Logging ### -The `WebSocket` class includes the own logging function. +The `WebSocket` class has the own logging function. You can use it with the `WebSocket.Log` property (returns a `WebSocketSharp.Logger`). @@ -626,7 +664,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 include the same logging function. +The `WebSocketServer` and `HttpServer` classes have the same logging function. ## Examples ## @@ -634,65 +672,51 @@ Examples using websocket-sharp. ### Example ### -**[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]**. +[Example] connects to the server executed by [Example2] or [Example3]. ### Example2 ### -**[Example2]** starts a WebSocket server. +[Example2] starts a WebSocket server. ### Example3 ### -**[Example3]** starts an HTTP server that allows to accept the WebSocket connection requests. +[Example3] starts an HTTP server that allows to accept the WebSocket handshake requests. -Would you access to [http://localhost:4649](http://localhost:4649) to do **WebSocket Echo Test** with your web browser after Example3 running? +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][rfc6455]**, and it's based on the following WebSocket references: +websocket-sharp supports **RFC 6455**, and it is based on the following 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][rfc7692] 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 [NuGet Gallery: websocket-sharp]: http://www.nuget.org/packages/WebSocketSharp -[Origin header]: http://tools.ietf.org/html/rfc6454#section-7 +[Origin]: http://tools.ietf.org/html/rfc6454#section-7 [Query]: http://tools.ietf.org/html/rfc3986#section-3.4 -[Security Sandbox of the Webplayer]: http://docs.unity3d.com/Manual/SecuritySandbox.html [Squid]: http://www.squid-cache.org [The MIT License]: https://raw.github.com/sta/websocket-sharp/master/LICENSE.txt [Unity]: http://unity3d.com -[WebGL Networking]: http://docs.unity3d.com/Manual/webgl-networking.html -[WebSocket-Sharp for Unity]: http://u3d.as/content/sta-blockhead/websocket-sharp-for-unity [api]: http://www.w3.org/TR/websockets [api_ja]: http://www.hcn.zaq.ne.jp/___/WEB/WebSocket-ja.html -[compression]: http://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-19 -[context take over]: http://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-19#section-8.1.1 +[context take over]: https://datatracker.ietf.org/doc/html/rfc7692#section-7.1.1 [draft-hixie-thewebsocketprotocol-75]: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75 [draft-ietf-hybi-thewebsocketprotocol-00]: http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00 [draft75]: https://github.com/sta/websocket-sharp/tree/draft75 @@ -701,3 +725,4 @@ websocket-sharp is provided under **[The MIT License]**. [rfc2617]: http://tools.ietf.org/html/rfc2617 [rfc6455]: http://tools.ietf.org/html/rfc6455 [rfc6455_ja]: http://www.hcn.zaq.ne.jp/___/WEB/RFC6455-ja.html +[rfc7692]: https://datatracker.ietf.org/doc/html/rfc7692 diff --git a/websocket-sharp.sln b/websocket-sharp.sln index 3c20e06a0..6ff1c0362 100644 --- a/websocket-sharp.sln +++ b/websocket-sharp.sln @@ -5,8 +5,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "websocket-sharp", "websocke EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example", "Example\Example.csproj", "{52805AEC-EFB1-4F42-BB8E-3ED4E692C568}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example1", "Example1\Example1.csproj", "{390E2568-57B7-4D17-91E5-C29336368CCF}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example2", "Example2\Example2.csproj", "{B81A24C8-25BB-42B2-AF99-1E1EACCE74C7}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example3", "Example3\Example3.csproj", "{C648BA25-77E5-4A40-A97F-D0AA37B9FB26}" @@ -19,14 +17,6 @@ Global 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 diff --git a/websocket-sharp/CloseEventArgs.cs b/websocket-sharp/CloseEventArgs.cs index e1cd18420..8aa46e2d2 100644 --- a/websocket-sharp/CloseEventArgs.cs +++ b/websocket-sharp/CloseEventArgs.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2024 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 @@ -35,77 +35,29 @@ namespace WebSocketSharp /// /// /// - /// A event occurs when the WebSocket connection - /// has been closed. + /// The close event occurs when the WebSocket connection has been closed. /// /// - /// If you would like to get the reason for the close, you should access - /// the or property. + /// If you would like to get the reason for the connection close, + /// you should access the or + /// property. /// /// public class CloseEventArgs : EventArgs { #region Private Fields - private bool _clean; - private ushort _code; private PayloadData _payloadData; - private string _reason; + private bool _wasClean; #endregion #region Internal Constructors - internal CloseEventArgs () - { - _code = (ushort) CloseStatusCode.NoStatus; - _payloadData = PayloadData.Empty; - } - - internal CloseEventArgs (ushort code) - { - _code = code; - } - - internal CloseEventArgs (CloseStatusCode code) - : this ((ushort) code) - { - } - - internal CloseEventArgs (PayloadData payloadData) + internal CloseEventArgs (PayloadData payloadData, bool clean) { _payloadData = payloadData; - - var data = payloadData.ApplicationData; - var len = data.Length; - _code = len > 1 - ? data.SubArray (0, 2).ToUInt16 (ByteOrder.Big) - : (ushort) CloseStatusCode.NoStatus; - - _reason = len > 2 - ? data.SubArray (2, len - 2).UTF8Decode () - : String.Empty; - } - - internal CloseEventArgs (ushort code, string reason) - { - _code = code; - _reason = reason; - } - - internal CloseEventArgs (CloseStatusCode code, string reason) - : this ((ushort) code, reason) - { - } - - #endregion - - #region Internal Properties - - internal PayloadData PayloadData { - get { - return _payloadData ?? (_payloadData = new PayloadData (_code.Append (_reason))); - } + _wasClean = clean; } #endregion @@ -113,26 +65,38 @@ internal PayloadData PayloadData { #region Public Properties /// - /// Gets the status code for the close. + /// Gets the status code for the connection close. /// /// - /// A that represents the status code for the close if any. + /// + /// A that represents the status code for + /// the connection close. + /// + /// + /// 1005 (no status) if not present. + /// /// public ushort Code { get { - return _code; + return _payloadData.Code; } } /// - /// Gets the reason for the close. + /// Gets the reason for the connection close. /// /// - /// A that represents the reason for the close if any. + /// + /// A that represents the reason for + /// the connection close. + /// + /// + /// An empty string if not present. + /// /// public string Reason { get { - return _reason ?? String.Empty; + return _payloadData.Reason; } } @@ -140,15 +104,12 @@ public string Reason { /// Gets a value indicating whether the connection has been closed cleanly. /// /// - /// true if the connection has been closed cleanly; otherwise, false. + /// true if the connection has been closed cleanly; otherwise, + /// false. /// public bool WasClean { get { - return _clean; - } - - internal set { - _clean = value; + return _wasClean; } } diff --git a/websocket-sharp/CloseStatusCode.cs b/websocket-sharp/CloseStatusCode.cs index 74bb9da7e..81f3317a4 100644 --- a/websocket-sharp/CloseStatusCode.cs +++ b/websocket-sharp/CloseStatusCode.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-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 @@ -36,12 +36,12 @@ namespace WebSocketSharp /// /// /// The values of this enumeration are defined in - /// Section 7.4 of RFC 6455. + /// + /// Section 7.4 of RFC 6455. /// /// - /// "Reserved value" must not be set as a status code in a connection close frame by - /// an endpoint. It's designated for use in applications expecting a status code to - /// indicate that the connection was closed due to the system grounds. + /// "Reserved value" cannot be sent as a status code in + /// closing handshake by an endpoint. /// /// public enum CloseStatusCode : ushort @@ -51,17 +51,19 @@ public enum CloseStatusCode : ushort /// Normal = 1000, /// - /// Equivalent to close status 1001. Indicates that an endpoint is going away. + /// Equivalent to close status 1001. Indicates that an endpoint is + /// going away. /// Away = 1001, /// - /// Equivalent to close status 1002. Indicates that an endpoint is terminating - /// the connection due to a protocol error. + /// Equivalent to close status 1002. Indicates that an endpoint is + /// terminating the connection due to a protocol error. /// ProtocolError = 1002, /// - /// Equivalent to close status 1003. Indicates that an endpoint is terminating - /// the connection because it has received a type of data that it cannot accept. + /// Equivalent to close status 1003. Indicates that an endpoint is + /// terminating the connection because it has received a type of + /// data that it cannot accept. /// UnsupportedData = 1003, /// @@ -69,46 +71,49 @@ public enum CloseStatusCode : ushort /// Undefined = 1004, /// - /// Equivalent to close status 1005. Indicates that no status code was actually present. - /// A Reserved value. + /// Equivalent to close status 1005. Indicates that no status code was + /// actually present. A Reserved value. /// NoStatus = 1005, /// - /// Equivalent to close status 1006. Indicates that the connection was closed abnormally. - /// A Reserved value. + /// Equivalent to close status 1006. Indicates that the connection was + /// closed abnormally. A Reserved value. /// Abnormal = 1006, /// - /// Equivalent to close status 1007. Indicates that an endpoint is terminating - /// the connection because it has received a message that contains data that - /// isn't consistent with the type of the message. + /// Equivalent to close status 1007. Indicates that an endpoint is + /// terminating the connection because it has received a message that + /// contains data that is not consistent with the type of the message. /// InvalidData = 1007, /// - /// Equivalent to close status 1008. Indicates that an endpoint is terminating - /// the connection because it has received a message that violates its policy. + /// Equivalent to close status 1008. Indicates that an endpoint is + /// terminating the connection because it has received a message that + /// violates its policy. /// PolicyViolation = 1008, /// - /// Equivalent to close status 1009. Indicates that an endpoint is terminating - /// the connection because it has received a message that is too big to process. + /// Equivalent to close status 1009. Indicates that an endpoint is + /// terminating the connection because it has received a message that + /// is too big to process. /// TooBig = 1009, /// - /// Equivalent to close status 1010. Indicates that a client is terminating - /// the connection because it has expected the server to negotiate one or more extension, - /// but the server didn't return them in the handshake response. + /// Equivalent to close status 1010. Indicates that a client is + /// terminating the connection because it has expected the server to + /// negotiate one or more extension, but the server did not return + /// them in the handshake response. /// MandatoryExtension = 1010, /// - /// Equivalent to close status 1011. Indicates that a server is terminating - /// the connection because it has encountered an unexpected condition that - /// prevented it from fulfilling the request. + /// Equivalent to close status 1011. Indicates that a server is + /// terminating the connection because it has encountered an unexpected + /// condition that prevented it from fulfilling the request. /// ServerError = 1011, /// - /// Equivalent to close status 1015. Indicates that the connection was closed - /// due to a failure to perform a TLS handshake. A Reserved value. + /// Equivalent to close status 1015. Indicates that the connection was + /// closed due to a failure to perform a TLS handshake. A Reserved value. /// TlsHandshakeFailure = 1015 } diff --git a/websocket-sharp/CompressionMethod.cs b/websocket-sharp/CompressionMethod.cs index 6e5aaaa54..42ab230a6 100644 --- a/websocket-sharp/CompressionMethod.cs +++ b/websocket-sharp/CompressionMethod.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2013-2015 sta.blockhead + * Copyright (c) 2013-2017 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 @@ -31,17 +31,17 @@ namespace WebSocketSharp { /// - /// Specifies the compression method used to compress a message on the WebSocket connection. + /// Specifies the method for compression. /// /// - /// The compression methods are defined in - /// + /// The methods are defined in + /// /// Compression Extensions for WebSocket. /// public enum CompressionMethod : byte { /// - /// Specifies non compression. + /// Specifies no compression. /// None, /// diff --git a/websocket-sharp/ErrorEventArgs.cs b/websocket-sharp/ErrorEventArgs.cs index a4c0b85e9..c02d0e000 100644 --- a/websocket-sharp/ErrorEventArgs.cs +++ b/websocket-sharp/ErrorEventArgs.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2022 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 @@ -42,15 +42,15 @@ namespace WebSocketSharp /// /// /// - /// A event occurs when the gets - /// an error. + /// The error event occurs when the interface + /// gets an error. /// /// /// If you would like to get the error message, you should access /// the property. /// /// - /// And if the error is due to an exception, you can get the exception by accessing + /// If the error is due to an exception, you can get it by accessing /// the property. /// /// @@ -84,8 +84,13 @@ internal ErrorEventArgs (string message, Exception exception) /// Gets the exception that caused the error. /// /// - /// An instance that represents the cause of the error, - /// or if the error isn't due to an exception. + /// + /// An instance that represents + /// the cause of the error. + /// + /// + /// if not present. + /// /// public Exception Exception { get { diff --git a/websocket-sharp/Ext.cs b/websocket-sharp/Ext.cs index 91983f894..2540f803e 100644 --- a/websocket-sharp/Ext.cs +++ b/websocket-sharp/Ext.cs @@ -3,9 +3,9 @@ * 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 GetStatusDescription method is derived from HttpListenerResponse.cs (System.Net) + * - The MaybeUri method is derived from Uri.cs (System) + * - The isPredefinedScheme method is derived from Uri.cs (System) * * The MIT License * @@ -14,7 +14,7 @@ * Copyright (c) 2003 Ben Maurer * Copyright (c) 2003, 2005, 2009 Novell, Inc. (http://www.novell.com) * Copyright (c) 2009 Stephane Delcroix - * Copyright (c) 2010-2015 sta.blockhead + * Copyright (c) 2010-2025 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 @@ -67,6 +67,7 @@ public static class Ext #region Private Fields private static readonly byte[] _last = new byte[] { 0x00 }; + private static readonly int _maxRetry = 5; private const string _tspecials = "()<>@,;:\\\"/[]?={} \t"; #endregion @@ -76,7 +77,6 @@ public static class Ext private static byte[] compress (this byte[] data) { if (data.LongLength == 0) - //return new byte[] { 0x00, 0x00, 0x00, 0xff, 0xff }; return data; using (var input = new MemoryStream (data)) @@ -85,18 +85,23 @@ private static byte[] compress (this byte[] data) private static MemoryStream compress (this Stream stream) { - var output = new MemoryStream (); + var ret = new MemoryStream (); + if (stream.Length == 0) - return output; + return ret; stream.Position = 0; - using (var ds = new DeflateStream (output, CompressionMode.Compress, true)) { + + var mode = CompressionMode.Compress; + + using (var ds = new DeflateStream (ret, mode, true)) { stream.CopyTo (ds, 1024); ds.Close (); // BFINAL set to 1. - output.Write (_last, 0, 1); - output.Position = 0; + ret.Write (_last, 0, 1); + + ret.Position = 0; - return output; + return ret; } } @@ -104,6 +109,7 @@ private static byte[] compressToArray (this Stream stream) { using (var output = stream.compress ()) { output.Close (); + return output.ToArray (); } } @@ -119,16 +125,21 @@ private static byte[] decompress (this byte[] data) private static MemoryStream decompress (this Stream stream) { - var output = new MemoryStream (); + var ret = new MemoryStream (); + if (stream.Length == 0) - return output; + return ret; stream.Position = 0; - using (var ds = new DeflateStream (stream, CompressionMode.Decompress, true)) { - ds.CopyTo (output, 1024); - output.Position = 0; - return output; + var mode = CompressionMode.Decompress; + + using (var ds = new DeflateStream (stream, mode, true)) { + ds.CopyTo (ret, 1024); + + ret.Position = 0; + + return ret; } } @@ -136,14 +147,39 @@ private static byte[] decompressToArray (this Stream stream) { using (var output = stream.decompress ()) { output.Close (); + return output.ToArray (); } } - private static void times (this ulong n, Action action) + private static bool isPredefinedScheme (this string value) { - for (ulong i = 0; i < n; i++) - action (); + var c = value[0]; + + if (c == 'h') + return value == "http" || value == "https"; + + if (c == 'w') + return value == "ws" || value == "wss"; + + if (c == 'f') + return value == "file" || value == "ftp"; + + if (c == 'g') + return value == "gopher"; + + if (c == 'm') + return value == "mailto"; + + if (c == 'n') { + c = value[1]; + + return c == 'e' + ? value == "news" || value == "net.pipe" || value == "net.tcp" + : value == "nntp"; + } + + return false; } #endregion @@ -152,107 +188,79 @@ private static void times (this ulong n, Action action) internal static byte[] Append (this ushort code, string reason) { - var ret = code.InternalToByteArray (ByteOrder.Big); - if (reason != null && reason.Length > 0) { - var buff = new List (ret); - buff.AddRange (Encoding.UTF8.GetBytes (reason)); - ret = buff.ToArray (); - } + var codeAsBytes = code.ToByteArray (ByteOrder.Big); - return ret; - } + if (reason == null || reason.Length == 0) + return codeAsBytes; - internal static string CheckIfAvailable ( - this ServerState state, bool ready, bool start, bool shutting) - { - return (!ready && (state == ServerState.Ready || state == ServerState.Stop)) || - (!start && state == ServerState.Start) || - (!shutting && state == ServerState.ShuttingDown) - ? "This operation isn't available in: " + state.ToString ().ToLower () - : null; - } + var buff = new List (codeAsBytes); + var reasonAsBytes = Encoding.UTF8.GetBytes (reason); - internal static string CheckIfAvailable ( - this WebSocketState state, bool connecting, bool open, bool closing, bool closed) - { - return (!connecting && state == WebSocketState.Connecting) || - (!open && state == WebSocketState.Open) || - (!closing && state == WebSocketState.Closing) || - (!closed && state == WebSocketState.Closed) - ? "This operation isn't available in: " + state.ToString ().ToLower () - : null; - } + buff.AddRange (reasonAsBytes); - internal static string CheckIfValidProtocols (this string[] protocols) - { - return protocols.Contains ( - protocol => protocol == null || protocol.Length == 0 || !protocol.IsToken ()) - ? "Contains an invalid value." - : protocols.ContainsTwice () - ? "Contains a value twice." - : null; + return buff.ToArray (); } - internal static string CheckIfValidServicePath (this string path) + internal static byte[] Compress ( + this byte[] data, + CompressionMethod method + ) { - return path == null || path.Length == 0 - ? "'path' is null or empty." - : path[0] != '/' - ? "'path' isn't an absolute path." - : path.IndexOfAny (new[] { '?', '#' }) > -1 - ? "'path' includes either or both query and fragment components." - : null; + return method == CompressionMethod.Deflate ? data.compress () : data; } - internal static string CheckIfValidSessionID (this string id) + internal static Stream Compress ( + this Stream stream, + CompressionMethod method + ) { - return id == null || id.Length == 0 ? "'id' is null or empty." : null; + return method == CompressionMethod.Deflate ? stream.compress () : stream; } - internal static string CheckIfValidWaitTime (this TimeSpan time) + internal static bool Contains (this string value, params char[] anyOf) { - return time <= TimeSpan.Zero ? "A wait time is zero or less." : null; + return anyOf != null && anyOf.Length > 0 + ? value.IndexOfAny (anyOf) > -1 + : false; } - internal static void Close (this HttpListenerResponse response, HttpStatusCode code) + internal static bool Contains ( + this NameValueCollection collection, + string name + ) { - response.StatusCode = (int) code; - response.OutputStream.Close (); + return collection[name] != null; } - internal static void CloseWithAuthChallenge ( - this HttpListenerResponse response, string challenge) + internal static bool Contains ( + this NameValueCollection collection, + string name, + string value, + StringComparison comparisonTypeForValue + ) { - response.Headers.InternalSet ("WWW-Authenticate", challenge, true); - response.Close (HttpStatusCode.Unauthorized); - } + var val = collection[name]; - internal static byte[] Compress (this byte[] data, CompressionMethod method) - { - return method == CompressionMethod.Deflate - ? data.compress () - : data; - } + if (val == null) + return false; - internal static Stream Compress (this Stream stream, CompressionMethod method) - { - return method == CompressionMethod.Deflate - ? stream.compress () - : stream; - } + foreach (var elm in val.Split (',')) { + if (elm.Trim ().Equals (value, comparisonTypeForValue)) + return true; + } - internal static byte[] CompressToArray (this Stream stream, CompressionMethod method) - { - return method == CompressionMethod.Deflate - ? stream.compressToArray () - : stream.ToByteArray (); + return false; } - internal static bool Contains (this IEnumerable source, Func condition) + internal static bool Contains ( + this IEnumerable source, + Func condition + ) { - foreach (T elm in source) + foreach (T elm in source) { if (condition (elm)) return true; + } return false; } @@ -260,70 +268,97 @@ internal static bool Contains (this IEnumerable source, Func cond internal static bool ContainsTwice (this string[] values) { var len = values.Length; + var end = len - 1; - Func contains = null; - contains = idx => { - if (idx < len - 1) { - for (var i = idx + 1; i < len; i++) - if (values[i] == values[idx]) - return true; + Func seek = null; + seek = idx => { + if (idx == end) + return false; - return contains (++idx); - } + var val = values[idx]; - return false; - }; + for (var i = idx + 1; i < len; i++) { + if (values[i] == val) + return true; + } + + return seek (++idx); + }; + + return seek (0); + } + + internal static T[] Copy (this T[] sourceArray, int length) + { + var dest = new T[length]; - return contains (0); + Array.Copy (sourceArray, 0, dest, 0, length); + + return dest; } - internal static T[] Copy (this T[] source, long length) + internal static T[] Copy (this T[] sourceArray, long length) { var dest = new T[length]; - Array.Copy (source, 0, dest, 0, length); + + Array.Copy (sourceArray, 0, dest, 0, length); return dest; } - internal static void CopyTo (this Stream source, Stream destination, int bufferLength) + internal static void CopyTo ( + this Stream sourceStream, + Stream destinationStream, + int bufferLength + ) { var buff = new byte[bufferLength]; - var nread = 0; - while ((nread = source.Read (buff, 0, bufferLength)) > 0) - destination.Write (buff, 0, nread); + + while (true) { + var nread = sourceStream.Read (buff, 0, bufferLength); + + if (nread <= 0) + break; + + destinationStream.Write (buff, 0, nread); + } } internal static void CopyToAsync ( - this Stream source, - Stream destination, + this Stream sourceStream, + Stream destinationStream, int bufferLength, Action completed, - Action error) + Action error + ) { var buff = new byte[bufferLength]; AsyncCallback callback = null; - callback = ar => { - try { - var nread = source.EndRead (ar); - if (nread <= 0) { - if (completed != null) - completed (); + callback = + ar => { + try { + var nread = sourceStream.EndRead (ar); - return; - } + if (nread <= 0) { + if (completed != null) + completed (); - destination.Write (buff, 0, nread); - source.BeginRead (buff, 0, bufferLength, callback, null); - } - catch (Exception ex) { - if (error != null) - error (ex); - } - }; + return; + } + + destinationStream.Write (buff, 0, nread); + + sourceStream.BeginRead (buff, 0, bufferLength, callback, null); + } + catch (Exception ex) { + if (error != null) + error (ex); + } + }; try { - source.BeginRead (buff, 0, bufferLength, callback, null); + sourceStream.BeginRead (buff, 0, bufferLength, callback, null); } catch (Exception ex) { if (error != null) @@ -331,181 +366,210 @@ internal static void CopyToAsync ( } } - internal static byte[] Decompress (this byte[] data, CompressionMethod method) + internal static byte[] Decompress ( + this byte[] data, + CompressionMethod method + ) { - return method == CompressionMethod.Deflate - ? data.decompress () - : data; + return method == CompressionMethod.Deflate ? data.decompress () : data; } - internal static Stream Decompress (this Stream stream, CompressionMethod method) + internal static Stream Decompress ( + this Stream stream, + CompressionMethod method + ) { return method == CompressionMethod.Deflate ? stream.decompress () : stream; } - internal static byte[] DecompressToArray (this Stream stream, CompressionMethod method) + internal static byte[] DecompressToArray ( + this Stream stream, + CompressionMethod method + ) { return method == CompressionMethod.Deflate ? stream.decompressToArray () : stream.ToByteArray (); } - /// - /// Determines whether the specified equals the specified , - /// and invokes the specified Action<int> delegate at the same time. - /// - /// - /// true if equals ; - /// otherwise, false. - /// - /// - /// An to compare. - /// - /// - /// A to compare. - /// - /// - /// An Action<int> delegate that references the method(s) called - /// at the same time as comparing. An parameter to pass to - /// the method(s) is . - /// - internal static bool EqualsWith (this int value, char c, Action action) + internal static void Emit ( + this EventHandler eventHandler, + object sender, + EventArgs e + ) { - action (value); - return value == c - 0; + if (eventHandler == null) + return; + + eventHandler (sender, e); + } + + internal static void Emit ( + this EventHandler eventHandler, + object sender, + TEventArgs e + ) + where TEventArgs : EventArgs + { + if (eventHandler == null) + return; + + eventHandler (sender, e); } - /// - /// Gets the absolute path from the specified . - /// - /// - /// A that represents the absolute path if it's successfully found; - /// otherwise, . - /// - /// - /// A that represents the URI to get the absolute path from. - /// internal static string GetAbsolutePath (this Uri uri) { if (uri.IsAbsoluteUri) return uri.AbsolutePath; var original = uri.OriginalString; + if (original[0] != '/') return null; var idx = original.IndexOfAny (new[] { '?', '#' }); + return idx > 0 ? original.Substring (0, idx) : original; } - internal static string GetMessage (this CloseStatusCode code) - { - return code == CloseStatusCode.ProtocolError - ? "A WebSocket protocol error has occurred." - : code == CloseStatusCode.UnsupportedData - ? "Unsupported data has been received." - : code == CloseStatusCode.Abnormal - ? "An exception has occurred." - : code == CloseStatusCode.InvalidData - ? "Invalid data has been received." - : code == CloseStatusCode.PolicyViolation - ? "A policy violation has occurred." - : code == CloseStatusCode.TooBig - ? "A too big message has been received." - : code == CloseStatusCode.MandatoryExtension - ? "WebSocket client didn't receive expected extension(s)." - : code == CloseStatusCode.ServerError - ? "WebSocket server got an internal error." - : code == CloseStatusCode.TlsHandshakeFailure - ? "An error has occurred during a TLS handshake." - : String.Empty; + internal static CookieCollection GetCookies ( + this NameValueCollection headers, + bool response + ) + { + var name = response ? "Set-Cookie" : "Cookie"; + var val = headers[name]; + + return val != null + ? CookieCollection.Parse (val, response) + : new CookieCollection (); + } + + internal static string GetDnsSafeHost (this Uri uri, bool bracketIPv6) + { + return bracketIPv6 && uri.HostNameType == UriHostNameType.IPv6 + ? uri.Host + : uri.DnsSafeHost; + } + + internal static string GetErrorMessage (this ushort code) + { + switch (code) { + case 1002: + return "A protocol error has occurred."; + case 1003: + return "Unsupported data has been received."; + case 1006: + return "An abnormal error has occurred."; + case 1007: + return "Invalid data has been received."; + case 1008: + return "A policy violation has occurred."; + case 1009: + return "A too big message has been received."; + case 1010: + return "The client did not receive expected extension(s)."; + case 1011: + return "The server got an internal error."; + case 1015: + return "An error has occurred during a TLS handshake."; + default: + return String.Empty; + } + } + + internal static string GetErrorMessage (this CloseStatusCode code) + { + return ((ushort) code).GetErrorMessage (); } - /// - /// Gets the name from the specified that contains a pair of name and - /// value separated by a separator character. - /// - /// - /// A that represents the name if any; otherwise, null. - /// - /// - /// A that contains a pair of name and value separated by - /// a separator character. - /// - /// - /// A that represents the separator character. - /// internal static string GetName (this string nameAndValue, char separator) { var idx = nameAndValue.IndexOf (separator); + return idx > 0 ? nameAndValue.Substring (0, idx).Trim () : null; } - /// - /// Gets the value from the specified that contains a pair of name and - /// value separated by a separator character. - /// - /// - /// A that represents the value if any; otherwise, null. - /// - /// - /// A that contains a pair of name and value separated by - /// a separator character. - /// - /// - /// A that represents the separator character. - /// + internal static string GetUTF8DecodedString (this byte[] bytes) + { + try { + return Encoding.UTF8.GetString (bytes); + } + catch { + return null; + } + } + + internal static byte[] GetUTF8EncodedBytes (this string s) + { + try { + return Encoding.UTF8.GetBytes (s); + } + catch { + return null; + } + } + internal static string GetValue (this string nameAndValue, char separator) { - var idx = nameAndValue.IndexOf (separator); - return idx > -1 && idx < nameAndValue.Length - 1 - ? nameAndValue.Substring (idx + 1).Trim () - : null; + return nameAndValue.GetValue (separator, false); } - internal static string GetValue (this string nameAndValue, char separator, bool unquote) + internal static string GetValue ( + this string nameAndValue, + char separator, + bool unquote + ) { var idx = nameAndValue.IndexOf (separator); + if (idx < 0 || idx == nameAndValue.Length - 1) return null; var val = nameAndValue.Substring (idx + 1).Trim (); + return unquote ? val.Unquote () : val; } - internal static TcpListenerWebSocketContext GetWebSocketContext ( - this TcpClient tcpClient, - string protocol, - bool secure, - ServerSslConfiguration sslConfig, - Logger logger) + internal static bool IsCompressionExtension ( + this string value, + CompressionMethod method + ) { - return new TcpListenerWebSocketContext (tcpClient, protocol, secure, sslConfig, logger); + var extStr = method.ToExtensionString (); + var compType = StringComparison.Ordinal; + + return value.StartsWith (extStr, compType); } - internal static byte[] InternalToByteArray (this ushort value, ByteOrder order) + internal static bool IsDefined (this CloseStatusCode code) { - var bytes = BitConverter.GetBytes (value); - if (!order.IsHostOrder ()) - Array.Reverse (bytes); - - return bytes; + return Enum.IsDefined (typeof (CloseStatusCode), code); } - internal static byte[] InternalToByteArray (this ulong value, ByteOrder order) + internal static bool IsEqualTo ( + this int value, + char c, + Action beforeComparing + ) { - var bytes = BitConverter.GetBytes (value); - if (!order.IsHostOrder ()) - Array.Reverse (bytes); + beforeComparing (value); - return bytes; + return value == c - 0; } - internal static bool IsCompressionExtension (this string value, CompressionMethod method) + internal static bool IsHttpMethod (this string value) { - return value.StartsWith (method.ToExtensionString ()); + return value == "GET" + || value == "HEAD" + || value == "POST" + || value == "PUT" + || value == "DELETE" + || value == "CONNECT" + || value == "OPTIONS" + || value == "TRACE"; } internal static bool IsPortNumber (this int value) @@ -513,38 +577,52 @@ internal static bool IsPortNumber (this int value) return value > 0 && value < 65536; } - internal static bool IsReserved (this ushort code) + internal static bool IsReserved (this CloseStatusCode code) { - return code == (ushort) CloseStatusCode.Undefined || - code == (ushort) CloseStatusCode.NoStatus || - code == (ushort) CloseStatusCode.Abnormal || - code == (ushort) CloseStatusCode.TlsHandshakeFailure; + return ((ushort) code).IsReservedStatusCode (); } - internal static bool IsReserved (this CloseStatusCode code) + internal static bool IsReservedStatusCode (this ushort code) { - return code == CloseStatusCode.Undefined || - code == CloseStatusCode.NoStatus || - code == CloseStatusCode.Abnormal || - code == CloseStatusCode.TlsHandshakeFailure; + return code == 1004 + || code == 1005 + || code == 1006 + || code == 1015; + } + + internal static bool IsSupportedOpcode (this int opcode) + { + return Enum.IsDefined (typeof (Opcode), opcode); } internal static bool IsText (this string value) { var len = value.Length; + for (var i = 0; i < len; i++) { var c = value[i]; - if (c < 0x20 && !"\r\n\t".Contains (c)) - return false; - - if (c == 0x7f) - return false; - if (c == '\n' && ++i < len) { - c = value[i]; - if (!" \t".Contains (c)) + if (c < 0x20) { + if ("\r\n\t".IndexOf (c) == -1) return false; + + if (c == '\n') { + i++; + + if (i == len) + break; + + c = value[i]; + + if (" \t".IndexOf (c) == -1) + return false; + } + + continue; } + + if (c == 0x7f) + return false; } return true; @@ -552,100 +630,180 @@ internal static bool IsText (this string value) internal static bool IsToken (this string value) { - foreach (var c in value) - if (c < 0x20 || c >= 0x7f || _tspecials.Contains (c)) + foreach (var c in value) { + if (c < 0x20) + return false; + + if (c > 0x7e) return false; + if (_tspecials.IndexOf (c) > -1) + return false; + } + return true; } + internal static bool KeepsAlive ( + this NameValueCollection headers, + Version version + ) + { + var compType = StringComparison.OrdinalIgnoreCase; + + return version > HttpVersion.Version10 + ? !headers.Contains ("Connection", "close", compType) + : headers.Contains ("Connection", "keep-alive", compType); + } + + internal static bool MaybeUri (this string value) + { + var idx = value.IndexOf (':'); + + if (idx < 2 || idx > 9) + return false; + + var schm = value.Substring (0, idx); + + return schm.isPredefinedScheme (); + } + internal static string Quote (this string value) { - return String.Format ("\"{0}\"", value.Replace ("\"", "\\\"")); + var fmt = "\"{0}\""; + var val = value.Replace ("\"", "\\\""); + + return String.Format (fmt, val); } internal static byte[] ReadBytes (this Stream stream, int length) { - var buff = new byte[length]; + var ret = new byte[length]; + var offset = 0; - try { - var nread = 0; - while (length > 0) { - nread = stream.Read (buff, offset, length); - if (nread == 0) - break; + var retry = 0; - offset += nread; - length -= nread; + while (length > 0) { + var nread = stream.Read (ret, offset, length); + + if (nread <= 0) { + if (retry < _maxRetry) { + retry++; + + continue; + } + + return ret.SubArray (0, offset); } - } - catch { + + retry = 0; + + offset += nread; + length -= nread; } - return buff.SubArray (0, offset); + return ret; } - internal static byte[] ReadBytes (this Stream stream, long length, int bufferLength) + internal static byte[] ReadBytes ( + this Stream stream, + long length, + int bufferLength + ) { using (var dest = new MemoryStream ()) { - try { - var buff = new byte[bufferLength]; - var nread = 0; - while (length > 0) { - if (length < bufferLength) - bufferLength = (int) length; - - nread = stream.Read (buff, 0, bufferLength); - if (nread == 0) - break; + var buff = new byte[bufferLength]; + var retry = 0; - dest.Write (buff, 0, nread); - length -= nread; + while (length > 0) { + if (length < bufferLength) + bufferLength = (int) length; + + var nread = stream.Read (buff, 0, bufferLength); + + if (nread <= 0) { + if (retry < _maxRetry) { + retry++; + + continue; + } + + break; } - } - catch { + + retry = 0; + + dest.Write (buff, 0, nread); + + length -= nread; } dest.Close (); + return dest.ToArray (); } } internal static void ReadBytesAsync ( - this Stream stream, int length, Action completed, Action error) + this Stream stream, + int length, + Action completed, + Action error + ) { - var buff = new byte[length]; + var ret = new byte[length]; + var offset = 0; + var retry = 0; AsyncCallback callback = null; - callback = ar => { - try { - var nread = stream.EndRead (ar); - if (nread == 0 || nread == length) { - if (completed != null) - completed (buff.SubArray (0, offset + nread)); - - return; - } + callback = + ar => { + try { + var nread = stream.EndRead (ar); - offset += nread; - length -= nread; + if (nread <= 0) { + if (retry < _maxRetry) { + retry++; - stream.BeginRead (buff, offset, length, callback, null); - } - catch (Exception ex) { - if (error != null) - error (ex); - } - }; + stream.BeginRead (ret, offset, length, callback, null); - try { - stream.BeginRead (buff, offset, length, callback, null); - } - catch (Exception ex) { - if (error != null) - error (ex); - } + return; + } + + if (completed != null) + completed (ret.SubArray (0, offset)); + + return; + } + + if (nread == length) { + if (completed != null) + completed (ret); + + return; + } + + retry = 0; + + offset += nread; + length -= nread; + + stream.BeginRead (ret, offset, length, callback, null); + } + catch (Exception ex) { + if (error != null) + error (ex); + } + }; + + try { + stream.BeginRead (ret, offset, length, callback, null); + } + catch (Exception ex) { + if (error != null) + error (ex); + } } internal static void ReadBytesAsync ( @@ -653,306 +811,519 @@ internal static void ReadBytesAsync ( long length, int bufferLength, Action completed, - Action error) + 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) + read = + len => { + if (len < bufferLength) + bufferLength = (int) len; + + stream.BeginRead ( + buff, + 0, + bufferLength, + ar => { + try { + var nread = stream.EndRead (ar); + + if (nread <= 0) { + if (retry < _maxRetry) { + retry++; + + read (len); + + return; + } + + if (completed != null) { + dest.Close (); + + var ret = dest.ToArray (); + + completed (ret); + } + + dest.Dispose (); + + return; + } + dest.Write (buff, 0, nread); - if (nread == 0 || nread == len) { - if (completed != null) { - dest.Close (); - completed (dest.ToArray ()); + if (nread == len) { + if (completed != null) { + dest.Close (); + + var ret = dest.ToArray (); + + completed (ret); + } + + dest.Dispose (); + + return; } - dest.Dispose (); - return; + retry = 0; + + read (len - nread); } + catch (Exception ex) { + dest.Dispose (); - read (len - nread); - } - catch (Exception ex) { - dest.Dispose (); - if (error != null) - error (ex); - } - }, - null); - }; + if (error != null) + error (ex); + } + }, + null + ); + }; try { read (length); } catch (Exception ex) { dest.Dispose (); + if (error != null) error (ex); } } - internal static string RemovePrefix (this string value, params string[] prefixes) - { - var idx = 0; - foreach (var prefix in prefixes) { - if (value.StartsWith (prefix)) { - idx = prefix.Length; - break; - } - } - - return idx > 0 ? value.Substring (idx) : value; - } - internal static T[] Reverse (this T[] array) { - var len = array.Length; - var reverse = new T[len]; + var len = array.LongLength; + var ret = new T[len]; var end = len - 1; - for (var i = 0; i <= end; i++) - reverse[i] = array[end - i]; - return reverse; + for (long i = 0; i <= end; i++) + ret[i] = array[end - i]; + + return ret; } internal static IEnumerable SplitHeaderValue ( - this string value, params char[] separators) + this string value, + params char[] separators + ) { var len = value.Length; - var seps = new string (separators); + var end = len - 1; var buff = new StringBuilder (32); var escaped = false; var quoted = false; - for (var i = 0; i < len; i++) { + for (var i = 0; i <= end; i++) { var c = value[i]; + + buff.Append (c); + if (c == '"') { - if (escaped) - escaped = !escaped; - else - quoted = !quoted; - } - else if (c == '\\') { - if (i < len - 1 && value[i + 1] == '"') - escaped = true; - } - else if (seps.Contains (c)) { - if (!quoted) { - yield return buff.ToString (); - buff.Length = 0; + if (escaped) { + escaped = false; continue; } + + quoted = !quoted; + + continue; } - else { + + if (c == '\\') { + if (i == end) + break; + + if (value[i + 1] == '"') + escaped = true; + + continue; } - buff.Append (c); + if (Array.IndexOf (separators, c) > -1) { + if (quoted) + continue; + + buff.Length -= 1; + + yield return buff.ToString (); + + buff.Length = 0; + + continue; + } } - if (buff.Length > 0) - yield return buff.ToString (); + yield return buff.ToString (); } internal static byte[] ToByteArray (this Stream stream) { - using (var output = new MemoryStream ()) { - stream.Position = 0; - stream.CopyTo (output, 1024); - output.Close (); + stream.Position = 0; - return output.ToArray (); + using (var buff = new MemoryStream ()) { + stream.CopyTo (buff, 1024); + buff.Close (); + + return buff.ToArray (); } } + internal static byte[] ToByteArray (this ushort value, ByteOrder order) + { + var ret = BitConverter.GetBytes (value); + + if (!order.IsHostOrder ()) + Array.Reverse (ret); + + return ret; + } + + internal static byte[] ToByteArray (this ulong value, ByteOrder order) + { + var ret = BitConverter.GetBytes (value); + + if (!order.IsHostOrder ()) + Array.Reverse (ret); + + return ret; + } + internal static CompressionMethod ToCompressionMethod (this string value) { - foreach (CompressionMethod method in Enum.GetValues (typeof (CompressionMethod))) + var methods = Enum.GetValues (typeof (CompressionMethod)); + + foreach (CompressionMethod method in methods) { if (method.ToExtensionString () == value) return method; + } return CompressionMethod.None; } internal static string ToExtensionString ( - this CompressionMethod method, params string[] parameters) + this CompressionMethod method, + params string[] parameters + ) { if (method == CompressionMethod.None) return String.Empty; - var m = String.Format ("permessage-{0}", method.ToString ().ToLower ()); + var name = method.ToString ().ToLower (); + var ename = String.Format ("permessage-{0}", name); + if (parameters == null || parameters.Length == 0) - return m; + return ename; + + var eparams = parameters.ToString ("; "); + + return String.Format ("{0}; {1}", ename, eparams); + } - return String.Format ("{0}; {1}", m, parameters.ToString ("; ")); + internal static int ToInt32 (this string numericString) + { + return Int32.Parse (numericString); } - internal static System.Net.IPAddress ToIPAddress (this string hostnameOrAddress) + internal static System.Net.IPAddress ToIPAddress (this string value) { + if (value == null || value.Length == 0) + return null; + + System.Net.IPAddress addr; + + if (System.Net.IPAddress.TryParse (value, out addr)) + return addr; + try { - return System.Net.Dns.GetHostAddresses (hostnameOrAddress)[0]; + var addrs = System.Net.Dns.GetHostAddresses (value); + + return addrs[0]; } catch { return null; } } - internal static List ToList (this IEnumerable source) + internal static List ToList ( + this IEnumerable source + ) { return new List (source); } + internal static string ToString ( + this System.Net.IPAddress address, + bool bracketIPv6 + ) + { + return bracketIPv6 + && address.AddressFamily == AddressFamily.InterNetworkV6 + ? String.Format ("[{0}]", address) + : address.ToString (); + } + internal static ushort ToUInt16 (this byte[] source, ByteOrder sourceOrder) { - return BitConverter.ToUInt16 (source.ToHostOrder (sourceOrder), 0); + var val = source.ToHostOrder (sourceOrder); + + return BitConverter.ToUInt16 (val, 0); } internal static ulong ToUInt64 (this byte[] source, ByteOrder sourceOrder) { - return BitConverter.ToUInt64 (source.ToHostOrder (sourceOrder), 0); + var val = source.ToHostOrder (sourceOrder); + + return BitConverter.ToUInt64 (val, 0); } - internal static string TrimEndSlash (this string value) + internal static Version ToVersion (this string versionString) { - value = value.TrimEnd ('/'); - return value.Length > 0 ? value : "/"; + return new Version (versionString); + } + + internal static IEnumerable TrimEach ( + this IEnumerable source + ) + { + foreach (var elm in source) + yield return elm.Trim (); + } + + internal static string TrimSlashFromEnd (this string value) + { + var ret = value.TrimEnd ('/'); + + return ret.Length > 0 ? ret : "/"; + } + + internal static string TrimSlashOrBackslashFromEnd (this string value) + { + var ret = value.TrimEnd ('/', '\\'); + + return ret.Length > 0 ? ret : value[0].ToString (); + } + + internal static bool TryCreateVersion ( + this string versionString, + out Version result + ) + { + result = null; + + try { + result = new Version (versionString); + } + catch { + return false; + } + + return true; } - /// - /// Tries to create a for WebSocket with - /// the specified . - /// - /// - /// true if a is successfully created; otherwise, false. - /// - /// - /// A that represents a WebSocket URL to try. - /// - /// - /// When this method returns, a that represents a WebSocket URL, - /// or if is invalid. - /// - /// - /// When this method returns, a that represents an error message, - /// or if is valid. - /// internal static bool TryCreateWebSocketUri ( - this string uriString, out Uri result, out string message) + this string uriString, + out Uri result, + out string message + ) { result = null; + message = null; var uri = uriString.ToUri (); + if (uri == null) { - message = "An invalid URI string: " + uriString; + message = "An invalid URI string."; + return false; } if (!uri.IsAbsoluteUri) { - message = "Not an absolute URI: " + uriString; + message = "A relative URI."; + return false; } var schm = uri.Scheme; - if (!(schm == "ws" || schm == "wss")) { - message = "The scheme part isn't 'ws' or 'wss': " + uriString; + var valid = schm == "ws" || schm == "wss"; + + if (!valid) { + message = "The scheme part is not \"ws\" or \"wss\"."; + + return false; + } + + var port = uri.Port; + + if (port == 0) { + message = "The port part is zero."; + return false; } if (uri.Fragment.Length > 0) { - message = "Includes the fragment component: " + uriString; + message = "It includes the fragment component."; + return false; } - var port = uri.Port; - if (port == 0) { - message = "The port part is zero: " + uriString; + if (port == -1) { + port = schm == "ws" ? 80 : 443; + uriString = String.Format ( + "{0}://{1}:{2}{3}", + schm, + uri.Host, + port, + uri.PathAndQuery + ); + + result = new Uri (uriString); + } + else { + result = uri; + } + + return true; + } + + internal static bool TryGetUTF8DecodedString ( + this byte[] bytes, + out string s + ) + { + s = null; + + try { + s = Encoding.UTF8.GetString (bytes); + } + catch { return false; } - result = port != -1 - ? uri - : new Uri ( - String.Format ( - "{0}://{1}:{2}{3}", - schm, - uri.Host, - schm == "ws" ? 80 : 443, - uri.PathAndQuery)); - - message = String.Empty; return true; } - internal static string Unquote (this string value) + internal static bool TryGetUTF8EncodedBytes ( + this string s, + out byte[] bytes + ) { - var start = value.IndexOf ('"'); - if (start < 0) - return value; + bytes = null; - var end = value.LastIndexOf ('"'); - var len = end - start - 1; + try { + bytes = Encoding.UTF8.GetBytes (s); + } + catch { + return false; + } - return len < 0 - ? value - : len == 0 - ? String.Empty - : value.Substring (start + 1, len).Replace ("\\\"", "\""); + return true; } - internal static string UTF8Decode (this byte[] bytes) + internal static bool TryOpenRead ( + this FileInfo fileInfo, + out FileStream fileStream + ) { + fileStream = null; + try { - return Encoding.UTF8.GetString (bytes); + fileStream = fileInfo.OpenRead (); } catch { - return null; + return false; } + + return true; + } + + internal static string Unquote (this string value) + { + var first = value.IndexOf ('"'); + + if (first == -1) + return value; + + var last = value.LastIndexOf ('"'); + + if (last == first) + return value; + + var len = last - first - 1; + + return len > 0 + ? value.Substring (first + 1, len).Replace ("\\\"", "\"") + : String.Empty; + } + + internal static bool Upgrades ( + this NameValueCollection headers, + string protocol + ) + { + var compType = StringComparison.OrdinalIgnoreCase; + + return headers.Contains ("Upgrade", protocol, compType) + && headers.Contains ("Connection", "Upgrade", compType); + } + + internal static string UrlDecode (this string value, Encoding encoding) + { + return value.IndexOfAny (new[] { '%', '+' }) > -1 + ? HttpUtility.UrlDecode (value, encoding) + : value; } - internal static byte[] UTF8Encode (this string s) + internal static string UrlEncode (this string value, Encoding encoding) { - return Encoding.UTF8.GetBytes (s); + return HttpUtility.UrlEncode (value, encoding); } - internal static void WriteBytes (this Stream stream, byte[] bytes, int bufferLength) + internal static void WriteBytes ( + this Stream stream, + byte[] bytes, + int bufferLength + ) { - using (var input = new MemoryStream (bytes)) - input.CopyTo (stream, bufferLength); + using (var src = new MemoryStream (bytes)) + src.CopyTo (stream, bufferLength); } internal static void WriteBytesAsync ( - this Stream stream, byte[] bytes, int bufferLength, Action completed, Action error) + this Stream stream, + byte[] bytes, + int bufferLength, + Action completed, + Action error + ) { - var input = new MemoryStream (bytes); - input.CopyToAsync ( + var src = new MemoryStream (bytes); + + src.CopyToAsync ( stream, bufferLength, () => { if (completed != null) completed (); - input.Dispose (); + src.Dispose (); }, ex => { - input.Dispose (); + src.Dispose (); + if (error != null) error (ex); - }); + } + ); } #endregion @@ -960,167 +1331,41 @@ internal static void WriteBytesAsync ( #region Public Methods /// - /// Determines whether the specified contains any of characters in - /// the specified array of . + /// Gets the description of the specified HTTP status code. /// /// - /// true if contains any of ; - /// otherwise, false. - /// - /// - /// A to test. - /// - /// - /// An array of that contains characters to find. - /// - public static bool Contains (this string value, params char[] chars) - { - return chars == null || chars.Length == 0 - ? true - : value == null || value.Length == 0 - ? false - : value.IndexOfAny (chars) > -1; - } - - /// - /// Determines whether the specified contains - /// the entry with the specified . - /// - /// - /// true if contains the entry with - /// ; otherwise, false. + /// A that represents the description of + /// the HTTP status code. /// - /// - /// A to test. - /// - /// - /// A that represents the key of the entry to find. + /// + /// + /// One of the enum values. + /// + /// + /// It specifies the HTTP status code. + /// /// - public static bool Contains (this NameValueCollection collection, string name) + public static string GetDescription (this HttpStatusCode code) { - return collection != null && collection.Count > 0 ? collection[name] != null : false; + return ((int) code).GetStatusDescription (); } /// - /// Determines whether the specified contains the entry with - /// the specified both and . + /// Gets the description of the specified HTTP status code. /// /// - /// true if contains the entry with both - /// and ; otherwise, false. + /// + /// A that represents the description of + /// the HTTP status code. + /// + /// + /// An empty string if the description is not present. + /// /// - /// - /// A to test. - /// - /// - /// A that represents the key of the entry to find. - /// - /// - /// A that represents the value of the entry to find. - /// - public static bool Contains (this NameValueCollection collection, string name, string value) - { - if (collection == null || collection.Count == 0) - return false; - - var vals = collection[name]; - if (vals == null) - return false; - - foreach (var val in vals.Split (',')) - if (val.Trim ().Equals (value, StringComparison.OrdinalIgnoreCase)) - return true; - - return false; - } - - /// - /// Emits the specified delegate if it isn't . - /// - /// - /// A to emit. - /// - /// - /// An from which emits this . - /// - /// - /// A that contains no event data. + /// + /// An that specifies the HTTP status code. /// - public static void Emit (this EventHandler eventHandler, object sender, EventArgs e) - { - if (eventHandler != null) - eventHandler (sender, e); - } - - /// - /// Emits the specified EventHandler<TEventArgs> delegate if it isn't - /// . - /// - /// - /// An EventHandler<TEventArgs> to emit. - /// - /// - /// An from which emits this . - /// - /// - /// A TEventArgs that represents the event data. - /// - /// - /// The type of the event data generated by the event. - /// - public static void Emit ( - this EventHandler eventHandler, object sender, TEventArgs e) - where TEventArgs : EventArgs - { - if (eventHandler != null) - eventHandler (sender, e); - } - - /// - /// Gets the collection of the HTTP cookies from the specified HTTP . - /// - /// - /// A that receives a collection of the HTTP cookies. - /// - /// - /// A that contains a collection of the HTTP headers. - /// - /// - /// true if is a collection of the response headers; - /// otherwise, false. - /// - public static CookieCollection GetCookies (this NameValueCollection headers, bool response) - { - var name = response ? "Set-Cookie" : "Cookie"; - return headers != null && headers.Contains (name) - ? CookieCollection.Parse (headers[name], response) - : new CookieCollection (); - } - - /// - /// Gets the description of the specified HTTP status . - /// - /// - /// A that represents the description of the HTTP status code. - /// - /// - /// One of enum values, indicates the HTTP status code. - /// - public static string GetDescription (this HttpStatusCode code) - { - return ((int) code).GetStatusDescription (); - } - - /// - /// Gets the description of the specified HTTP status . - /// - /// - /// A that represents the description of the HTTP status code. - /// - /// - /// An that represents the HTTP status code. - /// - public static string GetStatusDescription (this int code) + public static string GetStatusDescription (this int code) { switch (code) { case 100: return "Continue"; @@ -1175,27 +1420,36 @@ public static string GetStatusDescription (this int code) } /// - /// Determines whether the specified is in the allowable range of - /// the WebSocket close status code. + /// Determines whether the specified ushort is in the range of + /// the status code for the WebSocket connection close. /// /// - /// Not allowable ranges are the following: + /// + /// The ranges are the following: + /// /// /// /// - /// Numbers in the range 0-999 are not used. + /// 1000-2999: These numbers are reserved for definition by + /// the WebSocket protocol. /// /// /// /// - /// Numbers greater than 4999 are out of the reserved close status code ranges. + /// 3000-3999: These numbers are reserved for use by libraries, + /// frameworks, and applications. + /// + /// + /// + /// + /// 4000-4999: These numbers are reserved for private use. /// /// /// /// /// - /// true if is in the allowable range of the WebSocket - /// close status code; otherwise, false. + /// true if is in the range of + /// the status code for the close; otherwise, false. /// /// /// A to test. @@ -1206,36 +1460,39 @@ public static bool IsCloseStatusCode (this ushort value) } /// - /// Determines whether the specified is enclosed in the specified - /// . + /// Determines whether the specified string is enclosed in + /// the specified character. /// /// - /// true if is enclosed in ; - /// otherwise, false. + /// true if is enclosed in + /// ; otherwise, false. /// /// /// A to test. /// /// - /// A that represents the character to find. + /// A to find. /// public static bool IsEnclosedIn (this string value, char c) { - return value != null && - value.Length > 1 && - value[0] == c && - value[value.Length - 1] == c; + if (value == null) + return false; + + var len = value.Length; + + return len > 1 ? value[0] == c && value[len - 1] == c : false; } /// - /// Determines whether the specified is host (this computer + /// Determines whether the specified byte order is host (this computer /// architecture) byte order. /// /// - /// true if is host byte order; otherwise, false. + /// true if is host byte order; otherwise, + /// false. /// /// - /// One of the enum values, to test. + /// One of the enum values to test. /// public static bool IsHostOrder (this ByteOrder order) { @@ -1245,39 +1502,58 @@ public static bool IsHostOrder (this ByteOrder order) } /// - /// Determines whether the specified represents - /// the local IP address. + /// Determines whether the specified IP address is a local IP address. /// + /// + /// This local means NOT REMOTE for the current host. + /// /// - /// true if represents the local IP address; + /// true if is a local IP address; /// otherwise, false. /// /// /// A to test. /// + /// + /// is . + /// public static bool IsLocal (this System.Net.IPAddress address) { if (address == null) - return false; + throw new ArgumentNullException ("address"); - if (address.Equals (System.Net.IPAddress.Any) || System.Net.IPAddress.IsLoopback (address)) + if (address.Equals (System.Net.IPAddress.Any)) return true; - var host = System.Net.Dns.GetHostName (); - var addrs = System.Net.Dns.GetHostAddresses (host); - foreach (var addr in addrs) + if (address.Equals (System.Net.IPAddress.Loopback)) + return true; + + if (Socket.OSSupportsIPv6) { + if (address.Equals (System.Net.IPAddress.IPv6Any)) + return true; + + if (address.Equals (System.Net.IPAddress.IPv6Loopback)) + return true; + } + + var name = System.Net.Dns.GetHostName (); + var addrs = System.Net.Dns.GetHostAddresses (name); + + foreach (var addr in addrs) { if (address.Equals (addr)) return true; + } return false; } /// - /// Determines whether the specified is or empty. + /// Determines whether the specified string is or + /// an empty string. /// /// - /// true if is or empty; - /// otherwise, false. + /// true if is or + /// an empty string; otherwise, false. /// /// /// A to test. @@ -1288,435 +1564,248 @@ public static bool IsNullOrEmpty (this string value) } /// - /// Determines whether the specified is a predefined scheme. + /// Retrieves a sub-array from the specified array. A sub-array starts at + /// the specified index in the array. /// /// - /// true if is a predefined scheme; otherwise, false. + /// An array of T that receives a sub-array. /// - /// - /// A to test. + /// + /// An array of T from which to retrieve a sub-array. /// - public static bool IsPredefinedScheme (this string value) - { - if (value == null || value.Length < 2) - return false; - - var c = value[0]; - if (c == 'h') - return value == "http" || value == "https"; - - if (c == 'w') - return value == "ws" || value == "wss"; - - if (c == 'f') - return value == "file" || value == "ftp"; - - if (c == 'n') { - c = value[1]; - return c == 'e' - ? value == "news" || value == "net.pipe" || value == "net.tcp" - : value == "nntp"; - } - - return (c == 'g' && value == "gopher") || (c == 'm' && value == "mailto"); - } - - /// - /// Determines whether the specified is - /// an HTTP Upgrade request to switch to the specified . - /// - /// - /// true if is an HTTP Upgrade request to switch to - /// ; otherwise, false. - /// - /// - /// A that represents the HTTP request. + /// + /// An that specifies the zero-based index in the array + /// at which retrieving starts. /// - /// - /// A that represents the protocol name. + /// + /// An that specifies the number of elements to retrieve. /// + /// + /// The type of elements in the array. + /// /// + /// is . + /// + /// /// - /// is . + /// is less than zero. /// /// /// -or- /// /// - /// is . + /// is greater than the end of the array. + /// + /// + /// -or- + /// + /// + /// is less than zero. + /// + /// + /// -or- + /// + /// + /// is greater than the number of elements from + /// to the end of the array. /// /// - /// - /// is empty. - /// - public static bool IsUpgradeTo (this HttpListenerRequest request, string protocol) + public static T[] SubArray (this T[] array, int startIndex, int length) { - if (request == null) - throw new ArgumentNullException ("request"); - - if (protocol == null) - throw new ArgumentNullException ("protocol"); - - if (protocol.Length == 0) - throw new ArgumentException ("An empty string.", "protocol"); + if (array == null) + throw new ArgumentNullException ("array"); - return request.Headers.Contains ("Upgrade", protocol) && - request.Headers.Contains ("Connection", "Upgrade"); - } + var len = array.Length; - /// - /// Determines whether the specified is a URI string. - /// - /// - /// true if may be a URI string; otherwise, false. - /// - /// - /// A to test. - /// - public static bool MaybeUri (this string value) - { - if (value == null || value.Length == 0) - return false; + if (len == 0) { + if (startIndex != 0) + throw new ArgumentOutOfRangeException ("startIndex"); - var idx = value.IndexOf (':'); - if (idx == -1) - return false; + if (length != 0) + throw new ArgumentOutOfRangeException ("length"); - if (idx >= 10) - return false; + return array; + } - return value.Substring (0, idx).IsPredefinedScheme (); - } + if (startIndex < 0 || startIndex >= len) + throw new ArgumentOutOfRangeException ("startIndex"); - /// - /// Retrieves a sub-array from the specified . A sub-array starts at - /// the specified element position in . - /// - /// - /// An array of T that receives a sub-array, or an empty array of T if any problems with - /// the parameters. - /// - /// - /// An array of T from which to retrieve a sub-array. - /// - /// - /// An that represents the zero-based starting position of - /// a sub-array in . - /// - /// - /// An that represents the number of elements to retrieve. - /// - /// - /// The type of elements in . - /// - public static T[] SubArray (this T[] array, int startIndex, int length) - { - int len; - if (array == null || (len = array.Length) == 0) - return new T[0]; + if (length < 0 || length > len - startIndex) + throw new ArgumentOutOfRangeException ("length"); - if (startIndex < 0 || length <= 0 || startIndex + length > len) + if (length == 0) return new T[0]; - if (startIndex == 0 && length == len) + if (length == len) return array; - var subArray = new T[length]; - Array.Copy (array, startIndex, subArray, 0, length); + var ret = new T[length]; + + Array.Copy (array, startIndex, ret, 0, length); - return subArray; + return ret; } /// - /// Retrieves a sub-array from the specified . A sub-array starts at - /// the specified element position in . + /// Retrieves a sub-array from the specified array. A sub-array starts at + /// the specified index in the array. /// /// - /// An array of T that receives a sub-array, or an empty array of T if any problems with - /// the parameters. + /// An array of T that receives a sub-array. /// /// /// An array of T from which to retrieve a sub-array. /// /// - /// A that represents the zero-based starting position of - /// a sub-array in . + /// A that specifies the zero-based index in the array + /// at which retrieving starts. /// /// - /// A that represents the number of elements to retrieve. + /// A that specifies the number of elements to retrieve. /// /// - /// The type of elements in . + /// The type of elements in the array. /// + /// + /// is . + /// + /// + /// + /// is less than zero. + /// + /// + /// -or- + /// + /// + /// is greater than the end of the array. + /// + /// + /// -or- + /// + /// + /// is less than zero. + /// + /// + /// -or- + /// + /// + /// is greater than the number of elements from + /// to the end of the array. + /// + /// public static T[] SubArray (this T[] array, long startIndex, long length) { - long len; - if (array == null || (len = array.LongLength) == 0) - return new T[0]; + if (array == null) + throw new ArgumentNullException ("array"); - if (startIndex < 0 || length <= 0 || startIndex + length > len) - return new T[0]; + var len = array.LongLength; + + if (len == 0) { + if (startIndex != 0) + throw new ArgumentOutOfRangeException ("startIndex"); + + if (length != 0) + throw new ArgumentOutOfRangeException ("length"); - if (startIndex == 0 && length == len) return array; + } - var subArray = new T[length]; - Array.Copy (array, startIndex, subArray, 0, length); + if (startIndex < 0 || startIndex >= len) + throw new ArgumentOutOfRangeException ("startIndex"); - return subArray; - } + if (length < 0 || length > len - startIndex) + throw new ArgumentOutOfRangeException ("length"); - /// - /// Executes the specified delegate times. - /// - /// - /// An is the number of times to execute. - /// - /// - /// An delegate that references the method(s) to execute. - /// - public static void Times (this int n, Action action) - { - if (n > 0 && action != null) - ((ulong) n).times (action); - } + if (length == 0) + return new T[0]; - /// - /// Executes the specified delegate times. - /// - /// - /// A is the number of times to execute. - /// - /// - /// An delegate that references the method(s) to execute. - /// - public static void Times (this long n, Action action) - { - if (n > 0 && action != null) - ((ulong) n).times (action); - } + if (length == len) + return array; - /// - /// Executes the specified delegate times. - /// - /// - /// A is the number of times to execute. - /// - /// - /// An delegate that references the method(s) to execute. - /// - public static void Times (this uint n, Action action) - { - if (n > 0 && action != null) - ((ulong) n).times (action); - } + var ret = new T[length]; - /// - /// Executes the specified delegate times. - /// - /// - /// A is the number of times to execute. - /// - /// - /// An delegate that references the method(s) to execute. - /// - public static void Times (this ulong n, Action action) - { - if (n > 0 && action != null) - n.times (action); + Array.Copy (array, startIndex, ret, 0, length); + + return ret; } /// - /// Executes the specified Action<int> delegate times. + /// Executes the specified delegate times. /// /// - /// An is the number of times to execute. + /// An that specifies the number of times to execute. /// /// - /// An Action<int> delegate that references the method(s) to execute. - /// An parameter to pass to the method(s) is the zero-based count of - /// iteration. + /// + /// An Action<int> delegate to execute. + /// + /// + /// The parameter is the zero-based count of iteration. + /// /// public static void Times (this int n, Action action) { - if (n > 0 && action != null) - for (int i = 0; i < n; i++) - action (i); - } + if (n <= 0) + return; - /// - /// Executes the specified Action<long> delegate times. - /// - /// - /// A is the number of times to execute. - /// - /// - /// An Action<long> delegate that references the method(s) to execute. - /// A parameter to pass to the method(s) is the zero-based count of - /// iteration. - /// - public static void Times (this long n, Action action) - { - if (n > 0 && action != null) - for (long i = 0; i < n; i++) - action (i); - } + if (action == null) + return; - /// - /// Executes the specified Action<uint> delegate times. - /// - /// - /// A is the number of times to execute. - /// - /// - /// An Action<uint> delegate that references the method(s) to execute. - /// A parameter to pass to the method(s) is the zero-based count of - /// iteration. - /// - public static void Times (this uint n, Action action) - { - if (n > 0 && action != null) - for (uint i = 0; i < n; i++) - action (i); + for (int i = 0; i < n; i++) + action (i); } /// - /// Executes the specified Action<ulong> delegate times. + /// Executes the specified delegate times. /// /// - /// A is the number of times to execute. + /// A that specifies the number of times to execute. /// /// - /// An Action<ulong> delegate that references the method(s) to execute. - /// A parameter to pass to this method(s) is the zero-based count of - /// iteration. - /// - public static void Times (this ulong n, Action action) - { - if (n > 0 && action != null) - for (ulong i = 0; i < n; i++) - action (i); - } - - /// - /// Converts the specified array of to the specified type data. - /// - /// - /// A T converted from , or a default value of - /// T if is an empty array of or - /// if the type of T isn't , , , - /// , , , , - /// , , or . - /// - /// - /// An array of to convert. - /// - /// - /// One of the enum values, specifies the byte order of - /// . + /// + /// An Action<long> delegate to execute. + /// + /// + /// The parameter is the zero-based count of iteration. + /// /// - /// - /// The type of the return. The T must be a value type. - /// - /// - /// is . - /// - public static T To (this byte[] source, ByteOrder sourceOrder) - where T : struct + public static void Times (this long n, Action action) { - if (source == null) - throw new ArgumentNullException ("source"); + if (n <= 0) + return; - if (source.Length == 0) - return default (T); - - var type = typeof (T); - var buff = source.ToHostOrder (sourceOrder); - - return type == typeof (Boolean) - ? (T)(object) BitConverter.ToBoolean (buff, 0) - : type == typeof (Char) - ? (T)(object) BitConverter.ToChar (buff, 0) - : type == typeof (Double) - ? (T)(object) BitConverter.ToDouble (buff, 0) - : type == typeof (Int16) - ? (T)(object) BitConverter.ToInt16 (buff, 0) - : type == typeof (Int32) - ? (T)(object) BitConverter.ToInt32 (buff, 0) - : type == typeof (Int64) - ? (T)(object) BitConverter.ToInt64 (buff, 0) - : type == typeof (Single) - ? (T)(object) BitConverter.ToSingle (buff, 0) - : type == typeof (UInt16) - ? (T)(object) BitConverter.ToUInt16 (buff, 0) - : type == typeof (UInt32) - ? (T)(object) BitConverter.ToUInt32 (buff, 0) - : type == typeof (UInt64) - ? (T)(object) BitConverter.ToUInt64 (buff, 0) - : default (T); - } + if (action == null) + return; - /// - /// Converts the specified to an array of . - /// - /// - /// An array of converted from . - /// - /// - /// A T to convert. - /// - /// - /// One of the enum values, specifies the byte order of the return. - /// - /// - /// The type of . The T must be a value type. - /// - public static byte[] ToByteArray (this T value, ByteOrder order) - where T : struct - { - var type = typeof (T); - var bytes = type == typeof (Boolean) - ? BitConverter.GetBytes ((Boolean)(object) value) - : type == typeof (Byte) - ? new byte[] { (Byte)(object) value } - : type == typeof (Char) - ? BitConverter.GetBytes ((Char)(object) value) - : type == typeof (Double) - ? BitConverter.GetBytes ((Double)(object) value) - : type == typeof (Int16) - ? BitConverter.GetBytes ((Int16)(object) value) - : type == typeof (Int32) - ? BitConverter.GetBytes ((Int32)(object) value) - : type == typeof (Int64) - ? BitConverter.GetBytes ((Int64)(object) value) - : type == typeof (Single) - ? BitConverter.GetBytes ((Single)(object) value) - : type == typeof (UInt16) - ? BitConverter.GetBytes ((UInt16)(object) value) - : type == typeof (UInt32) - ? BitConverter.GetBytes ((UInt32)(object) value) - : type == typeof (UInt64) - ? BitConverter.GetBytes ((UInt64)(object) value) - : WebSocket.EmptyBytes; - - if (bytes.Length > 1 && !order.IsHostOrder ()) - Array.Reverse (bytes); - - return bytes; + for (long i = 0; i < n; i++) + action (i); } /// - /// Converts the order of the specified array of to the host byte order. + /// Converts the order of elements in the specified byte array to + /// host (this computer architecture) byte order. /// /// - /// An array of converted from . + /// + /// An array of converted from + /// . + /// + /// + /// if the number of elements in + /// it is less than 2 or is + /// same as host byte order. + /// /// /// /// An array of to convert. /// /// - /// One of the enum values, specifies the byte order of - /// . + /// + /// One of the enum values. + /// + /// + /// It specifies the order of elements in . + /// /// /// /// is . @@ -1726,23 +1815,33 @@ public static byte[] ToHostOrder (this byte[] source, ByteOrder sourceOrder) if (source == null) throw new ArgumentNullException ("source"); - return source.Length > 1 && !sourceOrder.IsHostOrder () ? source.Reverse () : source; + if (source.Length < 2) + return source; + + if (sourceOrder.IsHostOrder ()) + return source; + + return source.Reverse (); } /// - /// Converts the specified to a that - /// concatenates the each element of across the specified - /// . + /// Converts the specified array to a string. /// /// - /// A converted from , - /// or if is empty. + /// + /// A converted by concatenating each element of + /// across . + /// + /// + /// An empty string if is an empty array. + /// /// /// /// An array of T to convert. /// /// - /// A that represents the separator string. + /// A used to separate each element of + /// . /// /// /// The type of elements in . @@ -1756,112 +1855,46 @@ public static string ToString (this T[] array, string separator) throw new ArgumentNullException ("array"); var len = array.Length; + if (len == 0) return String.Empty; - if (separator == null) - separator = String.Empty; - var buff = new StringBuilder (64); - (len - 1).Times (i => buff.AppendFormat ("{0}{1}", array[i].ToString (), separator)); - - buff.Append (array[len - 1].ToString ()); - return buff.ToString (); - } + var end = len - 1; - /// - /// Converts the specified to a . - /// - /// - /// A converted from , - /// or if isn't successfully converted. - /// - /// - /// A to convert. - /// - public static Uri ToUri (this string uriString) - { - Uri ret; - Uri.TryCreate ( - uriString, uriString.MaybeUri () ? UriKind.Absolute : UriKind.Relative, out ret); + for (var i = 0; i < end; i++) + buff.AppendFormat ("{0}{1}", array[i], separator); - return ret; - } + buff.AppendFormat ("{0}", array[end]); - /// - /// URL-decodes the specified . - /// - /// - /// A that receives the decoded string, - /// or the if it's or empty. - /// - /// - /// A to decode. - /// - public static string UrlDecode (this string value) - { - return value != null && value.Length > 0 ? HttpUtility.UrlDecode (value) : value; + return buff.ToString (); } /// - /// URL-encodes the specified . + /// Converts the specified string to a . /// /// - /// A that receives the encoded string, - /// or if it's or empty. - /// - /// - /// A to encode. - /// - public static string UrlEncode (this string value) - { - return value != null && value.Length > 0 ? HttpUtility.UrlEncode (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 . + /// A converted from . /// /// - /// -or- + /// if the conversion has failed. /// - /// - /// is . - /// - /// - public static void WriteContent (this HttpListenerResponse response, byte[] content) + /// + /// + /// A to convert. + /// + public static Uri ToUri (this string value) { - if (response == null) - throw new ArgumentNullException ("response"); - - if (content == null) - throw new ArgumentNullException ("content"); + if (value == null || value.Length == 0) + return null; - var len = content.LongLength; - if (len == 0) { - response.Close (); - return; - } + var kind = value.MaybeUri () ? UriKind.Absolute : UriKind.Relative; + Uri ret; - response.ContentLength64 = len; - var output = response.OutputStream; - if (len <= Int32.MaxValue) - output.Write (content, 0, (int) len); - else - output.WriteBytes (content, 1024); + Uri.TryCreate (value, kind, out ret); - output.Close (); + return ret; } #endregion diff --git a/websocket-sharp/Fin.cs b/websocket-sharp/Fin.cs index 8965c378e..36622d7e5 100644 --- a/websocket-sharp/Fin.cs +++ b/websocket-sharp/Fin.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2025 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 @@ -35,9 +35,10 @@ namespace WebSocketSharp /// /// /// The values of this enumeration are defined in - /// Section 5.2 of RFC 6455. + /// + /// Section 5.2 of RFC 6455. /// - internal enum Fin : byte + internal enum Fin { /// /// Equivalent to numeric value 0. Indicates more frames of a message follow. diff --git a/websocket-sharp/HttpBase.cs b/websocket-sharp/HttpBase.cs index a7dbd4026..c4a244f45 100644 --- a/websocket-sharp/HttpBase.cs +++ b/websocket-sharp/HttpBase.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2012-2014 sta.blockhead + * Copyright (c) 2012-2024 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 @@ -30,6 +30,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.IO; +using System.Linq; using System.Text; using System.Threading; using WebSocketSharp.Net; @@ -41,20 +42,31 @@ internal abstract class HttpBase #region Private Fields private NameValueCollection _headers; - private const int _headersMaxLength = 8192; + private static readonly int _maxMessageHeaderLength; + private string _messageBody; + private byte[] _messageBodyData; private Version _version; #endregion - #region Internal Fields + #region Protected Fields - internal byte[] EntityBodyData; + protected static readonly string CrLf; + protected static readonly string CrLfHt; + protected static readonly string CrLfSp; #endregion - #region Protected Fields + #region Static Constructor - protected const string CrLf = "\r\n"; + static HttpBase () + { + _maxMessageHeaderLength = 8192; + + CrLf = "\r\n"; + CrLfHt = "\r\n\t"; + CrLfSp = "\r\n "; + } #endregion @@ -68,20 +80,40 @@ protected HttpBase (Version version, NameValueCollection headers) #endregion - #region Public Properties + #region Internal Properties - public string EntityBody { + internal byte[] MessageBodyData { get { - if (EntityBodyData == null || EntityBodyData.LongLength == 0) - return String.Empty; + return _messageBodyData; + } + } + + #endregion + + #region Protected Properties + + protected string HeaderSection { + get { + var buff = new StringBuilder (64); + + var fmt = "{0}: {1}{2}"; + + foreach (var key in _headers.AllKeys) + buff.AppendFormat (fmt, key, _headers[key], CrLf); + + buff.Append (CrLf); + + return buff.ToString (); + } + } - Encoding enc = null; + #endregion - var contentType = _headers["Content-Type"]; - if (contentType != null && contentType.Length > 0) - enc = HttpUtility.GetEncoding (contentType); + #region Public Properties - return (enc ?? Encoding.UTF8).GetString (EntityBodyData); + public bool HasMessageBody { + get { + return _messageBodyData != null; } } @@ -91,6 +123,17 @@ public NameValueCollection Headers { } } + public string MessageBody { + get { + if (_messageBody == null) + _messageBody = getMessageBody (); + + return _messageBody; + } + } + + public abstract string MessageHeader { get; } + public Version ProtocolVersion { get { return _version; @@ -101,14 +144,35 @@ public Version ProtocolVersion { #region Private Methods - private static byte[] readEntityBody (Stream stream, string length) + private string getMessageBody () + { + if (_messageBodyData == null || _messageBodyData.LongLength == 0) + return String.Empty; + + var contentType = _headers["Content-Type"]; + + var enc = contentType != null && contentType.Length > 0 + ? HttpUtility.GetEncoding (contentType) + : Encoding.UTF8; + + return enc.GetString (_messageBodyData); + } + + private static byte[] readMessageBodyFrom (Stream stream, string length) { long len; - if (!Int64.TryParse (length, out len)) - throw new ArgumentException ("Cannot be parsed.", "length"); - if (len < 0) - throw new ArgumentOutOfRangeException ("length", "Less than zero."); + if (!Int64.TryParse (length, out len)) { + var msg = "It could not be parsed."; + + throw new ArgumentException (msg, "length"); + } + + if (len < 0) { + var msg = "Less than zero."; + + throw new ArgumentOutOfRangeException ("length", msg); + } return len > 1024 ? stream.ReadBytes (len, 1024) @@ -117,62 +181,93 @@ private static byte[] readEntityBody (Stream stream, string length) : null; } - private static string[] readHeaders (Stream stream, int maxLength) + private static string[] readMessageHeaderFrom (Stream stream) { var buff = new List (); var cnt = 0; - Action add = i => { - if (i == -1) - throw new EndOfStreamException ("The header cannot be read from the data source."); - - buff.Add ((byte) i); - cnt++; - }; - - var read = false; - while (cnt < maxLength) { - if (stream.ReadByte ().EqualsWith ('\r', add) && - stream.ReadByte ().EqualsWith ('\n', add) && - stream.ReadByte ().EqualsWith ('\r', add) && - stream.ReadByte ().EqualsWith ('\n', add)) { - read = true; - break; + Action add = + i => { + if (i == -1) { + var msg = "The header could not be read from the data stream."; + + throw new EndOfStreamException (msg); + } + + buff.Add ((byte) i); + + cnt++; + }; + + var end = false; + + do { + end = stream.ReadByte ().IsEqualTo ('\r', add) + && stream.ReadByte ().IsEqualTo ('\n', add) + && stream.ReadByte ().IsEqualTo ('\r', add) + && stream.ReadByte ().IsEqualTo ('\n', add); + + if (cnt > _maxMessageHeaderLength) { + var msg = "The length of the header is greater than the max length."; + + throw new InvalidOperationException (msg); } } + while (!end); - if (!read) - throw new WebSocketException ("The length of header part is greater than the max length."); + var bytes = buff.ToArray (); - return Encoding.UTF8.GetString (buff.ToArray ()) - .Replace (CrLf + " ", " ") - .Replace (CrLf + "\t", " ") + return Encoding.UTF8.GetString (bytes) + .Replace (CrLfSp, " ") + .Replace (CrLfHt, " ") .Split (new[] { CrLf }, StringSplitOptions.RemoveEmptyEntries); } #endregion + #region Internal Methods + + internal void WriteTo (Stream stream) + { + var bytes = ToByteArray (); + + stream.Write (bytes, 0, bytes.Length); + } + + #endregion + #region Protected Methods - protected static T Read (Stream stream, Func parser, int millisecondsTimeout) + protected static T Read ( + Stream stream, + Func parser, + int millisecondsTimeout + ) where T : HttpBase { + T ret = null; + var timeout = false; var timer = new Timer ( - state => { - timeout = true; - stream.Close (); - }, - null, - millisecondsTimeout, - -1); - - T http = null; + state => { + timeout = true; + + stream.Close (); + }, + null, + millisecondsTimeout, + -1 + ); + Exception exception = null; + try { - http = parser (readHeaders (stream, _headersMaxLength)); - var contentLen = http.Headers["Content-Length"]; + var header = readMessageHeaderFrom (stream); + ret = parser (header); + + var contentLen = ret.Headers["Content-Length"]; + if (contentLen != null && contentLen.Length > 0) - http.EntityBodyData = readEntityBody (stream, contentLen); + ret._messageBodyData = readMessageBodyFrom (stream, contentLen); } catch (Exception ex) { exception = ex; @@ -182,16 +277,19 @@ protected static T Read (Stream stream, Func parser, int millise timer.Dispose (); } - var msg = timeout - ? "A timeout has occurred while reading an HTTP request/response." - : exception != null - ? "An exception has occurred while reading an HTTP request/response." - : null; + if (timeout) { + var msg = "A timeout has occurred."; + + throw new WebSocketException (msg); + } + + if (exception != null) { + var msg = "An exception has occurred."; - if (msg != null) throw new WebSocketException (msg, exception); + } - return http; + return ret; } #endregion @@ -200,9 +298,20 @@ protected static T Read (Stream stream, Func parser, int millise public byte[] ToByteArray () { - return Encoding.UTF8.GetBytes (ToString ()); + var headerData = Encoding.UTF8.GetBytes (MessageHeader); + + return _messageBodyData != null + ? headerData.Concat (_messageBodyData).ToArray () + : headerData; + } + + public override string ToString () + { + return _messageBodyData != null + ? MessageHeader + MessageBody + : MessageHeader; } - + #endregion } } diff --git a/websocket-sharp/HttpRequest.cs b/websocket-sharp/HttpRequest.cs index f9aa5cb33..946024d6a 100644 --- a/websocket-sharp/HttpRequest.cs +++ b/websocket-sharp/HttpRequest.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2024 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 @@ -45,48 +45,68 @@ internal class HttpRequest : HttpBase { #region Private Fields - private string _method; - private string _uri; - private bool _websocketRequest; - private bool _websocketRequestSet; + private CookieCollection _cookies; + private string _method; + private string _target; #endregion #region Private Constructors - private HttpRequest (string method, string uri, Version version, NameValueCollection headers) + private HttpRequest ( + string method, + string target, + Version version, + NameValueCollection headers + ) : base (version, headers) { _method = method; - _uri = uri; + _target = target; } #endregion #region Internal Constructors - internal HttpRequest (string method, string uri) - : this (method, uri, HttpVersion.Version11, new NameValueCollection ()) + internal HttpRequest (string method, string target) + : this (method, target, HttpVersion.Version11, new NameValueCollection ()) { Headers["User-Agent"] = "websocket-sharp/1.0"; } #endregion + #region Internal Properties + + internal string RequestLine { + get { + var fmt = "{0} {1} HTTP/{2}{3}"; + + return String.Format (fmt, _method, _target, ProtocolVersion, CrLf); + } + } + + #endregion + #region Public Properties public AuthenticationResponse AuthenticationResponse { get { - var res = Headers["Authorization"]; - return res != null && res.Length > 0 - ? AuthenticationResponse.Parse (res) + var val = Headers["Authorization"]; + + return val != null && val.Length > 0 + ? AuthenticationResponse.Parse (val) : null; } } public CookieCollection Cookies { get { - return Headers.GetCookies (false); + if (_cookies == null) + _cookies = Headers.GetCookies (false); + + return _cookies; } } @@ -98,23 +118,21 @@ public string HttpMethod { public bool IsWebSocketRequest { get { - if (!_websocketRequestSet) { - var headers = Headers; - _websocketRequest = _method == "GET" && - ProtocolVersion > HttpVersion.Version10 && - headers.Contains ("Upgrade", "websocket") && - headers.Contains ("Connection", "Upgrade"); - - _websocketRequestSet = true; - } + return _method == "GET" + && ProtocolVersion > HttpVersion.Version10 + && Headers.Upgrades ("websocket"); + } + } - return _websocketRequest; + public override string MessageHeader { + get { + return RequestLine + HeaderSection; } } - public string RequestUri { + public string RequestTarget { get { - return _uri; + return _target; } } @@ -122,59 +140,82 @@ public string RequestUri { #region Internal Methods - internal static HttpRequest CreateConnectRequest (Uri uri) + internal static HttpRequest CreateConnectRequest (Uri targetUri) { - var host = uri.DnsSafeHost; - var port = uri.Port; - var authority = String.Format ("{0}:{1}", host, port); - var req = new HttpRequest ("CONNECT", authority); - req.Headers["Host"] = port == 80 ? host : authority; + var fmt = "{0}:{1}"; + var host = targetUri.DnsSafeHost; + var port = targetUri.Port; + var authority = String.Format (fmt, host, port); + + var ret = new HttpRequest ("CONNECT", authority); - return req; + ret.Headers["Host"] = port != 80 ? authority : host; + + return ret; } - internal static HttpRequest CreateWebSocketRequest (Uri uri) + internal static HttpRequest CreateWebSocketHandshakeRequest (Uri targetUri) { - var req = new HttpRequest ("GET", uri.PathAndQuery); - var headers = req.Headers; + var ret = new HttpRequest ("GET", targetUri.PathAndQuery); + + var headers = ret.Headers; - // Only includes a port number in the Host header value if it's non-default. - // See: https://tools.ietf.org/html/rfc6455#page-17 - var port = uri.Port; - var schm = uri.Scheme; - headers["Host"] = (port == 80 && schm == "ws") || (port == 443 && schm == "wss") - ? uri.DnsSafeHost - : uri.Authority; + var port = targetUri.Port; + var schm = targetUri.Scheme; + var isDefaultPort = (port == 80 && schm == "ws") + || (port == 443 && schm == "wss"); + + headers["Host"] = !isDefaultPort + ? targetUri.Authority + : targetUri.DnsSafeHost; headers["Upgrade"] = "websocket"; headers["Connection"] = "Upgrade"; - return req; + return ret; } internal HttpResponse GetResponse (Stream stream, int millisecondsTimeout) { - var buff = ToByteArray (); - stream.Write (buff, 0, buff.Length); + WriteTo (stream); - return Read (stream, HttpResponse.Parse, millisecondsTimeout); + return HttpResponse.ReadResponse (stream, millisecondsTimeout); } - internal static HttpRequest Parse (string[] headerParts) + internal static HttpRequest Parse (string[] messageHeader) { - var requestLine = headerParts[0].Split (new[] { ' ' }, 3); - if (requestLine.Length != 3) - throw new ArgumentException ("Invalid request line: " + headerParts[0]); + var len = messageHeader.Length; + + if (len == 0) { + var msg = "An empty request header."; + + throw new ArgumentException (msg); + } + + var rlParts = messageHeader[0].Split (new[] { ' ' }, 3); + + if (rlParts.Length != 3) { + var msg = "It includes an invalid request line."; + + throw new ArgumentException (msg); + } + + var method = rlParts[0]; + var target = rlParts[1]; + var ver = rlParts[2].Substring (5).ToVersion (); var headers = new WebHeaderCollection (); - for (int i = 1; i < headerParts.Length; i++) - headers.InternalSet (headerParts[i], false); - return new HttpRequest ( - requestLine[0], requestLine[1], new Version (requestLine[2].Substring (5)), headers); + for (var i = 1; i < len; i++) + headers.InternalSet (messageHeader[i], false); + + return new HttpRequest (method, target, ver, headers); } - internal static HttpRequest Read (Stream stream, int millisecondsTimeout) + internal static HttpRequest ReadRequest ( + Stream stream, + int millisecondsTimeout + ) { return Read (stream, Parse, millisecondsTimeout); } @@ -189,33 +230,22 @@ public void SetCookies (CookieCollection cookies) return; var buff = new StringBuilder (64); - foreach (var cookie in cookies.Sorted) - if (!cookie.Expired) - buff.AppendFormat ("{0}; ", cookie.ToString ()); - var len = buff.Length; - if (len > 2) { - buff.Length = len - 2; - Headers["Cookie"] = buff.ToString (); - } - } + foreach (var cookie in cookies.SortedList) { + if (cookie.Expired) + continue; - public override string ToString () - { - var output = new StringBuilder (64); - output.AppendFormat ("{0} {1} HTTP/{2}{3}", _method, _uri, ProtocolVersion, CrLf); + buff.AppendFormat ("{0}; ", cookie); + } - var headers = Headers; - foreach (var key in headers.AllKeys) - output.AppendFormat ("{0}: {1}{2}", key, headers[key], CrLf); + var len = buff.Length; - output.Append (CrLf); + if (len <= 2) + return; - var entity = EntityBody; - if (entity.Length > 0) - output.Append (entity); + buff.Length = len - 2; - return output.ToString (); + Headers["Cookie"] = buff.ToString (); } #endregion diff --git a/websocket-sharp/HttpResponse.cs b/websocket-sharp/HttpResponse.cs index 19315a3f0..d511c94fe 100644 --- a/websocket-sharp/HttpResponse.cs +++ b/websocket-sharp/HttpResponse.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2012-2014 sta.blockhead + * Copyright (c) 2012-2024 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 @@ -38,14 +38,19 @@ internal class HttpResponse : HttpBase { #region Private Fields - private string _code; + private int _code; private string _reason; #endregion #region Private Constructors - private HttpResponse (string code, string reason, Version version, NameValueCollection headers) + private HttpResponse ( + int code, + string reason, + Version version, + NameValueCollection headers + ) : base (version, headers) { _code = code; @@ -56,58 +61,108 @@ private HttpResponse (string code, string reason, Version version, NameValueColl #region Internal Constructors + internal HttpResponse (int code) + : this (code, code.GetStatusDescription ()) + { + } + internal HttpResponse (HttpStatusCode code) - : this (code, code.GetDescription ()) + : this ((int) code) { } - internal HttpResponse (HttpStatusCode code, string reason) - : this (((int) code).ToString (), reason, HttpVersion.Version11, new NameValueCollection ()) + internal HttpResponse (int code, string reason) + : this ( + code, + reason, + HttpVersion.Version11, + new NameValueCollection () + ) { Headers["Server"] = "websocket-sharp/1.0"; } + internal HttpResponse (HttpStatusCode code, string reason) + : this ((int) code, reason) + { + } + + #endregion + + #region Internal Properties + + internal string StatusLine { + get { + return _reason != null + ? String.Format ( + "HTTP/{0} {1} {2}{3}", + ProtocolVersion, + _code, + _reason, + CrLf + ) + : String.Format ( + "HTTP/{0} {1}{2}", + ProtocolVersion, + _code, + CrLf + ); + } + } + #endregion #region Public Properties - public CookieCollection Cookies { + public bool CloseConnection { get { - return Headers.GetCookies (true); + var compType = StringComparison.OrdinalIgnoreCase; + + return Headers.Contains ("Connection", "close", compType); } } - public bool HasConnectionClose { + public CookieCollection Cookies { get { - return Headers.Contains ("Connection", "close"); + return Headers.GetCookies (true); } } public bool IsProxyAuthenticationRequired { get { - return _code == "407"; + return _code == 407; } } public bool IsRedirect { get { - return _code == "301" || _code == "302"; + return _code == 301 || _code == 302; + } + } + + public bool IsSuccess { + get { + return _code >= 200 && _code <= 299; } } public bool IsUnauthorized { get { - return _code == "401"; + return _code == 401; } } public bool IsWebSocketResponse { get { - var headers = Headers; - return ProtocolVersion > HttpVersion.Version10 && - _code == "101" && - headers.Contains ("Upgrade", "websocket") && - headers.Contains ("Connection", "Upgrade"); + return ProtocolVersion > HttpVersion.Version10 + && _code == 101 + && Headers.Upgrades ("websocket"); + } + } + + public override string MessageHeader { + get { + return StatusLine + HeaderSection; } } @@ -117,7 +172,7 @@ public string Reason { } } - public string StatusCode { + public int StatusCode { get { return _code; } @@ -129,46 +184,69 @@ public string StatusCode { internal static HttpResponse CreateCloseResponse (HttpStatusCode code) { - var res = new HttpResponse (code); - res.Headers["Connection"] = "close"; + var ret = new HttpResponse (code); + + ret.Headers["Connection"] = "close"; - return res; + return ret; } internal static HttpResponse CreateUnauthorizedResponse (string challenge) { - var res = new HttpResponse (HttpStatusCode.Unauthorized); - res.Headers["WWW-Authenticate"] = challenge; + var ret = new HttpResponse (HttpStatusCode.Unauthorized); + + ret.Headers["WWW-Authenticate"] = challenge; - return res; + return ret; } - internal static HttpResponse CreateWebSocketResponse () + internal static HttpResponse CreateWebSocketHandshakeResponse () { - var res = new HttpResponse (HttpStatusCode.SwitchingProtocols); + var ret = new HttpResponse (HttpStatusCode.SwitchingProtocols); + + var headers = ret.Headers; - var headers = res.Headers; headers["Upgrade"] = "websocket"; headers["Connection"] = "Upgrade"; - return res; + return ret; } - internal static HttpResponse Parse (string[] headerParts) + internal static HttpResponse Parse (string[] messageHeader) { - var statusLine = headerParts[0].Split (new[] { ' ' }, 3); - if (statusLine.Length != 3) - throw new ArgumentException ("Invalid status line: " + headerParts[0]); + var len = messageHeader.Length; + + if (len == 0) { + var msg = "An empty response header."; + + throw new ArgumentException (msg); + } + + var slParts = messageHeader[0].Split (new[] { ' ' }, 3); + var plen = slParts.Length; + + if (plen < 2) { + var msg = "It includes an invalid status line."; + + throw new ArgumentException (msg); + } + + var code = slParts[1].ToInt32 (); + var reason = plen == 3 ? slParts[2] : null; + var ver = slParts[0].Substring (5).ToVersion (); var headers = new WebHeaderCollection (); - for (int i = 1; i < headerParts.Length; i++) - headers.InternalSet (headerParts[i], true); - return new HttpResponse ( - statusLine[1], statusLine[2], new Version (statusLine[0].Substring (5)), headers); + for (var i = 1; i < len; i++) + headers.InternalSet (messageHeader[i], true); + + return new HttpResponse (code, reason, ver, headers); } - internal static HttpResponse Read (Stream stream, int millisecondsTimeout) + internal static HttpResponse ReadResponse ( + Stream stream, + int millisecondsTimeout + ) { return Read (stream, Parse, millisecondsTimeout); } @@ -183,26 +261,12 @@ public void SetCookies (CookieCollection cookies) return; var headers = Headers; - foreach (var cookie in cookies.Sorted) - headers.Add ("Set-Cookie", cookie.ToResponseString ()); - } - public override string ToString () - { - var output = new StringBuilder (64); - output.AppendFormat ("HTTP/{0} {1} {2}{3}", ProtocolVersion, _code, _reason, CrLf); + foreach (var cookie in cookies.SortedList) { + var val = cookie.ToResponseString (); - var headers = Headers; - foreach (var key in headers.AllKeys) - output.AppendFormat ("{0}: {1}{2}", key, headers[key], CrLf); - - output.Append (CrLf); - - var entity = EntityBody; - if (entity.Length > 0) - output.Append (entity); - - return output.ToString (); + headers.Add ("Set-Cookie", val); + } } #endregion diff --git a/websocket-sharp/LogData.cs b/websocket-sharp/LogData.cs index 9c0843093..bb3492a9b 100644 --- a/websocket-sharp/LogData.cs +++ b/websocket-sharp/LogData.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2013-2015 sta.blockhead + * Copyright (c) 2013-2024 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 @@ -53,6 +53,7 @@ internal LogData (LogLevel level, StackFrame caller, string message) _level = level; _caller = caller; _message = message ?? String.Empty; + _date = DateTime.Now; } @@ -64,7 +65,8 @@ internal LogData (LogLevel level, StackFrame caller, string message) /// Gets the information of the logging method caller. /// /// - /// A that provides the information of the logging method caller. + /// A that provides the information of + /// the logging method caller. /// public StackFrame Caller { get { @@ -76,7 +78,8 @@ public StackFrame Caller { /// Gets the date and time when the log data was created. /// /// - /// A that represents the date and time when the log data was created. + /// A that represents the date and time when + /// the log data was created. /// public DateTime Date { get { @@ -88,7 +91,12 @@ public DateTime Date { /// Gets the logging level of the log data. /// /// - /// One of the enum values, indicates the logging level of the log data. + /// + /// One of the enum values. + /// + /// + /// It represents the logging level of the log data. + /// /// public LogLevel Level { get { @@ -113,34 +121,36 @@ public string Message { #region Public Methods /// - /// Returns a that represents the current . + /// Returns a string that represents the current instance. /// /// - /// A that represents the current . + /// A that represents the current instance. /// public override string ToString () { - var header = String.Format ("{0}|{1,-5}|", _date, _level); + var date = String.Format ("[{0}]", _date); + var level = String.Format ("{0,-5}", _level.ToString ().ToUpper ()); + 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); + var num = _caller.GetFileLineNumber (); + var caller = String.Format ("{0}.{1}:{2}", type.Name, method.Name, num); #else - var headerAndCaller = String.Format ("{0}{1}.{2}|", header, type.Name, method.Name); + var caller = String.Format ("{0}.{1}", 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); + return String.Format ("{0} {1} {2} {3}", date, level, caller, _message); + + var buff = new StringBuilder (64); - var buff = new StringBuilder (String.Format ("{0}{1}\n", headerAndCaller, msgs[0]), 64); + buff.AppendFormat ("{0} {1} {2}\n\n", date, level, caller); - var fmt = String.Format ("{{0,{0}}}{{1}}\n", header.Length); - for (var i = 1; i < msgs.Length; i++) - buff.AppendFormat (fmt, "", msgs[i]); + foreach (var msg in msgs) + buff.AppendFormat (" {0}\n", msg); - buff.Length--; return buff.ToString (); } diff --git a/websocket-sharp/LogLevel.cs b/websocket-sharp/LogLevel.cs index ef9967728..5ff1d8fed 100644 --- a/websocket-sharp/LogLevel.cs +++ b/websocket-sharp/LogLevel.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2013-2015 sta.blockhead + * Copyright (c) 2013-2022 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 @@ -58,6 +58,10 @@ public enum LogLevel /// /// Specifies the top logging level. /// - Fatal + Fatal, + /// + /// Specifies not to output logs. + /// + None } } diff --git a/websocket-sharp/Logger.cs b/websocket-sharp/Logger.cs index 17850e67e..a280ce43f 100644 --- a/websocket-sharp/Logger.cs +++ b/websocket-sharp/Logger.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2013-2015 sta.blockhead + * Copyright (c) 2013-2024 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 @@ -37,17 +37,17 @@ namespace WebSocketSharp /// /// /// - /// If you output a log with lower than the value of the property, + /// If you output a log with lower than the current logging level, /// it cannot be outputted. /// /// - /// The default output action writes a log to the standard output stream and the log file - /// if the property has a valid path to it. + /// The default output method writes a log to the standard output + /// stream and the text file if it has a valid path. /// /// - /// If you would like to use the custom output action, you should set - /// the property to any Action<LogData, string> - /// delegate. + /// If you would like to use the custom output method, you should + /// specify it with the constructor or the + /// property. /// /// public class Logger @@ -67,7 +67,7 @@ public class Logger /// Initializes a new instance of the class. /// /// - /// This constructor initializes the current logging level with . + /// This constructor initializes the logging level with the Error level. /// public Logger () : this (LogLevel.Error, null, null) @@ -76,10 +76,11 @@ public Logger () /// /// Initializes a new instance of the class with - /// the specified logging . + /// the specified logging level. /// /// - /// One of the enum values. + /// One of the enum values that specifies + /// the logging level. /// public Logger (LogLevel level) : this (level, null, null) @@ -88,25 +89,26 @@ public Logger (LogLevel level) /// /// Initializes a new instance of the class with - /// the specified logging , path to the log , - /// and action. + /// the specified logging level, path to the log file, and delegate + /// used to output a log. /// /// - /// One of the enum values. + /// One of the enum values that specifies + /// the logging level. /// /// - /// A that represents the path to the log file. + /// A that specifies the path to the log file. /// /// - /// An Action<LogData, string> delegate that references the method(s) used to - /// output a log. A parameter passed to this delegate is - /// . + /// An that specifies + /// the delegate used to output a log. /// public Logger (LogLevel level, string file, Action output) { _level = level; _file = file; _output = output ?? defaultOutput; + _sync = new object (); } @@ -115,10 +117,10 @@ public Logger (LogLevel level, string file, Action output) #region Public Properties /// - /// Gets or sets the current path to the log file. + /// Gets or sets the path to the log file. /// /// - /// A that represents the current path to the log file if any. + /// A that represents the path to the log file if any. /// public string File { get { @@ -126,11 +128,8 @@ public string File { } set { - lock (_sync) { + lock (_sync) _file = value; - Warn ( - String.Format ("The current path to the log file has been changed to {0}.", _file)); - } } } @@ -141,7 +140,12 @@ public string File { /// A log with lower than the value of this property cannot be outputted. /// /// - /// One of the enum values, specifies the current logging level. + /// + /// One of the enum values. + /// + /// + /// It represents the current logging level. + /// /// public LogLevel Level { get { @@ -149,25 +153,28 @@ public LogLevel Level { } set { - lock (_sync) { + lock (_sync) _level = value; - Warn (String.Format ("The current logging level has been changed to {0}.", _level)); - } } } /// - /// Gets or sets the current output action used to output a log. + /// Gets or sets the delegate used to output a log. /// /// /// - /// An Action<LogData, string> delegate that references the method(s) used to - /// output a log. A parameter passed to this delegate is the value of + /// An delegate. + /// + /// + /// It represents the delegate called when the logger outputs a log. + /// + /// + /// The string parameter passed to the delegate is the value of /// the property. /// /// - /// If the value to set is , the current output action is changed to - /// the default output action. + /// If the value to set is , the default + /// output method is set. /// /// public Action Output { @@ -176,10 +183,8 @@ public Action Output { } set { - lock (_sync) { + lock (_sync) _output = value ?? defaultOutput; - Warn ("The current output action has been changed."); - } } } @@ -189,10 +194,12 @@ public Action Output { private static void defaultOutput (LogData data, string path) { - var log = data.ToString (); - Console.WriteLine (log); + var val = data.ToString (); + + Console.WriteLine (val); + if (path != null && path.Length > 0) - writeToFile (log, path); + writeToFile (val, path); } private void output (string message, LogLevel level) @@ -201,13 +208,18 @@ private void output (string message, LogLevel level) if (_level > level) return; - LogData data = null; try { - data = new LogData (level, new StackFrame (2, true), message); + var data = new LogData (level, new StackFrame (2, true), message); + _output (data, _file); } catch (Exception ex) { - data = new LogData (LogLevel.Fatal, new StackFrame (0, true), ex.Message); + var data = new LogData ( + LogLevel.Fatal, + new StackFrame (0, true), + ex.Message + ); + Console.WriteLine (data.ToString ()); } } @@ -225,14 +237,14 @@ private static void writeToFile (string value, string path) #region Public Methods /// - /// Outputs as a log with . + /// Outputs the specified message as a log with the Debug level. /// /// - /// If the current logging level is higher than , - /// this method doesn't output as a log. + /// If the current logging level is higher than the Debug level, + /// this method does not output the message as a log. /// /// - /// A that represents the message to output as a log. + /// A that specifies the message to output. /// public void Debug (string message) { @@ -243,14 +255,14 @@ public void Debug (string message) } /// - /// Outputs as a log with . + /// Outputs the specified message as a log with the Error level. /// /// - /// If the current logging level is higher than , - /// this method doesn't output as a log. + /// If the current logging level is higher than the Error level, + /// this method does not output the message as a log. /// /// - /// A that represents the message to output as a log. + /// A that specifies the message to output. /// public void Error (string message) { @@ -261,25 +273,28 @@ public void Error (string message) } /// - /// Outputs as a log with . + /// Outputs the specified message as a log with the Fatal level. /// /// - /// A that represents the message to output as a log. + /// A that specifies the message to output. /// public void Fatal (string message) { + if (_level > LogLevel.Fatal) + return; + output (message, LogLevel.Fatal); } /// - /// Outputs as a log with . + /// Outputs the specified message as a log with the Info level. /// /// - /// If the current logging level is higher than , - /// this method doesn't output as a log. + /// If the current logging level is higher than the Info level, + /// this method does not output the message as a log. /// /// - /// A that represents the message to output as a log. + /// A that specifies the message to output. /// public void Info (string message) { @@ -290,14 +305,14 @@ public void Info (string message) } /// - /// Outputs as a log with . + /// Outputs the specified message as a log with the Trace level. /// /// - /// If the current logging level is higher than , - /// this method doesn't output as a log. + /// If the current logging level is higher than the Trace level, + /// this method does not output the message as a log. /// /// - /// A that represents the message to output as a log. + /// A that specifies the message to output. /// public void Trace (string message) { @@ -308,14 +323,14 @@ public void Trace (string message) } /// - /// Outputs as a log with . + /// Outputs the specified message as a log with the Warn level. /// /// - /// If the current logging level is higher than , - /// this method doesn't output as a log. + /// If the current logging level is higher than the Warn level, + /// this method does not output the message as a log. /// /// - /// A that represents the message to output as a log. + /// A that specifies the message to output. /// public void Warn (string message) { diff --git a/websocket-sharp/Mask.cs b/websocket-sharp/Mask.cs index fcafac80c..2958f5a83 100644 --- a/websocket-sharp/Mask.cs +++ b/websocket-sharp/Mask.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2025 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 @@ -35,9 +35,10 @@ namespace WebSocketSharp /// /// /// The values of this enumeration are defined in - /// Section 5.2 of RFC 6455. + /// + /// Section 5.2 of RFC 6455. /// - internal enum Mask : byte + internal enum Mask { /// /// Equivalent to numeric value 0. Indicates not masked. diff --git a/websocket-sharp/MessageEventArgs.cs b/websocket-sharp/MessageEventArgs.cs index 0639baf39..63add90f7 100644 --- a/websocket-sharp/MessageEventArgs.cs +++ b/websocket-sharp/MessageEventArgs.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2022 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 @@ -35,13 +35,13 @@ namespace WebSocketSharp /// /// /// - /// A event occurs when the receives - /// a text or binary message, or a ping if the property is - /// set to true. + /// The message event occurs when the interface + /// receives a message or a ping if the + /// property is set to true. /// /// - /// If you would like to get the message data, you should access the or - /// property. + /// If you would like to get the message data, you should access + /// the or property. /// /// public class MessageEventArgs : EventArgs @@ -74,24 +74,41 @@ internal MessageEventArgs (Opcode opcode, byte[] rawData) #endregion + #region Internal Properties + + /// + /// Gets the opcode for the message. + /// + /// + /// , , + /// or . + /// + internal Opcode Opcode { + get { + return _opcode; + } + } + + #endregion + #region Public Properties /// /// Gets the message data as a . /// /// - /// A that represents the message data, - /// or if the message data cannot be decoded to a string. + /// + /// A that represents the message data + /// if the message type is text or ping. + /// + /// + /// if the message type is binary or + /// the message data could not be UTF-8-decoded. + /// /// public string Data { get { - if (!_dataSet) { - _data = _opcode != Opcode.Binary - ? _rawData.UTF8Decode () - : BitConverter.ToString (_rawData); - - _dataSet = true; - } + setData (); return _data; } @@ -141,21 +158,33 @@ public bool IsText { /// public byte[] RawData { get { + setData (); + return _rawData; } } - /// - /// Gets the message type. - /// - /// - /// , , or . - /// - [Obsolete ("This property will be removed. Use any of the Is properties instead.")] - public Opcode Type { - get { - return _opcode; + #endregion + + #region Private Methods + + private void setData () + { + if (_dataSet) + return; + + if (_opcode == Opcode.Binary) { + _dataSet = true; + + return; } + + string data; + + if (_rawData.TryGetUTF8DecodedString (out data)) + _data = data; + + _dataSet = true; } #endregion diff --git a/websocket-sharp/Net/AuthenticationBase.cs b/websocket-sharp/Net/AuthenticationBase.cs deleted file mode 100644 index 107750499..000000000 --- a/websocket-sharp/Net/AuthenticationBase.cs +++ /dev/null @@ -1,151 +0,0 @@ -#region License -/* - * AuthenticationBase.cs - * - * The MIT License - * - * 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 - -using System; -using System.Collections.Specialized; -using System.Text; - -namespace WebSocketSharp.Net -{ - internal abstract class AuthenticationBase - { - #region Private Fields - - private AuthenticationSchemes _scheme; - - #endregion - - #region Internal Fields - - internal NameValueCollection Parameters; - - #endregion - - #region Protected Constructors - - protected AuthenticationBase (AuthenticationSchemes scheme, NameValueCollection parameters) - { - _scheme = scheme; - Parameters = parameters; - } - - #endregion - - #region Public Properties - - public string Algorithm { - get { - return Parameters["algorithm"]; - } - } - - public string Nonce { - get { - return Parameters["nonce"]; - } - } - - public string Opaque { - get { - return Parameters["opaque"]; - } - } - - public string Qop { - get { - return Parameters["qop"]; - } - } - - public string Realm { - get { - return Parameters["realm"]; - } - } - - public AuthenticationSchemes Scheme { - get { - return _scheme; - } - } - - #endregion - - #region Internal Methods - - internal static string CreateNonceValue () - { - var src = new byte[16]; - var rand = new Random (); - rand.NextBytes (src); - - var res = new StringBuilder (32); - foreach (var b in src) - res.Append (b.ToString ("x2")); - - return res.ToString (); - } - - internal static NameValueCollection ParseParameters (string value) - { - var res = new NameValueCollection (); - foreach (var param in value.SplitHeaderValue (',')) { - var i = param.IndexOf ('='); - var name = i > 0 ? param.Substring (0, i).Trim () : null; - var val = i < 0 - ? param.Trim ().Trim ('"') - : i < param.Length - 1 - ? param.Substring (i + 1).Trim ().Trim ('"') - : String.Empty; - - res.Add (name, val); - } - - return res; - } - - internal abstract string ToBasicString (); - - internal abstract string ToDigestString (); - - #endregion - - #region Public Methods - - public override string ToString () - { - return _scheme == AuthenticationSchemes.Basic - ? ToBasicString () - : _scheme == AuthenticationSchemes.Digest - ? ToDigestString () - : String.Empty; - } - - #endregion - } -} diff --git a/websocket-sharp/Net/AuthenticationChallenge.cs b/websocket-sharp/Net/AuthenticationChallenge.cs index 3472204b9..726740801 100644 --- a/websocket-sharp/Net/AuthenticationChallenge.cs +++ b/websocket-sharp/Net/AuthenticationChallenge.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2013-2014 sta.blockhead + * Copyright (c) 2013-2024 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 @@ -32,27 +32,52 @@ namespace WebSocketSharp.Net { - internal class AuthenticationChallenge : AuthenticationBase + internal class AuthenticationChallenge { + #region Private Fields + + private NameValueCollection _parameters; + private AuthenticationSchemes _scheme; + + #endregion + #region Private Constructors - private AuthenticationChallenge (AuthenticationSchemes scheme, NameValueCollection parameters) - : base (scheme, parameters) + private AuthenticationChallenge ( + AuthenticationSchemes scheme, + NameValueCollection parameters + ) { + _scheme = scheme; + _parameters = parameters; } #endregion #region Internal Constructors - internal AuthenticationChallenge (AuthenticationSchemes scheme, string realm) - : base (scheme, new NameValueCollection ()) + internal AuthenticationChallenge ( + AuthenticationSchemes scheme, + string realm + ) + : this (scheme, new NameValueCollection ()) { - Parameters["realm"] = realm; + _parameters["realm"] = realm; + if (scheme == AuthenticationSchemes.Digest) { - Parameters["nonce"] = CreateNonceValue (); - Parameters["algorithm"] = "MD5"; - Parameters["qop"] = "auth"; + _parameters["nonce"] = CreateNonceValue (); + _parameters["algorithm"] = "MD5"; + _parameters["qop"] = "auth"; + } + } + + #endregion + + #region Internal Properties + + internal NameValueCollection Parameters { + get { + return _parameters; } } @@ -60,15 +85,51 @@ internal AuthenticationChallenge (AuthenticationSchemes scheme, string realm) #region Public Properties + public string Algorithm { + get { + return _parameters["algorithm"]; + } + } + public string Domain { get { - return Parameters["domain"]; + return _parameters["domain"]; + } + } + + public string Nonce { + get { + return _parameters["nonce"]; + } + } + + public string Opaque { + get { + return _parameters["opaque"]; + } + } + + public string Qop { + get { + return _parameters["qop"]; + } + } + + public string Realm { + get { + return _parameters["realm"]; + } + } + + public AuthenticationSchemes Scheme { + get { + return _scheme; } } public string Stale { get { - return Parameters["stale"]; + return _parameters["stale"]; } } @@ -86,59 +147,132 @@ internal static AuthenticationChallenge CreateDigestChallenge (string realm) return new AuthenticationChallenge (AuthenticationSchemes.Digest, realm); } + internal static string CreateNonceValue () + { + var rand = new Random (); + var bytes = new byte[16]; + + rand.NextBytes (bytes); + + var buff = new StringBuilder (32); + + foreach (var b in bytes) + buff.Append (b.ToString ("x2")); + + return buff.ToString (); + } + internal static AuthenticationChallenge Parse (string value) { var chal = value.Split (new[] { ' ' }, 2); + if (chal.Length != 2) return null; var schm = chal[0].ToLower (); - return schm == "basic" - ? new AuthenticationChallenge ( - AuthenticationSchemes.Basic, ParseParameters (chal[1])) - : schm == "digest" - ? new AuthenticationChallenge ( - AuthenticationSchemes.Digest, ParseParameters (chal[1])) - : null; + + if (schm == "basic") { + var parameters = ParseParameters (chal[1]); + + return new AuthenticationChallenge ( + AuthenticationSchemes.Basic, + parameters + ); + } + + if (schm == "digest") { + var parameters = ParseParameters (chal[1]); + + return new AuthenticationChallenge ( + AuthenticationSchemes.Digest, + parameters + ); + } + + return null; } - internal override string ToBasicString () + internal static NameValueCollection ParseParameters (string value) { - return String.Format ("Basic realm=\"{0}\"", Parameters["realm"]); + var ret = new NameValueCollection (); + + foreach (var param in value.SplitHeaderValue (',')) { + var i = param.IndexOf ('='); + + var name = i > 0 ? param.Substring (0, i).Trim () : null; + var val = i < 0 + ? param.Trim ().Trim ('"') + : i < param.Length - 1 + ? param.Substring (i + 1).Trim ().Trim ('"') + : String.Empty; + + ret.Add (name, val); + } + + return ret; } - internal override string ToDigestString () + internal string ToBasicString () { - var output = new StringBuilder (128); + return String.Format ("Basic realm=\"{0}\"", _parameters["realm"]); + } + + internal string ToDigestString () + { + var buff = new StringBuilder (128); - var domain = Parameters["domain"]; - if (domain != null) - output.AppendFormat ( + var domain = _parameters["domain"]; + var realm = _parameters["realm"]; + var nonce = _parameters["nonce"]; + + if (domain != null) { + buff.AppendFormat ( "Digest realm=\"{0}\", domain=\"{1}\", nonce=\"{2}\"", - Parameters["realm"], + realm, domain, - Parameters["nonce"]); - else - output.AppendFormat ( - "Digest realm=\"{0}\", nonce=\"{1}\"", Parameters["realm"], Parameters["nonce"]); + nonce + ); + } + else { + buff.AppendFormat ("Digest realm=\"{0}\", nonce=\"{1}\"", realm, nonce); + } + + var opaque = _parameters["opaque"]; - var opaque = Parameters["opaque"]; if (opaque != null) - output.AppendFormat (", opaque=\"{0}\"", opaque); + buff.AppendFormat (", opaque=\"{0}\"", opaque); + + var stale = _parameters["stale"]; - var stale = Parameters["stale"]; if (stale != null) - output.AppendFormat (", stale={0}", stale); + buff.AppendFormat (", stale={0}", stale); + + var algo = _parameters["algorithm"]; - var algo = Parameters["algorithm"]; if (algo != null) - output.AppendFormat (", algorithm={0}", algo); + buff.AppendFormat (", algorithm={0}", algo); + + var qop = _parameters["qop"]; - var qop = Parameters["qop"]; if (qop != null) - output.AppendFormat (", qop=\"{0}\"", qop); + buff.AppendFormat (", qop=\"{0}\"", qop); + + return buff.ToString (); + } + + #endregion + + #region Public Methods + + public override string ToString () + { + if (_scheme == AuthenticationSchemes.Basic) + return ToBasicString (); + + if (_scheme == AuthenticationSchemes.Digest) + return ToDigestString (); - return output.ToString (); + return String.Empty; } #endregion diff --git a/websocket-sharp/Net/AuthenticationResponse.cs b/websocket-sharp/Net/AuthenticationResponse.cs index cc49b372e..28fbd2236 100644 --- a/websocket-sharp/Net/AuthenticationResponse.cs +++ b/websocket-sharp/Net/AuthenticationResponse.cs @@ -2,13 +2,13 @@ /* * AuthenticationResponse.cs * - * ParseBasicCredentials is derived from System.Net.HttpListenerContext.cs of Mono - * (http://www.mono-project.com). + * The ParseBasicCredentials method is derived from HttpListenerContext.cs + * (System.Net) of Mono (http://www.mono-project.com). * * The MIT License * * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) - * Copyright (c) 2013-2014 sta.blockhead + * Copyright (c) 2013-2024 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 @@ -38,19 +38,25 @@ namespace WebSocketSharp.Net { - internal class AuthenticationResponse : AuthenticationBase + internal class AuthenticationResponse { #region Private Fields - private uint _nonceCount; + private uint _nonceCount; + private NameValueCollection _parameters; + private AuthenticationSchemes _scheme; #endregion #region Private Constructors - private AuthenticationResponse (AuthenticationSchemes scheme, NameValueCollection parameters) - : base (scheme, parameters) + private AuthenticationResponse ( + AuthenticationSchemes scheme, + NameValueCollection parameters + ) { + _scheme = scheme; + _parameters = parameters; } #endregion @@ -58,12 +64,20 @@ private AuthenticationResponse (AuthenticationSchemes scheme, NameValueCollectio #region Internal Constructors internal AuthenticationResponse (NetworkCredential credentials) - : this (AuthenticationSchemes.Basic, new NameValueCollection (), credentials, 0) + : this ( + AuthenticationSchemes.Basic, + new NameValueCollection (), + credentials, + 0 + ) { } internal AuthenticationResponse ( - AuthenticationChallenge challenge, NetworkCredential credentials, uint nonceCount) + AuthenticationChallenge challenge, + NetworkCredential credentials, + uint nonceCount + ) : this (challenge.Scheme, challenge.Parameters, credentials, nonceCount) { } @@ -72,13 +86,15 @@ internal AuthenticationResponse ( AuthenticationSchemes scheme, NameValueCollection parameters, NetworkCredential credentials, - uint nonceCount) - : base (scheme, parameters) + uint nonceCount + ) + : this (scheme, parameters) { - Parameters["username"] = credentials.UserName; - Parameters["password"] = credentials.Password; - Parameters["uri"] = credentials.Domain; + _parameters["username"] = credentials.Username; + _parameters["password"] = credentials.Password; + _parameters["uri"] = credentials.Domain; _nonceCount = nonceCount; + if (scheme == AuthenticationSchemes.Digest) initAsDigest (); } @@ -89,9 +105,13 @@ internal AuthenticationResponse ( internal uint NonceCount { get { - return _nonceCount < UInt32.MaxValue - ? _nonceCount - : 0; + return _nonceCount < UInt32.MaxValue ? _nonceCount : 0; + } + } + + internal NameValueCollection Parameters { + get { + return _parameters; } } @@ -99,39 +119,75 @@ internal uint NonceCount { #region Public Properties + public string Algorithm { + get { + return _parameters["algorithm"]; + } + } + public string Cnonce { get { - return Parameters["cnonce"]; + return _parameters["cnonce"]; } } public string Nc { get { - return Parameters["nc"]; + return _parameters["nc"]; + } + } + + public string Nonce { + get { + return _parameters["nonce"]; + } + } + + public string Opaque { + get { + return _parameters["opaque"]; } } public string Password { get { - return Parameters["password"]; + return _parameters["password"]; + } + } + + public string Qop { + get { + return _parameters["qop"]; + } + } + + public string Realm { + get { + return _parameters["realm"]; } } public string Response { get { - return Parameters["response"]; + return _parameters["response"]; + } + } + + public AuthenticationSchemes Scheme { + get { + return _scheme; } } public string Uri { get { - return Parameters["uri"]; + return _parameters["uri"]; } } public string UserName { get { - return Parameters["username"]; + return _parameters["username"]; } } @@ -139,16 +195,26 @@ public string UserName { #region Private Methods - private static string createA1 (string username, string password, string realm) + private static string createA1 ( + string username, + string password, + string realm + ) { return String.Format ("{0}:{1}:{2}", username, realm, password); } private static string createA1 ( - string username, string password, string realm, string nonce, string cnonce) + string username, + string password, + string realm, + string nonce, + string cnonce + ) { - return String.Format ( - "{0}:{1}:{2}", hash (createA1 (username, password, realm)), nonce, cnonce); + var a1 = createA1 (username, password, realm); + + return String.Format ("{0}:{1}:{2}", hash (a1), nonce, cnonce); } private static string createA2 (string method, string uri) @@ -163,33 +229,39 @@ private static string createA2 (string method, string uri, string entity) private static string hash (string value) { - var src = Encoding.UTF8.GetBytes (value); + var buff = new StringBuilder (64); + var md5 = MD5.Create (); - var hashed = md5.ComputeHash (src); + var bytes = Encoding.UTF8.GetBytes (value); + var res = md5.ComputeHash (bytes); - var res = new StringBuilder (64); - foreach (var b in hashed) - res.Append (b.ToString ("x2")); + foreach (var b in res) + buff.Append (b.ToString ("x2")); - return res.ToString (); + return buff.ToString (); } private void initAsDigest () { - var qops = Parameters["qop"]; + var qops = _parameters["qop"]; + if (qops != null) { - if (qops.Split (',').Contains (qop => qop.Trim ().ToLower () == "auth")) { - Parameters["qop"] = "auth"; - Parameters["cnonce"] = CreateNonceValue (); - Parameters["nc"] = String.Format ("{0:x8}", ++_nonceCount); + var hasAuth = qops.Split (',').Contains ( + qop => qop.Trim ().ToLower () == "auth" + ); + + if (hasAuth) { + _parameters["qop"] = "auth"; + _parameters["cnonce"] = AuthenticationChallenge.CreateNonceValue (); + _parameters["nc"] = String.Format ("{0:x8}", ++_nonceCount); } else { - Parameters["qop"] = null; + _parameters["qop"] = null; } } - Parameters["method"] = "GET"; - Parameters["response"] = CreateRequestDigest (Parameters); + _parameters["method"] = "GET"; + _parameters["response"] = CreateRequestDigest (_parameters); } #endregion @@ -198,8 +270,8 @@ private void initAsDigest () internal static string CreateRequestDigest (NameValueCollection parameters) { - var user = parameters["username"]; - var pass = parameters["password"]; + var uname = parameters["username"]; + var passwd = parameters["password"]; var realm = parameters["realm"]; var nonce = parameters["nonce"]; var uri = parameters["uri"]; @@ -210,8 +282,8 @@ internal static string CreateRequestDigest (NameValueCollection parameters) var method = parameters["method"]; var a1 = algo != null && algo.ToLower () == "md5-sess" - ? createA1 (user, pass, realm, nonce, cnonce) - : createA1 (user, pass, realm); + ? createA1 (uname, passwd, realm, nonce, cnonce) + : createA1 (uname, passwd, realm); var a2 = qop != null && qop.ToLower () == "auth-int" ? createA2 (method, uri, parameters["entity"]) @@ -219,89 +291,142 @@ internal static string CreateRequestDigest (NameValueCollection parameters) var secret = hash (a1); var data = qop != null - ? String.Format ("{0}:{1}:{2}:{3}:{4}", nonce, nc, cnonce, qop, hash (a2)) + ? String.Format ( + "{0}:{1}:{2}:{3}:{4}", + nonce, + nc, + cnonce, + qop, + hash (a2) + ) : String.Format ("{0}:{1}", nonce, hash (a2)); - return hash (String.Format ("{0}:{1}", secret, data)); + var keyed = String.Format ("{0}:{1}", secret, data); + + return hash (keyed); } internal static AuthenticationResponse Parse (string value) { try { var cred = value.Split (new[] { ' ' }, 2); + if (cred.Length != 2) return null; var schm = cred[0].ToLower (); - return schm == "basic" - ? new AuthenticationResponse ( - AuthenticationSchemes.Basic, ParseBasicCredentials (cred[1])) - : schm == "digest" - ? new AuthenticationResponse ( - AuthenticationSchemes.Digest, ParseParameters (cred[1])) - : null; + + if (schm == "basic") { + var parameters = ParseBasicCredentials (cred[1]); + + return new AuthenticationResponse ( + AuthenticationSchemes.Basic, + parameters + ); + } + + if (schm == "digest") { + var parameters = AuthenticationChallenge.ParseParameters (cred[1]); + + return new AuthenticationResponse ( + AuthenticationSchemes.Digest, + parameters + ); + } + + return null; } catch { + return null; } - - return null; } internal static NameValueCollection ParseBasicCredentials (string value) { + var ret = new NameValueCollection (); + // Decode the basic-credentials (a Base64 encoded string). - var userPass = Encoding.Default.GetString (Convert.FromBase64String (value)); + + var bytes = Convert.FromBase64String (value); + var userPass = Encoding.UTF8.GetString (bytes); // The format is [\]:. - var i = userPass.IndexOf (':'); - var user = userPass.Substring (0, i); - var pass = i < userPass.Length - 1 ? userPass.Substring (i + 1) : String.Empty; - // Check if 'domain' exists. - i = user.IndexOf ('\\'); - if (i > -1) - user = user.Substring (i + 1); + var idx = userPass.IndexOf (':'); + var uname = userPass.Substring (0, idx); + var passwd = idx < userPass.Length - 1 + ? userPass.Substring (idx + 1) + : String.Empty; - var res = new NameValueCollection (); - res["username"] = user; - res["password"] = pass; + // Check if exists. - return res; + idx = uname.IndexOf ('\\'); + + if (idx > -1) + uname = uname.Substring (idx + 1); + + ret["username"] = uname; + ret["password"] = passwd; + + return ret; } - internal override string ToBasicString () + internal string ToBasicString () { - var userPass = String.Format ("{0}:{1}", Parameters["username"], Parameters["password"]); - var cred = Convert.ToBase64String (Encoding.UTF8.GetBytes (userPass)); + var uname = _parameters["username"]; + var passwd = _parameters["password"]; + var userPass = String.Format ("{0}:{1}", uname, passwd); + + var bytes = Encoding.UTF8.GetBytes (userPass); + var cred = Convert.ToBase64String (bytes); return "Basic " + cred; } - internal override string ToDigestString () + internal string ToDigestString () { - var output = new StringBuilder (256); - output.AppendFormat ( + var buff = new StringBuilder (256); + + var uname = _parameters["username"]; + var realm = _parameters["realm"]; + var nonce = _parameters["nonce"]; + var uri = _parameters["uri"]; + var res = _parameters["response"]; + + buff.AppendFormat ( "Digest username=\"{0}\", realm=\"{1}\", nonce=\"{2}\", uri=\"{3}\", response=\"{4}\"", - Parameters["username"], - Parameters["realm"], - Parameters["nonce"], - Parameters["uri"], - Parameters["response"]); + uname, + realm, + nonce, + uri, + res + ); + + var opaque = _parameters["opaque"]; - var opaque = Parameters["opaque"]; if (opaque != null) - output.AppendFormat (", opaque=\"{0}\"", opaque); + buff.AppendFormat (", opaque=\"{0}\"", opaque); + + var algo = _parameters["algorithm"]; - var algo = Parameters["algorithm"]; if (algo != null) - output.AppendFormat (", algorithm={0}", algo); + buff.AppendFormat (", algorithm={0}", algo); + + var qop = _parameters["qop"]; - var qop = Parameters["qop"]; - if (qop != null) - output.AppendFormat ( - ", qop={0}, cnonce=\"{1}\", nc={2}", qop, Parameters["cnonce"], Parameters["nc"]); + if (qop != null) { + var cnonce = _parameters["cnonce"]; + var nc = _parameters["nc"]; - return output.ToString (); + buff.AppendFormat ( + ", qop={0}, cnonce=\"{1}\", nc={2}", + qop, + cnonce, + nc + ); + } + + return buff.ToString (); } #endregion @@ -310,12 +435,28 @@ internal override string ToDigestString () public IIdentity ToIdentity () { - var schm = Scheme; - return schm == AuthenticationSchemes.Basic - ? new HttpBasicIdentity (Parameters["username"], Parameters["password"]) as IIdentity - : schm == AuthenticationSchemes.Digest - ? new HttpDigestIdentity (Parameters) - : null; + if (_scheme == AuthenticationSchemes.Basic) { + var uname = _parameters["username"]; + var passwd = _parameters["password"]; + + return new HttpBasicIdentity (uname, passwd); + } + + if (_scheme == AuthenticationSchemes.Digest) + return new HttpDigestIdentity (_parameters); + + return null; + } + + public override string ToString () + { + if (_scheme == AuthenticationSchemes.Basic) + return ToBasicString (); + + if (_scheme == AuthenticationSchemes.Digest) + return ToDigestString (); + + return String.Empty; } #endregion diff --git a/websocket-sharp/Net/AuthenticationSchemes.cs b/websocket-sharp/Net/AuthenticationSchemes.cs index af0efb170..ab7721a15 100644 --- a/websocket-sharp/Net/AuthenticationSchemes.cs +++ b/websocket-sharp/Net/AuthenticationSchemes.cs @@ -2,13 +2,13 @@ /* * AuthenticationSchemes.cs * - * This code is derived from System.Net.AuthenticationSchemes.cs of Mono + * This code is derived from AuthenticationSchemes.cs (System.Net) of Mono * (http://www.mono-project.com). * * The MIT License * * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) - * Copyright (c) 2012-2014 sta.blockhead + * Copyright (c) 2012-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 @@ -42,25 +42,24 @@ namespace WebSocketSharp.Net { /// - /// Contains the values of the schemes for authentication. + /// Specifies the scheme for authentication. /// - [Flags] public enum AuthenticationSchemes { /// - /// Indicates that no authentication is allowed. + /// No authentication is allowed. /// None, /// - /// Indicates digest authentication. + /// Specifies digest authentication. /// Digest = 1, /// - /// Indicates basic authentication. + /// Specifies basic authentication. /// Basic = 8, /// - /// Indicates anonymous authentication. + /// Specifies anonymous authentication. /// Anonymous = 0x8000 } diff --git a/websocket-sharp/Net/Chunk.cs b/websocket-sharp/Net/Chunk.cs index 7b6268b7f..9ed28f864 100644 --- a/websocket-sharp/Net/Chunk.cs +++ b/websocket-sharp/Net/Chunk.cs @@ -8,7 +8,7 @@ * The MIT License * * Copyright (c) 2003 Ximian, Inc (http://www.ximian.com) - * Copyright (c) 2014-2015 sta.blockhead + * Copyright (c) 2014-2021 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 @@ -74,13 +74,15 @@ public int ReadLeft { public int Read (byte[] buffer, int offset, int count) { var left = _data.Length - _offset; + if (left == 0) - return left; + return 0; if (count > left) count = left; Buffer.BlockCopy (_data, _offset, buffer, offset, count); + _offset += count; return count; diff --git a/websocket-sharp/Net/ChunkStream.cs b/websocket-sharp/Net/ChunkStream.cs index a5271b573..3de4374db 100644 --- a/websocket-sharp/Net/ChunkStream.cs +++ b/websocket-sharp/Net/ChunkStream.cs @@ -8,7 +8,7 @@ * The MIT License * * Copyright (c) 2003 Ximian, Inc (http://www.ximian.com) - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2023 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 @@ -53,8 +53,11 @@ internal class ChunkStream private int _chunkRead; private int _chunkSize; private List _chunks; + private int _count; + private byte[] _endBuffer; private bool _gotIt; private WebHeaderCollection _headers; + private int _offset; private StringBuilder _saved; private bool _sawCr; private InputChunkState _state; @@ -67,24 +70,31 @@ internal class ChunkStream public ChunkStream (WebHeaderCollection headers) { _headers = headers; + _chunkSize = -1; _chunks = new List (); _saved = new StringBuilder (); } - public ChunkStream (byte[] buffer, int offset, int count, WebHeaderCollection headers) - : this (headers) - { - Write (buffer, offset, count); - } - #endregion #region Internal Properties - internal WebHeaderCollection Headers { + internal int Count { get { - return _headers; + return _count; + } + } + + internal byte[] EndBuffer { + get { + return _endBuffer; + } + } + + internal int Offset { + get { + return _offset; } } @@ -92,15 +102,15 @@ internal WebHeaderCollection Headers { #region Public Properties - public int ChunkLeft { + public WebHeaderCollection Headers { get { - return _chunkSize - _chunkRead; + return _headers; } } - public bool WantMore { + public bool WantsMore { get { - return _state != InputChunkState.End; + return _state < InputChunkState.End; } } @@ -111,19 +121,22 @@ public bool WantMore { private int read (byte[] buffer, int offset, int count) { var nread = 0; - var cnt = _chunks.Count; + for (var i = 0; i < cnt; i++) { var chunk = _chunks[i]; + if (chunk == null) continue; if (chunk.ReadLeft == 0) { _chunks[i] = null; + continue; } nread += chunk.Read (buffer, offset + nread, count - nread); + if (nread == count) break; } @@ -131,12 +144,6 @@ private int read (byte[] buffer, int offset, int count) return nread; } - private static string removeChunkExtension (string value) - { - var idx = value.IndexOf (';'); - return idx > -1 ? value.Substring (0, idx) : value; - } - private InputChunkState seekCrLf (byte[] buffer, ref int offset, int length) { if (!_sawCr) { @@ -144,6 +151,7 @@ private InputChunkState seekCrLf (byte[] buffer, ref int offset, int length) throwProtocolViolation ("CR is expected."); _sawCr = true; + if (offset == length) return InputChunkState.DataEnded; } @@ -154,11 +162,17 @@ private InputChunkState seekCrLf (byte[] buffer, ref int offset, int length) return InputChunkState.None; } - private InputChunkState setChunkSize (byte[] buffer, ref int offset, int length) + private InputChunkState setChunkSize ( + byte[] buffer, + ref int offset, + int length + ) { byte b = 0; + while (offset < length) { b = buffer[offset++]; + if (_sawCr) { if (b != 10) throwProtocolViolation ("LF is expected."); @@ -168,71 +182,77 @@ private InputChunkState setChunkSize (byte[] buffer, ref int offset, int length) if (b == 13) { _sawCr = true; + continue; } if (b == 10) throwProtocolViolation ("LF is unexpected."); - if (b == 32) // SP + if (_gotIt) + continue; + + if (b == 32 || b == 59) { // SP or ';' _gotIt = true; - if (!_gotIt) - _saved.Append ((char) b); + continue; + } - if (_saved.Length > 20) - throwProtocolViolation ("The chunk size is too long."); + _saved.Append ((char) b); } - if (!_sawCr || b != 10) + if (_saved.Length > 20) + throwProtocolViolation ("The chunk size is too big."); + + if (b != 10) return InputChunkState.None; - _chunkRead = 0; + var s = _saved.ToString (); + try { - _chunkSize = Int32.Parse ( - removeChunkExtension (_saved.ToString ()), NumberStyles.HexNumber); + _chunkSize = Int32.Parse (s, NumberStyles.HexNumber); } catch { throwProtocolViolation ("The chunk size cannot be parsed."); } + _chunkRead = 0; + if (_chunkSize == 0) { _trailerState = 2; + return InputChunkState.Trailer; } return InputChunkState.Data; } - private InputChunkState setTrailer (byte[] buffer, ref int offset, int length) + private InputChunkState setTrailer ( + byte[] buffer, + ref int offset, + int length + ) { - // Check if no trailer. - if (_trailerState == 2 && buffer[offset] == 13 && _saved.Length == 0) { - offset++; - if (offset < length && buffer[offset] == 10) { - offset++; - return InputChunkState.End; - } - - offset--; - } + while (offset < length) { + if (_trailerState == 4) // CR LF CR LF + break; - while (offset < length && _trailerState < 4) { var b = buffer[offset++]; + _saved.Append ((char) b); - if (_saved.Length > 4196) - throwProtocolViolation ("The trailer is too long."); - if (_trailerState == 1 || _trailerState == 3) { + if (_trailerState == 1 || _trailerState == 3) { // CR or CR LF CR if (b != 10) throwProtocolViolation ("LF is expected."); _trailerState++; + continue; } if (b == 13) { _trailerState++; + continue; } @@ -242,31 +262,52 @@ private InputChunkState setTrailer (byte[] buffer, ref int offset, int length) _trailerState = 0; } + var len = _saved.Length; + + if (len > 4196) + throwProtocolViolation ("The trailer is too long."); + if (_trailerState < 4) return InputChunkState.Trailer; - _saved.Length -= 2; - var reader = new StringReader (_saved.ToString ()); + if (len == 2) + return InputChunkState.End; + + _saved.Length = len - 2; + + var val = _saved.ToString (); + var reader = new StringReader (val); + + while (true) { + var line = reader.ReadLine (); + + if (line == null || line.Length == 0) + break; - string line; - while ((line = reader.ReadLine ()) != null && line.Length > 0) _headers.Add (line); + } return InputChunkState.End; } private static void throwProtocolViolation (string message) { - throw new WebException (message, null, WebExceptionStatus.ServerProtocolViolation, null); + throw new WebException ( + message, + null, + WebExceptionStatus.ServerProtocolViolation, + null + ); } - private void write (byte[] buffer, ref int offset, int length) + private void write (byte[] buffer, int offset, int length) { if (_state == InputChunkState.End) throwProtocolViolation ("The chunks were ended."); if (_state == InputChunkState.None) { _state = setChunkSize (buffer, ref offset, length); + if (_state == InputChunkState.None) return; @@ -275,64 +316,92 @@ private void write (byte[] buffer, ref int offset, int length) _gotIt = false; } - if (_state == InputChunkState.Data && offset < length) { + if (_state == InputChunkState.Data) { + if (offset >= length) + return; + _state = writeData (buffer, ref offset, length); + if (_state == InputChunkState.Data) return; } - if (_state == InputChunkState.DataEnded && offset < length) { + if (_state == InputChunkState.DataEnded) { + if (offset >= length) + return; + _state = seekCrLf (buffer, ref offset, length); + if (_state == InputChunkState.DataEnded) return; _sawCr = false; } - if (_state == InputChunkState.Trailer && offset < length) { + if (_state == InputChunkState.Trailer) { + if (offset >= length) + return; + _state = setTrailer (buffer, ref offset, length); + if (_state == InputChunkState.Trailer) return; _saved.Length = 0; } - if (offset < length) - write (buffer, ref offset, length); + if (_state == InputChunkState.End) { + _endBuffer = buffer; + _offset = offset; + _count = length - offset; + + return; + } + + if (offset >= length) + return; + + write (buffer, offset, length); } - private InputChunkState writeData (byte[] buffer, ref int offset, int length) + private InputChunkState writeData ( + byte[] buffer, + ref int offset, + int length + ) { var cnt = length - offset; var left = _chunkSize - _chunkRead; + if (cnt > left) cnt = left; var data = new byte[cnt]; + Buffer.BlockCopy (buffer, offset, data, 0, cnt); - _chunks.Add (new Chunk (data)); + + var chunk = new Chunk (data); + + _chunks.Add (chunk); offset += cnt; _chunkRead += cnt; - return _chunkRead == _chunkSize ? InputChunkState.DataEnded : InputChunkState.Data; + return _chunkRead == _chunkSize + ? InputChunkState.DataEnded + : InputChunkState.Data; } #endregion #region Internal Methods - internal void ResetBuffer () + internal void ResetChunkStore () { _chunkRead = 0; _chunkSize = -1; - _chunks.Clear (); - } - internal int WriteAndReadBack (byte[] buffer, int offset, int writeCount, int readCount) - { - Write (buffer, offset, writeCount); - return Read (buffer, offset, readCount); + _chunks.Clear (); } #endregion @@ -352,7 +421,7 @@ public void Write (byte[] buffer, int offset, int count) if (count <= 0) return; - write (buffer, ref offset, offset + count); + write (buffer, offset, offset + count); } #endregion diff --git a/websocket-sharp/Net/ChunkedRequestStream.cs b/websocket-sharp/Net/ChunkedRequestStream.cs index 913b505c3..f4a583925 100644 --- a/websocket-sharp/Net/ChunkedRequestStream.cs +++ b/websocket-sharp/Net/ChunkedRequestStream.cs @@ -8,7 +8,7 @@ * The MIT License * * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2023 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 @@ -46,7 +46,7 @@ internal class ChunkedRequestStream : RequestStream { #region Private Fields - private const int _bufferLength = 8192; + private static readonly int _bufferLength; private HttpListenerContext _context; private ChunkStream _decoder; private bool _disposed; @@ -54,27 +54,60 @@ internal class ChunkedRequestStream : RequestStream #endregion + #region Static Constructor + + static ChunkedRequestStream () + { + _bufferLength = 8192; + } + + #endregion + #region Internal Constructors internal ChunkedRequestStream ( - Stream stream, byte[] buffer, int offset, int count, HttpListenerContext context) - : base (stream, buffer, offset, count) + Stream innerStream, + byte[] initialBuffer, + int offset, + int count, + HttpListenerContext context + ) + : base (innerStream, initialBuffer, offset, count, -1) { _context = context; - _decoder = new ChunkStream ((WebHeaderCollection) context.Request.Headers); + + _decoder = new ChunkStream ( + (WebHeaderCollection) context.Request.Headers + ); } #endregion #region Internal Properties - internal ChunkStream Decoder { + internal bool HasRemainingBuffer { get { - return _decoder; + return _decoder.Count + Count > 0; } + } + + internal byte[] RemainingBuffer { + get { + using (var buff = new MemoryStream ()) { + var cnt = _decoder.Count; + + if (cnt > 0) + buff.Write (_decoder.EndBuffer, _decoder.Offset, cnt); + + cnt = Count; - set { - _decoder = value; + if (cnt > 0) + buff.Write (InitialBuffer, Offset, cnt); + + buff.Close (); + + return buff.ToArray (); + } } } @@ -86,26 +119,34 @@ private void onRead (IAsyncResult asyncResult) { var rstate = (ReadBufferState) asyncResult.AsyncState; var ares = rstate.AsyncResult; + try { var nread = base.EndRead (asyncResult); + _decoder.Write (ares.Buffer, ares.Offset, nread); + nread = _decoder.Read (rstate.Buffer, rstate.Offset, rstate.Count); + rstate.Offset += nread; rstate.Count -= nread; - if (rstate.Count == 0 || !_decoder.WantMore || nread == 0) { - _noMoreData = !_decoder.WantMore && nread == 0; + + if (rstate.Count == 0 || !_decoder.WantsMore || nread == 0) { + _noMoreData = !_decoder.WantsMore && nread == 0; + ares.Count = rstate.InitialCount - rstate.Count; + ares.Complete (); return; } - ares.Offset = 0; - ares.Count = Math.Min (_bufferLength, _decoder.ChunkLeft + 6); base.BeginRead (ares.Buffer, ares.Offset, ares.Count, onRead, rstate); } catch (Exception ex) { - _context.Connection.SendError (ex.Message, 400); + _context.ErrorMessage = "I/O operation aborted"; + + _context.SendError (); + ares.Complete (ex); } } @@ -115,45 +156,65 @@ private void onRead (IAsyncResult asyncResult) #region Public Methods public override IAsyncResult BeginRead ( - byte[] buffer, int offset, int count, AsyncCallback callback, object state) + byte[] buffer, + int offset, + int count, + AsyncCallback callback, + object state + ) { if (_disposed) - throw new ObjectDisposedException (GetType ().ToString ()); + throw new ObjectDisposedException (ObjectName); if (buffer == null) throw new ArgumentNullException ("buffer"); - if (offset < 0) - throw new ArgumentOutOfRangeException ("offset", "A negative value."); + if (offset < 0) { + var msg = "A negative value."; - if (count < 0) - throw new ArgumentOutOfRangeException ("count", "A negative value."); + throw new ArgumentOutOfRangeException ("offset", msg); + } + + if (count < 0) { + var msg = "A negative value."; + + throw new ArgumentOutOfRangeException ("count", msg); + } var len = buffer.Length; - if (offset + count > len) - throw new ArgumentException ( - "The sum of 'offset' and 'count' is greater than 'buffer' length."); + + if (offset + count > len) { + var msg = "The sum of offset and count is greater than the length of buffer."; + + throw new ArgumentException (msg); + } var ares = new HttpStreamAsyncResult (callback, state); + if (_noMoreData) { ares.Complete (); + return ares; } var nread = _decoder.Read (buffer, offset, count); + offset += nread; count -= nread; + if (count == 0) { - // Got all we wanted, no need to bother the decoder yet. ares.Count = nread; + ares.Complete (); return ares; } - if (!_decoder.WantMore) { + if (!_decoder.WantsMore) { _noMoreData = nread == 0; + ares.Count = nread; + ares.Complete (); return ares; @@ -164,7 +225,9 @@ public override IAsyncResult BeginRead ( ares.Count = _bufferLength; var rstate = new ReadBufferState (buffer, offset, count, ares); + rstate.InitialCount += nread; + base.BeginRead (ares.Buffer, ares.Offset, ares.Count, onRead, rstate); return ares; @@ -175,27 +238,35 @@ public override void Close () if (_disposed) return; - _disposed = true; base.Close (); + + _disposed = true; } public override int EndRead (IAsyncResult asyncResult) { if (_disposed) - throw new ObjectDisposedException (GetType ().ToString ()); + throw new ObjectDisposedException (ObjectName); if (asyncResult == null) throw new ArgumentNullException ("asyncResult"); var ares = asyncResult as HttpStreamAsyncResult; - if (ares == null) - throw new ArgumentException ("A wrong IAsyncResult.", "asyncResult"); + + if (ares == null) { + var msg = "A wrong IAsyncResult instance."; + + throw new ArgumentException (msg, "asyncResult"); + } if (!ares.IsCompleted) ares.AsyncWaitHandle.WaitOne (); - if (ares.HasException) - throw new HttpListenerException (400, "I/O operation aborted."); + if (ares.HasException) { + var msg = "The I/O operation has been aborted."; + + throw new HttpListenerException (995, msg); + } return ares.Count; } @@ -203,6 +274,7 @@ public override int EndRead (IAsyncResult asyncResult) public override int Read (byte[] buffer, int offset, int count) { var ares = BeginRead (buffer, offset, count, null, null); + return EndRead (ares); } diff --git a/websocket-sharp/Net/ClientSslConfiguration.cs b/websocket-sharp/Net/ClientSslConfiguration.cs index 5344164f6..33438a93c 100644 --- a/websocket-sharp/Net/ClientSslConfiguration.cs +++ b/websocket-sharp/Net/ClientSslConfiguration.cs @@ -5,7 +5,7 @@ * The MIT License * * Copyright (c) 2014 liryna - * Copyright (c) 2014 sta.blockhead + * Copyright (c) 2014-2024 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 @@ -34,6 +34,7 @@ */ #endregion +using System; using System.Net.Security; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; @@ -41,61 +42,72 @@ namespace WebSocketSharp.Net { /// - /// Stores the parameters used to configure a instance as a client. + /// Stores the parameters for an instance used by + /// a client. /// - public class ClientSslConfiguration : SslConfiguration + public class ClientSslConfiguration { #region Private Fields - private X509CertificateCollection _certs; - private string _host; + private bool _checkCertRevocation; + private LocalCertificateSelectionCallback _clientCertSelectionCallback; + private X509CertificateCollection _clientCerts; + private SslProtocols _enabledSslProtocols; + private RemoteCertificateValidationCallback _serverCertValidationCallback; + private string _targetHost; #endregion #region Public Constructors /// - /// Initializes a new instance of the class with - /// the specified . + /// Initializes a new instance of the + /// class with the specified target host name. /// /// - /// A that represents the name of the server that shares - /// a secure connection. + /// A that specifies the name of the server that + /// will share a secure connection with the client. /// + /// + /// is an empty string. + /// + /// + /// is . + /// public ClientSslConfiguration (string targetHost) - : this (targetHost, null, SslProtocols.Default, false) { + if (targetHost == null) + throw new ArgumentNullException ("targetHost"); + + if (targetHost.Length == 0) + throw new ArgumentException ("An empty string.", "targetHost"); + + _targetHost = targetHost; + + _enabledSslProtocols = SslProtocols.None; } /// - /// Initializes a new instance of the class with - /// the specified , , - /// , and . + /// Initializes a new instance of the + /// class copying from the specified configuration. /// - /// - /// A that represents the name of the server that shares - /// a secure connection. - /// - /// - /// A that contains client certificates. - /// - /// - /// The enum value that represents the protocols used for - /// authentication. + /// + /// A from which to copy. /// - /// - /// true if the certificate revocation list is checked during authentication; - /// otherwise, false. - /// - public ClientSslConfiguration ( - string targetHost, - X509CertificateCollection clientCertificates, - SslProtocols enabledSslProtocols, - bool checkCertificateRevocation) - : base (enabledSslProtocols, checkCertificateRevocation) + /// + /// is . + /// + public ClientSslConfiguration (ClientSslConfiguration configuration) { - _host = targetHost; - _certs = clientCertificates; + if (configuration == null) + throw new ArgumentNullException ("configuration"); + + _checkCertRevocation = configuration._checkCertRevocation; + _clientCertSelectionCallback = configuration._clientCertSelectionCallback; + _clientCerts = configuration._clientCerts; + _enabledSslProtocols = configuration._enabledSslProtocols; + _serverCertValidationCallback = configuration._serverCertValidationCallback; + _targetHost = configuration._targetHost; } #endregion @@ -103,80 +115,197 @@ public ClientSslConfiguration ( #region Public Properties /// - /// Gets or sets the collection that contains client certificates. + /// Gets or sets a value indicating whether the certificate revocation + /// list is checked during authentication. + /// + /// + /// + /// true if the certificate revocation list is checked during + /// authentication; otherwise, false. + /// + /// + /// The default value is false. + /// + /// + public bool CheckCertificateRevocation { + get { + return _checkCertRevocation; + } + + set { + _checkCertRevocation = value; + } + } + + /// + /// Gets or sets the collection of the certificates from which to select + /// one to supply to the server. /// /// - /// A that contains client certificates. + /// + /// A that contains + /// the certificates from which to select. + /// + /// + /// if not present. + /// + /// + /// The default value is . + /// /// public X509CertificateCollection ClientCertificates { get { - return _certs; + return _clientCerts; } set { - _certs = value; + _clientCerts = value; } } /// - /// Gets or sets the callback used to select a client certificate to supply to the server. + /// Gets or sets the callback used to select the certificate to supply to + /// the server. /// /// - /// If this callback returns , no client certificate will be supplied. + /// No certificate is supplied if the callback returns . /// /// - /// A delegate that references the method - /// used to select the client certificate. The default value is a function that only returns - /// . + /// + /// A delegate. + /// + /// + /// It represents the delegate called when the client selects + /// the certificate. + /// + /// + /// The default value invokes a method that only returns + /// . + /// /// public LocalCertificateSelectionCallback ClientCertificateSelectionCallback { get { - return CertificateSelectionCallback; + if (_clientCertSelectionCallback == null) + _clientCertSelectionCallback = defaultSelectClientCertificate; + + return _clientCertSelectionCallback; } set { - CertificateSelectionCallback = value; + _clientCertSelectionCallback = value; } } /// - /// Gets or sets the callback used to validate the certificate supplied by the server. + /// Gets or sets the enabled versions of the SSL/TLS protocols. + /// + /// + /// + /// Any of the enum values. + /// + /// + /// It represents the enabled versions of the SSL/TLS protocols. + /// + /// + /// The default value is . + /// + /// + public SslProtocols EnabledSslProtocols { + get { + return _enabledSslProtocols; + } + + set { + _enabledSslProtocols = value; + } + } + + /// + /// Gets or sets the callback used to validate the certificate supplied by + /// the server. /// /// - /// If this callback returns true, the server certificate will be valid. + /// The certificate is valid if the callback returns true. /// /// - /// A delegate that references the method - /// used to validate the server certificate. The default value is a function that only returns - /// true. + /// + /// A delegate. + /// + /// + /// It represents the delegate called when the client validates + /// the certificate. + /// + /// + /// The default value invokes a method that only returns true. + /// /// public RemoteCertificateValidationCallback ServerCertificateValidationCallback { get { - return CertificateValidationCallback; + if (_serverCertValidationCallback == null) + _serverCertValidationCallback = defaultValidateServerCertificate; + + return _serverCertValidationCallback; } set { - CertificateValidationCallback = value; + _serverCertValidationCallback = value; } } /// - /// Gets or sets the name of the server that shares a secure connection. + /// Gets or sets the target host name. /// /// - /// A that represents the name of the server that shares - /// a secure connection. + /// A that represents the name of the server that + /// will share a secure connection with the client. /// + /// + /// The value specified for a set operation is an empty string. + /// + /// + /// The value specified for a set operation is . + /// public string TargetHost { get { - return _host; + return _targetHost; } set { - _host = value; + if (value == null) + throw new ArgumentNullException ("value"); + + if (value.Length == 0) + throw new ArgumentException ("An empty string.", "value"); + + _targetHost = value; } } #endregion + + #region Private Methods + + private static X509Certificate defaultSelectClientCertificate ( + object sender, + string targetHost, + X509CertificateCollection clientCertificates, + X509Certificate serverCertificate, + string[] acceptableIssuers + ) + { + return null; + } + + private static bool defaultValidateServerCertificate ( + object sender, + X509Certificate certificate, + X509Chain chain, + SslPolicyErrors sslPolicyErrors + ) + { + return true; + } + + #endregion } } diff --git a/websocket-sharp/Net/Cookie.cs b/websocket-sharp/Net/Cookie.cs index fffc52730..149b5041e 100644 --- a/websocket-sharp/Net/Cookie.cs +++ b/websocket-sharp/Net/Cookie.cs @@ -2,13 +2,13 @@ /* * Cookie.cs * - * This code is derived from System.Net.Cookie.cs of Mono + * This code is derived from Cookie.cs (System.Net) of Mono * (http://www.mono-project.com). * * The MIT License * * Copyright (c) 2004,2009 Novell, Inc. (http://www.novell.com) - * Copyright (c) 2012-2014 sta.blockhead + * Copyright (c) 2012-2024 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 @@ -47,17 +47,37 @@ namespace WebSocketSharp.Net { /// - /// Provides a set of methods and properties used to manage an HTTP Cookie. + /// Provides a set of methods and properties used to manage an HTTP cookie. /// /// /// - /// The Cookie class supports the following cookie formats: - /// Netscape specification, - /// RFC 2109, and - /// RFC 2965 + /// This class refers to the following specifications: /// + /// + /// + /// + /// + /// Netscape specification + /// + /// + /// + /// + /// RFC 2109 + /// + /// + /// + /// + /// RFC 2965 + /// + /// + /// + /// + /// RFC 6265 + /// + /// + /// /// - /// The Cookie class cannot be inherited. + /// This class cannot be inherited. /// /// [Serializable] @@ -69,16 +89,17 @@ public sealed class Cookie private Uri _commentUri; private bool _discard; private string _domain; + private static readonly int[] _emptyPorts; private DateTime _expires; private bool _httpOnly; private string _name; private string _path; private string _port; private int[] _ports; - private static readonly char[] _reservedCharsForName; private static readonly char[] _reservedCharsForValue; + private string _sameSite; private bool _secure; - private DateTime _timestamp; + private DateTime _timeStamp; private string _value; private int _version; @@ -88,160 +109,209 @@ public sealed class Cookie static Cookie () { - _reservedCharsForName = new[] { ' ', '=', ';', ',', '\n', '\r', '\t' }; + _emptyPorts = new int[0]; _reservedCharsForValue = new[] { ';', ',' }; } #endregion - #region Public Constructors + #region Internal Constructors - /// - /// Initializes a new instance of the class. - /// - public Cookie () + internal Cookie () { - _comment = String.Empty; - _domain = String.Empty; - _expires = DateTime.MinValue; - _name = String.Empty; - _path = String.Empty; - _port = String.Empty; - _ports = new int[0]; - _timestamp = DateTime.Now; - _value = String.Empty; - _version = 0; + init (String.Empty, String.Empty, String.Empty, String.Empty); } + #endregion + + #region Public Constructors + /// - /// Initializes a new instance of the class with the specified - /// and . + /// Initializes a new instance of the class with + /// the specified name and value. /// /// - /// A that represents the Name of the cookie. + /// + /// A that specifies the name of the cookie. + /// + /// + /// The name must be a token defined in + /// + /// RFC 2616. + /// /// /// - /// A that represents the Value of the cookie. + /// A that specifies the value of the cookie. /// - /// + /// /// - /// is or empty. + /// is an empty string. /// /// - /// - or - + /// -or- /// /// - /// contains an invalid character. + /// starts with a dollar sign. /// /// - /// - or - + /// -or- /// /// - /// is . + /// contains an invalid character. /// /// - /// - or - + /// -or- /// /// - /// contains a string not enclosed in double quotes - /// that contains an invalid character. + /// is a string not enclosed in double quotes + /// although it contains a reserved character. /// /// + /// + /// is . + /// public Cookie (string name, string value) - : this () + : this (name, value, String.Empty, String.Empty) { - Name = name; - Value = value; } /// - /// Initializes a new instance of the class with the specified - /// , , and . + /// Initializes a new instance of the class with + /// the specified name, value, and path. /// /// - /// A that represents the Name of the cookie. + /// + /// A that specifies the name of the cookie. + /// + /// + /// The name must be a token defined in + /// + /// RFC 2616. + /// /// /// - /// A that represents the Value of the cookie. + /// A that specifies the value of the cookie. /// /// - /// A that represents the value of the Path attribute of the cookie. + /// A that specifies the value of the Path + /// attribute of the cookie. /// - /// + /// /// - /// is or empty. + /// is an empty string. /// /// - /// - or - + /// -or- /// /// - /// contains an invalid character. + /// starts with a dollar sign. /// /// - /// - or - + /// -or- /// /// - /// is . + /// contains an invalid character. /// /// - /// - or - + /// -or- /// /// - /// contains a string not enclosed in double quotes - /// that contains an invalid character. + /// is a string not enclosed in double quotes + /// although it contains a reserved character. /// /// + /// + /// is . + /// public Cookie (string name, string value, string path) - : this (name, value) + : this (name, value, path, String.Empty) { - Path = path; } /// - /// Initializes a new instance of the class with the specified - /// , , , and - /// . + /// Initializes a new instance of the class with + /// the specified name, value, path, and domain. /// /// - /// A that represents the Name of the cookie. + /// + /// A that specifies the name of the cookie. + /// + /// + /// The name must be a token defined in + /// + /// RFC 2616. + /// /// /// - /// A that represents the Value of the cookie. + /// A that specifies the value of the cookie. /// /// - /// A that represents the value of the Path attribute of the cookie. + /// A that specifies the value of the Path + /// attribute of the cookie. /// /// - /// A that represents the value of the Domain attribute of the cookie. + /// A that specifies the value of the Domain + /// attribute of the cookie. /// - /// + /// /// - /// is or empty. + /// is an empty string. /// /// - /// - or - + /// -or- /// /// - /// contains an invalid character. + /// starts with a dollar sign. /// /// - /// - or - + /// -or- /// /// - /// is . + /// contains an invalid character. /// /// - /// - or - + /// -or- /// /// - /// contains a string not enclosed in double quotes - /// that contains an invalid character. + /// is a string not enclosed in double quotes + /// although it contains a reserved character. /// /// + /// + /// is . + /// public Cookie (string name, string value, string path, string domain) - : this (name, value, path) { - Domain = domain; + if (name == null) + throw new ArgumentNullException ("name"); + + if (name.Length == 0) + throw new ArgumentException ("An empty string.", "name"); + + if (name[0] == '$') { + var msg = "It starts with a dollar sign."; + + throw new ArgumentException (msg, "name"); + } + + if (!name.IsToken ()) { + var msg = "It contains an invalid character."; + + throw new ArgumentException (msg, "name"); + } + + if (value == null) + value = String.Empty; + + if (value.Contains (_reservedCharsForValue)) { + if (!value.IsEnclosedIn ('"')) { + var msg = "A string not enclosed in double quotes."; + + throw new ArgumentException (msg, "value"); + } + } + + init (name, value, path ?? String.Empty, domain ?? String.Empty); } #endregion @@ -249,7 +319,9 @@ public Cookie (string name, string value, string path, string domain) #region Internal Properties internal bool ExactDomain { - get; set; + get { + return _domain.Length == 0 || _domain[0] != '.'; + } } internal int MaxAge { @@ -262,15 +334,32 @@ internal int MaxAge { : _expires; var span = expires - DateTime.Now; + return span > TimeSpan.Zero ? (int) span.TotalSeconds : 0; } + + set { + _expires = value > 0 + ? DateTime.Now.AddSeconds ((double) value) + : DateTime.Now; + } } internal int[] Ports { get { - return _ports; + return _ports ?? _emptyPorts; + } + } + + internal string SameSite { + get { + return _sameSite; + } + + set { + _sameSite = value; } } @@ -279,52 +368,74 @@ internal int[] Ports { #region Public Properties /// - /// Gets or sets the value of the Comment attribute of the cookie. + /// Gets the value of the Comment attribute of the cookie. /// /// - /// A that represents the comment to document intended use of the cookie. + /// + /// A that represents the comment to document + /// intended use of the cookie. + /// + /// + /// if not present. + /// + /// + /// The default value is . + /// /// public string Comment { get { return _comment; } - set { - _comment = value ?? String.Empty; + internal set { + _comment = value; } } /// - /// Gets or sets the value of the CommentURL attribute of the cookie. + /// Gets the value of the CommentURL attribute of the cookie. /// /// - /// A that represents the URI that provides the comment to document intended - /// use of the cookie. + /// + /// A that represents the URI that provides + /// the comment to document intended use of the cookie. + /// + /// + /// if not present. + /// + /// + /// The default value is . + /// /// public Uri CommentUri { get { return _commentUri; } - set { + internal set { _commentUri = value; } } /// - /// Gets or sets a value indicating whether the client discards the cookie unconditionally - /// when the client terminates. + /// Gets a value indicating whether the client discards the cookie + /// unconditionally when the client terminates. /// /// - /// true if the client discards the cookie unconditionally when the client terminates; - /// otherwise, false. The default value is false. + /// + /// true if the client discards the cookie unconditionally + /// when the client terminates; otherwise, false. + /// + /// + /// The default value is false. + /// /// public bool Discard { get { return _discard; } - set { + internal set { _discard = value; } } @@ -333,7 +444,13 @@ public bool Discard { /// Gets or sets the value of the Domain attribute of the cookie. /// /// - /// A that represents the URI for which the cookie is valid. + /// + /// A that represents the domain name that + /// the cookie is valid for. + /// + /// + /// An empty string if not necessary. + /// /// public string Domain { get { @@ -341,14 +458,7 @@ public string Domain { } set { - if (value.IsNullOrEmpty ()) { - _domain = String.Empty; - ExactDomain = true; - } - else { - _domain = value; - ExactDomain = value[0] != '.'; - } + _domain = value ?? String.Empty; } } @@ -356,8 +466,12 @@ public string Domain { /// Gets or sets a value indicating whether the cookie has expired. /// /// - /// true if the cookie has expired; otherwise, false. - /// The default value is false. + /// + /// true if the cookie has expired; otherwise, false. + /// + /// + /// The default value is false. + /// /// public bool Expired { get { @@ -373,8 +487,16 @@ public bool Expired { /// Gets or sets the value of the Expires attribute of the cookie. /// /// - /// A that represents the date and time at which the cookie expires. - /// The default value is . + /// + /// A that represents the date and time that + /// the cookie expires on. + /// + /// + /// if not necessary. + /// + /// + /// The default value is . + /// /// public DateTime Expires { get { @@ -387,11 +509,17 @@ public DateTime Expires { } /// - /// Gets or sets a value indicating whether non-HTTP APIs can access the cookie. + /// Gets or sets a value indicating whether non-HTTP APIs can access + /// the cookie. /// /// - /// true if non-HTTP APIs cannot access the cookie; otherwise, false. - /// The default value is false. + /// + /// true if non-HTTP APIs cannot access the cookie; otherwise, + /// false. + /// + /// + /// The default value is false. + /// /// public bool HttpOnly { get { @@ -404,31 +532,61 @@ public bool HttpOnly { } /// - /// Gets or sets the Name of the cookie. + /// Gets or sets the name of the cookie. /// /// - /// A that represents the Name of the cookie. + /// + /// A that represents the name of the cookie. + /// + /// + /// The name must be a token defined in + /// + /// RFC 2616. + /// /// - /// + /// /// - /// The value specified for a set operation is or empty. + /// The value specified for a set operation is an empty string. /// /// - /// - or - + /// -or- + /// + /// + /// The value specified for a set operation starts with a dollar sign. + /// + /// + /// -or- /// /// /// The value specified for a set operation contains an invalid character. /// /// + /// + /// The value specified for a set operation is . + /// public string Name { get { return _name; } set { - string msg; - if (!canSetName (value, out msg)) - throw new CookieException (msg); + if (value == null) + throw new ArgumentNullException ("value"); + + if (value.Length == 0) + throw new ArgumentException ("An empty string.", "value"); + + if (value[0] == '$') { + var msg = "It starts with a dollar sign."; + + throw new ArgumentException (msg, "value"); + } + + if (!value.IsToken ()) { + var msg = "It contains an invalid character."; + + throw new ArgumentException (msg, "value"); + } _name = value; } @@ -438,8 +596,8 @@ public string Name { /// Gets or sets the value of the Path attribute of the cookie. /// /// - /// A that represents the subset of URI on the origin server - /// to which the cookie applies. + /// A that represents the subset of URI on + /// the origin server that the cookie applies to. /// public string Path { get { @@ -452,52 +610,52 @@ public string Path { } /// - /// Gets or sets the value of the Port attribute of the cookie. + /// Gets the value of the Port attribute of the cookie. /// /// - /// A that represents the list of TCP ports to which the cookie applies. + /// + /// A that represents the list of TCP ports + /// that the cookie applies to. + /// + /// + /// if not present. + /// + /// + /// The default value is . + /// /// - /// - /// The value specified for a set operation isn't enclosed in double quotes or - /// couldn't be parsed. - /// public string Port { get { return _port; } - set { - if (value.IsNullOrEmpty ()) { - _port = String.Empty; - _ports = new int[0]; + internal set { + int[] ports; + if (!tryCreatePorts (value, out ports)) return; - } - - if (!value.IsEnclosedIn ('"')) - throw new CookieException ( - "The value specified for the Port attribute isn't enclosed in double quotes."); - - string err; - if (!tryCreatePorts (value, out _ports, out err)) - throw new CookieException ( - String.Format ( - "The value specified for the Port attribute contains an invalid value: {0}", err)); + _ports = ports; _port = value; } } /// - /// Gets or sets a value indicating whether the security level of the cookie is secure. + /// Gets or sets a value indicating whether the security level of + /// the cookie is secure. /// /// - /// When this property is true, the cookie may be included in the HTTP request - /// only if the request is transmitted over the HTTPS. + /// When this property is true, the cookie may be included in + /// the request only if the request is transmitted over HTTPS. /// /// - /// true if the security level of the cookie is secure; otherwise, false. - /// The default value is false. + /// + /// true if the security level of the cookie is secure; + /// otherwise, false. + /// + /// + /// The default value is false. + /// /// public bool Secure { get { @@ -513,31 +671,24 @@ public bool Secure { /// Gets the time when the cookie was issued. /// /// - /// A that represents the time when the cookie was issued. + /// A that represents the time when + /// the cookie was issued. /// public DateTime TimeStamp { get { - return _timestamp; + return _timeStamp; } } /// - /// Gets or sets the Value of the cookie. + /// Gets or sets the value of the cookie. /// /// - /// A that represents the Value of the cookie. + /// A that represents the value of the cookie. /// - /// - /// - /// The value specified for a set operation is . - /// - /// - /// - or - - /// - /// - /// The value specified for a set operation contains a string not enclosed in double quotes - /// that contains an invalid character. - /// + /// + /// The value specified for a set operation is a string not enclosed in + /// double quotes although it contains a reserved character. /// public string Value { get { @@ -545,32 +696,47 @@ public string Value { } set { - string msg; - if (!canSetValue (value, out msg)) - throw new CookieException (msg); + if (value == null) + value = String.Empty; + + if (value.Contains (_reservedCharsForValue)) { + if (!value.IsEnclosedIn ('"')) { + var msg = "A string not enclosed in double quotes."; - _value = value.Length > 0 ? value : "\"\""; + throw new ArgumentException (msg, "value"); + } + } + + _value = value; } } /// - /// Gets or sets the value of the Version attribute of the cookie. + /// Gets the value of the Version attribute of the cookie. /// /// - /// An that represents the version of the HTTP state management - /// to which the cookie conforms. + /// + /// An that represents the version of HTTP state + /// management that the cookie conforms to. + /// + /// + /// 0 or 1. + /// + /// + /// 0 if not present. + /// + /// + /// The default value is 0. + /// /// - /// - /// The value specified for a set operation isn't 0 or 1. - /// public int Version { get { return _version; } - set { + internal set { if (value < 0 || value > 1) - throw new ArgumentOutOfRangeException ("value", "Not 0 or 1."); + return; _version = value; } @@ -580,134 +746,129 @@ public int Version { #region Private Methods - private static bool canSetName (string name, out string message) + private static int hash (int i, int j, int k, int l, int m) { - if (name.IsNullOrEmpty ()) { - message = "The value specified for the Name is null or empty."; - return false; - } - - if (name[0] == '$' || name.Contains (_reservedCharsForName)) { - message = "The value specified for the Name contains an invalid character."; - return false; - } - - message = String.Empty; - return true; + return i + ^ (j << 13 | j >> 19) + ^ (k << 26 | k >> 6) + ^ (l << 7 | l >> 25) + ^ (m << 20 | m >> 12); } - private static bool canSetValue (string value, out string message) + private void init (string name, string value, string path, string domain) { - if (value == null) { - message = "The value specified for the Value is null."; - return false; - } - - if (value.Contains (_reservedCharsForValue) && !value.IsEnclosedIn ('"')) { - message = "The value specified for the Value contains an invalid character."; - return false; - } - - message = String.Empty; - return true; - } + _name = name; + _value = value; + _path = path; + _domain = domain; - private static int hash (int i, int j, int k, int l, int m) - { - return i ^ - (j << 13 | j >> 19) ^ - (k << 26 | k >> 6) ^ - (l << 7 | l >> 25) ^ - (m << 20 | m >> 12); + _expires = DateTime.MinValue; + _timeStamp = DateTime.Now; } private string toResponseStringVersion0 () { - var output = new StringBuilder (64); - output.AppendFormat ("{0}={1}", _name, _value); + var buff = new StringBuilder (64); - if (_expires != DateTime.MinValue) - output.AppendFormat ( - "; Expires={0}", - _expires.ToUniversalTime ().ToString ( - "ddd, dd'-'MMM'-'yyyy HH':'mm':'ss 'GMT'", - CultureInfo.CreateSpecificCulture ("en-US"))); + buff.AppendFormat ("{0}={1}", _name, _value); + + if (_expires != DateTime.MinValue) { + var expires = _expires + .ToUniversalTime () + .ToString ( + "ddd, dd'-'MMM'-'yyyy HH':'mm':'ss 'GMT'", + CultureInfo.CreateSpecificCulture ("en-US") + ); + + buff.AppendFormat ("; Expires={0}", expires); + } if (!_path.IsNullOrEmpty ()) - output.AppendFormat ("; Path={0}", _path); + buff.AppendFormat ("; Path={0}", _path); if (!_domain.IsNullOrEmpty ()) - output.AppendFormat ("; Domain={0}", _domain); + buff.AppendFormat ("; Domain={0}", _domain); + + if (!_sameSite.IsNullOrEmpty ()) + buff.AppendFormat ("; SameSite={0}", _sameSite); if (_secure) - output.Append ("; Secure"); + buff.Append ("; Secure"); if (_httpOnly) - output.Append ("; HttpOnly"); + buff.Append ("; HttpOnly"); - return output.ToString (); + return buff.ToString (); } private string toResponseStringVersion1 () { - var output = new StringBuilder (64); - output.AppendFormat ("{0}={1}; Version={2}", _name, _value, _version); + var buff = new StringBuilder (64); + + buff.AppendFormat ("{0}={1}; Version={2}", _name, _value, _version); if (_expires != DateTime.MinValue) - output.AppendFormat ("; Max-Age={0}", MaxAge); + buff.AppendFormat ("; Max-Age={0}", MaxAge); if (!_path.IsNullOrEmpty ()) - output.AppendFormat ("; Path={0}", _path); + buff.AppendFormat ("; Path={0}", _path); if (!_domain.IsNullOrEmpty ()) - output.AppendFormat ("; Domain={0}", _domain); + buff.AppendFormat ("; Domain={0}", _domain); - if (!_port.IsNullOrEmpty ()) { - if (_port == "\"\"") - output.Append ("; Port"); + if (_port != null) { + if (_port != "\"\"") + buff.AppendFormat ("; Port={0}", _port); else - output.AppendFormat ("; Port={0}", _port); + buff.Append ("; Port"); } - if (!_comment.IsNullOrEmpty ()) - output.AppendFormat ("; Comment={0}", _comment.UrlEncode ()); + if (_comment != null) { + var comment = HttpUtility.UrlEncode (_comment); + + buff.AppendFormat ("; Comment={0}", comment); + } if (_commentUri != null) { var url = _commentUri.OriginalString; - output.AppendFormat ("; CommentURL={0}", url.IsToken () ? url : url.Quote ()); + + buff.AppendFormat ( + "; CommentURL={0}", + !url.IsToken () ? url.Quote () : url + ); } if (_discard) - output.Append ("; Discard"); + buff.Append ("; Discard"); if (_secure) - output.Append ("; Secure"); + buff.Append ("; Secure"); - return output.ToString (); + return buff.ToString (); } - private static bool tryCreatePorts (string value, out int[] result, out string parseError) + private static bool tryCreatePorts (string value, out int[] result) { - var ports = value.Trim ('"').Split (','); - var len = ports.Length; + result = null; + + var arr = value.Trim ('"').Split (','); + var len = arr.Length; var res = new int[len]; + for (var i = 0; i < len; i++) { - res[i] = Int32.MinValue; + var s = arr[i].Trim (); - var port = ports[i].Trim (); - if (port.Length == 0) - continue; + if (s.Length == 0) { + res[i] = Int32.MinValue; - if (!Int32.TryParse (port, out res[i])) { - result = new int[0]; - parseError = port; + continue; + } + if (!Int32.TryParse (s, out res[i])) return false; - } } result = res; - parseError = String.Empty; return true; } @@ -716,7 +877,27 @@ private static bool tryCreatePorts (string value, out int[] result, out string p #region Internal Methods - // From client to server + internal bool EqualsWithoutValue (Cookie cookie) + { + var caseSensitive = StringComparison.InvariantCulture; + var caseInsensitive = StringComparison.InvariantCultureIgnoreCase; + + return _name.Equals (cookie._name, caseInsensitive) + && _path.Equals (cookie._path, caseSensitive) + && _domain.Equals (cookie._domain, caseInsensitive) + && _version == cookie._version; + } + + internal bool EqualsWithoutValueAndVersion (Cookie cookie) + { + var caseSensitive = StringComparison.InvariantCulture; + var caseInsensitive = StringComparison.InvariantCultureIgnoreCase; + + return _name.Equals (cookie._name, caseInsensitive) + && _path.Equals (cookie._path, caseSensitive) + && _domain.Equals (cookie._domain, caseInsensitive); + } + internal string ToRequestString (Uri uri) { if (_name.Length == 0) @@ -725,36 +906,59 @@ internal string ToRequestString (Uri uri) if (_version == 0) return String.Format ("{0}={1}", _name, _value); - var output = new StringBuilder (64); - output.AppendFormat ("$Version={0}; {1}={2}", _version, _name, _value); + var buff = new StringBuilder (64); + + buff.AppendFormat ("$Version={0}; {1}={2}", _version, _name, _value); if (!_path.IsNullOrEmpty ()) - output.AppendFormat ("; $Path={0}", _path); + buff.AppendFormat ("; $Path={0}", _path); else if (uri != null) - output.AppendFormat ("; $Path={0}", uri.GetAbsolutePath ()); + buff.AppendFormat ("; $Path={0}", uri.GetAbsolutePath ()); else - output.Append ("; $Path=/"); + buff.Append ("; $Path=/"); - var appendDomain = uri == null || uri.Host != _domain; - if (appendDomain && !_domain.IsNullOrEmpty ()) - output.AppendFormat ("; $Domain={0}", _domain); + if (!_domain.IsNullOrEmpty ()) { + if (uri == null || uri.Host != _domain) + buff.AppendFormat ("; $Domain={0}", _domain); + } - if (!_port.IsNullOrEmpty ()) { - if (_port == "\"\"") - output.Append ("; $Port"); + if (_port != null) { + if (_port != "\"\"") + buff.AppendFormat ("; $Port={0}", _port); else - output.AppendFormat ("; $Port={0}", _port); + buff.Append ("; $Port"); } - return output.ToString (); + return buff.ToString (); } - // From server to client internal string ToResponseString () { - return _name.Length > 0 - ? (_version == 0 ? toResponseStringVersion0 () : toResponseStringVersion1 ()) - : String.Empty; + if (_name.Length == 0) + return String.Empty; + + if (_version == 0) + return toResponseStringVersion0 (); + + return toResponseStringVersion1 (); + } + + internal static bool TryCreate ( + string name, + string value, + out Cookie result + ) + { + result = null; + + try { + result = new Cookie (name, value); + } + catch { + return false; + } + + return true; } #endregion @@ -762,58 +966,64 @@ internal string ToResponseString () #region Public Methods /// - /// Determines whether the specified is equal to the current - /// . + /// Determines whether the current cookie instance is equal to + /// the specified instance. /// /// - /// An to compare with the current . + /// + /// An instance to compare with + /// the current cookie instance. + /// + /// + /// An reference to a instance. + /// /// /// - /// true if is equal to the current ; - /// otherwise, false. + /// true if the current cookie instance is equal to + /// ; otherwise, false. /// - public override bool Equals (Object comparand) + public override bool Equals (object comparand) { var cookie = comparand as Cookie; - return cookie != null && - _name.Equals (cookie.Name, StringComparison.InvariantCultureIgnoreCase) && - _value.Equals (cookie.Value, StringComparison.InvariantCulture) && - _path.Equals (cookie.Path, StringComparison.InvariantCulture) && - _domain.Equals (cookie.Domain, StringComparison.InvariantCultureIgnoreCase) && - _version == cookie.Version; + + if (cookie == null) + return false; + + var caseSensitive = StringComparison.InvariantCulture; + var caseInsensitive = StringComparison.InvariantCultureIgnoreCase; + + return _name.Equals (cookie._name, caseInsensitive) + && _value.Equals (cookie._value, caseSensitive) + && _path.Equals (cookie._path, caseSensitive) + && _domain.Equals (cookie._domain, caseInsensitive) + && _version == cookie._version; } /// - /// Serves as a hash function for a object. + /// Gets a hash code for the current cookie instance. /// /// - /// An that represents the hash code for the current . + /// An that represents the hash code. /// public override int GetHashCode () { - return hash ( - StringComparer.InvariantCultureIgnoreCase.GetHashCode (_name), - _value.GetHashCode (), - _path.GetHashCode (), - StringComparer.InvariantCultureIgnoreCase.GetHashCode (_domain), - _version); + var i = StringComparer.InvariantCultureIgnoreCase.GetHashCode (_name); + var j = _value.GetHashCode (); + var k = _path.GetHashCode (); + var l = StringComparer.InvariantCultureIgnoreCase.GetHashCode (_domain); + var m = _version; + + return hash (i, j, k, l, m); } /// - /// Returns a that represents the current . + /// Returns a string that represents the current cookie instance. /// - /// - /// This method returns a to use to send an HTTP Cookie to - /// an origin server. - /// /// - /// A that represents the current . + /// A that is suitable for the Cookie request header. /// public override string ToString () { - // i.e., only used for clients - // See para 4.2.2 of RFC 2109 and para 3.3.4 of RFC 2965 - // See also bug #316017 return ToRequestString (null); } diff --git a/websocket-sharp/Net/CookieCollection.cs b/websocket-sharp/Net/CookieCollection.cs index a4caada92..02e065580 100644 --- a/websocket-sharp/Net/CookieCollection.cs +++ b/websocket-sharp/Net/CookieCollection.cs @@ -2,13 +2,13 @@ /* * CookieCollection.cs * - * This code is derived from System.Net.CookieCollection.cs of Mono + * This code is derived from CookieCollection.cs (System.Net) of Mono * (http://www.mono-project.com). * * The MIT License * * Copyright (c) 2004,2009 Novell, Inc. (http://www.novell.com) - * Copyright (c) 2012-2014 sta.blockhead + * Copyright (c) 2012-2025 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 @@ -48,15 +48,16 @@ namespace WebSocketSharp.Net { /// - /// Provides a collection container for instances of the class. + /// Provides a collection of instances of the class. /// [Serializable] - public class CookieCollection : ICollection, IEnumerable + public class CookieCollection : ICollection { #region Private Fields private List _list; - private object _sync; + private bool _readOnly; + private object _syncRoot; #endregion @@ -68,6 +69,7 @@ public class CookieCollection : ICollection, IEnumerable public CookieCollection () { _list = new List (); + _syncRoot = ((ICollection) _list).SyncRoot; } #endregion @@ -80,11 +82,22 @@ internal IList List { } } - internal IEnumerable Sorted { + internal bool ReadOnly { + get { + return _readOnly; + } + + set { + _readOnly = value; + } + } + + internal ICollection SortedList { get { var list = new List (_list); + if (list.Count > 1) - list.Sort (compareCookieWithinSorted); + list.Sort (compareForSortedList); return list; } @@ -98,7 +111,8 @@ internal IEnumerable Sorted { /// Gets the number of cookies in the collection. /// /// - /// An that represents the number of cookies in the collection. + /// An that represents the number of cookies in + /// the collection. /// public int Count { get { @@ -110,23 +124,31 @@ public int Count { /// Gets a value indicating whether the collection is read-only. /// /// - /// true if the collection is read-only; otherwise, false. - /// The default value is true. + /// + /// true if the collection is read-only; otherwise, false. + /// + /// + /// The default value is false. + /// /// public bool IsReadOnly { - // LAMESPEC: So how is one supposed to create a writable CookieCollection instance? - // We simply ignore this property, as this collection is always writable. get { - return true; + return _readOnly; } } /// - /// Gets a value indicating whether the access to the collection is thread safe. + /// Gets a value indicating whether the access to the collection is + /// thread safe. /// /// - /// true if the access to the collection is thread safe; otherwise, false. - /// The default value is false. + /// + /// true if the access to the collection is thread safe; + /// otherwise, false. + /// + /// + /// The default value is false. + /// /// public bool IsSynchronized { get { @@ -135,18 +157,17 @@ public bool IsSynchronized { } /// - /// Gets the at the specified from - /// the collection. + /// Gets the cookie at the specified index from the collection. /// /// - /// A at the specified in the collection. + /// A at the specified index in the collection. /// /// - /// An that represents the zero-based index of the + /// An that specifies the zero-based index of the cookie /// to find. /// /// - /// is out of allowable range of indexes for the collection. + /// is out of allowable range for the collection. /// public Cookie this[int index] { get { @@ -158,14 +179,18 @@ public Cookie this[int index] { } /// - /// Gets the with the specified from - /// the collection. + /// Gets the cookie with the specified name from the collection. /// /// - /// A with the specified in the collection. + /// + /// A with the specified name in the collection. + /// + /// + /// if not found. + /// /// /// - /// A that represents the name of the to find. + /// A that specifies the name of the cookie to find. /// /// /// is . @@ -175,9 +200,12 @@ public Cookie this[string name] { if (name == null) throw new ArgumentNullException ("name"); - foreach (var cookie in Sorted) - if (cookie.Name.Equals (name, StringComparison.InvariantCultureIgnoreCase)) + var caseInsensitive = StringComparison.InvariantCultureIgnoreCase; + + foreach (var cookie in SortedList) { + if (cookie.Name.Equals (name, caseInsensitive)) return cookie; + } return null; } @@ -187,11 +215,11 @@ public Cookie this[string name] { /// Gets an object used to synchronize access to the collection. /// /// - /// An used to synchronize access to the collection. + /// An used to synchronize access to the collection. /// - public Object SyncRoot { + public object SyncRoot { get { - return _sync ?? (_sync = ((ICollection) _list).SyncRoot); + return _syncRoot; } } @@ -199,208 +227,403 @@ public Object SyncRoot { #region Private Methods - private static int compareCookieWithinSort (Cookie x, Cookie y) + private void add (Cookie cookie) { - return (x.Name.Length + x.Value.Length) - (y.Name.Length + y.Value.Length); + var idx = search (cookie); + + if (idx == -1) { + _list.Add (cookie); + + return; + } + + _list[idx] = cookie; } - private static int compareCookieWithinSorted (Cookie x, Cookie y) + private static int compareForSort (Cookie x, Cookie y) { - var ret = 0; - return (ret = x.Version - y.Version) != 0 - ? ret - : (ret = x.Name.CompareTo (y.Name)) != 0 - ? ret - : y.Path.Length - x.Path.Length; + return (x.Name.Length + x.Value.Length) + - (y.Name.Length + y.Value.Length); + } + + private static int compareForSortedList (Cookie x, Cookie y) + { + var ret = x.Version - y.Version; + + if (ret != 0) + return ret; + + ret = x.Name.CompareTo (y.Name); + + if (ret != 0) + return ret; + + return y.Path.Length - x.Path.Length; } private static CookieCollection parseRequest (string value) { - var cookies = new CookieCollection (); + var ret = new CookieCollection (); Cookie cookie = null; var ver = 0; - var pairs = splitCookieHeaderValue (value); - for (var i = 0; i < pairs.Length; i++) { + var caseInsensitive = StringComparison.InvariantCultureIgnoreCase; + + var pairs = value.SplitHeaderValue (',', ';').ToList (); + + for (var i = 0; i < pairs.Count; i++) { var pair = pairs[i].Trim (); + if (pair.Length == 0) continue; - if (pair.StartsWith ("$version", StringComparison.InvariantCultureIgnoreCase)) { - ver = Int32.Parse (pair.GetValue ('=', true)); + var idx = pair.IndexOf ('='); + + if (idx == -1) { + if (cookie == null) + continue; + + if (pair.Equals ("$port", caseInsensitive)) { + cookie.Port = "\"\""; + + continue; + } + + continue; } - else if (pair.StartsWith ("$path", StringComparison.InvariantCultureIgnoreCase)) { - if (cookie != null) - cookie.Path = pair.GetValue ('='); + + if (idx == 0) { + if (cookie != null) { + ret.add (cookie); + + cookie = null; + } + + continue; } - else if (pair.StartsWith ("$domain", StringComparison.InvariantCultureIgnoreCase)) { - if (cookie != null) - cookie.Domain = pair.GetValue ('='); + + var name = pair.Substring (0, idx).TrimEnd (' '); + var val = idx < pair.Length - 1 + ? pair.Substring (idx + 1).TrimStart (' ') + : String.Empty; + + if (name.Equals ("$version", caseInsensitive)) { + if (val.Length == 0) + continue; + + var s = val.Unquote (); + + int num; + + if (!Int32.TryParse (s, out num)) + continue; + + ver = num; + + continue; } - else if (pair.StartsWith ("$port", StringComparison.InvariantCultureIgnoreCase)) { - var port = pair.Equals ("$port", StringComparison.InvariantCultureIgnoreCase) - ? "\"\"" - : pair.GetValue ('='); - if (cookie != null) - cookie.Port = port; + if (name.Equals ("$path", caseInsensitive)) { + if (cookie == null) + continue; + + if (val.Length == 0) + continue; + + cookie.Path = val; + + continue; } - else { - if (cookie != null) - cookies.Add (cookie); - string name; - string val = String.Empty; + if (name.Equals ("$domain", caseInsensitive)) { + if (cookie == null) + continue; - var pos = pair.IndexOf ('='); - if (pos == -1) { - name = pair; - } - else if (pos == pair.Length - 1) { - name = pair.Substring (0, pos).TrimEnd (' '); - } - else { - name = pair.Substring (0, pos).TrimEnd (' '); - val = pair.Substring (pos + 1).TrimStart (' '); - } + if (val.Length == 0) + continue; - cookie = new Cookie (name, val); - if (ver != 0) - cookie.Version = ver; + cookie.Domain = val; + + continue; } + + if (name.Equals ("$port", caseInsensitive)) { + if (cookie == null) + continue; + + if (val.Length == 0) + continue; + + cookie.Port = val; + + continue; + } + + if (cookie != null) + ret.add (cookie); + + if (!Cookie.TryCreate (name, val, out cookie)) + continue; + + if (ver != 0) + cookie.Version = ver; } if (cookie != null) - cookies.Add (cookie); + ret.add (cookie); - return cookies; + return ret; } private static CookieCollection parseResponse (string value) { - var cookies = new CookieCollection (); + var ret = new CookieCollection (); Cookie cookie = null; - var pairs = splitCookieHeaderValue (value); - for (var i = 0; i < pairs.Length; i++) { + var caseInsensitive = StringComparison.InvariantCultureIgnoreCase; + + var pairs = value.SplitHeaderValue (',', ';').ToList (); + + for (var i = 0; i < pairs.Count; i++) { var pair = pairs[i].Trim (); + if (pair.Length == 0) continue; - if (pair.StartsWith ("version", StringComparison.InvariantCultureIgnoreCase)) { - if (cookie != null) - cookie.Version = Int32.Parse (pair.GetValue ('=', true)); - } - else if (pair.StartsWith ("expires", StringComparison.InvariantCultureIgnoreCase)) { - var buff = new StringBuilder (pair.GetValue ('='), 32); - if (i < pairs.Length - 1) - buff.AppendFormat (", {0}", pairs[++i].Trim ()); + var idx = pair.IndexOf ('='); - DateTime expires; - if (!DateTime.TryParseExact ( - buff.ToString (), - new[] { "ddd, dd'-'MMM'-'yyyy HH':'mm':'ss 'GMT'", "r" }, - CultureInfo.CreateSpecificCulture ("en-US"), - DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, - out expires)) - expires = DateTime.Now; - - if (cookie != null && cookie.Expires == DateTime.MinValue) - cookie.Expires = expires.ToLocalTime (); + if (idx == -1) { + if (cookie == null) + continue; + + if (pair.Equals ("port", caseInsensitive)) { + cookie.Port = "\"\""; + + continue; + } + + if (pair.Equals ("discard", caseInsensitive)) { + cookie.Discard = true; + + continue; + } + + if (pair.Equals ("secure", caseInsensitive)) { + cookie.Secure = true; + + continue; + } + + if (pair.Equals ("httponly", caseInsensitive)) { + cookie.HttpOnly = true; + + continue; + } + + continue; } - else if (pair.StartsWith ("max-age", StringComparison.InvariantCultureIgnoreCase)) { - var max = Int32.Parse (pair.GetValue ('=', true)); - var expires = DateTime.Now.AddSeconds ((double) max); - if (cookie != null) - cookie.Expires = expires; + + if (idx == 0) { + if (cookie != null) { + ret.add (cookie); + + cookie = null; + } + + continue; } - else if (pair.StartsWith ("path", StringComparison.InvariantCultureIgnoreCase)) { - if (cookie != null) - cookie.Path = pair.GetValue ('='); + + var name = pair.Substring (0, idx).TrimEnd (' '); + var val = idx < pair.Length - 1 + ? pair.Substring (idx + 1).TrimStart (' ') + : String.Empty; + + if (name.Equals ("version", caseInsensitive)) { + if (cookie == null) + continue; + + if (val.Length == 0) + continue; + + var s = val.Unquote (); + + int num; + + if (!Int32.TryParse (s, out num)) + continue; + + cookie.Version = num; + + continue; } - else if (pair.StartsWith ("domain", StringComparison.InvariantCultureIgnoreCase)) { - if (cookie != null) - cookie.Domain = pair.GetValue ('='); + + if (name.Equals ("expires", caseInsensitive)) { + if (val.Length == 0) + continue; + + if (i == pairs.Count - 1) + break; + + i++; + + if (cookie == null) + continue; + + if (cookie.Expires != DateTime.MinValue) + continue; + + var buff = new StringBuilder (val, 32); + + buff.AppendFormat (", {0}", pairs[i].Trim ()); + + var s = buff.ToString (); + var fmts = new[] { "ddd, dd'-'MMM'-'yyyy HH':'mm':'ss 'GMT'", "r" }; + var provider = CultureInfo.CreateSpecificCulture ("en-US"); + var style = DateTimeStyles.AdjustToUniversal + | DateTimeStyles.AssumeUniversal; + + DateTime expires; + + var done = DateTime.TryParseExact ( + s, + fmts, + provider, + style, + out expires + ); + + if (!done) + continue; + + cookie.Expires = expires.ToLocalTime (); + + continue; } - else if (pair.StartsWith ("port", StringComparison.InvariantCultureIgnoreCase)) { - var port = pair.Equals ("port", StringComparison.InvariantCultureIgnoreCase) - ? "\"\"" - : pair.GetValue ('='); - if (cookie != null) - cookie.Port = port; + if (name.Equals ("max-age", caseInsensitive)) { + if (cookie == null) + continue; + + if (val.Length == 0) + continue; + + var s = val.Unquote (); + + int maxAge; + + if (!Int32.TryParse (s, out maxAge)) + continue; + + cookie.MaxAge = maxAge; + + continue; } - else if (pair.StartsWith ("comment", StringComparison.InvariantCultureIgnoreCase)) { - if (cookie != null) - cookie.Comment = pair.GetValue ('=').UrlDecode (); + + if (name.Equals ("path", caseInsensitive)) { + if (cookie == null) + continue; + + if (val.Length == 0) + continue; + + cookie.Path = val; + + continue; } - else if (pair.StartsWith ("commenturl", StringComparison.InvariantCultureIgnoreCase)) { - if (cookie != null) - cookie.CommentUri = pair.GetValue ('=', true).ToUri (); + + if (name.Equals ("domain", caseInsensitive)) { + if (cookie == null) + continue; + + if (val.Length == 0) + continue; + + cookie.Domain = val; + + continue; } - else if (pair.StartsWith ("discard", StringComparison.InvariantCultureIgnoreCase)) { - if (cookie != null) - cookie.Discard = true; + + if (name.Equals ("port", caseInsensitive)) { + if (cookie == null) + continue; + + if (val.Length == 0) + continue; + + cookie.Port = val; + + continue; } - else if (pair.StartsWith ("secure", StringComparison.InvariantCultureIgnoreCase)) { - if (cookie != null) - cookie.Secure = true; + + if (name.Equals ("comment", caseInsensitive)) { + if (cookie == null) + continue; + + if (val.Length == 0) + continue; + + cookie.Comment = urlDecode (val, Encoding.UTF8); + + continue; } - else if (pair.StartsWith ("httponly", StringComparison.InvariantCultureIgnoreCase)) { - if (cookie != null) - cookie.HttpOnly = true; + + if (name.Equals ("commenturl", caseInsensitive)) { + if (cookie == null) + continue; + + if (val.Length == 0) + continue; + + cookie.CommentUri = val.Unquote ().ToUri (); + + continue; } - else { - if (cookie != null) - cookies.Add (cookie); - string name; - string val = String.Empty; + if (name.Equals ("samesite", caseInsensitive)) { + if (cookie == null) + continue; - var pos = pair.IndexOf ('='); - if (pos == -1) { - name = pair; - } - else if (pos == pair.Length - 1) { - name = pair.Substring (0, pos).TrimEnd (' '); - } - else { - name = pair.Substring (0, pos).TrimEnd (' '); - val = pair.Substring (pos + 1).TrimStart (' '); - } + if (val.Length == 0) + continue; - cookie = new Cookie (name, val); + cookie.SameSite = val.Unquote (); + + continue; } + + if (cookie != null) + ret.add (cookie); + + Cookie.TryCreate (name, val, out cookie); } if (cookie != null) - cookies.Add (cookie); + ret.add (cookie); - return cookies; + return ret; } - private int searchCookie (Cookie cookie) + private int search (Cookie cookie) { - var name = cookie.Name; - var path = cookie.Path; - var domain = cookie.Domain; - var ver = cookie.Version; - for (var i = _list.Count - 1; i >= 0; i--) { - var c = _list[i]; - if (c.Name.Equals (name, StringComparison.InvariantCultureIgnoreCase) && - c.Path.Equals (path, StringComparison.InvariantCulture) && - c.Domain.Equals (domain, StringComparison.InvariantCultureIgnoreCase) && - c.Version == ver) + if (_list[i].EqualsWithoutValue (cookie)) return i; } return -1; } - private static string[] splitCookieHeaderValue (string value) + private static string urlDecode (string s, Encoding encoding) { - return new List (value.SplitHeaderValue (',', ';')).ToArray (); + if (s.IndexOfAny (new[] { '%', '+' }) == -1) + return s; + + try { + return HttpUtility.UrlDecode (s, encoding); + } + catch { + return null; + } } #endregion @@ -409,39 +632,48 @@ private static string[] splitCookieHeaderValue (string value) internal static CookieCollection Parse (string value, bool response) { - return response - ? parseResponse (value) - : parseRequest (value); + try { + return response ? parseResponse (value) : parseRequest (value); + } + catch (Exception ex) { + throw new CookieException ("It could not be parsed.", ex); + } } internal void SetOrRemove (Cookie cookie) { - var pos = searchCookie (cookie); - if (pos == -1) { - if (!cookie.Expired) - _list.Add (cookie); + var idx = search (cookie); + + if (idx == -1) { + if (cookie.Expired) + return; + + _list.Add (cookie); return; } - if (!cookie.Expired) { - _list[pos] = cookie; + if (cookie.Expired) { + _list.RemoveAt (idx); + return; } - _list.RemoveAt (pos); + _list[idx] = cookie; } internal void SetOrRemove (CookieCollection cookies) { - foreach (Cookie cookie in cookies) + foreach (var cookie in cookies._list) SetOrRemove (cookie); } internal void Sort () { - if (_list.Count > 1) - _list.Sort (compareCookieWithinSort); + if (_list.Count < 2) + return; + + _list.Sort (compareForSort); } #endregion @@ -449,7 +681,7 @@ internal void Sort () #region Public Methods /// - /// Adds the specified to the collection. + /// Adds the specified cookie to the collection. /// /// /// A to add. @@ -457,22 +689,25 @@ internal void Sort () /// /// is . /// - public void Add (Cookie cookie) + /// + /// This method is not available if the collection is read-only. + /// + public void Add (Cookie cookie) { - if (cookie == null) - throw new ArgumentNullException ("cookie"); + if (_readOnly) { + var msg = "The collection is read-only."; - var pos = searchCookie (cookie); - if (pos == -1) { - _list.Add (cookie); - return; + throw new InvalidOperationException (msg); } - _list[pos] = cookie; + if (cookie == null) + throw new ArgumentNullException ("cookie"); + + add (cookie); } /// - /// Adds the specified to the collection. + /// Adds the specified cookies to the collection. /// /// /// A that contains the cookies to add. @@ -480,115 +715,165 @@ public void Add (Cookie cookie) /// /// is . /// - public void Add (CookieCollection cookies) + /// + /// This method is not available if the collection is read-only. + /// + public void Add (CookieCollection cookies) { + if (_readOnly) { + var msg = "The collection is read-only."; + + throw new InvalidOperationException (msg); + } + if (cookies == null) throw new ArgumentNullException ("cookies"); - foreach (Cookie cookie in cookies) - Add (cookie); + foreach (var cookie in cookies._list) + add (cookie); } /// - /// Copies the elements of the collection to the specified , starting at - /// the specified in the . + /// Removes all cookies from the collection. + /// + /// + /// This method is not available if the collection is read-only. + /// + public void Clear () + { + if (_readOnly) { + var msg = "The collection is read-only."; + + throw new InvalidOperationException (msg); + } + + _list.Clear (); + } + + /// + /// Determines whether the collection contains the specified cookie. + /// + /// + /// true if the cookie is found in the collection; otherwise, + /// false. + /// + /// + /// A to find. + /// + /// + /// is . + /// + public bool Contains (Cookie cookie) + { + if (cookie == null) + throw new ArgumentNullException ("cookie"); + + return search (cookie) > -1; + } + + /// + /// Copies the elements of the collection to the specified array, + /// starting at the specified index. /// /// - /// An that represents the destination of the elements copied from - /// the collection. + /// An array of that specifies the destination of + /// the elements copied from the collection. /// /// - /// An that represents the zero-based index in - /// at which copying begins. + /// An that specifies the zero-based index in + /// the array at which copying starts. /// + /// + /// The space from to the end of + /// is not enough to copy to. + /// /// /// is . /// /// /// is less than zero. /// - /// - /// - /// is multidimensional. - /// - /// - /// -or- - /// - /// - /// The number of elements in the collection is greater than the available space from - /// to the end of the destination . - /// - /// - /// - /// The elements in the collection cannot be cast automatically to the type of the destination - /// . - /// - public void CopyTo (Array array, int index) + public void CopyTo (Cookie[] array, int index) { if (array == null) throw new ArgumentNullException ("array"); - if (index < 0) - throw new ArgumentOutOfRangeException ("index", "Less than zero."); + if (index < 0) { + var msg = "Less than zero."; - if (array.Rank > 1) - throw new ArgumentException ("Multidimensional.", "array"); + throw new ArgumentOutOfRangeException ("index", msg); + } - if (array.Length - index < _list.Count) - throw new ArgumentException ( - "The number of elements in this collection is greater than the available space of the destination array."); + if (array.Length - index < _list.Count) { + var msg = "The available space of the array is not enough to copy to."; - if (!array.GetType ().GetElementType ().IsAssignableFrom (typeof (Cookie))) - throw new InvalidCastException ( - "The elements in this collection cannot be cast automatically to the type of the destination array."); + throw new ArgumentException (msg); + } - ((IList) _list).CopyTo (array, index); + _list.CopyTo (array, index); } /// - /// Copies the elements of the collection to the specified array of , - /// starting at the specified in the . + /// Gets the enumerator that iterates through the collection. /// - /// - /// An array of that represents the destination of the elements - /// copied from the collection. - /// - /// - /// An that represents the zero-based index in - /// at which copying begins. + /// + /// An + /// instance that can be used to iterate through the collection. + /// + public IEnumerator GetEnumerator () + { + return _list.GetEnumerator (); + } + + /// + /// Removes the specified cookie from the collection. + /// + /// + /// true if the cookie is successfully found and removed; + /// otherwise, false. + /// + /// + /// A to remove. /// /// - /// is . - /// - /// - /// is less than zero. + /// is . /// - /// - /// The number of elements in the collection is greater than the available space from - /// to the end of the destination . + /// + /// This method is not available if the collection is read-only. /// - public void CopyTo (Cookie[] array, int index) + public bool Remove (Cookie cookie) { - if (array == null) - throw new ArgumentNullException ("array"); + if (_readOnly) { + var msg = "The collection is read-only."; + + throw new InvalidOperationException (msg); + } + + if (cookie == null) + throw new ArgumentNullException ("cookie"); - if (index < 0) - throw new ArgumentOutOfRangeException ("index", "Less than zero."); + var idx = search (cookie); - if (array.Length - index < _list.Count) - throw new ArgumentException ( - "The number of elements in this collection is greater than the available space of the destination array."); + if (idx == -1) + return false; - _list.CopyTo (array, index); + _list.RemoveAt (idx); + + return true; } + #endregion + + #region Explicit Interface Implementations + /// - /// Gets the enumerator used to iterate through the collection. + /// Gets the enumerator that iterates through the collection. /// /// - /// An instance used to iterate through the collection. + /// An instance that can be used to iterate + /// through the collection. /// - public IEnumerator GetEnumerator () + IEnumerator IEnumerable.GetEnumerator () { return _list.GetEnumerator (); } diff --git a/websocket-sharp/Net/CookieException.cs b/websocket-sharp/Net/CookieException.cs index 06123aec8..3c9ab3f9e 100644 --- a/websocket-sharp/Net/CookieException.cs +++ b/websocket-sharp/Net/CookieException.cs @@ -2,12 +2,12 @@ /* * CookieException.cs * - * This code is derived from System.Net.CookieException.cs of Mono + * This code is derived from CookieException.cs (System.Net) of Mono * (http://www.mono-project.com). * * The MIT License * - * Copyright (c) 2012-2014 sta.blockhead + * Copyright (c) 2012-2024 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 @@ -65,17 +65,24 @@ internal CookieException (string message, Exception innerException) #region Protected Constructors /// - /// Initializes a new instance of the class from - /// the specified and . + /// Initializes a new instance of the class + /// with the specified serialized data. /// /// - /// A that contains the serialized object data. + /// A that contains the serialized + /// object data. /// /// - /// A that specifies the source for the deserialization. + /// A that specifies the source for + /// the deserialization. /// + /// + /// is . + /// protected CookieException ( - SerializationInfo serializationInfo, StreamingContext streamingContext) + SerializationInfo serializationInfo, + StreamingContext streamingContext + ) : base (serializationInfo, streamingContext) { } @@ -97,19 +104,29 @@ public CookieException () #region Public Methods /// - /// Populates the specified with the data needed to serialize - /// the current . + /// Populates the specified instance with + /// the data needed to serialize the current instance. /// /// /// A that holds the serialized object data. /// /// - /// A that specifies the destination for the serialization. + /// A that specifies the destination for + /// the serialization. /// - [SecurityPermission ( - SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)] + /// + /// is . + /// + [ + SecurityPermission ( + SecurityAction.LinkDemand, + Flags = SecurityPermissionFlag.SerializationFormatter + ) + ] public override void GetObjectData ( - SerializationInfo serializationInfo, StreamingContext streamingContext) + SerializationInfo serializationInfo, + StreamingContext streamingContext + ) { base.GetObjectData (serializationInfo, streamingContext); } @@ -119,21 +136,30 @@ public override void GetObjectData ( #region Explicit Interface Implementation /// - /// Populates the specified with the data needed to serialize - /// the current . + /// Populates the specified instance with + /// the data needed to serialize the current instance. /// /// /// A that holds the serialized object data. /// /// - /// A that specifies the destination for the serialization. + /// A that specifies the destination for + /// the serialization. /// - [SecurityPermission ( - SecurityAction.LinkDemand, - Flags = SecurityPermissionFlag.SerializationFormatter, - SerializationFormatter = true)] + /// + /// is . + /// + [ + SecurityPermission ( + SecurityAction.LinkDemand, + Flags = SecurityPermissionFlag.SerializationFormatter, + SerializationFormatter = true + ) + ] void ISerializable.GetObjectData ( - SerializationInfo serializationInfo, StreamingContext streamingContext) + SerializationInfo serializationInfo, + StreamingContext streamingContext + ) { base.GetObjectData (serializationInfo, streamingContext); } diff --git a/websocket-sharp/Net/EndPointListener.cs b/websocket-sharp/Net/EndPointListener.cs index 936f81d06..3c8b3653a 100644 --- a/websocket-sharp/Net/EndPointListener.cs +++ b/websocket-sharp/Net/EndPointListener.cs @@ -8,7 +8,7 @@ * The MIT License * * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2023 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 @@ -61,16 +61,16 @@ internal sealed class EndPointListener { #region Private Fields - private List _all; // host == '+' - private static readonly string _defaultCertFolderPath; - private IPEndPoint _endpoint; - private Dictionary _prefixes; - private bool _secure; - private Socket _socket; - private ServerSslConfiguration _sslConfig; - private List _unhandled; // host == '*' - private Dictionary _unregistered; - private object _unregisteredSync; + private List _all; // host == '+' + private Dictionary _connections; + private object _connectionsSync; + private static readonly string _defaultCertFolderPath; + private IPEndPoint _endpoint; + private List _prefixes; + private bool _secure; + private Socket _socket; + private ServerSslConfiguration _sslConfig; + private List _unhandled; // host == '*' #endregion @@ -78,8 +78,9 @@ internal sealed class EndPointListener static EndPointListener () { - _defaultCertFolderPath = - Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData); + _defaultCertFolderPath = Environment.GetFolderPath ( + Environment.SpecialFolder.ApplicationData + ); } #endregion @@ -87,33 +88,52 @@ static EndPointListener () #region Internal Constructors internal EndPointListener ( - IPAddress address, - int port, - bool reuseAddress, + IPEndPoint endpoint, bool secure, string certificateFolderPath, - ServerSslConfiguration sslConfig) + ServerSslConfiguration sslConfig, + bool reuseAddress + ) { + _endpoint = endpoint; + if (secure) { - var cert = getCertificate (port, certificateFolderPath, sslConfig.ServerCertificate); - if (cert == null) - throw new ArgumentException ("No server certificate could be found."); + var cert = getCertificate ( + endpoint.Port, + certificateFolderPath, + sslConfig.ServerCertificate + ); + + if (cert == null) { + var msg = "No server certificate could be found."; + + throw new ArgumentException (msg); + } - _secure = secure; - _sslConfig = sslConfig; + _secure = true; + _sslConfig = new ServerSslConfiguration (sslConfig); _sslConfig.ServerCertificate = cert; } - _prefixes = new Dictionary (); - _unregistered = new Dictionary (); - _unregisteredSync = ((ICollection) _unregistered).SyncRoot; - - _socket = new Socket (address.AddressFamily, SocketType.Stream, ProtocolType.Tcp); - if (reuseAddress) - _socket.SetSocketOption (SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + _prefixes = new List (); + _connections = new Dictionary (); + _connectionsSync = ((ICollection) _connections).SyncRoot; + + _socket = new Socket ( + endpoint.Address.AddressFamily, + SocketType.Stream, + ProtocolType.Tcp + ); + + if (reuseAddress) { + _socket.SetSocketOption ( + SocketOptionLevel.Socket, + SocketOptionName.ReuseAddress, + true + ); + } - _endpoint = new IPEndPoint (address, port); - _socket.Bind (_endpoint); + _socket.Bind (endpoint); _socket.Listen (500); _socket.BeginAccept (onAccept, this); } @@ -150,90 +170,100 @@ public ServerSslConfiguration SslConfiguration { #region Private Methods - private static void addSpecial (List prefixes, HttpListenerPrefix prefix) + private static void addSpecial ( + List prefixes, + HttpListenerPrefix prefix + ) { var path = prefix.Path; - foreach (var pref in prefixes) - if (pref.Path == path) - throw new HttpListenerException (400, "The prefix is already in use."); // TODO: Code? + + foreach (var pref in prefixes) { + if (pref.Path == path) { + var msg = "The prefix is already in use."; + + throw new HttpListenerException (87, msg); + } + } prefixes.Add (prefix); } - private void checkIfRemove () + private void clearConnections () { - if (_prefixes.Count > 0) - return; + HttpConnection[] conns = null; - var list = _unhandled; - if (list != null && list.Count > 0) - return; + lock (_connectionsSync) { + var cnt = _connections.Count; - list = _all; - if (list != null && list.Count > 0) - return; + if (cnt == 0) + return; - EndPointManager.RemoveEndPoint (this); - } + conns = new HttpConnection[cnt]; - private static RSACryptoServiceProvider createRSAFromFile (string filename) - { - byte[] pvk = null; - using (var fs = File.Open (filename, FileMode.Open, FileAccess.Read, FileShare.Read)) { - pvk = new byte[fs.Length]; - fs.Read (pvk, 0, pvk.Length); + _connections.Values.CopyTo (conns, 0); + _connections.Clear (); } + foreach (var conn in conns) + conn.Close (true); + } + + private static RSACryptoServiceProvider createRSAFromFile (string path) + { var rsa = new RSACryptoServiceProvider (); - rsa.ImportCspBlob (pvk); + + var key = File.ReadAllBytes (path); + + rsa.ImportCspBlob (key); return rsa; } private static X509Certificate2 getCertificate ( - int port, string certificateFolderPath, X509Certificate2 defaultCertificate) + int port, + string folderPath, + X509Certificate2 defaultCertificate + ) { - if (certificateFolderPath == null || certificateFolderPath.Length == 0) - certificateFolderPath = _defaultCertFolderPath; + if (folderPath == null || folderPath.Length == 0) + folderPath = _defaultCertFolderPath; try { - var cer = Path.Combine (certificateFolderPath, String.Format ("{0}.cer", port)); - var key = Path.Combine (certificateFolderPath, String.Format ("{0}.key", port)); - if (File.Exists (cer) && File.Exists (key)) { - var cert = new X509Certificate2 (cer); - cert.PrivateKey = createRSAFromFile (key); + var cer = Path.Combine (folderPath, String.Format ("{0}.cer", port)); + var key = Path.Combine (folderPath, String.Format ("{0}.key", port)); - return cert; - } + var exists = File.Exists (cer) && File.Exists (key); + + if (!exists) + return defaultCertificate; + + var cert = new X509Certificate2 (cer); + + cert.PrivateKey = createRSAFromFile (key); + + return cert; } catch { + return defaultCertificate; } - - return defaultCertificate; } - private static HttpListener matchFromList ( - string host, string path, List list, out HttpListenerPrefix prefix) + private void leaveIfNoPrefix () { - prefix = null; - if (list == null) - return null; + if (_prefixes.Count > 0) + return; - HttpListener bestMatch = null; - var bestLen = -1; - foreach (var pref in list) { - var ppath = pref.Path; - if (ppath.Length < bestLen) - continue; + var prefs = _unhandled; - if (path.StartsWith (ppath)) { - bestLen = ppath.Length; - bestMatch = pref.Listener; - prefix = pref; - } - } + if (prefs != null && prefs.Count > 0) + return; - return bestMatch; + prefs = _all; + + if (prefs != null && prefs.Count > 0) + return; + + Close (); } private static void onAccept (IAsyncResult asyncResult) @@ -241,47 +271,71 @@ private static void onAccept (IAsyncResult asyncResult) var lsnr = (EndPointListener) asyncResult.AsyncState; Socket sock = null; + try { sock = lsnr._socket.EndAccept (asyncResult); + } + catch (ObjectDisposedException) { + return; + } + catch (Exception) { + // TODO: Logging. + } + + try { lsnr._socket.BeginAccept (onAccept, lsnr); } - catch { + catch (Exception) { + // TODO: Logging. + if (sock != null) sock.Close (); return; } + if (sock == null) + return; + processAccepted (sock, lsnr); } - private static void processAccepted (Socket socket, EndPointListener listener) + private static void processAccepted ( + Socket socket, + EndPointListener listener + ) { HttpConnection conn = null; + try { conn = new HttpConnection (socket, listener); - lock (listener._unregisteredSync) - listener._unregistered[conn] = conn; - - conn.BeginReadRequest (); } - catch { - if (conn != null) { - conn.Close (true); - return; - } + catch (Exception) { + // TODO: Logging. socket.Close (); + + return; } + + lock (listener._connectionsSync) + listener._connections.Add (conn, conn); + + conn.BeginReadRequest (); } - private static bool removeSpecial (List prefixes, HttpListenerPrefix prefix) + private static bool removeSpecial ( + List prefixes, + HttpListenerPrefix prefix + ) { var path = prefix.Path; var cnt = prefixes.Count; + for (var i = 0; i < cnt; i++) { if (prefixes[i].Path == path) { prefixes.RemoveAt (i); + return true; } } @@ -289,93 +343,128 @@ private static bool removeSpecial (List prefixes, HttpListen return false; } - private HttpListener searchListener (Uri uri, out HttpListenerPrefix prefix) + private static HttpListener searchHttpListenerFromSpecial ( + string path, + List prefixes + ) { - prefix = null; - if (uri == null) + if (prefixes == null) return null; - var host = uri.Host; - var dns = Uri.CheckHostName (host) == UriHostNameType.Dns; - var port = uri.Port; - var path = HttpUtility.UrlDecode (uri.AbsolutePath); - var pathSlash = path[path.Length - 1] == '/' ? path : path + "/"; + HttpListener ret = null; - HttpListener bestMatch = null; var bestLen = -1; - if (host != null && host.Length > 0) { - foreach (var pref in _prefixes.Keys) { - var ppath = pref.Path; - if (ppath.Length < bestLen) - continue; - if (pref.Port != port) - continue; + foreach (var pref in prefixes) { + var prefPath = pref.Path; + var len = prefPath.Length; - if (dns) { - var phost = pref.Host; - if (Uri.CheckHostName (phost) == UriHostNameType.Dns && phost != host) - continue; - } - - if (path.StartsWith (ppath) || pathSlash.StartsWith (ppath)) { - bestLen = ppath.Length; - bestMatch = _prefixes[pref]; - prefix = pref; - } - } - - if (bestLen != -1) - return bestMatch; - } - - var list = _unhandled; - bestMatch = matchFromList (host, path, list, out prefix); - if (path != pathSlash && bestMatch == null) - bestMatch = matchFromList (host, pathSlash, list, out prefix); + if (len < bestLen) + continue; - if (bestMatch != null) - return bestMatch; + var match = path.StartsWith (prefPath, StringComparison.Ordinal); - list = _all; - bestMatch = matchFromList (host, path, list, out prefix); - if (path != pathSlash && bestMatch == null) - bestMatch = matchFromList (host, pathSlash, list, out prefix); + if (!match) + continue; - if (bestMatch != null) - return bestMatch; + bestLen = len; + ret = pref.Listener; + } - return null; + return ret; } #endregion #region Internal Methods - internal static bool CertificateExists (int port, string certificateFolderPath) + internal static bool CertificateExists (int port, string folderPath) { - if (certificateFolderPath == null || certificateFolderPath.Length == 0) - certificateFolderPath = _defaultCertFolderPath; + if (folderPath == null || folderPath.Length == 0) + folderPath = _defaultCertFolderPath; - var cer = Path.Combine (certificateFolderPath, String.Format ("{0}.cer", port)); - var key = Path.Combine (certificateFolderPath, String.Format ("{0}.key", port)); + var cer = Path.Combine (folderPath, String.Format ("{0}.cer", port)); + var key = Path.Combine (folderPath, String.Format ("{0}.key", port)); return File.Exists (cer) && File.Exists (key); } internal void RemoveConnection (HttpConnection connection) { - lock (_unregisteredSync) - _unregistered.Remove (connection); + lock (_connectionsSync) + _connections.Remove (connection); + } + + internal bool TrySearchHttpListener (Uri uri, out HttpListener listener) + { + listener = null; + + if (uri == null) + return false; + + var host = uri.Host; + var dns = Uri.CheckHostName (host) == UriHostNameType.Dns; + var port = uri.Port.ToString (); + var path = HttpUtility.UrlDecode (uri.AbsolutePath); + + if (path[path.Length - 1] != '/') + path += "/"; + + if (host != null && host.Length > 0) { + var prefs = _prefixes; + var bestLen = -1; + + foreach (var pref in prefs) { + if (dns) { + var prefHost = pref.Host; + var prefDns = Uri.CheckHostName (prefHost) == UriHostNameType.Dns; + + if (prefDns) { + if (prefHost != host) + continue; + } + } + + if (pref.Port != port) + continue; + + var prefPath = pref.Path; + var len = prefPath.Length; + + if (len < bestLen) + continue; + + var match = path.StartsWith (prefPath, StringComparison.Ordinal); + + if (!match) + continue; + + bestLen = len; + listener = pref.Listener; + } + + if (bestLen != -1) + return true; + } + + listener = searchHttpListenerFromSpecial (path, _unhandled); + + if (listener != null) + return true; + + listener = searchHttpListenerFromSpecial (path, _all); + + return listener != null; } #endregion #region Public Methods - public void AddPrefix (HttpListenerPrefix prefix, HttpListener listener) + public void AddPrefix (HttpListenerPrefix prefix) { List current, future; + if (prefix.Host == "*") { do { current = _unhandled; @@ -383,10 +472,12 @@ public void AddPrefix (HttpListenerPrefix prefix, HttpListener listener) ? new List (current) : new List (); - prefix.Listener = listener; addSpecial (future, prefix); } - while (Interlocked.CompareExchange (ref _unhandled, future, current) != current); + while ( + Interlocked.CompareExchange (ref _unhandled, future, current) + != current + ); return; } @@ -398,113 +489,114 @@ public void AddPrefix (HttpListenerPrefix prefix, HttpListener listener) ? new List (current) : new List (); - prefix.Listener = listener; addSpecial (future, prefix); } - while (Interlocked.CompareExchange (ref _all, future, current) != current); + while ( + Interlocked.CompareExchange (ref _all, future, current) + != current + ); return; } - Dictionary prefs, prefs2; do { - prefs = _prefixes; - if (prefs.ContainsKey (prefix)) { - if (prefs[prefix] != listener) - throw new HttpListenerException ( - 400, String.Format ("There's another listener for {0}.", prefix)); // TODO: Code? + current = _prefixes; - return; - } + var idx = current.IndexOf (prefix); - prefs2 = new Dictionary (prefs); - prefs2[prefix] = listener; - } - while (Interlocked.CompareExchange (ref _prefixes, prefs2, prefs) != prefs); - } + if (idx > -1) { + if (current[idx].Listener != prefix.Listener) { + var fmt = "There is another listener for {0}."; + var msg = String.Format (fmt, prefix); - public bool BindContext (HttpListenerContext context) - { - HttpListenerPrefix pref; - var lsnr = searchListener (context.Request.Url, out pref); - if (lsnr == null) - return false; + throw new HttpListenerException (87, msg); + } - context.Listener = lsnr; - context.Connection.Prefix = pref; + return; + } - return true; + future = new List (current); + + future.Add (prefix); + } + while ( + Interlocked.CompareExchange (ref _prefixes, future, current) + != current + ); } public void Close () { _socket.Close (); - lock (_unregisteredSync) { - var conns = new List (_unregistered.Keys); - _unregistered.Clear (); - foreach (var conn in conns) - conn.Close (true); - - conns.Clear (); - } + clearConnections (); + EndPointManager.RemoveEndPoint (_endpoint); } - public void RemovePrefix (HttpListenerPrefix prefix, HttpListener listener) + public void RemovePrefix (HttpListenerPrefix prefix) { List current, future; + if (prefix.Host == "*") { do { current = _unhandled; + if (current == null) break; future = new List (current); + if (!removeSpecial (future, prefix)) - break; // The prefix wasn't found. + break; } - while (Interlocked.CompareExchange (ref _unhandled, future, current) != current); + while ( + Interlocked.CompareExchange (ref _unhandled, future, current) + != current + ); + + leaveIfNoPrefix (); - checkIfRemove (); return; } if (prefix.Host == "+") { do { current = _all; + if (current == null) break; future = new List (current); + if (!removeSpecial (future, prefix)) - break; // The prefix wasn't found. + break; } - while (Interlocked.CompareExchange (ref _all, future, current) != current); + while ( + Interlocked.CompareExchange (ref _all, future, current) + != current + ); + + leaveIfNoPrefix (); - checkIfRemove (); return; } - Dictionary prefs, prefs2; do { - prefs = _prefixes; - if (!prefs.ContainsKey (prefix)) + current = _prefixes; + + if (!current.Contains (prefix)) break; - prefs2 = new Dictionary (prefs); - prefs2.Remove (prefix); - } - while (Interlocked.CompareExchange (ref _prefixes, prefs2, prefs) != prefs); + future = new List (current); - checkIfRemove (); - } - - public void UnbindContext (HttpListenerContext context) - { - if (context == null || context.Listener == null) - return; + future.Remove (prefix); + } + while ( + Interlocked.CompareExchange (ref _prefixes, future, current) + != current + ); - context.Listener.UnregisterContext (context); + leaveIfNoPrefix (); } #endregion diff --git a/websocket-sharp/Net/EndPointManager.cs b/websocket-sharp/Net/EndPointManager.cs index 11fada49a..ac4582b24 100644 --- a/websocket-sharp/Net/EndPointManager.cs +++ b/websocket-sharp/Net/EndPointManager.cs @@ -8,7 +8,7 @@ * The MIT License * * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2020 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 @@ -55,8 +55,7 @@ internal sealed class EndPointManager { #region Private Fields - private static readonly Dictionary> - _addressToEndpoints; + private static readonly Dictionary _endpoints; #endregion @@ -64,7 +63,7 @@ private static readonly Dictionary> static EndPointManager () { - _addressToEndpoints = new Dictionary> (); + _endpoints = new Dictionary (); } #endregion @@ -81,101 +80,136 @@ private EndPointManager () private static void addPrefix (string uriPrefix, HttpListener listener) { - var pref = new HttpListenerPrefix (uriPrefix); + var pref = new HttpListenerPrefix (uriPrefix, listener); - var path = pref.Path; - if (path.IndexOf ('%') != -1) - throw new HttpListenerException (400, "Invalid path."); // TODO: Code? + var addr = convertToIPAddress (pref.Host); - if (path.IndexOf ("//", StringComparison.Ordinal) != -1) - throw new HttpListenerException (400, "Invalid path."); // TODO: Code? + if (addr == null) { + var msg = "The URI prefix includes an invalid host."; - // Listens on all the interfaces if host name cannot be parsed by IPAddress. - getEndPointListener (pref, listener).AddPrefix (pref, listener); - } + throw new HttpListenerException (87, msg); + } - private static IPAddress convertToIPAddress (string hostname) - { - if (hostname == "*" || hostname == "+") - return IPAddress.Any; + if (!addr.IsLocal ()) { + var msg = "The URI prefix includes an invalid host."; + + throw new HttpListenerException (87, msg); + } - IPAddress addr; - if (IPAddress.TryParse (hostname, out addr)) - return addr; + int port; - try { - var host = Dns.GetHostEntry (hostname); - return host != null ? host.AddressList[0] : IPAddress.Any; + if (!Int32.TryParse (pref.Port, out port)) { + var msg = "The URI prefix includes an invalid port."; + + throw new HttpListenerException (87, msg); } - catch { - return IPAddress.Any; + + if (!port.IsPortNumber ()) { + var msg = "The URI prefix includes an invalid port."; + + throw new HttpListenerException (87, msg); } - } - private static EndPointListener getEndPointListener ( - HttpListenerPrefix prefix, HttpListener listener) - { - var addr = convertToIPAddress (prefix.Host); + var path = pref.Path; - Dictionary eps = null; - if (_addressToEndpoints.ContainsKey (addr)) { - eps = _addressToEndpoints[addr]; + if (path.IndexOf ('%') != -1) { + var msg = "The URI prefix includes an invalid path."; + + throw new HttpListenerException (87, msg); } - else { - eps = new Dictionary (); - _addressToEndpoints[addr] = eps; + + if (path.IndexOf ("//", StringComparison.Ordinal) != -1) { + var msg = "The URI prefix includes an invalid path."; + + throw new HttpListenerException (87, msg); } - var port = prefix.Port; + var endpoint = new IPEndPoint (addr, port); + + EndPointListener lsnr; + + if (_endpoints.TryGetValue (endpoint, out lsnr)) { + if (lsnr.IsSecure ^ pref.IsSecure) { + var msg = "The URI prefix includes an invalid scheme."; - EndPointListener lsnr = null; - if (eps.ContainsKey (port)) { - lsnr = eps[port]; + throw new HttpListenerException (87, msg); + } } else { lsnr = new EndPointListener ( - addr, - port, - listener.ReuseAddress, - prefix.IsSecure, - listener.CertificateFolderPath, - listener.SslConfiguration); - - eps[port] = lsnr; + endpoint, + pref.IsSecure, + listener.CertificateFolderPath, + listener.SslConfiguration, + listener.ReuseAddress + ); + + _endpoints.Add (endpoint, lsnr); } - return lsnr; + lsnr.AddPrefix (pref); + } + + private static IPAddress convertToIPAddress (string hostname) + { + if (hostname == "*") + return IPAddress.Any; + + if (hostname == "+") + return IPAddress.Any; + + return hostname.ToIPAddress (); } private static void removePrefix (string uriPrefix, HttpListener listener) { - var pref = new HttpListenerPrefix (uriPrefix); + var pref = new HttpListenerPrefix (uriPrefix, listener); + + var addr = convertToIPAddress (pref.Host); + + if (addr == null) + return; + + if (!addr.IsLocal ()) + return; + + int port; + + if (!Int32.TryParse (pref.Port, out port)) + return; + + if (!port.IsPortNumber ()) + return; var path = pref.Path; + if (path.IndexOf ('%') != -1) return; if (path.IndexOf ("//", StringComparison.Ordinal) != -1) return; - getEndPointListener (pref, listener).RemovePrefix (pref, listener); + var endpoint = new IPEndPoint (addr, port); + + EndPointListener lsnr; + + if (!_endpoints.TryGetValue (endpoint, out lsnr)) + return; + + if (lsnr.IsSecure ^ pref.IsSecure) + return; + + lsnr.RemovePrefix (pref); } #endregion #region Internal Methods - internal static void RemoveEndPoint (EndPointListener listener) + internal static bool RemoveEndPoint (IPEndPoint endpoint) { - lock (((ICollection) _addressToEndpoints).SyncRoot) { - var addr = listener.Address; - var eps = _addressToEndpoints[addr]; - eps.Remove (listener.Port); - if (eps.Count == 0) - _addressToEndpoints.Remove (addr); - - listener.Close (); - } + lock (((ICollection) _endpoints).SyncRoot) + return _endpoints.Remove (endpoint); } #endregion @@ -185,7 +219,8 @@ internal static void RemoveEndPoint (EndPointListener listener) public static void AddListener (HttpListener listener) { var added = new List (); - lock (((ICollection) _addressToEndpoints).SyncRoot) { + + lock (((ICollection) _endpoints).SyncRoot) { try { foreach (var pref in listener.Prefixes) { addPrefix (pref, listener); @@ -203,20 +238,21 @@ public static void AddListener (HttpListener listener) public static void AddPrefix (string uriPrefix, HttpListener listener) { - lock (((ICollection) _addressToEndpoints).SyncRoot) + lock (((ICollection) _endpoints).SyncRoot) addPrefix (uriPrefix, listener); } public static void RemoveListener (HttpListener listener) { - lock (((ICollection) _addressToEndpoints).SyncRoot) + lock (((ICollection) _endpoints).SyncRoot) { foreach (var pref in listener.Prefixes) removePrefix (pref, listener); + } } public static void RemovePrefix (string uriPrefix, HttpListener listener) { - lock (((ICollection) _addressToEndpoints).SyncRoot) + lock (((ICollection) _endpoints).SyncRoot) removePrefix (uriPrefix, listener); } diff --git a/websocket-sharp/Net/HttpBasicIdentity.cs b/websocket-sharp/Net/HttpBasicIdentity.cs index d55cd9f2b..d26b29f69 100644 --- a/websocket-sharp/Net/HttpBasicIdentity.cs +++ b/websocket-sharp/Net/HttpBasicIdentity.cs @@ -2,13 +2,13 @@ /* * HttpBasicIdentity.cs * - * This code is derived from System.Net.HttpListenerBasicIdentity.cs of Mono - * (http://www.mono-project.com). + * This code is derived from HttpListenerBasicIdentity.cs (System.Net) of + * Mono (http://www.mono-project.com). * * The MIT License * * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) - * Copyright (c) 2014 sta.blockhead + * Copyright (c) 2014-2017 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 @@ -43,7 +43,7 @@ namespace WebSocketSharp.Net { /// - /// Holds the user name and password from the HTTP Basic authentication credentials. + /// Holds the username and password from an HTTP Basic authentication attempt. /// public class HttpBasicIdentity : GenericIdentity { @@ -53,7 +53,7 @@ public class HttpBasicIdentity : GenericIdentity #endregion - #region internal Constructors + #region Internal Constructors internal HttpBasicIdentity (string username, string password) : base (username, "Basic") @@ -66,7 +66,7 @@ internal HttpBasicIdentity (string username, string password) #region Public Properties /// - /// Gets the password from the HTTP Basic authentication credentials. + /// Gets the password from a basic authentication attempt. /// /// /// A that represents the password. diff --git a/websocket-sharp/Net/HttpConnection.cs b/websocket-sharp/Net/HttpConnection.cs index 3527731ed..e51efb11c 100644 --- a/websocket-sharp/Net/HttpConnection.cs +++ b/websocket-sharp/Net/HttpConnection.cs @@ -8,7 +8,7 @@ * The MIT License * * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2025 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 @@ -46,6 +46,7 @@ #endregion using System; +using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Security; @@ -59,27 +60,39 @@ internal sealed class HttpConnection { #region Private Fields - private byte[] _buffer; - private const int _bufferLength = 8192; - private HttpListenerContext _context; - private bool _contextBound; - private StringBuilder _currentLine; - private InputState _inputState; - private RequestStream _inputStream; - private HttpListener _lastListener; - private LineState _lineState; - private EndPointListener _listener; - private ResponseStream _outputStream; - private int _position; - private HttpListenerPrefix _prefix; - private MemoryStream _requestBuffer; - private int _reuses; - private bool _secure; - private Socket _socket; - private Stream _stream; - private object _sync; - private int _timeout; - private Timer _timer; + private int _attempts; + private byte[] _buffer; + private static readonly int _bufferLength; + private HttpListenerContext _context; + private StringBuilder _currentLine; + private EndPointListener _endPointListener; + private InputState _inputState; + private RequestStream _inputStream; + private bool _isSecure; + private LineState _lineState; + private EndPoint _localEndPoint; + private static readonly int _maxInputLength; + private ResponseStream _outputStream; + private int _position; + private EndPoint _remoteEndPoint; + private MemoryStream _requestBuffer; + private int _reuses; + private Socket _socket; + private Stream _stream; + private object _sync; + private int _timeout; + private Dictionary _timeoutCanceled; + private Timer _timer; + + #endregion + + #region Static Constructor + + static HttpConnection () + { + _bufferLength = 8192; + _maxInputLength = 32768; + } #endregion @@ -88,30 +101,41 @@ internal sealed class HttpConnection internal HttpConnection (Socket socket, EndPointListener listener) { _socket = socket; - _listener = listener; - _secure = listener.IsSecure; + _endPointListener = listener; var netStream = new NetworkStream (socket, false); - if (_secure) { - var conf = listener.SslConfiguration; - var sslStream = new SslStream (netStream, false, conf.ClientCertificateValidationCallback); + + if (listener.IsSecure) { + var sslConf = listener.SslConfiguration; + var sslStream = new SslStream ( + netStream, + false, + sslConf.ClientCertificateValidationCallback + ); + sslStream.AuthenticateAsServer ( - conf.ServerCertificate, - conf.ClientCertificateRequired, - conf.EnabledSslProtocols, - conf.CheckCertificateRevocation); + sslConf.ServerCertificate, + sslConf.ClientCertificateRequired, + sslConf.EnabledSslProtocols, + sslConf.CheckCertificateRevocation + ); + _isSecure = true; _stream = sslStream; } else { _stream = netStream; } + _buffer = new byte[_bufferLength]; + _localEndPoint = socket.LocalEndPoint; + _remoteEndPoint = socket.RemoteEndPoint; _sync = new object (); - _timeout = 90000; // 90k ms for first request, 15k ms from then on. + _timeoutCanceled = new Dictionary (); _timer = new Timer (onTimeout, this, Timeout.Infinite, Timeout.Infinite); - init (); + // 90k ms for first request, 15k ms from then on. + init (new MemoryStream (), 90000); } #endregion @@ -124,31 +148,27 @@ public bool IsClosed { } } - public bool IsSecure { + public bool IsLocal { get { - return _secure; + return ((IPEndPoint) _remoteEndPoint).Address.IsLocal (); } } - public IPEndPoint LocalEndPoint { + public bool IsSecure { get { - return (IPEndPoint) _socket.LocalEndPoint; + return _isSecure; } } - public HttpListenerPrefix Prefix { + public IPEndPoint LocalEndPoint { get { - return _prefix; - } - - set { - _prefix = value; + return (IPEndPoint) _localEndPoint; } } public IPEndPoint RemoteEndPoint { get { - return (IPEndPoint) _socket.RemoteEndPoint; + return (IPEndPoint) _remoteEndPoint; } } @@ -158,6 +178,12 @@ public int Reuses { } } + public Socket Socket { + get { + return _socket; + } + } + public Stream Stream { get { return _stream; @@ -180,8 +206,8 @@ private void close () closeSocket (); } - unbind (); - removeConnection (); + _context.Unregister (); + _endPointListener.RemoveConnection (this); } private void closeSocket () @@ -193,15 +219,43 @@ private void closeSocket () } _socket.Close (); + _socket = null; } + private static MemoryStream createRequestBuffer ( + RequestStream inputStream + ) + { + var ret = new MemoryStream (); + + if (inputStream is ChunkedRequestStream) { + var crs = (ChunkedRequestStream) inputStream; + + if (crs.HasRemainingBuffer) { + var buff = crs.RemainingBuffer; + + ret.Write (buff, 0, buff.Length); + } + + return ret; + } + + var cnt = inputStream.Count; + + if (cnt > 0) + ret.Write (inputStream.InitialBuffer, inputStream.Offset, cnt); + + return ret; + } + private void disposeRequestBuffer () { if (_requestBuffer == null) return; _requestBuffer.Dispose (); + _requestBuffer = null; } @@ -210,10 +264,8 @@ private void disposeStream () if (_stream == null) return; - _inputStream = null; - _outputStream = null; - _stream.Dispose (); + _stream = null; } @@ -229,24 +281,29 @@ private void disposeTimer () } _timer.Dispose (); + _timer = null; } - private void init () + private void init (MemoryStream requestBuffer, int timeout) { + _requestBuffer = requestBuffer; + _timeout = timeout; + _context = new HttpListenerContext (this); + _currentLine = new StringBuilder (64); _inputState = InputState.RequestLine; _inputStream = null; _lineState = LineState.None; _outputStream = null; _position = 0; - _prefix = null; - _requestBuffer = new MemoryStream (); } private static void onRead (IAsyncResult asyncResult) { var conn = (HttpConnection) asyncResult.AsyncState; + var current = conn._attempts; + if (conn._socket == null) return; @@ -254,161 +311,231 @@ private static void onRead (IAsyncResult asyncResult) if (conn._socket == null) return; - var nread = -1; - var len = 0; + conn._timer.Change (Timeout.Infinite, Timeout.Infinite); + conn._timeoutCanceled[current] = true; + + var nread = 0; + try { - conn._timer.Change (Timeout.Infinite, Timeout.Infinite); nread = conn._stream.EndRead (asyncResult); - conn._requestBuffer.Write (conn._buffer, 0, nread); - len = (int) conn._requestBuffer.Length; } - catch (Exception ex) { - if (conn._requestBuffer != null && conn._requestBuffer.Length > 0) { - conn.SendError (ex.Message, 400); - return; - } + catch (Exception) { + // TODO: Logging. conn.close (); + return; } if (nread <= 0) { conn.close (); + return; } - if (conn.processInput (conn._requestBuffer.GetBuffer (), len)) { - if (!conn._context.HasError) - conn._context.Request.FinishInitialization (); - - if (conn._context.HasError) { - conn.SendError (); - return; - } - - if (!conn._listener.BindContext (conn._context)) { - conn.SendError ("Invalid host", 400); - return; - } - - var lsnr = conn._context.Listener; - if (conn._lastListener != lsnr) { - conn.removeConnection (); - lsnr.AddConnection (conn); - conn._lastListener = lsnr; - } - - conn._contextBound = true; - lsnr.RegisterContext (conn._context); + conn._requestBuffer.Write (conn._buffer, 0, nread); + if (conn.processRequestBuffer ()) return; - } - conn._stream.BeginRead (conn._buffer, 0, _bufferLength, onRead, conn); + conn.BeginReadRequest (); } } private static void onTimeout (object state) { var conn = (HttpConnection) state; - conn.close (); + var current = conn._attempts; + + if (conn._socket == null) + return; + + lock (conn._sync) { + if (conn._socket == null) + return; + + if (conn._timeoutCanceled[current]) + return; + + conn._context.SendError (408); + } } - // true -> Done processing. - // false -> Need more input. private bool processInput (byte[] data, int length) { - if (_currentLine == null) - _currentLine = new StringBuilder (64); + // This method returns a bool: + // - true Done processing + // - false Need more input + + var req = _context.Request; - var nread = 0; try { - string line; - while ((line = readLineFrom (data, _position, length, out nread)) != null) { + while (true) { + int nread; + var line = readLineFrom (data, _position, length, out nread); + _position += nread; + + if (line == null) + break; + if (line.Length == 0) { if (_inputState == InputState.RequestLine) continue; - if (_position > 32768) + if (_position > _maxInputLength) _context.ErrorMessage = "Headers too long"; - _currentLine = null; return true; } if (_inputState == InputState.RequestLine) { - _context.Request.SetRequestLine (line); + req.SetRequestLine (line); + _inputState = InputState.Headers; } else { - _context.Request.AddHeader (line); + req.AddHeader (line); } - if (_context.HasError) + if (_context.HasErrorMessage) return true; } } - catch (Exception ex) { - _context.ErrorMessage = ex.Message; + catch (Exception) { + // TODO: Logging. + + _context.ErrorMessage = "Processing failure"; + return true; } - _position += nread; - if (_position >= 32768) { + if (_position >= _maxInputLength) { _context.ErrorMessage = "Headers too long"; + return true; } return false; } - private string readLineFrom (byte[] buffer, int offset, int length, out int read) + private bool processRequestBuffer () { - read = 0; - for (var i = offset; i < length && _lineState != LineState.Lf; i++) { - read++; - var b = buffer[i]; - if (b == 13) - _lineState = LineState.Cr; - else if (b == 10) - _lineState = LineState.Lf; - else - _currentLine.Append ((char) b); + // This method returns a bool: + // - true Done processing + // - false Need more write + + var data = _requestBuffer.GetBuffer (); + var len = (int) _requestBuffer.Length; + + if (!processInput (data, len)) + return false; + + var req = _context.Request; + + if (!_context.HasErrorMessage) + req.FinishInitialization (); + + if (_context.HasErrorMessage) { + _context.SendError (); + + return true; } - if (_lineState == LineState.Lf) { - _lineState = LineState.None; - var line = _currentLine.ToString (); - _currentLine.Length = 0; + var uri = req.Url; + HttpListener httplsnr; - return line; + if (!_endPointListener.TrySearchHttpListener (uri, out httplsnr)) { + _context.SendError (404); + + return true; } - return null; + httplsnr.RegisterContext (_context); + + return true; } - private void removeConnection () + private string readLineFrom ( + byte[] buffer, + int offset, + int length, + out int nread + ) { - if (_lastListener != null) - _lastListener.RemoveConnection (this); - else - _listener.RemoveConnection (this); + nread = 0; + + for (var i = offset; i < length; i++) { + nread++; + + var b = buffer[i]; + + if (b == 13) { + _lineState = LineState.Cr; + + continue; + } + + if (b == 10) { + _lineState = LineState.Lf; + + break; + } + + _currentLine.Append ((char) b); + } + + if (_lineState != LineState.Lf) + return null; + + var ret = _currentLine.ToString (); + + _currentLine.Length = 0; + _lineState = LineState.None; + + return ret; } - private void unbind () + private MemoryStream takeOverRequestBuffer () { - if (!_contextBound) - return; + if (_inputStream != null) + return createRequestBuffer (_inputStream); + + var ret = new MemoryStream (); - _listener.UnbindContext (_context); - _contextBound = false; + var buff = _requestBuffer.GetBuffer (); + var len = (int) _requestBuffer.Length; + var cnt = len - _position; + + if (cnt > 0) + ret.Write (buff, _position, cnt); + + disposeRequestBuffer (); + + return ret; } #endregion #region Internal Methods + internal void BeginReadRequest () + { + _attempts++; + + _timeoutCanceled.Add (_attempts, false); + _timer.Change (_timeout, Timeout.Infinite); + + try { + _stream.BeginRead (_buffer, 0, _bufferLength, onRead, this); + } + catch (Exception) { + // TODO: Logging. + + close (); + } + } + internal void Close (bool force) { if (_socket == null) @@ -418,48 +545,51 @@ internal void Close (bool force) if (_socket == null) return; - if (!force) { - GetResponseStream ().Close (false); - if (!_context.Response.CloseConnection && _context.Request.FlushInput ()) { - // Don't close. Keep working. - _reuses++; - disposeRequestBuffer (); - unbind (); - init (); - BeginReadRequest (); + if (force) { + if (_outputStream != null) + _outputStream.Close (true); - return; - } + close (); + + return; } - else if (_outputStream != null) { - _outputStream.Close (true); + + GetResponseStream ().Close (false); + + if (_context.Response.CloseConnection) { + close (); + + return; } - close (); - } - } + if (!_context.Request.FlushInput ()) { + close (); - #endregion + return; + } - #region Public Methods + _context.Unregister (); - public void BeginReadRequest () - { - if (_buffer == null) - _buffer = new byte[_bufferLength]; + _reuses++; - if (_reuses == 1) - _timeout = 15000; + var buff = takeOverRequestBuffer (); + var len = buff.Length; - try { - _timer.Change (_timeout, Timeout.Infinite); - _stream.BeginRead (_buffer, 0, _bufferLength, onRead, this); - } - catch { - close (); + init (buff, 15000); + + if (len > 0) { + if (processRequestBuffer ()) + return; + } + + BeginReadRequest (); } } + #endregion + + #region Public Methods + public void Close () { Close (false); @@ -467,25 +597,34 @@ public void Close () public RequestStream GetRequestStream (long contentLength, bool chunked) { - if (_inputStream != null || _socket == null) - return _inputStream; - lock (_sync) { if (_socket == null) + return null; + + if (_inputStream != null) return _inputStream; var buff = _requestBuffer.GetBuffer (); var len = (int) _requestBuffer.Length; + var cnt = len - _position; + + _inputStream = chunked + ? new ChunkedRequestStream ( + _stream, + buff, + _position, + cnt, + _context + ) + : new RequestStream ( + _stream, + buff, + _position, + cnt, + contentLength + ); + disposeRequestBuffer (); - if (chunked) { - _context.Response.SendChunked = true; - _inputStream = new ChunkedRequestStream ( - _stream, buff, _position, len - _position, _context); - } - else { - _inputStream = new RequestStream ( - _stream, buff, _position, len - _position, contentLength); - } return _inputStream; } @@ -493,62 +632,22 @@ public RequestStream GetRequestStream (long contentLength, bool chunked) public ResponseStream GetResponseStream () { - // TODO: Can we get this stream before reading the input? - - if (_outputStream != null || _socket == null) - return _outputStream; - lock (_sync) { if (_socket == null) + return null; + + if (_outputStream != null) return _outputStream; var lsnr = _context.Listener; var ignore = lsnr != null ? lsnr.IgnoreWriteExceptions : true; + _outputStream = new ResponseStream (_stream, _context.Response, ignore); return _outputStream; } } - public void SendError () - { - SendError (_context.ErrorMessage, _context.ErrorStatus); - } - - public void SendError (string message, int status) - { - if (_socket == null) - return; - - lock (_sync) { - if (_socket == null) - return; - - try { - var res = _context.Response; - res.StatusCode = status; - res.ContentType = "text/html"; - - var content = new StringBuilder (64); - content.AppendFormat ("

{0} {1}", status, res.StatusDescription); - if (message != null && message.Length > 0) - content.AppendFormat (" ({0})

", message); - else - content.Append (""); - - var enc = Encoding.UTF8; - var entity = enc.GetBytes (content.ToString ()); - res.ContentEncoding = enc; - res.ContentLength64 = entity.LongLength; - - res.Close (entity, true); - } - catch { - Close (true); - } - } - } - #endregion } } diff --git a/websocket-sharp/Net/HttpDigestIdentity.cs b/websocket-sharp/Net/HttpDigestIdentity.cs index caa471aa0..e2863aa9c 100644 --- a/websocket-sharp/Net/HttpDigestIdentity.cs +++ b/websocket-sharp/Net/HttpDigestIdentity.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2014 sta.blockhead + * Copyright (c) 2014-2024 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 @@ -33,7 +33,8 @@ namespace WebSocketSharp.Net { /// - /// Holds the user name and other parameters from the HTTP Digest authentication credentials. + /// Holds the username and other parameters from an HTTP Digest + /// authentication attempt. /// public class HttpDigestIdentity : GenericIdentity { @@ -46,7 +47,7 @@ public class HttpDigestIdentity : GenericIdentity #region Internal Constructors internal HttpDigestIdentity (NameValueCollection parameters) - : base (parameters ["username"], "Digest") + : base (parameters["username"], "Digest") { _parameters = parameters; } @@ -56,110 +57,110 @@ internal HttpDigestIdentity (NameValueCollection parameters) #region Public Properties /// - /// Gets the algorithm parameter from the HTTP Digest authentication credentials. + /// Gets the algorithm parameter from a digest authentication attempt. /// /// /// A that represents the algorithm parameter. /// public string Algorithm { get { - return _parameters ["algorithm"]; + return _parameters["algorithm"]; } } /// - /// Gets the cnonce parameter from the HTTP Digest authentication credentials. + /// Gets the cnonce parameter from a digest authentication attempt. /// /// /// A that represents the cnonce parameter. /// public string Cnonce { get { - return _parameters ["cnonce"]; + return _parameters["cnonce"]; } } /// - /// Gets the nc parameter from the HTTP Digest authentication credentials. + /// Gets the nc parameter from a digest authentication attempt. /// /// /// A that represents the nc parameter. /// public string Nc { get { - return _parameters ["nc"]; + return _parameters["nc"]; } } /// - /// Gets the nonce parameter from the HTTP Digest authentication credentials. + /// Gets the nonce parameter from a digest authentication attempt. /// /// /// A that represents the nonce parameter. /// public string Nonce { get { - return _parameters ["nonce"]; + return _parameters["nonce"]; } } /// - /// Gets the opaque parameter from the HTTP Digest authentication credentials. + /// Gets the opaque parameter from a digest authentication attempt. /// /// /// A that represents the opaque parameter. /// public string Opaque { get { - return _parameters ["opaque"]; + return _parameters["opaque"]; } } /// - /// Gets the qop parameter from the HTTP Digest authentication credentials. + /// Gets the qop parameter from a digest authentication attempt. /// /// /// A that represents the qop parameter. /// public string Qop { get { - return _parameters ["qop"]; + return _parameters["qop"]; } } /// - /// Gets the realm parameter from the HTTP Digest authentication credentials. + /// Gets the realm parameter from a digest authentication attempt. /// /// /// A that represents the realm parameter. /// public string Realm { get { - return _parameters ["realm"]; + return _parameters["realm"]; } } /// - /// Gets the response parameter from the HTTP Digest authentication credentials. + /// Gets the response parameter from a digest authentication attempt. /// /// /// A that represents the response parameter. /// public string Response { get { - return _parameters ["response"]; + return _parameters["response"]; } } /// - /// Gets the uri parameter from the HTTP Digest authentication credentials. + /// Gets the uri parameter from a digest authentication attempt. /// /// /// A that represents the uri parameter. /// public string Uri { get { - return _parameters ["uri"]; + return _parameters["uri"]; } } @@ -167,15 +168,23 @@ public string Uri { #region Internal Methods - internal bool IsValid (string password, string realm, string method, string entity) + internal bool IsValid ( + string password, + string realm, + string method, + string entity + ) { var parameters = new NameValueCollection (_parameters); - parameters ["password"] = password; - parameters ["realm"] = realm; - parameters ["method"] = method; - parameters ["entity"] = entity; - return _parameters ["response"] == AuthenticationResponse.CreateRequestDigest (parameters); + parameters["password"] = password; + parameters["realm"] = realm; + parameters["method"] = method; + parameters["entity"] = entity; + + var expectedDigest = AuthenticationResponse.CreateRequestDigest (parameters); + + return _parameters["response"] == expectedDigest; } #endregion diff --git a/websocket-sharp/Net/HttpHeaderInfo.cs b/websocket-sharp/Net/HttpHeaderInfo.cs index 717f8f46d..2b77e3d9d 100644 --- a/websocket-sharp/Net/HttpHeaderInfo.cs +++ b/websocket-sharp/Net/HttpHeaderInfo.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2013-2014 sta.blockhead + * Copyright (c) 2013-2020 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 @@ -34,17 +34,17 @@ internal class HttpHeaderInfo { #region Private Fields - private string _name; - private HttpHeaderType _type; + private string _headerName; + private HttpHeaderType _headerType; #endregion #region Internal Constructors - internal HttpHeaderInfo (string name, HttpHeaderType type) + internal HttpHeaderInfo (string headerName, HttpHeaderType headerType) { - _name = name; - _type = type; + _headerName = headerName; + _headerType = headerType; } #endregion @@ -53,13 +53,17 @@ internal HttpHeaderInfo (string name, HttpHeaderType type) internal bool IsMultiValueInRequest { get { - return (_type & HttpHeaderType.MultiValueInRequest) == HttpHeaderType.MultiValueInRequest; + var headerType = _headerType & HttpHeaderType.MultiValueInRequest; + + return headerType == HttpHeaderType.MultiValueInRequest; } } internal bool IsMultiValueInResponse { get { - return (_type & HttpHeaderType.MultiValueInResponse) == HttpHeaderType.MultiValueInResponse; + var headerType = _headerType & HttpHeaderType.MultiValueInResponse; + + return headerType == HttpHeaderType.MultiValueInResponse; } } @@ -67,27 +71,31 @@ internal bool IsMultiValueInResponse { #region Public Properties - public bool IsRequest { + public string HeaderName { get { - return (_type & HttpHeaderType.Request) == HttpHeaderType.Request; + return _headerName; } } - public bool IsResponse { + public HttpHeaderType HeaderType { get { - return (_type & HttpHeaderType.Response) == HttpHeaderType.Response; + return _headerType; } } - public string Name { + public bool IsRequest { get { - return _name; + var headerType = _headerType & HttpHeaderType.Request; + + return headerType == HttpHeaderType.Request; } } - public HttpHeaderType Type { + public bool IsResponse { get { - return _type; + var headerType = _headerType & HttpHeaderType.Response; + + return headerType == HttpHeaderType.Response; } } @@ -97,16 +105,22 @@ public HttpHeaderType Type { public bool IsMultiValue (bool response) { - return (_type & HttpHeaderType.MultiValue) == HttpHeaderType.MultiValue - ? (response ? IsResponse : IsRequest) - : (response ? IsMultiValueInResponse : IsMultiValueInRequest); + var headerType = _headerType & HttpHeaderType.MultiValue; + + if (headerType != HttpHeaderType.MultiValue) + return response ? IsMultiValueInResponse : IsMultiValueInRequest; + + return response ? IsResponse : IsRequest; } public bool IsRestricted (bool response) { - return (_type & HttpHeaderType.Restricted) == HttpHeaderType.Restricted - ? (response ? IsResponse : IsRequest) - : false; + var headerType = _headerType & HttpHeaderType.Restricted; + + if (headerType != HttpHeaderType.Restricted) + return false; + + return response ? IsResponse : IsRequest; } #endregion diff --git a/websocket-sharp/Net/HttpListener.cs b/websocket-sharp/Net/HttpListener.cs index 830fcd30c..44b964c0c 100644 --- a/websocket-sharp/Net/HttpListener.cs +++ b/websocket-sharp/Net/HttpListener.cs @@ -8,7 +8,7 @@ * The MIT License * * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2024 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 @@ -57,30 +57,48 @@ namespace WebSocketSharp.Net /// /// Provides a simple, programmatically controlled HTTP listener. /// + /// + /// + /// The listener supports HTTP/1.1 version request and response. + /// + /// + /// And the listener allows to accept WebSocket handshake requests. + /// + /// + /// This class cannot be inherited. + /// + /// public sealed class HttpListener : IDisposable { #region Private Fields - private AuthenticationSchemes _authSchemes; - private Func _authSchemeSelector; - private string _certFolderPath; - private Dictionary _connections; - private object _connectionsSync; - private List _ctxQueue; - private object _ctxQueueSync; - private Dictionary _ctxRegistry; - private object _ctxRegistrySync; - private Func _credFinder; - private bool _disposed; - private bool _ignoreWriteExceptions; - private bool _listening; - private Logger _logger; - private HttpListenerPrefixCollection _prefixes; - private string _realm; - private bool _reuseAddress; - private ServerSslConfiguration _sslConfig; - private List _waitQueue; - private object _waitQueueSync; + private AuthenticationSchemes _authSchemes; + private Func _authSchemeSelector; + private string _certFolderPath; + private Queue _contextQueue; + private LinkedList _contextRegistry; + private object _contextRegistrySync; + private static readonly string _defaultRealm; + private bool _disposed; + private bool _ignoreWriteExceptions; + private volatile bool _isListening; + private Logger _log; + private HttpListenerPrefixCollection _prefixes; + private string _realm; + private bool _reuseAddress; + private ServerSslConfiguration _sslConfig; + private object _sync; + private Func _userCredFinder; + private Queue _waitQueue; + + #endregion + + #region Static Constructor + + static HttpListener () + { + _defaultRealm = "SECRET AREA"; + } #endregion @@ -92,31 +110,22 @@ public sealed class HttpListener : IDisposable public HttpListener () { _authSchemes = AuthenticationSchemes.Anonymous; - - _connections = new Dictionary (); - _connectionsSync = ((ICollection) _connections).SyncRoot; - - _ctxQueue = new List (); - _ctxQueueSync = ((ICollection) _ctxQueue).SyncRoot; - - _ctxRegistry = new Dictionary (); - _ctxRegistrySync = ((ICollection) _ctxRegistry).SyncRoot; - - _logger = new Logger (); - + _contextQueue = new Queue (); + _contextRegistry = new LinkedList (); + _contextRegistrySync = ((ICollection) _contextRegistry).SyncRoot; + _log = new Logger (); _prefixes = new HttpListenerPrefixCollection (this); - - _waitQueue = new List (); - _waitQueueSync = ((ICollection) _waitQueue).SyncRoot; + _sync = new object (); + _waitQueue = new Queue (); } #endregion #region Internal Properties - internal bool IsDisposed { + internal string ObjectName { get { - return _disposed; + return GetType ().ToString (); } } @@ -138,109 +147,164 @@ internal bool ReuseAddress { /// Gets or sets the scheme used to authenticate the clients. /// /// - /// One of the enum values, - /// represents the scheme used to authenticate the clients. The default value is - /// . + /// + /// One of the + /// enum values. + /// + /// + /// It represents the scheme used to authenticate the clients. + /// + /// + /// The default value is + /// . + /// /// /// /// This listener has been closed. /// public AuthenticationSchemes AuthenticationSchemes { get { - CheckDisposed (); + if (_disposed) + throw new ObjectDisposedException (ObjectName); + return _authSchemes; } set { - CheckDisposed (); + if (_disposed) + throw new ObjectDisposedException (ObjectName); + _authSchemes = value; } } /// - /// Gets or sets the delegate called to select the scheme used to authenticate the clients. + /// Gets or sets the delegate called to determine the scheme used to + /// authenticate the clients. /// /// - /// If you set this property, the listener uses the authentication scheme selected by - /// the delegate for each request. Or if you don't set, the listener uses the value of - /// the property as the authentication - /// scheme for all requests. + /// + /// If this property is set, the listener uses the authentication + /// scheme selected by the delegate for each request. + /// + /// + /// Or if this property is not set, the listener uses the value of + /// the property + /// as the authentication scheme for all requests. + /// /// /// - /// A Func<, > - /// delegate that references the method used to select an authentication scheme. The default - /// value is . + /// + /// A + /// delegate. + /// + /// + /// It represents the delegate called when the listener selects + /// an authentication scheme. + /// + /// + /// if not necessary. + /// + /// + /// The default value is . + /// /// /// /// This listener has been closed. /// public Func AuthenticationSchemeSelector { get { - CheckDisposed (); + if (_disposed) + throw new ObjectDisposedException (ObjectName); + return _authSchemeSelector; } set { - CheckDisposed (); + if (_disposed) + throw new ObjectDisposedException (ObjectName); + _authSchemeSelector = value; } } /// - /// Gets or sets the path to the folder in which stores the certificate files used to - /// authenticate the server on the secure connection. + /// Gets or sets the path to the folder in which stores the certificate + /// files used to authenticate the server on the secure connection. /// /// /// - /// This property represents the path to the folder in which stores the certificate files - /// associated with each port number of added URI prefixes. A set of the certificate files - /// is a pair of the 'port number'.cer (DER) and 'port number'.key - /// (DER, RSA Private Key). + /// This property represents the path to the folder in which stores + /// the certificate files associated with each port number of added + /// URI prefixes. + /// + /// + /// A set of the certificate files is a pair of <port number>.cer + /// (DER) and <port number>.key (DER, RSA Private Key). /// /// - /// If this property is or empty, the result of - /// System.Environment.GetFolderPath - /// () is used as the default path. + /// If this property is or an empty string, + /// the result of the + /// with the method is used as + /// the default path. /// /// /// - /// A that represents the path to the folder in which stores - /// the certificate files. The default value is . + /// + /// A that represents the path to the folder + /// in which stores the certificate files. + /// + /// + /// The default value is . + /// /// /// /// This listener has been closed. /// public string CertificateFolderPath { get { - CheckDisposed (); + if (_disposed) + throw new ObjectDisposedException (ObjectName); + return _certFolderPath; } set { - CheckDisposed (); + if (_disposed) + throw new ObjectDisposedException (ObjectName); + _certFolderPath = value; } } /// - /// Gets or sets a value indicating whether the listener returns exceptions that occur when - /// sending the response to the client. + /// Gets or sets a value indicating whether the listener returns + /// exceptions that occur when sending the response to the client. /// /// - /// true if the listener shouldn't return those exceptions; otherwise, false. - /// The default value is false. + /// + /// true if the listener should not return those exceptions; + /// otherwise, false. + /// + /// + /// The default value is false. + /// /// /// /// This listener has been closed. /// public bool IgnoreWriteExceptions { get { - CheckDisposed (); + if (_disposed) + throw new ObjectDisposedException (ObjectName); + return _ignoreWriteExceptions; } set { - CheckDisposed (); + if (_disposed) + throw new ObjectDisposedException (ObjectName); + _ignoreWriteExceptions = value; } } @@ -253,12 +317,13 @@ public bool IgnoreWriteExceptions { ///
public bool IsListening { get { - return _listening; + return _isListening; } } /// - /// Gets a value indicating whether the listener can be used with the current operating system. + /// Gets a value indicating whether the listener can be used with + /// the current operating system. /// /// /// true. @@ -273,16 +338,26 @@ public static bool IsSupported { /// Gets the logging functions. /// /// - /// The default logging level is . If you would like to change it, - /// you should set the Log.Level property to any of the enum - /// values. + /// + /// The default logging level is . + /// + /// + /// If you would like to change it, you should set the Log.Level + /// property to any of the enum values. + /// /// /// /// A that provides the logging functions. /// + /// + /// This listener has been closed. + /// public Logger Log { get { - return _logger; + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + return _log; } } @@ -290,14 +365,17 @@ public Logger Log { /// Gets the URI prefixes handled by the listener. /// /// - /// A that contains the URI prefixes. + /// A that contains the URI + /// prefixes. /// /// /// This listener has been closed. /// public HttpListenerPrefixCollection Prefixes { get { - CheckDisposed (); + if (_disposed) + throw new ObjectDisposedException (ObjectName); + return _prefixes; } } @@ -305,45 +383,56 @@ public HttpListenerPrefixCollection Prefixes { /// /// Gets or sets the name of the realm associated with the listener. /// + /// + /// If this property is or an empty string, + /// "SECRET AREA" is used as the name of the realm. + /// /// - /// A that represents the name of the realm. The default value is - /// "SECRET AREA". + /// + /// A that represents the name of the realm. + /// + /// + /// The default value is . + /// /// /// /// This listener has been closed. /// public string Realm { get { - CheckDisposed (); - return _realm != null && _realm.Length > 0 ? _realm : (_realm = "SECRET AREA"); + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + return _realm; } set { - CheckDisposed (); + if (_disposed) + throw new ObjectDisposedException (ObjectName); + _realm = value; } } /// - /// Gets or sets the SSL configuration used to authenticate the server and - /// optionally the client for secure connection. + /// Gets the configuration for secure connection. /// /// - /// A that represents the configuration used to - /// authenticate the server and optionally the client for secure connection. + /// A that represents the + /// configuration used to provide secure connections. /// /// /// This listener has been closed. /// public ServerSslConfiguration SslConfiguration { get { - CheckDisposed (); - return _sslConfig ?? (_sslConfig = new ServerSslConfiguration (null)); - } + if (_disposed) + throw new ObjectDisposedException (ObjectName); - set { - CheckDisposed (); - _sslConfig = value; + if (_sslConfig == null) + _sslConfig = new ServerSslConfiguration (); + + return _sslConfig; } } @@ -353,7 +442,7 @@ public ServerSslConfiguration SslConfiguration { /// additional requests on the same connection. /// /// - /// This property isn't currently supported and always throws + /// This property is not currently supported and always throws /// a . /// /// @@ -374,26 +463,45 @@ public bool UnsafeConnectionNtlmAuthentication { } /// - /// Gets or sets the delegate called to find the credentials for an identity used to - /// authenticate a client. + /// Gets or sets the delegate called to find the credentials for + /// an identity used to authenticate a client. /// /// - /// A Func<, > delegate that - /// references the method used to find the credentials. The default value is a function that - /// only returns . + /// + /// A + /// delegate. + /// + /// + /// It represents the delegate called when the listener finds + /// the credentials used to authenticate a client. + /// + /// + /// It must return if the credentials + /// are not found. + /// + /// + /// if not necessary. + /// + /// + /// The default value is . + /// /// /// /// This listener has been closed. /// public Func UserCredentialsFinder { get { - CheckDisposed (); - return _credFinder ?? (_credFinder = id => null); + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + return _userCredFinder; } set { - CheckDisposed (); - _credFinder = value; + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + _userCredFinder = value; } } @@ -401,215 +509,221 @@ public Func UserCredentialsFinder { #region Private Methods - private void cleanup (bool force) + private bool authenticateClient (HttpListenerContext context) { - lock (_ctxRegistrySync) { - if (!force) - sendServiceUnavailable (); + var schm = selectAuthenticationScheme (context.Request); - cleanupContextRegistry (); - cleanupConnections (); - cleanupWaitQueue (); + if (schm == AuthenticationSchemes.Anonymous) + return true; + + if (schm == AuthenticationSchemes.None) { + var msg = "Authentication not allowed"; + + context.SendError (403, msg); + + return false; } - } - private void cleanupConnections () - { - lock (_connectionsSync) { - if (_connections.Count == 0) - return; + var realm = getRealm (); + + if (!context.SetUser (schm, realm, _userCredFinder)) { + context.SendAuthenticationChallenge (schm, realm); - // Need to copy this since closing will call the RemoveConnection method. - var keys = _connections.Keys; - var conns = new HttpConnection[keys.Count]; - keys.CopyTo (conns, 0); - _connections.Clear (); - for (var i = conns.Length - 1; i >= 0; i--) - conns[i].Close (true); + return false; } + + return true; } - private void cleanupContextRegistry () + private HttpListenerAsyncResult beginGetContext ( + AsyncCallback callback, + object state + ) { - lock (_ctxRegistrySync) { - if (_ctxRegistry.Count == 0) - return; + lock (_contextRegistrySync) { + if (!_isListening) { + var msg = "The method is canceled."; + + throw new HttpListenerException (995, msg); + } + + var ares = new HttpListenerAsyncResult (callback, state, _log); + + if (_contextQueue.Count == 0) { + _waitQueue.Enqueue (ares); + + return ares; + } + + var ctx = _contextQueue.Dequeue (); - // Need to copy this since closing will call the UnregisterContext method. - var keys = _ctxRegistry.Keys; - var ctxs = new HttpListenerContext[keys.Count]; - keys.CopyTo (ctxs, 0); - _ctxRegistry.Clear (); - for (var i = ctxs.Length - 1; i >= 0; i--) - ctxs[i].Connection.Close (true); + ares.Complete (ctx, true); + + return ares; } } - private void cleanupWaitQueue () + private void cleanupContextQueue (bool force) { - lock (_waitQueueSync) { - if (_waitQueue.Count == 0) - return; + if (_contextQueue.Count == 0) + return; - var ex = new ObjectDisposedException (GetType ().ToString ()); - foreach (var ares in _waitQueue) - ares.Complete (ex); + if (force) { + _contextQueue.Clear (); - _waitQueue.Clear (); + return; } + + var ctxs = _contextQueue.ToArray (); + + _contextQueue.Clear (); + + foreach (var ctx in ctxs) + ctx.SendError (503); } - private void close (bool force) + private void cleanupContextRegistry () { - EndPointManager.RemoveListener (this); - cleanup (force); + var cnt = _contextRegistry.Count; + + if (cnt == 0) + return; + + var ctxs = new HttpListenerContext[cnt]; + + lock (_contextRegistrySync) { + _contextRegistry.CopyTo (ctxs, 0); + _contextRegistry.Clear (); + } + + foreach (var ctx in ctxs) + ctx.Connection.Close (true); } - // Must be called with a lock on _ctxQueue. - private HttpListenerContext getContextFromQueue () + private void cleanupWaitQueue (string message) { - if (_ctxQueue.Count == 0) - return null; + if (_waitQueue.Count == 0) + return; - var ctx = _ctxQueue[0]; - _ctxQueue.RemoveAt (0); + var aress = _waitQueue.ToArray (); - return ctx; + _waitQueue.Clear (); + + foreach (var ares in aress) { + var ex = new HttpListenerException (995, message); + + ares.Complete (ex); + } } - private void sendServiceUnavailable () + private void close (bool force) { - lock (_ctxQueueSync) { - if (_ctxQueue.Count == 0) + lock (_sync) { + if (_disposed) return; - var ctxs = _ctxQueue.ToArray (); - _ctxQueue.Clear (); - foreach (var ctx in ctxs) { - var res = ctx.Response; - res.StatusCode = (int) HttpStatusCode.ServiceUnavailable; - res.Close (); + lock (_contextRegistrySync) { + if (!_isListening) { + _disposed = true; + + return; + } + + _isListening = false; } - } - } - #endregion + cleanupContextQueue (force); + cleanupContextRegistry (); - #region Internal Methods + var msg = "The listener is closed."; - internal void AddConnection (HttpConnection connection) - { - lock (_connectionsSync) - _connections[connection] = connection; + cleanupWaitQueue (msg); + + EndPointManager.RemoveListener (this); + + _disposed = true; + } } - internal bool Authenticate (HttpListenerContext context) + private string getRealm () { - var schm = SelectAuthenticationScheme (context); - if (schm == AuthenticationSchemes.Anonymous) - return true; + var realm = _realm; + + return realm != null && realm.Length > 0 ? realm : _defaultRealm; + } - if (schm != AuthenticationSchemes.Basic && schm != AuthenticationSchemes.Digest) { - context.Response.Close (HttpStatusCode.Forbidden); + private bool registerContext (HttpListenerContext context) + { + if (!_isListening) return false; - } - var realm = Realm; - var req = context.Request; - var user = HttpUtility.CreateUser ( - req.Headers["Authorization"], schm, realm, req.HttpMethod, UserCredentialsFinder); + lock (_contextRegistrySync) { + if (!_isListening) + return false; - if (user != null && user.Identity.IsAuthenticated) { - context.User = user; - return true; - } + context.Listener = this; + + _contextRegistry.AddLast (context); + + if (_waitQueue.Count == 0) { + _contextQueue.Enqueue (context); - if (schm == AuthenticationSchemes.Basic) - context.Response.CloseWithAuthChallenge ( - AuthenticationChallenge.CreateBasicChallenge (realm).ToBasicString ()); + return true; + } + + var ares = _waitQueue.Dequeue (); - if (schm == AuthenticationSchemes.Digest) - context.Response.CloseWithAuthChallenge ( - AuthenticationChallenge.CreateDigestChallenge (realm).ToDigestString ()); + ares.Complete (context, false); - return false; + return true; + } } - internal HttpListenerAsyncResult BeginGetContext (HttpListenerAsyncResult asyncResult) + private AuthenticationSchemes selectAuthenticationScheme ( + HttpListenerRequest request + ) { - CheckDisposed (); - if (_prefixes.Count == 0) - throw new InvalidOperationException ("The listener has no URI prefix on which listens."); - - if (!_listening) - throw new InvalidOperationException ("The listener hasn't been started."); - - // Lock _waitQueue early to avoid race conditions. - lock (_waitQueueSync) { - lock (_ctxQueueSync) { - var ctx = getContextFromQueue (); - if (ctx != null) { - asyncResult.Complete (ctx, true); - return asyncResult; - } - } + var selector = _authSchemeSelector; - _waitQueue.Add (asyncResult); - } + if (selector == null) + return _authSchemes; - return asyncResult; + try { + return selector (request); + } + catch { + return AuthenticationSchemes.None; + } } + #endregion + + #region Internal Methods + internal void CheckDisposed () { if (_disposed) - throw new ObjectDisposedException (GetType ().ToString ()); + throw new ObjectDisposedException (ObjectName); } - internal void RegisterContext (HttpListenerContext context) + internal bool RegisterContext (HttpListenerContext context) { - lock (_ctxRegistrySync) - _ctxRegistry[context] = context; - - HttpListenerAsyncResult ares = null; - lock (_waitQueueSync) { - if (_waitQueue.Count == 0) { - lock (_ctxQueueSync) - _ctxQueue.Add (context); - } - else { - ares = _waitQueue[0]; - _waitQueue.RemoveAt (0); - } - } + if (!authenticateClient (context)) + return false; - if (ares != null) - ares.Complete (context); - } + if (!registerContext (context)) { + context.SendError (503); - internal void RemoveConnection (HttpConnection connection) - { - lock (_connectionsSync) - _connections.Remove (connection); - } + return false; + } - internal AuthenticationSchemes SelectAuthenticationScheme (HttpListenerContext context) - { - return AuthenticationSchemeSelector != null - ? AuthenticationSchemeSelector (context.Request) - : _authSchemes; + return true; } internal void UnregisterContext (HttpListenerContext context) { - lock (_ctxRegistrySync) - _ctxRegistry.Remove (context); - - lock (_ctxQueueSync) { - var idx = _ctxQueue.IndexOf (context); - if (idx >= 0) - _ctxQueue.RemoveAt (idx); - } + lock (_contextRegistrySync) + _contextRegistry.Remove (context); } #endregion @@ -625,44 +739,73 @@ public void Abort () return; close (true); - _disposed = true; } /// /// Begins getting an incoming request asynchronously. /// /// - /// This asynchronous operation must be completed by calling the EndGetContext method. - /// Typically, the method is invoked by the delegate. + /// + /// This asynchronous operation must be ended by calling + /// the method. + /// + /// + /// Typically, the method is called by + /// . + /// /// /// - /// An that represents the status of the asynchronous operation. + /// An instance that represents the status of + /// the asynchronous operation. /// /// - /// An delegate that references the method to invoke when - /// the asynchronous operation completes. + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the asynchronous operation is + /// complete. + /// /// /// - /// An that represents a user defined object to pass to - /// the delegate. + /// An that specifies a user defined object to pass to + /// . /// + /// + /// This method is canceled. + /// /// /// - /// This listener has no URI prefix on which listens. + /// This listener has not been started or is currently stopped. /// /// /// -or- /// /// - /// This listener hasn't been started, or is currently stopped. + /// This listener has no URI prefix on which listens. /// /// /// /// This listener has been closed. /// - public IAsyncResult BeginGetContext (AsyncCallback callback, Object state) + public IAsyncResult BeginGetContext (AsyncCallback callback, object state) { - return BeginGetContext (new HttpListenerAsyncResult (callback, state)); + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (!_isListening) { + var msg = "The listener has not been started."; + + throw new InvalidOperationException (msg); + } + + if (_prefixes.Count == 0) { + var msg = "The listener has no URI prefix on which listens."; + + throw new InvalidOperationException (msg); + } + + return beginGetContext (callback, state); } /// @@ -674,72 +817,106 @@ public void Close () return; close (false); - _disposed = true; } /// /// Ends an asynchronous operation to get an incoming request. /// /// - /// This method completes an asynchronous operation started by calling - /// the BeginGetContext method. + /// This method ends an asynchronous operation started by calling + /// the method. /// /// /// A that represents a request. /// /// - /// An obtained by calling the BeginGetContext method. + /// An instance obtained by calling + /// the method. /// + /// + /// was not obtained by calling + /// the method. + /// /// /// is . /// - /// - /// wasn't obtained by calling the BeginGetContext method. + /// + /// This method is canceled. /// /// - /// This method was already called for the specified . + /// + /// This listener has not been started or is currently stopped. + /// + /// + /// -or- + /// + /// + /// This method was already called for . + /// /// /// /// This listener has been closed. /// public HttpListenerContext EndGetContext (IAsyncResult asyncResult) { - CheckDisposed (); + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (!_isListening) { + var msg = "The listener has not been started."; + + throw new InvalidOperationException (msg); + } + if (asyncResult == null) throw new ArgumentNullException ("asyncResult"); var ares = asyncResult as HttpListenerAsyncResult; - if (ares == null) - throw new ArgumentException ("A wrong IAsyncResult.", "asyncResult"); - if (ares.EndCalled) - throw new InvalidOperationException ("This IAsyncResult cannot be reused."); + if (ares == null) { + var msg = "A wrong IAsyncResult instance."; + + throw new ArgumentException (msg, "asyncResult"); + } + + lock (ares.SyncRoot) { + if (ares.EndCalled) { + var msg = "This IAsyncResult instance cannot be reused."; + + throw new InvalidOperationException (msg); + } + + ares.EndCalled = true; + } - ares.EndCalled = true; if (!ares.IsCompleted) ares.AsyncWaitHandle.WaitOne (); - return ares.GetContext (); // This may throw an exception. + return ares.Context; } /// /// Gets an incoming request. /// /// - /// This method waits for an incoming request, and returns when a request is received. + /// This method waits for an incoming request and returns when + /// a request is received. /// /// /// A that represents a request. /// + /// + /// This method is canceled. + /// /// /// - /// This listener has no URI prefix on which listens. + /// This listener has not been started or is currently stopped. /// /// /// -or- /// /// - /// This listener hasn't been started, or is currently stopped. + /// This listener has no URI prefix on which listens. /// /// /// @@ -747,10 +924,29 @@ public HttpListenerContext EndGetContext (IAsyncResult asyncResult) /// public HttpListenerContext GetContext () { - var ares = BeginGetContext (new HttpListenerAsyncResult (null, null)); - ares.InGet = true; + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (!_isListening) { + var msg = "The listener has not been started."; + + throw new InvalidOperationException (msg); + } + + if (_prefixes.Count == 0) { + var msg = "The listener has no URI prefix on which listens."; + + throw new InvalidOperationException (msg); + } + + var ares = beginGetContext (null, null); - return EndGetContext (ares); + ares.EndCalled = true; + + if (!ares.IsCompleted) + ares.AsyncWaitHandle.WaitOne (); + + return ares.Context; } /// @@ -761,12 +957,22 @@ public HttpListenerContext GetContext () /// public void Start () { - CheckDisposed (); - if (_listening) - return; + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + lock (_sync) { + if (_disposed) + throw new ObjectDisposedException (ObjectName); - EndPointManager.AddListener (this); - _listening = true; + lock (_contextRegistrySync) { + if (_isListening) + return; + + EndPointManager.AddListener (this); + + _isListening = true; + } + } } /// @@ -777,13 +983,29 @@ public void Start () /// public void Stop () { - CheckDisposed (); - if (!_listening) - return; + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + lock (_sync) { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + lock (_contextRegistrySync) { + if (!_isListening) + return; - _listening = false; - EndPointManager.RemoveListener (this); - sendServiceUnavailable (); + _isListening = false; + } + + cleanupContextQueue (false); + cleanupContextRegistry (); + + var msg = "The listener is stopped."; + + cleanupWaitQueue (msg); + + EndPointManager.RemoveListener (this); + } } #endregion @@ -799,7 +1021,6 @@ void IDisposable.Dispose () return; close (true); - _disposed = true; } #endregion diff --git a/websocket-sharp/Net/HttpListenerAsyncResult.cs b/websocket-sharp/Net/HttpListenerAsyncResult.cs index 569bac314..e4742d77b 100644 --- a/websocket-sharp/Net/HttpListenerAsyncResult.cs +++ b/websocket-sharp/Net/HttpListenerAsyncResult.cs @@ -8,7 +8,7 @@ * The MIT License * * Copyright (c) 2005 Ximian, Inc. (http://www.ximian.com) - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2024 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 @@ -45,7 +45,6 @@ #endregion using System; -using System.Security.Principal; using System.Threading; namespace WebSocketSharp.Net @@ -56,23 +55,29 @@ internal class HttpListenerAsyncResult : IAsyncResult private AsyncCallback _callback; private bool _completed; + private bool _completedSynchronously; private HttpListenerContext _context; private bool _endCalled; private Exception _exception; - private bool _inGet; + private Logger _log; private object _state; private object _sync; - private bool _syncCompleted; private ManualResetEvent _waitHandle; #endregion #region Internal Constructors - internal HttpListenerAsyncResult (AsyncCallback callback, object state) + internal HttpListenerAsyncResult ( + AsyncCallback callback, + object state, + Logger log + ) { _callback = callback; _state = state; + _log = log; + _sync = new object (); } @@ -80,6 +85,16 @@ internal HttpListenerAsyncResult (AsyncCallback callback, object state) #region Internal Properties + internal HttpListenerContext Context + { + get { + if (_exception != null) + throw _exception; + + return _context; + } + } + internal bool EndCalled { get { return _endCalled; @@ -90,13 +105,9 @@ internal bool EndCalled { } } - internal bool InGet { + internal object SyncRoot { get { - return _inGet; - } - - set { - _inGet = value; + return _sync; } } @@ -112,14 +123,18 @@ public object AsyncState { public WaitHandle AsyncWaitHandle { get { - lock (_sync) - return _waitHandle ?? (_waitHandle = new ManualResetEvent (_completed)); + lock (_sync) { + if (_waitHandle == null) + _waitHandle = new ManualResetEvent (_completed); + + return _waitHandle; + } } } public bool CompletedSynchronously { get { - return _syncCompleted; + return _completedSynchronously; } } @@ -134,25 +149,30 @@ public bool IsCompleted { #region Private Methods - private static void complete (HttpListenerAsyncResult asyncResult) + private void complete () { - asyncResult._completed = true; - - var waitHandle = asyncResult._waitHandle; - if (waitHandle != null) - waitHandle.Set (); - - var callback = asyncResult._callback; - if (callback != null) - ThreadPool.QueueUserWorkItem ( - state => { - try { - callback (asyncResult); - } - catch { - } - }, - null); + lock (_sync) { + _completed = true; + + if (_waitHandle != null) + _waitHandle.Set (); + } + + if (_callback == null) + return; + + ThreadPool.QueueUserWorkItem ( + state => { + try { + _callback (this); + } + catch (Exception ex) { + _log.Error (ex.Message); + _log.Debug (ex.ToString ()); + } + }, + null + ); } #endregion @@ -161,40 +181,20 @@ private static void complete (HttpListenerAsyncResult asyncResult) internal void Complete (Exception exception) { - _exception = _inGet && (exception is ObjectDisposedException) - ? new HttpListenerException (500, "Listener closed.") - : exception; + _exception = exception; - lock (_sync) - complete (this); + complete (); } - internal void Complete (HttpListenerContext context) + internal void Complete ( + HttpListenerContext context, + bool completedSynchronously + ) { - Complete (context, false); - } - - internal void Complete (HttpListenerContext context, bool syncCompleted) - { - var lsnr = context.Listener; - if (!lsnr.Authenticate (context)) { - lsnr.BeginGetContext (this); - return; - } - _context = context; - _syncCompleted = syncCompleted; - - lock (_sync) - complete (this); - } - - internal HttpListenerContext GetContext () - { - if (_exception != null) - throw _exception; + _completedSynchronously = completedSynchronously; - return _context; + complete (); } #endregion diff --git a/websocket-sharp/Net/HttpListenerContext.cs b/websocket-sharp/Net/HttpListenerContext.cs index b5b86b614..82ea0176c 100644 --- a/websocket-sharp/Net/HttpListenerContext.cs +++ b/websocket-sharp/Net/HttpListenerContext.cs @@ -8,7 +8,7 @@ * The MIT License * * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2024 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 @@ -39,28 +39,30 @@ using System; using System.Security.Principal; +using System.Text; using WebSocketSharp.Net.WebSockets; namespace WebSocketSharp.Net { /// - /// Provides the access to the HTTP request and response information - /// used by the . + /// Provides the access to the HTTP request and response objects used by + /// the class. /// /// - /// The HttpListenerContext class cannot be inherited. + /// This class cannot be inherited. /// public sealed class HttpListenerContext { #region Private Fields - private HttpConnection _connection; - private string _error; - private int _errorStatus; - private HttpListener _listener; - private HttpListenerRequest _request; - private HttpListenerResponse _response; - private IPrincipal _user; + private HttpConnection _connection; + private string _errorMessage; + private int _errorStatusCode; + private HttpListener _listener; + private HttpListenerRequest _request; + private HttpListenerResponse _response; + private IPrincipal _user; + private HttpListenerWebSocketContext _websocketContext; #endregion @@ -69,7 +71,8 @@ public sealed class HttpListenerContext internal HttpListenerContext (HttpConnection connection) { _connection = connection; - _errorStatus = 400; + + _errorStatusCode = 400; _request = new HttpListenerRequest (this); _response = new HttpListenerResponse (this); } @@ -86,27 +89,27 @@ internal HttpConnection Connection { internal string ErrorMessage { get { - return _error; + return _errorMessage; } set { - _error = value; + _errorMessage = value; } } - internal int ErrorStatus { + internal int ErrorStatusCode { get { - return _errorStatus; + return _errorStatusCode; } set { - _errorStatus = value; + _errorStatusCode = value; } } - internal bool HasError { + internal bool HasErrorMessage { get { - return _error != null; + return _errorMessage != null; } } @@ -125,10 +128,10 @@ internal HttpListener Listener { #region Public Properties /// - /// Gets the HTTP request information from a client. + /// Gets the HTTP request object that represents a client request. /// /// - /// A that represents the HTTP request. + /// A that represents the client request. /// public HttpListenerRequest Request { get { @@ -137,10 +140,11 @@ public HttpListenerRequest Request { } /// - /// Gets the HTTP response information used to send to the client. + /// Gets the HTTP response object used to send a response to the client. /// /// - /// A that represents the HTTP response to send. + /// A that represents a response to + /// the client request. /// public HttpListenerResponse Response { get { @@ -149,19 +153,143 @@ public HttpListenerResponse Response { } /// - /// Gets the client information (identity, authentication, and security roles). + /// Gets the client information. /// /// - /// A instance that represents the client information. + /// + /// A instance that represents identity, + /// authentication, and security roles for the client. + /// + /// + /// if the client is not authenticated. + /// /// public IPrincipal User { get { return _user; } + } + + #endregion + + #region Private Methods + + private static string createErrorContent ( + int statusCode, + string statusDescription, + string message + ) + { + return message != null && message.Length > 0 + ? String.Format ( + "

{0} {1} ({2})

", + statusCode, + statusDescription, + message + ) + : String.Format ( + "

{0} {1}

", + statusCode, + statusDescription + ); + } + + #endregion + + #region Internal Methods + + internal HttpListenerWebSocketContext GetWebSocketContext (string protocol) + { + _websocketContext = new HttpListenerWebSocketContext (this, protocol); + + return _websocketContext; + } + + internal void SendAuthenticationChallenge ( + AuthenticationSchemes scheme, + string realm + ) + { + _response.StatusCode = 401; + + var val = new AuthenticationChallenge (scheme, realm).ToString (); + + _response.Headers.InternalSet ("WWW-Authenticate", val, true); + + _response.Close (); + } + + internal void SendError () + { + try { + _response.StatusCode = _errorStatusCode; + _response.ContentType = "text/html"; + + var content = createErrorContent ( + _errorStatusCode, + _response.StatusDescription, + _errorMessage + ); + + var enc = Encoding.UTF8; + var entity = enc.GetBytes (content); + + _response.ContentEncoding = enc; + _response.ContentLength64 = entity.LongLength; - internal set { - _user = value; + _response.Close (entity, true); } + catch { + _connection.Close (true); + } + } + + internal void SendError (int statusCode) + { + _errorStatusCode = statusCode; + + SendError (); + } + + internal void SendError (int statusCode, string message) + { + _errorStatusCode = statusCode; + _errorMessage = message; + + SendError (); + } + + internal bool SetUser ( + AuthenticationSchemes scheme, + string realm, + Func credentialsFinder + ) + { + var user = HttpUtility.CreateUser ( + _request.Headers["Authorization"], + scheme, + realm, + _request.HttpMethod, + credentialsFinder + ); + + if (user == null) + return false; + + if (!user.Identity.IsAuthenticated) + return false; + + _user = user; + + return true; + } + + internal void Unregister () + { + if (_listener == null) + return; + + _listener.UnregisterContext (this); } #endregion @@ -169,18 +297,24 @@ internal set { #region Public Methods /// - /// Accepts a WebSocket connection request. + /// Accepts a WebSocket connection. /// /// - /// A that represents the WebSocket connection - /// request. + /// A that represents + /// the WebSocket handshake request. /// /// - /// A that represents the subprotocol used in the WebSocket connection. + /// + /// A that specifies the name of the subprotocol + /// supported on the WebSocket connection. + /// + /// + /// if not necessary. + /// /// /// /// - /// is empty. + /// is an empty string. /// /// /// -or- @@ -189,17 +323,128 @@ internal set { /// contains an invalid character. /// /// + /// + /// + /// This method has already been done. + /// + /// + /// -or- + /// + /// + /// The client request is not a WebSocket handshake request. + /// + /// public HttpListenerWebSocketContext AcceptWebSocket (string protocol) { + return AcceptWebSocket (protocol, null); + } + + /// + /// Accepts a WebSocket connection with initializing the WebSocket + /// interface. + /// + /// + /// A that represents + /// the WebSocket handshake request. + /// + /// + /// + /// A that specifies the name of the subprotocol + /// supported on the WebSocket connection. + /// + /// + /// if not necessary. + /// + /// + /// + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when a new WebSocket instance is + /// initialized. + /// + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// -or- + /// + /// + /// caused an exception. + /// + /// + /// + /// + /// This method has already been done. + /// + /// + /// -or- + /// + /// + /// The client request is not a WebSocket handshake request. + /// + /// + public HttpListenerWebSocketContext AcceptWebSocket ( + string protocol, + Action initializer + ) + { + if (_websocketContext != null) { + var msg = "The method has already been done."; + + throw new InvalidOperationException (msg); + } + + if (!_request.IsWebSocketRequest) { + var msg = "The request is not a WebSocket handshake request."; + + throw new InvalidOperationException (msg); + } + if (protocol != null) { - if (protocol.Length == 0) - throw new ArgumentException ("An empty string.", "protocol"); + if (protocol.Length == 0) { + var msg = "An empty string."; + + throw new ArgumentException (msg, "protocol"); + } + + if (!protocol.IsToken ()) { + var msg = "It contains an invalid character."; - if (!protocol.IsToken ()) - throw new ArgumentException ("Contains an invalid character.", "protocol"); + throw new ArgumentException (msg, "protocol"); + } } - return new HttpListenerWebSocketContext (this, protocol); + var ret = GetWebSocketContext (protocol); + + var ws = ret.WebSocket; + + if (initializer != null) { + try { + initializer (ws); + } + catch (Exception ex) { + if (ws.ReadyState == WebSocketState.New) + _websocketContext = null; + + var msg = "It caused an exception."; + + throw new ArgumentException (msg, "initializer", ex); + } + } + + ws.Accept (); + + return ret; } #endregion diff --git a/websocket-sharp/Net/HttpListenerException.cs b/websocket-sharp/Net/HttpListenerException.cs index a52eeec03..dec858d53 100644 --- a/websocket-sharp/Net/HttpListenerException.cs +++ b/websocket-sharp/Net/HttpListenerException.cs @@ -2,13 +2,13 @@ /* * HttpListenerException.cs * - * This code is derived from System.Net.HttpListenerException.cs of Mono + * This code is derived from HttpListenerException.cs (System.Net) of Mono * (http://www.mono-project.com). * * The MIT License * * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) - * Copyright (c) 2012-2014 sta.blockhead + * Copyright (c) 2012-2024 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 @@ -44,8 +44,8 @@ namespace WebSocketSharp.Net { /// - /// The exception that is thrown when a gets an error - /// processing an HTTP request. + /// The exception that is thrown when an error occurs processing + /// an HTTP request. /// [Serializable] public class HttpListenerException : Win32Exception @@ -53,17 +53,24 @@ public class HttpListenerException : Win32Exception #region Protected Constructors /// - /// Initializes a new instance of the class from - /// the specified and . + /// Initializes a new instance of the + /// class with the specified serialized data. /// /// - /// A that contains the serialized object data. + /// A that contains the serialized + /// object data. /// /// - /// A that specifies the source for the deserialization. + /// A that specifies the source for + /// the deserialization. /// + /// + /// is . + /// protected HttpListenerException ( - SerializationInfo serializationInfo, StreamingContext streamingContext) + SerializationInfo serializationInfo, + StreamingContext streamingContext + ) : base (serializationInfo, streamingContext) { } @@ -73,18 +80,19 @@ protected HttpListenerException ( #region Public Constructors /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the + /// class. /// public HttpListenerException () { } /// - /// Initializes a new instance of the class - /// with the specified . + /// Initializes a new instance of the + /// class with the specified error code. /// /// - /// An that identifies the error. + /// An that specifies the error code. /// public HttpListenerException (int errorCode) : base (errorCode) @@ -92,14 +100,14 @@ public HttpListenerException (int errorCode) } /// - /// Initializes a new instance of the class - /// with the specified and . + /// Initializes a new instance of the + /// class with the specified error code and message. /// /// - /// An that identifies the error. + /// An that specifies the error code. /// /// - /// A that describes the error. + /// A that specifies the message. /// public HttpListenerException (int errorCode, string message) : base (errorCode, message) @@ -114,7 +122,12 @@ public HttpListenerException (int errorCode, string message) /// Gets the error code that identifies the error that occurred. ///
/// - /// An that identifies the error. + /// + /// An that represents the error code. + /// + /// + /// It is any of the Win32 error codes. + /// /// public override int ErrorCode { get { diff --git a/websocket-sharp/Net/HttpListenerPrefix.cs b/websocket-sharp/Net/HttpListenerPrefix.cs index dd9699b2f..aba9d1bfd 100644 --- a/websocket-sharp/Net/HttpListenerPrefix.cs +++ b/websocket-sharp/Net/HttpListenerPrefix.cs @@ -8,7 +8,7 @@ * The MIT License * * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2024 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 @@ -39,7 +39,6 @@ #endregion using System; -using System.Net; namespace WebSocketSharp.Net { @@ -47,22 +46,24 @@ internal sealed class HttpListenerPrefix { #region Private Fields - private IPAddress[] _addresses; private string _host; + private bool _isSecure; private HttpListener _listener; private string _original; private string _path; - private ushort _port; - private bool _secure; + private string _port; + private string _prefix; + private string _scheme; #endregion #region Internal Constructors - // Must be called after calling the CheckPrefix method. - internal HttpListenerPrefix (string uriPrefix) + internal HttpListenerPrefix (string uriPrefix, HttpListener listener) { _original = uriPrefix; + _listener = listener; + parse (uriPrefix); } @@ -70,16 +71,6 @@ internal HttpListenerPrefix (string uriPrefix) #region Public Properties - public IPAddress[] Addresses { - get { - return _addresses; - } - - set { - _addresses = value; - } - } - public string Host { get { return _host; @@ -88,7 +79,7 @@ public string Host { public bool IsSecure { get { - return _secure; + return _isSecure; } } @@ -96,9 +87,11 @@ public HttpListener Listener { get { return _listener; } + } - set { - _listener = value; + public string Original { + get { + return _original; } } @@ -108,9 +101,15 @@ public string Path { } } - public int Port { + public string Port { get { - return (int) _port; + return _port; + } + } + + public string Scheme { + get { + return _scheme; } } @@ -120,30 +119,36 @@ public int Port { private void parse (string uriPrefix) { - var defaultPort = uriPrefix.StartsWith ("https://") ? 443 : 80; - if (defaultPort == 443) - _secure = true; + var compType = StringComparison.Ordinal; + + _isSecure = uriPrefix.StartsWith ("https", compType); + _scheme = _isSecure ? "https" : "http"; + + var hostStartIdx = uriPrefix.IndexOf (':') + 3; var len = uriPrefix.Length; - var startHost = uriPrefix.IndexOf (':') + 3; - var colon = uriPrefix.IndexOf (':', startHost, len - startHost); - var root = 0; - if (colon > 0) { - root = uriPrefix.IndexOf ('/', colon, len - colon); - _host = uriPrefix.Substring (startHost, colon - startHost); - _port = (ushort) Int32.Parse (uriPrefix.Substring (colon + 1, root - colon - 1)); + var rootIdx = uriPrefix + .IndexOf ('/', hostStartIdx + 1, len - hostStartIdx - 1); + + var colonIdx = uriPrefix + .LastIndexOf (':', rootIdx - 1, rootIdx - hostStartIdx - 1); + + var hasPort = uriPrefix[rootIdx - 1] != ']' && colonIdx > hostStartIdx; + + if (hasPort) { + _host = uriPrefix.Substring (hostStartIdx, colonIdx - hostStartIdx); + _port = uriPrefix.Substring (colonIdx + 1, rootIdx - colonIdx - 1); } else { - root = uriPrefix.IndexOf ('/', startHost, len - startHost); - _host = uriPrefix.Substring (startHost, root - startHost); - _port = (ushort) defaultPort; + _host = uriPrefix.Substring (hostStartIdx, rootIdx - hostStartIdx); + _port = _isSecure ? "443" : "80"; } - _path = uriPrefix.Substring (root); + _path = uriPrefix.Substring (rootIdx); + + var fmt = "{0}://{1}:{2}{3}"; - var pathLen = _path.Length; - if (pathLen > 1) - _path = _path.Substring (0, pathLen - 1); + _prefix = String.Format (fmt, _scheme, _host, _port, _path); } #endregion @@ -156,55 +161,81 @@ public static void CheckPrefix (string uriPrefix) throw new ArgumentNullException ("uriPrefix"); var len = uriPrefix.Length; - if (len == 0) - throw new ArgumentException ("An empty string."); - if (!(uriPrefix.StartsWith ("http://") || uriPrefix.StartsWith ("https://"))) - throw new ArgumentException ("The scheme isn't 'http' or 'https'."); + if (len == 0) { + var msg = "An empty string."; - var startHost = uriPrefix.IndexOf (':') + 3; - if (startHost >= len) - throw new ArgumentException ("No host is specified."); + throw new ArgumentException (msg, "uriPrefix"); + } - var colon = uriPrefix.IndexOf (':', startHost, len - startHost); - if (startHost == colon) - throw new ArgumentException ("No host is specified."); + var compType = StringComparison.Ordinal; + var isHttpSchm = uriPrefix.StartsWith ("http://", compType) + || uriPrefix.StartsWith ("https://", compType); - if (colon > 0) { - var root = uriPrefix.IndexOf ('/', colon, len - colon); - if (root == -1) - throw new ArgumentException ("No path is specified."); + if (!isHttpSchm) { + var msg = "The scheme is not http or https."; - int port; - if (!Int32.TryParse (uriPrefix.Substring (colon + 1, root - colon - 1), out port) || - !port.IsPortNumber ()) - throw new ArgumentException ("An invalid port is specified."); + throw new ArgumentException (msg, "uriPrefix"); } - else { - var root = uriPrefix.IndexOf ('/', startHost, len - startHost); - if (root == -1) - throw new ArgumentException ("No path is specified."); + + var endIdx = len - 1; + + if (uriPrefix[endIdx] != '/') { + var msg = "It ends without a forward slash."; + + throw new ArgumentException (msg, "uriPrefix"); + } + + var hostStartIdx = uriPrefix.IndexOf (':') + 3; + + if (hostStartIdx >= endIdx) { + var msg = "No host is specified."; + + throw new ArgumentException (msg, "uriPrefix"); + } + + if (uriPrefix[hostStartIdx] == ':') { + var msg = "No host is specified."; + + throw new ArgumentException (msg, "uriPrefix"); } - if (uriPrefix[len - 1] != '/') - throw new ArgumentException ("Ends without '/'."); + var rootIdx = uriPrefix.IndexOf ('/', hostStartIdx, len - hostStartIdx); + + if (rootIdx == hostStartIdx) { + var msg = "No host is specified."; + + throw new ArgumentException (msg, "uriPrefix"); + } + + if (uriPrefix[rootIdx - 1] == ':') { + var msg = "No port is specified."; + + throw new ArgumentException (msg, "uriPrefix"); + } + + if (rootIdx == endIdx - 1) { + var msg = "No path is specified."; + + throw new ArgumentException (msg, "uriPrefix"); + } } - // The Equals and GetHashCode methods are required to detect duplicates in any collection. - public override bool Equals (Object obj) + public override bool Equals (object obj) { var pref = obj as HttpListenerPrefix; - return pref != null && pref._original == _original; + + return pref != null && _prefix.Equals (pref._prefix); } public override int GetHashCode () { - return _original.GetHashCode (); + return _prefix.GetHashCode (); } public override string ToString () { - return _original; + return _prefix; } #endregion diff --git a/websocket-sharp/Net/HttpListenerPrefixCollection.cs b/websocket-sharp/Net/HttpListenerPrefixCollection.cs index 6373b8d65..3f5c44a2c 100644 --- a/websocket-sharp/Net/HttpListenerPrefixCollection.cs +++ b/websocket-sharp/Net/HttpListenerPrefixCollection.cs @@ -8,7 +8,7 @@ * The MIT License * * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2024 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 @@ -44,13 +44,14 @@ namespace WebSocketSharp.Net { /// - /// Provides the collection used to store the URI prefixes for the . + /// Provides a collection used to store the URI prefixes for a instance of + /// the class. /// /// - /// The responds to the request which has a requested URI that - /// the prefixes most closely match. + /// The instance responds to the request which + /// has a requested URI that the prefixes most closely match. /// - public class HttpListenerPrefixCollection : ICollection, IEnumerable, IEnumerable + public class HttpListenerPrefixCollection : ICollection { #region Private Fields @@ -64,6 +65,7 @@ public class HttpListenerPrefixCollection : ICollection, IEnumerable (); } @@ -84,7 +86,8 @@ public int Count { } /// - /// Gets a value indicating whether the access to the collection is read-only. + /// Gets a value indicating whether the access to the collection is + /// read-only. /// /// /// Always returns false. @@ -96,7 +99,8 @@ public bool IsReadOnly { } /// - /// Gets a value indicating whether the access to the collection is synchronized. + /// Gets a value indicating whether the access to the collection is + /// synchronized. /// /// /// Always returns false. @@ -112,67 +116,81 @@ public bool IsSynchronized { #region Public Methods /// - /// Adds the specified to the collection. + /// Adds the specified URI prefix to the collection. /// /// - /// A that represents the URI prefix to add. The prefix must be - /// a well-formed URI prefix with http or https scheme, and must end with a '/'. + /// + /// A that specifies the URI prefix to add. + /// + /// + /// It must be a well-formed URI prefix with http or https scheme, + /// and must end with a forward slash (/). + /// /// - /// - /// is . - /// /// /// is invalid. /// + /// + /// is . + /// /// - /// The associated with this collection is closed. + /// The instance associated with this + /// collection is closed. /// public void Add (string uriPrefix) { _listener.CheckDisposed (); + HttpListenerPrefix.CheckPrefix (uriPrefix); + if (_prefixes.Contains (uriPrefix)) return; - _prefixes.Add (uriPrefix); if (_listener.IsListening) EndPointManager.AddPrefix (uriPrefix, _listener); + + _prefixes.Add (uriPrefix); } /// /// Removes all URI prefixes from the collection. /// /// - /// The associated with this collection is closed. + /// The instance associated with this + /// collection is closed. /// public void Clear () { _listener.CheckDisposed (); - _prefixes.Clear (); + if (_listener.IsListening) EndPointManager.RemoveListener (_listener); + + _prefixes.Clear (); } /// - /// Returns a value indicating whether the collection contains the specified - /// . + /// Returns a value indicating whether the collection contains the + /// specified URI prefix. /// /// - /// true if the collection contains ; - /// otherwise, false. + /// true if the collection contains the URI prefix; otherwise, + /// false. /// /// - /// A that represents the URI prefix to test. + /// A that specifies the URI prefix to test. /// /// /// is . /// /// - /// The associated with this collection is closed. + /// The instance associated with this + /// collection is closed. /// public bool Contains (string uriPrefix) { _listener.CheckDisposed (); + if (uriPrefix == null) throw new ArgumentNullException ("uriPrefix"); @@ -180,49 +198,43 @@ public bool Contains (string uriPrefix) } /// - /// Copies the contents of the collection to the specified . + /// Copies the contents of the collection to the specified array of string. /// /// - /// An that receives the URI prefix strings in the collection. + /// An array of that specifies the destination of + /// the URI prefix strings copied from the collection. /// /// - /// An that represents the zero-based index in - /// at which copying begins. + /// An that specifies the zero-based index in + /// the array at which copying begins. /// - /// - /// The associated with this collection is closed. + /// + /// The space from to the end of + /// is not enough to copy to. + /// + /// + /// is . + /// + /// + /// is less than zero. /// - public void CopyTo (Array array, int offset) - { - _listener.CheckDisposed (); - ((ICollection) _prefixes).CopyTo (array, offset); - } - - /// - /// Copies the contents of the collection to the specified array of . - /// - /// - /// An array of that receives the URI prefix strings in the collection. - /// - /// - /// An that represents the zero-based index in - /// at which copying begins. - /// /// - /// The associated with this collection is closed. + /// The instance associated with this + /// collection is closed. /// public void CopyTo (string[] array, int offset) { _listener.CheckDisposed (); + _prefixes.CopyTo (array, offset); } /// - /// Gets the enumerator used to iterate through the . + /// Gets the enumerator that iterates through the collection. /// /// - /// An instance used to iterate - /// through the collection. + /// An + /// instance that can be used to iterate through the collection. /// public IEnumerator GetEnumerator () { @@ -230,32 +242,36 @@ public IEnumerator GetEnumerator () } /// - /// Removes the specified from the collection. + /// Removes the specified URI prefix from the collection. /// /// - /// true if is successfully found and removed; - /// otherwise, false. + /// true if the URI prefix is successfully removed; otherwise, + /// false. /// /// - /// A that represents the URI prefix to remove. + /// A that specifies the URI prefix to remove. /// /// /// is . /// /// - /// The associated with this collection is closed. + /// The instance associated with this + /// collection is closed. /// public bool Remove (string uriPrefix) { _listener.CheckDisposed (); + if (uriPrefix == null) throw new ArgumentNullException ("uriPrefix"); - var ret = _prefixes.Remove (uriPrefix); - if (ret && _listener.IsListening) + if (!_prefixes.Contains (uriPrefix)) + return false; + + if (_listener.IsListening) EndPointManager.RemovePrefix (uriPrefix, _listener); - return ret; + return _prefixes.Remove (uriPrefix); } #endregion @@ -263,10 +279,11 @@ public bool Remove (string uriPrefix) #region Explicit Interface Implementations /// - /// Gets the enumerator used to iterate through the . + /// Gets the enumerator that iterates through the collection. /// /// - /// An instance used to iterate through the collection. + /// An instance that can be used to iterate + /// through the collection. /// IEnumerator IEnumerable.GetEnumerator () { diff --git a/websocket-sharp/Net/HttpListenerRequest.cs b/websocket-sharp/Net/HttpListenerRequest.cs index 701128120..9c7b1a16d 100644 --- a/websocket-sharp/Net/HttpListenerRequest.cs +++ b/websocket-sharp/Net/HttpListenerRequest.cs @@ -8,7 +8,7 @@ * The MIT License * * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2024 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 @@ -48,37 +48,37 @@ namespace WebSocketSharp.Net { /// - /// Provides the access to a request to the . + /// Represents an incoming HTTP request to a + /// instance. /// /// - /// The HttpListenerRequest class cannot be inherited. + /// This class cannot be inherited. /// public sealed class HttpListenerRequest { #region Private Fields - private static readonly byte[] _100continue; - private string[] _acceptTypes; - private bool _chunked; - private Encoding _contentEncoding; - private long _contentLength; - private bool _contentLengthSet; - private HttpListenerContext _context; - private CookieCollection _cookies; - private WebHeaderCollection _headers; - private Guid _identifier; - private Stream _inputStream; - private bool _keepAlive; - private bool _keepAliveSet; - private string _method; - private NameValueCollection _queryString; - private Uri _referer; - private string _uri; - private Uri _url; - private string[] _userLanguages; - private Version _version; - private bool _websocketRequest; - private bool _websocketRequestSet; + private static readonly byte[] _100continue; + private string[] _acceptTypes; + private bool _chunked; + private HttpConnection _connection; + private Encoding _contentEncoding; + private long _contentLength; + private HttpListenerContext _context; + private CookieCollection _cookies; + private static readonly Encoding _defaultEncoding; + private WebHeaderCollection _headers; + private string _httpMethod; + private Stream _inputStream; + private Version _protocolVersion; + private NameValueCollection _queryString; + private string _rawUrl; + private Guid _requestTraceIdentifier; + private Uri _url; + private Uri _urlReferrer; + private bool _urlSet; + private string _userHostName; + private string[] _userLanguages; #endregion @@ -87,6 +87,7 @@ public sealed class HttpListenerRequest static HttpListenerRequest () { _100continue = Encoding.ASCII.GetBytes ("HTTP/1.1 100 Continue\r\n\r\n"); + _defaultEncoding = Encoding.UTF8; } #endregion @@ -96,9 +97,11 @@ static HttpListenerRequest () internal HttpListenerRequest (HttpListenerContext context) { _context = context; + + _connection = context.Connection; _contentLength = -1; _headers = new WebHeaderCollection (); - _identifier = Guid.NewGuid (); + _requestTraceIdentifier = Guid.NewGuid (); } #endregion @@ -106,28 +109,49 @@ internal HttpListenerRequest (HttpListenerContext context) #region Public Properties /// - /// Gets the media types which are acceptable for the response. + /// Gets the media types that are acceptable for the client. /// /// - /// An array of that contains the media type names in - /// the Accept request-header, or if the request didn't include - /// the Accept header. + /// + /// An array of that contains the names of + /// the media types specified in the value of the Accept header. + /// + /// + /// if the header is not present. + /// /// public string[] AcceptTypes { get { + var val = _headers["Accept"]; + + if (val == null) + return null; + + if (_acceptTypes == null) { + _acceptTypes = val + .SplitHeaderValue (',') + .TrimEach () + .ToList () + .ToArray (); + } + return _acceptTypes; } } /// - /// Gets an error code that identifies a problem with the client's certificate. + /// Gets an error code that identifies a problem with the certificate + /// provided by the client. /// /// - /// Always returns 0. + /// An that represents an error code. /// + /// + /// This property is not supported. + /// public int ClientCertificateError { get { - return 0; // TODO: Always returns 0. + throw new NotSupportedException (); } } @@ -135,22 +159,35 @@ public int ClientCertificateError { /// Gets the encoding for the entity body data included in the request. ///
/// - /// A that represents the encoding for the entity body data, - /// or if the request didn't include the information about - /// the encoding. + /// + /// A converted from the charset value of the + /// Content-Type header. + /// + /// + /// if the charset value is not available. + /// /// public Encoding ContentEncoding { get { - return _contentEncoding ?? (_contentEncoding = Encoding.Default); + if (_contentEncoding == null) + _contentEncoding = getContentEncoding (); + + return _contentEncoding; } } /// - /// Gets the number of bytes in the entity body data included in the request. + /// Gets the length in bytes of the entity body data included in the + /// request. /// /// - /// A that represents the value of the Content-Length entity-header, - /// or -1 if the value isn't known. + /// + /// A converted from the value of the Content-Length + /// header. + /// + /// + /// -1 if the header is not present. + /// /// public long ContentLength64 { get { @@ -159,10 +196,16 @@ public long ContentLength64 { } /// - /// Gets the media type of the entity body included in the request. + /// Gets the media type of the entity body data included in the request. /// /// - /// A that represents the value of the Content-Type entity-header. + /// + /// A that represents the value of the Content-Type + /// header. + /// + /// + /// if the header is not present. + /// /// public string ContentType { get { @@ -171,22 +214,31 @@ public string ContentType { } /// - /// Gets the cookies included in the request. + /// Gets the HTTP cookies included in the request. /// /// - /// A that contains the cookies included in the request. + /// + /// A that contains the cookies. + /// + /// + /// An empty collection if not included. + /// /// public CookieCollection Cookies { get { - return _cookies ?? (_cookies = _headers.GetCookies (false)); + if (_cookies == null) + _cookies = _headers.GetCookies (false); + + return _cookies; } } /// - /// Gets a value indicating whether the request has the entity body. + /// Gets a value indicating whether the request has the entity body data. /// /// - /// true if the request has the entity body; otherwise, false. + /// true if the request has the entity body data; otherwise, + /// false. /// public bool HasEntityBody { get { @@ -195,10 +247,10 @@ public bool HasEntityBody { } /// - /// Gets the HTTP headers used in the request. + /// Gets the HTTP headers included in the request. /// /// - /// A that contains the HTTP headers used in the request. + /// A that contains the headers. /// public NameValueCollection Headers { get { @@ -207,34 +259,45 @@ public NameValueCollection Headers { } /// - /// Gets the HTTP method used in the request. + /// Gets the HTTP method specified by the client. /// /// - /// A that represents the HTTP method used in the request. + /// A that represents the HTTP method specified in + /// the request line. /// public string HttpMethod { get { - return _method; + return _httpMethod; } } /// - /// Gets a that contains the entity body data included in the request. + /// Gets a stream that contains the entity body data included in + /// the request. /// /// - /// A that contains the entity body data included in the request. + /// + /// A that contains the entity body data. + /// + /// + /// if the entity body data is not available. + /// /// public Stream InputStream { get { - return _inputStream ?? - (_inputStream = HasEntityBody - ? _context.Connection.GetRequestStream (_contentLength, _chunked) - : Stream.Null); + if (_inputStream == null) { + _inputStream = _contentLength > 0 || _chunked + ? _connection + .GetRequestStream (_contentLength, _chunked) + : Stream.Null; + } + + return _inputStream; } } /// - /// Gets a value indicating whether the client that sent the request is authenticated. + /// Gets a value indicating whether the client is authenticated. /// /// /// true if the client is authenticated; otherwise, false. @@ -246,92 +309,82 @@ public bool IsAuthenticated { } /// - /// Gets a value indicating whether the request is sent from the local computer. + /// Gets a value indicating whether the request is sent from the + /// local computer. /// /// - /// true if the request is sent from the local computer; otherwise, false. + /// true if the request is sent from the same computer as + /// the server; otherwise, false. /// public bool IsLocal { get { - return RemoteEndPoint.Address.IsLocal (); + return _connection.IsLocal; } } /// - /// Gets a value indicating whether the HTTP connection is secured using the SSL protocol. + /// Gets a value indicating whether a secure connection is used to send + /// the request. /// /// - /// true if the HTTP connection is secured; otherwise, false. + /// true if the connection is secure; otherwise, false. /// public bool IsSecureConnection { get { - return _context.Connection.IsSecure; + return _connection.IsSecure; } } /// - /// Gets a value indicating whether the request is a WebSocket connection request. + /// Gets a value indicating whether the request is a WebSocket handshake + /// request. /// /// - /// true if the request is a WebSocket connection request; otherwise, false. + /// true if the request is a WebSocket handshake request; otherwise, + /// false. /// public bool IsWebSocketRequest { get { - if (!_websocketRequestSet) { - _websocketRequest = _method == "GET" && - _version > HttpVersion.Version10 && - _headers.Contains ("Upgrade", "websocket") && - _headers.Contains ("Connection", "Upgrade"); - - _websocketRequestSet = true; - } - - return _websocketRequest; + return _httpMethod == "GET" && _headers.Upgrades ("websocket"); } } /// - /// Gets a value indicating whether the client requests a persistent connection. + /// Gets a value indicating whether a persistent connection is requested. /// /// - /// true if the client requests a persistent connection; otherwise, false. + /// true if the request specifies that the connection is kept open; + /// otherwise, false. /// public bool KeepAlive { get { - if (!_keepAliveSet) { - string keepAlive; - _keepAlive = _version > HttpVersion.Version10 || - _headers.Contains ("Connection", "keep-alive") || - ((keepAlive = _headers["Keep-Alive"]) != null && keepAlive != "closed"); - - _keepAliveSet = true; - } - - return _keepAlive; + return _headers.KeepsAlive (_protocolVersion); } } /// - /// Gets the server endpoint as an IP address and a port number. + /// Gets the endpoint to which the request is sent. /// /// - /// A that represents the server endpoint. + /// A that represents the server + /// IP address and port number. /// public System.Net.IPEndPoint LocalEndPoint { get { - return _context.Connection.LocalEndPoint; + return _connection.LocalEndPoint; } } /// - /// Gets the HTTP version used in the request. + /// Gets the HTTP version specified by the client. /// /// - /// A that represents the HTTP version used in the request. + /// A that represents the HTTP version specified in + /// the request line. /// public Version ProtocolVersion { get { - return _version; + return _protocolVersion; } } @@ -339,48 +392,65 @@ public Version ProtocolVersion { /// Gets the query string included in the request. ///
/// - /// A that contains the query string parameters. + /// + /// A that contains the query + /// parameters. + /// + /// + /// Each query parameter is decoded in UTF-8. + /// + /// + /// An empty collection if not included. + /// /// public NameValueCollection QueryString { get { - return _queryString ?? - (_queryString = HttpUtility.InternalParseQueryString (_url.Query, Encoding.UTF8)); + if (_queryString == null) { + var url = Url; + var query = url != null ? url.Query : null; + + _queryString = QueryStringCollection.Parse (query, _defaultEncoding); + } + + return _queryString; } } /// - /// Gets the raw URL (without the scheme, host, and port) requested by the client. + /// Gets the raw URL specified by the client. /// /// - /// A that represents the raw URL requested by the client. + /// A that represents the request target specified in + /// the request line. /// public string RawUrl { get { - return _url.PathAndQuery; // TODO: Should decode? + return _rawUrl; } } /// - /// Gets the client endpoint as an IP address and a port number. + /// Gets the endpoint from which the request is sent. /// /// - /// A that represents the client endpoint. + /// A that represents the client + /// IP address and port number. /// public System.Net.IPEndPoint RemoteEndPoint { get { - return _context.Connection.RemoteEndPoint; + return _connection.RemoteEndPoint; } } /// - /// Gets the request identifier of a incoming HTTP request. + /// Gets the trace identifier of the request. /// /// - /// A that represents the identifier of a request. + /// A that represents the trace identifier. /// public Guid RequestTraceIdentifier { get { - return _identifier; + return _requestTraceIdentifier; } } @@ -388,32 +458,67 @@ public Guid RequestTraceIdentifier { /// Gets the URL requested by the client. /// /// - /// A that represents the URL requested by the client. + /// + /// A that represents the URL parsed from the request. + /// + /// + /// if the URL cannot be parsed. + /// /// public Uri Url { get { + if (!_urlSet) { + _url = HttpUtility + .CreateRequestUrl ( + _rawUrl, + _userHostName, + IsWebSocketRequest, + IsSecureConnection + ); + + _urlSet = true; + } + return _url; } } /// - /// Gets the URL of the resource from which the requested URL was obtained. + /// Gets the URI of the resource from which the requested URL was obtained. /// /// - /// A that represents the value of the Referer request-header, - /// or if the request didn't include an Referer header. + /// + /// A that represents the value of the Referer header. + /// + /// + /// if the header value is not available. + /// /// public Uri UrlReferrer { get { - return _referer; + var val = _headers["Referer"]; + + if (val == null) + return null; + + if (_urlReferrer == null) + _urlReferrer = val.ToUri (); + + return _urlReferrer; } } /// - /// Gets the information about the user agent originating the request. + /// Gets the user agent from which the request is originated. /// /// - /// A that represents the value of the User-Agent request-header. + /// + /// A that represents the value of the User-Agent + /// header. + /// + /// + /// if the header is not present. + /// /// public string UserAgent { get { @@ -422,39 +527,58 @@ public string UserAgent { } /// - /// Gets the server endpoint as an IP address and a port number. + /// Gets the IP address and port number to which the request is sent. /// /// - /// A that represents the server endpoint. + /// A that represents the server IP address and + /// port number. /// public string UserHostAddress { get { - return LocalEndPoint.ToString (); + return _connection.LocalEndPoint.ToString (); } } /// - /// Gets the internet host name and port number (if present) specified by the client. + /// Gets the server host name requested by the client. /// /// - /// A that represents the value of the Host request-header. + /// + /// A that represents the value of the Host header. + /// + /// + /// It includes the port number if provided. + /// /// public string UserHostName { get { - return _headers["Host"]; + return _userHostName; } } /// - /// Gets the natural languages which are preferred for the response. + /// Gets the natural languages that are acceptable for the client. /// /// - /// An array of that contains the natural language names in - /// the Accept-Language request-header, or if the request - /// didn't include an Accept-Language header. + /// + /// An array of that contains the names of the + /// natural languages specified in the value of the Accept-Language + /// header. + /// + /// + /// if the header is not present. + /// /// public string[] UserLanguages { get { + var val = _headers["Accept-Language"]; + + if (val == null) + return null; + + if (_userLanguages == null) + _userLanguages = val.Split (',').TrimEach ().ToList ().ToArray (); + return _userLanguages; } } @@ -463,138 +587,186 @@ public string[] UserLanguages { #region Private Methods - private static bool tryCreateVersion (string version, out Version result) + private Encoding getContentEncoding () { - try { - result = new Version (version); - return true; - } - catch { - result = null; - return false; - } + var val = _headers["Content-Type"]; + + if (val == null) + return _defaultEncoding; + + Encoding ret; + + return HttpUtility.TryGetEncoding (val, out ret) + ? ret + : _defaultEncoding; } #endregion #region Internal Methods - internal void AddHeader (string header) + internal void AddHeader (string headerField) { - var colon = header.IndexOf (':'); - if (colon == -1) { - _context.ErrorMessage = "Invalid header"; + var start = headerField[0]; + + if (start == ' ' || start == '\t') { + _context.ErrorMessage = "Invalid header field"; + return; } - var name = header.Substring (0, colon).Trim (); - var val = header.Substring (colon + 1).Trim (); - _headers.InternalSet (name, val, false); + var colon = headerField.IndexOf (':'); + + if (colon < 1) { + _context.ErrorMessage = "Invalid header field"; - var lower = name.ToLower (CultureInfo.InvariantCulture); - if (lower == "accept") { - _acceptTypes = new List (val.SplitHeaderValue (',')).ToArray (); return; } - if (lower == "accept-language") { - _userLanguages = val.Split (','); + var name = headerField.Substring (0, colon).Trim (); + + if (name.Length == 0 || !name.IsToken ()) { + _context.ErrorMessage = "Invalid header name"; + return; } - if (lower == "content-length") { - long len; - if (Int64.TryParse (val, out len) && len >= 0) { - _contentLength = len; - _contentLengthSet = true; + var val = colon < headerField.Length - 1 + ? headerField.Substring (colon + 1).Trim () + : String.Empty; + + _headers.InternalSet (name, val, false); + + var lower = name.ToLower (CultureInfo.InvariantCulture); + + if (lower == "host") { + if (_userHostName != null) { + _context.ErrorMessage = "Invalid Host header"; + + return; } - else { - _context.ErrorMessage = "Invalid Content-Length header"; + + if (val.Length == 0) { + _context.ErrorMessage = "Invalid Host header"; + + return; } + _userHostName = val; + return; } - if (lower == "content-type") { - try { - _contentEncoding = HttpUtility.GetEncoding (val); + if (lower == "content-length") { + if (_contentLength > -1) { + _context.ErrorMessage = "Invalid Content-Length header"; + + return; } - catch { - _context.ErrorMessage = "Invalid Content-Type header"; + + long len; + + if (!Int64.TryParse (val, out len)) { + _context.ErrorMessage = "Invalid Content-Length header"; + + return; + } + + if (len < 0) { + _context.ErrorMessage = "Invalid Content-Length header"; + + return; } + _contentLength = len; + return; } - - if (lower == "referer") - _referer = val.ToUri (); } internal void FinishInitialization () { - var host = _headers["Host"]; - var nohost = host == null || host.Length == 0; - if (_version > HttpVersion.Version10 && nohost) { - _context.ErrorMessage = "Invalid Host header"; + if (_userHostName == null) { + _context.ErrorMessage = "Host header required"; + return; } - if (nohost) - host = UserHostAddress; + var transferEnc = _headers["Transfer-Encoding"]; - _url = HttpUtility.CreateRequestUrl (_uri, host, IsWebSocketRequest, IsSecureConnection); - if (_url == null) { - _context.ErrorMessage = "Invalid request url"; - return; - } + if (transferEnc != null) { + var compType = StringComparison.OrdinalIgnoreCase; - var enc = Headers["Transfer-Encoding"]; - if (_version > HttpVersion.Version10 && enc != null && enc.Length > 0) { - _chunked = enc.ToLower () == "chunked"; - if (!_chunked) { - _context.ErrorMessage = String.Empty; - _context.ErrorStatus = 501; + if (!transferEnc.Equals ("chunked", compType)) { + _context.ErrorStatusCode = 501; + _context.ErrorMessage = "Invalid Transfer-Encoding header"; return; } + + _chunked = true; } - if (!_chunked && !_contentLengthSet) { - var method = _method.ToLower (); - if (method == "post" || method == "put") { - _context.ErrorMessage = String.Empty; - _context.ErrorStatus = 411; + if (_httpMethod == "POST" || _httpMethod == "PUT") { + if (_contentLength == -1 && !_chunked) { + _context.ErrorStatusCode = 411; + _context.ErrorMessage = "Content-Length header required"; + + return; + } + + if (_contentLength == 0 && !_chunked) { + _context.ErrorStatusCode = 411; + _context.ErrorMessage = "Invalid Content-Length header"; return; } } - var expect = Headers["Expect"]; - if (expect != null && expect.Length > 0 && expect.ToLower () == "100-continue") { - var output = _context.Connection.GetResponseStream (); + var expect = _headers["Expect"]; + + if (expect != null) { + var compType = StringComparison.OrdinalIgnoreCase; + + if (!expect.Equals ("100-continue", compType)) { + _context.ErrorStatusCode = 417; + _context.ErrorMessage = "Invalid Expect header"; + + return; + } + + var output = _connection.GetResponseStream (); + output.InternalWrite (_100continue, 0, _100continue.Length); } } - // Returns true is the stream could be reused. internal bool FlushInput () { - if (!HasEntityBody) + var input = InputStream; + + if (input == Stream.Null) return true; var len = 2048; - if (_contentLength > 0) - len = (int) Math.Min (_contentLength, (long) len); + + if (_contentLength > 0 && _contentLength < len) + len = (int) _contentLength; var buff = new byte[len]; + while (true) { - // TODO: Test if MS has a timeout when doing this. try { - var ares = InputStream.BeginRead (buff, 0, len, null, null); - if (!ares.IsCompleted && !ares.AsyncWaitHandle.WaitOne (100)) - return false; + var ares = input.BeginRead (buff, 0, len, null, null); + + if (!ares.IsCompleted) { + var timeout = 100; - if (InputStream.EndRead (ares) <= 0) + if (!ares.AsyncWaitHandle.WaitOne (timeout)) + return false; + } + + if (input.EndRead (ares) <= 0) return true; } catch { @@ -603,28 +775,76 @@ internal bool FlushInput () } } + internal bool IsUpgradeRequest (string protocol) + { + return _headers.Upgrades (protocol); + } + internal void SetRequestLine (string requestLine) { var parts = requestLine.Split (new[] { ' ' }, 3); - if (parts.Length != 3) { + + if (parts.Length < 3) { _context.ErrorMessage = "Invalid request line (parts)"; + return; } - _method = parts[0]; - if (!_method.IsToken ()) { + var method = parts[0]; + + if (method.Length == 0) { _context.ErrorMessage = "Invalid request line (method)"; + return; } - _uri = parts[1]; + if (!method.IsHttpMethod ()) { + _context.ErrorStatusCode = 501; + _context.ErrorMessage = "Invalid request line (method)"; + + return; + } + + var target = parts[1]; + + if (target.Length == 0) { + _context.ErrorMessage = "Invalid request line (target)"; + + return; + } - var ver = parts[2]; - if (ver.Length != 8 || - !ver.StartsWith ("HTTP/") || - !tryCreateVersion (ver.Substring (5), out _version) || - _version.Major < 1) + var rawVer = parts[2]; + + if (rawVer.Length != 8) { + _context.ErrorMessage = "Invalid request line (version)"; + + return; + } + + if (!rawVer.StartsWith ("HTTP/", StringComparison.Ordinal)) { _context.ErrorMessage = "Invalid request line (version)"; + + return; + } + + Version ver; + + if (!rawVer.Substring (5).TryCreateVersion (out ver)) { + _context.ErrorMessage = "Invalid request line (version)"; + + return; + } + + if (ver != HttpVersion.Version11) { + _context.ErrorStatusCode = 505; + _context.ErrorMessage = "Invalid request line (version)"; + + return; + } + + _httpMethod = method; + _rawUrl = target; + _protocolVersion = ver; } #endregion @@ -632,83 +852,88 @@ internal void SetRequestLine (string requestLine) #region Public Methods /// - /// Begins getting the client's X.509 v.3 certificate asynchronously. + /// Begins getting the certificate provided by the client asynchronously. /// - /// - /// This asynchronous operation must be completed by calling - /// the method. Typically, - /// that method is invoked by the delegate. - /// /// - /// An that contains the status of the asynchronous operation. + /// An instance that represents the status of + /// the asynchronous operation. /// /// - /// An delegate that references the method(s) called when - /// the asynchronous operation completes. + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the asynchronous operation is + /// complete. + /// /// /// - /// An that contains a user defined object to pass to - /// the delegate. + /// An that specifies a user defined object to pass to + /// . /// - /// - /// This method isn't implemented. + /// + /// This method is not supported. /// - public IAsyncResult BeginGetClientCertificate (AsyncCallback requestCallback, object state) + public IAsyncResult BeginGetClientCertificate ( + AsyncCallback requestCallback, + object state + ) { - // TODO: Not implemented. - throw new NotImplementedException (); + throw new NotSupportedException (); } /// - /// Ends an asynchronous operation to get the client's X.509 v.3 certificate. + /// Ends an asynchronous operation to get the certificate provided by + /// the client. /// - /// - /// This method completes an asynchronous operation started by calling - /// the method. - /// /// - /// A that contains the client's X.509 v.3 certificate. + /// A that represents an X.509 certificate + /// provided by the client. /// /// - /// An obtained by calling + /// An instance obtained by calling /// the method. /// - /// - /// This method isn't implemented. + /// + /// This method is not supported. /// public X509Certificate2 EndGetClientCertificate (IAsyncResult asyncResult) { - // TODO: Not implemented. - throw new NotImplementedException (); + throw new NotSupportedException (); } /// - /// Gets the client's X.509 v.3 certificate. + /// Gets the certificate provided by the client. /// /// - /// A that contains the client's X.509 v.3 certificate. + /// A that represents an X.509 certificate + /// provided by the client. /// - /// - /// This method isn't implemented. + /// + /// This method is not supported. /// public X509Certificate2 GetClientCertificate () { - // TODO: Not implemented. - throw new NotImplementedException (); + throw new NotSupportedException (); } /// - /// Returns a that represents - /// the current . + /// Returns a string that represents the current instance. /// /// - /// A that represents the current . + /// A that contains the request line and headers + /// included in the request. /// public override string ToString () { var buff = new StringBuilder (64); - buff.AppendFormat ("{0} {1} HTTP/{2}\r\n", _method, _uri, _version); - buff.Append (_headers.ToString ()); + + var fmt = "{0} {1} HTTP/{2}\r\n"; + var headers = _headers.ToString (); + + buff + .AppendFormat (fmt, _httpMethod, _rawUrl, _protocolVersion) + .Append (headers); return buff.ToString (); } diff --git a/websocket-sharp/Net/HttpListenerResponse.cs b/websocket-sharp/Net/HttpListenerResponse.cs index 92c0a7970..500d42827 100644 --- a/websocket-sharp/Net/HttpListenerResponse.cs +++ b/websocket-sharp/Net/HttpListenerResponse.cs @@ -8,7 +8,7 @@ * The MIT License * * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2024 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 @@ -53,31 +53,42 @@ namespace WebSocketSharp.Net { /// - /// Provides the access to a response to a request received by the . + /// Represents an HTTP response to an HTTP request received by + /// a instance. /// /// - /// The HttpListenerResponse class cannot be inherited. + /// This class cannot be inherited. /// public sealed class HttpListenerResponse : IDisposable { #region Private Fields - private bool _closeConnection; - private Encoding _contentEncoding; - private long _contentLength; - private string _contentType; - private HttpListenerContext _context; - private CookieCollection _cookies; - private bool _disposed; - private WebHeaderCollection _headers; - private bool _headersSent; - private bool _keepAlive; - private string _location; - private ResponseStream _outputStream; - private bool _sendChunked; - private int _statusCode; - private string _statusDescription; - private Version _version; + private bool _closeConnection; + private Encoding _contentEncoding; + private long _contentLength; + private string _contentType; + private HttpListenerContext _context; + private CookieCollection _cookies; + private static readonly string _defaultProductName; + private bool _disposed; + private WebHeaderCollection _headers; + private bool _headersSent; + private bool _keepAlive; + private ResponseStream _outputStream; + private Uri _redirectLocation; + private bool _sendChunked; + private int _statusCode; + private string _statusDescription; + private Version _version; + + #endregion + + #region Static Constructor + + static HttpListenerResponse () + { + _defaultProductName = "websocket-sharp/1.0"; + } #endregion @@ -86,6 +97,7 @@ public sealed class HttpListenerResponse : IDisposable internal HttpListenerResponse (HttpListenerContext context) { _context = context; + _keepAlive = true; _statusCode = 200; _statusDescription = "OK"; @@ -106,6 +118,86 @@ internal bool CloseConnection { } } + internal WebHeaderCollection FullHeaders { + get { + var headers = new WebHeaderCollection (HttpHeaderType.Response, true); + + if (_headers != null) + headers.Add (_headers); + + if (_contentType != null) { + var val = createContentTypeHeaderText (_contentType, _contentEncoding); + + headers.InternalSet ("Content-Type", val, true); + } + + if (headers["Server"] == null) + headers.InternalSet ("Server", _defaultProductName, true); + + if (headers["Date"] == null) { + var val = DateTime.UtcNow.ToString ("r", CultureInfo.InvariantCulture); + + headers.InternalSet ("Date", val, true); + } + + if (_sendChunked) { + headers.InternalSet ("Transfer-Encoding", "chunked", true); + } + else { + var val = _contentLength.ToString (CultureInfo.InvariantCulture); + + headers.InternalSet ("Content-Length", val, true); + } + + /* + * Apache forces closing the connection for these status codes: + * - 400 Bad Request + * - 408 Request Timeout + * - 411 Length Required + * - 413 Request Entity Too Large + * - 414 Request-Uri Too Long + * - 500 Internal Server Error + * - 503 Service Unavailable + */ + + var reuses = _context.Connection.Reuses; + var closeConn = !_context.Request.KeepAlive + || !_keepAlive + || reuses >= 100 + || _statusCode == 400 + || _statusCode == 408 + || _statusCode == 411 + || _statusCode == 413 + || _statusCode == 414 + || _statusCode == 500 + || _statusCode == 503; + + if (closeConn) { + headers.InternalSet ("Connection", "close", true); + } + else { + var fmt = "timeout=15,max={0}"; + var max = 100 - reuses; + var val = String.Format (fmt, max); + + headers.InternalSet ("Keep-Alive", val, true); + } + + if (_redirectLocation != null) + headers.InternalSet ("Location", _redirectLocation.AbsoluteUri, true); + + if (_cookies != null) { + foreach (var cookie in _cookies) { + var val = cookie.ToResponseString (); + + headers.InternalSet ("Set-Cookie", val, true); + } + } + + return headers; + } + } + internal bool HeadersSent { get { return _headersSent; @@ -116,19 +208,45 @@ internal bool HeadersSent { } } + internal string ObjectName { + get { + return GetType ().ToString (); + } + } + + internal string StatusLine { + get { + var fmt = "HTTP/{0} {1} {2}\r\n"; + + return String.Format (fmt, _version, _statusCode, _statusDescription); + } + } + #endregion #region Public Properties /// - /// Gets or sets the encoding for the entity body data included in the response. + /// Gets or sets the encoding for the entity body data included in + /// the response. /// /// - /// A that represents the encoding for the entity body data, - /// or if no encoding is specified. + /// + /// A that represents the encoding for + /// the entity body data. + /// + /// + /// if no encoding is specified. + /// + /// + /// The default value is . + /// /// + /// + /// The response is already being sent. + /// /// - /// This object is closed. + /// This instance is closed. /// public Encoding ContentEncoding { get { @@ -136,25 +254,43 @@ public Encoding ContentEncoding { } set { - checkDisposed (); + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (_headersSent) { + var msg = "The response is already being sent."; + + throw new InvalidOperationException (msg); + } + _contentEncoding = value; } } /// - /// Gets or sets the number of bytes in the entity body data included in the response. + /// Gets or sets the number of bytes in the entity body data included in + /// the response. /// /// - /// A that represents the value of the Content-Length entity-header. + /// + /// A that represents the number of bytes in + /// the entity body data. + /// + /// + /// It is used for the value of the Content-Length header. + /// + /// + /// The default value is zero. + /// /// /// /// The value specified for a set operation is less than zero. /// /// - /// The response has already been sent. + /// The response is already being sent. /// /// - /// This object is closed. + /// This instance is closed. /// public long ContentLength64 { get { @@ -162,27 +298,61 @@ public long ContentLength64 { } set { - checkDisposedOrHeadersSent (); - if (value < 0) - throw new ArgumentOutOfRangeException ("Less than zero.", "value"); + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (_headersSent) { + var msg = "The response is already being sent."; + + throw new InvalidOperationException (msg); + } + + if (value < 0) { + var msg = "Less than zero."; + + throw new ArgumentOutOfRangeException (msg, "value"); + } _contentLength = value; } } /// - /// Gets or sets the media type of the entity body included in the response. + /// Gets or sets the media type of the entity body included in + /// the response. /// /// - /// A that represents the media type of the entity body, - /// or if no media type is specified. This value is - /// used for the value of the Content-Type entity-header. + /// + /// A that represents the media type of + /// the entity body. + /// + /// + /// It is used for the value of the Content-Type header. + /// + /// + /// if no media type is specified. + /// + /// + /// The default value is . + /// /// /// - /// The value specified for a set operation is empty. + /// + /// The value specified for a set operation is an empty string. + /// + /// + /// -or- + /// + /// + /// The value specified for a set operation contains + /// an invalid character. + /// + /// + /// + /// The response is already being sent. /// /// - /// This object is closed. + /// This instance is closed. /// public string ContentType { get { @@ -190,23 +360,47 @@ public string ContentType { } set { - checkDisposed (); - if (value != null && value.Length == 0) + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (_headersSent) { + var msg = "The response is already being sent."; + + throw new InvalidOperationException (msg); + } + + if (value == null) { + _contentType = null; + + return; + } + + if (value.Length == 0) throw new ArgumentException ("An empty string.", "value"); + if (!isValidForContentType (value)) { + var msg = "It contains an invalid character."; + + throw new ArgumentException (msg, "value"); + } + _contentType = value; } } /// - /// Gets or sets the cookies sent with the response. + /// Gets or sets the collection of the HTTP cookies sent with the response. /// /// - /// A that contains the cookies sent with the response. + /// A that contains the cookies sent with + /// the response. /// public CookieCollection Cookies { get { - return _cookies ?? (_cookies = new CookieCollection ()); + if (_cookies == null) + _cookies = new CookieCollection (); + + return _cookies; } set { @@ -215,40 +409,58 @@ public CookieCollection Cookies { } /// - /// Gets or sets the HTTP headers sent to the client. + /// Gets or sets the collection of the HTTP headers sent to the client. /// /// - /// A that contains the headers sent to the client. + /// A that contains the headers sent to + /// the client. /// /// - /// The value specified for a set operation isn't valid for a response. + /// The value specified for a set operation is not valid for a response. /// public WebHeaderCollection Headers { get { - return _headers ?? (_headers = new WebHeaderCollection (HttpHeaderType.Response, false)); + if (_headers == null) + _headers = new WebHeaderCollection (HttpHeaderType.Response, false); + + return _headers; } set { - if (value != null && value.State != HttpHeaderType.Response) - throw new InvalidOperationException ( - "The specified headers aren't valid for a response."); + if (value == null) { + _headers = null; + + return; + } + + if (value.State != HttpHeaderType.Response) { + var msg = "The value is not valid for a response."; + + throw new InvalidOperationException (msg); + } _headers = value; } } /// - /// Gets or sets a value indicating whether the server requests a persistent connection. + /// Gets or sets a value indicating whether the server requests + /// a persistent connection. /// /// - /// true if the server requests a persistent connection; otherwise, false. - /// The default value is true. + /// + /// true if the server requests a persistent connection; + /// otherwise, false. + /// + /// + /// The default value is true. + /// /// /// - /// The response has already been sent. + /// The response is already being sent. /// /// - /// This object is closed. + /// This instance is closed. /// public bool KeepAlive { get { @@ -256,108 +468,151 @@ public bool KeepAlive { } set { - checkDisposedOrHeadersSent (); + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (_headersSent) { + var msg = "The response is already being sent."; + + throw new InvalidOperationException (msg); + } + _keepAlive = value; } } /// - /// Gets a to use to write the entity body data. + /// Gets a stream instance to which the entity body data can be written. /// /// - /// A to use to write the entity body data. + /// A instance to which the entity body data can be + /// written. /// /// - /// This object is closed. + /// This instance is closed. /// public Stream OutputStream { get { - checkDisposed (); - return _outputStream ?? (_outputStream = _context.Connection.GetResponseStream ()); + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (_outputStream == null) + _outputStream = _context.Connection.GetResponseStream (); + + return _outputStream; } } /// - /// Gets or sets the HTTP version used in the response. + /// Gets the HTTP version used for the response. /// /// - /// A that represents the version used in the response. + /// + /// A that represents the HTTP version used for + /// the response. + /// + /// + /// Always returns same as 1.1. + /// /// - /// - /// The value specified for a set operation is . - /// - /// - /// The value specified for a set operation doesn't have its Major property set to 1 or - /// doesn't have its Minor property set to either 0 or 1. - /// - /// - /// The response has already been sent. - /// - /// - /// This object is closed. - /// public Version ProtocolVersion { get { return _version; } - - set { - checkDisposedOrHeadersSent (); - if (value == null) - throw new ArgumentNullException ("value"); - - if (value.Major != 1 || (value.Minor != 0 && value.Minor != 1)) - throw new ArgumentException ("Not 1.0 or 1.1.", "value"); - - _version = value; - } } /// - /// Gets or sets the URL to which the client is redirected to locate a requested resource. + /// Gets or sets the URL to which the client is redirected to locate + /// a requested resource. /// /// - /// A that represents the value of the Location response-header, - /// or if no redirect location is specified. + /// + /// A that represents the absolute URL for + /// the redirect location. + /// + /// + /// It is used for the value of the Location header. + /// + /// + /// if no redirect location is specified. + /// + /// + /// The default value is . + /// /// /// - /// The value specified for a set operation isn't an absolute URL. + /// + /// The value specified for a set operation is an empty string. + /// + /// + /// -or- + /// + /// + /// The value specified for a set operation is not an absolute URL. + /// + /// + /// + /// The response is already being sent. /// /// - /// This object is closed. + /// This instance is closed. /// public string RedirectLocation { get { - return _location; + return _redirectLocation != null + ? _redirectLocation.OriginalString + : null; } set { - checkDisposed (); + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (_headersSent) { + var msg = "The response is already being sent."; + + throw new InvalidOperationException (msg); + } + if (value == null) { - _location = null; + _redirectLocation = null; + return; } - Uri uri = null; - if (!value.MaybeUri () || !Uri.TryCreate (value, UriKind.Absolute, out uri)) - throw new ArgumentException ("Not an absolute URL.", "value"); + if (value.Length == 0) + throw new ArgumentException ("An empty string.", "value"); + + Uri uri; - _location = value; + if (!Uri.TryCreate (value, UriKind.Absolute, out uri)) { + var msg = "Not an absolute URL."; + + throw new ArgumentException (msg, "value"); + } + + _redirectLocation = uri; } } /// - /// Gets or sets a value indicating whether the response uses the chunked transfer encoding. + /// Gets or sets a value indicating whether the response uses the chunked + /// transfer encoding. /// /// - /// true if the response uses the chunked transfer encoding; - /// otherwise, false. The default value is false. + /// + /// true if the response uses the chunked transfer encoding; + /// otherwise, false. + /// + /// + /// The default value is false. + /// /// /// - /// The response has already been sent. + /// The response is already being sent. /// /// - /// This object is closed. + /// This instance is closed. /// public bool SendChunked { get { @@ -365,7 +620,15 @@ public bool SendChunked { } set { - checkDisposedOrHeadersSent (); + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (_headersSent) { + var msg = "The response is already being sent."; + + throw new InvalidOperationException (msg); + } + _sendChunked = value; } } @@ -374,18 +637,28 @@ public bool SendChunked { /// Gets or sets the HTTP status code returned to the client. /// /// - /// An that represents the status code for the response to - /// the request. The default value is same as . + /// + /// An that represents the HTTP status code for + /// the response to the request. + /// + /// + /// The default value is 200. It indicates that the request has + /// succeeded. + /// /// /// - /// The response has already been sent. + /// The response is already being sent. /// /// - /// This object is closed. + /// This instance is closed. /// /// - /// The value specified for a set operation is invalid. Valid values are - /// between 100 and 999 inclusive. + /// + /// The value specified for a set operation is invalid. + /// + /// + /// Valid values are between 100 and 999 inclusive. + /// /// public int StatusCode { get { @@ -393,10 +666,20 @@ public int StatusCode { } set { - checkDisposedOrHeadersSent (); - if (value < 100 || value > 999) - throw new System.Net.ProtocolViolationException ( - "A value isn't between 100 and 999 inclusive."); + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (_headersSent) { + var msg = "The response is already being sent."; + + throw new InvalidOperationException (msg); + } + + if (value < 100 || value > 999) { + var msg = "A value is not between 100 and 999 inclusive."; + + throw new System.Net.ProtocolViolationException (msg); + } _statusCode = value; _statusDescription = value.GetStatusDescription (); @@ -404,22 +687,35 @@ public int StatusCode { } /// - /// Gets or sets the description of the HTTP status code returned to the client. + /// Gets or sets the description of the HTTP status code returned to + /// the client. /// /// - /// A that represents the description of the status code. The default - /// value is the RFC 2616 - /// description for the property value, - /// or if an RFC 2616 description doesn't exist. + /// + /// A that represents the description of + /// the HTTP status code for the response to the request. + /// + /// + /// The default value is + /// the + /// RFC 2616 description for the + /// property value. + /// + /// + /// An empty string if an RFC 2616 description does not exist. + /// /// /// - /// The value specified for a set operation contains invalid characters. + /// The value specified for a set operation contains an invalid character. + /// + /// + /// The value specified for a set operation is . /// /// - /// The response has already been sent. + /// The response is already being sent. /// /// - /// This object is closed. + /// This instance is closed. /// public string StatusDescription { get { @@ -427,14 +723,29 @@ public string StatusDescription { } set { - checkDisposedOrHeadersSent (); - if (value == null || value.Length == 0) { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (_headersSent) { + var msg = "The response is already being sent."; + + throw new InvalidOperationException (msg); + } + + if (value == null) + throw new ArgumentNullException ("value"); + + if (value.Length == 0) { _statusDescription = _statusCode.GetStatusDescription (); + return; } - if (!value.IsText () || value.IndexOfAny (new[] { '\r', '\n' }) > -1) - throw new ArgumentException ("Contains invalid characters.", "value"); + if (!isValidForStatusDescription (value)) { + var msg = "It contains an invalid character."; + + throw new ArgumentException (msg, "value"); + } _statusDescription = value; } @@ -444,137 +755,101 @@ public string StatusDescription { #region Private Methods - private bool canAddOrUpdate (Cookie cookie) + private bool canSetCookie (Cookie cookie) { - if (_cookies == null || _cookies.Count == 0) - return true; + var res = findCookie (cookie).ToList (); - var found = findCookie (cookie).ToList (); - if (found.Count == 0) + if (res.Count == 0) return true; var ver = cookie.Version; - foreach (var c in found) + + foreach (var c in res) { if (c.Version == ver) return true; + } return false; } - private void checkDisposed () + private void close (bool force) { - if (_disposed) - throw new ObjectDisposedException (GetType ().ToString ()); + _disposed = true; + + _context.Connection.Close (force); } - private void checkDisposedOrHeadersSent () + private void close (byte[] responseEntity, int bufferLength, bool willBlock) { - if (_disposed) - throw new ObjectDisposedException (GetType ().ToString ()); + if (willBlock) { + OutputStream.WriteBytes (responseEntity, bufferLength); + close (false); - if (_headersSent) - throw new InvalidOperationException ("Cannot be changed after the headers are sent."); + return; + } + + OutputStream.WriteBytesAsync ( + responseEntity, + bufferLength, + () => close (false), + null + ); } - private void close (bool force) + private static string createContentTypeHeaderText ( + string value, + Encoding encoding + ) { - _disposed = true; - _context.Connection.Close (force); + if (value.Contains ("charset=")) + return value; + + if (encoding == null) + return value; + + var fmt = "{0}; charset={1}"; + + return String.Format (fmt, value, encoding.WebName); } private IEnumerable findCookie (Cookie cookie) { - var name = cookie.Name; - var domain = cookie.Domain; - var path = cookie.Path; - if (_cookies != null) - foreach (Cookie c in _cookies) - if (c.Name.Equals (name, StringComparison.OrdinalIgnoreCase) && - c.Domain.Equals (domain, StringComparison.OrdinalIgnoreCase) && - c.Path.Equals (path, StringComparison.Ordinal)) - yield return c; - } - - #endregion + if (_cookies == null || _cookies.Count == 0) + yield break; - #region Internal Methods + foreach (var c in _cookies) { + if (c.EqualsWithoutValueAndVersion (cookie)) + yield return c; + } + } - internal WebHeaderCollection WriteHeadersTo (MemoryStream destination) + private static bool isValidForContentType (string value) { - var headers = new WebHeaderCollection (HttpHeaderType.Response, true); - if (_headers != null) - headers.Add (_headers); - - if (_contentType != null) { - var type = _contentType.IndexOf ("charset=", StringComparison.Ordinal) == -1 && - _contentEncoding != null - ? String.Format ("{0}; charset={1}", _contentType, _contentEncoding.WebName) - : _contentType; - - headers.InternalSet ("Content-Type", type, true); - } - - if (headers["Server"] == null) - headers.InternalSet ("Server", "websocket-sharp/1.0", true); - - var prov = CultureInfo.InvariantCulture; - if (headers["Date"] == null) - headers.InternalSet ("Date", DateTime.UtcNow.ToString ("r", prov), true); - - if (!_sendChunked) - headers.InternalSet ("Content-Length", _contentLength.ToString (prov), true); - else - headers.InternalSet ("Transfer-Encoding", "chunked", true); - - /* - * Apache forces closing the connection for these status codes: - * - 400 Bad Request - * - 408 Request Timeout - * - 411 Length Required - * - 413 Request Entity Too Large - * - 414 Request-Uri Too Long - * - 500 Internal Server Error - * - 503 Service Unavailable - */ - var closeConn = !_context.Request.KeepAlive || - !_keepAlive || - _statusCode == 400 || - _statusCode == 408 || - _statusCode == 411 || - _statusCode == 413 || - _statusCode == 414 || - _statusCode == 500 || - _statusCode == 503; - - var reuses = _context.Connection.Reuses; - if (closeConn || reuses >= 100) { - headers.InternalSet ("Connection", "close", true); - } - else { - headers.InternalSet ( - "Keep-Alive", String.Format ("timeout=15,max={0}", 100 - reuses), true); + foreach (var c in value) { + if (c < 0x20) + return false; - if (_context.Request.ProtocolVersion < HttpVersion.Version11) - headers.InternalSet ("Connection", "keep-alive", true); - } + if (c > 0x7e) + return false; - if (_location != null) - headers.InternalSet ("Location", _location, true); + if ("()<>@:\\[]?{}".IndexOf (c) > -1) + return false; + } - if (_cookies != null) - foreach (Cookie cookie in _cookies) - headers.InternalSet ("Set-Cookie", cookie.ToResponseString (), true); + return true; + } - var enc = _contentEncoding ?? Encoding.Default; - var writer = new StreamWriter (destination, enc, 256); - writer.Write ("HTTP/{0} {1} {2}\r\n", _version, _statusCode, _statusDescription); - writer.Write (headers.ToStringMultiValue (true)); - writer.Flush (); + private static bool isValidForStatusDescription (string value) + { + foreach (var c in value) { + if (c < 0x20) + return false; - // Assumes that the destination was at position 0. - destination.Position = enc.GetPreamble ().Length; + if (c > 0x7e) + return false; + } - return headers; + return true; } #endregion @@ -582,7 +857,7 @@ internal WebHeaderCollection WriteHeadersTo (MemoryStream destination) #region Public Methods /// - /// Closes the connection to the client without returning a response. + /// Closes the connection to the client without sending a response. /// public void Abort () { @@ -593,42 +868,7 @@ public void Abort () } /// - /// Adds an HTTP header with the specified and - /// to the headers for the response. - /// - /// - /// A that represents the name of the header to add. - /// - /// - /// A that represents the value of the header to add. - /// - /// - /// is or empty. - /// - /// - /// - /// or contains invalid characters. - /// - /// - /// -or- - /// - /// - /// is a restricted header name. - /// - /// - /// - /// The length of is greater than 65,535 characters. - /// - /// - /// The header cannot be allowed to add to the current headers. - /// - public void AddHeader (string name, string value) - { - Headers.Set (name, value); - } - - /// - /// Appends the specified to the cookies sent with the response. + /// Appends an HTTP cookie to the cookies sent with the response. /// /// /// A to append. @@ -642,21 +882,38 @@ public void AppendCookie (Cookie cookie) } /// - /// Appends a to the specified HTTP header sent with the response. + /// Appends an HTTP header with the specified name and value to + /// the headers for the response. /// /// - /// A that represents the name of the header to append - /// to. + /// A that specifies the name of the header to + /// append. /// /// - /// A that represents the value to append to the header. + /// A that specifies the value of the header to + /// append. /// - /// - /// is or empty. - /// /// /// - /// or contains invalid characters. + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is a string of spaces. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. /// /// /// -or- @@ -665,11 +922,15 @@ public void AppendCookie (Cookie cookie) /// is a restricted header name. /// /// + /// + /// is . + /// /// - /// The length of is greater than 65,535 characters. + /// The length of is greater than 65,535 + /// characters. /// /// - /// The current headers cannot allow the header to append a value. + /// The current headers do not allow the header. /// public void AppendHeader (string name, string value) { @@ -677,8 +938,8 @@ public void AppendHeader (string name, string value) } /// - /// Returns the response to the client and releases the resources used by - /// this instance. + /// Sends the response to the client and releases the resources used by + /// this instance. /// public void Close () { @@ -689,51 +950,62 @@ public void Close () } /// - /// Returns the response with the specified array of to the client and - /// releases the resources used by this instance. + /// Sends the response with the specified entity body data to the client + /// and releases the resources used by this instance. /// /// - /// An array of that contains the response entity body data. + /// An array of that contains the entity body data. /// /// - /// true if this method blocks execution while flushing the stream to the client; - /// otherwise, false. + /// A : true if this method blocks execution while + /// flushing the stream to the client; otherwise, false. /// /// /// is . /// /// - /// This object is closed. + /// This instance is closed. /// public void Close (byte[] responseEntity, bool willBlock) { - checkDisposed (); + if (_disposed) + throw new ObjectDisposedException (ObjectName); + if (responseEntity == null) throw new ArgumentNullException ("responseEntity"); - var len = responseEntity.Length; - var output = OutputStream; + var len = responseEntity.LongLength; + + if (len > Int32.MaxValue) { + close (responseEntity, 1024, willBlock); + + return; + } + + var stream = OutputStream; + if (willBlock) { - output.Write (responseEntity, 0, len); + stream.Write (responseEntity, 0, (int) len); close (false); return; } - output.BeginWrite ( + stream.BeginWrite ( responseEntity, 0, - len, + (int) len, ar => { - output.EndWrite (ar); + stream.EndWrite (ar); close (false); }, - null); + null + ); } /// - /// Copies some properties from the specified to - /// this response. + /// Copies some properties from the specified response instance to + /// this instance. /// /// /// A to copy. @@ -746,13 +1018,15 @@ public void CopyFrom (HttpListenerResponse templateResponse) if (templateResponse == null) throw new ArgumentNullException ("templateResponse"); - if (templateResponse._headers != null) { + var headers = templateResponse._headers; + + if (headers != null) { if (_headers != null) _headers.Clear (); - Headers.Add (templateResponse._headers); + Headers.Add (headers); } - else if (_headers != null) { + else { _headers = null; } @@ -765,80 +1039,161 @@ public void CopyFrom (HttpListenerResponse templateResponse) /// /// Configures the response to redirect the client's request to - /// the specified . + /// the specified URL. /// /// - /// This method sets the property to - /// , the property to - /// 302, and the property to - /// "Found". + /// This method sets the property to + /// , the property to + /// 302, and the property to "Found". /// /// - /// A that represents the URL to redirect the client's request to. + /// A that specifies the absolute URL to which + /// the client is redirected to locate a requested resource. /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is not an absolute URL. + /// + /// /// /// is . /// - /// - /// isn't an absolute URL. - /// /// - /// The response has already been sent. + /// The response is already being sent. /// /// - /// This object is closed. + /// This instance is closed. /// public void Redirect (string url) { - checkDisposedOrHeadersSent (); + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (_headersSent) { + var msg = "The response is already being sent."; + + throw new InvalidOperationException (msg); + } + if (url == null) throw new ArgumentNullException ("url"); - Uri uri = null; - if (!url.MaybeUri () || !Uri.TryCreate (url, UriKind.Absolute, out uri)) - throw new ArgumentException ("Not an absolute URL.", "url"); + if (url.Length == 0) + throw new ArgumentException ("An empty string.", "url"); + + Uri uri; + + if (!Uri.TryCreate (url, UriKind.Absolute, out uri)) { + var msg = "Not an absolute URL."; - _location = url; + throw new ArgumentException (msg, "url"); + } + + _redirectLocation = uri; _statusCode = 302; _statusDescription = "Found"; } /// - /// Adds or updates a in the cookies sent with the response. + /// Adds or updates an HTTP cookie in the cookies sent with the response. /// /// /// A to set. /// + /// + /// already exists in the cookies but + /// it cannot be updated. + /// /// /// is . /// - /// - /// already exists in the cookies and couldn't be replaced. - /// public void SetCookie (Cookie cookie) { if (cookie == null) throw new ArgumentNullException ("cookie"); - if (!canAddOrUpdate (cookie)) - throw new ArgumentException ("Cannot be replaced.", "cookie"); + if (!canSetCookie (cookie)) { + var msg = "It cannot be updated."; + + throw new ArgumentException (msg, "cookie"); + } Cookies.Add (cookie); } + /// + /// Adds or updates an HTTP header with the specified name and value in + /// the headers for the response. + /// + /// + /// A that specifies the name of the header to set. + /// + /// + /// A that specifies the value of the header to set. + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is a string of spaces. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// -or- + /// + /// + /// is a restricted header name. + /// + /// + /// + /// is . + /// + /// + /// The length of is greater than 65,535 + /// characters. + /// + /// + /// The current headers do not allow the header. + /// + public void SetHeader (string name, string value) + { + Headers.Set (name, value); + } + #endregion #region Explicit Interface Implementations /// - /// Releases all resources used by the . + /// Releases all resources used by this instance. /// void IDisposable.Dispose () { if (_disposed) return; - close (true); // Same as the Abort method. + close (true); } #endregion diff --git a/websocket-sharp/Net/HttpRequestHeader.cs b/websocket-sharp/Net/HttpRequestHeader.cs index 08785db34..aab3497ff 100644 --- a/websocket-sharp/Net/HttpRequestHeader.cs +++ b/websocket-sharp/Net/HttpRequestHeader.cs @@ -2,13 +2,13 @@ /* * HttpRequestHeader.cs * - * This code is derived from System.Net.HttpRequestHeader.cs of Mono + * This code is derived from HttpRequestHeader.cs (System.Net) of Mono * (http://www.mono-project.com). * * The MIT License * * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) - * Copyright (c) 2014 sta.blockhead + * Copyright (c) 2014-2020 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 @@ -40,12 +40,12 @@ namespace WebSocketSharp.Net { /// - /// Contains the HTTP headers that may be specified in a client request. + /// Indicates the HTTP header that may be specified in a client request. /// /// - /// The HttpRequestHeader enumeration contains the HTTP request headers defined in - /// RFC 2616 for the HTTP/1.1 and - /// RFC 6455 for the WebSocket. + /// The headers of this enumeration are defined in + /// RFC 2616 or + /// RFC 6455. /// public enum HttpRequestHeader { diff --git a/websocket-sharp/Net/HttpResponseHeader.cs b/websocket-sharp/Net/HttpResponseHeader.cs index d8f36ed84..d32afe6c9 100644 --- a/websocket-sharp/Net/HttpResponseHeader.cs +++ b/websocket-sharp/Net/HttpResponseHeader.cs @@ -2,13 +2,13 @@ /* * HttpResponseHeader.cs * - * This code is derived from System.Net.HttpResponseHeader.cs of Mono + * This code is derived from HttpResponseHeader.cs (System.Net) of Mono * (http://www.mono-project.com). * * The MIT License * * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) - * Copyright (c) 2014 sta.blockhead + * Copyright (c) 2014-2020 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 @@ -40,12 +40,12 @@ namespace WebSocketSharp.Net { /// - /// Contains the HTTP headers that can be specified in a server response. + /// Indicates the HTTP header that can be specified in a server response. /// /// - /// The HttpResponseHeader enumeration contains the HTTP response headers defined in - /// RFC 2616 for the HTTP/1.1 and - /// RFC 6455 for the WebSocket. + /// The headers of this enumeration are defined in + /// RFC 2616 or + /// RFC 6455. /// public enum HttpResponseHeader { diff --git a/websocket-sharp/Net/HttpStatusCode.cs b/websocket-sharp/Net/HttpStatusCode.cs index 123415f01..b773a4964 100644 --- a/websocket-sharp/Net/HttpStatusCode.cs +++ b/websocket-sharp/Net/HttpStatusCode.cs @@ -2,7 +2,7 @@ /* * HttpStatusCode.cs * - * This code is derived from System.Net.HttpStatusCode.cs of Mono + * This code is derived from HttpStatusCode.cs (System.Net) of Mono * (http://www.mono-project.com). * * It was automatically generated from ECMA CLI XML Library Specification. @@ -14,7 +14,7 @@ * The MIT License * * Copyright (c) 2001 Ximian, Inc. (http://www.ximian.com) - * Copyright (c) 2012-2014 sta.blockhead + * Copyright (c) 2012-2020 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 @@ -39,68 +39,64 @@ namespace WebSocketSharp.Net { /// - /// Contains the values of the HTTP status codes. + /// Indicates the HTTP status code that can be specified in a server response. /// /// - /// The HttpStatusCode enumeration contains the values of the HTTP status codes defined in - /// RFC 2616 for the HTTP/1.1. + /// The values of this enumeration are defined in + /// RFC 2616. /// public enum HttpStatusCode { /// - /// Equivalent to status code 100. - /// Indicates that the client should continue with its request. + /// Equivalent to status code 100. Indicates that the client should continue + /// with its request. /// Continue = 100, /// - /// Equivalent to status code 101. - /// Indicates that the server is switching the HTTP version or protocol on the connection. + /// Equivalent to status code 101. Indicates that the server is switching + /// the HTTP version or protocol on the connection. /// SwitchingProtocols = 101, /// - /// Equivalent to status code 200. - /// Indicates that the client's request has succeeded. + /// Equivalent to status code 200. Indicates that the client's request has + /// succeeded. /// OK = 200, /// - /// Equivalent to status code 201. - /// Indicates that the client's request has been fulfilled and resulted in a new resource being - /// created. + /// Equivalent to status code 201. Indicates that the client's request has + /// been fulfilled and resulted in a new resource being created. /// Created = 201, /// - /// Equivalent to status code 202. - /// Indicates that the client's request has been accepted for processing, but the processing - /// hasn't been completed. + /// Equivalent to status code 202. Indicates that the client's request has + /// been accepted for processing, but the processing has not been completed. /// Accepted = 202, /// - /// Equivalent to status code 203. - /// Indicates that the returned metainformation is from a local or a third-party copy instead of - /// the origin server. + /// Equivalent to status code 203. Indicates that the returned metainformation + /// is from a local or a third-party copy instead of the origin server. /// NonAuthoritativeInformation = 203, /// - /// Equivalent to status code 204. - /// Indicates that the server has fulfilled the client's request but doesn't need to return - /// an entity-body. + /// Equivalent to status code 204. Indicates that the server has fulfilled + /// the client's request but does not need to return an entity-body. /// NoContent = 204, /// - /// Equivalent to status code 205. - /// Indicates that the server has fulfilled the client's request, and the user agent should - /// reset the document view which caused the request to be sent. + /// Equivalent to status code 205. Indicates that the server has fulfilled + /// the client's request, and the user agent should reset the document view + /// which caused the request to be sent. /// ResetContent = 205, /// - /// Equivalent to status code 206. - /// Indicates that the server has fulfilled the partial GET request for the resource. + /// Equivalent to status code 206. Indicates that the server has fulfilled + /// the partial GET request for the resource. /// PartialContent = 206, /// /// - /// Equivalent to status code 300. - /// Indicates that the requested resource corresponds to any of multiple representations. + /// Equivalent to status code 300. Indicates that the requested resource + /// corresponds to any of multiple representations. /// /// /// MultipleChoices is a synonym for Ambiguous. @@ -109,8 +105,8 @@ public enum HttpStatusCode MultipleChoices = 300, /// /// - /// Equivalent to status code 300. - /// Indicates that the requested resource corresponds to any of multiple representations. + /// Equivalent to status code 300. Indicates that the requested resource + /// corresponds to any of multiple representations. /// /// /// Ambiguous is a synonym for MultipleChoices. @@ -119,9 +115,9 @@ public enum HttpStatusCode Ambiguous = 300, /// /// - /// Equivalent to status code 301. - /// Indicates that the requested resource has been assigned a new permanent URI and - /// any future references to this resource should use one of the returned URIs. + /// Equivalent to status code 301. Indicates that the requested resource + /// has been assigned a new permanent URI and any future references to + /// this resource should use one of the returned URIs. /// /// /// MovedPermanently is a synonym for Moved. @@ -130,9 +126,9 @@ public enum HttpStatusCode MovedPermanently = 301, /// /// - /// Equivalent to status code 301. - /// Indicates that the requested resource has been assigned a new permanent URI and - /// any future references to this resource should use one of the returned URIs. + /// Equivalent to status code 301. Indicates that the requested resource + /// has been assigned a new permanent URI and any future references to + /// this resource should use one of the returned URIs. /// /// /// Moved is a synonym for MovedPermanently. @@ -141,8 +137,8 @@ public enum HttpStatusCode Moved = 301, /// /// - /// Equivalent to status code 302. - /// Indicates that the requested resource is located temporarily under a different URI. + /// Equivalent to status code 302. Indicates that the requested resource + /// is located temporarily under a different URI. /// /// /// Found is a synonym for Redirect. @@ -151,8 +147,8 @@ public enum HttpStatusCode Found = 302, /// /// - /// Equivalent to status code 302. - /// Indicates that the requested resource is located temporarily under a different URI. + /// Equivalent to status code 302. Indicates that the requested resource + /// is located temporarily under a different URI. /// /// /// Redirect is a synonym for Found. @@ -161,9 +157,9 @@ public enum HttpStatusCode Redirect = 302, /// /// - /// Equivalent to status code 303. - /// Indicates that the response to the request can be found under a different URI and - /// should be retrieved using a GET method on that resource. + /// Equivalent to status code 303. Indicates that the response to + /// the request can be found under a different URI and should be + /// retrieved using a GET method on that resource. /// /// /// SeeOther is a synonym for RedirectMethod. @@ -172,9 +168,9 @@ public enum HttpStatusCode SeeOther = 303, /// /// - /// Equivalent to status code 303. - /// Indicates that the response to the request can be found under a different URI and - /// should be retrieved using a GET method on that resource. + /// Equivalent to status code 303. Indicates that the response to + /// the request can be found under a different URI and should be + /// retrieved using a GET method on that resource. /// /// /// RedirectMethod is a synonym for SeeOther. @@ -182,27 +178,26 @@ public enum HttpStatusCode /// RedirectMethod = 303, /// - /// Equivalent to status code 304. - /// Indicates that the client has performed a conditional GET request and access is allowed, - /// but the document hasn't been modified. + /// Equivalent to status code 304. Indicates that the client has performed + /// a conditional GET request and access is allowed, but the document has + /// not been modified. /// NotModified = 304, /// - /// Equivalent to status code 305. - /// Indicates that the requested resource must be accessed through the proxy given by - /// the Location field. + /// Equivalent to status code 305. Indicates that the requested resource + /// must be accessed through the proxy given by the Location field. /// UseProxy = 305, /// - /// Equivalent to status code 306. - /// This status code was used in a previous version of the specification, is no longer used, - /// and is reserved for future use. + /// Equivalent to status code 306. This status code was used in a previous + /// version of the specification, is no longer used, and is reserved for + /// future use. /// Unused = 306, /// /// - /// Equivalent to status code 307. - /// Indicates that the requested resource is located temporarily under a different URI. + /// Equivalent to status code 307. Indicates that the requested resource + /// is located temporarily under a different URI. /// /// /// TemporaryRedirect is a synonym for RedirectKeepVerb. @@ -211,8 +206,8 @@ public enum HttpStatusCode TemporaryRedirect = 307, /// /// - /// Equivalent to status code 307. - /// Indicates that the requested resource is located temporarily under a different URI. + /// Equivalent to status code 307. Indicates that the requested resource + /// is located temporarily under a different URI. /// /// /// RedirectKeepVerb is a synonym for TemporaryRedirect. @@ -220,139 +215,131 @@ public enum HttpStatusCode /// RedirectKeepVerb = 307, /// - /// Equivalent to status code 400. - /// Indicates that the client's request couldn't be understood by the server due to - /// malformed syntax. + /// Equivalent to status code 400. Indicates that the client's request could + /// not be understood by the server due to malformed syntax. /// BadRequest = 400, /// - /// Equivalent to status code 401. - /// Indicates that the client's request requires user authentication. + /// Equivalent to status code 401. Indicates that the client's request + /// requires user authentication. /// Unauthorized = 401, /// - /// Equivalent to status code 402. - /// This status code is reserved for future use. + /// Equivalent to status code 402. This status code is reserved for future + /// use. /// PaymentRequired = 402, /// - /// Equivalent to status code 403. - /// Indicates that the server understood the client's request but is refusing to fulfill it. + /// Equivalent to status code 403. Indicates that the server understood + /// the client's request but is refusing to fulfill it. /// Forbidden = 403, /// - /// Equivalent to status code 404. - /// Indicates that the server hasn't found anything matching the request URI. + /// Equivalent to status code 404. Indicates that the server has not found + /// anything matching the request URI. /// NotFound = 404, /// - /// Equivalent to status code 405. - /// Indicates that the method specified in the request line isn't allowed for the resource - /// identified by the request URI. + /// Equivalent to status code 405. Indicates that the method specified + /// in the request line is not allowed for the resource identified by + /// the request URI. /// MethodNotAllowed = 405, /// - /// Equivalent to status code 406. - /// Indicates that the server doesn't have the appropriate resource to respond to the Accept - /// headers in the client's request. + /// Equivalent to status code 406. Indicates that the server does not + /// have the appropriate resource to respond to the Accept headers in + /// the client's request. /// NotAcceptable = 406, /// - /// Equivalent to status code 407. - /// Indicates that the client must first authenticate itself with the proxy. + /// Equivalent to status code 407. Indicates that the client must first + /// authenticate itself with the proxy. /// ProxyAuthenticationRequired = 407, /// - /// Equivalent to status code 408. - /// Indicates that the client didn't produce a request within the time that the server was - /// prepared to wait. + /// Equivalent to status code 408. Indicates that the client did not produce + /// a request within the time that the server was prepared to wait. /// RequestTimeout = 408, /// - /// Equivalent to status code 409. - /// Indicates that the client's request couldn't be completed due to a conflict on the server. + /// Equivalent to status code 409. Indicates that the client's request could + /// not be completed due to a conflict on the server. /// Conflict = 409, /// - /// Equivalent to status code 410. - /// Indicates that the requested resource is no longer available at the server and - /// no forwarding address is known. + /// Equivalent to status code 410. Indicates that the requested resource is + /// no longer available at the server and no forwarding address is known. /// Gone = 410, /// - /// Equivalent to status code 411. - /// Indicates that the server refuses to accept the client's request without a defined - /// Content-Length. + /// Equivalent to status code 411. Indicates that the server refuses to + /// accept the client's request without a defined Content-Length. /// LengthRequired = 411, /// - /// Equivalent to status code 412. - /// Indicates that the precondition given in one or more of the request headers evaluated to - /// false when it was tested on the server. + /// Equivalent to status code 412. Indicates that the precondition given in + /// one or more of the request headers evaluated to false when it was tested + /// on the server. /// PreconditionFailed = 412, /// - /// Equivalent to status code 413. - /// Indicates that the entity of the client's request is larger than the server is willing or - /// able to process. + /// Equivalent to status code 413. Indicates that the entity of the client's + /// request is larger than the server is willing or able to process. /// RequestEntityTooLarge = 413, /// - /// Equivalent to status code 414. - /// Indicates that the request URI is longer than the server is willing to interpret. + /// Equivalent to status code 414. Indicates that the request URI is longer + /// than the server is willing to interpret. /// RequestUriTooLong = 414, /// - /// Equivalent to status code 415. - /// Indicates that the entity of the client's request is in a format not supported by - /// the requested resource for the requested method. + /// Equivalent to status code 415. Indicates that the entity of the client's + /// request is in a format not supported by the requested resource for the + /// requested method. /// UnsupportedMediaType = 415, /// - /// Equivalent to status code 416. - /// Indicates that none of the range specifier values in a Range request header overlap - /// the current extent of the selected resource. + /// Equivalent to status code 416. Indicates that none of the range + /// specifier values in a Range request header overlap the current + /// extent of the selected resource. /// RequestedRangeNotSatisfiable = 416, /// - /// Equivalent to status code 417. - /// Indicates that the expectation given in an Expect request header couldn't be met by - /// the server. + /// Equivalent to status code 417. Indicates that the expectation given in + /// an Expect request header could not be met by the server. /// ExpectationFailed = 417, /// - /// Equivalent to status code 500. - /// Indicates that the server encountered an unexpected condition which prevented it from - /// fulfilling the client's request. + /// Equivalent to status code 500. Indicates that the server encountered + /// an unexpected condition which prevented it from fulfilling the client's + /// request. /// InternalServerError = 500, /// - /// Equivalent to status code 501. - /// Indicates that the server doesn't support the functionality required to fulfill the client's - /// request. + /// Equivalent to status code 501. Indicates that the server does not + /// support the functionality required to fulfill the client's request. /// NotImplemented = 501, /// - /// Equivalent to status code 502. - /// Indicates that a gateway or proxy server received an invalid response from the upstream - /// server. + /// Equivalent to status code 502. Indicates that a gateway or proxy server + /// received an invalid response from the upstream server. /// BadGateway = 502, /// - /// Equivalent to status code 503. - /// Indicates that the server is currently unable to handle the client's request due to - /// a temporary overloading or maintenance of the server. + /// Equivalent to status code 503. Indicates that the server is currently + /// unable to handle the client's request due to a temporary overloading + /// or maintenance of the server. /// ServiceUnavailable = 503, /// - /// Equivalent to status code 504. - /// Indicates that a gateway or proxy server didn't receive a timely response from the upstream - /// server or some other auxiliary server. + /// Equivalent to status code 504. Indicates that a gateway or proxy server + /// did not receive a timely response from the upstream server or some other + /// auxiliary server. /// GatewayTimeout = 504, /// - /// Equivalent to status code 505. - /// Indicates that the server doesn't support the HTTP version used in the client's request. + /// Equivalent to status code 505. Indicates that the server does not + /// support the HTTP version used in the client's request. /// HttpVersionNotSupported = 505, } diff --git a/websocket-sharp/Net/HttpStreamAsyncResult.cs b/websocket-sharp/Net/HttpStreamAsyncResult.cs index 44189303c..09447ea21 100644 --- a/websocket-sharp/Net/HttpStreamAsyncResult.cs +++ b/websocket-sharp/Net/HttpStreamAsyncResult.cs @@ -8,7 +8,7 @@ * The MIT License * * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2021 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 @@ -65,6 +65,7 @@ internal HttpStreamAsyncResult (AsyncCallback callback, object state) { _callback = callback; _state = state; + _sync = new object (); } @@ -136,8 +137,12 @@ public object AsyncState { public WaitHandle AsyncWaitHandle { get { - lock (_sync) - return _waitHandle ?? (_waitHandle = new ManualResetEvent (_completed)); + lock (_sync) { + if (_waitHandle == null) + _waitHandle = new ManualResetEvent (_completed); + + return _waitHandle; + } } } @@ -165,6 +170,7 @@ internal void Complete () return; _completed = true; + if (_waitHandle != null) _waitHandle.Set (); @@ -175,8 +181,19 @@ internal void Complete () internal void Complete (Exception exception) { - _exception = exception; - Complete (); + lock (_sync) { + if (_completed) + return; + + _completed = true; + _exception = exception; + + if (_waitHandle != null) + _waitHandle.Set (); + + if (_callback != null) + _callback.BeginInvoke (this, ar => _callback.EndInvoke (ar), null); + } } #endregion diff --git a/websocket-sharp/Net/HttpUtility.cs b/websocket-sharp/Net/HttpUtility.cs index c5cd117ed..b7db7b922 100644 --- a/websocket-sharp/Net/HttpUtility.cs +++ b/websocket-sharp/Net/HttpUtility.cs @@ -2,13 +2,13 @@ /* * HttpUtility.cs * - * This code is derived from System.Net.HttpUtility.cs of Mono + * This code is derived from HttpUtility.cs (System.Net) of Mono * (http://www.mono-project.com). * * The MIT License * * Copyright (c) 2005-2009 Novell, Inc. (http://www.novell.com) - * Copyright (c) 2012-2014 sta.blockhead + * Copyright (c) 2012-2024 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 @@ -51,84 +51,310 @@ namespace WebSocketSharp.Net { - internal sealed class HttpUtility + internal static class HttpUtility { #region Private Fields private static Dictionary _entities; - private static char[] _hexChars = "0123456789abcdef".ToCharArray (); - private static object _sync = new object (); + private static char[] _hexChars; + private static object _sync; + + #endregion + + #region Static Constructor + + static HttpUtility () + { + _hexChars = "0123456789ABCDEF".ToCharArray (); + _sync = new object (); + } #endregion #region Private Methods - private static int getChar (byte[] bytes, int offset, int length) + private static Dictionary getEntities () { - var val = 0; - var end = length + offset; - for (var i = offset; i < end; i++) { - var current = getInt (bytes[i]); - if (current == -1) - return -1; + lock (_sync) { + if (_entities == null) + initEntities (); - val = (val << 4) + current; + return _entities; } + } + + private static int getNumber (char c) + { + if (c >= '0' && c <= '9') + return c - '0'; + + if (c >= 'A' && c <= 'F') + return c - 'A' + 10; - return val; + if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + + return -1; } - private static int getChar (string s, int offset, int length) + private static int getNumber (byte[] bytes, int offset, int count) { - var val = 0; - var end = length + offset; - for (var i = offset; i < end; i++) { - var c = s[i]; - if (c > 127) - return -1; + var ret = 0; + + var end = offset + count - 1; - var current = getInt ((byte) c); - if (current == -1) + for (var i = offset; i <= end; i++) { + var c = (char) bytes[i]; + var n = getNumber (c); + + if (n == -1) return -1; - val = (val << 4) + current; + ret = (ret << 4) + n; } - return val; + return ret; } - private static char[] getChars (MemoryStream buffer, Encoding encoding) + private static int getNumber (string s, int offset, int count) { - return encoding.GetChars (buffer.GetBuffer (), 0, (int) buffer.Length); + var ret = 0; + + var end = offset + count - 1; + + for (var i = offset; i <= end; i++) { + var c = s[i]; + var n = getNumber (c); + + if (n == -1) + return -1; + + ret = (ret << 4) + n; + } + + return ret; } - private static Dictionary getEntities () + private static string htmlDecode (string s) { - lock (_sync) { - if (_entities == null) - initEntities (); + var buff = new StringBuilder (); - return _entities; + // 0: None + // 1: Right after '&' + // 2: Between '&' and ';' but no NCR + // 3: '#' found after '&' and getting numbers + // 4: 'x' found after '#' and getting numbers + var state = 0; + + var reference = new StringBuilder (); + var num = 0; + + foreach (var c in s) { + if (state == 0) { + if (c == '&') { + reference.Append ('&'); + + state = 1; + + continue; + } + + buff.Append (c); + + continue; + } + + if (c == '&') { + buff.Append (reference.ToString ()); + + reference.Length = 0; + + reference.Append ('&'); + + state = 1; + + continue; + } + + reference.Append (c); + + if (state == 1) { + if (c == ';') { + buff.Append (reference.ToString ()); + + reference.Length = 0; + state = 0; + + continue; + } + + num = 0; + state = c == '#' ? 3 : 2; + + continue; + } + + if (state == 2) { + if (c == ';') { + var entity = reference.ToString (); + var name = entity.Substring (1, entity.Length - 2); + + var entities = getEntities (); + + if (entities.ContainsKey (name)) + buff.Append (entities[name]); + else + buff.Append (entity); + + reference.Length = 0; + state = 0; + + continue; + } + + continue; + } + + if (state == 3) { + if (c == ';') { + if (reference.Length > 3 && num < 65536) + buff.Append ((char) num); + else + buff.Append (reference.ToString ()); + + reference.Length = 0; + state = 0; + + continue; + } + + if (c == 'x') { + state = reference.Length == 3 ? 4 : 2; + + continue; + } + + if (!isNumeric (c)) { + state = 2; + + continue; + } + + num = num * 10 + (c - '0'); + + continue; + } + + if (state == 4) { + if (c == ';') { + if (reference.Length > 4 && num < 65536) + buff.Append ((char) num); + else + buff.Append (reference.ToString ()); + + reference.Length = 0; + state = 0; + + continue; + } + + var n = getNumber (c); + + if (n == -1) { + state = 2; + + continue; + } + + num = (num << 4) + n; + } } + + if (reference.Length > 0) + buff.Append (reference.ToString ()); + + return buff.ToString (); } - private static int getInt (byte b) + /// + /// Converts the specified string to an HTML-encoded string. + /// + /// + /// + /// This method starts encoding with a NCR from the character code 160 + /// but does not stop at the character code 255. + /// + /// + /// One reason is the unicode characters < and > that + /// look like < and >. + /// + /// + /// + /// A that represents an encoded string. + /// + /// + /// A to encode. + /// + /// + /// A : true if encodes without a NCR; + /// otherwise, false. + /// + private static string htmlEncode (string s, bool minimal) { - var c = (char) b; - return c >= '0' && c <= '9' - ? c - '0' - : c >= 'a' && c <= 'f' - ? c - 'a' + 10 - : c >= 'A' && c <= 'F' - ? c - 'A' + 10 - : -1; + var buff = new StringBuilder (); + + foreach (var c in s) { + if (c == '"') { + buff.Append ("""); + + continue; + } + + if (c == '&') { + buff.Append ("&"); + + continue; + } + + if (c == '<') { + buff.Append ("<"); + + continue; + } + + if (c == '>') { + buff.Append (">"); + + continue; + } + + if (c > 159) { + if (!minimal) { + var val = String.Format ("&#{0};", (int) c); + + buff.Append (val); + + continue; + } + } + + buff.Append (c); + } + + return buff.ToString (); } + /// + /// Initializes the _entities field. + /// + /// + /// This method builds a dictionary of HTML character entity references. + /// This dictionary comes from the HTML 4.01 W3C recommendation. + /// private static void initEntities () { - // Build the dictionary of HTML entity references. - // This list comes from the HTML 4.01 W3C recommendation. _entities = new Dictionary (); + _entities.Add ("nbsp", '\u00A0'); _entities.Add ("iexcl", '\u00A1'); _entities.Add ("cent", '\u00A2'); @@ -383,120 +609,137 @@ private static void initEntities () _entities.Add ("euro", '\u20AC'); } - private static bool notEncoded (char c) + private static bool isAlphabet (char c) { - return c == '!' || - c == '\'' || - c == '(' || - c == ')' || - c == '*' || - c == '-' || - c == '.' || - c == '_'; + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); } - private static void urlEncode (char c, Stream result, bool unicode) + private static bool isNumeric (char c) { - if (c > 255) { - // FIXME: What happens when there is an internal error? - //if (!unicode) - // throw new ArgumentOutOfRangeException ("c", c, "Greater than 255."); + return c >= '0' && c <= '9'; + } - result.WriteByte ((byte) '%'); - result.WriteByte ((byte) 'u'); + private static bool isUnreserved (char c) + { + return c == '*' + || c == '-' + || c == '.' + || c == '_'; + } - var i = (int) c; - var idx = i >> 12; - result.WriteByte ((byte) _hexChars[idx]); + private static bool isUnreservedInRfc2396 (char c) + { + return c == '!' + || c == '\'' + || c == '(' + || c == ')' + || c == '*' + || c == '-' + || c == '.' + || c == '_' + || c == '~'; + } - idx = (i >> 8) & 0x0F; - result.WriteByte ((byte) _hexChars[idx]); + private static bool isUnreservedInRfc3986 (char c) + { + return c == '-' + || c == '.' + || c == '_' + || c == '~'; + } - idx = (i >> 4) & 0x0F; - result.WriteByte ((byte) _hexChars[idx]); + private static byte[] urlDecodeToBytes (byte[] bytes, int offset, int count) + { + using (var buff = new MemoryStream ()) { + var end = offset + count - 1; - idx = i & 0x0F; - result.WriteByte ((byte) _hexChars[idx]); + for (var i = offset; i <= end; i++) { + var b = bytes[i]; + var c = (char) b; - return; - } + if (c == '%') { + if (i > end - 2) + break; - if (c > ' ' && notEncoded (c)) { - result.WriteByte ((byte) c); - return; - } + var num = getNumber (bytes, i + 1, 2); - if (c == ' ') { - result.WriteByte ((byte) '+'); - return; - } + if (num == -1) + break; - if ((c < '0') || - (c < 'A' && c > '9') || - (c > 'Z' && c < 'a') || - (c > 'z')) { - if (unicode && c > 127) { - result.WriteByte ((byte) '%'); - result.WriteByte ((byte) 'u'); - result.WriteByte ((byte) '0'); - result.WriteByte ((byte) '0'); - } - else { - result.WriteByte ((byte) '%'); - } + buff.WriteByte ((byte) num); - var i = (int) c; - var idx = i >> 4; - result.WriteByte ((byte) _hexChars[idx]); + i += 2; - idx = i & 0x0F; - result.WriteByte ((byte) _hexChars[idx]); + continue; + } - return; - } + if (c == '+') { + buff.WriteByte ((byte) ' '); + + continue; + } - result.WriteByte ((byte) c); + buff.WriteByte (b); + } + + buff.Close (); + + return buff.ToArray (); + } } - private static void urlPathEncode (char c, Stream result) + private static void urlEncode (byte b, Stream output) { - if (c < 33 || c > 126) { - var bytes = Encoding.UTF8.GetBytes (c.ToString ()); - foreach (var b in bytes) { - result.WriteByte ((byte) '%'); + if (b > 31 && b < 127) { + var c = (char) b; - var i = (int) b; - var idx = i >> 4; - result.WriteByte ((byte) _hexChars[idx]); + if (c == ' ') { + output.WriteByte ((byte) '+'); - idx = i & 0x0F; - result.WriteByte ((byte) _hexChars[idx]); + return; } - return; - } + if (isNumeric (c)) { + output.WriteByte (b); - if (c == ' ') { - result.WriteByte ((byte) '%'); - result.WriteByte ((byte) '2'); - result.WriteByte ((byte) '0'); + return; + } - return; + if (isAlphabet (c)) { + output.WriteByte (b); + + return; + } + + if (isUnreserved (c)) { + output.WriteByte (b); + + return; + } } - result.WriteByte ((byte) c); + var i = (int) b; + var bytes = new byte[] { + (byte) '%', + (byte) _hexChars[i >> 4], + (byte) _hexChars[i & 0x0F] + }; + + output.Write (bytes, 0, 3); } - private static void writeCharBytes (char c, IList buffer, Encoding encoding) + private static byte[] urlEncodeToBytes (byte[] bytes, int offset, int count) { - if (c > 255) { - foreach (var b in encoding.GetBytes (new[] { c })) - buffer.Add (b); + using (var buff = new MemoryStream ()) { + var end = offset + count - 1; - return; - } + for (var i = offset; i <= end; i++) + urlEncode (bytes[i], buff); - buffer.Add ((byte) c); + buff.Close (); + + return buff.ToArray (); + } } #endregion @@ -504,21 +747,34 @@ private static void writeCharBytes (char c, IList buffer, Encoding encoding) #region Internal Methods internal static Uri CreateRequestUrl ( - string requestUri, string host, bool websocketRequest, bool secure) + string requestUri, + string host, + bool websocketRequest, + bool secure + ) { - if (requestUri == null || requestUri.Length == 0 || host == null || host.Length == 0) + if (requestUri == null || requestUri.Length == 0) + return null; + + if (host == null || host.Length == 0) return null; string schm = null; string path = null; - if (requestUri.StartsWith ("/")) { + + if (requestUri.IndexOf ('/') == 0) { path = requestUri; } else if (requestUri.MaybeUri ()) { Uri uri; - var valid = Uri.TryCreate (requestUri, UriKind.Absolute, out uri) && - (((schm = uri.Scheme).StartsWith ("http") && !websocketRequest) || - (schm.StartsWith ("ws") && websocketRequest)); + + if (!Uri.TryCreate (requestUri, UriKind.Absolute, out uri)) + return null; + + schm = uri.Scheme; + var valid = websocketRequest + ? schm == "ws" || schm == "wss" + : schm == "http" || schm == "https"; if (!valid) return null; @@ -529,24 +785,24 @@ internal static Uri CreateRequestUrl ( else if (requestUri == "*") { } else { - // As authority form + // As the authority form. + host = requestUri; } - if (schm == null) - schm = (websocketRequest ? "ws" : "http") + (secure ? "s" : String.Empty); + if (schm == null) { + schm = websocketRequest + ? (secure ? "wss" : "ws") + : (secure ? "https" : "http"); + } - var colon = host.IndexOf (':'); - if (colon == -1) - host = String.Format ("{0}:{1}", host, schm == "http" || schm == "ws" ? 80 : 443); + if (host.IndexOf (':') == -1) + host = String.Format ("{0}:{1}", host, secure ? 443 : 80); var url = String.Format ("{0}://{1}{2}", schm, host, path); + Uri ret; - Uri res; - if (!Uri.TryCreate (url, UriKind.Absolute, out res)) - return null; - - return res; + return Uri.TryCreate (url, UriKind.Absolute, out ret) ? ret : null; } internal static IPrincipal CreateUser ( @@ -554,21 +810,44 @@ internal static IPrincipal CreateUser ( AuthenticationSchemes scheme, string realm, string method, - Func credentialsFinder) + Func credentialsFinder + ) { - if (response == null || - !response.StartsWith (scheme.ToString (), StringComparison.OrdinalIgnoreCase)) + if (response == null || response.Length == 0) + return null; + + if (scheme == AuthenticationSchemes.Digest) { + if (realm == null || realm.Length == 0) + return null; + + if (method == null || method.Length == 0) + return null; + } + else { + if (scheme != AuthenticationSchemes.Basic) + return null; + } + + if (credentialsFinder == null) + return null; + + var compType = StringComparison.OrdinalIgnoreCase; + + if (!response.StartsWith (scheme.ToString (), compType)) return null; var res = AuthenticationResponse.Parse (response); + if (res == null) return null; var id = res.ToIdentity (); + if (id == null) return null; NetworkCredential cred = null; + try { cred = credentialsFinder (id); } @@ -578,155 +857,58 @@ internal static IPrincipal CreateUser ( if (cred == null) return null; - var valid = scheme == AuthenticationSchemes.Basic - ? ((HttpBasicIdentity) id).Password == cred.Password - : scheme == AuthenticationSchemes.Digest - ? ((HttpDigestIdentity) id).IsValid (cred.Password, realm, method, null) - : false; + if (scheme == AuthenticationSchemes.Basic) { + var basicId = (HttpBasicIdentity) id; - return valid + return basicId.Password == cred.Password + ? new GenericPrincipal (id, cred.Roles) + : null; + } + + var digestId = (HttpDigestIdentity) id; + + return digestId.IsValid (cred.Password, realm, method, null) ? new GenericPrincipal (id, cred.Roles) : null; } internal static Encoding GetEncoding (string contentType) { - var parts = contentType.Split (';'); - foreach (var p in parts) { - var part = p.Trim (); - if (part.StartsWith ("charset", StringComparison.OrdinalIgnoreCase)) - return Encoding.GetEncoding (part.GetValue ('=', true)); - } + var name = "charset="; + var compType = StringComparison.OrdinalIgnoreCase; - return null; - } + foreach (var elm in contentType.SplitHeaderValue (';')) { + var part = elm.Trim (); - internal static NameValueCollection InternalParseQueryString (string query, Encoding encoding) - { - int len; - if (query == null || (len = query.Length) == 0 || (len == 1 && query[0] == '?')) - return new NameValueCollection (1); - - if (query[0] == '?') - query = query.Substring (1); - - var res = new QueryStringCollection (); - var components = query.Split ('&'); - foreach (var component in components) { - var i = component.IndexOf ('='); - if (i > -1) { - var name = UrlDecode (component.Substring (0, i), encoding); - var val = component.Length > i + 1 - ? UrlDecode (component.Substring (i + 1), encoding) - : String.Empty; - - res.Add (name, val); - } - else { - res.Add (null, UrlDecode (component, encoding)); - } - } - - return res; - } - - internal static string InternalUrlDecode ( - byte[] bytes, int offset, int count, Encoding encoding) - { - var output = new StringBuilder (); - using (var acc = new MemoryStream ()) { - var end = count + offset; - for (var i = offset; i < end; i++) { - if (bytes[i] == '%' && i + 2 < count && bytes[i + 1] != '%') { - int xchar; - if (bytes[i + 1] == (byte) 'u' && i + 5 < end) { - if (acc.Length > 0) { - output.Append (getChars (acc, encoding)); - acc.SetLength (0); - } - - xchar = getChar (bytes, i + 2, 4); - if (xchar != -1) { - output.Append ((char) xchar); - i += 5; - - continue; - } - } - else if ((xchar = getChar (bytes, i + 1, 2)) != -1) { - acc.WriteByte ((byte) xchar); - i += 2; - - continue; - } - } - - if (acc.Length > 0) { - output.Append (getChars (acc, encoding)); - acc.SetLength (0); - } + if (!part.StartsWith (name, compType)) + continue; - if (bytes[i] == '+') { - output.Append (' '); - continue; - } + var val = part.GetValue ('=', true); - output.Append ((char) bytes[i]); - } + if (val == null || val.Length == 0) + return null; - if (acc.Length > 0) - output.Append (getChars (acc, encoding)); + return Encoding.GetEncoding (val); } - return output.ToString (); + return null; } - internal static byte[] InternalUrlDecodeToBytes (byte[] bytes, int offset, int count) + internal static bool TryGetEncoding ( + string contentType, + out Encoding result + ) { - using (var res = new MemoryStream ()) { - var end = offset + count; - for (var i = offset; i < end; i++) { - var c = (char) bytes[i]; - if (c == '+') { - c = ' '; - } - else if (c == '%' && i < end - 2) { - var xchar = getChar (bytes, i + 1, 2); - if (xchar != -1) { - c = (char) xchar; - i += 2; - } - } - - res.WriteByte ((byte) c); - } + result = null; - res.Close (); - return res.ToArray (); + try { + result = GetEncoding (contentType); } - } - - internal static byte[] InternalUrlEncodeToBytes (byte[] bytes, int offset, int count) - { - using (var res = new MemoryStream ()) { - var end = offset + count; - for (var i = offset; i < end; i++) - urlEncode ((char) bytes[i], res, false); - - res.Close (); - return res.ToArray (); + catch { + return false; } - } - internal static byte[] InternalUrlEncodeUnicodeToBytes (string s) - { - using (var res = new MemoryStream ()) { - foreach (var c in s) - urlEncode (c, res, true); - - res.Close (); - return res.ToArray (); - } + return result != null; } #endregion @@ -735,256 +917,74 @@ internal static byte[] InternalUrlEncodeUnicodeToBytes (string s) public static string HtmlAttributeEncode (string s) { - if (s == null || s.Length == 0 || !s.Contains ('&', '"', '<', '>')) - return s; + if (s == null) + throw new ArgumentNullException ("s"); - var output = new StringBuilder (); - foreach (var c in s) - output.Append ( - c == '&' - ? "&" - : c == '"' - ? """ - : c == '<' - ? "<" - : c == '>' - ? ">" - : c.ToString ()); - - return output.ToString (); + return s.Length > 0 ? htmlEncode (s, true) : s; } public static void HtmlAttributeEncode (string s, TextWriter output) { + if (s == null) + throw new ArgumentNullException ("s"); + if (output == null) throw new ArgumentNullException ("output"); - output.Write (HtmlAttributeEncode (s)); + if (s.Length == 0) + return; + + var encodedS = htmlEncode (s, true); + + output.Write (encodedS); } - /// - /// Decodes an HTML-encoded and returns the decoded . - /// - /// - /// A that represents the decoded string. - /// - /// - /// A to decode. - /// public static string HtmlDecode (string s) { - if (s == null || s.Length == 0 || !s.Contains ('&')) - return s; - - var entity = new StringBuilder (); - var output = new StringBuilder (); - - // 0 -> nothing, - // 1 -> right after '&' - // 2 -> between '&' and ';' but no '#' - // 3 -> '#' found after '&' and getting numbers - var state = 0; - - var number = 0; - var haveTrailingDigits = false; - foreach (var c in s) { - if (state == 0) { - if (c == '&') { - entity.Append (c); - state = 1; - } - else { - output.Append (c); - } - - continue; - } - - if (c == '&') { - state = 1; - if (haveTrailingDigits) { - entity.Append (number.ToString (CultureInfo.InvariantCulture)); - haveTrailingDigits = false; - } - - output.Append (entity.ToString ()); - entity.Length = 0; - entity.Append ('&'); - - continue; - } - - if (state == 1) { - if (c == ';') { - state = 0; - output.Append (entity.ToString ()); - output.Append (c); - entity.Length = 0; - } - else { - number = 0; - if (c != '#') - state = 2; - else - state = 3; - - entity.Append (c); - } - } - else if (state == 2) { - entity.Append (c); - if (c == ';') { - var key = entity.ToString (); - var entities = getEntities (); - if (key.Length > 1 && entities.ContainsKey (key.Substring (1, key.Length - 2))) - key = entities[key.Substring (1, key.Length - 2)].ToString (); - - output.Append (key); - state = 0; - entity.Length = 0; - } - } - else if (state == 3) { - if (c == ';') { - if (number > 65535) { - output.Append ("&#"); - output.Append (number.ToString (CultureInfo.InvariantCulture)); - output.Append (";"); - } - else { - output.Append ((char) number); - } - - state = 0; - entity.Length = 0; - haveTrailingDigits = false; - } - else if (Char.IsDigit (c)) { - number = number * 10 + ((int) c - '0'); - haveTrailingDigits = true; - } - else { - state = 2; - if (haveTrailingDigits) { - entity.Append (number.ToString (CultureInfo.InvariantCulture)); - haveTrailingDigits = false; - } - - entity.Append (c); - } - } - } - - if (entity.Length > 0) - output.Append (entity.ToString ()); - else if (haveTrailingDigits) - output.Append (number.ToString (CultureInfo.InvariantCulture)); + if (s == null) + throw new ArgumentNullException ("s"); - return output.ToString (); + return s.Length > 0 ? htmlDecode (s) : s; } - /// - /// Decodes an HTML-encoded and sends the decoded - /// to the specified . - /// - /// - /// A to decode. - /// - /// - /// A that receives the decoded string. - /// public static void HtmlDecode (string s, TextWriter output) { + if (s == null) + throw new ArgumentNullException ("s"); + if (output == null) throw new ArgumentNullException ("output"); - output.Write (HtmlDecode (s)); + if (s.Length == 0) + return; + + var decodedS = htmlDecode (s); + + output.Write (decodedS); } - /// - /// HTML-encodes a and returns the encoded . - /// - /// - /// A that represents the encoded string. - /// - /// - /// A to encode. - /// public static string HtmlEncode (string s) { - if (s == null || s.Length == 0) - return s; - - var needEncode = false; - foreach (var c in s) { - if (c == '&' || c == '"' || c == '<' || c == '>' || c > 159) { - needEncode = true; - break; - } - } - - if (!needEncode) - return s; - - var output = new StringBuilder (); - foreach (var c in s) { - if (c == '&') { - output.Append ("&"); - } - else if (c == '"') { - output.Append ("""); - } - else if (c == '<') { - output.Append ("<"); - } - else if (c == '>') { - output.Append (">"); - } - else if (c > 159) { - // MS starts encoding with &# from 160 and stops at 255. - // We don't do that. One reason is the 65308/65310 unicode - // characters that look like '<' and '>'. - output.Append ("&#"); - output.Append (((int) c).ToString (CultureInfo.InvariantCulture)); - output.Append (";"); - } - else { - output.Append (c); - } - } + if (s == null) + throw new ArgumentNullException ("s"); - return output.ToString (); + return s.Length > 0 ? htmlEncode (s, false) : s; } - /// - /// HTML-encodes a and sends the encoded - /// to the specified . - /// - /// - /// A to encode. - /// - /// - /// A that receives the encoded string. - /// public static void HtmlEncode (string s, TextWriter output) { + if (s == null) + throw new ArgumentNullException ("s"); + if (output == null) throw new ArgumentNullException ("output"); - output.Write (HtmlEncode (s)); - } - - public static NameValueCollection ParseQueryString (string query) - { - return ParseQueryString (query, Encoding.UTF8); - } + if (s.Length == 0) + return; - public static NameValueCollection ParseQueryString (string query, Encoding encoding) - { - if (query == null) - throw new ArgumentNullException ("query"); + var encodedS = htmlEncode (s, false); - return InternalParseQueryString (query, encoding ?? Encoding.UTF8); + output.Write (encodedS); } public static string UrlDecode (string s) @@ -992,71 +992,56 @@ public static string UrlDecode (string s) return UrlDecode (s, Encoding.UTF8); } - public static string UrlDecode (string s, Encoding encoding) + public static string UrlDecode (byte[] bytes, Encoding encoding) { - if (s == null || s.Length == 0 || !s.Contains ('%', '+')) - return s; - - if (encoding == null) - encoding = Encoding.UTF8; - - var buff = new List (); - var len = s.Length; - for (var i = 0; i < len; i++) { - var c = s[i]; - if (c == '%' && i + 2 < len && s[i + 1] != '%') { - int xchar; - if (s[i + 1] == 'u' && i + 5 < len) { - // Unicode hex sequence. - xchar = getChar (s, i + 2, 4); - if (xchar != -1) { - writeCharBytes ((char) xchar, buff, encoding); - i += 5; - } - else { - writeCharBytes ('%', buff, encoding); - } - } - else if ((xchar = getChar (s, i + 1, 2)) != -1) { - writeCharBytes ((char) xchar, buff, encoding); - i += 2; - } - else { - writeCharBytes ('%', buff, encoding); - } + if (bytes == null) + throw new ArgumentNullException ("bytes"); - continue; - } + var len = bytes.Length; - if (c == '+') { - writeCharBytes (' ', buff, encoding); - continue; - } + if (len == 0) + return String.Empty; - writeCharBytes (c, buff, encoding); - } + var decodedBytes = urlDecodeToBytes (bytes, 0, len); - return encoding.GetString (buff.ToArray ()); + return (encoding ?? Encoding.UTF8).GetString (decodedBytes); } - public static string UrlDecode (byte[] bytes, Encoding encoding) + public static string UrlDecode (string s, Encoding encoding) { - int len; - return bytes == null - ? null - : (len = bytes.Length) == 0 - ? String.Empty - : InternalUrlDecode (bytes, 0, len, encoding ?? Encoding.UTF8); + if (s == null) + throw new ArgumentNullException ("s"); + + if (s.Length == 0) + return s; + + var bytes = Encoding.ASCII.GetBytes (s); + var decodedBytes = urlDecodeToBytes (bytes, 0, bytes.Length); + + return (encoding ?? Encoding.UTF8).GetString (decodedBytes); } - public static string UrlDecode (byte[] bytes, int offset, int count, Encoding encoding) + public static string UrlDecode ( + byte[] bytes, + int offset, + int count, + Encoding encoding + ) { if (bytes == null) - return null; + throw new ArgumentNullException ("bytes"); var len = bytes.Length; - if (len == 0 || count == 0) + + if (len == 0) { + if (offset != 0) + throw new ArgumentOutOfRangeException ("offset"); + + if (count != 0) + throw new ArgumentOutOfRangeException ("count"); + return String.Empty; + } if (offset < 0 || offset >= len) throw new ArgumentOutOfRangeException ("offset"); @@ -1064,113 +1049,143 @@ public static string UrlDecode (byte[] bytes, int offset, int count, Encoding en if (count < 0 || count > len - offset) throw new ArgumentOutOfRangeException ("count"); - return InternalUrlDecode (bytes, offset, count, encoding ?? Encoding.UTF8); + if (count == 0) + return String.Empty; + + var decodedBytes = urlDecodeToBytes (bytes, offset, count); + + return (encoding ?? Encoding.UTF8).GetString (decodedBytes); } public static byte[] UrlDecodeToBytes (byte[] bytes) { - int len; - return bytes != null && (len = bytes.Length) > 0 - ? InternalUrlDecodeToBytes (bytes, 0, len) - : bytes; - } + if (bytes == null) + throw new ArgumentNullException ("bytes"); - public static byte[] UrlDecodeToBytes (string s) - { - return UrlDecodeToBytes (s, Encoding.UTF8); + var len = bytes.Length; + + return len > 0 ? urlDecodeToBytes (bytes, 0, len) : bytes; } - public static byte[] UrlDecodeToBytes (string s, Encoding encoding) + public static byte[] UrlDecodeToBytes (string s) { if (s == null) - return null; + throw new ArgumentNullException ("s"); if (s.Length == 0) return new byte[0]; - var bytes = (encoding ?? Encoding.UTF8).GetBytes (s); - return InternalUrlDecodeToBytes (bytes, 0, bytes.Length); + var bytes = Encoding.ASCII.GetBytes (s); + + return urlDecodeToBytes (bytes, 0, bytes.Length); } public static byte[] UrlDecodeToBytes (byte[] bytes, int offset, int count) { - int len; - if (bytes == null || (len = bytes.Length) == 0) - return bytes; + if (bytes == null) + throw new ArgumentNullException ("bytes"); - if (count == 0) - return new byte[0]; + var len = bytes.Length; + + if (len == 0) { + if (offset != 0) + throw new ArgumentOutOfRangeException ("offset"); + + if (count != 0) + throw new ArgumentOutOfRangeException ("count"); + + return bytes; + } if (offset < 0 || offset >= len) throw new ArgumentOutOfRangeException ("offset"); - if (count < 0 || count > len - offset ) + if (count < 0 || count > len - offset) throw new ArgumentOutOfRangeException ("count"); - return InternalUrlDecodeToBytes (bytes, offset, count); + return count > 0 ? urlDecodeToBytes (bytes, offset, count) : new byte[0]; } public static string UrlEncode (byte[] bytes) { - int len; - return bytes == null - ? null - : (len = bytes.Length) == 0 - ? String.Empty - : Encoding.ASCII.GetString (InternalUrlEncodeToBytes (bytes, 0, len)); + if (bytes == null) + throw new ArgumentNullException ("bytes"); + + var len = bytes.Length; + + if (len == 0) + return String.Empty; + + var encodedBytes = urlEncodeToBytes (bytes, 0, len); + + return Encoding.ASCII.GetString (encodedBytes); } public static string UrlEncode (string s) { return UrlEncode (s, Encoding.UTF8); } - + public static string UrlEncode (string s, Encoding encoding) { - int len; - if (s == null || (len = s.Length) == 0) - return s; - - var needEncode = false; - foreach (var c in s) { - if ((c < '0') || (c < 'A' && c > '9') || (c > 'Z' && c < 'a') || (c > 'z')) { - if (notEncoded (c)) - continue; + if (s == null) + throw new ArgumentNullException ("s"); - needEncode = true; - break; - } - } + var len = s.Length; - if (!needEncode) + if (len == 0) return s; if (encoding == null) encoding = Encoding.UTF8; - // Avoided GetByteCount call. - var bytes = new byte[encoding.GetMaxByteCount (len)]; - var realLen = encoding.GetBytes (s, 0, len, bytes, 0); + var maxCnt = encoding.GetMaxByteCount (len); + var bytes = new byte[maxCnt]; + var cnt = encoding.GetBytes (s, 0, len, bytes, 0); + var encodedBytes = urlEncodeToBytes (bytes, 0, cnt); - return Encoding.ASCII.GetString (InternalUrlEncodeToBytes (bytes, 0, realLen)); + return Encoding.ASCII.GetString (encodedBytes); } - + public static string UrlEncode (byte[] bytes, int offset, int count) { - var encoded = UrlEncodeToBytes (bytes, offset, count); - return encoded == null - ? null - : encoded.Length == 0 - ? String.Empty - : Encoding.ASCII.GetString (encoded); + if (bytes == null) + throw new ArgumentNullException ("bytes"); + + var len = bytes.Length; + + if (len == 0) { + if (offset != 0) + throw new ArgumentOutOfRangeException ("offset"); + + if (count != 0) + throw new ArgumentOutOfRangeException ("count"); + + return String.Empty; + } + + if (offset < 0 || offset >= len) + throw new ArgumentOutOfRangeException ("offset"); + + if (count < 0 || count > len - offset) + throw new ArgumentOutOfRangeException ("count"); + + if (count == 0) + return String.Empty; + + var encodedBytes = urlEncodeToBytes (bytes, offset, count); + + return Encoding.ASCII.GetString (encodedBytes); } public static byte[] UrlEncodeToBytes (byte[] bytes) { - int len; - return bytes != null && (len = bytes.Length) > 0 - ? InternalUrlEncodeToBytes (bytes, 0, len) - : bytes; + if (bytes == null) + throw new ArgumentNullException ("bytes"); + + var len = bytes.Length; + + return len > 0 ? urlEncodeToBytes (bytes, 0, len) : bytes; } public static byte[] UrlEncodeToBytes (string s) @@ -1181,23 +1196,32 @@ public static byte[] UrlEncodeToBytes (string s) public static byte[] UrlEncodeToBytes (string s, Encoding encoding) { if (s == null) - return null; + throw new ArgumentNullException ("s"); if (s.Length == 0) return new byte[0]; var bytes = (encoding ?? Encoding.UTF8).GetBytes (s); - return InternalUrlEncodeToBytes (bytes, 0, bytes.Length); + + return urlEncodeToBytes (bytes, 0, bytes.Length); } public static byte[] UrlEncodeToBytes (byte[] bytes, int offset, int count) { - int len; - if (bytes == null || (len = bytes.Length) == 0) - return bytes; + if (bytes == null) + throw new ArgumentNullException ("bytes"); - if (count == 0) - return new byte[0]; + var len = bytes.Length; + + if (len == 0) { + if (offset != 0) + throw new ArgumentOutOfRangeException ("offset"); + + if (count != 0) + throw new ArgumentOutOfRangeException ("count"); + + return bytes; + } if (offset < 0 || offset >= len) throw new ArgumentOutOfRangeException ("offset"); @@ -1205,37 +1229,7 @@ public static byte[] UrlEncodeToBytes (byte[] bytes, int offset, int count) if (count < 0 || count > len - offset) throw new ArgumentOutOfRangeException ("count"); - return InternalUrlEncodeToBytes (bytes, offset, count); - } - - public static string UrlEncodeUnicode (string s) - { - return s != null && s.Length > 0 - ? Encoding.ASCII.GetString (InternalUrlEncodeUnicodeToBytes (s)) - : s; - } - - public static byte[] UrlEncodeUnicodeToBytes (string s) - { - return s == null - ? null - : s.Length == 0 - ? new byte[0] - : InternalUrlEncodeUnicodeToBytes (s); - } - - public static string UrlPathEncode (string s) - { - if (s == null || s.Length == 0) - return s; - - using (var res = new MemoryStream ()) { - foreach (var c in s) - urlPathEncode (c, res); - - res.Close (); - return Encoding.ASCII.GetString (res.ToArray ()); - } + return count > 0 ? urlEncodeToBytes (bytes, offset, count) : new byte[0]; } #endregion diff --git a/websocket-sharp/Net/HttpVersion.cs b/websocket-sharp/Net/HttpVersion.cs index d20061e0b..95f8f0a38 100644 --- a/websocket-sharp/Net/HttpVersion.cs +++ b/websocket-sharp/Net/HttpVersion.cs @@ -2,12 +2,12 @@ /* * HttpVersion.cs * - * This code is derived from System.Net.HttpVersion.cs of Mono + * This code is derived from HttpVersion.cs (System.Net) of Mono * (http://www.mono-project.com). * * The MIT License * - * Copyright (c) 2012-2014 sta.blockhead + * Copyright (c) 2012-2024 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/websocket-sharp/Net/NetworkCredential.cs b/websocket-sharp/Net/NetworkCredential.cs index e66394a91..f97df971f 100644 --- a/websocket-sharp/Net/NetworkCredential.cs +++ b/websocket-sharp/Net/NetworkCredential.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2014 sta.blockhead + * Copyright (c) 2014-2024 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 @@ -31,16 +31,26 @@ namespace WebSocketSharp.Net { /// - /// Provides the credentials for HTTP authentication (Basic/Digest). + /// Provides the credentials for the password-based authentication. /// public class NetworkCredential { #region Private Fields - private string _domain; - private string _password; - private string [] _roles; - private string _username; + private string _domain; + private static readonly string[] _noRoles; + private string _password; + private string[] _roles; + private string _username; + + #endregion + + #region Static Constructor + + static NetworkCredential () + { + _noRoles = new string[0]; + } #endregion @@ -48,52 +58,65 @@ public class NetworkCredential /// /// Initializes a new instance of the class - /// with the specified user name and password. + /// with the specified username and password. /// /// - /// A that represents the user name associated with the - /// credentials. + /// A that specifies the username associated with + /// the credentials. /// /// - /// A that represents the password for the user name + /// A that specifies the password for the username /// associated with the credentials. /// /// - /// is or empty. + /// is an empty string. + /// + /// + /// is . /// public NetworkCredential (string username, string password) - : this (username, password, null, new string [0]) + : this (username, password, null, null) { } /// /// Initializes a new instance of the class - /// with the specified user name, password, domain, and roles. + /// with the specified username, password, domain and roles. /// /// - /// A that represents the user name associated with the - /// credentials. + /// A that specifies the username associated with + /// the credentials. /// /// - /// A that represents the password for the user name + /// A that specifies the password for the username /// associated with the credentials. /// /// - /// A that represents the name of the user domain - /// associated with the credentials. + /// A that specifies the domain associated with + /// the credentials. /// /// - /// An array of that contains the role names to which - /// the user associated with the credentials belongs if any. + /// An array of that specifies the roles associated + /// with the credentials if any. /// /// - /// is or empty. + /// is an empty string. + /// + /// + /// is . /// public NetworkCredential ( - string username, string password, string domain, params string [] roles) + string username, + string password, + string domain, + params string[] roles + ) { - if (username == null || username.Length == 0) - throw new ArgumentException ("Must not be null or empty.", "username"); + if (username == null) + throw new ArgumentNullException ("username"); + + if (username.Length == 0) + throw new ArgumentException ("An empty string.", "username"); _username = username; _password = password; @@ -106,11 +129,17 @@ public NetworkCredential ( #region Public Properties /// - /// Gets the name of the user domain associated with the credentials. + /// Gets the domain associated with the credentials. /// /// - /// A that represents the name of the user domain - /// associated with the credentials. + /// + /// A that represents the domain name + /// to which the username belongs. + /// + /// + /// An empty string if the value was initialized with + /// . + /// /// public string Domain { get { @@ -123,11 +152,16 @@ internal set { } /// - /// Gets the password for the user name associated with the credentials. + /// Gets the password for the username associated with the credentials. /// /// - /// A that represents the password for the user name - /// associated with the credentials. + /// + /// A that represents the password. + /// + /// + /// An empty string if the value was initialized with + /// . + /// /// public string Password { get { @@ -140,16 +174,21 @@ internal set { } /// - /// Gets the role names to which the user associated with the credentials - /// belongs. + /// Gets the roles associated with the credentials. /// /// - /// An array of that contains the role names to which - /// the user associated with the credentials belongs. + /// + /// An array of that represents the role names + /// to which the username belongs. + /// + /// + /// An empty array if the value was initialized with + /// . + /// /// - public string [] Roles { + public string[] Roles { get { - return _roles; + return _roles ?? _noRoles; } internal set { @@ -158,13 +197,12 @@ internal set { } /// - /// Gets the user name associated with the credentials. + /// Gets the username associated with the credentials. /// /// - /// A that represents the user name associated with the - /// credentials. + /// A that represents the username. /// - public string UserName { + public string Username { get { return _username; } diff --git a/websocket-sharp/Net/QueryStringCollection.cs b/websocket-sharp/Net/QueryStringCollection.cs index 6a2285fb6..d5b7a8d9a 100644 --- a/websocket-sharp/Net/QueryStringCollection.cs +++ b/websocket-sharp/Net/QueryStringCollection.cs @@ -2,13 +2,13 @@ /* * QueryStringCollection.cs * - * This code is derived from System.Net.HttpUtility.cs of Mono + * This code is derived from HttpUtility.cs (System.Net) of Mono * (http://www.mono-project.com). * * The MIT License * * Copyright (c) 2005-2009 Novell, Inc. (http://www.novell.com) - * Copyright (c) 2014 sta.blockhead + * Copyright (c) 2018-2023 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 @@ -48,21 +48,97 @@ namespace WebSocketSharp.Net { internal sealed class QueryStringCollection : NameValueCollection { + #region Public Constructors + + public QueryStringCollection () + { + } + + public QueryStringCollection (int capacity) + : base (capacity) + { + } + + #endregion + + #region Public Methods + + public static QueryStringCollection Parse (string query) + { + return Parse (query, Encoding.UTF8); + } + + public static QueryStringCollection Parse (string query, Encoding encoding) + { + if (query == null) + return new QueryStringCollection (1); + + if (query.Length == 0) + return new QueryStringCollection (1); + + if (query == "?") + return new QueryStringCollection (1); + + if (query[0] == '?') + query = query.Substring (1); + + if (encoding == null) + encoding = Encoding.UTF8; + + var ret = new QueryStringCollection (); + + foreach (var component in query.Split ('&')) { + var len = component.Length; + + if (len == 0) + continue; + + if (component == "=") + continue; + + string name = null; + string val = null; + + var idx = component.IndexOf ('='); + + if (idx < 0) { + val = component.UrlDecode (encoding); + } + else if (idx == 0) { + val = component.Substring (1).UrlDecode (encoding); + } + else { + name = component.Substring (0, idx).UrlDecode (encoding); + + var start = idx + 1; + val = start < len + ? component.Substring (start).UrlDecode (encoding) + : String.Empty; + } + + ret.Add (name, val); + } + + return ret; + } + public override string ToString () { - var cnt = Count; - if (cnt == 0) + if (Count == 0) return String.Empty; - var output = new StringBuilder (); - var keys = AllKeys; - foreach (var key in keys) - output.AppendFormat ("{0}={1}&", key, this [key]); + var buff = new StringBuilder (); + + var fmt = "{0}={1}&"; - if (output.Length > 0) - output.Length--; + foreach (var key in AllKeys) + buff.AppendFormat (fmt, key, this[key]); - return output.ToString (); + buff.Length--; + + return buff.ToString (); } + + #endregion } } diff --git a/websocket-sharp/Net/ReadBufferState.cs b/websocket-sharp/Net/ReadBufferState.cs index 780a69b5a..bf0de8848 100644 --- a/websocket-sharp/Net/ReadBufferState.cs +++ b/websocket-sharp/Net/ReadBufferState.cs @@ -8,7 +8,7 @@ * The MIT License * * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) - * Copyright (c) 2014-2015 sta.blockhead + * Copyright (c) 2014-2023 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 @@ -56,13 +56,18 @@ internal class ReadBufferState #region Public Constructors public ReadBufferState ( - byte[] buffer, int offset, int count, HttpStreamAsyncResult asyncResult) + byte[] buffer, + int offset, + int count, + HttpStreamAsyncResult asyncResult + ) { _buffer = buffer; _offset = offset; _count = count; - _initialCount = count; _asyncResult = asyncResult; + + _initialCount = count; } #endregion diff --git a/websocket-sharp/Net/RequestStream.cs b/websocket-sharp/Net/RequestStream.cs index dd40b3784..dd40f920a 100644 --- a/websocket-sharp/Net/RequestStream.cs +++ b/websocket-sharp/Net/RequestStream.cs @@ -8,7 +8,7 @@ * The MIT License * * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2023 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 @@ -46,27 +46,27 @@ internal class RequestStream : Stream { #region Private Fields - private long _bodyLeft; - private byte[] _buffer; - private int _count; - private bool _disposed; - private int _offset; - private Stream _stream; + private long _bodyLeft; + private int _count; + private bool _disposed; + private byte[] _initialBuffer; + private Stream _innerStream; + private int _offset; #endregion #region Internal Constructors - internal RequestStream (Stream stream, byte[] buffer, int offset, int count) - : this (stream, buffer, offset, count, -1) - { - } - internal RequestStream ( - Stream stream, byte[] buffer, int offset, int count, long contentLength) + Stream innerStream, + byte[] initialBuffer, + int offset, + int count, + long contentLength + ) { - _stream = stream; - _buffer = buffer; + _innerStream = innerStream; + _initialBuffer = initialBuffer; _offset = offset; _count = count; _bodyLeft = contentLength; @@ -74,6 +74,34 @@ internal RequestStream ( #endregion + #region Internal Properties + + internal int Count { + get { + return _count; + } + } + + internal byte[] InitialBuffer { + get { + return _initialBuffer; + } + } + + internal string ObjectName { + get { + return GetType ().ToString (); + } + } + + internal int Offset { + get { + return _offset; + } + } + + #endregion + #region Public Properties public override bool CanRead { @@ -114,40 +142,30 @@ public override long Position { #region Private Methods - // Returns 0 if we can keep reading from the base stream, - // > 0 if we read something from the buffer, - // -1 if we had a content length set and we finished reading that many bytes. - private int fillFromBuffer (byte[] buffer, int offset, int count) + private int fillFromInitialBuffer (byte[] buffer, int offset, int count) { - if (buffer == null) - throw new ArgumentNullException ("buffer"); - - if (offset < 0) - throw new ArgumentOutOfRangeException ("offset", "A negative value."); - - if (count < 0) - throw new ArgumentOutOfRangeException ("count", "A negative value."); - - var len = buffer.Length; - if (offset + count > len) - throw new ArgumentException ( - "The sum of 'offset' and 'count' is greater than 'buffer' length."); + // This method returns a int: + // - > 0 The number of bytes read from the initial buffer + // - 0 No more bytes read from the initial buffer + // - -1 No more content data if (_bodyLeft == 0) return -1; - if (_count == 0 || count == 0) + if (_count == 0) return 0; if (count > _count) count = _count; - if (_bodyLeft > 0 && count > _bodyLeft) + if (_bodyLeft > 0 && _bodyLeft < count) count = (int) _bodyLeft; - Buffer.BlockCopy (_buffer, _offset, buffer, offset, count); + Buffer.BlockCopy (_initialBuffer, _offset, buffer, offset, count); + _offset += count; _count -= count; + if (_bodyLeft > 0) _bodyLeft -= count; @@ -159,32 +177,70 @@ private int fillFromBuffer (byte[] buffer, int offset, int count) #region Public Methods public override IAsyncResult BeginRead ( - byte[] buffer, int offset, int count, AsyncCallback callback, object state) + byte[] buffer, + int offset, + int count, + AsyncCallback callback, + object state + ) { if (_disposed) - throw new ObjectDisposedException (GetType ().ToString ()); + throw new ObjectDisposedException (ObjectName); + + if (buffer == null) + throw new ArgumentNullException ("buffer"); + + if (offset < 0) { + var msg = "A negative value."; + + throw new ArgumentOutOfRangeException ("offset", msg); + } + + if (count < 0) { + var msg = "A negative value."; + + throw new ArgumentOutOfRangeException ("count", msg); + } + + var len = buffer.Length; + + if (offset + count > len) { + var msg = "The sum of offset and count is greater than the length of buffer."; + + throw new ArgumentException (msg); + } - var nread = fillFromBuffer (buffer, offset, count); - if (nread > 0 || nread == -1) { + if (count == 0) + return _innerStream.BeginRead (buffer, offset, 0, callback, state); + + var nread = fillFromInitialBuffer (buffer, offset, count); + + if (nread != 0) { var ares = new HttpStreamAsyncResult (callback, state); + ares.Buffer = buffer; ares.Offset = offset; ares.Count = count; ares.SyncRead = nread > 0 ? nread : 0; + ares.Complete (); return ares; } - // Avoid reading past the end of the request to allow for HTTP pipelining. - if (_bodyLeft >= 0 && count > _bodyLeft) + if (_bodyLeft > 0 && _bodyLeft < count) count = (int) _bodyLeft; - return _stream.BeginRead (buffer, offset, count, callback, state); + return _innerStream.BeginRead (buffer, offset, count, callback, state); } public override IAsyncResult BeginWrite ( - byte[] buffer, int offset, int count, AsyncCallback callback, object state) + byte[] buffer, + int offset, + int count, + AsyncCallback callback, + object state + ) { throw new NotSupportedException (); } @@ -197,21 +253,22 @@ public override void Close () public override int EndRead (IAsyncResult asyncResult) { if (_disposed) - throw new ObjectDisposedException (GetType ().ToString ()); + throw new ObjectDisposedException (ObjectName); if (asyncResult == null) throw new ArgumentNullException ("asyncResult"); if (asyncResult is HttpStreamAsyncResult) { var ares = (HttpStreamAsyncResult) asyncResult; + if (!ares.IsCompleted) ares.AsyncWaitHandle.WaitOne (); return ares.SyncRead; } - // Close on exception? - var nread = _stream.EndRead (asyncResult); + var nread = _innerStream.EndRead (asyncResult); + if (nread > 0 && _bodyLeft > 0) _bodyLeft -= nread; @@ -230,17 +287,47 @@ public override void Flush () public override int Read (byte[] buffer, int offset, int count) { if (_disposed) - throw new ObjectDisposedException (GetType ().ToString ()); + throw new ObjectDisposedException (ObjectName); + + if (buffer == null) + throw new ArgumentNullException ("buffer"); - // Call the fillFromBuffer method to check for buffer boundaries even when _bodyLeft is 0. - var nread = fillFromBuffer (buffer, offset, count); - if (nread == -1) // No more bytes available (Content-Length). + if (offset < 0) { + var msg = "A negative value."; + + throw new ArgumentOutOfRangeException ("offset", msg); + } + + if (count < 0) { + var msg = "A negative value."; + + throw new ArgumentOutOfRangeException ("count", msg); + } + + var len = buffer.Length; + + if (offset + count > len) { + var msg = "The sum of offset and count is greater than the length of buffer."; + + throw new ArgumentException (msg); + } + + if (count == 0) + return 0; + + var nread = fillFromInitialBuffer (buffer, offset, count); + + if (nread == -1) return 0; if (nread > 0) return nread; - nread = _stream.Read (buffer, offset, count); + if (_bodyLeft > 0 && _bodyLeft < count) + count = (int) _bodyLeft; + + nread = _innerStream.Read (buffer, offset, count); + if (nread > 0 && _bodyLeft > 0) _bodyLeft -= nread; diff --git a/websocket-sharp/Net/ResponseStream.cs b/websocket-sharp/Net/ResponseStream.cs index 85059a407..456d1e470 100644 --- a/websocket-sharp/Net/ResponseStream.cs +++ b/websocket-sharp/Net/ResponseStream.cs @@ -8,7 +8,7 @@ * The MIT License * * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2023 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 @@ -47,24 +47,40 @@ internal class ResponseStream : Stream { #region Private Fields - private MemoryStream _body; - private static readonly byte[] _crlf = new byte[] { 13, 10 }; + private MemoryStream _bodyBuffer; + private static readonly byte[] _crlf; private bool _disposed; + private Stream _innerStream; + private static readonly byte[] _lastChunk; + private static readonly int _maxHeadersLength; private HttpListenerResponse _response; private bool _sendChunked; - private Stream _stream; private Action _write; private Action _writeBody; private Action _writeChunked; #endregion + #region Static Constructor + + static ResponseStream () + { + _crlf = new byte[] { 13, 10 }; // "\r\n" + _lastChunk = new byte[] { 48, 13, 10, 13, 10 }; // "0\r\n\r\n" + _maxHeadersLength = 32768; + } + + #endregion + #region Internal Constructors internal ResponseStream ( - Stream stream, HttpListenerResponse response, bool ignoreWriteExceptions) + Stream innerStream, + HttpListenerResponse response, + bool ignoreWriteExceptions + ) { - _stream = stream; + _innerStream = innerStream; _response = response; if (ignoreWriteExceptions) { @@ -72,11 +88,21 @@ internal ResponseStream ( _writeChunked = writeChunkedWithoutThrowingException; } else { - _write = stream.Write; + _write = innerStream.Write; _writeChunked = writeChunked; } - _body = new MemoryStream (); + _bodyBuffer = new MemoryStream (); + } + + #endregion + + #region Internal Properties + + internal string ObjectName { + get { + return GetType ().ToString (); + } } #endregion @@ -124,80 +150,118 @@ public override long Position { private bool flush (bool closing) { if (!_response.HeadersSent) { - if (!flushHeaders (closing)) { - if (closing) - _response.CloseConnection = true; - + if (!flushHeaders ()) return false; - } + + _response.HeadersSent = true; _sendChunked = _response.SendChunked; _writeBody = _sendChunked ? _writeChunked : _write; } flushBody (closing); - if (closing && _sendChunked) { - var last = getChunkSizeBytes (0, true); - _write (last, 0, last.Length); - } return true; } private void flushBody (bool closing) { - using (_body) { - var len = _body.Length; + using (_bodyBuffer) { + var len = _bodyBuffer.Length; + if (len > Int32.MaxValue) { - _body.Position = 0; + _bodyBuffer.Position = 0; + var buffLen = 1024; var buff = new byte[buffLen]; var nread = 0; - while ((nread = _body.Read (buff, 0, buffLen)) > 0) + + while (true) { + nread = _bodyBuffer.Read (buff, 0, buffLen); + + if (nread <= 0) + break; + _writeBody (buff, 0, nread); + } } else if (len > 0) { - _writeBody (_body.GetBuffer (), 0, (int) len); + var buff = _bodyBuffer.GetBuffer (); + + _writeBody (buff, 0, (int) len); } } - _body = !closing ? new MemoryStream () : null; + if (!closing) { + _bodyBuffer = new MemoryStream (); + + return; + } + + if (_sendChunked) + _write (_lastChunk, 0, 5); + + _bodyBuffer = null; } - private bool flushHeaders (bool closing) + private bool flushHeaders () { - using (var buff = new MemoryStream ()) { - var headers = _response.WriteHeadersTo (buff); - var start = buff.Position; - var len = buff.Length - start; - if (len > 32768) + if (!_response.SendChunked) { + if (_response.ContentLength64 != _bodyBuffer.Length) return false; + } + + var headers = _response.FullHeaders; + + var stream = new MemoryStream (); + var enc = Encoding.UTF8; + + using (var writer = new StreamWriter (stream, enc, 256)) { + writer.Write (_response.StatusLine); + + var s = headers.ToStringMultiValue (true); + + writer.Write (s); + writer.Flush (); - if (!_response.SendChunked && _response.ContentLength64 != _body.Length) + var start = enc.GetPreamble ().Length; + var len = stream.Length - start; + + if (len > _maxHeadersLength) return false; - _write (buff.GetBuffer (), (int) start, (int) len); - _response.CloseConnection = headers["Connection"] == "close"; - _response.HeadersSent = true; + var buff = stream.GetBuffer (); + + _write (buff, start, (int) len); } + _response.CloseConnection = headers["Connection"] == "close"; + return true; } - private static byte[] getChunkSizeBytes (int size, bool final) + private static byte[] getChunkSizeStringAsBytes (int size) { - return Encoding.ASCII.GetBytes (String.Format ("{0:x}\r\n{1}", size, final ? "\r\n" : "")); + var fmt = "{0:x}\r\n"; + var s = String.Format (fmt, size); + + return Encoding.ASCII.GetBytes (s); } private void writeChunked (byte[] buffer, int offset, int count) { - var size = getChunkSizeBytes (count, false); - _stream.Write (size, 0, size.Length); - _stream.Write (buffer, offset, count); - _stream.Write (_crlf, 0, 2); + var size = getChunkSizeStringAsBytes (count); + + _innerStream.Write (size, 0, size.Length); + _innerStream.Write (buffer, offset, count); + _innerStream.Write (_crlf, 0, 2); } - private void writeChunkedWithoutThrowingException (byte[] buffer, int offset, int count) + private void writeChunkedWithoutThrowingException ( + byte[] buffer, + int offset, + int count + ) { try { writeChunked (buffer, offset, count); @@ -206,10 +270,14 @@ private void writeChunkedWithoutThrowingException (byte[] buffer, int offset, in } } - private void writeWithoutThrowingException (byte[] buffer, int offset, int count) + private void writeWithoutThrowingException ( + byte[] buffer, + int offset, + int count + ) { try { - _stream.Write (buffer, offset, count); + _innerStream.Write (buffer, offset, count); } catch { } @@ -225,23 +293,29 @@ internal void Close (bool force) return; _disposed = true; - if (!force && flush (true)) { - _response.Close (); - } - else { - if (_sendChunked) { - var last = getChunkSizeBytes (0, true); - _write (last, 0, last.Length); - } - _body.Dispose (); - _body = null; + if (!force) { + if (flush (true)) { + _response.Close (); - _response.Abort (); + _response = null; + _innerStream = null; + + return; + } + + _response.CloseConnection = true; } + if (_sendChunked) + _write (_lastChunk, 0, 5); + + _bodyBuffer.Dispose (); + _response.Abort (); + + _bodyBuffer = null; _response = null; - _stream = null; + _innerStream = null; } internal void InternalWrite (byte[] buffer, int offset, int count) @@ -254,18 +328,28 @@ internal void InternalWrite (byte[] buffer, int offset, int count) #region Public Methods public override IAsyncResult BeginRead ( - byte[] buffer, int offset, int count, AsyncCallback callback, object state) + byte[] buffer, + int offset, + int count, + AsyncCallback callback, + object state + ) { throw new NotSupportedException (); } public override IAsyncResult BeginWrite ( - byte[] buffer, int offset, int count, AsyncCallback callback, object state) + byte[] buffer, + int offset, + int count, + AsyncCallback callback, + object state + ) { if (_disposed) - throw new ObjectDisposedException (GetType ().ToString ()); + throw new ObjectDisposedException (ObjectName); - return _body.BeginWrite (buffer, offset, count, callback, state); + return _bodyBuffer.BeginWrite (buffer, offset, count, callback, state); } public override void Close () @@ -286,15 +370,22 @@ public override int EndRead (IAsyncResult asyncResult) public override void EndWrite (IAsyncResult asyncResult) { if (_disposed) - throw new ObjectDisposedException (GetType ().ToString ()); + throw new ObjectDisposedException (ObjectName); - _body.EndWrite (asyncResult); + _bodyBuffer.EndWrite (asyncResult); } public override void Flush () { - if (!_disposed && (_sendChunked || _response.SendChunked)) - flush (false); + if (_disposed) + return; + + var sendChunked = _sendChunked || _response.SendChunked; + + if (!sendChunked) + return; + + flush (false); } public override int Read (byte[] buffer, int offset, int count) @@ -315,9 +406,9 @@ public override void SetLength (long value) public override void Write (byte[] buffer, int offset, int count) { if (_disposed) - throw new ObjectDisposedException (GetType ().ToString ()); + throw new ObjectDisposedException (ObjectName); - _body.Write (buffer, offset, count); + _bodyBuffer.Write (buffer, offset, count); } #endregion diff --git a/websocket-sharp/Net/ServerSslConfiguration.cs b/websocket-sharp/Net/ServerSslConfiguration.cs index 3f0883afe..b4de5d64c 100644 --- a/websocket-sharp/Net/ServerSslConfiguration.cs +++ b/websocket-sharp/Net/ServerSslConfiguration.cs @@ -5,7 +5,7 @@ * The MIT License * * Copyright (c) 2014 liryna - * Copyright (c) 2014 sta.blockhead + * Copyright (c) 2014-2024 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 @@ -34,6 +34,7 @@ */ #endregion +using System; using System.Net.Security; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; @@ -41,63 +42,52 @@ namespace WebSocketSharp.Net { /// - /// Stores the parameters used to configure a instance as a server. + /// Stores the parameters for instances used by + /// a server. /// - public class ServerSslConfiguration : SslConfiguration + public class ServerSslConfiguration { #region Private Fields - private X509Certificate2 _cert; - private bool _clientCertRequired; + private bool _checkCertRevocation; + private bool _clientCertRequired; + private RemoteCertificateValidationCallback _clientCertValidationCallback; + private SslProtocols _enabledSslProtocols; + private X509Certificate2 _serverCert; #endregion #region Public Constructors /// - /// Initializes a new instance of the class with - /// the specified . + /// Initializes a new instance of the + /// class. /// - /// - /// A that represents the certificate used to authenticate - /// the server. - /// - public ServerSslConfiguration (X509Certificate2 serverCertificate) - : this (serverCertificate, false, SslProtocols.Default, false) + public ServerSslConfiguration () { + _enabledSslProtocols = SslProtocols.None; } /// - /// Initializes a new instance of the class with - /// the specified , - /// , , - /// and . + /// Initializes a new instance of the + /// class copying from the specified configuration. /// - /// - /// A that represents the certificate used to authenticate - /// the server. - /// - /// - /// true if the client must supply a certificate for authentication; - /// otherwise, false. - /// - /// - /// The enum value that represents the protocols used for - /// authentication. + /// + /// A from which to copy. /// - /// - /// true if the certificate revocation list is checked during authentication; - /// otherwise, false. - /// - public ServerSslConfiguration ( - X509Certificate2 serverCertificate, - bool clientCertificateRequired, - SslProtocols enabledSslProtocols, - bool checkCertificateRevocation) - : base (enabledSslProtocols, checkCertificateRevocation) + /// + /// is . + /// + public ServerSslConfiguration (ServerSslConfiguration configuration) { - _cert = serverCertificate; - _clientCertRequired = clientCertificateRequired; + if (configuration == null) + throw new ArgumentNullException ("configuration"); + + _checkCertRevocation = configuration._checkCertRevocation; + _clientCertRequired = configuration._clientCertRequired; + _clientCertValidationCallback = configuration._clientCertValidationCallback; + _enabledSslProtocols = configuration._enabledSslProtocols; + _serverCert = configuration._serverCert; } #endregion @@ -105,11 +95,40 @@ public ServerSslConfiguration ( #region Public Properties /// - /// Gets or sets a value indicating whether the client must supply a certificate for - /// authentication. + /// Gets or sets a value indicating whether the certificate revocation + /// list is checked during authentication. /// /// - /// true if the client must supply a certificate; otherwise, false. + /// + /// true if the certificate revocation list is checked during + /// authentication; otherwise, false. + /// + /// + /// The default value is false. + /// + /// + public bool CheckCertificateRevocation { + get { + return _checkCertRevocation; + } + + set { + _checkCertRevocation = value; + } + } + + /// + /// Gets or sets a value indicating whether each client is asked for + /// a certificate for authentication. + /// + /// + /// + /// true if each client is asked for a certificate for + /// authentication; otherwise, false. + /// + /// + /// The default value is false. + /// /// public bool ClientCertificateRequired { get { @@ -122,43 +141,99 @@ public bool ClientCertificateRequired { } /// - /// Gets or sets the callback used to validate the certificate supplied by the client. + /// Gets or sets the callback used to validate the certificate supplied by + /// each client. /// /// - /// If this callback returns true, the client certificate will be valid. + /// The certificate is valid if the callback returns true. /// /// - /// A delegate that references the method - /// used to validate the client certificate. The default value is a function that only returns - /// true. + /// + /// A delegate. + /// + /// + /// It represents the delegate called when the server validates + /// the certificate. + /// + /// + /// The default value invokes a method that only returns true. + /// /// public RemoteCertificateValidationCallback ClientCertificateValidationCallback { get { - return CertificateValidationCallback; + if (_clientCertValidationCallback == null) + _clientCertValidationCallback = defaultValidateClientCertificate; + + return _clientCertValidationCallback; } set { - CertificateValidationCallback = value; + _clientCertValidationCallback = value; } } /// - /// Gets or sets the certificate used to authenticate the server for secure connection. + /// Gets or sets the enabled versions of the SSL/TLS protocols. /// /// - /// A that represents the certificate used to authenticate - /// the server. + /// + /// Any of the enum values. + /// + /// + /// It represents the enabled versions of the SSL/TLS protocols. + /// + /// + /// The default value is . + /// + /// + public SslProtocols EnabledSslProtocols { + get { + return _enabledSslProtocols; + } + + set { + _enabledSslProtocols = value; + } + } + + /// + /// Gets or sets the certificate used to authenticate the server. + /// + /// + /// + /// A that represents an X.509 certificate. + /// + /// + /// if not present. + /// + /// + /// The default value is . + /// /// public X509Certificate2 ServerCertificate { get { - return _cert; + return _serverCert; } set { - _cert = value; + _serverCert = value; } } #endregion + + #region Private Methods + + private static bool defaultValidateClientCertificate ( + object sender, + X509Certificate certificate, + X509Chain chain, + SslPolicyErrors sslPolicyErrors + ) + { + return true; + } + + #endregion } } diff --git a/websocket-sharp/Net/SslConfiguration.cs b/websocket-sharp/Net/SslConfiguration.cs deleted file mode 100644 index bfd3e5ac0..000000000 --- a/websocket-sharp/Net/SslConfiguration.cs +++ /dev/null @@ -1,172 +0,0 @@ -#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/WebHeaderCollection.cs b/websocket-sharp/Net/WebHeaderCollection.cs index 8423d2f17..da4a79d94 100644 --- a/websocket-sharp/Net/WebHeaderCollection.cs +++ b/websocket-sharp/Net/WebHeaderCollection.cs @@ -9,7 +9,7 @@ * * Copyright (c) 2003 Ximian, Inc. (http://www.ximian.com) * Copyright (c) 2007 Novell, Inc. (http://www.novell.com) - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2024 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 @@ -52,7 +52,8 @@ namespace WebSocketSharp.Net { /// - /// Provides a collection of the HTTP headers associated with a request or response. + /// Provides a collection of the HTTP headers associated with a request or + /// response. /// [Serializable] [ComVisible (true)] @@ -71,378 +72,485 @@ public class WebHeaderCollection : NameValueCollection, ISerializable static WebHeaderCollection () { _headers = - new Dictionary (StringComparer.InvariantCultureIgnoreCase) { + new Dictionary ( + StringComparer.InvariantCultureIgnoreCase + ) + { { "Accept", new HttpHeaderInfo ( "Accept", - HttpHeaderType.Request | HttpHeaderType.Restricted | HttpHeaderType.MultiValue) + HttpHeaderType.Request + | HttpHeaderType.Restricted + | HttpHeaderType.MultiValue + ) }, { "AcceptCharset", new HttpHeaderInfo ( "Accept-Charset", - HttpHeaderType.Request | HttpHeaderType.MultiValue) + HttpHeaderType.Request | HttpHeaderType.MultiValue + ) }, { "AcceptEncoding", new HttpHeaderInfo ( "Accept-Encoding", - HttpHeaderType.Request | HttpHeaderType.MultiValue) + HttpHeaderType.Request | HttpHeaderType.MultiValue + ) }, { "AcceptLanguage", new HttpHeaderInfo ( "Accept-Language", - HttpHeaderType.Request | HttpHeaderType.MultiValue) + HttpHeaderType.Request | HttpHeaderType.MultiValue + ) }, { "AcceptRanges", new HttpHeaderInfo ( "Accept-Ranges", - HttpHeaderType.Response | HttpHeaderType.MultiValue) + HttpHeaderType.Response | HttpHeaderType.MultiValue + ) }, { "Age", new HttpHeaderInfo ( "Age", - HttpHeaderType.Response) + HttpHeaderType.Response + ) }, { "Allow", new HttpHeaderInfo ( "Allow", - HttpHeaderType.Request | HttpHeaderType.Response | HttpHeaderType.MultiValue) + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.MultiValue + ) }, { "Authorization", new HttpHeaderInfo ( "Authorization", - HttpHeaderType.Request | HttpHeaderType.MultiValue) + HttpHeaderType.Request + | HttpHeaderType.Restricted + | HttpHeaderType.MultiValue + ) }, { "CacheControl", new HttpHeaderInfo ( "Cache-Control", - HttpHeaderType.Request | HttpHeaderType.Response | HttpHeaderType.MultiValue) + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.MultiValue + ) }, { "Connection", new HttpHeaderInfo ( "Connection", - HttpHeaderType.Request | - HttpHeaderType.Response | - HttpHeaderType.Restricted | - HttpHeaderType.MultiValue) + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.Restricted + | HttpHeaderType.MultiValue + ) }, { "ContentEncoding", new HttpHeaderInfo ( "Content-Encoding", - HttpHeaderType.Request | HttpHeaderType.Response | HttpHeaderType.MultiValue) + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.MultiValue + ) }, { "ContentLanguage", new HttpHeaderInfo ( "Content-Language", - HttpHeaderType.Request | HttpHeaderType.Response | HttpHeaderType.MultiValue) + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.MultiValue + ) }, { "ContentLength", new HttpHeaderInfo ( "Content-Length", - HttpHeaderType.Request | HttpHeaderType.Response | HttpHeaderType.Restricted) + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.Restricted + ) }, { "ContentLocation", new HttpHeaderInfo ( "Content-Location", - HttpHeaderType.Request | HttpHeaderType.Response) + HttpHeaderType.Request | HttpHeaderType.Response + ) }, { "ContentMd5", new HttpHeaderInfo ( "Content-MD5", - HttpHeaderType.Request | HttpHeaderType.Response) + HttpHeaderType.Request | HttpHeaderType.Response + ) }, { "ContentRange", new HttpHeaderInfo ( "Content-Range", - HttpHeaderType.Request | HttpHeaderType.Response) + HttpHeaderType.Request | HttpHeaderType.Response + ) }, { "ContentType", new HttpHeaderInfo ( "Content-Type", - HttpHeaderType.Request | HttpHeaderType.Response | HttpHeaderType.Restricted) + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.Restricted + ) }, { "Cookie", new HttpHeaderInfo ( "Cookie", - HttpHeaderType.Request) + HttpHeaderType.Request | HttpHeaderType.Restricted + ) }, { "Cookie2", new HttpHeaderInfo ( "Cookie2", - HttpHeaderType.Request) + HttpHeaderType.Request | HttpHeaderType.Restricted + ) }, { "Date", new HttpHeaderInfo ( "Date", - HttpHeaderType.Request | HttpHeaderType.Response | HttpHeaderType.Restricted) + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.Restricted + ) }, { "Expect", new HttpHeaderInfo ( "Expect", - HttpHeaderType.Request | HttpHeaderType.Restricted | HttpHeaderType.MultiValue) + HttpHeaderType.Request + | HttpHeaderType.Restricted + | HttpHeaderType.MultiValue + ) }, { "Expires", new HttpHeaderInfo ( "Expires", - HttpHeaderType.Request | HttpHeaderType.Response) + HttpHeaderType.Request | HttpHeaderType.Response + ) }, { "ETag", new HttpHeaderInfo ( "ETag", - HttpHeaderType.Response) + HttpHeaderType.Response + ) }, { "From", new HttpHeaderInfo ( "From", - HttpHeaderType.Request) + HttpHeaderType.Request + ) }, { "Host", new HttpHeaderInfo ( "Host", - HttpHeaderType.Request | HttpHeaderType.Restricted) + HttpHeaderType.Request | HttpHeaderType.Restricted + ) }, { "IfMatch", new HttpHeaderInfo ( "If-Match", - HttpHeaderType.Request | HttpHeaderType.MultiValue) + HttpHeaderType.Request | HttpHeaderType.MultiValue + ) }, { "IfModifiedSince", new HttpHeaderInfo ( "If-Modified-Since", - HttpHeaderType.Request | HttpHeaderType.Restricted) + HttpHeaderType.Request | HttpHeaderType.Restricted + ) }, { "IfNoneMatch", new HttpHeaderInfo ( "If-None-Match", - HttpHeaderType.Request | HttpHeaderType.MultiValue) + HttpHeaderType.Request | HttpHeaderType.MultiValue + ) }, { "IfRange", new HttpHeaderInfo ( "If-Range", - HttpHeaderType.Request) + HttpHeaderType.Request + ) }, { "IfUnmodifiedSince", new HttpHeaderInfo ( "If-Unmodified-Since", - HttpHeaderType.Request) + HttpHeaderType.Request + ) }, { "KeepAlive", new HttpHeaderInfo ( "Keep-Alive", - HttpHeaderType.Request | HttpHeaderType.Response | HttpHeaderType.MultiValue) + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.MultiValue + ) }, { "LastModified", new HttpHeaderInfo ( "Last-Modified", - HttpHeaderType.Request | HttpHeaderType.Response) + HttpHeaderType.Request | HttpHeaderType.Response + ) }, { "Location", new HttpHeaderInfo ( "Location", - HttpHeaderType.Response) + HttpHeaderType.Response | HttpHeaderType.Restricted + ) }, { "MaxForwards", new HttpHeaderInfo ( "Max-Forwards", - HttpHeaderType.Request) + HttpHeaderType.Request + ) }, { "Pragma", new HttpHeaderInfo ( "Pragma", - HttpHeaderType.Request | HttpHeaderType.Response) + HttpHeaderType.Request | HttpHeaderType.Response + ) }, { "ProxyAuthenticate", new HttpHeaderInfo ( "Proxy-Authenticate", - HttpHeaderType.Response | HttpHeaderType.MultiValue) + HttpHeaderType.Response + | HttpHeaderType.Restricted + | HttpHeaderType.MultiValue + ) }, { "ProxyAuthorization", new HttpHeaderInfo ( "Proxy-Authorization", - HttpHeaderType.Request) + HttpHeaderType.Request | HttpHeaderType.Restricted + ) }, { "ProxyConnection", new HttpHeaderInfo ( "Proxy-Connection", - HttpHeaderType.Request | HttpHeaderType.Response | HttpHeaderType.Restricted) + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.Restricted + ) }, { "Public", new HttpHeaderInfo ( "Public", - HttpHeaderType.Response | HttpHeaderType.MultiValue) + HttpHeaderType.Response | HttpHeaderType.MultiValue + ) }, { "Range", new HttpHeaderInfo ( "Range", - HttpHeaderType.Request | HttpHeaderType.Restricted | HttpHeaderType.MultiValue) + HttpHeaderType.Request + | HttpHeaderType.Restricted + | HttpHeaderType.MultiValue + ) }, { "Referer", new HttpHeaderInfo ( "Referer", - HttpHeaderType.Request | HttpHeaderType.Restricted) + HttpHeaderType.Request | HttpHeaderType.Restricted + ) }, { "RetryAfter", new HttpHeaderInfo ( "Retry-After", - HttpHeaderType.Response) + HttpHeaderType.Response + ) }, { "SecWebSocketAccept", new HttpHeaderInfo ( "Sec-WebSocket-Accept", - HttpHeaderType.Response | HttpHeaderType.Restricted) + HttpHeaderType.Response | HttpHeaderType.Restricted + ) }, { "SecWebSocketExtensions", new HttpHeaderInfo ( "Sec-WebSocket-Extensions", - HttpHeaderType.Request | - HttpHeaderType.Response | - HttpHeaderType.Restricted | - HttpHeaderType.MultiValueInRequest) + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.Restricted + | HttpHeaderType.MultiValueInRequest + ) }, { "SecWebSocketKey", new HttpHeaderInfo ( "Sec-WebSocket-Key", - HttpHeaderType.Request | HttpHeaderType.Restricted) + HttpHeaderType.Request | HttpHeaderType.Restricted + ) }, { "SecWebSocketProtocol", new HttpHeaderInfo ( "Sec-WebSocket-Protocol", - HttpHeaderType.Request | HttpHeaderType.Response | HttpHeaderType.MultiValueInRequest) + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.Restricted + | HttpHeaderType.MultiValueInRequest + ) }, { "SecWebSocketVersion", new HttpHeaderInfo ( "Sec-WebSocket-Version", - HttpHeaderType.Request | - HttpHeaderType.Response | - HttpHeaderType.Restricted | - HttpHeaderType.MultiValueInResponse) + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.Restricted + | HttpHeaderType.MultiValueInResponse + ) }, { "Server", new HttpHeaderInfo ( "Server", - HttpHeaderType.Response) + HttpHeaderType.Response + ) }, { "SetCookie", new HttpHeaderInfo ( "Set-Cookie", - HttpHeaderType.Response | HttpHeaderType.MultiValue) + HttpHeaderType.Response + | HttpHeaderType.Restricted + | HttpHeaderType.MultiValue + ) }, { "SetCookie2", new HttpHeaderInfo ( "Set-Cookie2", - HttpHeaderType.Response | HttpHeaderType.MultiValue) + HttpHeaderType.Response + | HttpHeaderType.Restricted + | HttpHeaderType.MultiValue + ) }, { "Te", new HttpHeaderInfo ( "TE", - HttpHeaderType.Request) + HttpHeaderType.Request + ) }, { "Trailer", new HttpHeaderInfo ( "Trailer", - HttpHeaderType.Request | HttpHeaderType.Response) + HttpHeaderType.Request | HttpHeaderType.Response + ) }, { "TransferEncoding", new HttpHeaderInfo ( "Transfer-Encoding", - HttpHeaderType.Request | - HttpHeaderType.Response | - HttpHeaderType.Restricted | - HttpHeaderType.MultiValue) + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.Restricted + | HttpHeaderType.MultiValue + ) }, { "Translate", new HttpHeaderInfo ( "Translate", - HttpHeaderType.Request) + HttpHeaderType.Request + ) }, { "Upgrade", new HttpHeaderInfo ( "Upgrade", - HttpHeaderType.Request | HttpHeaderType.Response | HttpHeaderType.MultiValue) + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.Restricted + | HttpHeaderType.MultiValue + ) }, { "UserAgent", new HttpHeaderInfo ( "User-Agent", - HttpHeaderType.Request | HttpHeaderType.Restricted) + HttpHeaderType.Request | HttpHeaderType.Restricted + ) }, { "Vary", new HttpHeaderInfo ( "Vary", - HttpHeaderType.Response | HttpHeaderType.MultiValue) + HttpHeaderType.Response | HttpHeaderType.MultiValue + ) }, { "Via", new HttpHeaderInfo ( "Via", - HttpHeaderType.Request | HttpHeaderType.Response | HttpHeaderType.MultiValue) + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.MultiValue + ) }, { "Warning", new HttpHeaderInfo ( "Warning", - HttpHeaderType.Request | HttpHeaderType.Response | HttpHeaderType.MultiValue) + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.MultiValue + ) }, { "WwwAuthenticate", new HttpHeaderInfo ( "WWW-Authenticate", - HttpHeaderType.Response | HttpHeaderType.Restricted | HttpHeaderType.MultiValue) + HttpHeaderType.Response + | HttpHeaderType.Restricted + | HttpHeaderType.MultiValue + ) } }; } @@ -462,23 +570,28 @@ internal WebHeaderCollection (HttpHeaderType state, bool internallyUsed) #region Protected Constructors /// - /// Initializes a new instance of the class from - /// the specified and . + /// Initializes a new instance of the + /// class with the specified serialized data. /// /// - /// A that contains the serialized object data. + /// A that contains the serialized + /// object data. /// /// - /// A that specifies the source for the deserialization. + /// A that specifies the source for + /// the deserialization. /// + /// + /// An element with the specified name is not found in + /// . + /// /// /// is . /// - /// - /// An element with the specified name isn't found in . - /// protected WebHeaderCollection ( - SerializationInfo serializationInfo, StreamingContext streamingContext) + SerializationInfo serializationInfo, + StreamingContext streamingContext + ) { if (serializationInfo == null) throw new ArgumentNullException ("serializationInfo"); @@ -488,10 +601,12 @@ protected WebHeaderCollection ( _state = (HttpHeaderType) serializationInfo.GetInt32 ("State"); var cnt = serializationInfo.GetInt32 ("Count"); + for (var i = 0; i < cnt; i++) { base.Add ( serializationInfo.GetString (i.ToString ()), - serializationInfo.GetString ((cnt + i).ToString ())); + serializationInfo.GetString ((cnt + i).ToString ()) + ); } } catch (SerializationException ex) { @@ -504,7 +619,8 @@ protected WebHeaderCollection ( #region Public Constructors /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the + /// class. /// public WebHeaderCollection () { @@ -528,7 +644,8 @@ internal HttpHeaderType State { /// Gets all header names in the collection. /// /// - /// An array of that contains all header names in the collection. + /// An array of that contains all header names in + /// the collection. /// public override string[] AllKeys { get { @@ -540,7 +657,8 @@ public override string[] AllKeys { /// Gets the number of headers in the collection. /// /// - /// An that represents the number of headers in the collection. + /// An that represents the number of headers in + /// the collection. /// public override int Count { get { @@ -549,14 +667,18 @@ public override int Count { } /// - /// Gets or sets the specified request in the collection. + /// Gets or sets the specified request header. /// /// - /// A that represents the value of the request . + /// A that represents the value of the request header. /// /// - /// One of the enum values, represents - /// the request header to get or set. + /// + /// One of the enum values. + /// + /// + /// It specifies the request header to get or set. + /// /// /// /// @@ -566,19 +688,22 @@ public override int Count { /// -or- /// /// - /// contains invalid characters. + /// contains an invalid character. /// /// /// - /// The length of is greater than 65,535 characters. + /// The length of is greater than 65,535 + /// characters. /// /// - /// The current instance doesn't allow - /// the request . + /// This instance does not allow the request header. /// public string this[HttpRequestHeader header] { get { - return Get (Convert (header)); + var key = header.ToString (); + var name = getHeaderName (key); + + return Get (name); } set { @@ -587,14 +712,18 @@ public string this[HttpRequestHeader header] { } /// - /// Gets or sets the specified response in the collection. + /// Gets or sets the specified response header. /// /// - /// A that represents the value of the response . + /// A that represents the value of the response header. /// /// - /// One of the enum values, represents - /// the response header to get or set. + /// + /// One of the enum values. + /// + /// + /// It specifies the response header to get or set. + /// /// /// /// @@ -604,19 +733,22 @@ public string this[HttpRequestHeader header] { /// -or- /// /// - /// contains invalid characters. + /// contains an invalid character. /// /// /// - /// The length of is greater than 65,535 characters. + /// The length of is greater than 65,535 + /// characters. /// /// - /// The current instance doesn't allow - /// the response . + /// This instance does not allow the response header. /// public string this[HttpResponseHeader header] { get { - return Get (Convert (header)); + var key = header.ToString (); + var name = getHeaderName (key); + + return Get (name); } set { @@ -641,165 +773,175 @@ public override NameObjectCollectionBase.KeysCollection Keys { #region Private Methods - private void add (string name, string value, bool ignoreRestricted) + private void add (string name, string value, HttpHeaderType headerType) { - var act = ignoreRestricted - ? (Action ) addWithoutCheckingNameAndRestricted - : addWithoutCheckingName; + base.Add (name, value); - doWithCheckingState (act, checkName (name), value, true); - } + if (_state != HttpHeaderType.Unspecified) + return; - private void addWithoutCheckingName (string name, string value) - { - doWithoutCheckingName (base.Add, name, value); - } + if (headerType == HttpHeaderType.Unspecified) + return; - private void addWithoutCheckingNameAndRestricted (string name, string value) - { - base.Add (name, checkValue (value)); + _state = headerType; } - private static int checkColonSeparated (string header) + private void checkAllowed (HttpHeaderType headerType) { - var idx = header.IndexOf (':'); - if (idx == -1) - throw new ArgumentException ("No colon could be found.", "header"); + if (_state == HttpHeaderType.Unspecified) + return; - return idx; - } + if (headerType == HttpHeaderType.Unspecified) + return; - private static HttpHeaderType checkHeaderType (string name) - { - var info = getHeaderInfo (name); - return info == null - ? HttpHeaderType.Unspecified - : info.IsRequest && !info.IsResponse - ? HttpHeaderType.Request - : !info.IsRequest && info.IsResponse - ? HttpHeaderType.Response - : HttpHeaderType.Unspecified; + if (headerType != _state) { + var msg = "This instance does not allow the header."; + + throw new InvalidOperationException (msg); + } } - private static string checkName (string name) + private static string checkName (string name, string paramName) { - if (name == null || name.Length == 0) - throw new ArgumentNullException ("name"); + if (name == null) { + var msg = "The name is null."; + + throw new ArgumentNullException (paramName, msg); + } + + if (name.Length == 0) { + var msg = "The name is an empty string."; + + throw new ArgumentException (msg, paramName); + } name = name.Trim (); - if (!IsHeaderName (name)) - throw new ArgumentException ("Contains invalid characters.", "name"); - return name; - } + if (name.Length == 0) { + var msg = "The name is a string of spaces."; - private void checkRestricted (string name) - { - if (!_internallyUsed && isRestricted (name, true)) - throw new ArgumentException ("This header must be modified with the appropiate property."); + throw new ArgumentException (msg, paramName); + } + + if (!name.IsToken ()) { + var msg = "The name contains an invalid character."; + + throw new ArgumentException (msg, paramName); + } + + return name; } - private void checkState (bool response) + private void checkRestricted (string name, HttpHeaderType headerType) { - if (_state == HttpHeaderType.Unspecified) + if (_internallyUsed) return; - if (response && _state == HttpHeaderType.Request) - throw new InvalidOperationException ( - "This collection has already been used to store the request headers."); + var res = headerType == HttpHeaderType.Response; + + if (isRestricted (name, res)) { + var msg = "The header is a restricted header."; - if (!response && _state == HttpHeaderType.Response) - throw new InvalidOperationException ( - "This collection has already been used to store the response headers."); + throw new ArgumentException (msg); + } } - private static string checkValue (string value) + private static string checkValue (string value, string paramName) { - if (value == null || value.Length == 0) + if (value == null) return String.Empty; value = value.Trim (); - if (value.Length > 65535) - throw new ArgumentOutOfRangeException ("value", "Greater than 65,535 characters."); - if (!IsHeaderValue (value)) - throw new ArgumentException ("Contains invalid characters.", "value"); + var len = value.Length; + + if (len == 0) + return value; + + if (len > 65535) { + var msg = "The length of the value is greater than 65,535 characters."; + + throw new ArgumentOutOfRangeException (paramName, msg); + } + + if (!value.IsText ()) { + var msg = "The value contains an invalid character."; + + throw new ArgumentException (msg, paramName); + } return value; } - private static string convert (string key) + private static HttpHeaderInfo getHeaderInfo (string name) { - HttpHeaderInfo info; - return _headers.TryGetValue (key, out info) ? info.Name : String.Empty; - } + var compType = StringComparison.InvariantCultureIgnoreCase; - private void doWithCheckingState ( - Action action, string name, string value, bool setState) - { - var type = checkHeaderType (name); - if (type == HttpHeaderType.Request) - doWithCheckingState (action, name, value, false, setState); - else if (type == HttpHeaderType.Response) - doWithCheckingState (action, name, value, true, setState); - else - action (name, value); + foreach (var headerInfo in _headers.Values) { + if (headerInfo.HeaderName.Equals (name, compType)) + return headerInfo; + } + + return null; } - private void doWithCheckingState ( - Action action, string name, string value, bool response, bool setState) + private static string getHeaderName (string key) { - checkState (response); - action (name, value); - if (setState && _state == HttpHeaderType.Unspecified) - _state = response ? HttpHeaderType.Response : HttpHeaderType.Request; + HttpHeaderInfo headerInfo; + + return _headers.TryGetValue (key, out headerInfo) + ? headerInfo.HeaderName + : null; } - private void doWithoutCheckingName (Action action, string name, string value) + private static HttpHeaderType getHeaderType (string name) { - checkRestricted (name); - action (name, checkValue (value)); + var headerInfo = getHeaderInfo (name); + + if (headerInfo == null) + return HttpHeaderType.Unspecified; + + if (headerInfo.IsRequest) { + return !headerInfo.IsResponse + ? HttpHeaderType.Request + : HttpHeaderType.Unspecified; + } + + return headerInfo.IsResponse + ? HttpHeaderType.Response + : HttpHeaderType.Unspecified; } - private static HttpHeaderInfo getHeaderInfo (string name) + private static bool isMultiValue (string name, bool response) { - foreach (var info in _headers.Values) - if (info.Name.Equals (name, StringComparison.InvariantCultureIgnoreCase)) - return info; + var headerInfo = getHeaderInfo (name); - return null; + return headerInfo != null && headerInfo.IsMultiValue (response); } private static bool isRestricted (string name, bool response) { - var info = getHeaderInfo (name); - return info != null && info.IsRestricted (response); - } + var headerInfo = getHeaderInfo (name); - private void removeWithoutCheckingName (string name, string unuse) - { - checkRestricted (name); - base.Remove (name); + return headerInfo != null && headerInfo.IsRestricted (response); } - private void setWithoutCheckingName (string name, string value) + private void set (string name, string value, HttpHeaderType headerType) { - doWithoutCheckingName (base.Set, name, value); - } + base.Set (name, value); - #endregion + if (_state != HttpHeaderType.Unspecified) + return; - #region Internal Methods + if (headerType == HttpHeaderType.Unspecified) + return; - internal static string Convert (HttpRequestHeader header) - { - return convert (header.ToString ()); + _state = headerType; } - internal static string Convert (HttpResponseHeader header) - { - return convert (header.ToString ()); - } + #endregion + + #region Internal Methods internal void InternalRemove (string name) { @@ -808,52 +950,71 @@ internal void InternalRemove (string name) internal void InternalSet (string header, bool response) { - var pos = checkColonSeparated (header); - InternalSet (header.Substring (0, pos), header.Substring (pos + 1), response); + var idx = header.IndexOf (':'); + + if (idx == -1) { + var msg = "It does not contain a colon character."; + + throw new ArgumentException (msg, "header"); + } + + var name = header.Substring (0, idx); + var val = idx < header.Length - 1 + ? header.Substring (idx + 1) + : String.Empty; + + name = checkName (name, "header"); + val = checkValue (val, "header"); + + if (isMultiValue (name, response)) { + base.Add (name, val); + + return; + } + + base.Set (name, val); } internal void InternalSet (string name, string value, bool response) { - value = checkValue (value); - if (IsMultiValue (name, response)) + value = checkValue (value, "value"); + + if (isMultiValue (name, response)) { base.Add (name, value); - else - base.Set (name, value); - } - internal static bool IsHeaderName (string name) - { - return name != null && name.Length > 0 && name.IsToken (); - } + return; + } - internal static bool IsHeaderValue (string value) - { - return value.IsText (); + base.Set (name, value); } - internal static bool IsMultiValue (string headerName, bool response) + internal string ToStringMultiValue (bool response) { - if (headerName == null || headerName.Length == 0) - return false; + var cnt = Count; - var info = getHeaderInfo (headerName); - return info != null && info.IsMultiValue (response); - } + if (cnt == 0) + return "\r\n"; - internal string ToStringMultiValue (bool response) - { var buff = new StringBuilder (); - Count.Times ( - i => { - var key = GetKey (i); - if (IsMultiValue (key, response)) - foreach (var val in GetValues (i)) - buff.AppendFormat ("{0}: {1}\r\n", key, val); - else - buff.AppendFormat ("{0}: {1}\r\n", key, Get (i)); - }); - - return buff.Append ("\r\n").ToString (); + + var fmt = "{0}: {1}\r\n"; + + for (var i = 0; i < cnt; i++) { + var name = GetKey (i); + + if (isMultiValue (name, response)) { + foreach (var val in GetValues (i)) + buff.AppendFormat (fmt, name, val); + + continue; + } + + buff.AppendFormat (fmt, name, Get (i)); + } + + buff.Append ("\r\n"); + + return buff.ToString (); } #endregion @@ -865,27 +1026,54 @@ internal string ToStringMultiValue (bool response) /// the restricted header list. /// /// - /// A that represents the name of the header to add. + /// A that specifies the name of the header to add. /// /// - /// A that represents the value of the header to add. + /// A that specifies the value of the header to add. /// - /// - /// is or empty. - /// /// - /// or contains invalid characters. + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is a string of spaces. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// + /// is . /// /// - /// The length of is greater than 65,535 characters. + /// The length of is greater than 65,535 + /// characters. /// /// - /// The current instance doesn't allow - /// the . + /// This instance does not allow the header. /// protected void AddWithoutValidate (string headerName, string headerValue) { - add (headerName, headerValue, true); + headerName = checkName (headerName, "headerName"); + headerValue = checkValue (headerValue, "headerValue"); + + var headerType = getHeaderType (headerName); + + checkAllowed (headerType); + + add (headerName, headerValue, headerType); } #endregion @@ -893,133 +1081,222 @@ protected void AddWithoutValidate (string headerName, string headerValue) #region Public Methods /// - /// Adds the specified to the collection. + /// Adds the specified header to the collection. /// /// - /// A that represents the header with the name and value separated by - /// a colon (':'). + /// A that specifies the header to add, + /// with the name and value separated by a colon character (':'). /// - /// - /// is , empty, or the name part of - /// is empty. - /// /// /// - /// doesn't contain a colon. + /// is an empty string. /// /// /// -or- /// /// - /// is a restricted header. + /// does not contain a colon character. + /// + /// + /// -or- + /// + /// + /// The name part of is an empty string. + /// + /// + /// -or- + /// + /// + /// The name part of is a string of spaces. + /// + /// + /// -or- + /// + /// + /// The name part of contains an invalid + /// character. /// /// /// -or- /// /// - /// The name or value part of contains invalid characters. + /// The value part of contains an invalid + /// character. + /// + /// + /// -or- /// + /// + /// is a restricted header. + /// + /// + /// + /// is . /// /// - /// The length of the value part of is greater than 65,535 characters. + /// The length of the value part of is greater + /// than 65,535 characters. /// /// - /// The current instance doesn't allow - /// the . + /// This instance does not allow the header. /// public void Add (string header) { - if (header == null || header.Length == 0) + if (header == null) throw new ArgumentNullException ("header"); - var pos = checkColonSeparated (header); - add (header.Substring (0, pos), header.Substring (pos + 1), false); + var len = header.Length; + + if (len == 0) { + var msg = "An empty string."; + + throw new ArgumentException (msg, "header"); + } + + var idx = header.IndexOf (':'); + + if (idx == -1) { + var msg = "It does not contain a colon character."; + + throw new ArgumentException (msg, "header"); + } + + var name = header.Substring (0, idx); + var val = idx < len - 1 ? header.Substring (idx + 1) : String.Empty; + + name = checkName (name, "header"); + val = checkValue (val, "header"); + + var headerType = getHeaderType (name); + + checkRestricted (name, headerType); + checkAllowed (headerType); + + add (name, val, headerType); } /// - /// Adds the specified request with - /// the specified to the collection. + /// Adds the specified request header with the specified value to + /// the collection. /// /// - /// One of the enum values, represents - /// the request header to add. + /// + /// One of the enum values. + /// + /// + /// It specifies the request header to add. + /// /// /// - /// A that represents the value of the header to add. + /// A that specifies the value of the header to add. /// /// /// - /// is a restricted header. + /// contains an invalid character. /// /// /// -or- /// /// - /// contains invalid characters. + /// is a restricted header. /// /// /// - /// The length of is greater than 65,535 characters. + /// The length of is greater than 65,535 + /// characters. /// /// - /// The current instance doesn't allow - /// the request . + /// This instance does not allow the request header. /// public void Add (HttpRequestHeader header, string value) { - doWithCheckingState (addWithoutCheckingName, Convert (header), value, false, true); + value = checkValue (value, "value"); + + var key = header.ToString (); + var name = getHeaderName (key); + + checkRestricted (name, HttpHeaderType.Request); + checkAllowed (HttpHeaderType.Request); + + add (name, value, HttpHeaderType.Request); } /// - /// Adds the specified response with - /// the specified to the collection. + /// Adds the specified response header with the specified value to + /// the collection. /// /// - /// One of the enum values, represents - /// the response header to add. + /// + /// One of the enum values. + /// + /// + /// It specifies the response header to add. + /// /// /// - /// A that represents the value of the header to add. + /// A that specifies the value of the header to add. /// /// /// - /// is a restricted header. + /// contains an invalid character. /// /// /// -or- /// /// - /// contains invalid characters. + /// is a restricted header. /// /// /// - /// The length of is greater than 65,535 characters. + /// The length of is greater than 65,535 + /// characters. /// /// - /// The current instance doesn't allow - /// the response . + /// This instance does not allow the response header. /// public void Add (HttpResponseHeader header, string value) { - doWithCheckingState (addWithoutCheckingName, Convert (header), value, true, true); + value = checkValue (value, "value"); + + var key = header.ToString (); + var name = getHeaderName (key); + + checkRestricted (name, HttpHeaderType.Response); + checkAllowed (HttpHeaderType.Response); + + add (name, value, HttpHeaderType.Response); } /// - /// Adds a header with the specified and - /// to the collection. + /// Adds a header with the specified name and value to the collection. /// /// - /// A that represents the name of the header to add. + /// A that specifies the name of the header to add. /// /// - /// A that represents the value of the header to add. + /// A that specifies the value of the header to add. /// - /// - /// is or empty. - /// /// /// - /// or contains invalid characters. + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is a string of spaces. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. /// /// /// -or- @@ -1028,16 +1305,27 @@ public void Add (HttpResponseHeader header, string value) /// is a restricted header name. /// /// + /// + /// is . + /// /// - /// The length of is greater than 65,535 characters. + /// The length of is greater than 65,535 + /// characters. /// /// - /// The current instance doesn't allow - /// the header . + /// This instance does not allow the header. /// public override void Add (string name, string value) { - add (name, value, false); + name = checkName (name, "name"); + value = checkValue (value, "value"); + + var headerType = getHeaderType (name); + + checkRestricted (name, headerType); + checkAllowed (headerType); + + add (name, value, headerType); } /// @@ -1046,20 +1334,23 @@ public override void Add (string name, string value) public override void Clear () { base.Clear (); + _state = HttpHeaderType.Unspecified; } /// - /// Get the value of the header at the specified in the collection. + /// Get the value of the header at the specified index in the collection. /// /// /// A that receives the value of the header. /// /// - /// An that represents the zero-based index of the header to find. + /// An that specifies the zero-based index of the header + /// to get. /// /// - /// is out of allowable range of indexes for the collection. + /// is out of allowable range of indexes for + /// the collection. /// public override string Get (int index) { @@ -1067,14 +1358,18 @@ public override string Get (int index) } /// - /// Get the value of the header with the specified in the collection. + /// Get the value of the header with the specified name in the collection. /// /// - /// A that receives the value of the header if found; - /// otherwise, . + /// + /// A that receives the value of the header. + /// + /// + /// if not found. + /// /// /// - /// A that represents the name of the header to find. + /// A that specifies the name of the header to get. /// public override string Get (string name) { @@ -1085,7 +1380,8 @@ public override string Get (string name) /// Gets the enumerator used to iterate through the collection. /// /// - /// An instance used to iterate through the collection. + /// An instance used to iterate through + /// the collection. /// public override IEnumerator GetEnumerator () { @@ -1093,16 +1389,18 @@ public override IEnumerator GetEnumerator () } /// - /// Get the name of the header at the specified in the collection. + /// Get the name of the header at the specified index in the collection. /// /// - /// A that receives the header name. + /// A that receives the name of the header. /// /// - /// An that represents the zero-based index of the header to find. + /// An that specifies the zero-based index of the header + /// to get. /// /// - /// is out of allowable range of indexes for the collection. + /// is out of allowable range of indexes for + /// the collection. /// public override string GetKey (int index) { @@ -1110,58 +1408,78 @@ public override string GetKey (int index) } /// - /// Gets an array of header values stored in the specified position of - /// the collection. + /// Get the values of the header at the specified index in the collection. /// /// - /// An array of that receives the header values if found; - /// otherwise, . + /// + /// An array of that receives the values of + /// the header. + /// + /// + /// if not present. + /// /// /// - /// An that represents the zero-based index of the header to find. + /// An that specifies the zero-based index of the header + /// to get. /// /// - /// is out of allowable range of indexes for the collection. + /// is out of allowable range of indexes for + /// the collection. /// public override string[] GetValues (int index) { var vals = base.GetValues (index); + return vals != null && vals.Length > 0 ? vals : null; } /// - /// Gets an array of header values stored in the specified . + /// Get the values of the header with the specified name in the collection. /// /// - /// An array of that receives the header values if found; - /// otherwise, . + /// + /// An array of that receives the values of + /// the header. + /// + /// + /// if not present. + /// /// - /// - /// A that represents the name of the header to find. + /// + /// A that specifies the name of the header to get. /// - public override string[] GetValues (string header) + public override string[] GetValues (string name) { - var vals = base.GetValues (header); + var vals = base.GetValues (name); + return vals != null && vals.Length > 0 ? vals : null; } /// - /// Populates the specified with the data needed to serialize - /// the . + /// Populates the specified instance with + /// the data needed to serialize the current instance. /// /// /// A that holds the serialized object data. /// /// - /// A that specifies the destination for the serialization. + /// A that specifies the destination for + /// the serialization. /// /// /// is . /// - [SecurityPermission ( - SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)] + [ + SecurityPermission ( + SecurityAction.LinkDemand, + Flags = SecurityPermissionFlag.SerializationFormatter + ) + ] public override void GetObjectData ( - SerializationInfo serializationInfo, StreamingContext streamingContext) + SerializationInfo serializationInfo, + StreamingContext streamingContext + ) { if (serializationInfo == null) throw new ArgumentNullException ("serializationInfo"); @@ -1170,118 +1488,179 @@ public override void GetObjectData ( serializationInfo.AddValue ("State", (int) _state); var cnt = Count; + serializationInfo.AddValue ("Count", cnt); - cnt.Times ( - i => { - serializationInfo.AddValue (i.ToString (), GetKey (i)); - serializationInfo.AddValue ((cnt + i).ToString (), Get (i)); - }); + + for (var i = 0; i < cnt; i++) { + serializationInfo.AddValue (i.ToString (), GetKey (i)); + serializationInfo.AddValue ((cnt + i).ToString (), Get (i)); + } } /// /// Determines whether the specified header can be set for the request. /// /// - /// true if the header is restricted; otherwise, false. + /// true if the header cannot be set; otherwise, false. /// /// - /// A that represents the name of the header to test. + /// A that specifies the name of the header to test. /// - /// - /// is or empty. - /// /// - /// contains invalid characters. + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is a string of spaces. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// + /// is . /// public static bool IsRestricted (string headerName) { - return isRestricted (checkName (headerName), false); + return IsRestricted (headerName, false); } /// - /// Determines whether the specified header can be set for the request or the response. + /// Determines whether the specified header can be set for the request or + /// the response. /// /// - /// true if the header is restricted; otherwise, false. + /// true if the header cannot be set; otherwise, false. /// /// - /// A that represents the name of the header to test. + /// A that specifies the name of the header to test. /// /// - /// true if does the test for the response; for the request, false. + /// A : true if the test is for the response; + /// otherwise, false. /// - /// - /// is or empty. - /// /// - /// contains invalid characters. + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is a string of spaces. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// + /// is . /// public static bool IsRestricted (string headerName, bool response) { - return isRestricted (checkName (headerName), response); + headerName = checkName (headerName, "headerName"); + + return isRestricted (headerName, response); } /// - /// Implements the interface and raises the deserialization event - /// when the deserialization is complete. + /// Implements the interface and raises + /// the deserialization event when the deserialization is complete. /// /// - /// An that represents the source of the deserialization event. + /// An instance that represents the source of + /// the deserialization event. /// public override void OnDeserialization (object sender) { } /// - /// Removes the specified request from the collection. + /// Removes the specified request header from the collection. /// /// - /// One of the enum values, represents - /// the request header to remove. + /// + /// One of the enum values. + /// + /// + /// It specifies the request header to remove. + /// /// /// /// is a restricted header. /// /// - /// The current instance doesn't allow - /// the request . + /// This instance does not allow the request header. /// public void Remove (HttpRequestHeader header) { - doWithCheckingState (removeWithoutCheckingName, Convert (header), null, false, false); + var key = header.ToString (); + var name = getHeaderName (key); + + checkRestricted (name, HttpHeaderType.Request); + checkAllowed (HttpHeaderType.Request); + + base.Remove (name); } /// - /// Removes the specified response from the collection. + /// Removes the specified response header from the collection. /// /// - /// One of the enum values, represents - /// the response header to remove. + /// + /// One of the enum values. + /// + /// + /// It specifies the response header to remove. + /// /// /// /// is a restricted header. /// /// - /// The current instance doesn't allow - /// the response . + /// This instance does not allow the response header. /// public void Remove (HttpResponseHeader header) { - doWithCheckingState (removeWithoutCheckingName, Convert (header), null, true, false); + var key = header.ToString (); + var name = getHeaderName (key); + + checkRestricted (name, HttpHeaderType.Response); + checkAllowed (HttpHeaderType.Response); + + base.Remove (name); } /// /// Removes the specified header from the collection. /// /// - /// A that represents the name of the header to remove. + /// A that specifies the name of the header to remove. /// - /// - /// is or empty. - /// /// /// - /// contains invalid characters. + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is a string of spaces. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. /// /// /// -or- @@ -1290,96 +1669,146 @@ public void Remove (HttpResponseHeader header) /// is a restricted header name. /// /// + /// + /// is . + /// /// - /// The current instance doesn't allow - /// the header . + /// This instance does not allow the header. /// public override void Remove (string name) { - doWithCheckingState (removeWithoutCheckingName, checkName (name), null, false); + name = checkName (name, "name"); + + var headerType = getHeaderType (name); + + checkRestricted (name, headerType); + checkAllowed (headerType); + + base.Remove (name); } /// - /// Sets the specified request to the specified value. + /// Sets the specified request header to the specified value. /// /// - /// One of the enum values, represents - /// the request header to set. + /// + /// One of the enum values. + /// + /// + /// It specifies the request header to set. + /// /// /// - /// A that represents the value of the request header to set. + /// A that specifies the value of the request header + /// to set. /// /// /// - /// is a restricted header. + /// contains an invalid character. /// /// /// -or- /// /// - /// contains invalid characters. + /// is a restricted header. /// /// /// - /// The length of is greater than 65,535 characters. + /// The length of is greater than 65,535 + /// characters. /// /// - /// The current instance doesn't allow - /// the request . + /// This instance does not allow the request header. /// public void Set (HttpRequestHeader header, string value) { - doWithCheckingState (setWithoutCheckingName, Convert (header), value, false, true); + value = checkValue (value, "value"); + + var key = header.ToString (); + var name = getHeaderName (key); + + checkRestricted (name, HttpHeaderType.Request); + checkAllowed (HttpHeaderType.Request); + + set (name, value, HttpHeaderType.Request); } /// - /// Sets the specified response to the specified value. + /// Sets the specified response header to the specified value. /// /// - /// One of the enum values, represents - /// the response header to set. + /// + /// One of the enum values. + /// + /// + /// It specifies the response header to set. + /// /// /// - /// A that represents the value of the response header to set. + /// A that specifies the value of the response header + /// to set. /// /// /// - /// is a restricted header. + /// contains an invalid character. /// /// /// -or- /// /// - /// contains invalid characters. + /// is a restricted header. /// /// /// - /// The length of is greater than 65,535 characters. + /// The length of is greater than 65,535 + /// characters. /// /// - /// The current instance doesn't allow - /// the response . + /// This instance does not allow the response header. /// public void Set (HttpResponseHeader header, string value) { - doWithCheckingState (setWithoutCheckingName, Convert (header), value, true, true); + value = checkValue (value, "value"); + + var key = header.ToString (); + var name = getHeaderName (key); + + checkRestricted (name, HttpHeaderType.Response); + checkAllowed (HttpHeaderType.Response); + + set (name, value, HttpHeaderType.Response); } /// /// Sets the specified header to the specified value. /// /// - /// A that represents the name of the header to set. + /// A that specifies the name of the header to set. /// /// - /// A that represents the value of the header to set. + /// A that specifies the value of the header to set. /// - /// - /// is or empty. - /// /// /// - /// or contains invalid characters. + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is a string of spaces. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. /// /// /// -or- @@ -1388,43 +1817,66 @@ public void Set (HttpResponseHeader header, string value) /// is a restricted header name. /// /// + /// + /// is . + /// /// - /// The length of is greater than 65,535 characters. + /// The length of is greater than 65,535 + /// characters. /// /// - /// The current instance doesn't allow - /// the header . + /// This instance does not allow the header. /// public override void Set (string name, string value) { - doWithCheckingState (setWithoutCheckingName, checkName (name), value, true); + name = checkName (name, "name"); + value = checkValue (value, "value"); + + var headerType = getHeaderType (name); + + checkRestricted (name, headerType); + checkAllowed (headerType); + + set (name, value, headerType); } /// - /// Converts the current to an array of . + /// Converts the current instance to an array of byte. /// /// - /// An array of that receives the converted current - /// . + /// An array of converted from a string that represents + /// the current instance. /// public byte[] ToByteArray () { - return Encoding.UTF8.GetBytes (ToString ()); + var s = ToString (); + + return Encoding.UTF8.GetBytes (s); } /// - /// Returns a that represents the current - /// . + /// Returns a string that represents the current instance. /// /// - /// A that represents the current . + /// A that represents all headers in the collection. /// public override string ToString () { + var cnt = Count; + + if (cnt == 0) + return "\r\n"; + var buff = new StringBuilder (); - Count.Times (i => buff.AppendFormat ("{0}: {1}\r\n", GetKey (i), Get (i))); - return buff.Append ("\r\n").ToString (); + var fmt = "{0}: {1}\r\n"; + + for (var i = 0; i < cnt; i++) + buff.AppendFormat (fmt, GetKey (i), Get (i)); + + buff.Append ("\r\n"); + + return buff.ToString (); } #endregion @@ -1432,24 +1884,30 @@ public override string ToString () #region Explicit Interface Implementations /// - /// Populates the specified with the data needed to serialize - /// the current . + /// Populates the specified instance with + /// the data needed to serialize the current instance. /// /// /// A that holds the serialized object data. /// /// - /// A that specifies the destination for the serialization. + /// A that specifies the destination for + /// the serialization. /// /// /// is . /// - [SecurityPermission ( - SecurityAction.LinkDemand, - Flags = SecurityPermissionFlag.SerializationFormatter, - SerializationFormatter = true)] + [ + SecurityPermission ( + SecurityAction.LinkDemand, + Flags = SecurityPermissionFlag.SerializationFormatter, + SerializationFormatter = true + ) + ] void ISerializable.GetObjectData ( - SerializationInfo serializationInfo, StreamingContext streamingContext) + SerializationInfo serializationInfo, + StreamingContext streamingContext + ) { GetObjectData (serializationInfo, streamingContext); } diff --git a/websocket-sharp/Net/WebSockets/HttpListenerWebSocketContext.cs b/websocket-sharp/Net/WebSockets/HttpListenerWebSocketContext.cs index f89e8962a..7e0358d1f 100644 --- a/websocket-sharp/Net/WebSockets/HttpListenerWebSocketContext.cs +++ b/websocket-sharp/Net/WebSockets/HttpListenerWebSocketContext.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2025 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 @@ -30,13 +30,14 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.IO; +using System.Net.Sockets; using System.Security.Principal; namespace WebSocketSharp.Net.WebSockets { /// - /// Provides the properties used to access the information in a WebSocket connection request - /// received by the . + /// Provides the access to the information in a WebSocket handshake request + /// to a instance. /// public class HttpListenerWebSocketContext : WebSocketContext { @@ -49,7 +50,10 @@ public class HttpListenerWebSocketContext : WebSocketContext #region Internal Constructors - internal HttpListenerWebSocketContext (HttpListenerContext context, string protocol) + internal HttpListenerWebSocketContext ( + HttpListenerContext context, + string protocol + ) { _context = context; _websocket = new WebSocket (this, protocol); @@ -65,6 +69,12 @@ internal Logger Log { } } + internal Socket Socket { + get { + return _context.Connection.Socket; + } + } + internal Stream Stream { get { return _context.Connection.Stream; @@ -76,10 +86,16 @@ internal Stream Stream { #region Public Properties /// - /// Gets the HTTP cookies included in the request. + /// Gets the HTTP cookies included in the handshake request. /// /// - /// A that contains the cookies. + /// + /// A that contains + /// the cookies. + /// + /// + /// An empty collection if not included. + /// /// public override CookieCollection CookieCollection { get { @@ -88,7 +104,7 @@ public override CookieCollection CookieCollection { } /// - /// Gets the HTTP headers included in the request. + /// Gets the HTTP headers included in the handshake request. /// /// /// A that contains the headers. @@ -100,14 +116,20 @@ public override NameValueCollection Headers { } /// - /// Gets the value of the Host header included in the request. + /// Gets the value of the Host header included in the handshake request. /// /// - /// A that represents the value of the Host header. + /// + /// A that represents the server host name requested + /// by the client. + /// + /// + /// It includes the port number if provided. + /// /// public override string Host { get { - return _context.Request.Headers["Host"]; + return _context.Request.UserHostName; } } @@ -119,15 +141,17 @@ public override string Host { /// public override bool IsAuthenticated { get { - return _context.User != null; + return _context.Request.IsAuthenticated; } } /// - /// Gets a value indicating whether the client connected from the local computer. + /// Gets a value indicating whether the handshake request is sent from + /// the local computer. /// /// - /// true if the client connected from the local computer; otherwise, false. + /// true if the handshake request is sent from the same computer + /// as the server; otherwise, false. /// public override bool IsLocal { get { @@ -136,22 +160,25 @@ public override bool IsLocal { } /// - /// Gets a value indicating whether the WebSocket connection is secured. + /// Gets a value indicating whether a secure connection is used to send + /// the handshake request. /// /// - /// true if the connection is secured; otherwise, false. + /// true if the connection is secure; otherwise, false. /// public override bool IsSecureConnection { get { - return _context.Connection.IsSecure; + return _context.Request.IsSecureConnection; } } /// - /// Gets a value indicating whether the request is a WebSocket connection request. + /// Gets a value indicating whether the request is a WebSocket handshake + /// request. /// /// - /// true if the request is a WebSocket connection request; otherwise, false. + /// true if the request is a WebSocket handshake request; otherwise, + /// false. /// public override bool IsWebSocketRequest { get { @@ -160,10 +187,15 @@ public override bool IsWebSocketRequest { } /// - /// Gets the value of the Origin header included in the request. + /// Gets the value of the Origin header included in the handshake request. /// /// - /// A that represents the value of the Origin header. + /// + /// A that represents the value of the Origin header. + /// + /// + /// if not included. + /// /// public override string Origin { get { @@ -172,10 +204,16 @@ public override string Origin { } /// - /// Gets the query string included in the request. + /// Gets the query string included in the handshake request. /// /// - /// A that contains the query string parameters. + /// + /// A that contains the query + /// parameters. + /// + /// + /// An empty collection if not included. + /// /// public override NameValueCollection QueryString { get { @@ -187,7 +225,12 @@ public override NameValueCollection QueryString { /// Gets the URI requested by the client. /// /// - /// A that represents the requested URI. + /// + /// A that represents the URI parsed from the request. + /// + /// + /// if the URI cannot be parsed. + /// /// public override Uri RequestUri { get { @@ -196,14 +239,21 @@ public override Uri RequestUri { } /// - /// Gets the value of the Sec-WebSocket-Key header included in the request. + /// Gets the value of the Sec-WebSocket-Key header included in + /// the handshake request. /// - /// - /// This property provides a part of the information used by the server to prove that it - /// received a valid WebSocket connection request. - /// /// - /// A that represents the value of the Sec-WebSocket-Key header. + /// + /// A that represents the value of + /// the Sec-WebSocket-Key header. + /// + /// + /// The value is used to prove that the server received + /// a valid WebSocket handshake request. + /// + /// + /// if not included. + /// /// public override string SecWebSocketKey { get { @@ -212,33 +262,49 @@ public override string SecWebSocketKey { } /// - /// Gets the values of the Sec-WebSocket-Protocol header included in the request. + /// Gets the names of the subprotocols from the Sec-WebSocket-Protocol + /// header included in the handshake request. /// - /// - /// This property represents the subprotocols requested by the client. - /// /// - /// An instance that provides - /// an enumerator which supports the iteration over the values of the Sec-WebSocket-Protocol - /// header. + /// + /// An + /// instance. + /// + /// + /// It provides an enumerator which supports the iteration over + /// the collection of the names of the subprotocols. + /// /// public override IEnumerable SecWebSocketProtocols { get { - var protocols = _context.Request.Headers["Sec-WebSocket-Protocol"]; - if (protocols != null) - foreach (var protocol in protocols.Split (',')) - yield return protocol.Trim (); + var val = _context.Request.Headers["Sec-WebSocket-Protocol"]; + + if (val == null || val.Length == 0) + yield break; + + foreach (var elm in val.Split (',')) { + var protocol = elm.Trim (); + + if (protocol.Length == 0) + continue; + + yield return protocol; + } } } /// - /// Gets the value of the Sec-WebSocket-Version header included in the request. + /// Gets the value of the Sec-WebSocket-Version header included in + /// the handshake request. /// - /// - /// This property represents the WebSocket protocol version. - /// /// - /// A that represents the value of the Sec-WebSocket-Version header. + /// + /// A that represents the WebSocket protocol + /// version specified by the client. + /// + /// + /// if not included. + /// /// public override string SecWebSocketVersion { get { @@ -247,22 +313,29 @@ public override string SecWebSocketVersion { } /// - /// Gets the server endpoint as an IP address and a port number. + /// Gets the endpoint to which the handshake request is sent. /// /// - /// A that represents the server endpoint. + /// A that represents the server + /// IP address and port number. /// public override System.Net.IPEndPoint ServerEndPoint { get { - return _context.Connection.LocalEndPoint; + return _context.Request.LocalEndPoint; } } /// - /// Gets the client information (identity, authentication, and security roles). + /// Gets the client information. /// /// - /// A instance that represents the client information. + /// + /// A instance that represents identity, + /// authentication, and security roles for the client. + /// + /// + /// if the client is not authenticated. + /// /// public override IPrincipal User { get { @@ -271,23 +344,24 @@ public override IPrincipal User { } /// - /// Gets the client endpoint as an IP address and a port number. + /// Gets the endpoint from which the handshake request is sent. /// /// - /// A that represents the client endpoint. + /// A that represents the client + /// IP address and port number. /// public override System.Net.IPEndPoint UserEndPoint { get { - return _context.Connection.RemoteEndPoint; + return _context.Request.RemoteEndPoint; } } /// - /// Gets the instance used for two-way communication - /// between client and server. + /// Gets the WebSocket interface used for two-way communication between + /// the client and server. /// /// - /// A . + /// A that represents the interface. /// public override WebSocket WebSocket { get { @@ -306,7 +380,9 @@ internal void Close () internal void Close (HttpStatusCode code) { - _context.Response.Close (code); + _context.Response.StatusCode = (int) code; + + _context.Response.Close (); } #endregion @@ -314,12 +390,11 @@ internal void Close (HttpStatusCode code) #region Public Methods /// - /// Returns a that represents the current - /// . + /// Returns a string that represents the current instance. /// /// - /// A that represents the current - /// . + /// A that contains the request line and headers + /// included in the handshake request. /// public override string ToString () { diff --git a/websocket-sharp/Net/WebSockets/TcpListenerWebSocketContext.cs b/websocket-sharp/Net/WebSockets/TcpListenerWebSocketContext.cs index 9def9f3d2..3e8d12777 100644 --- a/websocket-sharp/Net/WebSockets/TcpListenerWebSocketContext.cs +++ b/websocket-sharp/Net/WebSockets/TcpListenerWebSocketContext.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2025 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 @@ -39,28 +39,26 @@ using System.IO; using System.Net.Security; using System.Net.Sockets; -using System.Security.Cryptography.X509Certificates; using System.Security.Principal; using System.Text; namespace WebSocketSharp.Net.WebSockets { /// - /// Provides the properties used to access the information in a WebSocket connection request - /// received by the . + /// Provides the access to the information in a WebSocket handshake request + /// to a instance. /// internal class TcpListenerWebSocketContext : WebSocketContext { #region Private Fields - private CookieCollection _cookies; - private Logger _logger; + private bool _isSecureConnection; + private Logger _log; private NameValueCollection _queryString; private HttpRequest _request; - private bool _secure; + private Uri _requestUri; private Stream _stream; private TcpClient _tcpClient; - private Uri _uri; private IPrincipal _user; private WebSocket _websocket; @@ -73,33 +71,36 @@ internal TcpListenerWebSocketContext ( string protocol, bool secure, ServerSslConfiguration sslConfig, - Logger logger) + Logger log + ) { _tcpClient = tcpClient; - _secure = secure; - _logger = logger; + _log = log; var netStream = tcpClient.GetStream (); + if (secure) { var sslStream = new SslStream ( - netStream, false, sslConfig.ClientCertificateValidationCallback); + netStream, + false, + sslConfig.ClientCertificateValidationCallback + ); sslStream.AuthenticateAsServer ( sslConfig.ServerCertificate, sslConfig.ClientCertificateRequired, sslConfig.EnabledSslProtocols, - sslConfig.CheckCertificateRevocation); + sslConfig.CheckCertificateRevocation + ); + _isSecureConnection = true; _stream = sslStream; } else { _stream = netStream; } - _request = HttpRequest.Read (_stream, 90000); - _uri = HttpUtility.CreateRequestUrl ( - _request.RequestUri, _request.Headers["Host"], _request.IsWebSocketRequest, secure); - + _request = HttpRequest.ReadRequest (_stream, 90000); _websocket = new WebSocket (this, protocol); } @@ -107,15 +108,15 @@ internal TcpListenerWebSocketContext ( #region Internal Properties - internal string HttpMethod { + internal Logger Log { get { - return _request.HttpMethod; + return _log; } } - internal Logger Log { + internal Socket Socket { get { - return _logger; + return _tcpClient.Client; } } @@ -130,19 +131,25 @@ internal Stream Stream { #region Public Properties /// - /// Gets the HTTP cookies included in the request. + /// Gets the HTTP cookies included in the handshake request. /// /// - /// A that contains the cookies. + /// + /// A that contains + /// the cookies. + /// + /// + /// An empty collection if not included. + /// /// public override CookieCollection CookieCollection { get { - return _cookies ?? (_cookies = _request.Cookies); + return _request.Cookies; } } /// - /// Gets the HTTP headers included in the request. + /// Gets the HTTP headers included in the handshake request. /// /// /// A that contains the headers. @@ -154,10 +161,16 @@ public override NameValueCollection Headers { } /// - /// Gets the value of the Host header included in the request. + /// Gets the value of the Host header included in the handshake request. /// /// - /// A that represents the value of the Host header. + /// + /// A that represents the server host name requested + /// by the client. + /// + /// + /// It includes the port number if provided. + /// /// public override string Host { get { @@ -178,10 +191,12 @@ public override bool IsAuthenticated { } /// - /// Gets a value indicating whether the client connected from the local computer. + /// Gets a value indicating whether the handshake request is sent from + /// the local computer. /// /// - /// true if the client connected from the local computer; otherwise, false. + /// true if the handshake request is sent from the same computer + /// as the server; otherwise, false. /// public override bool IsLocal { get { @@ -190,22 +205,25 @@ public override bool IsLocal { } /// - /// Gets a value indicating whether the WebSocket connection is secured. + /// Gets a value indicating whether a secure connection is used to send + /// the handshake request. /// /// - /// true if the connection is secured; otherwise, false. + /// true if the connection is secure; otherwise, false. /// public override bool IsSecureConnection { get { - return _secure; + return _isSecureConnection; } } /// - /// Gets a value indicating whether the request is a WebSocket connection request. + /// Gets a value indicating whether the request is a WebSocket handshake + /// request. /// /// - /// true if the request is a WebSocket connection request; otherwise, false. + /// true if the request is a WebSocket handshake request; otherwise, + /// false. /// public override bool IsWebSocketRequest { get { @@ -214,10 +232,15 @@ public override bool IsWebSocketRequest { } /// - /// Gets the value of the Origin header included in the request. + /// Gets the value of the Origin header included in the handshake request. /// /// - /// A that represents the value of the Origin header. + /// + /// A that represents the value of the Origin header. + /// + /// + /// if not included. + /// /// public override string Origin { get { @@ -226,16 +249,27 @@ public override string Origin { } /// - /// Gets the query string included in the request. + /// Gets the query string included in the handshake request. /// /// - /// A that contains the query string parameters. + /// + /// A that contains the query + /// parameters. + /// + /// + /// An empty collection if not included. + /// /// public override NameValueCollection QueryString { get { - return _queryString ?? - (_queryString = HttpUtility.InternalParseQueryString ( - _uri != null ? _uri.Query : null, Encoding.UTF8)); + if (_queryString == null) { + var uri = RequestUri; + var query = uri != null ? uri.Query : null; + + _queryString = QueryStringCollection.Parse (query, Encoding.UTF8); + } + + return _queryString; } } @@ -243,23 +277,44 @@ public override NameValueCollection QueryString { /// Gets the URI requested by the client. /// /// - /// A that represents the requested URI. + /// + /// A that represents the URI parsed from the request. + /// + /// + /// if the URI cannot be parsed. + /// /// public override Uri RequestUri { get { - return _uri; + if (_requestUri == null) { + _requestUri = HttpUtility.CreateRequestUrl ( + _request.RequestTarget, + _request.Headers["Host"], + _request.IsWebSocketRequest, + _isSecureConnection + ); + } + + return _requestUri; } } /// - /// Gets the value of the Sec-WebSocket-Key header included in the request. + /// Gets the value of the Sec-WebSocket-Key header included in + /// the handshake request. /// - /// - /// This property provides a part of the information used by the server to prove that it - /// received a valid WebSocket connection request. - /// /// - /// A that represents the value of the Sec-WebSocket-Key header. + /// + /// A that represents the value of + /// the Sec-WebSocket-Key header. + /// + /// + /// The value is used to prove that the server received + /// a valid WebSocket handshake request. + /// + /// + /// if not included. + /// /// public override string SecWebSocketKey { get { @@ -268,33 +323,49 @@ public override string SecWebSocketKey { } /// - /// Gets the values of the Sec-WebSocket-Protocol header included in the request. + /// Gets the names of the subprotocols from the Sec-WebSocket-Protocol + /// header included in the handshake request. /// - /// - /// This property represents the subprotocols requested by the client. - /// /// - /// An instance that provides - /// an enumerator which supports the iteration over the values of the Sec-WebSocket-Protocol - /// header. + /// + /// An + /// instance. + /// + /// + /// It provides an enumerator which supports the iteration over + /// the collection of the names of the subprotocols. + /// /// public override IEnumerable SecWebSocketProtocols { get { - var protocols = _request.Headers["Sec-WebSocket-Protocol"]; - if (protocols != null) - foreach (var protocol in protocols.Split (',')) - yield return protocol.Trim (); + var val = _request.Headers["Sec-WebSocket-Protocol"]; + + if (val == null || val.Length == 0) + yield break; + + foreach (var elm in val.Split (',')) { + var protocol = elm.Trim (); + + if (protocol.Length == 0) + continue; + + yield return protocol; + } } } /// - /// Gets the value of the Sec-WebSocket-Version header included in the request. + /// Gets the value of the Sec-WebSocket-Version header included in + /// the handshake request. /// - /// - /// This property represents the WebSocket protocol version. - /// /// - /// A that represents the value of the Sec-WebSocket-Version header. + /// + /// A that represents the WebSocket protocol + /// version specified by the client. + /// + /// + /// if not included. + /// /// public override string SecWebSocketVersion { get { @@ -303,10 +374,11 @@ public override string SecWebSocketVersion { } /// - /// Gets the server endpoint as an IP address and a port number. + /// Gets the endpoint to which the handshake request is sent. /// /// - /// A that represents the server endpoint. + /// A that represents the server + /// IP address and port number. /// public override System.Net.IPEndPoint ServerEndPoint { get { @@ -315,10 +387,16 @@ public override System.Net.IPEndPoint ServerEndPoint { } /// - /// Gets the client information (identity, authentication, and security roles). + /// Gets the client information. /// /// - /// A instance that represents the client information. + /// + /// A instance that represents identity, + /// authentication, and security roles for the client. + /// + /// + /// if the client is not authenticated. + /// /// public override IPrincipal User { get { @@ -327,10 +405,11 @@ public override IPrincipal User { } /// - /// Gets the client endpoint as an IP address and a port number. + /// Gets the endpoint from which the handshake request is sent. /// /// - /// A that represents the client endpoint. + /// A that represents the client + /// IP address and port number. /// public override System.Net.IPEndPoint UserEndPoint { get { @@ -339,11 +418,11 @@ public override System.Net.IPEndPoint UserEndPoint { } /// - /// Gets the instance used for two-way communication - /// between client and server. + /// Gets the WebSocket interface used for two-way communication between + /// the client and server. /// /// - /// A . + /// A that represents the interface. /// public override WebSocket WebSocket { get { @@ -363,19 +442,42 @@ internal void Close () internal void Close (HttpStatusCode code) { - _websocket.Close (HttpResponse.CreateCloseResponse (code)); + HttpResponse.CreateCloseResponse (code).WriteTo (_stream); + + _stream.Close (); + _tcpClient.Close (); } internal void SendAuthenticationChallenge (string challenge) { - var buff = HttpResponse.CreateUnauthorizedResponse (challenge).ToByteArray (); - _stream.Write (buff, 0, buff.Length); - _request = HttpRequest.Read (_stream, 15000); + HttpResponse.CreateUnauthorizedResponse (challenge).WriteTo (_stream); + + _request = HttpRequest.ReadRequest (_stream, 15000); } - internal void SetUser (IPrincipal value) + internal bool SetUser ( + AuthenticationSchemes scheme, + string realm, + Func credentialsFinder + ) { - _user = value; + var user = HttpUtility.CreateUser ( + _request.Headers["Authorization"], + scheme, + realm, + _request.HttpMethod, + credentialsFinder + ); + + if (user == null) + return false; + + if (!user.Identity.IsAuthenticated) + return false; + + _user = user; + + return true; } #endregion @@ -383,12 +485,11 @@ internal void SetUser (IPrincipal value) #region Public Methods /// - /// Returns a that represents the current - /// . + /// Returns a string that represents the current instance. /// /// - /// A that represents the current - /// . + /// A that contains the request line and headers + /// included in the handshake request. /// public override string ToString () { diff --git a/websocket-sharp/Net/WebSockets/WebSocketContext.cs b/websocket-sharp/Net/WebSockets/WebSocketContext.cs index 5f88e7094..84841f5bd 100644 --- a/websocket-sharp/Net/WebSockets/WebSocketContext.cs +++ b/websocket-sharp/Net/WebSockets/WebSocketContext.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2012-2014 sta.blockhead + * Copyright (c) 2012-2022 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 @@ -34,10 +34,10 @@ namespace WebSocketSharp.Net.WebSockets { /// - /// Exposes the properties used to access the information in a WebSocket connection request. + /// Exposes the access to the information in a WebSocket handshake request. /// /// - /// The WebSocketContext class is an abstract class. + /// This class is an abstract class. /// public abstract class WebSocketContext { @@ -55,15 +55,16 @@ protected WebSocketContext () #region Public Properties /// - /// Gets the HTTP cookies included in the request. + /// Gets the HTTP cookies included in the handshake request. /// /// - /// A that contains the cookies. + /// A that contains + /// the cookies. /// public abstract CookieCollection CookieCollection { get; } /// - /// Gets the HTTP headers included in the request. + /// Gets the HTTP headers included in the handshake request. /// /// /// A that contains the headers. @@ -71,10 +72,11 @@ protected WebSocketContext () public abstract NameValueCollection Headers { get; } /// - /// Gets the value of the Host header included in the request. + /// Gets the value of the Host header included in the handshake request. /// /// - /// A that represents the value of the Host header. + /// A that represents the server host name requested + /// by the client. /// public abstract string Host { get; } @@ -87,31 +89,36 @@ protected WebSocketContext () public abstract bool IsAuthenticated { get; } /// - /// Gets a value indicating whether the client connected from the local computer. + /// Gets a value indicating whether the handshake request is sent from + /// the local computer. /// /// - /// true if the client connected from the local computer; otherwise, false. + /// true if the handshake request is sent from the same computer + /// as the server; otherwise, false. /// public abstract bool IsLocal { get; } /// - /// Gets a value indicating whether the WebSocket connection is secured. + /// Gets a value indicating whether a secure connection is used to send + /// the handshake request. /// /// - /// true if the connection is secured; otherwise, false. + /// true if the connection is secure; otherwise, false. /// public abstract bool IsSecureConnection { get; } /// - /// Gets a value indicating whether the request is a WebSocket connection request. + /// Gets a value indicating whether the request is a WebSocket handshake + /// request. /// /// - /// true if the request is a WebSocket connection request; otherwise, false. + /// true if the request is a WebSocket handshake request; otherwise, + /// false. /// public abstract bool IsWebSocketRequest { get; } /// - /// Gets the value of the Origin header included in the request. + /// Gets the value of the Origin header included in the handshake request. /// /// /// A that represents the value of the Origin header. @@ -119,10 +126,10 @@ protected WebSocketContext () public abstract string Origin { get; } /// - /// Gets the query string included in the request. + /// Gets the query string included in the handshake request. /// /// - /// A that contains the query string parameters. + /// A that contains the query parameters. /// public abstract NameValueCollection QueryString { get; } @@ -130,76 +137,85 @@ protected WebSocketContext () /// Gets the URI requested by the client. /// /// - /// A that represents the requested URI. + /// A that represents the URI parsed from the request. /// public abstract Uri RequestUri { get; } /// - /// Gets the value of the Sec-WebSocket-Key header included in the request. + /// Gets the value of the Sec-WebSocket-Key header included in + /// the handshake request. /// - /// - /// This property provides a part of the information used by the server to prove that it - /// received a valid WebSocket connection request. - /// /// - /// A that represents the value of the Sec-WebSocket-Key header. + /// + /// A that represents the value of + /// the Sec-WebSocket-Key header. + /// + /// + /// The value is used to prove that the server received + /// a valid WebSocket handshake request. + /// /// public abstract string SecWebSocketKey { get; } /// - /// Gets the values of the Sec-WebSocket-Protocol header included in the request. + /// Gets the names of the subprotocols from the Sec-WebSocket-Protocol + /// header included in the handshake request. /// - /// - /// This property represents the subprotocols requested by the client. - /// /// - /// An instance that provides - /// an enumerator which supports the iteration over the values of the Sec-WebSocket-Protocol - /// header. + /// + /// An + /// instance. + /// + /// + /// It provides an enumerator which supports the iteration over + /// the collection of the names of the subprotocols. + /// /// public abstract IEnumerable SecWebSocketProtocols { get; } /// - /// Gets the value of the Sec-WebSocket-Version header included in the request. + /// Gets the value of the Sec-WebSocket-Version header included in + /// the handshake request. /// - /// - /// This property represents the WebSocket protocol version. - /// /// - /// A that represents the value of the Sec-WebSocket-Version header. + /// A that represents the WebSocket protocol + /// version specified by the client. /// public abstract string SecWebSocketVersion { get; } /// - /// Gets the server endpoint as an IP address and a port number. + /// Gets the endpoint to which the handshake request is sent. /// /// - /// A that represents the server endpoint. + /// A that represents the server + /// IP address and port number. /// public abstract System.Net.IPEndPoint ServerEndPoint { get; } /// - /// Gets the client information (identity, authentication, and security roles). + /// Gets the client information. /// /// - /// A instance that represents the client information. + /// A instance that represents identity, + /// authentication, and security roles for the client. /// public abstract IPrincipal User { get; } /// - /// Gets the client endpoint as an IP address and a port number. + /// Gets the endpoint from which the handshake request is sent. /// /// - /// A that represents the client endpoint. + /// A that represents the client + /// IP address and port number. /// public abstract System.Net.IPEndPoint UserEndPoint { get; } /// - /// Gets the instance used for two-way communication - /// between client and server. + /// Gets the WebSocket interface used for two-way communication between + /// the client and server. /// /// - /// A . + /// A that represents the interface. /// public abstract WebSocket WebSocket { get; } diff --git a/websocket-sharp/Opcode.cs b/websocket-sharp/Opcode.cs index faabdb0dd..06da1b5e1 100644 --- a/websocket-sharp/Opcode.cs +++ b/websocket-sharp/Opcode.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2025 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 @@ -35,9 +35,10 @@ namespace WebSocketSharp /// /// /// The values of this enumeration are defined in - /// Section 5.2 of RFC 6455. + /// + /// Section 5.2 of RFC 6455. /// - public enum Opcode : byte + internal enum Opcode { /// /// Equivalent to numeric value 0. Indicates continuation frame. diff --git a/websocket-sharp/PayloadData.cs b/websocket-sharp/PayloadData.cs index 6bbf6905d..e01fcd27e 100644 --- a/websocket-sharp/PayloadData.cs +++ b/websocket-sharp/PayloadData.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2025 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 @@ -36,9 +36,10 @@ internal class PayloadData : IEnumerable { #region Private Fields - private byte[] _data; - private long _extDataLength; - private long _length; + private byte[] _data; + private static readonly byte[] _emptyBytes; + private long _extDataLength; + private long _length; #endregion @@ -50,16 +51,17 @@ internal class PayloadData : IEnumerable public static readonly PayloadData Empty; /// - /// Represents the allowable max length. + /// Represents the allowable max length of payload data. /// /// /// - /// A will occur if the payload data length is - /// greater than the value of this field. + /// A is thrown when the length of + /// incoming payload data is greater than the value of this field. /// /// - /// If you would like to change the value, you must set it to a value between - /// WebSocket.FragmentLength and Int64.MaxValue inclusive. + /// If you would like to change the value of this field, it must be + /// a number between and + /// inclusive. /// /// public static readonly ulong MaxLength; @@ -70,7 +72,9 @@ internal class PayloadData : IEnumerable static PayloadData () { - Empty = new PayloadData (); + _emptyBytes = new byte[0]; + + Empty = new PayloadData (_emptyBytes, 0); MaxLength = Int64.MaxValue; } @@ -78,11 +82,6 @@ static PayloadData () #region Internal Constructors - internal PayloadData () - { - _data = WebSocket.EmptyBytes; - } - internal PayloadData (byte[] data) : this (data, data.LongLength) { @@ -94,10 +93,24 @@ internal PayloadData (byte[] data, long length) _length = length; } + internal PayloadData (ushort code, string reason) + { + _data = code.Append (reason); + _length = _data.LongLength; + } + #endregion #region Internal Properties + internal ushort Code { + get { + return _length >= 2 + ? _data.SubArray (0, 2).ToUInt16 (ByteOrder.Big) + : (ushort) 1005; + } + } + internal long ExtensionDataLength { get { return _extDataLength; @@ -108,9 +121,24 @@ internal long ExtensionDataLength { } } - internal bool IncludesReservedCloseStatusCode { + internal bool HasReservedCode { get { - return _length > 1 && _data.SubArray (0, 2).ToUInt16 (ByteOrder.Big).IsReserved (); + return _length >= 2 && Code.IsReservedStatusCode (); + } + } + + internal string Reason { + get { + if (_length <= 2) + return String.Empty; + + var bytes = _data.SubArray (2, _length - 2); + + string reason; + + return bytes.TryGetUTF8DecodedString (out reason) + ? reason + : String.Empty; } } @@ -130,7 +158,7 @@ public byte[] ExtensionData { get { return _extDataLength > 0 ? _data.SubArray (0, _extDataLength) - : WebSocket.EmptyBytes; + : _emptyBytes; } } diff --git a/websocket-sharp/Rsv.cs b/websocket-sharp/Rsv.cs index 8a10567c5..c2a4dea7a 100644 --- a/websocket-sharp/Rsv.cs +++ b/websocket-sharp/Rsv.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2025 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 @@ -31,13 +31,15 @@ namespace WebSocketSharp { /// - /// Indicates whether each RSV (RSV1, RSV2, and RSV3) of a WebSocket frame is non-zero. + /// Indicates whether each RSV (RSV1, RSV2, and RSV3) of a WebSocket + /// frame is non-zero. /// /// /// The values of this enumeration are defined in - /// Section 5.2 of RFC 6455. + /// + /// Section 5.2 of RFC 6455. /// - internal enum Rsv : byte + internal enum Rsv { /// /// Equivalent to numeric value 0. Indicates zero. diff --git a/websocket-sharp/Server/HttpRequestEventArgs.cs b/websocket-sharp/Server/HttpRequestEventArgs.cs index 34a83ecf8..45af3c1ee 100644 --- a/websocket-sharp/Server/HttpRequestEventArgs.cs +++ b/websocket-sharp/Server/HttpRequestEventArgs.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2024 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 @@ -27,41 +27,49 @@ #endregion using System; +using System.IO; +using System.Security.Principal; +using System.Text; using WebSocketSharp.Net; namespace WebSocketSharp.Server { /// - /// Represents the event data for the HTTP request event that the emits. + /// Represents the event data for the HTTP request events of the + /// class. /// /// /// - /// An HTTP request event occurs when the receives an HTTP request. + /// An HTTP request event occurs when the + /// instance receives an HTTP request. /// /// - /// If you would like to get the request data sent from a client, - /// you should access the property. + /// You should access the property if you would + /// like to get the request data sent from a client. /// /// - /// And if you would like to get the response data used to return a response, - /// you should access the property. + /// And you should access the property if you + /// would like to get the response data to return to the client. /// /// public class HttpRequestEventArgs : EventArgs { #region Private Fields - private HttpListenerRequest _request; - private HttpListenerResponse _response; + private HttpListenerContext _context; + private string _docRootPath; #endregion #region Internal Constructors - internal HttpRequestEventArgs (HttpListenerContext context) + internal HttpRequestEventArgs ( + HttpListenerContext context, + string documentRootPath + ) { - _request = context.Request; - _response = context.Response; + _context = context; + _docRootPath = documentRootPath; } #endregion @@ -69,29 +77,190 @@ internal HttpRequestEventArgs (HttpListenerContext context) #region Public Properties /// - /// Gets the HTTP request data sent from a client. + /// Gets the request data sent from a client. /// /// - /// A that represents the request data. + /// A that provides the methods and + /// properties for the request data. /// public HttpListenerRequest Request { get { - return _request; + return _context.Request; } } /// - /// Gets the HTTP response data used to return a response to the client. + /// Gets the response data to return to the client. /// /// - /// A that represents the response data. + /// A that provides the methods and + /// properties for the response data. /// public HttpListenerResponse Response { get { - return _response; + return _context.Response; } } + /// + /// Gets the information for the client. + /// + /// + /// + /// A instance that represents identity, + /// authentication scheme, and security roles for the client. + /// + /// + /// if the client is not authenticated. + /// + /// + public IPrincipal User { + get { + return _context.User; + } + } + + #endregion + + #region Private Methods + + private string createFilePath (string childPath) + { + childPath = childPath.TrimStart ('/', '\\'); + + return new StringBuilder (_docRootPath, 32) + .AppendFormat ("/{0}", childPath) + .ToString () + .Replace ('\\', '/'); + } + + private static bool tryReadFile (string path, out byte[] contents) + { + contents = null; + + if (!File.Exists (path)) + return false; + + try { + contents = File.ReadAllBytes (path); + } + catch { + return false; + } + + return true; + } + + #endregion + + #region Public Methods + + /// + /// Reads the specified file from the document folder of the + /// class. + /// + /// + /// + /// An array of that receives the contents of + /// the file. + /// + /// + /// if the read has failed. + /// + /// + /// + /// A that specifies a virtual path to find + /// the file from the document folder. + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// contains "..". + /// + /// + /// + /// is . + /// + public byte[] ReadFile (string path) + { + if (path == null) + throw new ArgumentNullException ("path"); + + if (path.Length == 0) + throw new ArgumentException ("An empty string.", "path"); + + if (path.Contains ("..")) { + var msg = "It contains \"..\"."; + + throw new ArgumentException (msg, "path"); + } + + path = createFilePath (path); + byte[] contents; + + tryReadFile (path, out contents); + + return contents; + } + + /// + /// Tries to read the specified file from the document folder of + /// the class. + /// + /// + /// true if the try has succeeded; otherwise, false. + /// + /// + /// A that specifies a virtual path to find + /// the file from the document folder. + /// + /// + /// + /// When this method returns, an array of that + /// receives the contents of the file. + /// + /// + /// if the read has failed. + /// + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// contains "..". + /// + /// + /// + /// is . + /// + public bool TryReadFile (string path, out byte[] contents) + { + if (path == null) + throw new ArgumentNullException ("path"); + + if (path.Length == 0) + throw new ArgumentException ("An empty string.", "path"); + + if (path.Contains ("..")) { + var msg = "It contains \"..\"."; + + throw new ArgumentException (msg, "path"); + } + + path = createFilePath (path); + + return tryReadFile (path, out contents); + } + #endregion } } diff --git a/websocket-sharp/Server/HttpServer.cs b/websocket-sharp/Server/HttpServer.cs index d4ec6ad98..209109984 100644 --- a/websocket-sharp/Server/HttpServer.cs +++ b/websocket-sharp/Server/HttpServer.cs @@ -2,11 +2,9 @@ /* * HttpServer.cs * - * A simple HTTP server that allows to accept the WebSocket connection requests. - * * The MIT License * - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2024 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 @@ -44,6 +42,7 @@ using System.Net.Sockets; using System.Security.Cryptography.X509Certificates; using System.Security.Principal; +using System.Text; using System.Threading; using WebSocketSharp.Net; using WebSocketSharp.Net.WebSockets; @@ -51,27 +50,33 @@ namespace WebSocketSharp.Server { /// - /// Provides a simple HTTP server that allows to accept the WebSocket connection requests. + /// Provides a simple HTTP server. /// /// - /// The HttpServer class can provide multiple WebSocket services. + /// + /// The server supports HTTP/1.1 version request and response. + /// + /// + /// Also the server allows to accept WebSocket handshake requests. + /// + /// + /// This class can provide multiple WebSocket services. + /// /// public class HttpServer { #region Private Fields private System.Net.IPAddress _address; - private string _hostname; + private string _docRootPath; + private bool _isSecure; private HttpListener _listener; - private Logger _logger; + private Logger _log; private int _port; private Thread _receiveThread; - private string _rootPath; - private bool _secure; private WebSocketServiceManager _services; private volatile ServerState _state; private object _sync; - private bool _windows; #endregion @@ -81,7 +86,8 @@ public class HttpServer /// Initializes a new instance of the class. /// /// - /// An instance initialized by this constructor listens for the incoming requests on port 80. + /// The new instance listens for incoming requests on + /// and port 80. /// public HttpServer () { @@ -90,22 +96,23 @@ public HttpServer () /// /// Initializes a new instance of the class with - /// the specified . + /// the specified port. /// /// /// - /// An instance initialized by this constructor listens for the incoming requests on - /// . + /// The new instance listens for incoming requests on + /// and . /// /// - /// If is 443, that instance provides a secure connection. + /// It provides secure connections if is 443. /// /// /// - /// An that represents the port number on which to listen. + /// An that specifies the number of the port on which + /// to listen. /// /// - /// isn't between 1 and 65535 inclusive. + /// is less than 1 or greater than 65535. /// public HttpServer (int port) : this (port, port == 443) @@ -114,28 +121,29 @@ public HttpServer (int port) /// /// Initializes a new instance of the class with - /// the specified HTTP URL. + /// the specified URL. /// /// /// - /// An instance initialized by this constructor listens for the incoming requests on - /// the host name and port in . + /// The new instance listens for incoming requests on the IP address and + /// port of . + /// + /// + /// Either port 80 or 443 is used if includes + /// no port. Port 443 is used if the scheme of + /// is https; otherwise, port 80 is used. /// /// - /// If doesn't include a port, either port 80 or 443 is used on - /// which to listen. It's determined by the scheme (http or https) in . - /// (Port 80 if the scheme is http.) + /// The new instance provides secure connections if the scheme of + /// is https. /// /// /// - /// A that represents the HTTP URL of the server. + /// A that specifies the HTTP URL of the server. /// - /// - /// is . - /// /// /// - /// is empty. + /// is an empty string. /// /// /// -or- @@ -144,6 +152,9 @@ public HttpServer (int port) /// is invalid. /// /// + /// + /// is . + /// public HttpServer (string url) { if (url == null) @@ -154,71 +165,87 @@ public HttpServer (string url) Uri uri; string msg; + if (!tryCreateUri (url, out uri, out msg)) throw new ArgumentException (msg, "url"); - var host = uri.DnsSafeHost; + var host = uri.GetDnsSafeHost (true); var addr = host.ToIPAddress (); - if (!addr.IsLocal ()) - throw new ArgumentException ("The host part isn't a local host name: " + url, "url"); + + if (addr == null) { + msg = "The host part could not be converted to an IP address."; + + throw new ArgumentException (msg, "url"); + } + + if (!addr.IsLocal ()) { + msg = "The IP address of the host is not a local IP address."; + + throw new ArgumentException (msg, "url"); + } init (host, addr, uri.Port, uri.Scheme == "https"); } /// /// Initializes a new instance of the class with - /// the specified and . + /// the specified port and boolean if secure or not. /// /// - /// An instance initialized by this constructor listens for the incoming requests on - /// . + /// The new instance listens for incoming requests on + /// and . /// /// - /// An that represents the port number on which to listen. + /// An that specifies the number of the port on which + /// to listen. /// /// - /// A that indicates providing a secure connection or not. - /// (true indicates providing a secure connection.) + /// A : true if the new instance provides + /// secure connections; otherwise, false. /// /// - /// isn't between 1 and 65535 inclusive. + /// is less than 1 or greater than 65535. /// public HttpServer (int port, bool secure) { - if (!port.IsPortNumber ()) - throw new ArgumentOutOfRangeException ( - "port", "Not between 1 and 65535 inclusive: " + port); + if (!port.IsPortNumber ()) { + var msg = "Less than 1 or greater than 65535."; + + throw new ArgumentOutOfRangeException ("port", msg); + } init ("*", System.Net.IPAddress.Any, port, secure); } /// /// Initializes a new instance of the class with - /// the specified and . + /// the specified IP address and port. /// /// /// - /// An instance initialized by this constructor listens for the incoming requests on + /// The new instance listens for incoming requests on /// and . /// /// - /// If is 443, that instance provides a secure connection. + /// It provides secure connections if is 443. /// /// /// - /// A that represents the local IP address of the server. + /// A that specifies the local IP + /// address on which to listen. /// /// - /// An that represents the port number on which to listen. + /// An that specifies the number of the port on which + /// to listen. /// + /// + /// is not a local IP address. + /// /// /// is . /// - /// - /// isn't a local IP address. - /// /// - /// isn't between 1 and 65535 inclusive. + /// is less than 1 or greater than 65535. /// public HttpServer (System.Net.IPAddress address, int port) : this (address, port, port == 443) @@ -227,45 +254,51 @@ public HttpServer (System.Net.IPAddress address, int port) /// /// Initializes a new instance of the class with - /// the specified , , - /// and . + /// the specified IP address, port, and boolean if secure or not. /// /// - /// An instance initialized by this constructor listens for the incoming requests on + /// The new instance listens for incoming requests on /// and . /// /// - /// A that represents the local IP address of the server. + /// A that specifies the local IP + /// address on which to listen. /// /// - /// An that represents the port number on which to listen. + /// An that specifies the number of the port on which + /// to listen. /// /// - /// A that indicates providing a secure connection or not. - /// (true indicates providing a secure connection.) + /// A : true if the new instance provides + /// secure connections; otherwise, false. /// + /// + /// is not a local IP address. + /// /// /// is . /// - /// - /// isn't a local IP address. - /// /// - /// isn't between 1 and 65535 inclusive. + /// is less than 1 or greater than 65535. /// public HttpServer (System.Net.IPAddress address, int port, bool secure) { if (address == null) throw new ArgumentNullException ("address"); - if (!address.IsLocal ()) - throw new ArgumentException ("Not a local IP address: " + address, "address"); + if (!address.IsLocal ()) { + var msg = "Not a local IP address."; + + throw new ArgumentException (msg, "address"); + } + + if (!port.IsPortNumber ()) { + var msg = "Less than 1 or greater than 65535."; - if (!port.IsPortNumber ()) - throw new ArgumentOutOfRangeException ( - "port", "Not between 1 and 65535 inclusive: " + port); + throw new ArgumentOutOfRangeException ("port", msg); + } - init (null, address, port, secure); + init (address.ToString (true), address, port, secure); } #endregion @@ -273,10 +306,11 @@ public HttpServer (System.Net.IPAddress address, int port, bool secure) #region Public Properties /// - /// Gets the local IP address of the server. + /// Gets the IP address of the server. /// /// - /// A that represents the local IP address of the server. + /// A that represents the local IP + /// address on which to listen for incoming requests. /// public System.Net.IPAddress Address { get { @@ -287,10 +321,22 @@ public System.Net.IPAddress Address { /// /// Gets or sets the scheme used to authenticate the clients. /// + /// + /// The set operation works if the current state of the server is + /// Ready or Stop. + /// /// - /// One of the enum values, - /// indicates the scheme used to authenticate the clients. The default value is - /// . + /// + /// One of the + /// enum values. + /// + /// + /// It represents the scheme used to authenticate the clients. + /// + /// + /// The default value is + /// . + /// /// public AuthenticationSchemes AuthenticationSchemes { get { @@ -298,13 +344,100 @@ public AuthenticationSchemes AuthenticationSchemes { } set { - var msg = _state.CheckIfAvailable (true, false, false); - if (msg != null) { - _logger.Error (msg); - return; + lock (_sync) { + if (!canSet ()) + return; + + _listener.AuthenticationSchemes = value; } + } + } - _listener.AuthenticationSchemes = value; + /// + /// Gets or sets the path to the document folder of the server. + /// + /// + /// The set operation works if the current state of the server is + /// Ready or Stop. + /// + /// + /// + /// A that represents a path to the folder + /// from which to find the requested file. + /// + /// + /// / or \ is trimmed from the end of the value if present. + /// + /// + /// The default value is "./Public". + /// + /// + /// + /// + /// The value specified for a set operation is an empty string. + /// + /// + /// -or- + /// + /// + /// The value specified for a set operation is an absolute root. + /// + /// + /// -or- + /// + /// + /// The value specified for a set operation is an invalid path string. + /// + /// + /// + /// The value specified for a set operation is . + /// + public string DocumentRootPath { + get { + return _docRootPath; + } + + set { + if (value == null) + throw new ArgumentNullException ("value"); + + if (value.Length == 0) + throw new ArgumentException ("An empty string.", "value"); + + value = value.TrimSlashOrBackslashFromEnd (); + + if (value == "/") + throw new ArgumentException ("An absolute root.", "value"); + + if (value == "\\") + throw new ArgumentException ("An absolute root.", "value"); + + if (value.Length == 2 && value[1] == ':') + throw new ArgumentException ("An absolute root.", "value"); + + string full = null; + + try { + full = Path.GetFullPath (value); + } + catch (Exception ex) { + throw new ArgumentException ("An invalid path string.", "value", ex); + } + + if (full == "/") + throw new ArgumentException ("An absolute root.", "value"); + + full = full.TrimSlashOrBackslashFromEnd (); + + if (full.Length == 2 && full[1] == ':') + throw new ArgumentException ("An absolute root.", "value"); + + lock (_sync) { + if (!canSet ()) + return; + + _docRootPath = value; + } } } @@ -321,24 +454,34 @@ public bool IsListening { } /// - /// Gets a value indicating whether the server provides a secure connection. + /// Gets a value indicating whether the server provides secure connections. /// /// - /// true if the server provides a secure connection; otherwise, false. + /// true if the server provides secure connections; otherwise, + /// false. /// public bool IsSecure { get { - return _secure; + return _isSecure; } } /// /// Gets or sets a value indicating whether the server cleans up - /// the inactive sessions in the WebSocket services periodically. + /// the inactive sessions periodically. /// + /// + /// The set operation works if the current state of the server is + /// Ready or Stop. + /// /// - /// true if the server cleans up the inactive sessions every 60 seconds; - /// otherwise, false. The default value is true. + /// + /// true if the server cleans up the inactive sessions + /// every 60 seconds; otherwise, false. + /// + /// + /// The default value is false. + /// /// public bool KeepClean { get { @@ -346,38 +489,31 @@ public bool KeepClean { } set { - var msg = _state.CheckIfAvailable (true, false, false); - if (msg != null) { - _logger.Error (msg); - return; - } - _services.KeepClean = value; } } /// - /// Gets the logging functions. + /// Gets the logging function for the server. /// /// - /// The default logging level is . If you would like to change it, - /// you should set the Log.Level property to any of the enum - /// values. + /// The default logging level is . /// /// - /// A that provides the logging functions. + /// A that provides the logging function. /// public Logger Log { get { - return _logger; + return _log; } } /// - /// Gets the port on which to listen for incoming requests. + /// Gets the port of the server. /// /// - /// An that represents the port number on which to listen. + /// An that represents the number of the port on which + /// to listen for incoming requests. /// public int Port { get { @@ -388,9 +524,21 @@ public int Port { /// /// Gets or sets the name of the realm associated with the server. /// + /// + /// The set operation works if the current state of the server is + /// Ready or Stop. + /// /// - /// A that represents the name of the realm. The default value is - /// "SECRET AREA". + /// + /// A that represents the name of the realm. + /// + /// + /// "SECRET AREA" is used as the name of the realm if the value is + /// or an empty string. + /// + /// + /// The default value is . + /// /// public string Realm { get { @@ -398,27 +546,37 @@ public string Realm { } set { - var msg = _state.CheckIfAvailable (true, false, false); - if (msg != null) { - _logger.Error (msg); - return; - } + lock (_sync) { + if (!canSet ()) + return; - _listener.Realm = value; + _listener.Realm = value; + } } } /// - /// Gets or sets a value indicating whether the server is allowed to be bound to - /// an address that is already in use. + /// Gets or sets a value indicating whether the server is allowed to + /// be bound to an address that is already in use. /// /// - /// If you would like to resolve to wait for socket in TIME_WAIT state, - /// you should set this property to true. + /// + /// You should set this property to true if you would like to + /// resolve to wait for socket in TIME_WAIT state. + /// + /// + /// The set operation works if the current state of the server is + /// Ready or Stop. + /// /// /// - /// true if the server is allowed to be bound to an address that is already in use; - /// otherwise, false. The default value is false. + /// + /// true if the server is allowed to be bound to an address + /// that is already in use; otherwise, false. + /// + /// + /// The default value is false. + /// /// public bool ReuseAddress { get { @@ -426,71 +584,68 @@ public bool ReuseAddress { } set { - var msg = _state.CheckIfAvailable (true, false, false); - if (msg != null) { - _logger.Error (msg); - return; - } + lock (_sync) { + if (!canSet ()) + return; - _listener.ReuseAddress = value; - } - } - - /// - /// Gets or sets the document root path of the server. - /// - /// - /// A that represents the document root path of the server. - /// The default value is "./Public". - /// - public string RootPath { - get { - return _rootPath != null && _rootPath.Length > 0 ? _rootPath : (_rootPath = "./Public"); - } - - set { - var msg = _state.CheckIfAvailable (true, false, false); - if (msg != null) { - _logger.Error (msg); - return; + _listener.ReuseAddress = value; } - - _rootPath = value; } } /// - /// Gets or sets the SSL configuration used to authenticate the server and - /// optionally the client for secure connection. + /// Gets the configuration for secure connection. /// + /// + /// The configuration is used when the server attempts to start, + /// so it must be configured before the start method is called. + /// /// - /// A that represents the configuration used to - /// authenticate the server and optionally the client for secure connection. + /// A that represents the + /// configuration used to provide secure connections. /// + /// + /// The server does not provide secure connections. + /// public ServerSslConfiguration SslConfiguration { get { - return _listener.SslConfiguration; - } + if (!_isSecure) { + var msg = "The server does not provide secure connections."; - set { - var msg = _state.CheckIfAvailable (true, false, false); - if (msg != null) { - _logger.Error (msg); - return; + throw new InvalidOperationException (msg); } - _listener.SslConfiguration = value; + return _listener.SslConfiguration; } } /// - /// Gets or sets the delegate called to find the credentials for an identity used to - /// authenticate a client. + /// Gets or sets the delegate called to find the credentials for + /// an identity used to authenticate a client. /// + /// + /// The set operation works if the current state of the server is + /// Ready or Stop. + /// /// - /// A Func<, > delegate that - /// references the method(s) used to find the credentials. The default value is a function that - /// only returns . + /// + /// A + /// delegate. + /// + /// + /// It represents the delegate called when the server finds + /// the credentials used to authenticate a client. + /// + /// + /// It must return if the credentials + /// are not found. + /// + /// + /// if not necessary. + /// + /// + /// The default value is . + /// /// public Func UserCredentialsFinder { get { @@ -498,44 +653,52 @@ public Func UserCredentialsFinder { } set { - var msg = _state.CheckIfAvailable (true, false, false); - if (msg != null) { - _logger.Error (msg); - return; - } + lock (_sync) { + if (!canSet ()) + return; - _listener.UserCredentialsFinder = value; + _listener.UserCredentialsFinder = value; + } } } /// - /// Gets or sets the wait time for the response to the WebSocket Ping or Close. + /// Gets or sets the time to wait for the response to the WebSocket + /// Ping or Close. /// + /// + /// The set operation works if the current state of the server is + /// Ready or Stop. + /// /// - /// A that represents the wait time. The default value is - /// the same as 1 second. + /// + /// A that represents the time to wait for + /// the response. + /// + /// + /// The default value is the same as 1 second. + /// /// + /// + /// The value specified for a set operation is zero or less. + /// public TimeSpan WaitTime { get { return _services.WaitTime; } set { - var msg = _state.CheckIfAvailable (true, false, false) ?? value.CheckIfValidWaitTime (); - if (msg != null) { - _logger.Error (msg); - return; - } - _services.WaitTime = value; } } /// - /// Gets the access to the WebSocket services provided by the server. + /// Gets the management function for the WebSocket services provided by + /// the server. /// /// - /// A that manages the WebSocket services. + /// A that manages the WebSocket + /// services provided by the server. /// public WebSocketServiceManager WebSocketServices { get { @@ -572,11 +735,6 @@ public WebSocketServiceManager WebSocketServices { /// public event EventHandler OnOptions; - /// - /// Occurs when the server receives an HTTP PATCH request. - /// - public event EventHandler OnPatch; - /// /// Occurs when the server receives an HTTP POST request. /// @@ -599,50 +757,97 @@ public WebSocketServiceManager WebSocketServices { private void abort () { lock (_sync) { - if (!IsListening) + if (_state != ServerState.Start) return; _state = ServerState.ShuttingDown; } - _services.Stop (new CloseEventArgs (CloseStatusCode.ServerError), true, false); - _listener.Abort (); + try { + _services.Stop (1006, String.Empty); + } + catch (Exception ex) { + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + } + + try { + _listener.Abort (); + } + catch (Exception ex) { + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + } _state = ServerState.Stop; } - private string checkIfCertificateExists () + private bool canSet () { - if (!_secure) - return null; + return _state == ServerState.Ready || _state == ServerState.Stop; + } - var usr = _listener.SslConfiguration.ServerCertificate != null; - var port = EndPointListener.CertificateExists (_port, _listener.CertificateFolderPath); - if (usr && port) { - _logger.Warn ("The server certificate associated with the port number already exists."); - return null; + private bool checkCertificate (out string message) + { + message = null; + + var byUser = _listener.SslConfiguration.ServerCertificate != null; + + var path = _listener.CertificateFolderPath; + var withPort = EndPointListener.CertificateExists (_port, path); + + var either = byUser || withPort; + + if (!either) { + message = "There is no server certificate for secure connection."; + + return false; } - return !(usr || port) ? "The secure connection requires a server certificate." : null; + var both = byUser && withPort; + + if (both) { + var msg = "The server certificate associated with the port is used."; + + _log.Warn (msg); + } + + return true; + } + + private static HttpListener createListener ( + string hostname, + int port, + bool secure + ) + { + var ret = new HttpListener (); + + var fmt = "{0}://{1}:{2}/"; + var schm = secure ? "https" : "http"; + var pref = String.Format (fmt, schm, hostname, port); + + ret.Prefixes.Add (pref); + + return ret; } - private void init (string hostname, System.Net.IPAddress address, int port, bool secure) + private void init ( + string hostname, + System.Net.IPAddress address, + int port, + bool secure + ) { - _hostname = hostname ?? address.ToString (); _address = address; _port = port; - _secure = secure; - - _listener = new HttpListener (); - _listener.Prefixes.Add ( - String.Format ("http{0}://{1}:{2}/", secure ? "s" : "", _hostname, port)); + _isSecure = secure; - _logger = _listener.Log; - _services = new WebSocketServiceManager (_logger); + _docRootPath = "./Public"; + _listener = createListener (hostname, port, secure); + _log = _listener.Log; + _services = new WebSocketServiceManager (_log); _sync = new object (); - - var os = Environment.OSVersion; - _windows = os.Platform != PlatformID.Unix && os.Platform != PlatformID.MacOSX; } private void processRequest (HttpListenerContext context) @@ -658,29 +863,49 @@ private void processRequest (HttpListenerContext context) ? OnPut : method == "DELETE" ? OnDelete - : method == "OPTIONS" - ? OnOptions - : method == "TRACE" - ? OnTrace - : method == "CONNECT" - ? OnConnect - : method == "PATCH" - ? OnPatch - : null; - - if (evt != null) - evt (this, new HttpRequestEventArgs (context)); - else - context.Response.StatusCode = (int) HttpStatusCode.NotImplemented; + : method == "CONNECT" + ? OnConnect + : method == "OPTIONS" + ? OnOptions + : method == "TRACE" + ? OnTrace + : null; + + if (evt == null) { + context.ErrorStatusCode = 501; + + context.SendError (); + + return; + } + + var e = new HttpRequestEventArgs (context, _docRootPath); + + evt (this, e); context.Response.Close (); } private void processRequest (HttpListenerWebSocketContext context) { + var uri = context.RequestUri; + + if (uri == null) { + context.Close (HttpStatusCode.BadRequest); + + return; + } + + var path = uri.AbsolutePath; + + if (path.IndexOfAny (new[] { '%', '+' }) > -1) + path = HttpUtility.UrlDecode (path, Encoding.UTF8); + WebSocketServiceHost host; - if (!_services.InternalTryGetServiceHost (context.RequestUri.AbsolutePath, out host)) { + + if (!_services.InternalTryGetServiceHost (path, out host)) { context.Close (HttpStatusCode.NotImplemented); + return; } @@ -690,90 +915,199 @@ private void processRequest (HttpListenerWebSocketContext context) private void receiveRequest () { while (true) { + HttpListenerContext ctx = null; + try { - var ctx = _listener.GetContext (); + ctx = _listener.GetContext (); + ThreadPool.QueueUserWorkItem ( state => { try { - if (ctx.Request.IsUpgradeTo ("websocket")) { - processRequest (ctx.AcceptWebSocket (null)); + if (ctx.Request.IsUpgradeRequest ("websocket")) { + processRequest (ctx.GetWebSocketContext (null)); + return; } processRequest (ctx); } catch (Exception ex) { - _logger.Fatal (ex.ToString ()); + _log.Error (ex.Message); + _log.Debug (ex.ToString ()); + ctx.Connection.Close (true); } - }); + } + ); } catch (HttpListenerException ex) { - _logger.Warn ("Receiving has been stopped.\n reason: " + ex.Message); + if (_state == ServerState.ShuttingDown) + return; + + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + + break; + } + catch (InvalidOperationException ex) { + if (_state == ServerState.ShuttingDown) + return; + + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + break; } catch (Exception ex) { - _logger.Fatal (ex.ToString ()); + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + + if (ctx != null) + ctx.Connection.Close (true); + + if (_state == ServerState.ShuttingDown) + return; + break; } } - if (IsListening) - abort (); + abort (); + } + + private void start () + { + lock (_sync) { + if (_state == ServerState.Start || _state == ServerState.ShuttingDown) + return; + + if (_isSecure) { + string msg; + + if (!checkCertificate (out msg)) + throw new InvalidOperationException (msg); + } + + _services.Start (); + + try { + startReceiving (); + } + catch { + _services.Stop (1011, String.Empty); + + throw; + } + + _state = ServerState.Start; + } } private void startReceiving () { - _listener.Start (); - _receiveThread = new Thread (new ThreadStart (receiveRequest)); + try { + _listener.Start (); + } + catch (Exception ex) { + var msg = "The underlying listener has failed to start."; + + throw new InvalidOperationException (msg, ex); + } + + var receiver = new ThreadStart (receiveRequest); + _receiveThread = new Thread (receiver); _receiveThread.IsBackground = true; + _receiveThread.Start (); } + private void stop (ushort code, string reason) + { + lock (_sync) { + if (_state != ServerState.Start) + return; + + _state = ServerState.ShuttingDown; + } + + try { + _services.Stop (code, reason); + } + catch (Exception ex) { + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + } + + try { + var timeout = 5000; + + stopReceiving (timeout); + } + catch (Exception ex) { + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + } + + _state = ServerState.Stop; + } + private void stopReceiving (int millisecondsTimeout) { - _listener.Close (); + _listener.Stop (); _receiveThread.Join (millisecondsTimeout); } - private static bool tryCreateUri (string uriString, out Uri result, out string message) + private static bool tryCreateUri ( + string uriString, + out Uri result, + out string message + ) { result = null; + message = null; var uri = uriString.ToUri (); + if (uri == null) { - message = "An invalid URI string: " + uriString; + message = "An invalid URI string."; + return false; } if (!uri.IsAbsoluteUri) { - message = "Not an absolute URI: " + uriString; + message = "A relative URI."; + return false; } var schm = uri.Scheme; - if (!(schm == "http" || schm == "https")) { - message = "The scheme part isn't 'http' or 'https': " + uriString; + var isHttpSchm = schm == "http" || schm == "https"; + + if (!isHttpSchm) { + message = "The scheme part is not 'http' or 'https'."; + return false; } if (uri.PathAndQuery != "/") { - message = "Includes the path or query component: " + uriString; + message = "It includes either or both path and query components."; + return false; } if (uri.Fragment.Length > 0) { - message = "Includes the fragment component: " + uriString; + message = "It includes the fragment component."; + return false; } if (uri.Port == 0) { - message = "The port part is zero: " + uriString; + message = "The port part is zero."; + return false; } result = uri; - message = String.Empty; return true; } @@ -783,222 +1117,216 @@ private static bool tryCreateUri (string uriString, out Uri result, out string m #region Public Methods /// - /// Adds the WebSocket service with the specified behavior, , - /// and . + /// Adds a WebSocket service with the specified behavior and path. /// - /// + /// /// - /// This method converts to URL-decoded string, - /// and removes '/' from tail end of . + /// A that specifies an absolute path to + /// the service to add. /// /// - /// returns an initialized specified typed - /// instance. + /// / is trimmed from the end of the string if present. /// - /// - /// - /// A that represents the absolute path to the service to add. - /// - /// - /// A Func<T> delegate that references the method used to initialize - /// a new specified typed instance (a new - /// instance). /// /// - /// The type of the behavior of the service to add. The TBehavior must inherit - /// the class. + /// + /// The type of the behavior for the service. + /// + /// + /// It must inherit the class. + /// + /// + /// Also it must have a public parameterless constructor. + /// /// - public void AddWebSocketService (string path, Func initializer) - where TBehavior : WebSocketBehavior + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is not an absolute path. + /// + /// + /// -or- + /// + /// + /// includes either or both + /// query and fragment components. + /// + /// + /// -or- + /// + /// + /// is already in use. + /// + /// + /// + /// is . + /// + public void AddWebSocketService (string path) + where TBehavior : WebSocketBehavior, new () { - var msg = path.CheckIfValidServicePath () ?? - (initializer == null ? "'initializer' is null." : null); - - if (msg != null) { - _logger.Error (msg); - return; - } - - _services.Add (path, initializer); + _services.AddService (path, null); } /// - /// Adds a WebSocket service with the specified behavior and . + /// Adds a WebSocket service with the specified behavior, path, + /// and initializer. /// - /// - /// This method converts to URL-decoded string, - /// and removes '/' from tail end of . - /// /// - /// A that represents the absolute path to the service to add. + /// + /// A that specifies an absolute path to + /// the service to add. + /// + /// + /// / is trimmed from the end of the string if present. + /// /// - /// - /// The type of the behavior of the service to add. The TBehaviorWithNew must inherit - /// the class, and must have a public parameterless - /// constructor. - /// - public void AddWebSocketService (string path) - where TBehaviorWithNew : WebSocketBehavior, new () - { - AddWebSocketService (path, () => new TBehaviorWithNew ()); - } - - /// - /// Gets the contents of the file with the specified . - /// - /// - /// An array of that receives the contents of the file, - /// or if it doesn't exist. - /// - /// - /// A that represents the virtual path to the file to find. + /// + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the service initializes + /// a new session instance. + /// + /// + /// if not necessary. + /// /// - public byte[] GetFile (string path) + /// + /// + /// The type of the behavior for the service. + /// + /// + /// It must inherit the class. + /// + /// + /// Also it must have a public parameterless constructor. + /// + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is not an absolute path. + /// + /// + /// -or- + /// + /// + /// includes either or both + /// query and fragment components. + /// + /// + /// -or- + /// + /// + /// is already in use. + /// + /// + /// + /// is . + /// + public void AddWebSocketService ( + string path, + Action initializer + ) + where TBehavior : WebSocketBehavior, new () { - path = RootPath + path; - if (_windows) - path = path.Replace ("/", "\\"); - - return File.Exists (path) ? File.ReadAllBytes (path) : null; + _services.AddService (path, initializer); } /// - /// Removes the WebSocket service with the specified . + /// Removes a WebSocket service with the specified path. /// /// - /// This method converts to URL-decoded string, - /// and removes '/' from tail end of . + /// The service is stopped with close status 1001 (going away) + /// if the current state of the service is Start. /// /// - /// true if the service is successfully found and removed; otherwise, false. + /// true if the service is successfully found and removed; + /// otherwise, false. /// /// - /// A that represents the absolute path to the service to find. + /// + /// A that specifies an absolute path to + /// the service to remove. + /// + /// + /// / is trimmed from the end of the string if present. + /// /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is not an absolute path. + /// + /// + /// -or- + /// + /// + /// includes either or both + /// query and fragment components. + /// + /// + /// + /// is . + /// public bool RemoveWebSocketService (string path) { - var msg = path.CheckIfValidServicePath (); - if (msg != null) { - _logger.Error (msg); - return false; - } - - return _services.Remove (path); + return _services.RemoveService (path); } /// - /// Starts receiving the HTTP requests. + /// Starts receiving incoming requests. /// + /// + /// This method works if the current state of the server is Ready or Stop. + /// + /// + /// + /// There is no server certificate for secure connection. + /// + /// + /// -or- + /// + /// + /// The underlying has failed to start. + /// + /// public void Start () { - lock (_sync) { - var msg = _state.CheckIfAvailable (true, false, false) ?? checkIfCertificateExists (); - if (msg != null) { - _logger.Error (msg); - return; - } - - _services.Start (); - startReceiving (); + if (_state == ServerState.Start || _state == ServerState.ShuttingDown) + return; - _state = ServerState.Start; - } + start (); } /// - /// Stops receiving the HTTP requests. + /// Stops receiving incoming requests. /// + /// + /// This method works if the current state of the server is Start. + /// public void Stop () { - lock (_sync) { - var msg = _state.CheckIfAvailable (false, true, false); - if (msg != null) { - _logger.Error (msg); - return; - } - - _state = ServerState.ShuttingDown; - } - - _services.Stop (new CloseEventArgs (), true, true); - stopReceiving (5000); - - _state = ServerState.Stop; - } - - /// - /// Stops receiving the HTTP requests with the specified and - /// used to stop the WebSocket services. - /// - /// - /// A that represents the status code indicating the reason for the stop. - /// - /// - /// A that represents the reason for the stop. - /// - public void Stop (ushort code, string reason) - { - lock (_sync) { - var msg = _state.CheckIfAvailable (false, true, false) ?? - WebSocket.CheckCloseParameters (code, reason, false); - - if (msg != null) { - _logger.Error (msg); - return; - } - - _state = ServerState.ShuttingDown; - } - - if (code == (ushort) CloseStatusCode.NoStatus) { - _services.Stop (new CloseEventArgs (), true, true); - } - else { - var send = !code.IsReserved (); - _services.Stop (new CloseEventArgs (code, reason), send, send); - } - - stopReceiving (5000); - - _state = ServerState.Stop; - } - - /// - /// Stops receiving the HTTP requests with the specified and - /// used to stop the WebSocket services. - /// - /// - /// One of the enum values, represents the status code indicating - /// the reason for the stop. - /// - /// - /// A that represents the reason for the stop. - /// - public void Stop (CloseStatusCode code, string reason) - { - lock (_sync) { - var msg = _state.CheckIfAvailable (false, true, false) ?? - WebSocket.CheckCloseParameters (code, reason, false); - - if (msg != null) { - _logger.Error (msg); - return; - } - - _state = ServerState.ShuttingDown; - } - - if (code == CloseStatusCode.NoStatus) { - _services.Stop (new CloseEventArgs (), true, true); - } - else { - var send = !code.IsReserved (); - _services.Stop (new CloseEventArgs (code, reason), send, send); - } - - stopReceiving (5000); + if (_state != ServerState.Start) + return; - _state = ServerState.Stop; + stop (1001, String.Empty); } #endregion diff --git a/websocket-sharp/Server/IWebSocketSession.cs b/websocket-sharp/Server/IWebSocketSession.cs index 530740a9c..4ddc84aca 100644 --- a/websocket-sharp/Server/IWebSocketSession.cs +++ b/websocket-sharp/Server/IWebSocketSession.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2013-2014 sta.blockhead + * Copyright (c) 2013-2022 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 @@ -27,25 +27,16 @@ #endregion using System; -using WebSocketSharp.Net.WebSockets; namespace WebSocketSharp.Server { /// - /// Exposes the properties used to access the information in a session in a WebSocket service. + /// Exposes the access to the information in a WebSocket session. /// public interface IWebSocketSession { #region Properties - /// - /// Gets the information in the connection request to the WebSocket service. - /// - /// - /// A that provides the access to the connection request. - /// - WebSocketContext Context { get; } - /// /// Gets the unique ID of the session. /// @@ -54,30 +45,22 @@ public interface IWebSocketSession /// string ID { get; } - /// - /// Gets the WebSocket subprotocol used in the session. - /// - /// - /// A that represents the subprotocol if any. - /// - string Protocol { get; } - /// /// Gets the time that the session has started. /// /// - /// A that represents the time that the session has started. + /// A that represents the time that the session + /// has started. /// DateTime StartTime { get; } /// - /// Gets the state of the used in the session. + /// Gets the WebSocket interface for the session. /// /// - /// One of the enum values, indicates the state of - /// the used in the session. + /// A that represents the interface. /// - WebSocketState State { get; } + WebSocket WebSocket { get; } #endregion } diff --git a/websocket-sharp/Server/WebSocketBehavior.cs b/websocket-sharp/Server/WebSocketBehavior.cs index 5ae6c8f6e..a0befe74d 100644 --- a/websocket-sharp/Server/WebSocketBehavior.cs +++ b/websocket-sharp/Server/WebSocketBehavior.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2025 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 @@ -27,33 +27,40 @@ #endregion using System; +using System.Collections.Specialized; using System.IO; +using System.Security.Principal; using WebSocketSharp.Net; using WebSocketSharp.Net.WebSockets; namespace WebSocketSharp.Server { /// - /// Exposes the methods and properties used to define the behavior of a WebSocket service - /// provided by the or . + /// Exposes a set of methods and properties used to define the behavior of + /// a WebSocket service provided by the or + /// class. /// /// - /// The WebSocketBehavior class is an abstract class. + /// This class is an abstract class. /// public abstract class WebSocketBehavior : IWebSocketSession { #region Private Fields - private WebSocketContext _context; - private Func _cookiesValidator; - private bool _emitOnPing; - private string _id; - private bool _ignoreExtensions; - private Func _originValidator; - private string _protocol; - private WebSocketSessionManager _sessions; - private DateTime _startTime; - private WebSocket _websocket; + private WebSocketContext _context; + private Action _cookiesResponder; + private bool _emitOnPing; + private Func _hostValidator; + private string _id; + private bool _ignoreExtensions; + private bool _noDelay; + private Func _originValidator; + private string _protocol; + private bool _registered; + private WebSocketSessionManager _sessions; + private DateTime _startTime; + private Action _userHeadersResponder; + private WebSocket _websocket; #endregion @@ -72,193 +79,476 @@ protected WebSocketBehavior () #region Protected Properties /// - /// Gets the logging functions. + /// Gets the HTTP headers for a session. /// /// - /// A that provides the logging functions, - /// or if the WebSocket connection isn't established. + /// A that contains the headers + /// included in the WebSocket handshake request. /// - protected Logger Log { + /// + /// The get operation is not available when the session has not started yet. + /// + protected NameValueCollection Headers { get { - return _websocket != null ? _websocket.Log : null; + if (!_registered) { + var msg = "The session has not started yet."; + + throw new InvalidOperationException (msg); + } + + return _context.Headers; + } + } + + /// + /// Gets a value indicating whether the communication is possible for + /// a session. + /// + /// + /// true if the communication is possible; otherwise, false. + /// + /// + /// The get operation is not available when the session has not started yet. + /// + protected bool IsAlive { + get { + if (!_registered) { + var msg = "The session has not started yet."; + + throw new InvalidOperationException (msg); + } + + return _websocket.IsAlive; + } + } + + /// + /// Gets the query string for a session. + /// + /// + /// + /// A that contains the query + /// parameters included in the WebSocket handshake request. + /// + /// + /// An empty collection if not included. + /// + /// + /// + /// The get operation is not available when the session has not started yet. + /// + protected NameValueCollection QueryString { + get { + if (!_registered) { + var msg = "The session has not started yet."; + + throw new InvalidOperationException (msg); + } + + return _context.QueryString; + } + } + + /// + /// Gets the current state of the WebSocket interface for a session. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It indicates the current state of the interface. + /// + /// + /// + /// The get operation is not available when the session has not started yet. + /// + protected WebSocketState ReadyState { + get { + if (!_registered) { + var msg = "The session has not started yet."; + + throw new InvalidOperationException (msg); + } + + return _websocket.ReadyState; } } - + /// - /// Gets the access to the sessions in the WebSocket service. + /// Gets the management function for the sessions in the service. /// /// - /// A that provides the access to the sessions, - /// or if the WebSocket connection isn't established. + /// A that manages the sessions in + /// the service. /// + /// + /// The get operation is not available when the session has not started yet. + /// protected WebSocketSessionManager Sessions { get { + if (!_registered) { + var msg = "The session has not started yet."; + + throw new InvalidOperationException (msg); + } + return _sessions; } } - #endregion + /// + /// Gets the client information for a session. + /// + /// + /// + /// A instance that represents identity, + /// authentication, and security roles for the client. + /// + /// + /// if the client is not authenticated. + /// + /// + /// + /// The get operation is not available when the session has not started yet. + /// + protected IPrincipal User { + get { + if (!_registered) { + var msg = "The session has not started yet."; - #region Public Properties + throw new InvalidOperationException (msg); + } + + return _context.User; + } + } /// - /// Gets the information in a connection request to the WebSocket service. + /// Gets the client endpoint for a session. /// /// - /// A that provides the access to the connection request, - /// or if the WebSocket connection isn't established. + /// A that represents the client + /// IP address and port number. /// - public WebSocketContext Context { + /// + /// The get operation is not available when the session has not started yet. + /// + protected System.Net.IPEndPoint UserEndPoint { get { - return _context; + if (!_registered) { + var msg = "The session has not started yet."; + + throw new InvalidOperationException (msg); + } + + return _context.UserEndPoint; } } + #endregion + + #region Public Properties + /// - /// Gets or sets the delegate called to validate the HTTP cookies included in - /// a connection request to the WebSocket service. + /// Gets or sets the delegate used to respond to the HTTP cookies. /// - /// - /// This delegate is called when the used in a session validates - /// the connection request. - /// /// /// - /// A Func<CookieCollection, CookieCollection, bool> delegate that references - /// the method(s) used to validate the cookies. 1st passed to - /// this delegate contains the cookies to validate if any. 2nd - /// passed to this delegate receives the cookies to send to the client. + /// A + /// delegate. /// /// - /// This delegate should return true if the cookies are valid. + /// It represents the delegate called when the WebSocket interface + /// for a session respond to the handshake request. /// /// - /// The default value is , and it does nothing to validate. + /// 1st parameter passed to the delegate + /// contains the cookies included in the handshake request if any. + /// + /// + /// 2nd parameter passed to the delegate + /// holds the cookies to send to the client. + /// + /// + /// if not necessary. + /// + /// + /// The default value is . /// /// - public Func CookiesValidator { + /// + /// The set operation is not available when the session has already started. + /// + public Action CookiesResponder { get { - return _cookiesValidator; + return _cookiesResponder; } set { - _cookiesValidator = value; + if (_registered) { + var msg = "The session has already started."; + + throw new InvalidOperationException (msg); + } + + _cookiesResponder = value; } } /// - /// Gets or sets a value indicating whether the used in a session emits - /// a event when receives a Ping. + /// Gets or sets a value indicating whether the WebSocket interface for + /// a session emits the message event when it receives a ping. /// /// - /// true if the emits a event - /// when receives a Ping; otherwise, false. The default value is false. + /// + /// true if the interface emits the message event when it receives + /// a ping; otherwise, false. + /// + /// + /// The default value is false. + /// /// + /// + /// The set operation is not available when the session has already started. + /// public bool EmitOnPing { get { - return _websocket != null ? _websocket.EmitOnPing : _emitOnPing; + return _emitOnPing; } set { - if (_websocket != null) { - _websocket.EmitOnPing = value; - return; + if (_registered) { + var msg = "The session has already started."; + + throw new InvalidOperationException (msg); } _emitOnPing = value; } } + /// + /// Gets or sets the delegate used to validate the Host header. + /// + /// + /// + /// A delegate. + /// + /// + /// It represents the delegate called when the WebSocket interface + /// for a session validates the handshake request. + /// + /// + /// The parameter passed to the delegate is + /// the value of the Host header. + /// + /// + /// The method invoked by the delegate must return true + /// if the header value is valid. + /// + /// + /// if not necessary. + /// + /// + /// The default value is . + /// + /// + /// + /// The set operation is not available when the session has already started. + /// + public Func HostValidator { + get { + return _hostValidator; + } + + set { + if (_registered) { + var msg = "The session has already started."; + + throw new InvalidOperationException (msg); + } + + _hostValidator = value; + } + } + /// /// Gets the unique ID of a session. /// /// - /// A that represents the unique ID of the session, - /// or if the WebSocket connection isn't established. + /// A that represents the unique ID of the session. /// public string ID { get { + if (_id == null) + _id = WebSocketSessionManager.CreateID (); + return _id; } } /// - /// Gets or sets a value indicating whether the WebSocket service ignores - /// the Sec-WebSocket-Extensions header included in a connection request. + /// Gets or sets a value indicating whether the WebSocket interface for + /// a session ignores the Sec-WebSocket-Extensions header. /// /// - /// true if the WebSocket service ignores the extensions; - /// otherwise, false. The default value is false. + /// + /// true if the interface ignores the extensions requested + /// from the client; otherwise, false. + /// + /// + /// The default value is false. + /// /// + /// + /// The set operation is not available when the session has already started. + /// public bool IgnoreExtensions { get { return _ignoreExtensions; } set { + if (_registered) { + var msg = "The session has already started."; + + throw new InvalidOperationException (msg); + } + _ignoreExtensions = value; } } /// - /// Gets or sets the delegate called to validate the Origin header included in - /// a connection request to the WebSocket service. + /// Gets or sets a value indicating whether the underlying TCP socket of + /// the WebSocket interface for a session disables a delay when send or + /// receive buffer is not full. + /// + /// + /// + /// true if the delay is disabled; otherwise, false. + /// + /// + /// The default value is false. + /// + /// + /// + /// + /// The set operation is not available when the session has already started. + /// + public bool NoDelay { + get { + return _noDelay; + } + + set { + if (_registered) { + var msg = "The session has already started."; + + throw new InvalidOperationException (msg); + } + + _noDelay = value; + } + } + + /// + /// Gets or sets the delegate used to validate the Origin header. /// - /// - /// This delegate is called when the used in a session validates - /// the connection request. - /// /// /// - /// A Func<string, bool> delegate that references the method(s) used to validate - /// the origin header. A passed to this delegate represents the value of - /// the origin header to validate if any. + /// A delegate. + /// + /// + /// It represents the delegate called when the WebSocket interface + /// for a session validates the handshake request. + /// + /// + /// The parameter passed to the delegate is + /// the value of the Origin header or if + /// the header is not present. + /// + /// + /// The method invoked by the delegate must return true + /// if the header value is valid. /// /// - /// This delegate should return true if the origin header is valid. + /// if not necessary. /// /// - /// The default value is , and it does nothing to validate. + /// The default value is . /// /// + /// + /// The set operation is not available when the session has already started. + /// public Func OriginValidator { get { return _originValidator; } set { + if (_registered) { + var msg = "The session has already started."; + + throw new InvalidOperationException (msg); + } + _originValidator = value; } } /// - /// Gets or sets the WebSocket subprotocol used in the WebSocket service. + /// Gets or sets the name of the WebSocket subprotocol for a session. /// - /// - /// Set operation of this property is available before the WebSocket connection has - /// been established. - /// /// /// - /// A that represents the subprotocol if any. - /// The default value is . + /// A that represents the name of the subprotocol. /// /// - /// The value to set must be a token defined in - /// RFC 2616. + /// The value specified for a set operation must be a token defined in + /// + /// RFC 2616. + /// + /// + /// The value is initialized if not requested. + /// + /// + /// The default value is an empty string. /// /// + /// + /// The value specified for a set operation is not a token. + /// + /// + /// The set operation is not available when the session has already started. + /// public string Protocol { get { - return _websocket != null ? _websocket.Protocol : (_protocol ?? String.Empty); + return _registered + ? _websocket.Protocol + : (_protocol ?? String.Empty); } set { - if (State != WebSocketState.Connecting) - return; + if (_registered) { + var msg = "The session has already started."; + + throw new InvalidOperationException (msg); + } + + if (value.IsNullOrEmpty ()) { + _protocol = null; - if (value != null && (value.Length == 0 || !value.IsToken ())) return; + } + + if (!value.IsToken ()) { + var msg = "Not a token."; + + throw new ArgumentException (msg, "value"); + } _protocol = value; } @@ -268,8 +558,13 @@ public string Protocol { /// Gets the time that a session has started. /// /// - /// A that represents the time that the session has started, - /// or if the WebSocket connection isn't established. + /// + /// A that represents the time that the session + /// has started. + /// + /// + /// when the session has not started yet. + /// /// public DateTime StartTime { get { @@ -278,15 +573,48 @@ public DateTime StartTime { } /// - /// Gets the state of the used in a session. + /// Gets or sets the delegate used to respond to the user headers. /// /// - /// One of the enum values, indicates the state of - /// the . + /// + /// A + /// delegate. + /// + /// + /// It represents the delegate called when the WebSocket interface + /// for a session respond to the handshake request. + /// + /// + /// 1st parameter passed to the delegate + /// contains the HTTP headers included in the handshake request. + /// + /// + /// 2nd parameter passed to the delegate + /// holds the user headers to send to the client. + /// + /// + /// if not necessary. + /// + /// + /// The default value is . + /// /// - public WebSocketState State { + /// + /// The set operation is not available when the session has already started. + /// + public Action UserHeadersResponder { get { - return _websocket != null ? _websocket.ReadyState : WebSocketState.Connecting; + return _userHeadersResponder; + } + + set { + if (_registered) { + var msg = "The session has already started."; + + throw new InvalidOperationException (msg); + } + + _userHeadersResponder = value; } } @@ -294,22 +622,34 @@ public WebSocketState State { #region Private Methods - private string checkIfValidConnectionRequest (WebSocketContext context) + private string checkHandshakeRequest (WebSocketContext context) { - return _originValidator != null && !_originValidator (context.Origin) - ? "Invalid Origin header." - : _cookiesValidator != null && - !_cookiesValidator (context.CookieCollection, context.WebSocket.CookieCollection) - ? "Invalid Cookies." - : null; + if (_hostValidator != null) { + if (!_hostValidator (context.Host)) { + var msg = "The Host header is invalid."; + + return msg; + } + } + + if (_originValidator != null) { + if (!_originValidator (context.Origin)) { + var msg = "The Origin header is non-existent or invalid."; + + return msg; + } + } + + return null; } private void onClose (object sender, CloseEventArgs e) { - if (_id == null) + if (!_registered) return; _sessions.Remove (_id); + OnClose (e); } @@ -325,48 +665,75 @@ private void onMessage (object sender, MessageEventArgs e) private void onOpen (object sender, EventArgs e) { - _id = _sessions.Add (this); - if (_id == null) { + _registered = _sessions.Add (this); + + if (!_registered) { _websocket.Close (CloseStatusCode.Away); + return; } _startTime = DateTime.Now; + OnOpen (); } - #endregion + private void respondToHandshakeRequest (WebSocketContext context) + { + if (_cookiesResponder != null) { + var reqCookies = context.CookieCollection; + var resCookies = context.WebSocket.Cookies; - #region Internal Methods + _cookiesResponder (reqCookies, resCookies); + } - internal void Start (WebSocketContext context, WebSocketSessionManager sessions) - { - if (_websocket != null) { - _websocket.Log.Error ("This session has already been started."); - context.WebSocket.Close (HttpStatusCode.ServiceUnavailable); + if (_userHeadersResponder != null) { + var reqHeaders = context.Headers; + var userHeaders = context.WebSocket.UserHeaders; - return; + _userHeadersResponder (reqHeaders, userHeaders); } + } + + #endregion + + #region Internal Methods + internal void Start ( + WebSocketContext context, + WebSocketSessionManager sessions + ) + { _context = context; _sessions = sessions; _websocket = context.WebSocket; - _websocket.CustomHandshakeRequestChecker = checkIfValidConnectionRequest; - _websocket.EmitOnPing = _emitOnPing; - _websocket.IgnoreExtensions = _ignoreExtensions; - _websocket.Protocol = _protocol; + _websocket.CustomHandshakeRequestChecker = checkHandshakeRequest; + _websocket.CustomHandshakeRequestResponder = respondToHandshakeRequest; + + if (_emitOnPing) + _websocket.EmitOnPing = true; + + if (_ignoreExtensions) + _websocket.IgnoreExtensions = true; + + if (_noDelay) + _websocket.NoDelay = true; + + if (_protocol != null) + _websocket.Protocol = _protocol; var waitTime = sessions.WaitTime; + if (waitTime != _websocket.WaitTime) _websocket.WaitTime = waitTime; - _websocket.OnOpen += onOpen; - _websocket.OnMessage += onMessage; - _websocket.OnError += onError; _websocket.OnClose += onClose; + _websocket.OnError += onError; + _websocket.OnMessage += onMessage; + _websocket.OnOpen += onOpen; - _websocket.InternalAccept (); + _websocket.Accept (); } #endregion @@ -374,213 +741,866 @@ internal void Start (WebSocketContext context, WebSocketSessionManager sessions) #region Protected Methods /// - /// Calls the method with the specified and - /// . + /// Closes the WebSocket connection for a session. /// /// - /// This method doesn't call the method if is - /// or empty. + /// This method does nothing when the current state of the WebSocket + /// interface is Closing or Closed. /// - /// - /// A that represents the error message. - /// - /// - /// An instance that represents the cause of the error if any. - /// - protected void Error (string message, Exception exception) + /// + /// This method is not available when the session has not started yet. + /// + protected void Close () { - if (message != null && message.Length > 0) - OnError (new ErrorEventArgs (message, exception)); - } - - /// - /// Called when the WebSocket connection used in a session has been closed. - /// - /// - /// A that represents the event data passed to - /// a event. - /// - protected virtual void OnClose (CloseEventArgs e) - { - } - - /// - /// Called when the used in a session gets an error. - /// - /// - /// A that represents the event data passed to - /// a event. - /// - protected virtual void OnError (ErrorEventArgs e) - { - } + if (!_registered) { + var msg = "The session has not started yet."; - /// - /// Called when the used in a session receives a message. - /// - /// - /// A that represents the event data passed to - /// a event. - /// - protected virtual void OnMessage (MessageEventArgs e) - { - } + throw new InvalidOperationException (msg); + } - /// - /// Called when the WebSocket connection used in a session has been established. - /// - protected virtual void OnOpen () - { + _websocket.Close (); } /// - /// Sends binary to the client on a session. + /// Closes the WebSocket connection for a session with the specified + /// status code and reason. /// /// - /// This method is available after the WebSocket connection has been established. + /// This method does nothing when the current state of the WebSocket + /// interface is Closing or Closed. /// - /// - /// An array of that represents the binary data to send. - /// - protected void Send (byte[] data) + /// + /// + /// A that specifies the status code indicating + /// the reason for the close. + /// + /// + /// The status codes are defined in + /// + /// Section 7.4 of RFC 6455. + /// + /// + /// + /// + /// A that specifies the reason for the close. + /// + /// + /// Its size must be 123 bytes or less in UTF-8. + /// + /// + /// + /// + /// is 1010 (mandatory extension). + /// + /// + /// -or- + /// + /// + /// is 1005 (no status) and + /// is specified. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// + /// is less than 1000 or greater than 4999. + /// + /// + /// -or- + /// + /// + /// The size of is greater than 123 bytes. + /// + /// + /// + /// This method is not available when the session has not started yet. + /// + protected void Close (ushort code, string reason) { - if (_websocket != null) - _websocket.Send (data); + if (!_registered) { + var msg = "The session has not started yet."; + + throw new InvalidOperationException (msg); + } + + _websocket.Close (code, reason); } /// - /// Sends the specified as binary data to the client on a session. + /// Closes the WebSocket connection for a session with the specified + /// status code and reason. /// /// - /// This method is available after the WebSocket connection has been established. + /// This method does nothing when the current state of the WebSocket + /// interface is Closing or Closed. /// - /// - /// A that represents the file to send. + /// + /// + /// One of the enum values. + /// + /// + /// It specifies the status code indicating the reason for the close. + /// + /// + /// + /// + /// A that specifies the reason for the close. + /// + /// + /// Its size must be 123 bytes or less in UTF-8. + /// /// - protected void Send (FileInfo file) + /// + /// + /// is an undefined enum value. + /// + /// + /// -or- + /// + /// + /// is . + /// + /// + /// -or- + /// + /// + /// is and + /// is specified. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// The size of is greater than 123 bytes. + /// + /// + /// This method is not available when the session has not started yet. + /// + protected void Close (CloseStatusCode code, string reason) { - if (_websocket != null) - _websocket.Send (file); + if (!_registered) { + var msg = "The session has not started yet."; + + throw new InvalidOperationException (msg); + } + + _websocket.Close (code, reason); } /// - /// Sends text to the client on a session. + /// Closes the WebSocket connection for a session asynchronously. /// /// - /// This method is available after the WebSocket connection has been established. + /// + /// This method does not wait for the close to be complete. + /// + /// + /// This method does nothing when the current state of the WebSocket + /// interface is Closing or Closed. + /// /// - /// - /// A that represents the text data to send. + /// + /// This method is not available when the session has not started yet. + /// + protected void CloseAsync () + { + if (!_registered) { + var msg = "The session has not started yet."; + + throw new InvalidOperationException (msg); + } + + _websocket.CloseAsync (); + } + + /// + /// Closes the WebSocket connection for a session asynchronously with + /// the specified status code and reason. + /// + /// + /// + /// This method does not wait for the close to be complete. + /// + /// + /// This method does nothing when the current state of the WebSocket + /// interface is Closing or Closed. + /// + /// + /// + /// + /// A that specifies the status code indicating + /// the reason for the close. + /// + /// + /// The status codes are defined in + /// + /// Section 7.4 of RFC 6455. + /// /// - protected void Send (string data) + /// + /// + /// A that specifies the reason for the close. + /// + /// + /// Its size must be 123 bytes or less in UTF-8. + /// + /// + /// + /// + /// is 1010 (mandatory extension). + /// + /// + /// -or- + /// + /// + /// is 1005 (no status) and + /// is specified. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// + /// is less than 1000 or greater than 4999. + /// + /// + /// -or- + /// + /// + /// The size of is greater than 123 bytes. + /// + /// + /// + /// This method is not available when the session has not started yet. + /// + protected void CloseAsync (ushort code, string reason) { - if (_websocket != null) - _websocket.Send (data); + if (!_registered) { + var msg = "The session has not started yet."; + + throw new InvalidOperationException (msg); + } + + _websocket.CloseAsync (code, reason); } /// - /// Sends binary asynchronously to the client on a session. + /// Closes the WebSocket connection for a session asynchronously with + /// the specified status code and reason. /// /// /// - /// This method is available after the WebSocket connection has been established. + /// This method does not wait for the close to be complete. /// /// - /// This method doesn't wait for the send to be complete. + /// This method does nothing when the current state of the WebSocket + /// interface is Closing or Closed. /// /// + /// + /// + /// One of the enum values. + /// + /// + /// It specifies the status code indicating the reason for the close. + /// + /// + /// + /// + /// A that specifies the reason for the close. + /// + /// + /// Its size must be 123 bytes or less in UTF-8. + /// + /// + /// + /// + /// is an undefined enum value. + /// + /// + /// -or- + /// + /// + /// is . + /// + /// + /// -or- + /// + /// + /// is and + /// is specified. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// The size of is greater than 123 bytes. + /// + /// + /// This method is not available when the session has not started yet. + /// + protected void CloseAsync (CloseStatusCode code, string reason) + { + if (!_registered) { + var msg = "The session has not started yet."; + + throw new InvalidOperationException (msg); + } + + _websocket.CloseAsync (code, reason); + } + + /// + /// Called when the WebSocket connection for a session has been closed. + /// + /// + /// A that represents the close event data + /// passed from the WebSocket interface. + /// + protected virtual void OnClose (CloseEventArgs e) + { + } + + /// + /// Called when the WebSocket interface for a session gets an error. + /// + /// + /// A that represents the error event data + /// passed from the interface. + /// + protected virtual void OnError (ErrorEventArgs e) + { + } + + /// + /// Called when the WebSocket interface for a session receives a message. + /// + /// + /// A that represents the message event data + /// passed from the interface. + /// + protected virtual void OnMessage (MessageEventArgs e) + { + } + + /// + /// Called when the WebSocket connection for a session has been established. + /// + protected virtual void OnOpen () + { + } + + /// + /// Sends a ping to the client for a session. + /// + /// + /// true if the send has successfully done and a pong has been + /// received within a time; otherwise, false. + /// + /// + /// This method is not available when the session has not started yet. + /// + protected bool Ping () + { + if (!_registered) { + var msg = "The session has not started yet."; + + throw new InvalidOperationException (msg); + } + + return _websocket.Ping (); + } + + /// + /// Sends a ping with the specified message to the client for a session. + /// + /// + /// true if the send has successfully done and a pong has been + /// received within a time; otherwise, false. + /// + /// + /// + /// A that specifies the message to send. + /// + /// + /// Its size must be 125 bytes or less in UTF-8. + /// + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// The size of is greater than 125 bytes. + /// + /// + /// This method is not available when the session has not started yet. + /// + protected bool Ping (string message) + { + if (!_registered) { + var msg = "The session has not started yet."; + + throw new InvalidOperationException (msg); + } + + return _websocket.Ping (message); + } + + /// + /// Sends the specified data to the client for a session. + /// /// - /// An array of that represents the binary data to send. + /// An array of that specifies the binary data to send. /// - /// - /// An Action<bool> delegate that references the method(s) called when - /// the send is complete. A passed to this delegate is true - /// if the send is complete successfully. + /// + /// is . + /// + /// + /// + /// This method is not available when the session has not started yet. + /// + /// + /// -or- + /// + /// + /// This method is not available when the current state of the WebSocket + /// interface is not Open. + /// + /// + protected void Send (byte[] data) + { + if (!_registered) { + var msg = "The session has not started yet."; + + throw new InvalidOperationException (msg); + } + + _websocket.Send (data); + } + + /// + /// Sends the specified file to the client for a session. + /// + /// + /// + /// A that specifies the file to send. + /// + /// + /// The file is sent as the binary data. + /// /// - protected void SendAsync (byte[] data, Action completed) + /// + /// + /// The file does not exist. + /// + /// + /// -or- + /// + /// + /// The file could not be opened. + /// + /// + /// + /// is . + /// + /// + /// + /// This method is not available when the session has not started yet. + /// + /// + /// -or- + /// + /// + /// This method is not available when the current state of the WebSocket + /// interface is not Open. + /// + /// + protected void Send (FileInfo fileInfo) { - if (_websocket != null) - _websocket.SendAsync (data, completed); + if (!_registered) { + var msg = "The session has not started yet."; + + throw new InvalidOperationException (msg); + } + + _websocket.Send (fileInfo); } /// - /// Sends the specified as binary data asynchronously to - /// the client on a session. + /// Sends the specified data to the client for a session. /// - /// + /// + /// A that specifies the text data to send. + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// is . + /// + /// + /// + /// This method is not available when the session has not started yet. + /// + /// + /// -or- + /// + /// + /// This method is not available when the current state of the WebSocket + /// interface is not Open. + /// + /// + protected void Send (string data) + { + if (!_registered) { + var msg = "The session has not started yet."; + + throw new InvalidOperationException (msg); + } + + _websocket.Send (data); + } + + /// + /// Sends the data from the specified stream instance to the client for + /// a session. + /// + /// + /// + /// 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. + /// + /// + /// + /// cannot be read. + /// + /// + /// -or- + /// /// - /// This method is available after the WebSocket connection has been established. + /// is less than 1. /// /// - /// This method doesn't wait for the send to be complete. + /// -or- /// + /// + /// No data could be read from . + /// + /// + /// + /// is . + /// + /// + /// + /// This method is not available when the session has not started yet. + /// + /// + /// -or- + /// + /// + /// This method is not available when the current state of the WebSocket + /// interface is not Open. + /// + /// + protected void Send (Stream stream, int length) + { + if (!_registered) { + var msg = "The session has not started yet."; + + throw new InvalidOperationException (msg); + } + + _websocket.Send (stream, length); + } + + /// + /// Sends the specified data to the client for a session asynchronously. + /// + /// + /// This method does not wait for the send to be complete. /// - /// - /// A that represents the file to send. + /// + /// An array of that specifies the binary data to send. /// /// - /// An Action<bool> delegate that references the method(s) called when - /// the send is complete. A passed to this delegate is true - /// if the send is complete successfully. + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the send is complete. + /// + /// + /// The parameter passed to the delegate is true + /// if the send has successfully done; otherwise, false. + /// + /// + /// if not necessary. + /// /// - protected void SendAsync (FileInfo file, Action completed) + /// + /// is . + /// + /// + /// + /// This method is not available when the session has not started yet. + /// + /// + /// -or- + /// + /// + /// This method is not available when the current state of the WebSocket + /// interface is not Open. + /// + /// + protected void SendAsync (byte[] data, Action completed) { - if (_websocket != null) - _websocket.SendAsync (file, completed); + if (!_registered) { + var msg = "The session has not started yet."; + + throw new InvalidOperationException (msg); + } + + _websocket.SendAsync (data, completed); } /// - /// Sends text asynchronously to the client on a session. + /// Sends the specified file to the client for a session asynchronously. /// /// + /// 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 delegate. + /// + /// + /// It specifies the delegate called when the send is complete. + /// + /// + /// The parameter passed to the delegate is true + /// if the send has successfully done; otherwise, false. + /// + /// + /// if not necessary. + /// + /// + /// + /// + /// The file does not exist. + /// + /// + /// -or- + /// + /// + /// The file could not be opened. + /// + /// + /// + /// is . + /// + /// + /// + /// This method is not available when the session has not started yet. + /// /// - /// This method is available after the WebSocket connection has been established. + /// -or- /// /// - /// This method doesn't wait for the send to be complete. + /// This method is not available when the current state of the WebSocket + /// interface is not Open. /// + /// + protected void SendAsync (FileInfo fileInfo, Action completed) + { + if (!_registered) { + var msg = "The session has not started yet."; + + throw new InvalidOperationException (msg); + } + + _websocket.SendAsync (fileInfo, completed); + } + + /// + /// Sends the specified data to the client for a session asynchronously. + /// + /// + /// This method does not wait for the send to be complete. /// /// - /// A that represents the text data to send. + /// A that specifies the text data to send. /// /// - /// An Action<bool> delegate that references the method(s) called when - /// the send is complete. A passed to this delegate is true - /// if the send is complete successfully. + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the send is complete. + /// + /// + /// The parameter passed to the delegate is true + /// if the send has successfully done; otherwise, false. + /// + /// + /// if not necessary. + /// /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// is . + /// + /// + /// + /// This method is not available when the session has not started yet. + /// + /// + /// -or- + /// + /// + /// This method is not available when the current state of the WebSocket + /// interface is not Open. + /// + /// protected void SendAsync (string data, Action completed) { - if (_websocket != null) - _websocket.SendAsync (data, completed); + if (!_registered) { + var msg = "The session has not started yet."; + + throw new InvalidOperationException (msg); + } + + _websocket.SendAsync (data, completed); } /// - /// Sends binary data from the specified asynchronously to - /// the client on a session. + /// Sends the data from the specified stream instance to the client for + /// a session asynchronously. /// /// + /// This method does not wait for the send to be complete. + /// + /// /// - /// This method is available after the WebSocket connection has been established. + /// A instance from which to read the data to send. /// /// - /// This method doesn't wait for the send to be complete. + /// The data is sent as the binary data. /// - /// - /// - /// A from which contains the binary data to send. /// /// - /// An that represents the number of bytes to send. + /// An that specifies the number of bytes to send. /// /// - /// An Action<bool> delegate that references the method(s) called when - /// the send is complete. A passed to this delegate is true - /// if the send is complete successfully. + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the send is complete. + /// + /// + /// The parameter passed to the delegate is true + /// if the send has successfully done; otherwise, false. + /// + /// + /// if not necessary. + /// /// + /// + /// + /// cannot be read. + /// + /// + /// -or- + /// + /// + /// is less than 1. + /// + /// + /// -or- + /// + /// + /// No data could be read from . + /// + /// + /// + /// is . + /// + /// + /// + /// This method is not available when the session has not started yet. + /// + /// + /// -or- + /// + /// + /// This method is not available when the current state of the WebSocket + /// interface is not Open. + /// + /// protected void SendAsync (Stream stream, int length, Action completed) { - if (_websocket != null) - _websocket.SendAsync (stream, length, completed); + if (!_registered) { + var msg = "The session has not started yet."; + + throw new InvalidOperationException (msg); + } + + _websocket.SendAsync (stream, length, completed); + } + + #endregion + + #region Explicit Interface Implementations + + /// + /// Gets the WebSocket interface for a session. + /// + /// + /// + /// A that represents + /// the WebSocket interface. + /// + /// + /// when the session has not started yet. + /// + /// + WebSocket IWebSocketSession.WebSocket { + get { + return _websocket; + } } #endregion diff --git a/websocket-sharp/Server/WebSocketServer.cs b/websocket-sharp/Server/WebSocketServer.cs index 457df8c6d..50f03f020 100644 --- a/websocket-sharp/Server/WebSocketServer.cs +++ b/websocket-sharp/Server/WebSocketServer.cs @@ -2,11 +2,9 @@ /* * WebSocketServer.cs * - * A C# implementation of the WebSocket protocol server. - * * The MIT License * - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2024 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 @@ -54,7 +52,7 @@ namespace WebSocketSharp.Server /// Provides a WebSocket protocol server. /// /// - /// The WebSocketServer class can provide multiple WebSocket services. + /// This class can provide multiple WebSocket services. /// public class WebSocketServer { @@ -62,20 +60,32 @@ public class WebSocketServer private System.Net.IPAddress _address; private AuthenticationSchemes _authSchemes; - private Func _credFinder; - private bool _dnsStyle; + private static readonly string _defaultRealm; private string _hostname; + private bool _isDnsStyle; + private bool _isSecure; private TcpListener _listener; - private Logger _logger; + private Logger _log; private int _port; private string _realm; + private string _realmInUse; private Thread _receiveThread; private bool _reuseAddress; - private bool _secure; private WebSocketServiceManager _services; private ServerSslConfiguration _sslConfig; + private ServerSslConfiguration _sslConfigInUse; private volatile ServerState _state; private object _sync; + private Func _userCredFinder; + + #endregion + + #region Static Constructor + + static WebSocketServer () + { + _defaultRealm = "SECRET AREA"; + } #endregion @@ -85,32 +95,35 @@ public class WebSocketServer /// Initializes a new instance of the class. /// /// - /// An instance initialized by this constructor listens for the incoming connection requests on - /// port 80. + /// The new instance listens for incoming handshake requests on + /// and port 80. /// public WebSocketServer () { - init (null, System.Net.IPAddress.Any, 80, false); + var addr = System.Net.IPAddress.Any; + + init (addr.ToString (), addr, 80, false); } /// - /// Initializes a new instance of the class with - /// the specified . + /// Initializes a new instance of the class + /// with the specified port. /// /// /// - /// An instance initialized by this constructor listens for the incoming connection requests - /// on . + /// The new instance listens for incoming handshake requests on + /// and . /// /// - /// If is 443, that instance provides a secure connection. + /// It provides secure connections if is 443. /// /// /// - /// An that represents the port number on which to listen. + /// An that specifies the number of the port on which + /// to listen. /// /// - /// isn't between 1 and 65535 inclusive. + /// is less than 1 or greater than 65535. /// public WebSocketServer (int port) : this (port, port == 443) @@ -118,29 +131,30 @@ public WebSocketServer (int port) } /// - /// Initializes a new instance of the class with - /// the specified WebSocket URL. + /// Initializes a new instance of the class + /// with the specified URL. /// /// /// - /// An instance initialized by this constructor listens for the incoming connection requests - /// on the host name and port in . + /// The new instance listens for incoming handshake requests on + /// the IP address and port of . + /// + /// + /// Either port 80 or 443 is used if includes + /// no port. Port 443 is used if the scheme of + /// is wss; otherwise, port 80 is used. /// /// - /// If doesn't include a port, either port 80 or 443 is used on - /// which to listen. It's determined by the scheme (ws or wss) in . - /// (Port 80 if the scheme is ws.) + /// The new instance provides secure connections if the scheme of + /// is wss. /// /// /// - /// A that represents the WebSocket URL of the server. + /// A that specifies the WebSocket URL of the server. /// - /// - /// is . - /// /// /// - /// is empty. + /// is an empty string. /// /// /// -or- @@ -149,6 +163,9 @@ public WebSocketServer (int port) /// is invalid. /// /// + /// + /// is . + /// public WebSocketServer (string url) { if (url == null) @@ -159,71 +176,89 @@ public WebSocketServer (string url) Uri uri; string msg; + if (!tryCreateUri (url, out uri, out msg)) throw new ArgumentException (msg, "url"); var host = uri.DnsSafeHost; var addr = host.ToIPAddress (); - if (!addr.IsLocal ()) - throw new ArgumentException ("The host part isn't a local host name: " + url, "url"); + + if (addr == null) { + msg = "The host part could not be converted to an IP address."; + + throw new ArgumentException (msg, "url"); + } + + if (!addr.IsLocal ()) { + msg = "The IP address of the host is not a local IP address."; + + throw new ArgumentException (msg, "url"); + } init (host, addr, uri.Port, uri.Scheme == "wss"); } /// - /// Initializes a new instance of the class with - /// the specified and . + /// Initializes a new instance of the class + /// with the specified port and boolean if secure or not. /// /// - /// An instance initialized by this constructor listens for the incoming connection requests on - /// . + /// The new instance listens for incoming handshake requests on + /// and . /// /// - /// An that represents the port number on which to listen. + /// An that specifies the number of the port on which + /// to listen. /// /// - /// A that indicates providing a secure connection or not. - /// (true indicates providing a secure connection.) + /// A : true if the new instance provides + /// secure connections; otherwise, false. /// /// - /// isn't between 1 and 65535 inclusive. + /// is less than 1 or greater than 65535. /// public WebSocketServer (int port, bool secure) { - if (!port.IsPortNumber ()) - throw new ArgumentOutOfRangeException ( - "port", "Not between 1 and 65535 inclusive: " + port); + if (!port.IsPortNumber ()) { + var msg = "Less than 1 or greater than 65535."; + + throw new ArgumentOutOfRangeException ("port", msg); + } - init (null, System.Net.IPAddress.Any, port, secure); + var addr = System.Net.IPAddress.Any; + + init (addr.ToString (), addr, port, secure); } /// - /// Initializes a new instance of the class with - /// the specified and . + /// Initializes a new instance of the class + /// with the specified IP address and port. /// /// /// - /// An instance initialized by this constructor listens for the incoming connection requests - /// on and . + /// The new instance listens for incoming handshake requests on + /// and . /// /// - /// If is 443, that instance provides a secure connection. + /// It provides secure connections if is 443. /// /// /// - /// A that represents the local IP address of the server. + /// A that specifies the local IP + /// address on which to listen. /// /// - /// An that represents the port number on which to listen. + /// An that specifies the number of the port on which + /// to listen. /// + /// + /// is not a local IP address. + /// /// /// is . /// - /// - /// isn't a local IP address. - /// /// - /// isn't between 1 and 65535 inclusive. + /// is less than 1 or greater than 65535. /// public WebSocketServer (System.Net.IPAddress address, int port) : this (address, port, port == 443) @@ -231,46 +266,52 @@ public WebSocketServer (System.Net.IPAddress address, int port) } /// - /// Initializes a new instance of the class with - /// the specified , , - /// and . + /// Initializes a new instance of the class + /// with the specified IP address, port, and boolean if secure or not. /// /// - /// An instance initialized by this constructor listens for the incoming connection requests on + /// The new instance listens for incoming handshake requests on /// and . /// /// - /// A that represents the local IP address of the server. + /// A that specifies the local IP + /// address on which to listen. /// /// - /// An that represents the port number on which to listen. + /// An that specifies the number of the port on which + /// to listen. /// /// - /// A that indicates providing a secure connection or not. - /// (true indicates providing a secure connection.) + /// A : true if the new instance provides + /// secure connections; otherwise, false. /// + /// + /// is not a local IP address. + /// /// /// is . /// - /// - /// isn't a local IP address. - /// /// - /// isn't between 1 and 65535 inclusive. + /// is less than 1 or greater than 65535. /// public WebSocketServer (System.Net.IPAddress address, int port, bool secure) { if (address == null) throw new ArgumentNullException ("address"); - if (!address.IsLocal ()) - throw new ArgumentException ("Not a local IP address: " + address, "address"); + if (!address.IsLocal ()) { + var msg = "Not a local IP address."; - if (!port.IsPortNumber ()) - throw new ArgumentOutOfRangeException ( - "port", "Not between 1 and 65535 inclusive: " + port); + throw new ArgumentException (msg, "address"); + } + + if (!port.IsPortNumber ()) { + var msg = "Less than 1 or greater than 65535."; + + throw new ArgumentOutOfRangeException ("port", msg); + } - init (null, address, port, secure); + init (address.ToString (), address, port, secure); } #endregion @@ -278,10 +319,11 @@ public WebSocketServer (System.Net.IPAddress address, int port, bool secure) #region Public Properties /// - /// Gets the local IP address of the server. + /// Gets the IP address of the server. /// /// - /// A that represents the local IP address of the server. + /// A that represents the local IP + /// address on which to listen for incoming handshake requests. /// public System.Net.IPAddress Address { get { @@ -292,10 +334,22 @@ public System.Net.IPAddress Address { /// /// Gets or sets the scheme used to authenticate the clients. /// + /// + /// The set operation works if the current state of the server is + /// Ready or Stop. + /// /// - /// One of the enum values, - /// indicates the scheme used to authenticate the clients. The default value is - /// . + /// + /// One of the + /// enum values. + /// + /// + /// It represents the scheme used to authenticate the clients. + /// + /// + /// The default value is + /// . + /// /// public AuthenticationSchemes AuthenticationSchemes { get { @@ -303,13 +357,12 @@ public AuthenticationSchemes AuthenticationSchemes { } set { - var msg = _state.CheckIfAvailable (true, false, false); - if (msg != null) { - _logger.Error (msg); - return; - } + lock (_sync) { + if (!canSet ()) + return; - _authSchemes = value; + _authSchemes = value; + } } } @@ -326,14 +379,15 @@ public bool IsListening { } /// - /// Gets a value indicating whether the server provides a secure connection. + /// Gets a value indicating whether the server provides secure connections. /// /// - /// true if the server provides a secure connection; otherwise, false. + /// true if the server provides secure connections; otherwise, + /// false. /// public bool IsSecure { get { - return _secure; + return _isSecure; } } @@ -341,9 +395,18 @@ public bool IsSecure { /// Gets or sets a value indicating whether the server cleans up /// the inactive sessions periodically. /// + /// + /// The set operation works if the current state of the server is + /// Ready or Stop. + /// /// - /// true if the server cleans up the inactive sessions every 60 seconds; - /// otherwise, false. The default value is true. + /// + /// true if the server cleans up the inactive sessions + /// every 60 seconds; otherwise, false. + /// + /// + /// The default value is false. + /// /// public bool KeepClean { get { @@ -351,38 +414,31 @@ public bool KeepClean { } set { - var msg = _state.CheckIfAvailable (true, false, false); - if (msg != null) { - _logger.Error (msg); - return; - } - _services.KeepClean = value; } } /// - /// Gets the logging functions. + /// Gets the logging function for the server. /// /// - /// The default logging level is . If you would like to change it, - /// you should set the Log.Level property to any of the enum - /// values. + /// The default logging level is . /// /// - /// A that provides the logging functions. + /// A that provides the logging function. /// public Logger Log { get { - return _logger; + return _log; } } /// - /// Gets the port on which to listen for incoming connection requests. + /// Gets the port of the server. /// /// - /// An that represents the port number on which to listen. + /// An that represents the number of the port on which + /// to listen for incoming handshake requests. /// public int Port { get { @@ -393,37 +449,59 @@ public int Port { /// /// Gets or sets the name of the realm associated with the server. /// + /// + /// The set operation works if the current state of the server is + /// Ready or Stop. + /// /// - /// A that represents the name of the realm. The default value is - /// "SECRET AREA". + /// + /// A that represents the name of the realm. + /// + /// + /// "SECRET AREA" is used as the name of the realm if the value is + /// or an empty string. + /// + /// + /// The default value is . + /// /// public string Realm { get { - return _realm ?? (_realm = "SECRET AREA"); + return _realm; } set { - var msg = _state.CheckIfAvailable (true, false, false); - if (msg != null) { - _logger.Error (msg); - return; - } + lock (_sync) { + if (!canSet ()) + return; - _realm = value; + _realm = value; + } } } /// - /// Gets or sets a value indicating whether the server is allowed to be bound to - /// an address that is already in use. + /// Gets or sets a value indicating whether the server is allowed to + /// be bound to an address that is already in use. /// /// - /// If you would like to resolve to wait for socket in TIME_WAIT state, - /// you should set this property to true. + /// + /// You should set this property to true if you would like to + /// resolve to wait for socket in TIME_WAIT state. + /// + /// + /// The set operation works if the current state of the server is + /// Ready or Stop. + /// /// /// - /// true if the server is allowed to be bound to an address that is already in use; - /// otherwise, false. The default value is false. + /// + /// true if the server is allowed to be bound to an address + /// that is already in use; otherwise, false. + /// + /// + /// The default value is false. + /// /// public bool ReuseAddress { get { @@ -431,93 +509,121 @@ public bool ReuseAddress { } set { - var msg = _state.CheckIfAvailable (true, false, false); - if (msg != null) { - _logger.Error (msg); - return; - } + lock (_sync) { + if (!canSet ()) + return; - _reuseAddress = value; + _reuseAddress = value; + } } } /// - /// Gets or sets the SSL configuration used to authenticate the server and - /// optionally the client for secure connection. + /// Gets the configuration for secure connection. /// + /// + /// The configuration is used when the server attempts to start, + /// so it must be configured before the start method is called. + /// /// - /// A that represents the configuration used to - /// authenticate the server and optionally the client for secure connection. + /// A that represents the + /// configuration used to provide secure connections. /// + /// + /// The server does not provide secure connections. + /// public ServerSslConfiguration SslConfiguration { get { - return _sslConfig ?? (_sslConfig = new ServerSslConfiguration (null)); - } + if (!_isSecure) { + var msg = "The server does not provide secure connections."; - set { - var msg = _state.CheckIfAvailable (true, false, false); - if (msg != null) { - _logger.Error (msg); - return; + throw new InvalidOperationException (msg); } - _sslConfig = value; + return getSslConfiguration (); } } /// - /// Gets or sets the delegate called to find the credentials for an identity used to - /// authenticate a client. + /// Gets or sets the delegate called to find the credentials for + /// an identity used to authenticate a client. /// + /// + /// The set operation works if the current state of the server is + /// Ready or Stop. + /// /// - /// A Func<, > delegate that - /// references the method(s) used to find the credentials. The default value is a function that - /// only returns . + /// + /// A + /// delegate. + /// + /// + /// It represents the delegate called when the server finds + /// the credentials used to authenticate a client. + /// + /// + /// It must return if the credentials + /// are not found. + /// + /// + /// if not necessary. + /// + /// + /// The default value is . + /// /// public Func UserCredentialsFinder { get { - return _credFinder ?? (_credFinder = identity => null); + return _userCredFinder; } set { - var msg = _state.CheckIfAvailable (true, false, false); - if (msg != null) { - _logger.Error (msg); - return; - } + lock (_sync) { + if (!canSet ()) + return; - _credFinder = value; + _userCredFinder = value; + } } } /// - /// Gets or sets the wait time for the response to the WebSocket Ping or Close. + /// Gets or sets the time to wait for the response to the WebSocket + /// Ping or Close. /// + /// + /// The set operation works if the current state of the server is + /// Ready or Stop. + /// /// - /// A that represents the wait time. The default value is - /// the same as 1 second. + /// + /// A that represents the time to wait for + /// the response. + /// + /// + /// The default value is the same as 1 second. + /// /// + /// + /// The value specified for a set operation is zero or less. + /// public TimeSpan WaitTime { get { return _services.WaitTime; } set { - var msg = _state.CheckIfAvailable (true, false, false) ?? value.CheckIfValidWaitTime (); - if (msg != null) { - _logger.Error (msg); - return; - } - _services.WaitTime = value; } } /// - /// Gets the access to the WebSocket services provided by the server. + /// Gets the management function for the WebSocket services provided by + /// the server. /// /// - /// A that manages the WebSocket services. + /// A that manages the WebSocket + /// services provided by the server. /// public WebSocketServiceManager WebSocketServices { get { @@ -532,100 +638,143 @@ public WebSocketServiceManager WebSocketServices { private void abort () { lock (_sync) { - if (!IsListening) + if (_state != ServerState.Start) return; _state = ServerState.ShuttingDown; } - _listener.Stop (); - _services.Stop (new CloseEventArgs (CloseStatusCode.ServerError), true, false); + try { + _listener.Stop (); + } + catch (Exception ex) { + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + } + + try { + _services.Stop (1006, String.Empty); + } + catch (Exception ex) { + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + } _state = ServerState.Stop; } - private static bool authenticate ( - TcpListenerWebSocketContext context, - AuthenticationSchemes scheme, - string realm, - Func credentialsFinder) + private bool authenticateClient (TcpListenerWebSocketContext context) { - var chal = scheme == AuthenticationSchemes.Basic - ? AuthenticationChallenge.CreateBasicChallenge (realm).ToBasicString () - : scheme == AuthenticationSchemes.Digest - ? AuthenticationChallenge.CreateDigestChallenge (realm).ToDigestString () - : null; + if (_authSchemes == AuthenticationSchemes.Anonymous) + return true; - if (chal == null) { - context.Close (HttpStatusCode.Forbidden); + if (_authSchemes == AuthenticationSchemes.None) return false; - } + + var chal = new AuthenticationChallenge (_authSchemes, _realmInUse) + .ToString (); var retry = -1; Func auth = null; - auth = () => { - retry++; - if (retry > 99) { - context.Close (HttpStatusCode.Forbidden); - return false; - } + auth = + () => { + retry++; - var user = HttpUtility.CreateUser ( - context.Headers["Authorization"], scheme, realm, context.HttpMethod, credentialsFinder); + if (retry > 99) + return false; - if (user != null && user.Identity.IsAuthenticated) { - context.SetUser (user); - return true; - } + if (context.SetUser (_authSchemes, _realmInUse, _userCredFinder)) + return true; - context.SendAuthenticationChallenge (chal); - return auth (); - }; + context.SendAuthenticationChallenge (chal); + + return auth (); + }; return auth (); } - private string checkIfCertificateExists () + private bool canSet () + { + return _state == ServerState.Ready || _state == ServerState.Stop; + } + + private bool checkHostNameForRequest (string name) { - return _secure && (_sslConfig == null || _sslConfig.ServerCertificate == null) - ? "The secure connection requires a server certificate." - : null; + return !_isDnsStyle + || Uri.CheckHostName (name) != UriHostNameType.Dns + || name == _hostname; } - private void init (string hostname, System.Net.IPAddress address, int port, bool secure) + private string getRealm () { - _hostname = hostname ?? address.ToString (); + var realm = _realm; + + return realm != null && realm.Length > 0 ? realm : _defaultRealm; + } + + private ServerSslConfiguration getSslConfiguration () + { + if (_sslConfig == null) + _sslConfig = new ServerSslConfiguration (); + + return _sslConfig; + } + + private void init ( + string hostname, + System.Net.IPAddress address, + int port, + bool secure + ) + { + _hostname = hostname; _address = address; _port = port; - _secure = secure; + _isSecure = secure; _authSchemes = AuthenticationSchemes.Anonymous; - _dnsStyle = Uri.CheckHostName (hostname) == UriHostNameType.Dns; + _isDnsStyle = Uri.CheckHostName (hostname) == UriHostNameType.Dns; _listener = new TcpListener (address, port); - _logger = new Logger (); - _services = new WebSocketServiceManager (_logger); + _log = new Logger (); + _services = new WebSocketServiceManager (_log); _sync = new object (); } private void processRequest (TcpListenerWebSocketContext context) { + if (!authenticateClient (context)) { + context.Close (HttpStatusCode.Forbidden); + + return; + } + var uri = context.RequestUri; - if (uri == null || uri.Port != _port) { + + if (uri == null) { context.Close (HttpStatusCode.BadRequest); + return; } - if (_dnsStyle) { - var hostname = uri.DnsSafeHost; - if (Uri.CheckHostName (hostname) == UriHostNameType.Dns && hostname != _hostname) { - context.Close (HttpStatusCode.NotFound); - return; - } + var name = uri.DnsSafeHost; + + if (!checkHostNameForRequest (name)) { + context.Close (HttpStatusCode.NotFound); + + return; } + var path = uri.AbsolutePath; + + if (path.IndexOfAny (new[] { '%', '+' }) > -1) + path = HttpUtility.UrlDecode (path, Encoding.UTF8); + WebSocketServiceHost host; - if (!_services.InternalTryGetServiceHost (uri.AbsolutePath, out host)) { + + if (!_services.InternalTryGetServiceHost (path, out host)) { context.Close (HttpStatusCode.NotImplemented); + return; } @@ -635,64 +784,178 @@ private void processRequest (TcpListenerWebSocketContext context) private void receiveRequest () { while (true) { + TcpClient cl = null; + try { - var cl = _listener.AcceptTcpClient (); + cl = _listener.AcceptTcpClient (); + ThreadPool.QueueUserWorkItem ( state => { try { - var ctx = cl.GetWebSocketContext (null, _secure, _sslConfig, _logger); - if (_authSchemes != AuthenticationSchemes.Anonymous && - !authenticate (ctx, _authSchemes, Realm, UserCredentialsFinder)) - return; + var ctx = new TcpListenerWebSocketContext ( + cl, + null, + _isSecure, + _sslConfigInUse, + _log + ); processRequest (ctx); } catch (Exception ex) { - _logger.Fatal (ex.ToString ()); + _log.Error (ex.Message); + _log.Debug (ex.ToString ()); + cl.Close (); } - }); + } + ); } catch (SocketException ex) { - _logger.Warn ("Receiving has been stopped.\n reason: " + ex.Message); + if (_state == ServerState.ShuttingDown) + return; + + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + + break; + } + catch (InvalidOperationException ex) { + if (_state == ServerState.ShuttingDown) + return; + + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + break; } catch (Exception ex) { - _logger.Fatal (ex.ToString ()); + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + + if (cl != null) + cl.Close (); + + if (_state == ServerState.ShuttingDown) + return; + break; } } - if (IsListening) - abort (); + abort (); + } + + private void start () + { + lock (_sync) { + if (_state == ServerState.Start || _state == ServerState.ShuttingDown) + return; + + if (_isSecure) { + var src = getSslConfiguration (); + var conf = new ServerSslConfiguration (src); + + if (conf.ServerCertificate == null) { + var msg = "There is no server certificate for secure connection."; + + throw new InvalidOperationException (msg); + } + + _sslConfigInUse = conf; + } + + _realmInUse = getRealm (); + + _services.Start (); + + try { + startReceiving (); + } + catch { + _services.Stop (1011, String.Empty); + + throw; + } + + _state = ServerState.Start; + } } private void startReceiving () { - if (_reuseAddress) + if (_reuseAddress) { _listener.Server.SetSocketOption ( - SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + SocketOptionLevel.Socket, + SocketOptionName.ReuseAddress, + true + ); + } + + try { + _listener.Start (); + } + catch (Exception ex) { + var msg = "The underlying listener has failed to start."; - _listener.Start (); - _receiveThread = new Thread (new ThreadStart (receiveRequest)); + throw new InvalidOperationException (msg, ex); + } + + var receiver = new ThreadStart (receiveRequest); + _receiveThread = new Thread (receiver); _receiveThread.IsBackground = true; + _receiveThread.Start (); } + private void stop (ushort code, string reason) + { + lock (_sync) { + if (_state != ServerState.Start) + return; + + _state = ServerState.ShuttingDown; + } + + try { + var timeout = 5000; + + stopReceiving (timeout); + } + catch (Exception ex) { + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + } + + try { + _services.Stop (code, reason); + } + catch (Exception ex) { + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + } + + _state = ServerState.Stop; + } + private void stopReceiving (int millisecondsTimeout) { _listener.Stop (); _receiveThread.Join (millisecondsTimeout); } - private static bool tryCreateUri (string uriString, out Uri result, out string message) + private static bool tryCreateUri ( + string uriString, + out Uri result, + out string message + ) { if (!uriString.TryCreateWebSocketUri (out result, out message)) return false; if (result.PathAndQuery != "/") { result = null; - message = "Includes the path or query component: " + uriString; + message = "It includes either or both path and query components."; return false; } @@ -705,201 +968,216 @@ private static bool tryCreateUri (string uriString, out Uri result, out string m #region Public Methods /// - /// Adds a WebSocket service with the specified behavior, , - /// and . + /// Adds a WebSocket service with the specified behavior and path. /// - /// + /// /// - /// This method converts to URL-decoded string, - /// and removes '/' from tail end of . + /// A that specifies an absolute path to + /// the service to add. /// /// - /// returns an initialized specified typed - /// instance. + /// / is trimmed from the end of the string if present. /// - /// - /// - /// A that represents the absolute path to the service to add. - /// - /// - /// A Func<T> delegate that references the method used to initialize - /// a new specified typed instance (a new - /// instance). /// /// - /// The type of the behavior of the service to add. The TBehavior must inherit - /// the class. + /// + /// The type of the behavior for the service. + /// + /// + /// It must inherit the class. + /// + /// + /// Also it must have a public parameterless constructor. + /// /// - public void AddWebSocketService (string path, Func initializer) - where TBehavior : WebSocketBehavior + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is not an absolute path. + /// + /// + /// -or- + /// + /// + /// includes either or both + /// query and fragment components. + /// + /// + /// -or- + /// + /// + /// is already in use. + /// + /// + /// + /// is . + /// + public void AddWebSocketService (string path) + where TBehavior : WebSocketBehavior, new () { - var msg = path.CheckIfValidServicePath () ?? - (initializer == null ? "'initializer' is null." : null); - - if (msg != null) { - _logger.Error (msg); - return; - } - - _services.Add (path, initializer); + _services.AddService (path, null); } /// - /// Adds a WebSocket service with the specified behavior and . + /// Adds a WebSocket service with the specified behavior, path, + /// and initializer. /// - /// - /// This method converts to URL-decoded string, - /// and removes '/' from tail end of . - /// /// - /// A that represents the absolute path to the service to add. + /// + /// A that specifies an absolute path to + /// the service to add. + /// + /// + /// / is trimmed from the end of the string if present. + /// + /// + /// + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the service initializes + /// a new session instance. + /// + /// + /// if not necessary. + /// /// - /// - /// The type of the behavior of the service to add. The TBehaviorWithNew must inherit - /// the class, and must have a public parameterless - /// constructor. + /// + /// + /// The type of the behavior for the service. + /// + /// + /// It must inherit the class. + /// + /// + /// Also it must have a public parameterless constructor. + /// /// - public void AddWebSocketService (string path) - where TBehaviorWithNew : WebSocketBehavior, new () + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is not an absolute path. + /// + /// + /// -or- + /// + /// + /// includes either or both + /// query and fragment components. + /// + /// + /// -or- + /// + /// + /// is already in use. + /// + /// + /// + /// is . + /// + public void AddWebSocketService ( + string path, + Action initializer + ) + where TBehavior : WebSocketBehavior, new () { - AddWebSocketService (path, () => new TBehaviorWithNew ()); + _services.AddService (path, initializer); } /// - /// Removes the WebSocket service with the specified . + /// Removes a WebSocket service with the specified path. /// /// - /// This method converts to URL-decoded string, - /// and removes '/' from tail end of . + /// The service is stopped with close status 1001 (going away) + /// if the current state of the service is Start. /// /// - /// true if the service is successfully found and removed; otherwise, false. + /// true if the service is successfully found and removed; + /// otherwise, false. /// /// - /// A that represents the absolute path to the service to find. + /// + /// A that specifies an absolute path to + /// the service to remove. + /// + /// + /// / is trimmed from the end of the string if present. + /// /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is not an absolute path. + /// + /// + /// -or- + /// + /// + /// includes either or both + /// query and fragment components. + /// + /// + /// + /// is . + /// public bool RemoveWebSocketService (string path) { - var msg = path.CheckIfValidServicePath (); - if (msg != null) { - _logger.Error (msg); - return false; - } - - return _services.Remove (path); + return _services.RemoveService (path); } /// - /// Starts receiving the WebSocket connection requests. + /// Starts receiving incoming handshake requests. /// + /// + /// This method works if the current state of the server is Ready or Stop. + /// + /// + /// + /// There is no server certificate for secure connection. + /// + /// + /// -or- + /// + /// + /// The underlying has failed to start. + /// + /// public void Start () { - lock (_sync) { - var msg = _state.CheckIfAvailable (true, false, false) ?? checkIfCertificateExists (); - if (msg != null) { - _logger.Error (msg); - return; - } - - _services.Start (); - startReceiving (); + if (_state == ServerState.Start || _state == ServerState.ShuttingDown) + return; - _state = ServerState.Start; - } + start (); } /// - /// Stops receiving the WebSocket connection requests. + /// Stops receiving incoming handshake requests. /// + /// + /// This method works if the current state of the server is Start. + /// public void Stop () { - lock (_sync) { - var msg = _state.CheckIfAvailable (false, true, false); - if (msg != null) { - _logger.Error (msg); - return; - } - - _state = ServerState.ShuttingDown; - } - - stopReceiving (5000); - _services.Stop (new CloseEventArgs (), true, true); - - _state = ServerState.Stop; - } - - /// - /// Stops receiving the WebSocket connection requests with - /// the specified and . - /// - /// - /// A that represents the status code indicating the reason for the stop. - /// - /// - /// A that represents the reason for the stop. - /// - public void Stop (ushort code, string reason) - { - lock (_sync) { - var msg = _state.CheckIfAvailable (false, true, false) ?? - WebSocket.CheckCloseParameters (code, reason, false); - - if (msg != null) { - _logger.Error (msg); - return; - } - - _state = ServerState.ShuttingDown; - } - - stopReceiving (5000); - if (code == (ushort) CloseStatusCode.NoStatus) { - _services.Stop (new CloseEventArgs (), true, true); - } - else { - var send = !code.IsReserved (); - _services.Stop (new CloseEventArgs (code, reason), send, send); - } - - _state = ServerState.Stop; - } - - /// - /// Stops receiving the WebSocket connection requests with - /// the specified and . - /// - /// - /// One of the enum values, represents the status code indicating - /// the reason for the stop. - /// - /// - /// A that represents the reason for the stop. - /// - public void Stop (CloseStatusCode code, string reason) - { - lock (_sync) { - var msg = _state.CheckIfAvailable (false, true, false) ?? - WebSocket.CheckCloseParameters (code, reason, false); - - if (msg != null) { - _logger.Error (msg); - return; - } - - _state = ServerState.ShuttingDown; - } - - stopReceiving (5000); - if (code == CloseStatusCode.NoStatus) { - _services.Stop (new CloseEventArgs (), true, true); - } - else { - var send = !code.IsReserved (); - _services.Stop (new CloseEventArgs (code, reason), send, send); - } + if (_state != ServerState.Start) + return; - _state = ServerState.Stop; + stop (1001, String.Empty); } #endregion diff --git a/websocket-sharp/Server/WebSocketServiceHost.cs b/websocket-sharp/Server/WebSocketServiceHost.cs index 8d41d2104..ee20d92a9 100644 --- a/websocket-sharp/Server/WebSocketServiceHost.cs +++ b/websocket-sharp/Server/WebSocketServiceHost.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2023 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 @@ -39,21 +39,43 @@ namespace WebSocketSharp.Server { /// - /// Exposes the methods and properties used to access the information in a WebSocket service - /// provided by the or . + /// Exposes the methods and properties used to access the information in + /// a WebSocket service provided by the or + /// class. /// /// - /// The WebSocketServiceHost class is an abstract class. + /// This class is an abstract class. /// public abstract class WebSocketServiceHost { + #region Private Fields + + private Logger _log; + private string _path; + private WebSocketSessionManager _sessions; + + #endregion + #region Protected Constructors /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the + /// class with the specified path and logging function. /// - protected WebSocketServiceHost () + /// + /// A that specifies the absolute path to + /// the service. + /// + /// + /// A that specifies the logging function for + /// the service. + /// + protected WebSocketServiceHost (string path, Logger log) { + _path = path; + _log = log; + + _sessions = new WebSocketSessionManager (log); } #endregion @@ -62,7 +84,23 @@ protected WebSocketServiceHost () internal ServerState State { get { - return Sessions.State; + return _sessions.State; + } + } + + #endregion + + #region Protected Properties + + /// + /// Gets the logging function for the service. + /// + /// + /// A that provides the logging function. + /// + protected Logger Log { + get { + return _log; } } @@ -71,47 +109,86 @@ internal ServerState State { #region Public Properties /// - /// Gets or sets a value indicating whether the WebSocket service cleans up + /// Gets or sets a value indicating whether the service cleans up /// the inactive sessions periodically. /// + /// + /// The set operation works if the current state of the service is + /// Ready or Stop. + /// /// - /// true if the service cleans up the inactive sessions periodically; - /// otherwise, false. + /// true if the service cleans up the inactive sessions every + /// 60 seconds; otherwise, false. /// - public abstract bool KeepClean { get; set; } + public bool KeepClean { + get { + return _sessions.KeepClean; + } + + set { + _sessions.KeepClean = value; + } + } /// - /// Gets the path to the WebSocket service. + /// Gets the path to the service. /// /// - /// A that represents the absolute path to the service. + /// A that represents the absolute path to + /// the service. /// - public abstract string Path { get; } + public string Path { + get { + return _path; + } + } /// - /// Gets the access to the sessions in the WebSocket service. + /// Gets the management function for the sessions in the service. /// /// - /// A that manages the sessions in the service. + /// A that manages the sessions in + /// the service. /// - public abstract WebSocketSessionManager Sessions { get; } + public WebSocketSessionManager Sessions { + get { + return _sessions; + } + } /// - /// Gets the of the behavior of the WebSocket service. + /// Gets the type of the behavior of the service. /// /// - /// A that represents the type of the behavior of the service. + /// A that represents the type of the behavior of + /// the service. /// - public abstract Type Type { get; } + public abstract Type BehaviorType { get; } /// - /// Gets or sets the wait time for the response to the WebSocket Ping or Close. + /// Gets or sets the time to wait for the response to the WebSocket + /// Ping or Close. /// + /// + /// The set operation works if the current state of the service is + /// Ready or Stop. + /// /// - /// A that represents the wait time. The default value is - /// the same as 1 second. + /// A that represents the time to wait for + /// the response. /// - public abstract TimeSpan WaitTime { get; set; } + /// + /// The value specified for a set operation is zero or less. + /// + public TimeSpan WaitTime { + get { + return _sessions.WaitTime; + } + + set { + _sessions.WaitTime = value; + } + } #endregion @@ -119,20 +196,17 @@ internal ServerState State { internal void Start () { - Sessions.Start (); + _sessions.Start (); } internal void StartSession (WebSocketContext context) { - CreateSession ().Start (context, Sessions); + CreateSession ().Start (context, _sessions); } internal void Stop (ushort code, string reason) { - var e = new CloseEventArgs (code, reason); - var send = !code.IsReserved (); - var bytes = send ? WebSocketFrame.CreateCloseFrame (e.PayloadData, false).ToArray () : null; - Sessions.Stop (e, bytes, send); + _sessions.Stop (code, reason); } #endregion @@ -140,10 +214,11 @@ internal void Stop (ushort code, string reason) #region Protected Methods /// - /// Creates a new session in the WebSocket service. + /// Creates a new session for the service. /// /// - /// A instance that represents a new session. + /// A instance that represents + /// the new session. /// protected abstract WebSocketBehavior CreateSession (); diff --git a/websocket-sharp/Server/WebSocketServiceHost`1.cs b/websocket-sharp/Server/WebSocketServiceHost`1.cs index 78e3983e8..8aac424e3 100644 --- a/websocket-sharp/Server/WebSocketServiceHost`1.cs +++ b/websocket-sharp/Server/WebSocketServiceHost`1.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2015 sta.blockhead + * Copyright (c) 2015-2024 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 @@ -31,81 +31,54 @@ namespace WebSocketSharp.Server { internal class WebSocketServiceHost : WebSocketServiceHost - where TBehavior : WebSocketBehavior + where TBehavior : WebSocketBehavior, new () { #region Private Fields - private Func _initializer; - private Logger _logger; - private string _path; - private WebSocketSessionManager _sessions; + private Func _creator; #endregion #region Internal Constructors - internal WebSocketServiceHost (string path, Func initializer, Logger logger) + internal WebSocketServiceHost ( + string path, + Action initializer, + Logger log + ) + : base (path, log) { - _path = path; - _initializer = initializer; - _logger = logger; - _sessions = new WebSocketSessionManager (logger); + _creator = createSessionCreator (initializer); } #endregion #region Public Properties - public override bool KeepClean { + public override Type BehaviorType { get { - return _sessions.KeepClean; - } - - set { - var msg = _sessions.State.CheckIfAvailable (true, false, false); - if (msg != null) { - _logger.Error (msg); - return; - } - - _sessions.KeepClean = value; - } - } - - public override string Path { - get { - return _path; + return typeof (TBehavior); } } - public override WebSocketSessionManager Sessions { - get { - return _sessions; - } - } + #endregion - public override Type Type { - get { - return typeof (TBehavior); - } - } + #region Private Methods - public override TimeSpan WaitTime { - get { - return _sessions.WaitTime; - } + private static Func createSessionCreator ( + Action initializer + ) + { + if (initializer == null) + return () => new TBehavior (); - set { - var msg = _sessions.State.CheckIfAvailable (true, false, false) ?? - value.CheckIfValidWaitTime (); + return () => { + var ret = new TBehavior (); - if (msg != null) { - _logger.Error (msg); - return; - } + initializer (ret); - _sessions.WaitTime = value; - } + return ret; + }; } #endregion @@ -114,7 +87,7 @@ public override TimeSpan WaitTime { protected override WebSocketBehavior CreateSession () { - return _initializer (); + return _creator (); } #endregion diff --git a/websocket-sharp/Server/WebSocketServiceManager.cs b/websocket-sharp/Server/WebSocketServiceManager.cs index 8ed76b335..29021062a 100644 --- a/websocket-sharp/Server/WebSocketServiceManager.cs +++ b/websocket-sharp/Server/WebSocketServiceManager.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2024 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 @@ -29,24 +29,23 @@ using System; using System.Collections; using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Threading; -using WebSocketSharp.Net; namespace WebSocketSharp.Server { /// - /// Manages the WebSocket services provided by the or - /// . + /// Provides the management function for the WebSocket services. /// + /// + /// This class manages the WebSocket services provided by the + /// or class. + /// public class WebSocketServiceManager { #region Private Fields - private volatile bool _clean; private Dictionary _hosts; - private Logger _logger; + private volatile bool _keepClean; + private Logger _log; private volatile ServerState _state; private object _sync; private TimeSpan _waitTime; @@ -55,16 +54,10 @@ public class WebSocketServiceManager #region Internal Constructors - internal WebSocketServiceManager () - : this (new Logger ()) + internal WebSocketServiceManager (Logger log) { - } - - internal WebSocketServiceManager (Logger logger) - { - _logger = logger; + _log = log; - _clean = true; _hosts = new Dictionary (); _state = ServerState.Ready; _sync = ((ICollection) _hosts).SyncRoot; @@ -89,11 +82,17 @@ public int Count { } /// - /// Gets the host instances for the Websocket services. + /// Gets the service host instances for the WebSocket services. /// /// - /// An IEnumerable<WebSocketServiceHost> instance that provides an enumerator - /// which supports the iteration over the collection of the host instances for the services. + /// + /// An + /// instance. + /// + /// + /// It provides an enumerator which supports the iteration over + /// the collection of the service host instances. + /// /// public IEnumerable Hosts { get { @@ -103,45 +102,110 @@ public IEnumerable Hosts { } /// - /// Gets the WebSocket service host with the specified . + /// Gets the service host instance for a WebSocket service with + /// the specified path. /// /// - /// A instance that provides the access to - /// the information in the service, or if it's not found. + /// + /// A instance that represents + /// the service host instance. + /// + /// + /// It provides the function to access the information in the service. + /// + /// + /// if not found. + /// /// /// - /// A that represents the absolute path to the service to find. + /// + /// A that specifies an absolute path to + /// the service to get. + /// + /// + /// / is trimmed from the end of the string if present. + /// /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is not an absolute path. + /// + /// + /// -or- + /// + /// + /// includes either or both + /// query and fragment components. + /// + /// + /// + /// is . + /// public WebSocketServiceHost this[string path] { get { + if (path == null) + throw new ArgumentNullException ("path"); + + if (path.Length == 0) + throw new ArgumentException ("An empty string.", "path"); + + if (path[0] != '/') { + var msg = "Not an absolute path."; + + throw new ArgumentException (msg, "path"); + } + + if (path.IndexOfAny (new[] { '?', '#' }) > -1) { + var msg = "It includes either or both query and fragment components."; + + throw new ArgumentException (msg, "path"); + } + WebSocketServiceHost host; - TryGetServiceHost (path, out host); + + InternalTryGetServiceHost (path, out host); return host; } } /// - /// Gets a value indicating whether the manager cleans up the inactive sessions - /// in the WebSocket services periodically. + /// Gets or sets a value indicating whether the inactive sessions in + /// the WebSocket services are cleaned up periodically. /// + /// + /// The set operation works if the current state of the server is + /// Ready or Stop. + /// /// - /// true if the manager cleans up the inactive sessions every 60 seconds; - /// otherwise, false. + /// + /// true if the inactive sessions are cleaned up every 60 + /// seconds; otherwise, false. + /// + /// + /// The default value is false. + /// /// public bool KeepClean { get { - return _clean; + return _keepClean; } - internal set { + set { lock (_sync) { - if (!(value ^ _clean)) + if (!canSet ()) return; - _clean = value; foreach (var host in _hosts.Values) host.KeepClean = value; + + _keepClean = value; } } } @@ -150,8 +214,14 @@ internal set { /// Gets the paths for the WebSocket services. /// /// - /// An IEnumerable<string> instance that provides an enumerator which supports - /// the iteration over the collection of the paths for the services. + /// + /// An + /// instance. + /// + /// + /// It provides an enumerator which supports the iteration over + /// the collection of the paths. + /// /// public IEnumerable Paths { get { @@ -161,44 +231,45 @@ public IEnumerable Paths { } /// - /// Gets the total number of the sessions in the WebSocket services. - /// - /// - /// An that represents the total number of the sessions in the services. - /// - public int SessionCount { - get { - var cnt = 0; - foreach (var host in Hosts) { - if (_state != ServerState.Start) - break; - - cnt += host.Sessions.Count; - } - - return cnt; - } - } - - /// - /// Gets the wait time for the response to the WebSocket Ping or Close. + /// Gets or sets the time to wait for the response to the WebSocket + /// Ping or Close. /// + /// + /// The set operation works if the current state of the server is + /// Ready or Stop. + /// /// - /// A that represents the wait time. + /// + /// A that represents the time to wait for + /// the response. + /// + /// + /// The default value is the same as 1 second. + /// /// + /// + /// The value specified for a set operation is zero or less. + /// public TimeSpan WaitTime { get { return _waitTime; } - internal set { + set { + if (value <= TimeSpan.Zero) { + var msg = "Zero or less."; + + throw new ArgumentOutOfRangeException ("value", msg); + } + lock (_sync) { - if (value == _waitTime) + if (!canSet ()) return; - _waitTime = value; foreach (var host in _hosts.Values) host.WaitTime = value; + + _waitTime = value; } } } @@ -207,143 +278,24 @@ internal set { #region Private Methods - private void broadcast (Opcode opcode, byte[] data, Action completed) - { - var cache = new Dictionary (); - try { - foreach (var host in Hosts) { - if (_state != ServerState.Start) - break; - - host.Sessions.Broadcast (opcode, data, cache); - } - - if (completed != null) - completed (); - } - catch (Exception ex) { - _logger.Fatal (ex.ToString ()); - } - finally { - cache.Clear (); - } - } - - private void broadcast (Opcode opcode, Stream stream, Action completed) + private bool canSet () { - var cache = new Dictionary (); - try { - foreach (var host in Hosts) { - if (_state != ServerState.Start) - break; - - host.Sessions.Broadcast (opcode, stream, cache); - } - - if (completed != null) - completed (); - } - catch (Exception ex) { - _logger.Fatal (ex.ToString ()); - } - finally { - foreach (var cached in cache.Values) - cached.Dispose (); - - cache.Clear (); - } - } - - private void broadcastAsync (Opcode opcode, byte[] data, Action completed) - { - ThreadPool.QueueUserWorkItem (state => broadcast (opcode, data, completed)); - } - - private void broadcastAsync (Opcode opcode, Stream stream, Action completed) - { - ThreadPool.QueueUserWorkItem (state => broadcast (opcode, stream, completed)); - } - - private Dictionary> broadping ( - byte[] frameAsBytes, TimeSpan timeout) - { - var ret = new Dictionary> (); - foreach (var host in Hosts) { - if (_state != ServerState.Start) - break; - - ret.Add (host.Path, host.Sessions.Broadping (frameAsBytes, timeout)); - } - - return ret; + return _state == ServerState.Ready || _state == ServerState.Stop; } #endregion #region Internal Methods - internal void Add (string path, Func initializer) - where TBehavior : WebSocketBehavior - { - lock (_sync) { - path = HttpUtility.UrlDecode (path).TrimEndSlash (); - - WebSocketServiceHost host; - if (_hosts.TryGetValue (path, out host)) { - _logger.Error ( - "A WebSocket service with the specified path already exists:\n path: " + path); - - return; - } - - host = new WebSocketServiceHost (path, initializer, _logger); - if (!_clean) - host.KeepClean = false; - - if (_waitTime != host.WaitTime) - host.WaitTime = _waitTime; - - if (_state == ServerState.Start) - host.Start (); - - _hosts.Add (path, host); - } - } - - internal bool InternalTryGetServiceHost (string path, out WebSocketServiceHost host) - { - bool ret; - lock (_sync) { - path = HttpUtility.UrlDecode (path).TrimEndSlash (); - ret = _hosts.TryGetValue (path, out host); - } - - if (!ret) - _logger.Error ( - "A WebSocket service with the specified path isn't found:\n path: " + path); - - return ret; - } - - internal bool Remove (string path) + internal bool InternalTryGetServiceHost ( + string path, + out WebSocketServiceHost host + ) { - WebSocketServiceHost host; - lock (_sync) { - path = HttpUtility.UrlDecode (path).TrimEndSlash (); - if (!_hosts.TryGetValue (path, out host)) { - _logger.Error ( - "A WebSocket service with the specified path isn't found:\n path: " + path); - - return false; - } - - _hosts.Remove (path); - } - - if (host.State == ServerState.Start) - host.Stop ((ushort) CloseStatusCode.Away, null); + path = path.TrimSlashFromEnd (); - return true; + lock (_sync) + return _hosts.TryGetValue (path, out host); } internal void Start () @@ -356,16 +308,14 @@ internal void Start () } } - internal void Stop (CloseEventArgs e, bool send, bool receive) + internal void Stop (ushort code, string reason) { lock (_sync) { _state = ServerState.ShuttingDown; - var bytes = send ? WebSocketFrame.CreateCloseFrame (e.PayloadData, false).ToArray () : null; foreach (var host in _hosts.Values) - host.Sessions.Stop (e, bytes, receive); + host.Stop (code, reason); - _hosts.Clear (); _state = ServerState.Stop; } } @@ -375,235 +325,285 @@ internal void Stop (CloseEventArgs e, bool send, bool receive) #region Public Methods /// - /// Sends binary to every client in the WebSocket services. + /// Adds a WebSocket service with the specified behavior, path, + /// and initializer. /// - /// - /// An array of that represents the binary data to send. + /// + /// + /// A that specifies an absolute path to + /// the service to add. + /// + /// + /// / is trimmed from the end of the string if present. + /// /// - public void Broadcast (byte[] data) + /// + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the service initializes + /// a new session instance. + /// + /// + /// if not necessary. + /// + /// + /// + /// + /// The type of the behavior for the service. + /// + /// + /// It must inherit the class. + /// + /// + /// Also it must have a public parameterless constructor. + /// + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is not an absolute path. + /// + /// + /// -or- + /// + /// + /// includes either or both + /// query and fragment components. + /// + /// + /// -or- + /// + /// + /// is already in use. + /// + /// + /// + /// is . + /// + public void AddService ( + string path, + Action initializer + ) + where TBehavior : WebSocketBehavior, new () { - var msg = _state.CheckIfAvailable (false, true, false) ?? - WebSocket.CheckSendParameter (data); + if (path == null) + throw new ArgumentNullException ("path"); - if (msg != null) { - _logger.Error (msg); - return; - } + if (path.Length == 0) + throw new ArgumentException ("An empty string.", "path"); - if (data.LongLength <= WebSocket.FragmentLength) - broadcast (Opcode.Binary, data, null); - else - broadcast (Opcode.Binary, new MemoryStream (data), null); - } + if (path[0] != '/') { + var msg = "Not an absolute path."; - /// - /// Sends text to every client in the WebSocket services. - /// - /// - /// A that represents the text data to send. - /// - public void Broadcast (string data) - { - var msg = _state.CheckIfAvailable (false, true, false) ?? - WebSocket.CheckSendParameter (data); + throw new ArgumentException (msg, "path"); + } - if (msg != null) { - _logger.Error (msg); - return; + if (path.IndexOfAny (new[] { '?', '#' }) > -1) { + var msg = "It includes either or both query and fragment components."; + + throw new ArgumentException (msg, "path"); } - var bytes = data.UTF8Encode (); - if (bytes.LongLength <= WebSocket.FragmentLength) - broadcast (Opcode.Text, bytes, null); - else - broadcast (Opcode.Text, new MemoryStream (bytes), null); - } + path = path.TrimSlashFromEnd (); - /// - /// Sends binary asynchronously to every client in - /// the WebSocket services. - /// - /// - /// This method doesn't wait for the send to be complete. - /// - /// - /// An array of that represents the binary data to send. - /// - /// - /// An delegate that references the method(s) called when - /// the send is complete. - /// - public void BroadcastAsync (byte[] data, Action completed) - { - var msg = _state.CheckIfAvailable (false, true, false) ?? - WebSocket.CheckSendParameter (data); + lock (_sync) { + WebSocketServiceHost host; - if (msg != null) { - _logger.Error (msg); - return; - } + if (_hosts.TryGetValue (path, out host)) { + var msg = "It is already in use."; + + throw new ArgumentException (msg, "path"); + } + + host = new WebSocketServiceHost (path, initializer, _log); + + if (_keepClean) + host.KeepClean = true; + + if (_waitTime != host.WaitTime) + host.WaitTime = _waitTime; + + if (_state == ServerState.Start) + host.Start (); - if (data.LongLength <= WebSocket.FragmentLength) - broadcastAsync (Opcode.Binary, data, completed); - else - broadcastAsync (Opcode.Binary, new MemoryStream (data), completed); + _hosts.Add (path, host); + } } /// - /// Sends text asynchronously to every client in - /// the WebSocket services. + /// Removes all WebSocket services managed by the manager. /// /// - /// This method doesn't wait for the send to be complete. + /// Each service is stopped with close status 1001 (going away) + /// if the current state of the service is Start. /// - /// - /// A that represents the text data to send. - /// - /// - /// An delegate that references the method(s) called when - /// the send is complete. - /// - public void BroadcastAsync (string data, Action completed) + public void Clear () { - var msg = _state.CheckIfAvailable (false, true, false) ?? - WebSocket.CheckSendParameter (data); + List hosts = null; - if (msg != null) { - _logger.Error (msg); - return; + lock (_sync) { + hosts = _hosts.Values.ToList (); + + _hosts.Clear (); } - var bytes = data.UTF8Encode (); - if (bytes.LongLength <= WebSocket.FragmentLength) - broadcastAsync (Opcode.Text, bytes, completed); - else - broadcastAsync (Opcode.Text, new MemoryStream (bytes), completed); + foreach (var host in hosts) { + if (host.State == ServerState.Start) + host.Stop (1001, String.Empty); + } } /// - /// Sends binary data from the specified asynchronously to - /// every client in the WebSocket services. + /// Removes a WebSocket service with the specified path. /// /// - /// This method doesn't wait for the send to be complete. + /// The service is stopped with close status 1001 (going away) + /// if the current state of the service is Start. /// - /// - /// A from which contains the binary data to send. - /// - /// - /// An that represents the number of bytes to send. - /// - /// - /// An delegate that references the method(s) called when - /// the send is complete. + /// + /// true if the service is successfully found and removed; + /// otherwise, false. + /// + /// + /// + /// A that specifies an absolute path to + /// the service to remove. + /// + /// + /// / is trimmed from the end of the string if present. + /// /// - public void BroadcastAsync (Stream stream, int length, Action completed) + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is not an absolute path. + /// + /// + /// -or- + /// + /// + /// includes either or both + /// query and fragment components. + /// + /// + /// + /// is . + /// + public bool RemoveService (string path) { - var msg = _state.CheckIfAvailable (false, true, false) ?? - WebSocket.CheckSendParameters (stream, length); + if (path == null) + throw new ArgumentNullException ("path"); - if (msg != null) { - _logger.Error (msg); - return; - } + if (path.Length == 0) + throw new ArgumentException ("An empty string.", "path"); - stream.ReadBytesAsync ( - length, - data => { - var len = data.Length; - if (len == 0) { - _logger.Error ("The data cannot be read from 'stream'."); - return; - } - - if (len < length) - _logger.Warn ( - String.Format ( - "The data with 'length' cannot be read from 'stream':\n expected: {0}\n actual: {1}", - length, - len)); - - if (len <= WebSocket.FragmentLength) - broadcast (Opcode.Binary, data, completed); - else - broadcast (Opcode.Binary, new MemoryStream (data), completed); - }, - ex => _logger.Fatal (ex.ToString ())); - } + if (path[0] != '/') { + var msg = "Not an absolute path."; - /// - /// Sends a Ping to every client in the WebSocket services. - /// - /// - /// A Dictionary<string, Dictionary<string, bool>> that contains - /// a collection of pairs of a service path and a collection of pairs of a session ID - /// and a value indicating whether the manager received a Pong from each client in a time, - /// or if this method isn't available. - /// - public Dictionary> Broadping () - { - var msg = _state.CheckIfAvailable (false, true, false); - if (msg != null) { - _logger.Error (msg); - return null; + throw new ArgumentException (msg, "path"); } - return broadping (WebSocketFrame.EmptyPingBytes, _waitTime); - } + if (path.IndexOfAny (new[] { '?', '#' }) > -1) { + var msg = "It includes either or both query and fragment components."; - /// - /// Sends a Ping with the specified to every client in - /// the WebSocket services. - /// - /// - /// A Dictionary<string, Dictionary<string, bool>> that contains - /// a collection of pairs of a service path and a collection of pairs of a session ID - /// and a value indicating whether the manager received a Pong from each client in a time, - /// or if this method isn't available or - /// is invalid. - /// - /// - /// A that represents the message to send. - /// - public Dictionary> Broadping (string message) - { - if (message == null || message.Length == 0) - return Broadping (); + throw new ArgumentException (msg, "path"); + } + + path = path.TrimSlashFromEnd (); + WebSocketServiceHost host; - byte[] data = null; - var msg = _state.CheckIfAvailable (false, true, false) ?? - WebSocket.CheckPingParameter (message, out data); + lock (_sync) { + if (!_hosts.TryGetValue (path, out host)) + return false; - if (msg != null) { - _logger.Error (msg); - return null; + _hosts.Remove (path); } - return broadping (WebSocketFrame.CreatePingFrame (data, false).ToArray (), _waitTime); + if (host.State == ServerState.Start) + host.Stop (1001, String.Empty); + + return true; } /// - /// Tries to get the WebSocket service host with the specified . + /// Tries to get the service host instance for a WebSocket service with + /// the specified path. /// /// - /// true if the service is successfully found; otherwise, false. + /// true if the try has succeeded; otherwise, false. /// /// - /// A that represents the absolute path to the service to find. + /// + /// A that specifies an absolute path to + /// the service to get. + /// + /// + /// / is trimmed from the end of the string if present. + /// /// /// - /// When this method returns, a instance that - /// provides the access to the information in the service, or - /// if it's not found. This parameter is passed uninitialized. + /// + /// When this method returns, a + /// instance that receives the service host instance. + /// + /// + /// It provides the function to access the information in the service. + /// + /// + /// if not found. + /// /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is not an absolute path. + /// + /// + /// -or- + /// + /// + /// includes either or both + /// query and fragment components. + /// + /// + /// + /// is . + /// public bool TryGetServiceHost (string path, out WebSocketServiceHost host) { - var msg = _state.CheckIfAvailable (false, true, false) ?? path.CheckIfValidServicePath (); - if (msg != null) { - _logger.Error (msg); - host = null; + if (path == null) + throw new ArgumentNullException ("path"); + + if (path.Length == 0) + throw new ArgumentException ("An empty string.", "path"); + + if (path[0] != '/') { + var msg = "Not an absolute path."; + + throw new ArgumentException (msg, "path"); + } + + if (path.IndexOfAny (new[] { '?', '#' }) > -1) { + var msg = "It includes either or both query and fragment components."; - return false; + throw new ArgumentException (msg, "path"); } return InternalTryGetServiceHost (path, out host); diff --git a/websocket-sharp/Server/WebSocketSessionManager.cs b/websocket-sharp/Server/WebSocketSessionManager.cs index 88271994a..e1d1a2001 100644 --- a/websocket-sharp/Server/WebSocketSessionManager.cs +++ b/websocket-sharp/Server/WebSocketSessionManager.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2025 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 @@ -30,6 +30,7 @@ using System.Collections; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; using System.Threading; using System.Timers; @@ -37,15 +38,20 @@ namespace WebSocketSharp.Server { /// - /// Manages the sessions in a Websocket service. + /// Provides the management function for the sessions in a WebSocket service. /// + /// + /// This class manages the sessions in a WebSocket service provided by the + /// or class. + /// public class WebSocketSessionManager { #region Private Fields - private volatile bool _clean; private object _forSweep; - private Logger _logger; + private volatile bool _keepClean; + private Logger _log; + private static readonly byte[] _rawEmptyPingFrame; private Dictionary _sessions; private volatile ServerState _state; private volatile bool _sweeping; @@ -55,18 +61,21 @@ public class WebSocketSessionManager #endregion - #region Internal Constructors + #region Static Constructor - internal WebSocketSessionManager () - : this (new Logger ()) + static WebSocketSessionManager () { + _rawEmptyPingFrame = WebSocketFrame.CreatePingFrame (false).ToArray (); } - internal WebSocketSessionManager (Logger logger) + #endregion + + #region Internal Constructors + + internal WebSocketSessionManager (Logger log) { - _logger = logger; + _log = log; - _clean = true; _forSweep = new object (); _sessions = new Dictionary (); _state = ServerState.Ready; @@ -91,22 +100,29 @@ internal ServerState State { #region Public Properties /// - /// Gets the IDs for the active sessions in the Websocket service. + /// Gets the IDs for the active sessions in the WebSocket service. /// /// - /// An IEnumerable<string> instance that provides an enumerator which - /// supports the iteration over the collection of the IDs for the active sessions. + /// + /// An + /// instance. + /// + /// + /// It provides an enumerator which supports the iteration over + /// the collection of the IDs for the active sessions. + /// /// public IEnumerable ActiveIDs { get { - foreach (var res in Broadping (WebSocketFrame.EmptyPingBytes, _waitTime)) + foreach (var res in broadping (_rawEmptyPingFrame)) { if (res.Value) yield return res.Key; + } } } /// - /// Gets the number of the sessions in the Websocket service. + /// Gets the number of the sessions in the WebSocket service. /// /// /// An that represents the number of the sessions. @@ -119,114 +135,178 @@ public int Count { } /// - /// Gets the IDs for the sessions in the Websocket service. + /// Gets the IDs for the sessions in the WebSocket service. /// /// - /// An IEnumerable<string> instance that provides an enumerator which - /// supports the iteration over the collection of the IDs for the sessions. + /// + /// An + /// instance. + /// + /// + /// It provides an enumerator which supports the iteration over + /// the collection of the IDs for the sessions. + /// /// public IEnumerable IDs { get { - if (_state == ServerState.ShuttingDown) - return new string[0]; + if (_state != ServerState.Start) + return Enumerable.Empty (); + + lock (_sync) { + if (_state != ServerState.Start) + return Enumerable.Empty (); - lock (_sync) return _sessions.Keys.ToList (); + } } } /// - /// Gets the IDs for the inactive sessions in the Websocket service. + /// Gets the IDs for the inactive sessions in the WebSocket service. /// /// - /// An IEnumerable<string> instance that provides an enumerator which - /// supports the iteration over the collection of the IDs for the inactive sessions. + /// + /// An + /// instance. + /// + /// + /// It provides an enumerator which supports the iteration over + /// the collection of the IDs for the inactive sessions. + /// /// public IEnumerable InactiveIDs { get { - foreach (var res in Broadping (WebSocketFrame.EmptyPingBytes, _waitTime)) + foreach (var res in broadping (_rawEmptyPingFrame)) { if (!res.Value) yield return res.Key; + } } } /// - /// Gets the session with the specified . + /// Gets the session instance with the specified ID. /// /// - /// A instance that provides the access to - /// the information in the session, or if it's not found. + /// + /// A instance that provides + /// the function to access the information in the session. + /// + /// + /// if not found. + /// /// /// - /// A that represents the ID of the session to find. + /// A that specifies the ID of the session to get. /// + /// + /// is an empty string. + /// + /// + /// is . + /// public IWebSocketSession this[string id] { get { + if (id == null) + throw new ArgumentNullException ("id"); + + if (id.Length == 0) + throw new ArgumentException ("An empty string.", "id"); + IWebSocketSession session; - TryGetSession (id, out session); + + tryGetSession (id, out session); return session; } } /// - /// Gets a value indicating whether the manager cleans up the inactive sessions in - /// the WebSocket service periodically. + /// Gets or sets a value indicating whether the inactive sessions in + /// the WebSocket service are cleaned up periodically. /// + /// + /// The set operation works if the current state of the service is + /// Ready or Stop. + /// /// - /// true if the manager cleans up the inactive sessions every 60 seconds; + /// true if the inactive sessions are cleaned up every 60 seconds; /// otherwise, false. /// public bool KeepClean { get { - return _clean; + return _keepClean; } - internal set { - if (!(value ^ _clean)) - return; + set { + lock (_sync) { + if (!canSet ()) + return; - _clean = value; - if (_state == ServerState.Start) - _sweepTimer.Enabled = value; + _keepClean = value; + } } } /// - /// Gets the sessions in the Websocket service. + /// Gets the session instances in the WebSocket service. /// /// - /// An IEnumerable<IWebSocketSession> instance that provides an enumerator - /// which supports the iteration over the collection of the sessions in the service. + /// + /// An + /// instance. + /// + /// + /// It provides an enumerator which supports the iteration over + /// the collection of the session instances. + /// /// public IEnumerable Sessions { get { - if (_state == ServerState.ShuttingDown) - return new IWebSocketSession[0]; + if (_state != ServerState.Start) + return Enumerable.Empty (); + + lock (_sync) { + if (_state != ServerState.Start) + return Enumerable.Empty (); - lock (_sync) return _sessions.Values.ToList (); + } } } /// - /// Gets the wait time for the response to the WebSocket Ping or Close. + /// Gets or sets the time to wait for the response to the WebSocket + /// Ping or Close. /// + /// + /// The set operation works if the current state of the service is + /// Ready or Stop. + /// /// - /// A that represents the wait time. + /// A that represents the time to wait for + /// the response. /// + /// + /// The value specified for a set operation is zero or less. + /// public TimeSpan WaitTime { get { return _waitTime; } - internal set { - if (value == _waitTime) - return; + set { + if (value <= TimeSpan.Zero) { + var msg = "Zero or less."; + + throw new ArgumentOutOfRangeException ("value", msg); + } + + lock (_sync) { + if (!canSet ()) + return; - _waitTime = value; - foreach (var session in Sessions) - session.Context.WebSocket.WaitTime = value; + _waitTime = value; + } } } @@ -237,29 +317,55 @@ internal set { private void broadcast (Opcode opcode, byte[] data, Action completed) { var cache = new Dictionary (); + try { - Broadcast (opcode, data, cache); + foreach (var session in Sessions) { + if (_state != ServerState.Start) { + _log.Error ("The send is cancelled."); + + break; + } + + session.WebSocket.Send (opcode, data, cache); + } + if (completed != null) completed (); } catch (Exception ex) { - _logger.Fatal (ex.ToString ()); + _log.Error (ex.Message); + _log.Debug (ex.ToString ()); } finally { cache.Clear (); } } - private void broadcast (Opcode opcode, Stream stream, Action completed) + private void broadcast ( + Opcode opcode, + Stream sourceStream, + Action completed + ) { - var cache = new Dictionary (); + var cache = new Dictionary (); + try { - Broadcast (opcode, stream, cache); + foreach (var session in Sessions) { + if (_state != ServerState.Start) { + _log.Error ("The send is cancelled."); + + break; + } + + session.WebSocket.Send (opcode, sourceStream, cache); + } + if (completed != null) completed (); } catch (Exception ex) { - _logger.Fatal (ex.ToString ()); + _log.Error (ex.Message); + _log.Debug (ex.ToString ()); } finally { foreach (var cached in cache.Values) @@ -271,17 +377,44 @@ 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)); + ThreadPool.QueueUserWorkItem ( + state => broadcast (opcode, data, completed) + ); } - private void broadcastAsync (Opcode opcode, Stream stream, Action completed) + private void broadcastAsync ( + Opcode opcode, + Stream sourceStream, + Action completed + ) { - ThreadPool.QueueUserWorkItem (state => broadcast (opcode, stream, completed)); + ThreadPool.QueueUserWorkItem ( + state => broadcast (opcode, sourceStream, completed) + ); } - private static string createID () + private Dictionary broadping (byte[] rawFrame) { - return Guid.NewGuid ().ToString ("N"); + var ret = new Dictionary (); + + foreach (var session in Sessions) { + if (_state != ServerState.Start) { + ret.Clear (); + + break; + } + + var res = session.WebSocket.Ping (rawFrame); + + ret.Add (session.ID, res); + } + + return ret; + } + + private bool canSet () + { + return _state == ServerState.Ready || _state == ServerState.Stop; } private void setSweepTimer (double interval) @@ -290,68 +423,59 @@ private void setSweepTimer (double interval) _sweepTimer.Elapsed += (sender, e) => Sweep (); } - private bool tryGetSession (string id, out IWebSocketSession session) + private void stop (PayloadData payloadData, bool send) { - bool ret; - lock (_sync) - ret = _sessions.TryGetValue (id, out session); + var rawFrame = send + ? WebSocketFrame + .CreateCloseFrame (payloadData, false) + .ToArray () + : null; - if (!ret) - _logger.Error ("A session with the specified ID isn't found:\n ID: " + id); + lock (_sync) { + _state = ServerState.ShuttingDown; + _sweepTimer.Enabled = false; - return ret; + foreach (var session in _sessions.Values.ToList ()) + session.WebSocket.Close (payloadData, rawFrame); + + _state = ServerState.Stop; + } } - #endregion + private bool tryGetSession (string id, out IWebSocketSession session) + { + session = null; - #region Internal Methods + if (_state != ServerState.Start) + return false; - internal string Add (IWebSocketSession session) - { lock (_sync) { if (_state != ServerState.Start) - return null; + return false; - var id = createID (); - _sessions.Add (id, session); - - return id; + return _sessions.TryGetValue (id, out session); } } - internal void Broadcast ( - Opcode opcode, byte[] data, Dictionary cache) - { - foreach (var session in Sessions) { - if (_state != ServerState.Start) - break; + #endregion - session.Context.WebSocket.Send (opcode, data, cache); - } - } + #region Internal Methods - internal void Broadcast ( - Opcode opcode, Stream stream, Dictionary cache) + internal bool Add (IWebSocketSession session) { - foreach (var session in Sessions) { + lock (_sync) { if (_state != ServerState.Start) - break; + return false; - session.Context.WebSocket.Send (opcode, stream, cache); + _sessions.Add (session.ID, session); + + return true; } } - internal Dictionary Broadping (byte[] frameAsBytes, TimeSpan timeout) + internal static string CreateID () { - var ret = new Dictionary (); - foreach (var session in Sessions) { - if (_state != ServerState.Start) - break; - - ret.Add (session.ID, session.Context.WebSocket.Ping (frameAsBytes, timeout)); - } - - return ret; + return Guid.NewGuid ().ToString ("N"); } internal bool Remove (string id) @@ -363,22 +487,23 @@ internal bool Remove (string id) internal void Start () { lock (_sync) { - _sweepTimer.Enabled = _clean; + _sweepTimer.Enabled = _keepClean; _state = ServerState.Start; } } - internal void Stop (CloseEventArgs e, byte[] frameAsBytes, bool receive) + internal void Stop (ushort code, string reason) { - lock (_sync) { - _state = ServerState.ShuttingDown; - - _sweepTimer.Enabled = false; - foreach (var session in _sessions.Values.ToList ()) - session.Context.WebSocket.Close (e, frameAsBytes, receive); + if (code == 1005) { + stop (PayloadData.Empty, true); - _state = ServerState.Stop; + return; } + + var payloadData = new PayloadData (code, reason); + var send = !code.IsReservedStatusCode (); + + stop (payloadData, send); } #endregion @@ -386,21 +511,28 @@ internal void Stop (CloseEventArgs e, byte[] frameAsBytes, bool receive) #region Public Methods /// - /// Sends binary to every client in the WebSocket service. + /// Sends the specified data to every client in the WebSocket service. /// /// - /// An array of that represents the binary data to send. + /// An array of that specifies the binary data to send. /// + /// + /// is . + /// + /// + /// The current state of the service is not Start. + /// public void Broadcast (byte[] data) { - var msg = _state.CheckIfAvailable (false, true, false) ?? - WebSocket.CheckSendParameter (data); + if (_state != ServerState.Start) { + var msg = "The current state of the service is not Start."; - if (msg != null) { - _logger.Error (msg); - return; + throw new InvalidOperationException (msg); } + if (data == null) + throw new ArgumentNullException ("data"); + if (data.LongLength <= WebSocket.FragmentLength) broadcast (Opcode.Binary, data, null); else @@ -408,22 +540,39 @@ public void Broadcast (byte[] data) } /// - /// Sends text to every client in the WebSocket service. + /// Sends the specified data to every client in the WebSocket service. /// /// - /// A that represents the text data to send. + /// A that specifies the text data to send. /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// is . + /// + /// + /// The current state of the service is not Start. + /// public void Broadcast (string data) { - var msg = _state.CheckIfAvailable (false, true, false) ?? - WebSocket.CheckSendParameter (data); + if (_state != ServerState.Start) { + var msg = "The current state of the service is not Start."; - if (msg != null) { - _logger.Error (msg); - return; + 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"); } - var bytes = data.UTF8Encode (); if (bytes.LongLength <= WebSocket.FragmentLength) broadcast (Opcode.Text, bytes, null); else @@ -431,29 +580,126 @@ public void Broadcast (string data) } /// - /// Sends binary asynchronously to every client in + /// Sends the data from the specified stream instance to every client in /// the WebSocket service. /// + /// + /// + /// 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. + /// + /// + /// + /// cannot be read. + /// + /// + /// -or- + /// + /// + /// is less than 1. + /// + /// + /// -or- + /// + /// + /// No data could be read from . + /// + /// + /// + /// is . + /// + /// + /// The current state of the service is not Start. + /// + public void Broadcast (Stream stream, int length) + { + if (_state != ServerState.Start) { + var msg = "The current state of the service is not Start."; + + 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) { + var fmt = "Only {0} byte(s) of data could be read from the stream."; + var msg = String.Format (fmt, len); + + _log.Warn (msg); + } + + if (len <= WebSocket.FragmentLength) + broadcast (Opcode.Binary, bytes, null); + else + broadcast (Opcode.Binary, new MemoryStream (bytes), null); + } + + /// + /// Sends the specified data to every client in the WebSocket service + /// asynchronously. + /// /// - /// This method doesn't wait for the send to be complete. + /// This method does not wait for the send to be complete. /// /// - /// An array of that represents the binary data to send. + /// An array of that specifies the binary data to send. /// /// - /// An delegate that references the method(s) called when - /// the send is complete. + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the send is complete. + /// + /// + /// if not necessary. + /// /// + /// + /// is . + /// + /// + /// The current state of the service is not Start. + /// public void BroadcastAsync (byte[] data, Action completed) { - var msg = _state.CheckIfAvailable (false, true, false) ?? - WebSocket.CheckSendParameter (data); + if (_state != ServerState.Start) { + var msg = "The current state of the service is not Start."; - if (msg != null) { - _logger.Error (msg); - return; + throw new InvalidOperationException (msg); } + if (data == null) + throw new ArgumentNullException ("data"); + if (data.LongLength <= WebSocket.FragmentLength) broadcastAsync (Opcode.Binary, data, completed); else @@ -461,30 +707,54 @@ public void BroadcastAsync (byte[] data, Action completed) } /// - /// Sends text asynchronously to every client in - /// the WebSocket service. + /// Sends the specified data to every client in the WebSocket service + /// asynchronously. /// /// - /// This method doesn't wait for the send to be complete. + /// This method does not wait for the send to be complete. /// /// - /// A that represents the text data to send. + /// A that specifies the text data to send. /// /// - /// An delegate that references the method(s) called when - /// the send is complete. + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the send is complete. + /// + /// + /// if not necessary. + /// /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// is . + /// + /// + /// The current state of the service is not Start. + /// public void BroadcastAsync (string data, Action completed) { - var msg = _state.CheckIfAvailable (false, true, false) ?? - WebSocket.CheckSendParameter (data); + if (_state != ServerState.Start) { + var msg = "The current state of the service is not Start."; - if (msg != null) { - _logger.Error (msg); - return; + 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"); } - var bytes = data.UTF8Encode (); if (bytes.LongLength <= WebSocket.FragmentLength) broadcastAsync (Opcode.Text, bytes, completed); else @@ -492,304 +762,785 @@ public void BroadcastAsync (string data, Action completed) } /// - /// Sends binary data from the specified asynchronously to - /// every client in the WebSocket service. + /// Sends the data from the specified stream instance to every client in + /// the WebSocket service asynchronously. /// /// - /// This method doesn't wait for the send to be complete. + /// This method does not wait for the send to be complete. /// /// - /// A from which contains the binary data to send. + /// + /// A instance from which to read the data to send. + /// + /// + /// The data is sent as the binary data. + /// /// /// - /// An that represents the number of bytes to send. + /// An that specifies the number of bytes to send. /// /// - /// An delegate that references the method(s) called when - /// the send is complete. + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the send is complete. + /// + /// + /// if not necessary. + /// /// + /// + /// + /// cannot be read. + /// + /// + /// -or- + /// + /// + /// is less than 1. + /// + /// + /// -or- + /// + /// + /// No data could be read from . + /// + /// + /// + /// is . + /// + /// + /// The current state of the service is not Start. + /// public void BroadcastAsync (Stream stream, int length, Action completed) { - var msg = _state.CheckIfAvailable (false, true, false) ?? - WebSocket.CheckSendParameters (stream, length); + if (_state != ServerState.Start) { + var msg = "The current state of the service is not Start."; - if (msg != null) { - _logger.Error (msg); - return; + throw new InvalidOperationException (msg); } - stream.ReadBytesAsync ( - length, - data => { - var len = data.Length; - if (len == 0) { - _logger.Error ("The data cannot be read from 'stream'."); - return; - } + if (stream == null) + throw new ArgumentNullException ("stream"); - if (len < length) - _logger.Warn ( - String.Format ( - "The data with 'length' cannot be read from 'stream':\n expected: {0}\n actual: {1}", - length, - len)); + if (!stream.CanRead) { + var msg = "It cannot be read."; - if (len <= WebSocket.FragmentLength) - broadcast (Opcode.Binary, data, completed); - else - broadcast (Opcode.Binary, new MemoryStream (data), completed); - }, - ex => _logger.Fatal (ex.ToString ())); - } + throw new ArgumentException (msg, "stream"); + } - /// - /// Sends a Ping to every client in the WebSocket service. - /// - /// - /// A Dictionary<string, bool> that contains a collection of pairs of - /// a session ID and a value indicating whether the manager received a Pong from - /// each client in a time. - /// - public Dictionary Broadping () - { - var msg = _state.CheckIfAvailable (false, true, false); - if (msg != null) { - _logger.Error (msg); - return null; + if (length < 1) { + var msg = "Less than 1."; + + throw new ArgumentException (msg, "length"); } - return Broadping (WebSocketFrame.EmptyPingBytes, _waitTime); - } + var bytes = stream.ReadBytes (length); + var len = bytes.Length; - /// - /// Sends a Ping with the specified to every client in - /// the WebSocket service. - /// - /// - /// A Dictionary<string, bool> that contains a collection of pairs of - /// a session ID and a value indicating whether the manager received a Pong from - /// each client in a time. - /// - /// - /// A that represents the message to send. - /// - public Dictionary Broadping (string message) - { - if (message == null || message.Length == 0) - return Broadping (); + if (len == 0) { + var msg = "No data could be read from it."; + + throw new ArgumentException (msg, "stream"); + } - byte[] data = null; - var msg = _state.CheckIfAvailable (false, true, false) ?? - WebSocket.CheckPingParameter (message, out data); + if (len < length) { + var fmt = "Only {0} byte(s) of data could be read from the stream."; + var msg = String.Format (fmt, len); - if (msg != null) { - _logger.Error (msg); - return null; + _log.Warn (msg); } - return Broadping (WebSocketFrame.CreatePingFrame (data, false).ToArray (), _waitTime); + if (len <= WebSocket.FragmentLength) + broadcastAsync (Opcode.Binary, bytes, completed); + else + broadcastAsync (Opcode.Binary, new MemoryStream (bytes), completed); } /// - /// Closes the session with the specified . + /// Closes the session with the specified ID. /// /// - /// A that represents the ID of the session to close. + /// A that specifies the ID of the session to close. /// + /// + /// is an empty string. + /// + /// + /// is . + /// + /// + /// The session could not be found. + /// public void CloseSession (string id) { IWebSocketSession session; - if (TryGetSession (id, out session)) - session.Context.WebSocket.Close (); + + if (!TryGetSession (id, out session)) { + var msg = "The session could not be found."; + + throw new InvalidOperationException (msg); + } + + session.WebSocket.Close (); } /// - /// Closes the session with the specified , , - /// and . + /// Closes the session with the specified ID, status code, and reason. /// /// - /// A that represents the ID of the session to close. + /// A that specifies the ID of the session to close. /// /// - /// A that represents the status code indicating the reason for the close. + /// + /// A that specifies 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. + /// + /// A that specifies the reason for the close. + /// + /// + /// Its size must be 123 bytes or less in UTF-8. + /// /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is 1010 (mandatory extension). + /// + /// + /// -or- + /// + /// + /// is 1005 (no status) and + /// is specified. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// is . + /// + /// + /// + /// is less than 1000 or greater than 4999. + /// + /// + /// -or- + /// + /// + /// The size of is greater than 123 bytes. + /// + /// + /// + /// The session could not be found. + /// public void CloseSession (string id, ushort code, string reason) { IWebSocketSession session; - if (TryGetSession (id, out session)) - session.Context.WebSocket.Close (code, reason); + + if (!TryGetSession (id, out session)) { + var msg = "The session could not be found."; + + throw new InvalidOperationException (msg); + } + + session.WebSocket.Close (code, reason); } /// - /// Closes the session with the specified , , - /// and . + /// Closes the session with the specified ID, status code, and reason. /// /// - /// A that represents the ID of the session to close. + /// A that specifies the ID of the session to close. /// /// - /// One of the enum values, represents the status code - /// indicating the reason for the close. + /// + /// One of the enum values. + /// + /// + /// It specifies the status code indicating the reason for the close. + /// /// /// - /// A that represents the reason for the close. + /// + /// A that specifies the reason for the close. + /// + /// + /// Its size must be 123 bytes or less in UTF-8. + /// /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is an undefined enum value. + /// + /// + /// -or- + /// + /// + /// is . + /// + /// + /// -or- + /// + /// + /// is and + /// is specified. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// is . + /// + /// + /// The size of is greater than 123 bytes. + /// + /// + /// The session could not be found. + /// public void CloseSession (string id, CloseStatusCode code, string reason) { IWebSocketSession session; - if (TryGetSession (id, out session)) - session.Context.WebSocket.Close (code, reason); + + if (!TryGetSession (id, out session)) { + var msg = "The session could not be found."; + + throw new InvalidOperationException (msg); + } + + session.WebSocket.Close (code, reason); } /// - /// Sends a Ping to the client on the session with the specified . + /// Sends a ping to the client using the specified session. /// /// - /// true if the manager receives a Pong from the client in a time; - /// otherwise, false. + /// true if the send has successfully done and a pong has been + /// received within a time; otherwise, false. /// /// - /// A that represents the ID of the session to find. + /// A that specifies the ID of the session. /// + /// + /// is an empty string. + /// + /// + /// is . + /// + /// + /// The session could not be found. + /// public bool PingTo (string id) { IWebSocketSession session; - return TryGetSession (id, out session) && session.Context.WebSocket.Ping (); + + if (!TryGetSession (id, out session)) { + var msg = "The session could not be found."; + + throw new InvalidOperationException (msg); + } + + return session.WebSocket.Ping (); } /// - /// Sends a Ping with the specified to the client on - /// the session with the specified . + /// Sends a ping with the specified message to the client using + /// the specified session. /// /// - /// true if the manager receives a Pong from the client in a time; - /// otherwise, false. + /// true if the send has successfully done and a pong has been + /// received within a time; otherwise, false. /// /// - /// A that represents the message to send. + /// + /// A that specifies the message to send. + /// + /// + /// Its size must be 125 bytes or less in UTF-8. + /// /// /// - /// A that represents the ID of the session to find. + /// A that specifies the ID of the session. /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// is . + /// + /// + /// The size of is greater than 125 bytes. + /// + /// + /// The session could not be found. + /// public bool PingTo (string message, string id) { IWebSocketSession session; - return TryGetSession (id, out session) && session.Context.WebSocket.Ping (message); + + if (!TryGetSession (id, out session)) { + var msg = "The session could not be found."; + + throw new InvalidOperationException (msg); + } + + return session.WebSocket.Ping (message); } /// - /// Sends binary to the client on the session with - /// the specified . + /// Sends the specified data to the client using the specified session. /// /// - /// An array of that represents the binary data to send. + /// An array of that specifies the binary data to send. /// /// - /// A that represents the ID of the session to find. + /// A that specifies the ID of the session. /// + /// + /// is an empty string. + /// + /// + /// + /// is . + /// + /// + /// -or- + /// + /// + /// is . + /// + /// + /// + /// + /// The session could not be found. + /// + /// + /// -or- + /// + /// + /// The current state of the WebSocket interface is not Open. + /// + /// public void SendTo (byte[] data, string id) { IWebSocketSession session; - if (TryGetSession (id, out session)) - session.Context.WebSocket.Send (data); + + if (!TryGetSession (id, out session)) { + var msg = "The session could not be found."; + + throw new InvalidOperationException (msg); + } + + session.WebSocket.Send (data); } /// - /// Sends text to the client on the session with - /// the specified . + /// Sends the specified data to the client using the specified session. /// /// - /// A that represents the text data to send. + /// A that specifies the text data to send. /// /// - /// A that represents the ID of the session to find. + /// A that specifies the ID of the session. /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// + /// is . + /// + /// + /// -or- + /// + /// + /// is . + /// + /// + /// + /// + /// The session could not be found. + /// + /// + /// -or- + /// + /// + /// The current state of the WebSocket interface is not Open. + /// + /// public void SendTo (string data, string id) { IWebSocketSession session; - if (TryGetSession (id, out session)) - session.Context.WebSocket.Send (data); + + if (!TryGetSession (id, out session)) { + var msg = "The session could not be found."; + + throw new InvalidOperationException (msg); + } + + session.WebSocket.Send (data); + } + + /// + /// Sends the data from the specified stream instance to the client using + /// the specified session. + /// + /// + /// + /// 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. + /// + /// + /// A that specifies the ID of the session. + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// cannot be read. + /// + /// + /// -or- + /// + /// + /// is less than 1. + /// + /// + /// -or- + /// + /// + /// No data could be read from . + /// + /// + /// + /// + /// is . + /// + /// + /// -or- + /// + /// + /// is . + /// + /// + /// + /// + /// The session could not be found. + /// + /// + /// -or- + /// + /// + /// The current state of the WebSocket interface is not Open. + /// + /// + public void SendTo (Stream stream, int length, string id) + { + IWebSocketSession session; + + if (!TryGetSession (id, out session)) { + var msg = "The session could not be found."; + + throw new InvalidOperationException (msg); + } + + session.WebSocket.Send (stream, length); } /// - /// Sends binary asynchronously to the client on - /// the session with the specified . + /// Sends the specified data to the client using the specified session + /// asynchronously. /// /// - /// This method doesn't wait for the send to be complete. + /// This method does not wait for the send to be complete. /// /// - /// An array of that represents the binary data to send. + /// An array of that specifies the binary data to send. /// /// - /// A that represents the ID of the session to find. + /// A that specifies the ID of the session. /// /// - /// An Action<bool> delegate that references the method(s) called when - /// the send is complete. A passed to this delegate is true - /// if the send is complete successfully. + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the send is complete. + /// + /// + /// The parameter passed to the delegate is + /// true if the send has successfully done; otherwise, + /// false. + /// + /// + /// if not necessary. + /// /// + /// + /// is an empty string. + /// + /// + /// + /// is . + /// + /// + /// -or- + /// + /// + /// is . + /// + /// + /// + /// + /// The session could not be found. + /// + /// + /// -or- + /// + /// + /// The current state of the WebSocket interface is not Open. + /// + /// public void SendToAsync (byte[] data, string id, Action completed) { IWebSocketSession session; - if (TryGetSession (id, out session)) - session.Context.WebSocket.SendAsync (data, completed); + + if (!TryGetSession (id, out session)) { + var msg = "The session could not be found."; + + throw new InvalidOperationException (msg); + } + + session.WebSocket.SendAsync (data, completed); } /// - /// Sends text asynchronously to the client on - /// the session with the specified . + /// Sends the specified data to the client using the specified session + /// asynchronously. /// /// - /// This method doesn't wait for the send to be complete. + /// This method does not wait for the send to be complete. /// /// - /// A that represents the text data to send. + /// A that specifies the text data to send. /// /// - /// A that represents the ID of the session to find. + /// A that specifies the ID of the session. /// /// - /// An Action<bool> delegate that references the method(s) called when - /// the send is complete. A passed to this delegate is true - /// if the send is complete successfully. + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the send is complete. + /// + /// + /// The parameter passed to the delegate is + /// true if the send has successfully done; otherwise, + /// false. + /// + /// + /// if not necessary. + /// /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// + /// is . + /// + /// + /// -or- + /// + /// + /// is . + /// + /// + /// + /// + /// The session could not be found. + /// + /// + /// -or- + /// + /// + /// The current state of the WebSocket interface is not Open. + /// + /// public void SendToAsync (string data, string id, Action completed) { IWebSocketSession session; - if (TryGetSession (id, out session)) - session.Context.WebSocket.SendAsync (data, completed); + + if (!TryGetSession (id, out session)) { + var msg = "The session could not be found."; + + throw new InvalidOperationException (msg); + } + + session.WebSocket.SendAsync (data, completed); } /// - /// Sends binary data from the specified asynchronously to - /// the client on the session with the specified . + /// Sends the data from the specified stream instance to the client using + /// the specified session asynchronously. /// /// - /// This method doesn't wait for the send to be complete. + /// This method does not wait for the send to be complete. /// /// - /// A from which contains the binary data to send. + /// + /// A instance from which to read the data to send. + /// + /// + /// The data is sent as the binary data. + /// /// /// - /// An that represents the number of bytes to send. + /// An that specifies the number of bytes to send. /// /// - /// A that represents the ID of the session to find. + /// A that specifies the ID of the session. /// /// - /// An Action<bool> delegate that references the method(s) called when - /// the send is complete. A passed to this delegate is true - /// if the send is complete successfully. + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the send is complete. + /// + /// + /// The parameter passed to the delegate is + /// true if the send has successfully done; otherwise, + /// false. + /// + /// + /// if not necessary. + /// /// - public void SendToAsync (Stream stream, int length, string id, Action completed) + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// cannot be read. + /// + /// + /// -or- + /// + /// + /// is less than 1. + /// + /// + /// -or- + /// + /// + /// No data could be read from . + /// + /// + /// + /// + /// is . + /// + /// + /// -or- + /// + /// + /// is . + /// + /// + /// + /// + /// The session could not be found. + /// + /// + /// -or- + /// + /// + /// The current state of the WebSocket interface is not Open. + /// + /// + public void SendToAsync ( + Stream stream, + int length, + string id, + Action completed + ) { IWebSocketSession session; - if (TryGetSession (id, out session)) - session.Context.WebSocket.SendAsync (stream, length, completed); + + if (!TryGetSession (id, out session)) { + var msg = "The session could not be found."; + + throw new InvalidOperationException (msg); + } + + session.WebSocket.SendAsync (stream, length, completed); } /// @@ -797,56 +1548,88 @@ public void SendToAsync (Stream stream, int length, string id, Action comp /// public void Sweep () { - if (_state != ServerState.Start || _sweeping || Count == 0) + if (_sweeping) { + _log.Trace ("The sweep process is already in progress."); + return; + } lock (_forSweep) { + if (_sweeping) { + _log.Trace ("The sweep process is already in progress."); + + return; + } + _sweeping = true; - foreach (var id in InactiveIDs) { + } + + foreach (var id in InactiveIDs) { + if (_state != ServerState.Start) + break; + + lock (_sync) { if (_state != ServerState.Start) break; - lock (_sync) { - IWebSocketSession session; - if (_sessions.TryGetValue (id, out session)) { - var state = session.State; - if (state == WebSocketState.Open) - session.Context.WebSocket.Close (CloseStatusCode.ProtocolError); - else if (state == WebSocketState.Closing) - continue; - else - _sessions.Remove (id); - } + IWebSocketSession session; + + if (!_sessions.TryGetValue (id, out session)) + continue; + + var state = session.WebSocket.ReadyState; + + if (state == WebSocketState.Open) { + session.WebSocket.Close (CloseStatusCode.Abnormal); + + continue; } + + if (state == WebSocketState.Closing) + continue; + + _sessions.Remove (id); } + } + lock (_forSweep) _sweeping = false; - } } /// - /// Tries to get the session with the specified . + /// Tries to get the session instance with the specified ID. /// /// - /// true if the session is successfully found; otherwise, false. + /// true if the try has succeeded; otherwise, false. /// /// - /// A that represents the ID of the session to find. + /// A that specifies the ID of the session to get. /// /// - /// When this method returns, a instance that - /// provides the access to the information in the session, or - /// if it's not found. This parameter is passed uninitialized. + /// + /// When this method returns, a instance + /// that receives the session instance. + /// + /// + /// It provides the function to access the information in the session. + /// + /// + /// if not found. + /// /// + /// + /// is an empty string. + /// + /// + /// is . + /// public bool TryGetSession (string id, out IWebSocketSession session) { - var msg = _state.CheckIfAvailable (false, true, false) ?? id.CheckIfValidSessionID (); - if (msg != null) { - _logger.Error (msg); - session = null; + if (id == null) + throw new ArgumentNullException ("id"); - return false; - } + if (id.Length == 0) + throw new ArgumentException ("An empty string.", "id"); return tryGetSession (id, out session); } diff --git a/websocket-sharp/WebSocket.cs b/websocket-sharp/WebSocket.cs index 29661bec6..5d4727f42 100644 --- a/websocket-sharp/WebSocket.cs +++ b/websocket-sharp/WebSocket.cs @@ -2,15 +2,13 @@ /* * WebSocket.cs * - * A C# implementation of the WebSocket interface. - * * This code is derived from WebSocket.java * (http://github.com/adamac/Java-WebSocket-client). * * The MIT License * * Copyright (c) 2009 Adam MacBeth - * Copyright (c) 2010-2015 sta.blockhead + * Copyright (c) 2010-2025 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 @@ -61,74 +59,90 @@ namespace WebSocketSharp /// Implements the WebSocket interface. /// /// - /// The WebSocket class provides a set of methods and properties for two-way communication using - /// the WebSocket protocol (RFC 6455). + /// + /// 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 AutoResetEvent _exitReceiving; - private Opcode _fopcode; - private object _forConn; - private object _forEvent; - private object _forMessageEventQueue; - private object _forSend; - private MemoryStream _fragmentsBuffer; - private const string _guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - private Func - _handshakeRequestChecker; - private bool _ignoreExtensions; - private bool _inContinuation; - private volatile Logger _logger; - private Queue _messageEventQueue; - private uint _nonceCount; - private string _origin; - private bool _preAuth; - private string _protocol; - private string[] _protocols; - private NetworkCredential _proxyCredentials; - private Uri _proxyUri; - private volatile WebSocketState _readyState; - private AutoResetEvent _receivePong; - private bool _secure; - private ClientSslConfiguration _sslConfig; - private Stream _stream; - private TcpClient _tcpClient; - private Uri _uri; - private const string _version = "13"; - private TimeSpan _waitTime; + private AuthenticationChallenge _authChallenge; + private string _base64Key; + private Action _closeContext; + private CompressionMethod _compression; + private WebSocketContext _context; + private CookieCollection _cookies; + private NetworkCredential _credentials; + private bool _emitOnPing; + private static readonly byte[] _emptyBytes; + private bool _enableRedirection; + private string _extensions; + 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 Action _handshakeRequestResponder; + private CookieCollection _handshakeResponseCookies; + private NameValueCollection _handshakeResponseHeaders; + private bool _hasExtension; + private bool _hasProtocol; + private bool _ignoreExtensions; + private bool _inContinuation; + private volatile bool _inMessage; + private bool _isClient; + private bool _isSecure; + private volatile Logger _log; + private static readonly int _maxRetryCountForConnect; + private Action _message; + private Queue _messageEventQueue; + private bool _noDelay; + private uint _nonceCount; + private string _origin; + private ManualResetEvent _pongReceived; + private bool _preAuth; + private string _protocol; + private string[] _protocols; + private NetworkCredential _proxyCredentials; + private Uri _proxyUri; + private volatile WebSocketState _readyState; + private ManualResetEvent _receivingExited; + private int _retryCountForConnect; + private Socket _socket; + private ClientSslConfiguration _sslConfig; + private Stream _stream; + private TcpClient _tcpClient; + private Uri _uri; + private WebHeaderCollection _userHeaders; + 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. + /// 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. + /// The data will be fragmented if its 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. + /// 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; @@ -144,7 +158,9 @@ private Func static WebSocket () { - EmptyBytes = new byte[0]; + _emptyBytes = new byte[0]; + _maxRetryCountForConnect = 10; + FragmentLength = 1016; RandomNumber = new RNGCryptoServiceProvider (); } @@ -160,8 +176,10 @@ internal WebSocket (HttpListenerWebSocketContext context, string protocol) _protocol = protocol; _closeContext = context.Close; - _logger = context.Log; - _secure = context.IsSecureConnection; + _isSecure = context.IsSecureConnection; + _log = context.Log; + _message = messages; + _socket = context.Socket; _stream = context.Stream; _waitTime = TimeSpan.FromSeconds (1); @@ -175,8 +193,10 @@ internal WebSocket (TcpListenerWebSocketContext context, string protocol) _protocol = protocol; _closeContext = context.Close; - _logger = context.Log; - _secure = context.IsSecureConnection; + _isSecure = context.IsSecureConnection; + _log = context.Log; + _message = messages; + _socket = context.Socket; _stream = context.Stream; _waitTime = TimeSpan.FromSeconds (1); @@ -189,30 +209,56 @@ internal WebSocket (TcpListenerWebSocketContext context, string protocol) /// /// Initializes a new instance of the class with - /// the specified WebSocket URL and subprotocols. + /// the specified URL and optionally subprotocols. /// /// - /// A that represents the WebSocket URL to connect. + /// + /// 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 contains the WebSocket subprotocols if any. - /// Each value of must be a token defined in - /// RFC 2616. + /// + /// 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 invalid. + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is an invalid WebSocket URL string. + /// + /// + /// -or- + /// + /// + /// contains a value that is not a token. /// /// /// -or- /// /// - /// is invalid. + /// contains a value twice. /// /// + /// + /// is . + /// public WebSocket (string url, params string[] protocols) { if (url == null) @@ -222,21 +268,24 @@ public WebSocket (string url, params string[] protocols) 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) { - msg = protocols.CheckIfValidProtocols (); - if (msg != null) + if (!checkProtocols (protocols, out msg)) throw new ArgumentException (msg, "protocols"); _protocols = protocols; + _hasProtocol = true; } _base64Key = CreateBase64Key (); - _client = true; - _logger = new Logger (); - _secure = _uri.Scheme == "wss"; + _isClient = true; + _isSecure = _uri.Scheme == "wss"; + _log = new Logger (); + _message = messagec; + _retryCountForConnect = -1; _waitTime = TimeSpan.FromSeconds (5); init (); @@ -246,8 +295,11 @@ public WebSocket (string url, params string[] protocols) #region Internal Properties - internal CookieCollection CookieCollection { + internal CookieCollection Cookies { get { + if (_cookies == null) + _cookies = new CookieCollection (); + return _cookies; } } @@ -255,7 +307,7 @@ internal CookieCollection CookieCollection { // As server internal Func CustomHandshakeRequestChecker { get { - return _handshakeRequestChecker ?? (context => null); + return _handshakeRequestChecker; } set { @@ -263,6 +315,17 @@ internal Func CustomHandshakeRequestChecker { } } + // As server + internal Action CustomHandshakeRequestResponder { + get { + return _handshakeRequestResponder; + } + + set { + _handshakeRequestResponder = value; + } + } + // As server internal bool IgnoreExtensions { get { @@ -274,9 +337,17 @@ internal bool IgnoreExtensions { } } - internal bool IsConnected { + internal WebHeaderCollection UserHeaders { get { - return _readyState == WebSocketState.Open || _readyState == WebSocketState.Closing; + if (_userHeaders == null) { + var state = _isClient + ? HttpHeaderType.Request + : HttpHeaderType.Response; + + _userHeaders = new WebHeaderCollection (state, false); + } + + return _userHeaders; } } @@ -285,25 +356,49 @@ internal bool IsConnected { #region Public Properties /// - /// Gets or sets the compression method used to compress a message on the WebSocket connection. + /// Gets or sets the compression method used to compress a message. /// /// - /// One of the enum values, specifies the compression method - /// used to compress a message. The default value is . + /// + /// One of the enum values. + /// + /// + /// It indicates the compression method used to compress a message. + /// + /// + /// The default value is . + /// /// + /// + /// + /// The set operation is not available if the interface is not for + /// the client. + /// + /// + /// -or- + /// + /// + /// The set operation is not available when the current state of + /// the interface is neither New nor Closed. + /// + /// public CompressionMethod Compression { get { return _compression; } set { - lock (_forConn) { - var msg = checkIfAvailable (true, false, true, false, false, true); - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred in setting the compression.", null); + if (!_isClient) { + var msg = "The set operation is not available."; - return; + throw new InvalidOperationException (msg); + } + + lock (_forState) { + if (!canSet ()) { + var msg = "The set operation is not available."; + + throw new InvalidOperationException (msg); } _compression = value; @@ -311,28 +406,17 @@ public CompressionMethod Compression { } } - /// - /// Gets the HTTP cookies included in the WebSocket connection request and response. - /// - /// - /// An - /// instance that 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 for - /// the authentication. The default value is . + /// + /// A that represents the credentials + /// used to authenticate the client. + /// + /// + /// The default value is . + /// /// public NetworkCredential Credentials { get { @@ -341,44 +425,83 @@ public NetworkCredential Credentials { } /// - /// Gets or sets a value indicating whether the emits - /// a event when receives a ping. + /// Gets or sets a value indicating whether the interface emits + /// the message event when it receives a ping. /// /// - /// true if the emits a event - /// when receives a ping; otherwise, false. The default value is false. + /// + /// true if the interface emits the message event when + /// it receives a ping; otherwise, false. + /// + /// + /// The default value is false. + /// /// + /// + /// The set operation is not available when the current state of + /// the interface is neither New nor Closed. + /// public bool EmitOnPing { get { return _emitOnPing; } set { - _emitOnPing = value; + lock (_forState) { + if (!canSet ()) { + var msg = "The set operation is not available."; + + throw new InvalidOperationException (msg); + } + + _emitOnPing = value; + } } } /// - /// Gets or sets a value indicating whether the redirects - /// the connection request to the new URL located in the connection response. + /// Gets or sets a value indicating whether the URL redirection for + /// the handshake request is allowed. /// /// - /// true if the redirects the connection request to - /// the new URL; otherwise, false. The default value is false. + /// + /// true if the interface allows the URL redirection for + /// the handshake request; otherwise, false. + /// + /// + /// The default value is false. + /// /// + /// + /// + /// The set operation is not available if the interface is not for + /// the client. + /// + /// + /// -or- + /// + /// + /// The set operation is not available when the current state of + /// the interface is neither New nor Closed. + /// + /// public bool EnableRedirection { get { return _enableRedirection; } set { - lock (_forConn) { - var msg = checkIfAvailable (true, false, true, false, false, true); - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred in setting the enable redirection.", null); + if (!_isClient) { + var msg = "The set operation is not available."; - return; + throw new InvalidOperationException (msg); + } + + lock (_forState) { + if (!canSet ()) { + var msg = "The set operation is not available."; + + throw new InvalidOperationException (msg); } _enableRedirection = value; @@ -387,11 +510,16 @@ public bool EnableRedirection { } /// - /// Gets the WebSocket extensions selected by the server. + /// Gets the extensions selected by the server. /// /// - /// A that represents the extensions if any. - /// The default value is . + /// + /// A that represents a list of the extensions + /// negotiated between the client and server. + /// + /// + /// An empty string if not specified or selected. + /// /// public string Extensions { get { @@ -400,105 +528,290 @@ public string Extensions { } /// - /// Gets a value indicating whether the WebSocket connection is alive. + /// Gets the HTTP cookies included in the handshake response. + /// + /// + /// + /// A that contains the cookies + /// included in the handshake response if any. + /// + /// + /// if the interface could not receive + /// the handshake response. + /// + /// + /// + /// + /// The get operation is not available if the interface is not for + /// the client. + /// + /// + /// -or- + /// + /// + /// The get operation is not available when the current state of + /// the interface is New or Connecting. + /// + /// + public CookieCollection HandshakeResponseCookies { + get { + if (!_isClient) { + var msg = "The get operation is not available."; + + throw new InvalidOperationException (msg); + } + + lock (_forState) { + var canGet = _readyState > WebSocketState.Connecting; + + if (!canGet) { + var msg = "The get operation is not available."; + + throw new InvalidOperationException (msg); + } + + return _handshakeResponseCookies; + } + } + } + + /// + /// Gets the HTTP headers included in the handshake response. + /// + /// + /// + /// A that contains the headers + /// included in the handshake response. + /// + /// + /// if the interface could not receive + /// the handshake response. + /// + /// + /// + /// + /// The get operation is not available if the interface is not for + /// the client. + /// + /// + /// -or- + /// + /// + /// The get operation is not available when the current state of + /// the interface is New or Connecting. + /// + /// + public NameValueCollection HandshakeResponseHeaders { + get { + if (!_isClient) { + var msg = "The get operation is not available."; + + throw new InvalidOperationException (msg); + } + + lock (_forState) { + var canGet = _readyState > WebSocketState.Connecting; + + if (!canGet) { + var msg = "The get operation is not available."; + + throw new InvalidOperationException (msg); + } + + return _handshakeResponseHeaders; + } + } + } + + /// + /// Gets a value indicating whether the communication is possible. /// /// - /// true if the connection is alive; otherwise, false. + /// true if the communication is possible; otherwise, false. /// public bool IsAlive { get { - return Ping (); + return ping (_emptyBytes); } } /// - /// Gets a value indicating whether the WebSocket connection is secure. + /// Gets a value indicating whether the connection is secure. /// /// /// true if the connection is secure; otherwise, false. /// public bool IsSecure { get { - return _secure; + return _isSecure; } } /// - /// Gets the logging functions. + /// Gets the logging function. /// /// - /// The default logging level is . If you would like to change it, - /// you should set this Log.Level property to any of the enum - /// values. + /// The default logging level is . /// /// - /// A that provides the logging functions. + /// A that provides the logging function. /// + /// + /// The get operation is not available if the interface is not for + /// the client. + /// public Logger Log { get { - return _logger; + if (!_isClient) { + var msg = "The get operation is not available."; + + throw new InvalidOperationException (msg); + } + + return _log; } internal set { - _logger = value; + _log = value; + } + } + + /// + /// Gets or sets a value indicating whether the underlying TCP socket + /// disables a delay when send or receive buffer is not full. + /// + /// + /// + /// true if the delay is disabled; otherwise, false. + /// + /// + /// The default value is false. + /// + /// + /// + /// + /// The set operation is not available when the current state of + /// the interface is neither New nor Closed. + /// + public bool NoDelay { + get { + return _noDelay; + } + + set { + lock (_forState) { + if (!canSet ()) { + var msg = "The set operation is not available."; + + throw new InvalidOperationException (msg); + } + + _noDelay = value; + } } } /// /// Gets or sets the value of the HTTP Origin header to send with - /// the WebSocket connection request to the server. + /// the handshake request. /// /// - /// The sends the Origin header if this property has any. + /// + /// The HTTP Origin header is defined in + /// + /// Section 7 of RFC 6454. + /// + /// + /// The interface sends the Origin header if this property has any. + /// /// /// /// - /// A that represents the value of - /// the Origin header to send. - /// The default value is . + /// A that represents the value of the Origin + /// header to send. + /// + /// + /// The syntax is <scheme>://<host>[:<port>]. /// /// - /// The Origin header has the following syntax: - /// <scheme>://<host>[:<port>] + /// The default value is . /// /// + /// + /// + /// 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. + /// + /// + /// + /// + /// The set operation is not available if the interface is not for + /// the client. + /// + /// + /// -or- + /// + /// + /// The set operation is not available when the current state of + /// the interface is neither New nor Closed. + /// + /// public string Origin { get { return _origin; } set { - lock (_forConn) { - var msg = checkIfAvailable (true, false, true, false, false, true); - if (msg == null) { - if (value.IsNullOrEmpty ()) { - _origin = value; - return; - } - - Uri origin; - if (!Uri.TryCreate (value, UriKind.Absolute, out origin) || origin.Segments.Length > 1) - msg = "The syntax of an origin must be '://[:]'."; + if (!_isClient) { + var msg = "The set operation is not available."; + + throw new InvalidOperationException (msg); + } + + if (!value.IsNullOrEmpty ()) { + Uri uri; + + if (!Uri.TryCreate (value, UriKind.Absolute, out uri)) { + var msg = "Not an absolute URI string."; + + throw new ArgumentException (msg, "value"); } - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred in setting the origin.", null); + if (uri.Segments.Length > 1) { + var msg = "It includes the path segments."; - return; + throw new ArgumentException (msg, "value"); + } + } + + lock (_forState) { + if (!canSet ()) { + var msg = "The set operation is not available."; + + throw new InvalidOperationException (msg); } - _origin = value.TrimEnd ('/'); + _origin = !value.IsNullOrEmpty () ? value.TrimEnd ('/') : value; } } } /// - /// Gets the WebSocket subprotocol selected by the server. + /// Gets the name of subprotocol selected by the server. /// /// - /// A that represents the subprotocol if any. - /// The default value is . + /// + /// 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 { @@ -511,11 +824,18 @@ internal set { } /// - /// Gets the state of the WebSocket connection. + /// Gets the current state of the interface. /// /// - /// One of the enum values, indicates the state of the connection. - /// The default value is . + /// + /// One of the enum values. + /// + /// + /// It indicates the current state of the interface. + /// + /// + /// The default value is . + /// /// public WebSocketState ReadyState { get { @@ -524,70 +844,102 @@ public WebSocketState ReadyState { } /// - /// Gets or sets the SSL configuration used to authenticate the server and - /// optionally the client for secure connection. + /// Gets the configuration for secure connection. /// + /// + /// The configuration is used when the interface attempts to connect, + /// so it must be configured before any connect method is called. + /// /// - /// A that represents the configuration used - /// to authenticate the server and optionally the client for secure connection, - /// or if the is used in a server. + /// A that represents the + /// configuration used to establish a secure connection. /// + /// + /// + /// The get operation is not available if the interface is not for + /// the client. + /// + /// + /// -or- + /// + /// + /// The get operation is not available if the interface does not use + /// a secure connection. + /// + /// public ClientSslConfiguration SslConfiguration { get { - return _client - ? (_sslConfig ?? (_sslConfig = new ClientSslConfiguration (_uri.DnsSafeHost))) - : null; - } + if (!_isClient) { + var msg = "The get operation is not available."; - set { - lock (_forConn) { - var msg = checkIfAvailable (true, false, true, false, false, true); - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred in setting the ssl configuration.", null); + throw new InvalidOperationException (msg); + } - return; - } + if (!_isSecure) { + var msg = "The get operation is not available."; - _sslConfig = value; + throw new InvalidOperationException (msg); } + + return getSslConfiguration (); } } /// - /// Gets the WebSocket URL used to connect, or accepted. + /// Gets the URL to which to connect. /// /// - /// A that represents the URL used to connect, or accepted. + /// + /// A that represents the URL to which to connect. + /// + /// + /// Also it represents the URL requested by the client if the interface + /// is for the server. + /// /// public Uri Url { get { - return _client ? _uri : _context.RequestUri; + return _isClient ? _uri : _context.RequestUri; } } /// - /// Gets or sets the wait time for the response to the Ping or Close. + /// Gets or sets the time to wait for the response to the ping or close. /// /// - /// A that represents the wait time. The default value is the same as - /// 5 seconds, or 1 second if the is used in a server. + /// + /// A that represents the time to wait for + /// the response. + /// + /// + /// The default value is the same as 5 seconds if the interface is + /// for the client. + /// /// - public TimeSpan WaitTime { + /// + /// The value specified for a set operation is zero or less. + /// + /// + /// The set operation is not available when the current state of + /// the interface is neither New nor Closed. + /// + public TimeSpan WaitTime { get { return _waitTime; } set { - lock (_forConn) { - var msg = checkIfAvailable (true, true, true, false, false, true) ?? - value.CheckIfValidWaitTime (); + if (value <= TimeSpan.Zero) { + var msg = "Zero or less."; + + throw new ArgumentOutOfRangeException ("value", msg); + } - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred in setting the wait time.", null); + lock (_forState) { + if (!canSet ()) { + var msg = "The set operation is not available."; - return; + throw new InvalidOperationException (msg); } _waitTime = value; @@ -600,22 +952,22 @@ public TimeSpan WaitTime { #region Public Events /// - /// Occurs when the WebSocket connection has been closed. + /// Occurs when the connection has been closed. /// public event EventHandler OnClose; /// - /// Occurs when the gets an error. + /// Occurs when the interface gets an error. /// public event EventHandler OnError; /// - /// Occurs when the receives a message. + /// Occurs when the interface receives a message. /// public event EventHandler OnMessage; /// - /// Occurs when the WebSocket connection has been established. + /// Occurs when the connection has been established. /// public event EventHandler OnOpen; @@ -623,2012 +975,3535 @@ public TimeSpan WaitTime { #region Private Methods + private void abort (string reason, Exception exception) + { + var code = exception is WebSocketException + ? ((WebSocketException) exception).Code + : (ushort) 1006; + + abort (code, reason); + } + + private void abort (ushort code, string reason) + { + var data = new PayloadData (code, reason); + + close (data, false, false); + } + // As server private bool accept () { - lock (_forConn) { - var msg = _readyState.CheckIfAvailable (true, false, false, false); - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred in accepting.", null); + lock (_forState) { + if (_readyState == WebSocketState.Open) { + _log.Trace ("The connection has already been established."); + + return false; + } + + if (_readyState == WebSocketState.Closing) { + _log.Error ("The close process is in progress."); + + error ("An error has occurred before accepting.", null); + + return false; + } + + if (_readyState == WebSocketState.Closed) { + _log.Error ("The connection has been closed."); + + error ("An error has occurred before accepting.", null); return false; } + _readyState = WebSocketState.Connecting; + + var accepted = false; + try { - if (acceptHandshake ()) { - _readyState = WebSocketState.Open; - return true; - } + accepted = acceptHandshake (); } catch (Exception ex) { - processException (ex, "An exception has occurred while accepting."); + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + + abort (1011, "An exception has occurred while accepting."); } - return false; + if (!accepted) + return false; + + _readyState = WebSocketState.Open; + + return true; } } // As server private bool acceptHandshake () { - _logger.Debug ( - String.Format ("A connection request from {0}:\n{1}", _context.UserEndPoint, _context)); + string msg; + + if (!checkHandshakeRequest (_context, out msg)) { + _log.Error (msg); + _log.Debug (_context.ToString ()); + + refuseHandshake (1002, "A handshake error has occurred."); + + return false; + } + + if (!customCheckHandshakeRequest (_context, out msg)) { + _log.Error (msg); + _log.Debug (_context.ToString ()); - var msg = checkIfValidHandshakeRequest (_context); - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred while accepting.", null); - Close (HttpStatusCode.BadRequest); + refuseHandshake (1002, "A handshake error has occurred."); return false; } - if (_protocol != null && - !_context.SecWebSocketProtocols.Contains (protocol => protocol == _protocol)) - _protocol = null; + _base64Key = _context.Headers["Sec-WebSocket-Key"]; + + if (_protocol != null) { + var matched = _context + .SecWebSocketProtocols + .Contains (p => p == _protocol); + + if (!matched) + _protocol = null; + } if (!_ignoreExtensions) { - var exts = _context.Headers["Sec-WebSocket-Extensions"]; - if (exts != null && exts.Length > 0) - processSecWebSocketExtensionsHeader (exts); + var val = _context.Headers["Sec-WebSocket-Extensions"]; + + processSecWebSocketExtensionsClientHeader (val); } - return sendHttpResponse (createHandshakeResponse ()); - } + customRespondToHandshakeRequest (_context); - private string checkIfAvailable ( - bool client, bool server, bool connecting, bool open, bool closing, bool closed) - { - return !client && _client - ? "This operation isn't available in: client" - : !server && !_client - ? "This operation isn't available in: server" - : _readyState.CheckIfAvailable (connecting, open, closing, closed); + if (_noDelay) + _socket.NoDelay = true; + + createHandshakeResponse ().WriteTo (_stream); + + return true; } - // As server - private string checkIfValidHandshakeRequest (WebSocketContext context) + private bool canSet () { - var headers = context.Headers; - return context.RequestUri == null - ? "Specifies an invalid Request-URI." - : !context.IsWebSocketRequest - ? "Not a WebSocket connection request." - : !validateSecWebSocketKeyHeader (headers["Sec-WebSocket-Key"]) - ? "Includes an invalid Sec-WebSocket-Key header." - : !validateSecWebSocketVersionClientHeader (headers["Sec-WebSocket-Version"]) - ? "Includes an invalid Sec-WebSocket-Version header." - : CustomHandshakeRequestChecker (context); + return _readyState == WebSocketState.New + || _readyState == WebSocketState.Closed; } - // As client - private string checkIfValidHandshakeResponse (HttpResponse response) + // As server + private bool checkHandshakeRequest ( + WebSocketContext context, + out string message + ) { - var headers = response.Headers; - return response.IsRedirect - ? "Indicates the redirection." - : response.IsUnauthorized - ? "Requires the authentication." - : !response.IsWebSocketResponse - ? "Not a WebSocket connection response." - : !validateSecWebSocketAcceptHeader (headers["Sec-WebSocket-Accept"]) - ? "Includes an invalid Sec-WebSocket-Accept header." - : !validateSecWebSocketProtocolHeader (headers["Sec-WebSocket-Protocol"]) - ? "Includes an invalid Sec-WebSocket-Protocol header." - : !validateSecWebSocketExtensionsHeader (headers["Sec-WebSocket-Extensions"]) - ? "Includes an invalid Sec-WebSocket-Extensions header." - : !validateSecWebSocketVersionServerHeader (headers["Sec-WebSocket-Version"]) - ? "Includes an invalid Sec-WebSocket-Version header." - : null; - } - - private string checkIfValidReceivedFrame (WebSocketFrame frame) - { - var masked = frame.IsMasked; - return _client && masked - ? "A frame from the server is masked." - : !_client && !masked - ? "A frame from a client isn't masked." - : _inContinuation && frame.IsData - ? "A data frame has been received while receiving the fragmented data." - : frame.IsCompressed && _compression == CompressionMethod.None - ? "A compressed frame is without an available decompression method." - : null; - } - - private void close (CloseEventArgs e, bool send, bool receive, bool received) - { - lock (_forConn) { - if (_readyState == WebSocketState.Closing) { - _logger.Info ("The closing is already in progress."); - return; - } - - if (_readyState == WebSocketState.Closed) { - _logger.Info ("The connection has been closed."); - return; - } + message = null; - send = send && _readyState == WebSocketState.Open; - receive = receive && send; + if (!context.IsWebSocketRequest) { + message = "Not a WebSocket handshake request."; - _readyState = WebSocketState.Closing; + return false; } - _logger.Trace ("Begin closing the connection."); + var headers = context.Headers; - var bytes = send ? WebSocketFrame.CreateCloseFrame (e.PayloadData, _client).ToArray () : null; - e.WasClean = closeHandshake (bytes, receive, received); - releaseResources (); + var key = headers["Sec-WebSocket-Key"]; - _logger.Trace ("End closing the connection."); + if (key == null) { + message = "The Sec-WebSocket-Key header is non-existent."; - _readyState = WebSocketState.Closed; - try { - OnClose.Emit (this, e); + return false; } - catch (Exception ex) { - _logger.Fatal (ex.ToString ()); - error ("An exception has occurred during an OnClose event.", ex); + + if (key.Length == 0) { + message = "The Sec-WebSocket-Key header is invalid."; + + return false; } - } - private void closeAsync (CloseEventArgs e, bool send, bool receive, bool received) - { - Action closer = close; - closer.BeginInvoke (e, send, receive, received, ar => closer.EndInvoke (ar), null); - } + var ver = headers["Sec-WebSocket-Version"]; - private bool closeHandshake (byte[] frameAsBytes, bool receive, bool received) - { - var sent = frameAsBytes != null && sendBytes (frameAsBytes); - received = received || - (receive && sent && _exitReceiving != null && _exitReceiving.WaitOne (_waitTime)); + if (ver == null) { + message = "The Sec-WebSocket-Version header is non-existent."; - var ret = sent && received; - _logger.Debug ( - String.Format ("Was clean?: {0}\n sent: {1}\n received: {2}", ret, sent, received)); + return false; + } - return ret; - } + if (ver != _version) { + message = "The Sec-WebSocket-Version header is invalid."; - // As client - private bool connect () - { - lock (_forConn) { - var msg = _readyState.CheckIfAvailable (true, false, false, true); - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred in connecting.", null); + return false; + } + + var subps = headers["Sec-WebSocket-Protocol"]; + + if (subps != null) { + if (subps.Length == 0) { + message = "The Sec-WebSocket-Protocol header is invalid."; return false; } + } - try { - _readyState = WebSocketState.Connecting; - if (doHandshake ()) { - _readyState = WebSocketState.Open; - return true; + if (!_ignoreExtensions) { + var exts = headers["Sec-WebSocket-Extensions"]; + + if (exts != null) { + if (exts.Length == 0) { + message = "The Sec-WebSocket-Extensions header is invalid."; + + return false; } } - catch (Exception ex) { - processException (ex, "An exception has occurred while connecting."); - } - - return false; } + + return true; } // As client - private string createExtensions () + private bool checkHandshakeResponse ( + HttpResponse response, + out string message + ) { - var buff = new StringBuilder (80); + message = null; - if (_compression != CompressionMethod.None) { - var str = _compression.ToExtensionString ( - "server_no_context_takeover", "client_no_context_takeover"); + if (response.IsRedirect) { + message = "The redirection is indicated."; - buff.AppendFormat ("{0}, ", str); + return false; } - var len = buff.Length; - if (len > 2) { - buff.Length = len - 2; - return buff.ToString (); + if (response.IsUnauthorized) { + message = "The authentication is required."; + + return false; } - return null; - } + if (!response.IsWebSocketResponse) { + message = "Not a WebSocket handshake response."; - // As server - private HttpResponse createHandshakeCloseResponse (HttpStatusCode code) - { - var ret = HttpResponse.CreateCloseResponse (code); - ret.Headers["Sec-WebSocket-Version"] = _version; + return false; + } - return ret; - } + var headers = response.Headers; - // As client - private HttpRequest createHandshakeRequest () - { - var ret = HttpRequest.CreateWebSocketRequest (_uri); + var key = headers["Sec-WebSocket-Accept"]; - var headers = ret.Headers; - if (!_origin.IsNullOrEmpty ()) - headers["Origin"] = _origin; + if (key == null) { + message = "The Sec-WebSocket-Accept header is non-existent."; - headers["Sec-WebSocket-Key"] = _base64Key; + return false; + } - if (_protocols != null) - headers["Sec-WebSocket-Protocol"] = _protocols.ToString (", "); + if (key != CreateResponseKey (_base64Key)) { + message = "The Sec-WebSocket-Accept header is invalid."; - var exts = createExtensions (); - if (exts != null) - headers["Sec-WebSocket-Extensions"] = exts; + return false; + } - headers["Sec-WebSocket-Version"] = _version; + var ver = headers["Sec-WebSocket-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 (ver != null) { + if (ver != _version) { + message = "The Sec-WebSocket-Version header is invalid."; + + return false; + } } - if (authRes != null) - headers["Authorization"] = authRes.ToString (); + var subp = headers["Sec-WebSocket-Protocol"]; - if (_cookies.Count > 0) - ret.SetCookies (_cookies); + if (subp == null) { + if (_hasProtocol) { + message = "The Sec-WebSocket-Protocol header is non-existent."; - return ret; - } + return false; + } + } + else { + var isValid = _hasProtocol + && subp.Length > 0 + && _protocols.Contains (p => p == subp); - // As server - private HttpResponse createHandshakeResponse () - { - var ret = HttpResponse.CreateWebSocketResponse (); + if (!isValid) { + message = "The Sec-WebSocket-Protocol header is invalid."; - var headers = ret.Headers; - headers["Sec-WebSocket-Accept"] = CreateResponseKey (_base64Key); + return false; + } + } - if (_protocol != null) - headers["Sec-WebSocket-Protocol"] = _protocol; + var exts = headers["Sec-WebSocket-Extensions"]; - if (_extensions != null) - headers["Sec-WebSocket-Extensions"] = _extensions; + if (exts != null) { + if (!validateSecWebSocketExtensionsServerHeader (exts)) { + message = "The Sec-WebSocket-Extensions header is invalid."; - if (_cookies.Count > 0) - ret.SetCookies (_cookies); + return false; + } + } - return ret; + return true; } - private MessageEventArgs dequeueFromMessageEventQueue () + private static bool checkProtocols (string[] protocols, out string message) { - lock (_forMessageEventQueue) - return _messageEventQueue.Count > 0 ? _messageEventQueue.Dequeue () : null; - } + message = null; - // As client - private bool doHandshake () - { - setClientStream (); - var res = sendHandshakeRequest (); - var msg = checkIfValidHandshakeResponse (res); - if (msg != null) { - _logger.Error (msg); + Func cond = p => p.IsNullOrEmpty () || !p.IsToken (); - msg = "An error has occurred while connecting."; - error (msg, null); - close (new CloseEventArgs (CloseStatusCode.Abnormal, msg), false, false, false); + if (protocols.Contains (cond)) { + message = "It contains a value that is not a token."; return false; } - var cookies = res.Cookies; - if (cookies.Count > 0) - _cookies.SetOrRemove (cookies); + if (protocols.ContainsTwice ()) { + message = "It contains a value twice."; + + return false; + } return true; } - private void enqueueToMessageEventQueue (MessageEventArgs e) + // As client + private bool checkProxyConnectResponse ( + HttpResponse response, + out string message + ) { - lock (_forMessageEventQueue) - _messageEventQueue.Enqueue (e); - } + message = null; - private void error (string message, Exception exception) - { - try { - OnError.Emit (this, new ErrorEventArgs (message, exception)); + if (response.IsProxyAuthenticationRequired) { + message = "The proxy authentication is required."; + + return false; } - catch (Exception ex) { - _logger.Fatal (ex.ToString ()); + + if (!response.IsSuccess) { + message = "The proxy has failed a connection to the requested URL."; + + return false; } - } - private void init () - { - _compression = CompressionMethod.None; - _cookies = new CookieCollection (); - _forConn = new object (); - _forEvent = new object (); - _forSend = new object (); - _messageEventQueue = new Queue (); - _forMessageEventQueue = ((ICollection) _messageEventQueue).SyncRoot; - _readyState = WebSocketState.Connecting; + return true; } - private void open () + private bool checkReceivedFrame (WebSocketFrame frame, out string message) { - try { - startReceiving (); + message = null; - lock (_forEvent) { - try { - OnOpen.Emit (this, EventArgs.Empty); - } - catch (Exception ex) { - processException (ex, "An exception has occurred during an OnOpen event."); - } + if (frame.IsMasked) { + if (_isClient) { + message = "A frame from the server is masked."; + + return false; } } - catch (Exception ex) { - processException (ex, "An exception has occurred while opening."); - } - } + else { + if (!_isClient) { + message = "A frame from a client is not masked."; - private bool processCloseFrame (WebSocketFrame frame) - { - var payload = frame.PayloadData; - close (new CloseEventArgs (payload), !payload.IncludesReservedCloseStatusCode, false, true); + return false; + } + } - return false; - } + if (frame.IsCompressed) { + if (_compression == CompressionMethod.None) { + message = "A frame is compressed without any agreement for it."; - private bool processDataFrame (WebSocketFrame frame) - { - enqueueToMessageEventQueue ( - frame.IsCompressed - ? new MessageEventArgs ( - frame.Opcode, frame.PayloadData.ApplicationData.Decompress (_compression)) - : new MessageEventArgs (frame)); + return false; + } - return true; - } + if (!frame.IsData) { + message = "A non data frame is compressed."; - private void processException (Exception exception, string message) - { - var code = CloseStatusCode.Abnormal; - var reason = message; - if (exception is WebSocketException) { - var wsex = (WebSocketException) exception; - code = wsex.Code; - reason = wsex.Message; + return false; + } } - if (code == CloseStatusCode.Abnormal || code == CloseStatusCode.TlsHandshakeFailure) - _logger.Fatal (exception.ToString ()); - else - _logger.Error (reason); + if (frame.IsData) { + if (_inContinuation) { + message = "A data frame was received while receiving continuation frames."; - error (message ?? code.GetMessage (), exception); - if (!_client && _readyState == WebSocketState.Connecting) { - Close (HttpStatusCode.BadRequest); - return; + return false; + } } - close ( - new CloseEventArgs (code, reason ?? code.GetMessage ()), !code.IsReserved (), false, false); - } + if (frame.IsControl) { + if (frame.Fin == Fin.More) { + message = "A control frame is fragmented."; - private bool processFragmentedFrame (WebSocketFrame frame) - { - if (!_inContinuation) { - // Must process first fragment. - if (frame.IsContinuation) - return true; + return false; + } - _fopcode = frame.Opcode; - _fragmentsBuffer = new MemoryStream (); - _inContinuation = true; + if (frame.PayloadLength > 125) { + message = "The payload length of a control frame is greater than 125."; + + return false; + } } - _fragmentsBuffer.WriteBytes (frame.PayloadData.ApplicationData, 1024); - if (frame.IsFinal) { - using (_fragmentsBuffer) { - var data = _compression != CompressionMethod.None - ? _fragmentsBuffer.DecompressToArray (_compression) - : _fragmentsBuffer.ToArray (); + if (frame.Rsv2 == Rsv.On) { + message = "The RSV2 of a frame is non-zero without any negotiation for it."; - enqueueToMessageEventQueue (new MessageEventArgs (_fopcode, data)); - } + return false; + } - _fragmentsBuffer = null; - _inContinuation = 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 bool processPingFrame (WebSocketFrame frame) + private void close (ushort code, string reason) { - if (send (new WebSocketFrame (Opcode.Pong, frame.PayloadData, _client).ToArray ())) - _logger.Trace ("Returned a pong."); + if (_readyState == WebSocketState.Closing) { + _log.Trace ("The close process is already in progress."); - if (_emitOnPing) - enqueueToMessageEventQueue (new MessageEventArgs (frame)); + return; + } - return true; - } + if (_readyState == WebSocketState.Closed) { + _log.Trace ("The connection has already been closed."); - private bool processPongFrame (WebSocketFrame frame) - { - _receivePong.Set (); - _logger.Trace ("Received a pong."); + return; + } - return true; - } + if (code == 1005) { + close (PayloadData.Empty, true, false); - private bool processReceivedFrame (WebSocketFrame frame) - { - var msg = checkIfValidReceivedFrame (frame); - if (msg != null) - return processUnsupportedFrame (frame, CloseStatusCode.ProtocolError, msg); + return; + } - frame.Unmask (); - return frame.IsFragmented - ? processFragmentedFrame (frame) - : frame.IsData - ? processDataFrame (frame) - : frame.IsPing - ? processPingFrame (frame) - : frame.IsPong - ? processPongFrame (frame) - : frame.IsClose - ? processCloseFrame (frame) - : processUnsupportedFrame (frame, CloseStatusCode.UnsupportedData, null); + var data = new PayloadData (code, reason); + var send = !code.IsReservedStatusCode (); + + close (data, send, false); } - // As server - private void processSecWebSocketExtensionsHeader (string value) + private void close (PayloadData payloadData, bool send, bool received) { - var buff = new StringBuilder (80); + lock (_forState) { + if (_readyState == WebSocketState.Closing) { + _log.Trace ("The close process is already in progress."); + + return; + } - var comp = false; - foreach (var e in value.SplitHeaderValue (',')) { - var ext = e.Trim (); - if (!comp && ext.IsCompressionExtension (CompressionMethod.Deflate)) { - _compression = CompressionMethod.Deflate; - var str = _compression.ToExtensionString ( - "client_no_context_takeover", "server_no_context_takeover"); + if (_readyState == WebSocketState.Closed) { + _log.Trace ("The connection has already been closed."); - buff.AppendFormat ("{0}, ", str); - comp = true; + return; } - } - var len = buff.Length; - if (len > 2) { - buff.Length = len - 2; - _extensions = buff.ToString (); + send = send && _readyState == WebSocketState.Open; + + _readyState = WebSocketState.Closing; } - } - private bool processUnsupportedFrame (WebSocketFrame frame, CloseStatusCode code, string reason) - { - _logger.Debug ("An unsupported frame:" + frame.PrintToString (false)); - processException (new WebSocketException (code, reason), null); + _log.Trace ("Begin closing the connection."); - return false; - } + var res = closeHandshake (payloadData, send, received); - // As client - private void releaseClientResources () - { - if (_stream != null) { - _stream.Dispose (); - _stream = null; - } + releaseResources (); - if (_tcpClient != null) { - _tcpClient.Close (); - _tcpClient = null; - } - } + _log.Trace ("End closing the connection."); - private void releaseCommonResources () - { - if (_fragmentsBuffer != null) { - _fragmentsBuffer.Dispose (); - _fragmentsBuffer = null; - _inContinuation = false; - } + _readyState = WebSocketState.Closed; - if (_receivePong != null) { - _receivePong.Close (); - _receivePong = null; - } + var e = new CloseEventArgs (payloadData, res); - if (_exitReceiving != null) { - _exitReceiving.Close (); - _exitReceiving = null; + try { + OnClose.Emit (this, e); + } + catch (Exception ex) { + _log.Error (ex.Message); + _log.Debug (ex.ToString ()); } } - private void releaseResources () + private void closeAsync (ushort code, string reason) { - if (_client) - releaseClientResources (); - else - releaseServerResources (); - - releaseCommonResources (); - } + if (_readyState == WebSocketState.Closing) { + _log.Trace ("The close process is already in progress."); - // As server - private void releaseServerResources () - { - if (_closeContext == null) return; + } - _closeContext (); - _closeContext = null; - _stream = null; - _context = null; - } - - private bool send (byte[] frameAsBytes) - { - lock (_forConn) { - if (_readyState != WebSocketState.Open) { - _logger.Error ("The sending has been interrupted."); - return false; - } + if (_readyState == WebSocketState.Closed) { + _log.Trace ("The connection has already been closed."); - return sendBytes (frameAsBytes); + return; } - } - 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; - } + if (code == 1005) { + closeAsync (PayloadData.Empty, true, false); - sent = send (opcode, stream, compressed); - if (!sent) - error ("The sending has been interrupted.", null); - } - catch (Exception ex) { - _logger.Fatal (ex.ToString ()); - error ("An exception has occurred while sending data.", ex); - } - finally { - if (compressed) - stream.Dispose (); + return; + } - src.Dispose (); - } + var data = new PayloadData (code, reason); + var send = !code.IsReservedStatusCode (); - return sent; - } + closeAsync (data, send, false); } - private bool send (Opcode opcode, Stream stream, bool compressed) + private void closeAsync (PayloadData payloadData, bool send, bool received) { - var len = stream.Length; + Action closer = close; + + closer.BeginInvoke ( + payloadData, + send, + received, + ar => closer.EndInvoke (ar), + null + ); + } - /* Not fragmented */ + private bool closeHandshake ( + PayloadData payloadData, + bool send, + bool received + ) + { + var sent = false; - if (len == 0) - return send (Fin.Final, opcode, EmptyBytes, compressed); + if (send) { + var frame = WebSocketFrame.CreateCloseFrame (payloadData, _isClient); + var bytes = frame.ToArray (); - var quo = len / FragmentLength; - var rem = (int) (len % FragmentLength); + sent = sendBytes (bytes); - byte[] buff = null; - if (quo == 0) { - buff = new byte[rem]; - return stream.Read (buff, 0, rem) == rem && - send (Fin.Final, opcode, buff, compressed); + if (_isClient) + frame.Unmask (); } - buff = new byte[FragmentLength]; - if (quo == 1 && rem == 0) - return stream.Read (buff, 0, FragmentLength) == FragmentLength && - send (Fin.Final, opcode, buff, compressed); - - /* Send fragmented */ - - // Begin - if (stream.Read (buff, 0, FragmentLength) != FragmentLength || - !send (Fin.More, opcode, buff, compressed)) - return false; + var wait = !received && sent && _receivingExited != null; - var n = rem == 0 ? quo - 2 : quo - 1; - for (long i = 0; i < n; i++) - if (stream.Read (buff, 0, FragmentLength) != FragmentLength || - !send (Fin.More, Opcode.Cont, buff, compressed)) - return false; + if (wait) + received = _receivingExited.WaitOne (_waitTime); - // End - if (rem == 0) - rem = FragmentLength; - else - buff = new byte[rem]; + var ret = sent && received; - return stream.Read (buff, 0, rem) == rem && send (Fin.Final, Opcode.Cont, buff, compressed); - } + var msg = String.Format ( + "The closing was clean? {0} (sent: {1} received: {2})", + ret, + sent, + received + ); - private bool send (Fin fin, Opcode opcode, byte[] data, bool compressed) - { - lock (_forConn) { - if (_readyState != WebSocketState.Open) { - _logger.Error ("The sending has been interrupted."); - return false; - } + _log.Debug (msg); - return sendBytes (new WebSocketFrame (fin, opcode, data, compressed, _client).ToArray ()); - } + return ret; } - private void sendAsync (Opcode opcode, Stream stream, Action completed) + // As client + private bool connect () { - Func sender = send; - sender.BeginInvoke ( - opcode, - stream, - ar => { - try { - var sent = sender.EndInvoke (ar); - if (completed != null) - completed (sent); - } - catch (Exception ex) { - _logger.Fatal (ex.ToString ()); - error ("An exception has occurred during a send callback.", ex); - } - }, - null); - } + if (_readyState == WebSocketState.Connecting) { + _log.Trace ("The connect process is in progress."); - private bool sendBytes (byte[] bytes) - { - try { - _stream.Write (bytes, 0, bytes.Length); - return true; - } - catch (Exception ex) { - _logger.Fatal (ex.ToString ()); return false; } - } - // As client - private HttpResponse sendHandshakeRequest () - { - var req = createHandshakeRequest (); - var res = sendHttpRequest (req, 90000); - if (res.IsUnauthorized) { - 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; + lock (_forState) { + if (_readyState == WebSocketState.Open) { + _log.Trace ("The connection has already been established."); + + return false; } - _authChallenge = AuthenticationChallenge.Parse (chal); - if (_authChallenge == null) { - _logger.Error ("An invalid authentication challenge is specified."); - return res; + if (_readyState == WebSocketState.Closing) { + _log.Error ("The close process is in progress."); + + error ("An error has occurred before connecting.", null); + + return false; } - if (_credentials != null && - (!_preAuth || _authChallenge.Scheme == AuthenticationSchemes.Digest)) { - if (res.HasConnectionClose) { - releaseClientResources (); - setClientStream (); - } + if (_retryCountForConnect >= _maxRetryCountForConnect) { + _log.Error ("An opportunity for reconnecting has been lost."); + + error ("An error has occurred before connecting.", null); - var authRes = new AuthenticationResponse (_authChallenge, _credentials, _nonceCount); - _nonceCount = authRes.NonceCount; - req.Headers["Authorization"] = authRes.ToString (); - res = sendHttpRequest (req, 15000); + return false; } - } - 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; - } + if (_readyState == WebSocketState.Closed) + initr (); - Uri uri; - string msg; - if (!url.TryCreateWebSocketUri (out uri, out msg)) { - _logger.Error ("An invalid url to redirect is located: " + msg); - return res; - } + _retryCountForConnect++; - releaseClientResources (); + _readyState = WebSocketState.Connecting; - _uri = uri; - _secure = uri.Scheme == "wss"; + var done = false; - setClientStream (); - return sendHandshakeRequest (); + try { + done = doHandshake (); } - } + catch (Exception ex) { + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); - return res; - } + abort ("An exception has occurred while connecting.", ex); + } - // 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 ()); + if (!done) + return false; - return res; - } + _retryCountForConnect = -1; - // As server - private bool sendHttpResponse (HttpResponse response) - { - _logger.Debug ("A response to this request:\n" + response.ToString ()); - return sendBytes (response.ToByteArray ()); - } + _readyState = WebSocketState.Open; + return true; + } + } + // As client - private void sendProxyConnectRequest () + private AuthenticationResponse createAuthenticationResponse () { - 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 (); - } + if (_credentials == null) + return null; - var authRes = new AuthenticationResponse (authChal, _proxyCredentials, 0); - req.Headers["Proxy-Authorization"] = authRes.ToString (); - res = sendHttpRequest (req, 15000); - } + if (_authChallenge == null) + return _preAuth ? new AuthenticationResponse (_credentials) : null; - if (res.IsProxyAuthenticationRequired) - throw new WebSocketException ("A proxy authentication is required."); - } + var ret = new AuthenticationResponse ( + _authChallenge, + _credentials, + _nonceCount + ); - if (res.StatusCode[0] != '2') - throw new WebSocketException ( - "The proxy has failed a connection to the requested host and port."); + _nonceCount = ret.NonceCount; + + return ret; } // As client - private void setClientStream () + private string createExtensions () { - 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 (); + 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); } - if (_secure) { - var conf = SslConfiguration; - var host = conf.TargetHost; - if (host != _uri.DnsSafeHost) - throw new WebSocketException ( - CloseStatusCode.TlsHandshakeFailure, "An invalid host name is specified."); + var len = buff.Length; - try { - var sslStream = new SslStream ( - _stream, - false, - conf.ServerCertificateValidationCallback, - conf.ClientCertificateSelectionCallback); + if (len <= 2) + return null; - sslStream.AuthenticateAsClient ( - host, - conf.ClientCertificates, - conf.EnabledSslProtocols, - conf.CheckCertificateRevocation); + buff.Length = len - 2; - _stream = sslStream; - } - catch (Exception ex) { - throw new WebSocketException (CloseStatusCode.TlsHandshakeFailure, ex); - } - } + return buff.ToString (); } - private void startReceiving () + // As server + private HttpResponse createHandshakeFailureResponse () { - if (_messageEventQueue.Count > 0) - _messageEventQueue.Clear (); - - _exitReceiving = new AutoResetEvent (false); - _receivePong = new AutoResetEvent (false); + var ret = HttpResponse.CreateCloseResponse (HttpStatusCode.BadRequest); - Action receive = null; - receive = () => - WebSocketFrame.ReadFrameAsync ( - _stream, - false, - frame => { - if (!processReceivedFrame (frame) || _readyState == WebSocketState.Closed) { - var exit = _exitReceiving; - if (exit != null) - exit.Set (); - - return; - } - - // Receive next asap because the Ping or Close needs a response to it. - receive (); - - if ((frame.IsControl && !(frame.IsPing && _emitOnPing)) || !frame.IsFinal) - return; - - lock (_forEvent) { - try { - var e = dequeueFromMessageEventQueue (); - if (e != null && _readyState == WebSocketState.Open) - OnMessage.Emit (this, e); - } - catch (Exception ex) { - processException (ex, "An exception has occurred during an OnMessage event."); - } - } - }, - ex => processException (ex, "An exception has occurred while receiving a message.")); + ret.Headers["Sec-WebSocket-Version"] = _version; - receive (); + return ret; } // As client - private bool validateSecWebSocketAcceptHeader (string value) + private HttpRequest createHandshakeRequest () { - return value != null && value == CreateResponseKey (_base64Key); - } + var ret = HttpRequest.CreateWebSocketHandshakeRequest (_uri); - // As client - private bool validateSecWebSocketExtensionsHeader (string value) - { - var comp = _compression != CompressionMethod.None; - if (value == null || value.Length == 0) { - if (comp) - _compression = CompressionMethod.None; + var headers = ret.Headers; - return true; - } + headers["Sec-WebSocket-Key"] = _base64Key; + headers["Sec-WebSocket-Version"] = _version; - if (!comp) - return false; + if (!_origin.IsNullOrEmpty ()) + headers["Origin"] = _origin; - foreach (var e in value.SplitHeaderValue (',')) { - var ext = e.Trim (); - if (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 (_hasProtocol) + headers["Sec-WebSocket-Protocol"] = _protocols.ToString (", "); - if (!ext.Contains ("client_no_context_takeover")) - _logger.Warn ("The server hasn't sent back 'client_no_context_takeover'."); + var exts = createExtensions (); - 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"; - }); + _hasExtension = exts != null; - if (invalid) - return false; - } - else { - return false; - } - } + if (_hasExtension) + headers["Sec-WebSocket-Extensions"] = exts; - _extensions = value; - return true; + var ares = createAuthenticationResponse (); + + if (ares != null) + headers["Authorization"] = ares.ToString (); + + var hasUserHeader = _userHeaders != null && _userHeaders.Count > 0; + + if (hasUserHeader) + headers.Add (_userHeaders); + + var hasCookie = _cookies != null && _cookies.Count > 0; + + if (hasCookie) + ret.SetCookies (_cookies); + + return ret; } // As server - private bool validateSecWebSocketKeyHeader (string value) + private HttpResponse createHandshakeResponse () { - if (value == null || value.Length == 0) - return false; + var ret = HttpResponse.CreateWebSocketHandshakeResponse (); - _base64Key = value; - return true; + 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; + + var hasUserHeader = _userHeaders != null && _userHeaders.Count > 0; + + if (hasUserHeader) + headers.Add (_userHeaders); + + var hasCookie = _cookies != null && _cookies.Count > 0; + + if (hasCookie) + ret.SetCookies (_cookies); + + return ret; } // As client - private bool validateSecWebSocketProtocolHeader (string value) + private TcpClient createTcpClient (string hostname, int port) { - if (value == null) - return _protocols == null; + var ret = new TcpClient (hostname, port); - if (_protocols == null || !_protocols.Contains (protocol => protocol == value)) - return false; + if (_noDelay) + ret.NoDelay = true; - _protocol = value; - return true; + return ret; } // As server - private bool validateSecWebSocketVersionClientHeader (string value) - { - return value != null && value == _version; - } - - // As client - private bool validateSecWebSocketVersionServerHeader (string value) + private bool customCheckHandshakeRequest ( + WebSocketContext context, + out string message + ) { - return value == null || value == _version; - } + message = null; - #endregion + if (_handshakeRequestChecker == null) + return true; - #region Internal Methods + message = _handshakeRequestChecker (context); - internal static string CheckCloseParameters (ushort code, string reason, bool client) - { - return !code.IsCloseStatusCode () - ? "An invalid close status code." - : code == (ushort) CloseStatusCode.NoStatus - ? (!reason.IsNullOrEmpty () ? "NoStatus cannot have a reason." : null) - : code == (ushort) CloseStatusCode.MandatoryExtension && !client - ? "MandatoryExtension cannot be used by a server." - : code == (ushort) CloseStatusCode.ServerError && client - ? "ServerError cannot be used by a client." - : !reason.IsNullOrEmpty () && reason.UTF8Encode ().Length > 123 - ? "A reason has greater than the allowable max size." - : null; + return message == null; } - internal static string CheckCloseParameters (CloseStatusCode code, string reason, bool client) + // As server + private void customRespondToHandshakeRequest (WebSocketContext context) { - return code == CloseStatusCode.NoStatus - ? (!reason.IsNullOrEmpty () ? "NoStatus cannot have a reason." : null) - : code == CloseStatusCode.MandatoryExtension && !client - ? "MandatoryExtension cannot be used by a server." - : code == CloseStatusCode.ServerError && client - ? "ServerError cannot be used by a client." - : !reason.IsNullOrEmpty () && reason.UTF8Encode ().Length > 123 - ? "A reason has greater than the allowable max size." - : null; - } + if (_handshakeRequestResponder == null) + return; - internal static string CheckPingParameter (string message, out byte[] bytes) - { - bytes = message.UTF8Encode (); - return bytes.Length > 125 ? "A message has greater than the allowable max size." : null; + _handshakeRequestResponder (context); } - internal static string CheckSendParameter (byte[] data) + private MessageEventArgs dequeueFromMessageEventQueue () { - return data == null ? "'data' is null." : null; + lock (_forMessageEventQueue) { + return _messageEventQueue.Count > 0 + ? _messageEventQueue.Dequeue () + : null; + } } - internal static string CheckSendParameter (FileInfo file) + // As client + private bool doHandshake () { - return file == null ? "'file' is null." : null; + setClientStream (); + + var res = sendHandshakeRequest (); + + _log.Debug (res.ToString ()); + + _handshakeResponseHeaders = res.Headers; + _handshakeResponseCookies = res.Cookies; + + string msg; + + if (!checkHandshakeResponse (res, out msg)) { + _log.Error (msg); + + abort (1002, "A handshake error has occurred."); + + return false; + } + + if (_hasProtocol) + _protocol = _handshakeResponseHeaders["Sec-WebSocket-Protocol"]; + + if (_hasExtension) { + var exts = _handshakeResponseHeaders["Sec-WebSocket-Extensions"]; + + if (exts != null) + _extensions = exts; + else + _compression = CompressionMethod.None; + } + + if (_handshakeResponseCookies.Count > 0) + Cookies.SetOrRemove (_handshakeResponseCookies); + + return true; } - internal static string CheckSendParameter (string data) + private void enqueueToMessageEventQueue (MessageEventArgs e) { - return data == null ? "'data' is null." : null; + lock (_forMessageEventQueue) + _messageEventQueue.Enqueue (e); } - internal static string CheckSendParameters (Stream stream, int length) + private void error (string message, Exception exception) { - return stream == null - ? "'stream' is null." - : !stream.CanRead - ? "'stream' cannot be read." - : length < 1 - ? "'length' is less than 1." - : null; + var e = new ErrorEventArgs (message, exception); + + try { + OnError.Emit (this, e); + } + catch (Exception ex) { + _log.Error (ex.Message); + _log.Debug (ex.ToString ()); + } } - // As server - internal void Close (HttpResponse response) + private ClientSslConfiguration getSslConfiguration () { - _readyState = WebSocketState.Closing; + if (_sslConfig == null) + _sslConfig = new ClientSslConfiguration (_uri.DnsSafeHost); - sendHttpResponse (response); - releaseServerResources (); + return _sslConfig; + } - _readyState = WebSocketState.Closed; + private void init () + { + _compression = CompressionMethod.None; + _forPing = new object (); + _forSend = new object (); + _forState = new object (); + _messageEventQueue = new Queue (); + _forMessageEventQueue = ((ICollection) _messageEventQueue).SyncRoot; + _readyState = WebSocketState.New; } - // As server - internal void Close (HttpStatusCode code) + // As client + private void initr () { - Close (createHandshakeCloseResponse (code)); + _handshakeResponseCookies = null; + _handshakeResponseHeaders = null; } - // As server - internal void Close (CloseEventArgs e, byte[] frameAsBytes, bool receive) + private void message () { - lock (_forConn) { - if (_readyState == WebSocketState.Closing) { - _logger.Info ("The closing is already in progress."); + MessageEventArgs e = null; + + lock (_forMessageEventQueue) { + if (_inMessage) return; - } - if (_readyState == WebSocketState.Closed) { - _logger.Info ("The connection has been closed."); + if (_messageEventQueue.Count == 0) return; - } - _readyState = WebSocketState.Closing; - } + if (_readyState != WebSocketState.Open) + return; - e.WasClean = closeHandshake (frameAsBytes, receive, false); - releaseServerResources (); - releaseCommonResources (); + e = _messageEventQueue.Dequeue (); - _readyState = WebSocketState.Closed; - try { - OnClose.Emit (this, e); - } - catch (Exception ex) { - _logger.Fatal (ex.ToString ()); + _inMessage = true; } + + _message (e); } - // As client - internal static string CreateBase64Key () + private void messagec (MessageEventArgs e) { - var src = new byte[16]; - RandomNumber.GetBytes (src); + do { + try { + OnMessage.Emit (this, e); + } + catch (Exception ex) { + _log.Error (ex.Message); + _log.Debug (ex.ToString ()); - return Convert.ToBase64String (src); - } + error ("An exception has occurred during an OnMessage 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 ()); + lock (_forMessageEventQueue) { + if (_messageEventQueue.Count == 0) { + _inMessage = false; - return Convert.ToBase64String (src); + break; + } + + if (_readyState != WebSocketState.Open) { + _inMessage = false; + + break; + } + + e = _messageEventQueue.Dequeue (); + } + } + while (true); } - // As server - internal void InternalAccept () + private void messages (MessageEventArgs e) { try { - if (acceptHandshake ()) { - _readyState = WebSocketState.Open; - open (); - } + OnMessage.Emit (this, e); } catch (Exception ex) { - processException (ex, "An exception has occurred while accepting."); + _log.Error (ex.Message); + _log.Debug (ex.ToString ()); + + error ("An exception has occurred during an OnMessage event.", ex); + } + + lock (_forMessageEventQueue) { + if (_messageEventQueue.Count == 0) { + _inMessage = false; + + return; + } + + if (_readyState != WebSocketState.Open) { + _inMessage = false; + + return; + } + + e = _messageEventQueue.Dequeue (); } + + ThreadPool.QueueUserWorkItem (state => messages (e)); } - internal bool Ping (byte[] frameAsBytes, TimeSpan timeout) + private void open () { + _inMessage = true; + + startReceiving (); + try { - AutoResetEvent pong; - return _readyState == WebSocketState.Open && - send (frameAsBytes) && - (pong = _receivePong) != null && - pong.WaitOne (timeout); + OnOpen.Emit (this, EventArgs.Empty); } catch (Exception ex) { - _logger.Fatal (ex.ToString ()); - return false; + _log.Error (ex.Message); + _log.Debug (ex.ToString ()); + + error ("An exception has occurred during the OnOpen event.", ex); } - } - // As server, used to broadcast - internal void Send (Opcode opcode, byte[] data, Dictionary cache) - { - lock (_forSend) { - lock (_forConn) { - if (_readyState != WebSocketState.Open) { - _logger.Error ("The sending has been interrupted."); - return; - } + MessageEventArgs e = null; - try { - byte[] cached; - if (!cache.TryGetValue (_compression, out cached)) { - cached = new WebSocketFrame ( - Fin.Final, - opcode, - data.Compress (_compression), - _compression != CompressionMethod.None, - false) - .ToArray (); - - cache.Add (_compression, cached); - } - - sendBytes (cached); - } - catch (Exception ex) { - _logger.Fatal (ex.ToString ()); - } + lock (_forMessageEventQueue) { + if (_messageEventQueue.Count == 0) { + _inMessage = false; + + return; + } + + if (_readyState != WebSocketState.Open) { + _inMessage = false; + + return; } + + e = _messageEventQueue.Dequeue (); } + + _message.BeginInvoke (e, ar => _message.EndInvoke (ar), null); } - // As server, used to broadcast - internal void Send (Opcode opcode, Stream stream, Dictionary cache) + private bool ping (byte[] data) { - lock (_forSend) { + if (_readyState != WebSocketState.Open) + return false; + + var received = _pongReceived; + + if (received == null) + return false; + + lock (_forPing) { try { - Stream cached; - if (!cache.TryGetValue (_compression, out cached)) { - cached = stream.Compress (_compression); - cache.Add (_compression, cached); - } - else { - cached.Position = 0; - } + received.Reset (); - send (opcode, cached, _compression != CompressionMethod.None); + var sent = send (Fin.Final, Opcode.Ping, data, false); + + if (!sent) + return false; + + return received.WaitOne (_waitTime); } - catch (Exception ex) { - _logger.Fatal (ex.ToString ()); + catch (ObjectDisposedException) { + return false; } } } - #endregion - - #region Public Methods - - /// - /// Accepts the WebSocket connection request. - /// - /// - /// This method isn't available in a client. - /// - public void Accept () + private bool processCloseFrame (WebSocketFrame frame) { - var msg = checkIfAvailable (false, true, true, false, false, false); - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred in accepting.", null); + var data = frame.PayloadData; + var send = !data.HasReservedCode; - return; - } + close (data, send, true); - if (accept ()) - open (); + return false; } - /// - /// Accepts the WebSocket connection request asynchronously. - /// - /// - /// - /// This method doesn't wait for the accept to be complete. - /// - /// - /// This method isn't available in a client. - /// - /// - public void AcceptAsync () + private bool processDataFrame (WebSocketFrame frame) { - var msg = checkIfAvailable (false, true, true, false, false, false); - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred in accepting.", null); + var e = frame.IsCompressed + ? new MessageEventArgs ( + frame.Opcode, + frame.PayloadData.ApplicationData.Decompress (_compression) + ) + : new MessageEventArgs (frame); - return; - } + enqueueToMessageEventQueue (e); - Func acceptor = accept; - acceptor.BeginInvoke ( - ar => { - if (acceptor.EndInvoke (ar)) - open (); - }, - null); + return true; } - /// - /// Closes the WebSocket connection, and releases all associated resources. - /// - public void Close () + private bool processFragmentFrame (WebSocketFrame frame) { - var msg = _readyState.CheckIfAvailable (true, true, false, false); - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred in closing the connection.", null); + if (!_inContinuation) { + if (frame.IsContinuation) + return true; - return; + _fragmentsOpcode = frame.Opcode; + _fragmentsCompressed = frame.IsCompressed; + _fragmentsBuffer = new MemoryStream (); + _inContinuation = true; } - close (new CloseEventArgs (), true, true, false); - } + _fragmentsBuffer.WriteBytes (frame.PayloadData.ApplicationData, 1024); - /// - /// Closes the WebSocket connection with the specified , - /// and releases all associated resources. - /// - /// - /// This method emits a event if isn't in - /// the allowable range of the close status code. - /// - /// - /// A that represents the status code indicating the reason for the close. - /// - public void Close (ushort code) - { - var msg = _readyState.CheckIfAvailable (true, true, false, false) ?? - CheckCloseParameters (code, null, _client); + if (frame.IsFinal) { + using (_fragmentsBuffer) { + var data = _fragmentsCompressed + ? _fragmentsBuffer.DecompressToArray (_compression) + : _fragmentsBuffer.ToArray (); - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred in closing the connection.", null); + var e = new MessageEventArgs (_fragmentsOpcode, data); - return; - } + enqueueToMessageEventQueue (e); + } - if (code == (ushort) CloseStatusCode.NoStatus) { - close (new CloseEventArgs (), true, true, false); - return; + _fragmentsBuffer = null; + _inContinuation = false; } - var send = !code.IsReserved (); - close (new CloseEventArgs (code), send, send, false); + return true; } - /// - /// Closes the WebSocket connection with the specified , - /// and releases all associated resources. - /// - /// - /// One of the enum values, represents the status code indicating - /// the reason for the close. - /// - public void Close (CloseStatusCode code) + private bool processPingFrame (WebSocketFrame frame) { - var msg = _readyState.CheckIfAvailable (true, true, false, false) ?? - CheckCloseParameters (code, null, _client); + _log.Trace ("A ping was received."); - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred in closing the connection.", null); + var pong = WebSocketFrame.CreatePongFrame (frame.PayloadData, _isClient); - return; - } + lock (_forState) { + if (_readyState != WebSocketState.Open) { + _log.Trace ("A pong to this ping cannot be sent."); + + return true; + } + + var bytes = pong.ToArray (); + var sent = sendBytes (bytes); + + if (!sent) + return false; + } + + _log.Trace ("A pong to this ping has been sent."); + + if (_emitOnPing) { + if (_isClient) + pong.Unmask (); + + var e = new MessageEventArgs (frame); + + enqueueToMessageEventQueue (e); + } + + return true; + } + + private bool processPongFrame (WebSocketFrame frame) + { + _log.Trace ("A pong was received."); + + try { + _pongReceived.Set (); + } + catch (NullReferenceException) { + return false; + } + catch (ObjectDisposedException) { + return false; + } + + _log.Trace ("It has been signaled."); + + return true; + } + + private bool processReceivedFrame (WebSocketFrame frame) + { + string msg; + + if (!checkReceivedFrame (frame, out msg)) { + _log.Error (msg); + _log.Debug (frame.ToString (false)); + + abort (1002, "An error has occurred while receiving."); + + return false; + } + + 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 compRequested = false; + + foreach (var elm in value.SplitHeaderValue (',')) { + var ext = elm.Trim (); + + if (ext.Length == 0) + continue; + + if (!compRequested) { + if (ext.IsCompressionExtension (CompressionMethod.Deflate)) { + _compression = CompressionMethod.Deflate; + + var str = _compression.ToExtensionString ( + "client_no_context_takeover", + "server_no_context_takeover" + ); + + buff.AppendFormat ("{0}, ", str); + + compRequested = true; + } + } + } + + var len = buff.Length; + + if (len <= 2) + return; + + buff.Length = len - 2; + + _extensions = buff.ToString (); + } + + private bool processUnsupportedFrame (WebSocketFrame frame) + { + _log.Fatal ("An unsupported frame was received."); + _log.Debug (frame.ToString (false)); + + abort (1003, "There is no way to handle it."); + + return false; + } + + // As server + private void refuseHandshake (ushort code, string reason) + { + createHandshakeFailureResponse ().WriteTo (_stream); + + abort (code, reason); + } + + // 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 (_isClient) + releaseClientResources (); + else + releaseServerResources (); + + releaseCommonResources (); + } + + // As server + private void releaseServerResources () + { + if (_closeContext != null) { + _closeContext (); + + _closeContext = null; + } + + _stream = null; + _context = null; + } + + private bool send (byte[] rawFrame) + { + lock (_forState) { + if (_readyState != WebSocketState.Open) { + _log.Error ("The current state of the interface is not Open."); + + return false; + } + + return sendBytes (rawFrame); + } + } + + private bool send (Opcode opcode, Stream sourceStream) + { + lock (_forSend) { + var dataStream = sourceStream; + var compressed = false; + var sent = false; + + try { + if (_compression != CompressionMethod.None) { + dataStream = sourceStream.Compress (_compression); + compressed = true; + } + + sent = send (opcode, dataStream, compressed); + + if (!sent) + error ("A send has failed.", null); + } + catch (Exception ex) { + _log.Error (ex.Message); + _log.Debug (ex.ToString ()); + + error ("An exception has occurred during a send.", ex); + } + finally { + if (compressed) + dataStream.Dispose (); + + sourceStream.Dispose (); + } + + return sent; + } + } + + private bool send (Opcode opcode, Stream dataStream, bool compressed) + { + var len = dataStream.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 dataStream.Read (buff, 0, rem) == rem + && send (Fin.Final, opcode, buff, compressed); + } + + if (quo == 1 && rem == 0) { + buff = new byte[FragmentLength]; + + return dataStream.Read (buff, 0, FragmentLength) == FragmentLength + && send (Fin.Final, opcode, buff, compressed); + } + + /* Send fragments */ + + // Begin + + buff = new byte[FragmentLength]; + + var sent = dataStream.Read (buff, 0, FragmentLength) == FragmentLength + && send (Fin.More, opcode, buff, compressed); + + if (!sent) + return false; + + // Continue + + var n = rem == 0 ? quo - 2 : quo - 1; + + for (long i = 0; i < n; i++) { + sent = dataStream.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 dataStream.Read (buff, 0, rem) == rem + && send (Fin.Final, Opcode.Cont, buff, false); + } + + private bool send (Fin fin, Opcode opcode, byte[] data, bool compressed) + { + var frame = new WebSocketFrame (fin, opcode, data, compressed, _isClient); + var rawFrame = frame.ToArray (); + + return send (rawFrame); + } + + private void sendAsync ( + Opcode opcode, + Stream sourceStream, + Action completed + ) + { + Func sender = send; + + sender.BeginInvoke ( + opcode, + sourceStream, + ar => { + try { + var sent = sender.EndInvoke (ar); + + if (completed != null) + completed (sent); + } + catch (Exception ex) { + _log.Error (ex.Message); + _log.Debug (ex.ToString ()); + + error ( + "An exception 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) { + _log.Error (ex.Message); + _log.Debug (ex.ToString ()); + + return false; + } + + return true; + } + + // As client + private HttpResponse sendHandshakeRequest () + { + var req = createHandshakeRequest (); + + _log.Debug (req.ToString ()); + + var timeout = 90000; + var res = req.GetResponse (_stream, timeout); + + if (res.IsUnauthorized) { + var val = res.Headers["WWW-Authenticate"]; + + if (val.IsNullOrEmpty ()) { + _log.Debug ("No authentication challenge is specified."); + + return res; + } + + var achal = AuthenticationChallenge.Parse (val); + + if (achal == null) { + _log.Debug ("An invalid authentication challenge is specified."); + + return res; + } + + _authChallenge = achal; + + if (_credentials == null) + return res; + + var ares = new AuthenticationResponse ( + _authChallenge, + _credentials, + _nonceCount + ); + + _nonceCount = ares.NonceCount; + + req.Headers["Authorization"] = ares.ToString (); + + if (res.CloseConnection) { + releaseClientResources (); + setClientStream (); + } + + _log.Debug (req.ToString ()); + + timeout = 15000; + res = req.GetResponse (_stream, timeout); + } + + if (res.IsRedirect) { + if (!_enableRedirection) + return res; + + var val = res.Headers["Location"]; + + if (val.IsNullOrEmpty ()) { + _log.Debug ("No URL to redirect is located."); + + return res; + } + + Uri uri; + string msg; + + if (!val.TryCreateWebSocketUri (out uri, out msg)) { + _log.Debug ("An invalid URL to redirect is located."); + + return res; + } + + releaseClientResources (); + + _uri = uri; + _isSecure = uri.Scheme == "wss"; + + setClientStream (); + + return sendHandshakeRequest (); + } + + return res; + } + + // As client + private HttpResponse sendProxyConnectRequest () + { + var req = HttpRequest.CreateConnectRequest (_uri); + + var timeout = 90000; + var res = req.GetResponse (_stream, timeout); + + if (res.IsProxyAuthenticationRequired) { + if (_proxyCredentials == null) + return res; + + var val = res.Headers["Proxy-Authenticate"]; + + if (val.IsNullOrEmpty ()) { + _log.Debug ("No proxy authentication challenge is specified."); + + return res; + } + + var achal = AuthenticationChallenge.Parse (val); + + if (achal == null) { + _log.Debug ("An invalid proxy authentication challenge is specified."); + + return res; + } + + var ares = new AuthenticationResponse (achal, _proxyCredentials, 0); + + req.Headers["Proxy-Authorization"] = ares.ToString (); + + if (res.CloseConnection) { + releaseClientResources (); + + _tcpClient = createTcpClient (_proxyUri.DnsSafeHost, _proxyUri.Port); + _stream = _tcpClient.GetStream (); + } + + timeout = 15000; + res = req.GetResponse (_stream, timeout); + } + + return res; + } + + // As client + private void setClientStream () + { + if (_proxyUri != null) { + _tcpClient = createTcpClient (_proxyUri.DnsSafeHost, _proxyUri.Port); + _stream = _tcpClient.GetStream (); + + var res = sendProxyConnectRequest (); + + string msg; + + if (!checkProxyConnectResponse (res, out msg)) + throw new WebSocketException (msg); + } + else { + _tcpClient = createTcpClient (_uri.DnsSafeHost, _uri.Port); + _stream = _tcpClient.GetStream (); + } + + if (_isSecure) { + var conf = getSslConfiguration (); + var host = conf.TargetHost; + + if (host != _uri.DnsSafeHost) { + var msg = "An invalid host name is specified."; + + throw new WebSocketException ( + CloseStatusCode.TlsHandshakeFailure, + msg + ); + } + + 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 => { + var doNext = processReceivedFrame (frame) + && _readyState != WebSocketState.Closed; + + if (!doNext) { + var exited = _receivingExited; + + if (exited != null) + exited.Set (); + + return; + } + + receive (); + + if (_inMessage) + return; + + message (); + }, + ex => { + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + + abort ("An exception has occurred while receiving.", ex); + } + ); + + receive (); + } + + // As client + private bool validateSecWebSocketExtensionsServerHeader (string value) + { + if (!_hasExtension) + return false; + + if (value.Length == 0) + return false; + + var compRequested = _compression != CompressionMethod.None; + + foreach (var elm in value.SplitHeaderValue (',')) { + var ext = elm.Trim (); + + if (compRequested && ext.IsCompressionExtension (_compression)) { + var param1 = "server_no_context_takeover"; + var param2 = "client_no_context_takeover"; + + if (!ext.Contains (param1)) { + // The server did not send back "server_no_context_takeover". + + return false; + } + + var name = _compression.ToExtensionString (); + + var isInvalid = ext.SplitHeaderValue (';').Contains ( + t => { + t = t.Trim (); + + var isValid = t == name + || t == param1 + || t == param2; + + return !isValid; + } + ); + + if (isInvalid) + return false; + + compRequested = false; + } + else { + return false; + } + } + + return true; + } + + #endregion + + #region Internal Methods + + // As server + internal void Accept () + { + var accepted = accept (); + + if (!accepted) + return; + + open (); + } + + // As server + internal void AcceptAsync () + { + Func acceptor = accept; + + acceptor.BeginInvoke ( + ar => { + var accepted = acceptor.EndInvoke (ar); + + if (!accepted) + return; + + open (); + }, + null + ); + } + + // As server + internal void Close (PayloadData payloadData, byte[] rawFrame) + { + lock (_forState) { + if (_readyState == WebSocketState.Closing) { + _log.Trace ("The close process is already in progress."); + + return; + } + + if (_readyState == WebSocketState.Closed) { + _log.Trace ("The connection has already been closed."); + + return; + } + + _readyState = WebSocketState.Closing; + } + + _log.Trace ("Begin closing the connection."); + + var sent = rawFrame != null && sendBytes (rawFrame); + var received = sent && _receivingExited != null + ? _receivingExited.WaitOne (_waitTime) + : false; + + var res = sent && received; + + var msg = String.Format ( + "The closing was clean? {0} (sent: {1} received: {2})", + res, + sent, + received + ); + + _log.Debug (msg); + + releaseServerResources (); + releaseCommonResources (); + + _log.Trace ("End closing the connection."); + + _readyState = WebSocketState.Closed; + + var e = new CloseEventArgs (payloadData, res); + + try { + OnClose.Emit (this, e); + } + catch (Exception ex) { + _log.Error (ex.Message); + _log.Debug (ex.ToString ()); + } + } + + // As client + internal static string CreateBase64Key () + { + var key = new byte[16]; + + RandomNumber.GetBytes (key); + + return Convert.ToBase64String (key); + } + + internal static string CreateResponseKey (string base64Key) + { + SHA1 sha1 = new SHA1CryptoServiceProvider (); + + var src = base64Key + _guid; + var bytes = src.GetUTF8EncodedBytes (); + var key = sha1.ComputeHash (bytes); + + return Convert.ToBase64String (key); + } + + // As server + internal bool Ping (byte[] rawFrame) + { + if (_readyState != WebSocketState.Open) + return false; + + var received = _pongReceived; + + if (received == null) + return false; + + lock (_forPing) { + try { + received.Reset (); + + var sent = send (rawFrame); + + if (!sent) + return false; + + return received.WaitOne (_waitTime); + } + catch (ObjectDisposedException) { + return false; + } + } + } + + // As server + internal void Send ( + Opcode opcode, + byte[] data, + Dictionary cache + ) + { + lock (_forSend) { + 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); + } + + send (found); + } + } + + // As server + internal void Send ( + Opcode opcode, + Stream sourceStream, + Dictionary cache + ) + { + lock (_forSend) { + Stream found; + + if (!cache.TryGetValue (_compression, out found)) { + found = sourceStream.Compress (_compression); + + cache.Add (_compression, found); + } + else { + found.Position = 0; + } + + send (opcode, found, _compression != CompressionMethod.None); + } + } + + #endregion + + #region Public Methods + + /// + /// Closes the connection. + /// + /// + /// This method does nothing if the current state of the interface is + /// Closing or Closed. + /// + public void Close () + { + close (1005, String.Empty); + } + + /// + /// Closes the connection with the specified status code. + /// + /// + /// This method does nothing if the current state of the interface is + /// Closing or Closed. + /// + /// + /// + /// A that specifies the status code indicating + /// the reason for the close. + /// + /// + /// The status codes are defined in + /// + /// Section 7.4 of RFC 6455. + /// + /// + /// + /// + /// is 1011 (server error). + /// It cannot be used by a client. + /// + /// + /// -or- + /// + /// + /// is 1010 (mandatory extension). + /// It cannot be used by a server. + /// + /// + /// + /// is less than 1000 or greater than 4999. + /// + public void Close (ushort code) + { + Close (code, String.Empty); + } + + /// + /// Closes the connection with the specified status code. + /// + /// + /// This method does nothing if the current state of the interface is + /// Closing or Closed. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It specifies the status code indicating the reason for the close. + /// + /// + /// + /// + /// is an undefined enum value. + /// + /// + /// -or- + /// + /// + /// is . + /// It cannot be used by a client. + /// + /// + /// -or- + /// + /// + /// is . + /// It cannot be used by a server. + /// + /// + public void Close (CloseStatusCode code) + { + Close (code, String.Empty); + } + + /// + /// Closes the connection with the specified status code and reason. + /// + /// + /// This method does nothing if the current state of the interface is + /// Closing or Closed. + /// + /// + /// + /// A that specifies the status code indicating + /// the reason for the close. + /// + /// + /// The status codes are defined in + /// + /// Section 7.4 of RFC 6455. + /// + /// + /// + /// + /// A that specifies the reason for the close. + /// + /// + /// Its size must be 123 bytes or less in UTF-8. + /// + /// + /// + /// + /// is 1011 (server error). + /// It cannot be used by a client. + /// + /// + /// -or- + /// + /// + /// is 1010 (mandatory extension). + /// It cannot be used by a server. + /// + /// + /// -or- + /// + /// + /// is 1005 (no status) and + /// is specified. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// + /// is less than 1000 or greater than 4999. + /// + /// + /// -or- + /// + /// + /// The size of is greater than 123 bytes. + /// + /// + 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 (_isClient) { + if (code == 1011) { + var msg = "1011 cannot be used."; + + throw new ArgumentException (msg, "code"); + } + } + else { + if (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 status code and reason. + /// + /// + /// This method does nothing if the current state of the interface is + /// Closing or Closed. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It specifies the status code indicating the reason for the close. + /// + /// + /// + /// + /// A that specifies the reason for the close. + /// + /// + /// Its size must be 123 bytes or less in UTF-8. + /// + /// + /// + /// + /// is an undefined enum value. + /// + /// + /// -or- + /// + /// + /// is . + /// It cannot be used by a client. + /// + /// + /// -or- + /// + /// + /// is . + /// It cannot be used by a server. + /// + /// + /// -or- + /// + /// + /// is and + /// is specified. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// The size of is greater than 123 bytes. + /// + public void Close (CloseStatusCode code, string reason) + { + if (!code.IsDefined ()) { + var msg = "An undefined enum value."; + + throw new ArgumentException (msg, "code"); + } + + if (_isClient) { + if (code == CloseStatusCode.ServerError) { + var msg = "ServerError cannot be used."; + + throw new ArgumentException (msg, "code"); + } + } + else { + if (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) { - close (new CloseEventArgs (), true, true, false); + 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 interface is + /// Closing or Closed. + /// + /// + public void CloseAsync () + { + closeAsync (1005, String.Empty); + } + + /// + /// Closes the connection asynchronously with the specified status code. + /// + /// + /// + /// This method does not wait for the close to be complete. + /// + /// + /// This method does nothing if the current state of the interface is + /// Closing or Closed. + /// + /// + /// + /// + /// A that specifies the status code indicating + /// the reason for the close. + /// + /// + /// The status codes are defined in + /// + /// Section 7.4 of RFC 6455. + /// + /// + /// + /// + /// is 1011 (server error). + /// It cannot be used by a client. + /// + /// + /// -or- + /// + /// + /// is 1010 (mandatory extension). + /// It cannot be used by a server. + /// + /// + /// + /// is less than 1000 or greater than 4999. + /// + public void CloseAsync (ushort code) + { + CloseAsync (code, String.Empty); + } + + /// + /// Closes the connection asynchronously with the specified status code. + /// + /// + /// + /// This method does not wait for the close to be complete. + /// + /// + /// This method does nothing if the current state of the interface is + /// Closing or Closed. + /// + /// + /// + /// + /// One of the enum values. + /// + /// + /// It specifies the status code indicating the reason for the close. + /// + /// + /// + /// + /// is an undefined enum value. + /// + /// + /// -or- + /// + /// + /// is . + /// It cannot be used by a client. + /// + /// + /// -or- + /// + /// + /// is . + /// It cannot be used by a server. + /// + /// + public void CloseAsync (CloseStatusCode code) + { + CloseAsync (code, String.Empty); + } + + /// + /// Closes the connection asynchronously with the specified status code and + /// reason. + /// + /// + /// + /// This method does not wait for the close to be complete. + /// + /// + /// This method does nothing if the current state of the interface is + /// Closing or Closed. + /// + /// + /// + /// + /// A that specifies the status code indicating + /// the reason for the close. + /// + /// + /// The status codes are defined in + /// + /// Section 7.4 of RFC 6455. + /// + /// + /// + /// + /// A that specifies the reason for the close. + /// + /// + /// Its size must be 123 bytes or less in UTF-8. + /// + /// + /// + /// + /// is 1011 (server error). + /// It cannot be used by a client. + /// + /// + /// -or- + /// + /// + /// is 1010 (mandatory extension). + /// It cannot be used by a server. + /// + /// + /// -or- + /// + /// + /// is 1005 (no status) and + /// is specified. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// + /// is less than 1000 or greater than 4999. + /// + /// + /// -or- + /// + /// + /// The size of is greater than 123 bytes. + /// + /// + 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 (_isClient) { + if (code == 1011) { + var msg = "1011 cannot be used."; + + throw new ArgumentException (msg, "code"); + } + } + else { + if (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 status code and + /// reason. + /// + /// + /// + /// This method does not wait for the close to be complete. + /// + /// + /// This method does nothing if the current state of the interface is + /// Closing or Closed. + /// + /// + /// + /// + /// One of the enum values. + /// + /// + /// It specifies the status code indicating the reason for the close. + /// + /// + /// + /// + /// A that specifies the reason for the close. + /// + /// + /// Its size must be 123 bytes or less in UTF-8. + /// + /// + /// + /// + /// is an undefined enum value. + /// + /// + /// -or- + /// + /// + /// is . + /// It cannot be used by a client. + /// + /// + /// -or- + /// + /// + /// is . + /// It cannot be used by a server. + /// + /// + /// -or- + /// + /// + /// is and + /// is specified. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// The size of is greater than 123 bytes. + /// + public void CloseAsync (CloseStatusCode code, string reason) + { + if (!code.IsDefined ()) { + var msg = "An undefined enum value."; + + throw new ArgumentException (msg, "code"); + } + + if (_isClient) { + if (code == CloseStatusCode.ServerError) { + var msg = "ServerError cannot be used."; + + throw new ArgumentException (msg, "code"); + } + } + else { + if (code == CloseStatusCode.MandatoryExtension) { + var msg = "MandatoryExtension cannot be used."; + + throw new ArgumentException (msg, "code"); + } + } + + if (reason.IsNullOrEmpty ()) { + closeAsync ((ushort) code, String.Empty); + return; } - var send = !code.IsReserved (); - close (new CloseEventArgs (code), send, send, false); + 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); } /// - /// Closes the WebSocket connection with the specified and - /// , and releases all associated resources. + /// Establishes a connection. /// /// - /// This method emits a event if isn't in - /// the allowable range of the close status code or the size of is - /// greater than 123 bytes. + /// This method does nothing when the current state of the interface is + /// Connecting or Open. /// - /// - /// A that represents the status code indicating the reason for the close. - /// - /// - /// A that represents the reason for the close. - /// - public void Close (ushort code, string reason) + /// + /// + /// The Connect method is not available if the interface is not for + /// the client. + /// + /// + /// -or- + /// + /// + /// The Connect method is not available if a series of reconnecting + /// has failed. + /// + /// + public void Connect () { - var msg = _readyState.CheckIfAvailable (true, true, false, false) ?? - CheckCloseParameters (code, reason, _client); + if (!_isClient) { + var msg = "The Connect method is not available."; + + throw new InvalidOperationException (msg); + } - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred in closing the connection.", null); + if (_retryCountForConnect >= _maxRetryCountForConnect) { + var msg = "The Connect method is not available."; - return; + throw new InvalidOperationException (msg); } - if (code == (ushort) CloseStatusCode.NoStatus) { - close (new CloseEventArgs (), true, true, false); + var connected = connect (); + + if (!connected) return; - } - var send = !code.IsReserved (); - close (new CloseEventArgs (code, reason), send, send, false); + open (); } /// - /// Closes the WebSocket connection with the specified and - /// , and releases all associated resources. + /// Establishes a connection asynchronously. /// /// - /// This method emits a event if the size of is - /// greater than 123 bytes. + /// + /// This method does not wait for the connect process to be complete. + /// + /// + /// This method does nothing when the current state of the interface is + /// Connecting or Open. + /// /// - /// - /// One of the enum values, represents the status code indicating - /// the reason for the close. - /// - /// - /// A that represents the reason for the close. - /// - public void Close (CloseStatusCode code, string reason) + /// + /// + /// The ConnectAsync method is not available if the interface is not + /// for the client. + /// + /// + /// -or- + /// + /// + /// The ConnectAsync method is not available if a series of reconnecting + /// has failed. + /// + /// + public void ConnectAsync () { - var msg = _readyState.CheckIfAvailable (true, true, false, false) ?? - CheckCloseParameters (code, reason, _client); + if (!_isClient) { + var msg = "The ConnectAsync method is not available."; - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred in closing the connection.", null); - - return; + throw new InvalidOperationException (msg); } - if (code == CloseStatusCode.NoStatus) { - close (new CloseEventArgs (), true, true, false); - return; + if (_retryCountForConnect >= _maxRetryCountForConnect) { + var msg = "The ConnectAsync method is not available."; + + throw new InvalidOperationException (msg); } - var send = !code.IsReserved (); - close (new CloseEventArgs (code, reason), send, send, false); + Func connector = connect; + + connector.BeginInvoke ( + ar => { + var connected = connector.EndInvoke (ar); + + if (!connected) + return; + + open (); + }, + null + ); } /// - /// Closes the WebSocket connection asynchronously, and releases all associated resources. + /// Sends a ping to the remote endpoint. /// - /// - /// This method doesn't wait for the close to be complete. - /// - public void CloseAsync () + /// + /// true if the send has successfully done and a pong has been + /// received within a time; otherwise, false. + /// + public bool Ping () { - var msg = _readyState.CheckIfAvailable (true, true, false, false); - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred in closing the connection.", null); - - return; - } - - closeAsync (new CloseEventArgs (), true, true, false); + return ping (_emptyBytes); } /// - /// Closes the WebSocket connection asynchronously with the specified , - /// and releases all associated resources. + /// Sends a ping with the specified message to the remote endpoint. /// - /// + /// + /// true if the send has successfully done and a pong has been + /// received within a time; otherwise, false. + /// + /// /// - /// This method doesn't wait for the close to be complete. + /// A that specifies the message to send. /// /// - /// This method emits a event if isn't in - /// the allowable range of the close status code. + /// Its size must be 125 bytes or less in UTF-8. /// - /// - /// - /// A that represents the status code indicating the reason for the close. /// - public void CloseAsync (ushort code) + /// + /// could not be UTF-8-encoded. + /// + /// + /// The size of is greater than 125 bytes. + /// + public bool Ping (string message) { - var msg = _readyState.CheckIfAvailable (true, true, false, false) ?? - CheckCloseParameters (code, null, _client); + if (message.IsNullOrEmpty ()) + return ping (_emptyBytes); - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred in closing the connection.", null); + byte[] bytes; - return; + if (!message.TryGetUTF8EncodedBytes (out bytes)) { + var msg = "It could not be UTF-8-encoded."; + + throw new ArgumentException (msg, "message"); } - if (code == (ushort) CloseStatusCode.NoStatus) { - closeAsync (new CloseEventArgs (), true, true, false); - return; + if (bytes.Length > 125) { + var msg = "Its size is greater than 125 bytes."; + + throw new ArgumentOutOfRangeException ("message", msg); } - var send = !code.IsReserved (); - closeAsync (new CloseEventArgs (code), send, send, false); + return ping (bytes); } /// - /// Closes the WebSocket connection asynchronously with the specified - /// , and releases all associated resources. + /// Sends the specified data to the remote endpoint. /// - /// - /// This method doesn't wait for the close to be complete. - /// - /// - /// One of the enum values, represents the status code indicating - /// the reason for the close. + /// + /// An array of that specifies the binary data to send. /// - public void CloseAsync (CloseStatusCode code) + /// + /// is . + /// + /// + /// The Send method is not available when the current state of + /// the interface is not Open. + /// + public void Send (byte[] data) { - var msg = _readyState.CheckIfAvailable (true, true, false, false) ?? - CheckCloseParameters (code, null, _client); + if (_readyState != WebSocketState.Open) { + var msg = "The Send method is not available."; - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred in closing the connection.", null); - - return; + throw new InvalidOperationException (msg); } - if (code == CloseStatusCode.NoStatus) { - closeAsync (new CloseEventArgs (), true, true, false); - return; - } + if (data == null) + throw new ArgumentNullException ("data"); - var send = !code.IsReserved (); - closeAsync (new CloseEventArgs (code), send, send, false); + send (Opcode.Binary, new MemoryStream (data)); } /// - /// Closes the WebSocket connection asynchronously with the specified and - /// , and releases all associated resources. + /// Sends the specified file to the remote endpoint. /// - /// + /// /// - /// This method doesn't wait for the close to be complete. + /// A that specifies the file to send. /// /// - /// This method emits a event if isn't in - /// the allowable range of the close status code or the size of is - /// greater than 123 bytes. + /// The file is sent as the binary data. /// - /// - /// - /// A that represents the status code indicating the reason for the close. - /// - /// - /// A that represents the reason for the close. /// - public void CloseAsync (ushort code, string reason) + /// + /// + /// The file does not exist. + /// + /// + /// -or- + /// + /// + /// The file could not be opened. + /// + /// + /// + /// is . + /// + /// + /// The Send method is not available when the current state of + /// the interface is not Open. + /// + public void Send (FileInfo fileInfo) { - var msg = _readyState.CheckIfAvailable (true, true, false, false) ?? - CheckCloseParameters (code, reason, _client); + if (_readyState != WebSocketState.Open) { + var msg = "The Send method is not available."; - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred in closing the connection.", null); + throw new InvalidOperationException (msg); + } - return; + if (fileInfo == null) + throw new ArgumentNullException ("fileInfo"); + + if (!fileInfo.Exists) { + var msg = "The file does not exist."; + + throw new ArgumentException (msg, "fileInfo"); } - if (code == (ushort) CloseStatusCode.NoStatus) { - closeAsync (new CloseEventArgs (), true, true, false); - return; + FileStream stream; + + if (!fileInfo.TryOpenRead (out stream)) { + var msg = "The file could not be opened."; + + throw new ArgumentException (msg, "fileInfo"); } - var send = !code.IsReserved (); - closeAsync (new CloseEventArgs (code, reason), send, send, false); + send (Opcode.Binary, stream); } /// - /// Closes the WebSocket connection asynchronously with the specified - /// and , and releases - /// all associated resources. + /// Sends the specified data to the remote endpoint. /// - /// - /// - /// This method doesn't wait for the close to be complete. - /// - /// - /// This method emits a event if the size of - /// is greater than 123 bytes. - /// - /// - /// - /// One of the enum values, represents the status code indicating - /// the reason for the close. - /// - /// - /// A that represents the reason for the close. + /// + /// A that specifies the text data to send. /// - public void CloseAsync (CloseStatusCode code, string reason) + /// + /// could not be UTF-8-encoded. + /// + /// + /// is . + /// + /// + /// The Send method is not available when the current state of + /// the interface is not Open. + /// + public void Send (string data) { - var msg = _readyState.CheckIfAvailable (true, true, false, false) ?? - CheckCloseParameters (code, reason, _client); + if (_readyState != WebSocketState.Open) { + var msg = "The Send method is not available."; - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred in closing the connection.", null); - - return; + throw new InvalidOperationException (msg); } - if (code == CloseStatusCode.NoStatus) { - closeAsync (new CloseEventArgs (), true, true, false); - return; - } + if (data == null) + throw new ArgumentNullException ("data"); - var send = !code.IsReserved (); - closeAsync (new CloseEventArgs (code, reason), send, send, false); - } + byte[] bytes; - /// - /// Establishes a WebSocket connection. - /// - /// - /// This method isn't available in a server. - /// - public void Connect () - { - var msg = checkIfAvailable (true, false, true, false, false, true); - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred in connecting.", null); + if (!data.TryGetUTF8EncodedBytes (out bytes)) { + var msg = "It could not be UTF-8-encoded."; - return; + throw new ArgumentException (msg, "data"); } - if (connect ()) - open (); + send (Opcode.Text, new MemoryStream (bytes)); } /// - /// Establishes a WebSocket connection asynchronously. + /// Sends the data from the specified stream instance to the remote endpoint. /// - /// + /// /// - /// This method doesn't wait for the connect to be complete. + /// A instance from which to read the data to send. /// /// - /// This method isn't available in a server. + /// The data is sent as the binary data. /// - /// - public void ConnectAsync () + /// + /// + /// An that specifies the number of bytes to send. + /// + /// + /// + /// cannot be read. + /// + /// + /// -or- + /// + /// + /// is less than 1. + /// + /// + /// -or- + /// + /// + /// No data could be read from . + /// + /// + /// + /// is . + /// + /// + /// The Send method is not available when the current state of + /// the interface is not Open. + /// + public void Send (Stream stream, int length) { - var msg = checkIfAvailable (true, false, true, false, false, true); - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred in connecting.", null); + if (_readyState != WebSocketState.Open) { + var msg = "The Send method is not available."; - return; + 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 receives a pong to this ping in a time; - /// otherwise, false. - /// - public bool Ping () - { - var bytes = _client - ? WebSocketFrame.CreatePingFrame (true).ToArray () - : WebSocketFrame.EmptyPingBytes; + if (stream == null) + throw new ArgumentNullException ("stream"); - return Ping (bytes, _waitTime); - } + if (!stream.CanRead) { + var msg = "It cannot be read."; - /// - /// Sends a ping with the specified using the WebSocket connection. - /// - /// - /// true if the receives a pong to this ping in a time; - /// otherwise, false. - /// - /// - /// A that represents the message to send. - /// - public bool Ping (string message) - { - if (message == null || message.Length == 0) - return Ping (); + throw new ArgumentException (msg, "stream"); + } - byte[] data; - var msg = CheckPingParameter (message, out data); - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred in sending a ping.", null); + if (length < 1) { + var msg = "Less than 1."; - return false; + throw new ArgumentException (msg, "length"); } - return Ping (WebSocketFrame.CreatePingFrame (data, _client).ToArray (), _waitTime); - } + var bytes = stream.ReadBytes (length); + var len = bytes.Length; - /// - /// Sends binary using the WebSocket connection. - /// - /// - /// An array of that represents the binary data to send. - /// - public void Send (byte[] data) - { - var msg = _readyState.CheckIfAvailable (false, true, false, false) ?? - CheckSendParameter (data); + if (len == 0) { + var msg = "No data could be read from it."; - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred in sending data.", null); + throw new ArgumentException (msg, "stream"); + } - return; + if (len < length) { + var fmt = "Only {0} byte(s) of data could be read from the stream."; + var msg = String.Format (fmt, len); + + _log.Warn (msg); } - send (Opcode.Binary, new MemoryStream (data)); + send (Opcode.Binary, new MemoryStream (bytes)); } /// - /// Sends the specified as binary data using the WebSocket connection. + /// Sends the specified data to the remote endpoint asynchronously. /// - /// - /// A that represents the file to send. + /// + /// This method does not wait for the send to be complete. + /// + /// + /// An array of that specifies the binary data to send. + /// + /// + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the send is complete. + /// + /// + /// The parameter passed to the delegate is true + /// if the send has successfully done; otherwise, false. + /// + /// + /// if not necessary. + /// /// - public void Send (FileInfo file) + /// + /// is . + /// + /// + /// The SendAsync method is not available when the current state of + /// the interface is not Open. + /// + public void SendAsync (byte[] data, Action completed) { - var msg = _readyState.CheckIfAvailable (false, true, false, false) ?? - CheckSendParameter (file); - - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred in sending data.", null); + if (_readyState != WebSocketState.Open) { + var msg = "The SendAsync method is not available."; - return; + throw new InvalidOperationException (msg); } - send (Opcode.Binary, file.OpenRead ()); + if (data == null) + throw new ArgumentNullException ("data"); + + sendAsync (Opcode.Binary, new MemoryStream (data), completed); } /// - /// Sends text using the WebSocket connection. + /// Sends the specified file to the remote endpoint asynchronously. /// - /// - /// A that represents the text data to send. + /// + /// 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. + /// /// - public void Send (string data) + /// + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the send is complete. + /// + /// + /// The parameter passed to the delegate is true + /// if the send has successfully done; otherwise, false. + /// + /// + /// if not necessary. + /// + /// + /// + /// + /// The file does not exist. + /// + /// + /// -or- + /// + /// + /// The file could not be opened. + /// + /// + /// + /// is . + /// + /// + /// The SendAsync method is not available when the current state of + /// the interface is not Open. + /// + public void SendAsync (FileInfo fileInfo, Action completed) { - var msg = _readyState.CheckIfAvailable (false, true, false, false) ?? - CheckSendParameter (data); - - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred in sending data.", null); + if (_readyState != WebSocketState.Open) { + var msg = "The SendAsync method is not available."; - return; + throw new InvalidOperationException (msg); } - send (Opcode.Text, new MemoryStream (data.UTF8Encode ())); - } - - /// - /// Sends binary asynchronously using the WebSocket connection. - /// - /// - /// This method doesn't wait for the send to be complete. - /// - /// - /// An array of that represents the binary data to send. - /// - /// - /// An Action<bool> delegate that references the method(s) called when - /// the send is complete. A passed to this delegate is true - /// if the send is complete successfully. - /// - public void SendAsync (byte[] data, Action completed) - { - var msg = _readyState.CheckIfAvailable (false, true, false, false) ?? - CheckSendParameter (data); + if (fileInfo == null) + throw new ArgumentNullException ("fileInfo"); - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred in sending data.", null); + if (!fileInfo.Exists) { + var msg = "The file does not exist."; - return; + throw new ArgumentException (msg, "fileInfo"); } - sendAsync (Opcode.Binary, new MemoryStream (data), completed); - } - - /// - /// Sends the specified as binary data asynchronously using - /// the WebSocket connection. - /// - /// - /// This method doesn't wait for the send to be complete. - /// - /// - /// A that represents the file to send. - /// - /// - /// An Action<bool> delegate that references the method(s) called when - /// the send is complete. A passed to this delegate is true - /// if the send is complete successfully. - /// - public void SendAsync (FileInfo file, Action completed) - { - var msg = _readyState.CheckIfAvailable (false, true, false, false) ?? - CheckSendParameter (file); + FileStream stream; - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred in sending data.", null); + if (!fileInfo.TryOpenRead (out stream)) { + var msg = "The file could not be opened."; - return; + throw new ArgumentException (msg, "fileInfo"); } - sendAsync (Opcode.Binary, file.OpenRead (), completed); + sendAsync (Opcode.Binary, stream, completed); } /// - /// Sends text asynchronously using the WebSocket connection. + /// Sends the specified data to the remote endpoint asynchronously. /// /// - /// This method doesn't wait for the send to be complete. + /// This method does not wait for the send to be complete. /// /// - /// A that represents the text data to send. + /// A that specifies the text data to send. /// /// - /// An Action<bool> delegate that references the method(s) called when - /// the send is complete. A passed to this delegate is true - /// if the send is complete successfully. + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the send is complete. + /// + /// + /// The parameter passed to the delegate is true + /// if the send has successfully done; otherwise, false. + /// + /// + /// if not necessary. + /// /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// is . + /// + /// + /// The SendAsync method is not available when the current state of + /// the interface is not Open. + /// public void SendAsync (string data, Action completed) { - var msg = _readyState.CheckIfAvailable (false, true, false, false) ?? - CheckSendParameter (data); + if (_readyState != WebSocketState.Open) { + var msg = "The SendAsync method is not available."; + + throw new InvalidOperationException (msg); + } - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred in sending data.", null); + if (data == null) + throw new ArgumentNullException ("data"); - return; + 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 (data.UTF8Encode ()), completed); + sendAsync (Opcode.Text, new MemoryStream (bytes), completed); } /// - /// Sends binary data from the specified asynchronously using - /// the WebSocket connection. + /// Sends the data from the specified stream instance to the remote + /// endpoint asynchronously. /// /// - /// This method doesn't wait for the send to be complete. + /// This method does not wait for the send to be complete. /// /// - /// A from which contains the binary data to send. + /// + /// A instance from which to read the data to send. + /// + /// + /// The data is sent as the binary data. + /// /// /// - /// An that represents the number of bytes to send. + /// An that specifies the number of bytes to send. /// /// - /// An Action<bool> delegate that references the method(s) called when - /// the send is complete. A passed to this delegate is true - /// if the send is complete successfully. + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the send is complete. + /// + /// + /// The parameter passed to the delegate is true + /// if the send has successfully done; otherwise, false. + /// + /// + /// if not necessary. + /// /// + /// + /// + /// cannot be read. + /// + /// + /// -or- + /// + /// + /// is less than 1. + /// + /// + /// -or- + /// + /// + /// No data could be read from . + /// + /// + /// + /// is . + /// + /// + /// The SendAsync method is not available when the current state of + /// the interface is not Open. + /// public void SendAsync (Stream stream, int length, Action completed) { - var msg = _readyState.CheckIfAvailable (false, true, false, false) ?? - CheckSendParameters (stream, length); + if (_readyState != WebSocketState.Open) { + var msg = "The SendAsync method is not available."; + + throw new InvalidOperationException (msg); + } - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred in sending data.", null); + if (stream == null) + throw new ArgumentNullException ("stream"); - return; + if (!stream.CanRead) { + var msg = "It cannot be read."; + + throw new ArgumentException (msg, "stream"); } - stream.ReadBytesAsync ( - length, - data => { - var len = data.Length; - if (len == 0) { - _logger.Error ("The data cannot be read from 'stream'."); - error ("An error has occurred in sending data.", null); + if (length < 1) { + var msg = "Less than 1."; - return; - } + throw new ArgumentException (msg, "length"); + } - if (len < length) - _logger.Warn ( - String.Format ( - "The data with 'length' cannot be read from 'stream':\n expected: {0}\n actual: {1}", - length, - len)); + var bytes = stream.ReadBytes (length); + var len = bytes.Length; - var sent = send (Opcode.Binary, new MemoryStream (data)); - if (completed != null) - completed (sent); - }, - ex => { - _logger.Fatal (ex.ToString ()); - error ("An exception has occurred while sending data.", ex); - }); + if (len == 0) { + var msg = "No data could be read from it."; + + throw new ArgumentException (msg, "stream"); + } + + if (len < length) { + var fmt = "Only {0} byte(s) of data could be read from the stream."; + var msg = String.Format (fmt, len); + + _log.Warn (msg); + } + + sendAsync (Opcode.Binary, new MemoryStream (bytes), completed); } /// - /// Sets an HTTP to send with - /// the WebSocket connection request to the server. + /// Sets an HTTP cookie to send with the handshake request or response. /// /// - /// A that represents the cookie to send. + /// A that specifies the cookie to send. /// + /// + /// is . + /// + /// + /// The SetCookie method is not available when the current state of + /// the interface is neither New nor Closed. + /// public void SetCookie (Cookie cookie) { - lock (_forConn) { - var msg = checkIfAvailable (true, false, true, false, false, true) ?? - (cookie == null ? "'cookie' is null." : null); + if (cookie == null) + throw new ArgumentNullException ("cookie"); - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred in setting a cookie.", null); + lock (_forState) { + if (!canSet ()) { + var msg = "The SetCookie method is not available."; - return; + throw new InvalidOperationException (msg); } - lock (_cookies.SyncRoot) - _cookies.SetOrRemove (cookie); + Cookies.SetOrRemove (cookie); } } /// - /// Sets a pair of and for - /// the HTTP authentication (Basic/Digest). + /// Sets the credentials for the HTTP authentication (Basic/Digest). /// /// - /// A that represents the user name used to authenticate. + /// + /// A that specifies the username associated + /// with the credentials. + /// + /// + /// or an empty string if initializes + /// the credentials. + /// /// /// - /// A that represents the password for - /// used to authenticate. + /// + /// A that specifies the password for the + /// username associated with the credentials. + /// + /// + /// or an empty string if not necessary. + /// /// /// - /// true if the sends the Basic authentication credentials - /// with the first connection request to the server; otherwise, false. + /// A : true if sends the credentials for + /// the Basic authentication in advance with the first handshake + /// request; otherwise, false. /// + /// + /// + /// contains an invalid character. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// + /// + /// The SetCredentials method is not available if the interface is not for + /// the client. + /// + /// + /// -or- + /// + /// + /// The SetCredentials method is not available when the current state of + /// the interface is neither New nor Closed. + /// + /// public void SetCredentials (string username, string password, bool preAuth) { - lock (_forConn) { - var msg = checkIfAvailable (true, false, true, false, false, true); - if (msg == null) { - if (username.IsNullOrEmpty ()) { - _credentials = null; - _preAuth = false; - _logger.Warn ("The credentials were set back to the default."); + if (!_isClient) { + var msg = "The SetCredentials method is not available."; - return; - } + throw new InvalidOperationException (msg); + } + + if (!username.IsNullOrEmpty ()) { + if (username.Contains (':') || !username.IsText ()) { + var msg = "It contains an invalid character."; + + throw new ArgumentException (msg, "username"); + } + } + + if (!password.IsNullOrEmpty ()) { + if (!password.IsText ()) { + var msg = "It contains an invalid character."; + + throw new ArgumentException (msg, "password"); + } + } + + lock (_forState) { + if (!canSet ()) { + var msg = "The SetCredentials method is not available."; - msg = username.Contains (':') || !username.IsText () - ? "'username' contains an invalid character." - : !password.IsNullOrEmpty () && !password.IsText () - ? "'password' contains an invalid character." - : null; + throw new InvalidOperationException (msg); } - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred in setting the credentials.", null); + if (username.IsNullOrEmpty ()) { + _credentials = null; + _preAuth = false; return; } - _credentials = new NetworkCredential (username, password, _uri.PathAndQuery); + _credentials = new NetworkCredential ( + username, + password, + _uri.PathAndQuery + ); + _preAuth = preAuth; } } /// - /// Sets an HTTP proxy server URL to connect through, and if necessary, - /// a pair of and for - /// the proxy server authentication (Basic/Digest). + /// Sets the URL of the HTTP proxy server through which to connect and + /// the credentials for the HTTP proxy authentication (Basic/Digest). /// /// - /// A that represents the proxy server URL to connect through. + /// + /// A that specifies 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 user name used to authenticate. + /// + /// A that specifies the username associated + /// with the credentials. + /// + /// + /// or an empty string if the credentials + /// are not necessary. + /// /// /// - /// A that represents the password for - /// used to authenticate. + /// + /// A that specifies the password for the + /// username associated with the credentials. + /// + /// + /// or an empty string if not necessary. + /// /// + /// + /// + /// 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. + /// + /// + /// + /// + /// The SetProxy method is not available if the interface is not for + /// the client. + /// + /// + /// -or- + /// + /// + /// The SetProxy method is not available when the current state of + /// the interface is neither New nor Closed. + /// + /// public void SetProxy (string url, string username, string password) { - lock (_forConn) { - var msg = checkIfAvailable (true, false, true, false, false, true); - if (msg == null) { - if (url.IsNullOrEmpty ()) { - _proxyUri = null; - _proxyCredentials = null; - _logger.Warn ("The proxy url and credentials were set back to the default."); + if (!_isClient) { + var msg = "The SetProxy method is not available."; - return; - } + throw new InvalidOperationException (msg); + } - Uri uri; - if (!Uri.TryCreate (url, UriKind.Absolute, out uri) || - uri.Scheme != "http" || - uri.Segments.Length > 1) { - msg = "The syntax of a proxy url must be 'http://[:]'."; - } - else { - _proxyUri = uri; + Uri uri = null; - if (username.IsNullOrEmpty ()) { - _proxyCredentials = null; - _logger.Warn ("The proxy credentials were set back to the default."); + if (!url.IsNullOrEmpty ()) { + if (!Uri.TryCreate (url, UriKind.Absolute, out uri)) { + var msg = "Not an absolute URI string."; - return; - } + throw new ArgumentException (msg, "url"); + } - msg = username.Contains (':') || !username.IsText () - ? "'username' contains an invalid character." - : !password.IsNullOrEmpty () && !password.IsText () - ? "'password' contains an invalid character." - : null; - } + if (uri.Scheme != "http") { + var msg = "The scheme part is not http."; + + throw new ArgumentException (msg, "url"); + } + + if (uri.Segments.Length > 1) { + var msg = "It includes the path segments."; + + throw new ArgumentException (msg, "url"); + } + } + + if (!username.IsNullOrEmpty ()) { + if (username.Contains (':') || !username.IsText ()) { + var msg = "It contains an invalid character."; + + throw new ArgumentException (msg, "username"); + } + } + + if (!password.IsNullOrEmpty ()) { + if (!password.IsText ()) { + var msg = "It contains an invalid character."; + + throw new ArgumentException (msg, "password"); + } + } + + lock (_forState) { + if (!canSet ()) { + var msg = "The SetProxy method is not available."; + + throw new InvalidOperationException (msg); } - if (msg != null) { - _logger.Error (msg); - error ("An error has occurred in setting the proxy.", null); + if (url.IsNullOrEmpty ()) { + _proxyUri = null; + _proxyCredentials = null; return; } - _proxyCredentials = new NetworkCredential ( - username, password, String.Format ("{0}:{1}", _uri.DnsSafeHost, _uri.Port)); + _proxyUri = uri; + + if (username.IsNullOrEmpty ()) { + _proxyCredentials = null; + + return; + } + + var domain = String.Format ("{0}:{1}", _uri.DnsSafeHost, _uri.Port); + + _proxyCredentials = new NetworkCredential (username, password, domain); + } + } + + /// + /// Sets a user header to send with the handshake request or response. + /// + /// + /// A that specifies the name of the header to set. + /// + /// + /// A that specifies the value of the header to set. + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is a string of spaces. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// -or- + /// + /// + /// is a restricted header name. + /// + /// + /// + /// is . + /// + /// + /// The length of is greater than 65,535 + /// characters. + /// + /// + /// + /// The SetUserHeader method is not available when the current state of + /// the interface is neither New nor Closed. + /// + /// + /// -or- + /// + /// + /// The SetUserHeader method is not available if the interface does not + /// allow the header type. + /// + /// + public void SetUserHeader (string name, string value) + { + lock (_forState) { + if (!canSet ()) { + var msg = "The SetUserHeader method is not available."; + + throw new InvalidOperationException (msg); + } + + UserHeaders.Set (name, value); } } @@ -2637,14 +4512,20 @@ public void SetProxy (string url, string username, string password) #region Explicit Interface Implementations /// - /// Closes the WebSocket connection, and releases all associated resources. + /// Closes the connection and releases all associated resources. /// /// - /// This method closes the connection with . + /// + /// This method closes the connection with close status 1001 (going away). + /// + /// + /// This method does nothing if the current state of the interface is + /// Closing or Closed. + /// /// void IDisposable.Dispose () { - close (new CloseEventArgs (CloseStatusCode.Away), true, true, false); + close (1001, String.Empty); } #endregion diff --git a/websocket-sharp/WebSocketException.cs b/websocket-sharp/WebSocketException.cs index 3380eaabe..12bfc48f8 100644 --- a/websocket-sharp/WebSocketException.cs +++ b/websocket-sharp/WebSocketException.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2012-2014 sta.blockhead + * Copyright (c) 2012-2024 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 @@ -31,13 +31,28 @@ namespace WebSocketSharp { /// - /// The exception that is thrown when a gets a fatal error. + /// The exception that is thrown when a fatal error occurs in + /// the WebSocket communication. /// public class WebSocketException : Exception { #region Private Fields - private CloseStatusCode _code; + private ushort _code; + + #endregion + + #region Private Constructors + + private WebSocketException ( + ushort code, + string message, + Exception innerException + ) + : base (message ?? code.GetErrorMessage (), innerException) + { + _code = code; + } #endregion @@ -78,10 +93,13 @@ internal WebSocketException (CloseStatusCode code, string message) { } - internal WebSocketException (CloseStatusCode code, string message, Exception innerException) - : base (message ?? code.GetMessage (), innerException) + internal WebSocketException ( + CloseStatusCode code, + string message, + Exception innerException + ) + : this ((ushort) code, message, innerException) { - _code = code; } #endregion @@ -92,10 +110,15 @@ internal WebSocketException (CloseStatusCode code, string message, Exception inn /// Gets the status code indicating the cause of the exception. /// /// - /// One of the enum values, represents the status code - /// indicating the cause of the exception. + /// + /// A that represents the status code indicating + /// the cause of the exception. + /// + /// + /// It is one of the status codes for the WebSocket connection close. + /// /// - public CloseStatusCode Code { + public ushort Code { get { return _code; } diff --git a/websocket-sharp/WebSocketFrame.cs b/websocket-sharp/WebSocketFrame.cs index 7ee1353a5..9ce51b945 100644 --- a/websocket-sharp/WebSocketFrame.cs +++ b/websocket-sharp/WebSocketFrame.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2012-2015 sta.blockhead + * Copyright (c) 2012-2025 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 @@ -45,29 +45,19 @@ internal class WebSocketFrame : IEnumerable { #region Private Fields - private byte[] _extPayloadLength; - private Fin _fin; - private Mask _mask; - private byte[] _maskingKey; - private Opcode _opcode; - private PayloadData _payloadData; - private byte _payloadLength; - private Rsv _rsv1; - private Rsv _rsv2; - private Rsv _rsv3; - - #endregion - - #region Internal Fields - - /// - /// Represents the Ping frame without the payload data as an array of . - /// - /// - /// The value of this field is created from a non masked frame, so it can only be used to - /// send a Ping from a server. - /// - internal static readonly byte[] EmptyPingBytes; + private static readonly int _defaultHeaderLength; + private static readonly int _defaultMaskingKeyLength; + private static readonly byte[] _emptyBytes; + private byte[] _extPayloadLength; + private Fin _fin; + private Mask _mask; + private byte[] _maskingKey; + private Opcode _opcode; + private PayloadData _payloadData; + private int _payloadLength; + private Rsv _rsv1; + private Rsv _rsv2; + private Rsv _rsv3; #endregion @@ -75,7 +65,9 @@ internal class WebSocketFrame : IEnumerable static WebSocketFrame () { - EmptyPingBytes = CreatePingFrame (false).ToArray (); + _defaultHeaderLength = 2; + _defaultMaskingKeyLength = 4; + _emptyBytes = new byte[0]; } #endregion @@ -90,47 +82,56 @@ private WebSocketFrame () #region Internal Constructors - internal WebSocketFrame (Opcode opcode, PayloadData payloadData, bool mask) - : this (Fin.Final, opcode, payloadData, false, mask) - { - } - - internal WebSocketFrame (Fin fin, Opcode opcode, byte[] data, bool compressed, bool mask) + internal WebSocketFrame ( + Fin fin, + Opcode opcode, + byte[] data, + bool compressed, + bool mask + ) : this (fin, opcode, new PayloadData (data), compressed, mask) { } internal WebSocketFrame ( - Fin fin, Opcode opcode, PayloadData payloadData, bool compressed, bool mask) + Fin fin, + Opcode opcode, + PayloadData payloadData, + bool compressed, + bool mask + ) { _fin = fin; - _rsv1 = isData (opcode) && compressed ? Rsv.On : Rsv.Off; + _opcode = opcode; + + _rsv1 = compressed ? Rsv.On : Rsv.Off; _rsv2 = Rsv.Off; _rsv3 = Rsv.Off; - _opcode = opcode; var len = payloadData.Length; + if (len < 126) { - _payloadLength = (byte) len; - _extPayloadLength = WebSocket.EmptyBytes; + _payloadLength = (int) len; + _extPayloadLength = _emptyBytes; } else if (len < 0x010000) { - _payloadLength = (byte) 126; - _extPayloadLength = ((ushort) len).InternalToByteArray (ByteOrder.Big); + _payloadLength = 126; + _extPayloadLength = ((ushort) len).ToByteArray (ByteOrder.Big); } else { - _payloadLength = (byte) 127; - _extPayloadLength = len.InternalToByteArray (ByteOrder.Big); + _payloadLength = 127; + _extPayloadLength = len.ToByteArray (ByteOrder.Big); } if (mask) { _mask = Mask.On; _maskingKey = createMaskingKey (); + payloadData.Mask (_maskingKey); } else { _mask = Mask.Off; - _maskingKey = WebSocket.EmptyBytes; + _maskingKey = _emptyBytes; } _payloadData = payloadData; @@ -140,19 +141,23 @@ internal WebSocketFrame ( #region Internal Properties - internal int ExtendedPayloadLengthCount { + internal ulong ExactPayloadLength { get { - return _payloadLength < 126 ? 0 : (_payloadLength == 126 ? 2 : 8); + return _payloadLength < 126 + ? (ulong) _payloadLength + : _payloadLength == 126 + ? _extPayloadLength.ToUInt16 (ByteOrder.Big) + : _extPayloadLength.ToUInt64 (ByteOrder.Big); } } - internal ulong FullPayloadLength { + internal int ExtendedPayloadLengthWidth { get { return _payloadLength < 126 - ? _payloadLength + ? 0 : _payloadLength == 126 - ? _extPayloadLength.ToUInt16 (ByteOrder.Big) - : _extPayloadLength.ToUInt64 (ByteOrder.Big); + ? 2 + : 8; } } @@ -198,13 +203,13 @@ public bool IsContinuation { public bool IsControl { get { - return _opcode == Opcode.Close || _opcode == Opcode.Ping || _opcode == Opcode.Pong; + return _opcode >= Opcode.Close; } } public bool IsData { get { - return _opcode == Opcode.Binary || _opcode == Opcode.Text; + return _opcode == Opcode.Text || _opcode == Opcode.Binary; } } @@ -214,7 +219,7 @@ public bool IsFinal { } } - public bool IsFragmented { + public bool IsFragment { get { return _fin == Fin.More || _opcode == Opcode.Cont; } @@ -246,7 +251,12 @@ public bool IsText { public ulong Length { get { - return 2 + (ulong) (_extPayloadLength.Length + _maskingKey.Length) + _payloadData.Length; + return (ulong) ( + _defaultHeaderLength + + _extPayloadLength.Length + + _maskingKey.Length + ) + + _payloadData.Length; } } @@ -274,7 +284,7 @@ public PayloadData PayloadData { } } - public byte PayloadLength { + public int PayloadLength { get { return _payloadLength; } @@ -304,142 +314,21 @@ public Rsv Rsv3 { private static byte[] createMaskingKey () { - var key = new byte[4]; + var key = new byte[_defaultMaskingKeyLength]; + WebSocket.RandomNumber.GetBytes (key); return key; } - private static string dump (WebSocketFrame frame) + private static WebSocketFrame processHeader (byte[] header) { - var len = frame.Length; - var cnt = (long) (len / 4); - var rem = (int) (len % 4); - - int cntDigit; - string cntFmt; - if (cnt < 10000) { - cntDigit = 4; - cntFmt = "{0,4}"; - } - else if (cnt < 0x010000) { - cntDigit = 4; - cntFmt = "{0,4:X}"; - } - else if (cnt < 0x0100000000) { - cntDigit = 8; - cntFmt = "{0,8:X}"; - } - else { - cntDigit = 16; - cntFmt = "{0,16:X}"; - } - - var spFmt = String.Format ("{{0,{0}}}", cntDigit); - var headerFmt = String.Format (@" -{0} 01234567 89ABCDEF 01234567 89ABCDEF -{0}+--------+--------+--------+--------+\n", spFmt); - var lineFmt = String.Format ("{0}|{{1,8}} {{2,8}} {{3,8}} {{4,8}}|\n", cntFmt); - var footerFmt = String.Format ("{0}+--------+--------+--------+--------+", spFmt); + if (header.Length != _defaultHeaderLength) { + var msg = "The header part of a frame could not be read."; - var output = new StringBuilder (64); - Func> linePrinter = () => { - long lineCnt = 0; - return (arg1, arg2, arg3, arg4) => - output.AppendFormat (lineFmt, ++lineCnt, arg1, arg2, arg3, arg4); - }; - var printLine = linePrinter (); - - output.AppendFormat (headerFmt, String.Empty); - - var bytes = frame.ToArray (); - for (long i = 0; i <= cnt; i++) { - var j = i * 4; - if (i < cnt) { - printLine ( - Convert.ToString (bytes[j], 2).PadLeft (8, '0'), - Convert.ToString (bytes[j + 1], 2).PadLeft (8, '0'), - Convert.ToString (bytes[j + 2], 2).PadLeft (8, '0'), - Convert.ToString (bytes[j + 3], 2).PadLeft (8, '0')); - - continue; - } - - if (rem > 0) - printLine ( - Convert.ToString (bytes[j], 2).PadLeft (8, '0'), - rem >= 2 ? Convert.ToString (bytes[j + 1], 2).PadLeft (8, '0') : String.Empty, - rem == 3 ? Convert.ToString (bytes[j + 2], 2).PadLeft (8, '0') : String.Empty, - String.Empty); + throw new WebSocketException (msg); } - output.AppendFormat (footerFmt, String.Empty); - return output.ToString (); - } - - private static bool isControl (Opcode opcode) - { - return opcode == Opcode.Close || opcode == Opcode.Ping || opcode == Opcode.Pong; - } - - private static bool isData (Opcode opcode) - { - return opcode == Opcode.Text || opcode == Opcode.Binary; - } - - private static string print (WebSocketFrame frame) - { - // Payload Length - var payloadLen = frame._payloadLength; - - // Extended Payload Length - var extPayloadLen = payloadLen > 125 ? frame.FullPayloadLength.ToString () : String.Empty; - - // Masking Key - var maskingKey = BitConverter.ToString (frame._maskingKey); - - // Payload Data - var payload = payloadLen == 0 - ? String.Empty - : payloadLen > 125 - ? "---" - : frame.IsText && - !(frame.IsMasked || frame.IsFragmented || frame.IsCompressed) - ? frame._payloadData.ApplicationData.UTF8Decode () - : frame._payloadData.ToString (); - - var fmt = @" - FIN: {0} - RSV1: {1} - RSV2: {2} - RSV3: {3} - Opcode: {4} - MASK: {5} - Payload Length: {6} -Extended Payload Length: {7} - Masking Key: {8} - Payload Data: {9}"; - - return String.Format ( - fmt, - frame._fin, - frame._rsv1, - frame._rsv2, - frame._rsv3, - frame._opcode, - frame._mask, - payloadLen, - extPayloadLen, - maskingKey, - payload); - } - - private static WebSocketFrame processHeader (byte[] header) - { - if (header.Length != 2) - throw new WebSocketException ( - "The header part of a frame cannot be read from the data source."); - // FIN var fin = (header[0] & 0x80) == 0x80 ? Fin.Final : Fin.More; @@ -453,52 +342,56 @@ private static WebSocketFrame processHeader (byte[] header) var rsv3 = (header[0] & 0x10) == 0x10 ? Rsv.On : Rsv.Off; // Opcode - var opcode = (Opcode) (header[0] & 0x0f); + var opcode = header[0] & 0x0f; // MASK var mask = (header[1] & 0x80) == 0x80 ? Mask.On : Mask.Off; // Payload Length - var payloadLen = (byte) (header[1] & 0x7f); + var payloadLen = header[1] & 0x7f; - // Check if valid header. - var err = isControl (opcode) && payloadLen > 125 - ? "A control frame has payload data which is greater than the allowable max length." - : isControl (opcode) && fin == Fin.More - ? "A control frame is fragmented." - : !isData (opcode) && rsv1 == Rsv.On - ? "A non data frame is compressed." - : null; + if (!opcode.IsSupportedOpcode ()) { + var msg = "The opcode of a frame is not supported."; - if (err != null) - throw new WebSocketException (CloseStatusCode.ProtocolError, err); + throw new WebSocketException (CloseStatusCode.UnsupportedData, msg); + } var frame = new WebSocketFrame (); + frame._fin = fin; frame._rsv1 = rsv1; frame._rsv2 = rsv2; frame._rsv3 = rsv3; - frame._opcode = opcode; + frame._opcode = (Opcode) opcode; frame._mask = mask; frame._payloadLength = payloadLen; return frame; } - private static WebSocketFrame readExtendedPayloadLength (Stream stream, WebSocketFrame frame) + private static WebSocketFrame readExtendedPayloadLength ( + Stream stream, + WebSocketFrame frame + ) { - var len = frame.ExtendedPayloadLengthCount; + var len = frame.ExtendedPayloadLengthWidth; + if (len == 0) { - frame._extPayloadLength = WebSocket.EmptyBytes; + frame._extPayloadLength = _emptyBytes; + return frame; } var bytes = stream.ReadBytes (len); - if (bytes.Length != len) - throw new WebSocketException ( - "The 'Extended Payload Length' of a frame cannot be read from the data source."); + + if (bytes.Length != len) { + var msg = "The extended payload length of a frame could not be read."; + + throw new WebSocketException (msg); + } frame._extPayloadLength = bytes; + return frame; } @@ -506,11 +399,14 @@ private static void readExtendedPayloadLengthAsync ( Stream stream, WebSocketFrame frame, Action completed, - Action error) + Action error + ) { - var len = frame.ExtendedPayloadLengthCount; + var len = frame.ExtendedPayloadLengthWidth; + if (len == 0) { - frame._extPayloadLength = WebSocket.EmptyBytes; + frame._extPayloadLength = _emptyBytes; + completed (frame); return; @@ -519,41 +415,65 @@ private static void readExtendedPayloadLengthAsync ( stream.ReadBytesAsync ( len, bytes => { - if (bytes.Length != len) - throw new WebSocketException ( - "The 'Extended Payload Length' of a frame cannot be read from the data source."); + if (bytes.Length != len) { + var msg = "The extended payload length of a frame could not be read."; + + throw new WebSocketException (msg); + } frame._extPayloadLength = bytes; + completed (frame); }, - error); + error + ); } private static WebSocketFrame readHeader (Stream stream) { - return processHeader (stream.ReadBytes (2)); + var bytes = stream.ReadBytes (_defaultHeaderLength); + + return processHeader (bytes); } private static void readHeaderAsync ( - Stream stream, Action completed, Action error) + Stream stream, + Action completed, + Action error + ) { - stream.ReadBytesAsync (2, bytes => completed (processHeader (bytes)), error); + stream.ReadBytesAsync ( + _defaultHeaderLength, + bytes => { + var frame = processHeader (bytes); + + completed (frame); + }, + error + ); } - private static WebSocketFrame readMaskingKey (Stream stream, WebSocketFrame frame) + private static WebSocketFrame readMaskingKey ( + Stream stream, + WebSocketFrame frame + ) { - var len = frame.IsMasked ? 4 : 0; - if (len == 0) { - frame._maskingKey = WebSocket.EmptyBytes; + if (!frame.IsMasked) { + frame._maskingKey = _emptyBytes; + return frame; } - var bytes = stream.ReadBytes (len); - if (bytes.Length != len) - throw new WebSocketException ( - "The 'Masking Key' of a frame cannot be read from the data source."); + var bytes = stream.ReadBytes (_defaultMaskingKeyLength); + + if (bytes.Length != _defaultMaskingKeyLength) { + var msg = "The masking key of a frame could not be read."; + + throw new WebSocketException (msg); + } frame._maskingKey = bytes; + return frame; } @@ -561,53 +481,66 @@ private static void readMaskingKeyAsync ( Stream stream, WebSocketFrame frame, Action completed, - Action error) + Action error + ) { - var len = frame.IsMasked ? 4 : 0; - if (len == 0) { - frame._maskingKey = WebSocket.EmptyBytes; + if (!frame.IsMasked) { + frame._maskingKey = _emptyBytes; + completed (frame); return; } stream.ReadBytesAsync ( - len, + _defaultMaskingKeyLength, bytes => { - if (bytes.Length != len) - throw new WebSocketException ( - "The 'Masking Key' of a frame cannot be read from the data source."); + if (bytes.Length != _defaultMaskingKeyLength) { + var msg = "The masking key of a frame could not be read."; + + throw new WebSocketException (msg); + } frame._maskingKey = bytes; + completed (frame); }, - error); + error + ); } - private static WebSocketFrame readPayloadData (Stream stream, WebSocketFrame frame) + private static WebSocketFrame readPayloadData ( + Stream stream, + WebSocketFrame frame + ) { - var len = frame.FullPayloadLength; - if (len == 0) { + var exactPayloadLen = frame.ExactPayloadLength; + + if (exactPayloadLen > PayloadData.MaxLength) { + var msg = "The payload data of a frame is too big."; + + throw new WebSocketException (CloseStatusCode.TooBig, msg); + } + + if (exactPayloadLen == 0) { frame._payloadData = PayloadData.Empty; + return frame; } - // Check if allowable length. - if (len > PayloadData.MaxLength) - throw new WebSocketException ( - CloseStatusCode.TooBig, - "The length of 'Payload Data' of a frame is greater than the allowable max length."); + var len = (long) exactPayloadLen; + var bytes = frame._payloadLength > 126 + ? stream.ReadBytes (len, 1024) + : stream.ReadBytes ((int) len); + + if (bytes.LongLength != len) { + var msg = "The payload data of a frame could not be read."; - var llen = (long) len; - var bytes = frame._payloadLength < 127 - ? stream.ReadBytes ((int) len) - : stream.ReadBytes (llen, 1024); + throw new WebSocketException (msg); + } - if (bytes.LongLength != llen) - throw new WebSocketException ( - "The 'Payload Data' of a frame cannot be read from the data source."); + frame._payloadData = new PayloadData (bytes, len); - frame._payloadData = new PayloadData (bytes, llen); return frame; } @@ -615,62 +548,242 @@ private static void readPayloadDataAsync ( Stream stream, WebSocketFrame frame, Action completed, - Action error) + Action error + ) { - var len = frame.FullPayloadLength; - if (len == 0) { + var exactPayloadLen = frame.ExactPayloadLength; + + if (exactPayloadLen > PayloadData.MaxLength) { + var msg = "The payload data of a frame is too big."; + + throw new WebSocketException (CloseStatusCode.TooBig, msg); + } + + if (exactPayloadLen == 0) { frame._payloadData = PayloadData.Empty; + completed (frame); return; } - // Check if allowable length. - if (len > PayloadData.MaxLength) - throw new WebSocketException ( - CloseStatusCode.TooBig, - "The length of 'Payload Data' of a frame is greater than the allowable max length."); + var len = (long) exactPayloadLen; - var llen = (long) len; - Action compl = bytes => { - if (bytes.LongLength != llen) - throw new WebSocketException ( - "The 'Payload Data' of a frame cannot be read from the data source."); + Action comp = + bytes => { + if (bytes.LongLength != len) { + var msg = "The payload data of a frame could not be read."; - frame._payloadData = new PayloadData (bytes, llen); - completed (frame); - }; + throw new WebSocketException (msg); + } + + frame._payloadData = new PayloadData (bytes, len); + + completed (frame); + }; + + if (frame._payloadLength > 126) { + stream.ReadBytesAsync (len, 1024, comp, error); - if (frame._payloadLength < 127) { - stream.ReadBytesAsync ((int) len, compl, error); return; } - stream.ReadBytesAsync (llen, 1024, compl, error); + stream.ReadBytesAsync ((int) len, comp, error); + } + + private string toDumpString () + { + var len = Length; + var cnt = (long) (len / 4); + var rem = (int) (len % 4); + + string spFmt; + string cntFmt; + + if (cnt < 10000) { + spFmt = "{0,4}"; + cntFmt = "{0,4}"; + } + else if (cnt < 0x010000) { + spFmt = "{0,4}"; + cntFmt = "{0,4:X}"; + } + else if (cnt < 0x0100000000) { + spFmt = "{0,8}"; + cntFmt = "{0,8:X}"; + } + else { + spFmt = "{0,16}"; + cntFmt = "{0,16:X}"; + } + + var baseFmt = @"{0} 01234567 89ABCDEF 01234567 89ABCDEF +{0}+--------+--------+--------+--------+ +"; + var headerFmt = String.Format (baseFmt, spFmt); + + baseFmt = "{0}|{{1,8}} {{2,8}} {{3,8}} {{4,8}}|\n"; + var lineFmt = String.Format (baseFmt, cntFmt); + + baseFmt = "{0}+--------+--------+--------+--------+"; + var footerFmt = String.Format (baseFmt, spFmt); + + var buff = new StringBuilder (64); + + Func> lineWriter = + () => { + long lineCnt = 0; + + return (arg1, arg2, arg3, arg4) => { + buff.AppendFormat ( + lineFmt, + ++lineCnt, + arg1, + arg2, + arg3, + arg4 + ); + }; + }; + + var writeLine = lineWriter (); + var bytes = ToArray (); + + buff.AppendFormat (headerFmt, String.Empty); + + for (long i = 0; i <= cnt; i++) { + var j = i * 4; + + if (i < cnt) { + var arg1 = Convert.ToString (bytes[j], 2).PadLeft (8, '0'); + var arg2 = Convert.ToString (bytes[j + 1], 2).PadLeft (8, '0'); + var arg3 = Convert.ToString (bytes[j + 2], 2).PadLeft (8, '0'); + var arg4 = Convert.ToString (bytes[j + 3], 2).PadLeft (8, '0'); + + writeLine (arg1, arg2, arg3, arg4); + + continue; + } + + if (rem > 0) { + var arg1 = Convert.ToString (bytes[j], 2).PadLeft (8, '0'); + var arg2 = rem >= 2 + ? Convert.ToString (bytes[j + 1], 2).PadLeft (8, '0') + : String.Empty; + + var arg3 = rem == 3 + ? Convert.ToString (bytes[j + 2], 2).PadLeft (8, '0') + : String.Empty; + + writeLine (arg1, arg2, arg3, String.Empty); + } + } + + buff.AppendFormat (footerFmt, String.Empty); + + return buff.ToString (); + } + + private string toString () + { + var extPayloadLen = _payloadLength >= 126 + ? ExactPayloadLength.ToString () + : String.Empty; + + var maskingKey = _mask == Mask.On + ? BitConverter.ToString (_maskingKey) + : String.Empty; + + var payloadData = _payloadLength >= 126 + ? "***" + : _payloadLength > 0 + ? _payloadData.ToString () + : String.Empty; + + var fmt = @" FIN: {0} + RSV1: {1} + RSV2: {2} + RSV3: {3} + Opcode: {4} + MASK: {5} + Payload Length: {6} +Extended Payload Length: {7} + Masking Key: {8} + Payload Data: {9}"; + + return String.Format ( + fmt, + _fin, + _rsv1, + _rsv2, + _rsv3, + _opcode, + _mask, + _payloadLength, + extPayloadLen, + maskingKey, + payloadData + ); } #endregion #region Internal Methods - internal static WebSocketFrame CreateCloseFrame (PayloadData payloadData, bool mask) + internal static WebSocketFrame CreateCloseFrame ( + PayloadData payloadData, + bool mask + ) { - return new WebSocketFrame (Fin.Final, Opcode.Close, payloadData, false, mask); + return new WebSocketFrame ( + Fin.Final, + Opcode.Close, + payloadData, + false, + mask + ); } internal static WebSocketFrame CreatePingFrame (bool mask) { - return new WebSocketFrame (Fin.Final, Opcode.Ping, PayloadData.Empty, false, mask); + return new WebSocketFrame ( + Fin.Final, + Opcode.Ping, + PayloadData.Empty, + false, + mask + ); } internal static WebSocketFrame CreatePingFrame (byte[] data, bool mask) { - return new WebSocketFrame (Fin.Final, Opcode.Ping, new PayloadData (data), false, mask); + return new WebSocketFrame ( + Fin.Final, + Opcode.Ping, + new PayloadData (data), + false, + mask + ); + } + + internal static WebSocketFrame CreatePongFrame ( + PayloadData payloadData, + bool mask + ) + { + return new WebSocketFrame ( + Fin.Final, + Opcode.Pong, + payloadData, + false, + mask + ); } internal static WebSocketFrame ReadFrame (Stream stream, bool unmask) { var frame = readHeader (stream); + readExtendedPayloadLength (stream, frame); readMaskingKey (stream, frame); readPayloadData (stream, frame); @@ -682,7 +795,11 @@ internal static WebSocketFrame ReadFrame (Stream stream, bool unmask) } internal static void ReadFrameAsync ( - Stream stream, bool unmask, Action completed, Action error) + Stream stream, + bool unmask, + Action completed, + Action error + ) { readHeaderAsync ( stream, @@ -704,10 +821,19 @@ internal static void ReadFrameAsync ( completed (frame3); }, - error), - error), - error), - error); + error + ), + error + ), + error + ), + error + ); + } + + internal string ToString (bool dump) + { + return dump ? toDumpString () : toString (); } internal void Unmask () @@ -715,9 +841,10 @@ internal void Unmask () if (_mask == Mask.Off) return; - _mask = Mask.Off; _payloadData.Mask (_maskingKey); - _maskingKey = WebSocket.EmptyBytes; + + _maskingKey = _emptyBytes; + _mask = Mask.Off; } #endregion @@ -730,50 +857,49 @@ public IEnumerator GetEnumerator () yield return b; } - public void Print (bool dumped) - { - Console.WriteLine (dumped ? dump (this) : print (this)); - } - - public string PrintToString (bool dumped) - { - return dumped ? dump (this) : print (this); - } - public byte[] ToArray () { using (var buff = new MemoryStream ()) { var header = (int) _fin; + header = (header << 1) + (int) _rsv1; header = (header << 1) + (int) _rsv2; header = (header << 1) + (int) _rsv3; header = (header << 4) + (int) _opcode; header = (header << 1) + (int) _mask; - header = (header << 7) + (int) _payloadLength; - buff.Write (((ushort) header).InternalToByteArray (ByteOrder.Big), 0, 2); + header = (header << 7) + _payloadLength; - if (_payloadLength > 125) - buff.Write (_extPayloadLength, 0, _payloadLength == 126 ? 2 : 8); + var headerAsUInt16 = (ushort) header; + var headerAsBytes = headerAsUInt16.ToByteArray (ByteOrder.Big); + + buff.Write (headerAsBytes, 0, _defaultHeaderLength); + + if (_payloadLength >= 126) + buff.Write (_extPayloadLength, 0, _extPayloadLength.Length); if (_mask == Mask.On) - buff.Write (_maskingKey, 0, 4); + buff.Write (_maskingKey, 0, _defaultMaskingKeyLength); if (_payloadLength > 0) { var bytes = _payloadData.ToArray (); - if (_payloadLength < 127) - buff.Write (bytes, 0, bytes.Length); - else + + if (_payloadLength > 126) buff.WriteBytes (bytes, 1024); + else + buff.Write (bytes, 0, bytes.Length); } buff.Close (); + return buff.ToArray (); } } public override string ToString () { - return BitConverter.ToString (ToArray ()); + var val = ToArray (); + + return BitConverter.ToString (val); } #endregion diff --git a/websocket-sharp/WebSocketState.cs b/websocket-sharp/WebSocketState.cs index 469350cb9..fa1704970 100644 --- a/websocket-sharp/WebSocketState.cs +++ b/websocket-sharp/WebSocketState.cs @@ -4,7 +4,7 @@ * * The MIT License * - * Copyright (c) 2010-2015 sta.blockhead + * Copyright (c) 2010-2022 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 @@ -31,32 +31,34 @@ namespace WebSocketSharp { /// - /// Indicates the state of a WebSocket connection. + /// Indicates the state of the WebSocket interface. /// - /// - /// The values of this enumeration are defined in - /// The WebSocket API. - /// public enum WebSocketState : ushort { /// - /// Equivalent to numeric value 0. Indicates that the connection hasn't yet been established. + /// Equivalent to numeric value 0. Indicates that a new interface has + /// been created. /// - Connecting = 0, + New = 0, /// - /// Equivalent to numeric value 1. Indicates that the connection has been established, - /// and the communication is possible. + /// Equivalent to numeric value 1. Indicates that the connect process is + /// in progress. /// - Open = 1, + Connecting = 1, /// - /// Equivalent to numeric value 2. Indicates that the connection is going through - /// the closing handshake, or the WebSocket.Close method has been invoked. + /// Equivalent to numeric value 2. Indicates that the connection has + /// been established and the communication is possible. /// - Closing = 2, + Open = 2, /// - /// Equivalent to numeric value 3. Indicates that the connection has been closed or - /// couldn't be established. + /// Equivalent to numeric value 3. Indicates that the close process is + /// in progress. /// - Closed = 3 + Closing = 3, + /// + /// Equivalent to numeric value 4. Indicates that the connection has + /// been closed or could not be established. + /// + Closed = 4 } } diff --git a/websocket-sharp/websocket-sharp.csproj b/websocket-sharp/websocket-sharp.csproj index 087679547..9dac290aa 100644 --- a/websocket-sharp/websocket-sharp.csproj +++ b/websocket-sharp/websocket-sharp.csproj @@ -51,7 +51,7 @@ true - + @@ -127,7 +127,6 @@ - @@ -135,7 +134,6 @@ -