diff --git a/.vs/websocket-sharp/DesignTimeBuild/.dtbcache b/.vs/websocket-sharp/DesignTimeBuild/.dtbcache new file mode 100644 index 000000000..4e286051d Binary files /dev/null and b/.vs/websocket-sharp/DesignTimeBuild/.dtbcache differ diff --git a/.vs/websocket-sharp/v16/Server/sqlite3/db.lock b/.vs/websocket-sharp/v16/Server/sqlite3/db.lock new file mode 100644 index 000000000..e69de29bb diff --git a/.vs/websocket-sharp/v16/Server/sqlite3/storage.ide b/.vs/websocket-sharp/v16/Server/sqlite3/storage.ide new file mode 100644 index 000000000..88827beea Binary files /dev/null and b/.vs/websocket-sharp/v16/Server/sqlite3/storage.ide differ diff --git a/Example/Example.csproj b/Example/Example.csproj index 38c5b4200..9b4dea019 100644 --- a/Example/Example.csproj +++ b/Example/Example.csproj @@ -1,5 +1,5 @@ - + Debug AnyCPU @@ -10,6 +10,11 @@ Example example v3.5 + + + + + 3.5 true diff --git a/Example1/Example1.csproj b/Example1/Example1.csproj index 81c52eff2..b2ae026e8 100644 --- a/Example1/Example1.csproj +++ b/Example1/Example1.csproj @@ -1,5 +1,5 @@ - + Debug AnyCPU @@ -10,6 +10,11 @@ Example example1 v3.5 + + + + + 3.5 true diff --git a/Example2/Example2.csproj b/Example2/Example2.csproj index 685a1ef6d..5531ca3a6 100644 --- a/Example2/Example2.csproj +++ b/Example2/Example2.csproj @@ -1,5 +1,5 @@ - + Debug AnyCPU @@ -10,6 +10,11 @@ Example2 example2 v3.5 + + + + + 3.5 true diff --git a/Example3/Example3.csproj b/Example3/Example3.csproj index ce4fe265c..0fa630233 100644 --- a/Example3/Example3.csproj +++ b/Example3/Example3.csproj @@ -1,5 +1,5 @@ - + Debug AnyCPU @@ -10,6 +10,11 @@ Example3 example3 v3.5 + + + + + 3.5 true diff --git a/websocket-sharp-core/AssemblyInfo.cs b/websocket-sharp-core/AssemblyInfo.cs new file mode 100644 index 000000000..777b07d3b --- /dev/null +++ b/websocket-sharp-core/AssemblyInfo.cs @@ -0,0 +1,26 @@ +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("websocket-sharp")] +[assembly: AssemblyDescription("A C# implementation of the WebSocket protocol client and server")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("websocket-sharp.dll")] +[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.1.2.1")] + +// 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/websocket-sharp-core/ByteOrder.cs b/websocket-sharp-core/ByteOrder.cs new file mode 100644 index 000000000..317f462ea --- /dev/null +++ b/websocket-sharp-core/ByteOrder.cs @@ -0,0 +1,47 @@ +#region License +/* + * ByteOrder.cs + * + * The MIT License + * + * Copyright (c) 2012-2015 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; + +namespace WebSocketSharp +{ + /// + /// Specifies the byte order. + /// + public enum ByteOrder + { + /// + /// Specifies Little-endian. + /// + Little, + /// + /// Specifies Big-endian. + /// + Big + } +} diff --git a/websocket-sharp-core/CloseEventArgs.cs b/websocket-sharp-core/CloseEventArgs.cs new file mode 100644 index 000000000..8127ce418 --- /dev/null +++ b/websocket-sharp-core/CloseEventArgs.cs @@ -0,0 +1,113 @@ +#region License +/* + * CloseEventArgs.cs + * + * The MIT License + * + * Copyright (c) 2012-2019 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; + +namespace WebSocketSharp +{ + /// + /// Represents the event data for the event. + /// + /// + /// + /// That event occurs when the WebSocket connection has been closed. + /// + /// + /// 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 PayloadData _payloadData; + + #endregion + + #region Internal Constructors + + internal CloseEventArgs (PayloadData payloadData, bool clean) + { + _payloadData = payloadData; + _clean = clean; + } + + internal CloseEventArgs (ushort code, string reason, bool clean) + { + _payloadData = new PayloadData (code, reason); + _clean = clean; + } + + #endregion + + #region Public Properties + + /// + /// Gets the status code for the connection close. + /// + /// + /// A that represents the status code for + /// the connection close if present. + /// + public ushort Code { + get { + return _payloadData.Code; + } + } + + /// + /// Gets the reason for the connection close. + /// + /// + /// A that represents the reason for + /// the connection close if present. + /// + public string Reason { + get { + return _payloadData.Reason; + } + } + + /// + /// Gets a value indicating whether the connection has been closed cleanly. + /// + /// + /// true if the connection has been closed cleanly; otherwise, + /// false. + /// + public bool WasClean { + get { + return _clean; + } + } + + #endregion + } +} diff --git a/websocket-sharp-core/CloseStatusCode.cs b/websocket-sharp-core/CloseStatusCode.cs new file mode 100644 index 000000000..81f3317a4 --- /dev/null +++ b/websocket-sharp-core/CloseStatusCode.cs @@ -0,0 +1,120 @@ +#region License +/* + * CloseStatusCode.cs + * + * The MIT License + * + * 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 + * 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; + +namespace WebSocketSharp +{ + /// + /// Indicates the status code for the WebSocket connection close. + /// + /// + /// + /// The values of this enumeration are defined in + /// + /// Section 7.4 of RFC 6455. + /// + /// + /// "Reserved value" cannot be sent as a status code in + /// closing handshake by an endpoint. + /// + /// + public enum CloseStatusCode : ushort + { + /// + /// Equivalent to close status 1000. Indicates normal close. + /// + Normal = 1000, + /// + /// 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. + /// + 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. + /// + UnsupportedData = 1003, + /// + /// Equivalent to close status 1004. Still undefined. A Reserved value. + /// + Undefined = 1004, + /// + /// 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. + /// + 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 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. + /// + 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. + /// + 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 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. + /// + 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. + /// + TlsHandshakeFailure = 1015 + } +} diff --git a/websocket-sharp-core/CompressionMethod.cs b/websocket-sharp-core/CompressionMethod.cs new file mode 100644 index 000000000..42ab230a6 --- /dev/null +++ b/websocket-sharp-core/CompressionMethod.cs @@ -0,0 +1,52 @@ +#region License +/* + * CompressionMethod.cs + * + * The MIT License + * + * 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 + * 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; + +namespace WebSocketSharp +{ + /// + /// Specifies the method for compression. + /// + /// + /// The methods are defined in + /// + /// Compression Extensions for WebSocket. + /// + public enum CompressionMethod : byte + { + /// + /// Specifies no compression. + /// + None, + /// + /// Specifies DEFLATE. + /// + Deflate + } +} diff --git a/websocket-sharp-core/ErrorEventArgs.cs b/websocket-sharp-core/ErrorEventArgs.cs new file mode 100644 index 000000000..41502ab08 --- /dev/null +++ b/websocket-sharp-core/ErrorEventArgs.cs @@ -0,0 +1,109 @@ +#region License +/* + * ErrorEventArgs.cs + * + * The MIT License + * + * 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 + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Contributors +/* + * Contributors: + * - Frank Razenberg + */ +#endregion + +using System; + +namespace WebSocketSharp +{ + /// + /// Represents the event data for the event. + /// + /// + /// + /// That event occurs when the 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 it by accessing + /// the property. + /// + /// + public class ErrorEventArgs : EventArgs + { + #region Private Fields + + private Exception _exception; + private string _message; + + #endregion + + #region Internal Constructors + + internal ErrorEventArgs (string message) + : this (message, null) + { + } + + internal ErrorEventArgs (string message, Exception exception) + { + _message = message; + _exception = exception; + } + + #endregion + + #region Public Properties + + /// + /// Gets the exception that caused the error. + /// + /// + /// An instance that represents the cause of + /// the error if it is due to an exception; otherwise, . + /// + public Exception Exception { + get { + return _exception; + } + } + + /// + /// Gets the error message. + /// + /// + /// A that represents the error message. + /// + public string Message { + get { + return _message; + } + } + + #endregion + } +} diff --git a/websocket-sharp-core/Ext.cs b/websocket-sharp-core/Ext.cs new file mode 100644 index 000000000..c6b656302 --- /dev/null +++ b/websocket-sharp-core/Ext.cs @@ -0,0 +1,2268 @@ +#region License +/* + * Ext.cs + * + * Some parts of this code are derived from Mono (http://www.mono-project.com): + * - GetStatusDescription is derived from HttpListenerResponse.cs (System.Net) + * - IsPredefinedScheme is derived from Uri.cs (System) + * - MaybeUri is derived from Uri.cs (System) + * + * The MIT License + * + * Copyright (c) 2001 Garrett Rooney + * Copyright (c) 2003 Ian MacLean + * Copyright (c) 2003 Ben Maurer + * Copyright (c) 2003, 2005, 2009 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2009 Stephane Delcroix + * Copyright (c) 2010-2016 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Contributors +/* + * Contributors: + * - Liryna + * - Nikola Kovacevic + * - Chris Swiedler + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.IO; +using System.IO.Compression; +using System.Net.Sockets; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using WebSocketSharp.Net; +using WebSocketSharp.Net.WebSockets; +using WebSocketSharp.Server; + +namespace WebSocketSharp +{ + /// + /// Provides a set of static methods for websocket-sharp. + /// + public static class Ext + { + #region Private Fields + + private static readonly byte[] _last = new byte[] { 0x00 }; + private static readonly int _retry = 5; + private const string _tspecials = "()<>@,;:\\\"/[]?={} \t"; + + #endregion + + #region Private Methods + + 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)) + return input.compressToArray (); + } + + private static MemoryStream compress (this Stream stream) + { + var output = new MemoryStream (); + if (stream.Length == 0) + return output; + + stream.Position = 0; + using (var ds = new DeflateStream (output, CompressionMode.Compress, true)) { + stream.CopyTo (ds, 1024); + ds.Close (); // BFINAL set to 1. + output.Write (_last, 0, 1); + output.Position = 0; + + return output; + } + } + + private static byte[] compressToArray (this Stream stream) + { + using (var output = stream.compress ()) { + output.Close (); + return output.ToArray (); + } + } + + private static byte[] decompress (this byte[] data) + { + if (data.LongLength == 0) + return data; + + using (var input = new MemoryStream (data)) + return input.decompressToArray (); + } + + private static MemoryStream decompress (this Stream stream) + { + var output = new MemoryStream (); + if (stream.Length == 0) + return output; + + stream.Position = 0; + using (var ds = new DeflateStream (stream, CompressionMode.Decompress, true)) { + ds.CopyTo (output, 1024); + output.Position = 0; + + return output; + } + } + + private static byte[] decompressToArray (this Stream stream) + { + using (var output = stream.decompress ()) { + output.Close (); + return output.ToArray (); + } + } + + private static bool isHttpMethod (this string value) + { + return value == "GET" + || value == "HEAD" + || value == "POST" + || value == "PUT" + || value == "DELETE" + || value == "CONNECT" + || value == "OPTIONS" + || value == "TRACE"; + } + + private static bool isHttpMethod10 (this string value) + { + return value == "GET" + || value == "HEAD" + || value == "POST"; + } + + #endregion + + #region Internal Methods + + internal static byte[] Append (this ushort code, string reason) + { + var bytes = code.InternalToByteArray (ByteOrder.Big); + + if (reason == null || reason.Length == 0) + return bytes; + + var buff = new List (bytes); + buff.AddRange (Encoding.UTF8.GetBytes (reason)); + + return buff.ToArray (); + } + + internal static void Close ( + this HttpListenerResponse response, HttpStatusCode code + ) + { + response.StatusCode = (int) code; + response.OutputStream.Close (); + } + + internal static void CloseWithAuthChallenge ( + this HttpListenerResponse response, string challenge + ) + { + response.Headers.InternalSet ("WWW-Authenticate", challenge, true); + response.Close (HttpStatusCode.Unauthorized); + } + + internal static byte[] Compress (this byte[] data, CompressionMethod method) + { + return method == CompressionMethod.Deflate + ? data.compress () + : data; + } + + internal static Stream Compress (this Stream stream, CompressionMethod method) + { + return method == CompressionMethod.Deflate + ? stream.compress () + : stream; + } + + internal static byte[] CompressToArray (this Stream stream, CompressionMethod method) + { + return method == CompressionMethod.Deflate + ? stream.compressToArray () + : stream.ToByteArray (); + } + + /// + /// Determines whether the specified string contains any of characters in + /// the specified array of . + /// + /// + /// true if contains any of characters in + /// ; otherwise, false. + /// + /// + /// A to test. + /// + /// + /// An array of that contains one or more characters to + /// seek. + /// + internal static bool Contains (this string value, params char[] anyOf) + { + return anyOf != null && anyOf.Length > 0 + ? value.IndexOfAny (anyOf) > -1 + : false; + } + + internal static bool Contains ( + this NameValueCollection collection, string name + ) + { + return collection[name] != null; + } + + internal static bool Contains ( + this NameValueCollection collection, + string name, + string value, + StringComparison comparisonTypeForValue + ) + { + var val = collection[name]; + if (val == null) + return false; + + foreach (var elm in val.Split (',')) { + if (elm.Trim ().Equals (value, comparisonTypeForValue)) + return true; + } + + return false; + } + + internal static bool Contains ( + this IEnumerable source, Func condition + ) + { + foreach (T elm in source) { + if (condition (elm)) + return true; + } + + return false; + } + + internal static bool ContainsTwice (this string[] values) + { + var len = values.Length; + var end = len - 1; + + Func seek = null; + seek = idx => { + if (idx == end) + return false; + + var val = values[idx]; + 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[] source, int length) + { + var dest = new T[length]; + Array.Copy (source, 0, dest, 0, length); + + return dest; + } + + internal static T[] Copy (this T[] source, long length) + { + var dest = new T[length]; + Array.Copy (source, 0, dest, 0, length); + + return dest; + } + + internal static void CopyTo ( + this Stream source, Stream destination, int bufferLength + ) + { + var buff = new byte[bufferLength]; + var nread = 0; + + while (true) { + nread = source.Read (buff, 0, bufferLength); + if (nread <= 0) + break; + + destination.Write (buff, 0, nread); + } + } + + internal static void CopyToAsync ( + this Stream source, + Stream destination, + int bufferLength, + Action completed, + 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 (); + + return; + } + + destination.Write (buff, 0, nread); + source.BeginRead (buff, 0, bufferLength, callback, null); + } + catch (Exception ex) { + if (error != null) + error (ex); + } + }; + + try { + source.BeginRead (buff, 0, bufferLength, callback, null); + } + catch (Exception ex) { + if (error != null) + error (ex); + } + } + + internal static byte[] Decompress (this byte[] data, CompressionMethod method) + { + return method == CompressionMethod.Deflate + ? data.decompress () + : data; + } + + 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) + { + 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) + { + action (value); + return value == c - 0; + } + + /// + /// 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 CookieCollection GetCookies ( + this NameValueCollection headers, bool response + ) + { + var val = headers[response ? "Set-Cookie" : "Cookie"]; + 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 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; + } + + /// + /// Gets the name from the specified string that contains a pair of + /// name and value separated by a character. + /// + /// + /// + /// A that represents the name. + /// + /// + /// if the name is not present. + /// + /// + /// + /// A that contains a pair of name and value. + /// + /// + /// A used to separate name and value. + /// + internal static string GetName (this string nameAndValue, char separator) + { + var idx = nameAndValue.IndexOf (separator); + return idx > 0 ? nameAndValue.Substring (0, idx).Trim () : null; + } + + internal static string GetUTF8DecodedString (this byte[] bytes) + { + return Encoding.UTF8.GetString (bytes); + } + + internal static byte[] GetUTF8EncodedBytes (this string s) + { + return Encoding.UTF8.GetBytes (s); + } + + /// + /// Gets the value from the specified string that contains a pair of + /// name and value separated by a character. + /// + /// + /// + /// A that represents the value. + /// + /// + /// if the value is not present. + /// + /// + /// + /// A that contains a pair of name and value. + /// + /// + /// A used to separate name and value. + /// + internal static string GetValue (this string nameAndValue, char separator) + { + return nameAndValue.GetValue (separator, false); + } + + /// + /// Gets the value from the specified string that contains a pair of + /// name and value separated by a character. + /// + /// + /// + /// A that represents the value. + /// + /// + /// if the value is not present. + /// + /// + /// + /// A that contains a pair of name and value. + /// + /// + /// A used to separate name and value. + /// + /// + /// A : true if unquotes the value; otherwise, + /// false. + /// + 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 byte[] InternalToByteArray ( + this ushort value, ByteOrder order + ) + { + var ret = BitConverter.GetBytes (value); + + if (!order.IsHostOrder ()) + Array.Reverse (ret); + + return ret; + } + + internal static byte[] InternalToByteArray ( + this ulong value, ByteOrder order + ) + { + var ret = BitConverter.GetBytes (value); + + if (!order.IsHostOrder ()) + Array.Reverse (ret); + + return ret; + } + + internal static bool IsCompressionExtension ( + this string value, CompressionMethod method + ) + { + return value.StartsWith (method.ToExtensionString ()); + } + + internal static bool IsControl (this byte opcode) + { + return opcode > 0x7 && opcode < 0x10; + } + + internal static bool IsControl (this Opcode opcode) + { + return opcode >= Opcode.Close; + } + + internal static bool IsData (this byte opcode) + { + return opcode == 0x1 || opcode == 0x2; + } + + internal static bool IsData (this Opcode opcode) + { + return opcode == Opcode.Text || opcode == Opcode.Binary; + } + + internal static bool IsHttpMethod (this string value, Version version) + { + return version == HttpVersion.Version10 + ? value.isHttpMethod10 () + : value.isHttpMethod (); + } + + internal static bool IsPortNumber (this int value) + { + return value > 0 && value < 65536; + } + + internal static bool IsReserved (this ushort code) + { + return code == 1004 + || code == 1005 + || code == 1006 + || code == 1015; + } + + internal static bool IsReserved (this CloseStatusCode code) + { + return code == CloseStatusCode.Undefined + || code == CloseStatusCode.NoStatus + || code == CloseStatusCode.Abnormal + || code == CloseStatusCode.TlsHandshakeFailure; + } + + internal static bool IsSupported (this byte 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) { + 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; + } + + internal static bool IsToken (this string value) + { + 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 comparison = StringComparison.OrdinalIgnoreCase; + return version < HttpVersion.Version11 + ? headers.Contains ("Connection", "keep-alive", comparison) + : !headers.Contains ("Connection", "close", comparison); + } + + internal static string Quote (this string value) + { + return String.Format ("\"{0}\"", value.Replace ("\"", "\\\"")); + } + + internal static byte[] ReadBytes (this Stream stream, int length) + { + var buff = new byte[length]; + var offset = 0; + var retry = 0; + var nread = 0; + + while (length > 0) { + nread = stream.Read (buff, offset, length); + if (nread <= 0) { + if (retry < _retry) { + retry++; + continue; + } + + return buff.SubArray (0, offset); + } + + retry = 0; + + offset += nread; + length -= nread; + } + + return buff; + } + + internal static byte[] ReadBytes ( + this Stream stream, long length, int bufferLength + ) + { + using (var dest = new MemoryStream ()) { + var buff = new byte[bufferLength]; + var retry = 0; + var nread = 0; + + while (length > 0) { + if (length < bufferLength) + bufferLength = (int) length; + + nread = stream.Read (buff, 0, bufferLength); + if (nread <= 0) { + if (retry < _retry) { + retry++; + continue; + } + + break; + } + + 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 + ) + { + var buff = new byte[length]; + var offset = 0; + var retry = 0; + + AsyncCallback callback = null; + callback = + ar => { + try { + var nread = stream.EndRead (ar); + if (nread <= 0) { + if (retry < _retry) { + retry++; + stream.BeginRead (buff, offset, length, callback, null); + + return; + } + + if (completed != null) + completed (buff.SubArray (0, offset)); + + return; + } + + if (nread == length) { + if (completed != null) + completed (buff); + + return; + } + + retry = 0; + + offset += nread; + length -= nread; + + stream.BeginRead (buff, offset, length, callback, null); + } + catch (Exception ex) { + if (error != null) + error (ex); + } + }; + + try { + stream.BeginRead (buff, offset, length, callback, null); + } + catch (Exception ex) { + if (error != null) + error (ex); + } + } + + internal static void ReadBytesAsync ( + this Stream stream, + long length, + int bufferLength, + Action completed, + 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) { + if (retry < _retry) { + retry++; + read (len); + + return; + } + + if (completed != null) { + dest.Close (); + completed (dest.ToArray ()); + } + + dest.Dispose (); + return; + } + + dest.Write (buff, 0, nread); + + if (nread == len) { + if (completed != null) { + dest.Close (); + completed (dest.ToArray ()); + } + + dest.Dispose (); + return; + } + + retry = 0; + + read (len - nread); + } + catch (Exception ex) { + dest.Dispose (); + if (error != null) + error (ex); + } + }, + null + ); + }; + + try { + read (length); + } + catch (Exception ex) { + dest.Dispose (); + if (error != null) + error (ex); + } + } + + internal static T[] Reverse (this T[] array) + { + var len = array.Length; + var ret = new T[len]; + + var end = len - 1; + for (var i = 0; i <= end; i++) + ret[i] = array[end - i]; + + return ret; + } + + internal static IEnumerable SplitHeaderValue ( + this string value, params char[] separators + ) + { + var len = value.Length; + + var buff = new StringBuilder (32); + var end = len - 1; + var escaped = false; + var quoted = false; + + for (var i = 0; i <= end; i++) { + var c = value[i]; + buff.Append (c); + + if (c == '"') { + if (escaped) { + escaped = false; + continue; + } + + quoted = !quoted; + continue; + } + + if (c == '\\') { + if (i == end) + break; + + if (value[i + 1] == '"') + escaped = true; + + continue; + } + + if (Array.IndexOf (separators, c) > -1) { + if (quoted) + continue; + + buff.Length -= 1; + yield return buff.ToString (); + + buff.Length = 0; + continue; + } + } + + 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 (); + + return output.ToArray (); + } + } + + internal static CompressionMethod ToCompressionMethod (this string value) + { + 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 + ) + { + if (method == CompressionMethod.None) + return String.Empty; + + var name = String.Format ( + "permessage-{0}", method.ToString ().ToLower () + ); + + return parameters != null && parameters.Length > 0 + ? String.Format ("{0}; {1}", name, parameters.ToString ("; ")) + : name; + } + + 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 { + var addrs = System.Net.Dns.GetHostAddresses (value); + return addrs[0]; + } + catch { + return null; + } + } + + 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.ToString ()) + : address.ToString (); + } + + internal static ushort ToUInt16 (this byte[] source, ByteOrder sourceOrder) + { + return BitConverter.ToUInt16 (source.ToHostOrder (sourceOrder), 0); + } + + internal static ulong ToUInt64 (this byte[] source, ByteOrder sourceOrder) + { + return BitConverter.ToUInt64 (source.ToHostOrder (sourceOrder), 0); + } + + internal static IEnumerable Trim (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 new for WebSocket with + /// the specified . + /// + /// + /// true if the was successfully created; + /// otherwise, false. + /// + /// + /// A that represents a WebSocket URL to try. + /// + /// + /// When this method returns, a that + /// represents the 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 + ) + { + result = null; + message = null; + + var uri = uriString.ToUri (); + if (uri == null) { + message = "An invalid URI string."; + return false; + } + + if (!uri.IsAbsoluteUri) { + message = "A relative URI."; + return false; + } + + var schm = uri.Scheme; + if (!(schm == "ws" || schm == "wss")) { + 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 = "It includes the fragment component."; + return false; + } + + result = port != -1 + ? uri + : new Uri ( + String.Format ( + "{0}://{1}:{2}{3}", + schm, + uri.Host, + schm == "ws" ? 80 : 443, + uri.PathAndQuery + ) + ); + + return true; + } + + internal static bool TryGetUTF8DecodedString (this byte[] bytes, out string s) + { + s = null; + + try { + s = Encoding.UTF8.GetString (bytes); + } + catch { + return false; + } + + return true; + } + + internal static bool TryGetUTF8EncodedBytes (this string s, out byte[] bytes) + { + bytes = null; + + try { + bytes = Encoding.UTF8.GetBytes (s); + } + catch { + return false; + } + + return true; + } + + internal static bool TryOpenRead ( + this FileInfo fileInfo, out FileStream fileStream + ) + { + fileStream = null; + + try { + fileStream = fileInfo.OpenRead (); + } + catch { + return false; + } + + return true; + } + + internal static string Unquote (this string value) + { + var start = value.IndexOf ('"'); + if (start == -1) + return value; + + var end = value.LastIndexOf ('"'); + if (end == start) + return value; + + var len = end - start - 1; + return len > 0 + ? value.Substring (start + 1, len).Replace ("\\\"", "\"") + : String.Empty; + } + + internal static bool Upgrades ( + this NameValueCollection headers, string protocol + ) + { + var comparison = StringComparison.OrdinalIgnoreCase; + return headers.Contains ("Upgrade", protocol, comparison) + && headers.Contains ("Connection", "Upgrade", comparison); + } + + internal static string UrlDecode (this string value, Encoding encoding) + { + return HttpUtility.UrlDecode (value, encoding); + } + + internal static string UrlEncode (this string value, Encoding encoding) + { + return HttpUtility.UrlEncode (value, encoding); + } + + internal static void WriteBytes ( + this Stream stream, byte[] bytes, int 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 + ) + { + var src = new MemoryStream (bytes); + src.CopyToAsync ( + stream, + bufferLength, + () => { + if (completed != null) + completed (); + + src.Dispose (); + }, + ex => { + src.Dispose (); + if (error != null) + error (ex); + } + ); + } + + #endregion + + #region Public Methods + + /// + /// Emits the specified delegate if it isn't . + /// + /// + /// A to emit. + /// + /// + /// An from which emits this . + /// + /// + /// A that contains no event data. + /// + 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 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) + { + switch (code) { + case 100: return "Continue"; + case 101: return "Switching Protocols"; + case 102: return "Processing"; + case 200: return "OK"; + case 201: return "Created"; + case 202: return "Accepted"; + case 203: return "Non-Authoritative Information"; + case 204: return "No Content"; + case 205: return "Reset Content"; + case 206: return "Partial Content"; + case 207: return "Multi-Status"; + case 300: return "Multiple Choices"; + case 301: return "Moved Permanently"; + case 302: return "Found"; + case 303: return "See Other"; + case 304: return "Not Modified"; + case 305: return "Use Proxy"; + case 307: return "Temporary Redirect"; + case 400: return "Bad Request"; + case 401: return "Unauthorized"; + case 402: return "Payment Required"; + case 403: return "Forbidden"; + case 404: return "Not Found"; + case 405: return "Method Not Allowed"; + case 406: return "Not Acceptable"; + case 407: return "Proxy Authentication Required"; + case 408: return "Request Timeout"; + case 409: return "Conflict"; + case 410: return "Gone"; + case 411: return "Length Required"; + case 412: return "Precondition Failed"; + case 413: return "Request Entity Too Large"; + case 414: return "Request-Uri Too Long"; + case 415: return "Unsupported Media Type"; + case 416: return "Requested Range Not Satisfiable"; + case 417: return "Expectation Failed"; + case 422: return "Unprocessable Entity"; + case 423: return "Locked"; + case 424: return "Failed Dependency"; + case 500: return "Internal Server Error"; + case 501: return "Not Implemented"; + case 502: return "Bad Gateway"; + case 503: return "Service Unavailable"; + case 504: return "Gateway Timeout"; + case 505: return "Http Version Not Supported"; + case 507: return "Insufficient Storage"; + } + + return String.Empty; + } + + /// + /// Determines whether the specified ushort is in the range of + /// the status code for the WebSocket connection close. + /// + /// + /// + /// The ranges are the following: + /// + /// + /// + /// + /// 1000-2999: These numbers are reserved for definition by + /// the WebSocket protocol. + /// + /// + /// + /// + /// 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 range of + /// the status code for the close; otherwise, false. + /// + /// + /// A to test. + /// + public static bool IsCloseStatusCode (this ushort value) + { + return value > 999 && value < 5000; + } + + /// + /// Determines whether the specified string is enclosed in + /// the specified character. + /// + /// + /// true if is enclosed in + /// ; otherwise, false. + /// + /// + /// A to test. + /// + /// + /// A to find. + /// + public static bool IsEnclosedIn (this string value, char c) + { + if (value == null) + return false; + + var len = value.Length; + if (len < 2) + return false; + + return value[0] == c && value[len - 1] == c; + } + + /// + /// Determines whether the specified byte order is host (this computer + /// architecture) byte order. + /// + /// + /// true if is host byte order; otherwise, + /// false. + /// + /// + /// One of the enum values to test. + /// + public static bool IsHostOrder (this ByteOrder order) + { + // true: !(true ^ true) or !(false ^ false) + // false: !(true ^ false) or !(false ^ true) + return !(BitConverter.IsLittleEndian ^ (order == ByteOrder.Little)); + } + + /// + /// Determines whether the specified IP address is a local IP address. + /// + /// + /// This local means NOT REMOTE for the current host. + /// + /// + /// 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) + throw new ArgumentNullException ("address"); + + if (address.Equals (System.Net.IPAddress.Any)) + return true; + + 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 host = System.Net.Dns.GetHostName (); + var addrs = System.Net.Dns.GetHostAddresses (host); + foreach (var addr in addrs) { + if (address.Equals (addr)) + return true; + } + + return false; + } + + /// + /// Determines whether the specified string is or + /// an empty string. + /// + /// + /// true if is or + /// an empty string; otherwise, false. + /// + /// + /// A to test. + /// + public static bool IsNullOrEmpty (this string value) + { + return value == null || value.Length == 0; + } + + /// + /// Determines whether the specified string is a predefined scheme. + /// + /// + /// true if is a predefined scheme; + /// otherwise, false. + /// + /// + /// A to test. + /// + 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 == '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; + } + + /// + /// Determines whether the specified string 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) + return false; + + if (value.Length == 0) + return false; + + var idx = value.IndexOf (':'); + if (idx == -1) + return false; + + if (idx >= 10) + return false; + + var schm = value.Substring (0, idx); + return schm.IsPredefinedScheme (); + } + + /// + /// 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. + /// + /// + /// An array of T from which to retrieve a sub-array. + /// + /// + /// An that represents the zero-based index in the array + /// at which retrieving starts. + /// + /// + /// An that represents the number of elements to retrieve. + /// + /// + /// 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, int startIndex, int length) + { + if (array == null) + throw new ArgumentNullException ("array"); + + var len = array.Length; + if (len == 0) { + if (startIndex != 0) + throw new ArgumentOutOfRangeException ("startIndex"); + + if (length != 0) + throw new ArgumentOutOfRangeException ("length"); + + return array; + } + + if (startIndex < 0 || startIndex >= len) + throw new ArgumentOutOfRangeException ("startIndex"); + + if (length < 0 || length > len - startIndex) + throw new ArgumentOutOfRangeException ("length"); + + if (length == 0) + return new T[0]; + + if (length == len) + return array; + + var subArray = new T[length]; + Array.Copy (array, startIndex, subArray, 0, length); + + return subArray; + } + + /// + /// 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. + /// + /// + /// An array of T from which to retrieve a sub-array. + /// + /// + /// A that represents the zero-based index in the array + /// at which retrieving starts. + /// + /// + /// A that represents the number of elements to retrieve. + /// + /// + /// 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) + { + if (array == null) + throw new ArgumentNullException ("array"); + + var len = array.LongLength; + if (len == 0) { + if (startIndex != 0) + throw new ArgumentOutOfRangeException ("startIndex"); + + if (length != 0) + throw new ArgumentOutOfRangeException ("length"); + + return array; + } + + if (startIndex < 0 || startIndex >= len) + throw new ArgumentOutOfRangeException ("startIndex"); + + if (length < 0 || length > len - startIndex) + throw new ArgumentOutOfRangeException ("length"); + + if (length == 0) + return new T[0]; + + if (length == len) + return array; + + var subArray = new T[length]; + Array.Copy (array, startIndex, subArray, 0, length); + + return subArray; + } + + /// + /// Executes the specified delegate times. + /// + /// + /// An that specifies the number of times to execute. + /// + /// + /// An delegate to execute. + /// + public static void Times (this int n, Action action) + { + if (n <= 0) + return; + + if (action == null) + return; + + for (int i = 0; i < n; i++) + action (); + } + + /// + /// Executes the specified delegate times. + /// + /// + /// A that specifies the number of times to execute. + /// + /// + /// An delegate to execute. + /// + public static void Times (this long n, Action action) + { + if (n <= 0) + return; + + if (action == null) + return; + + for (long i = 0; i < n; i++) + action (); + } + + /// + /// Executes the specified delegate times. + /// + /// + /// A that specifies the number of times to execute. + /// + /// + /// An delegate to execute. + /// + public static void Times (this uint n, Action action) + { + if (n == 0) + return; + + if (action == null) + return; + + for (uint i = 0; i < n; i++) + action (); + } + + /// + /// Executes the specified delegate times. + /// + /// + /// A that specifies the number of times to execute. + /// + /// + /// An delegate to execute. + /// + public static void Times (this ulong n, Action action) + { + if (n == 0) + return; + + if (action == null) + return; + + for (ulong i = 0; i < n; i++) + action (); + } + + /// + /// Executes the specified delegate times. + /// + /// + /// An that specifies the number of times to execute. + /// + /// + /// + /// 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) + return; + + if (action == null) + return; + + for (int i = 0; i < n; i++) + action (i); + } + + /// + /// Executes the specified delegate times. + /// + /// + /// A that specifies the number of times to execute. + /// + /// + /// + /// An Action<long> delegate to execute. + /// + /// + /// The parameter is the zero-based count of iteration. + /// + /// + public static void Times (this long n, Action action) + { + if (n <= 0) + return; + + if (action == null) + return; + + for (long i = 0; i < n; i++) + action (i); + } + + /// + /// Executes the specified delegate times. + /// + /// + /// A that specifies the number of times to execute. + /// + /// + /// + /// An Action<uint> delegate to execute. + /// + /// + /// The parameter is the zero-based count of iteration. + /// + /// + public static void Times (this uint n, Action action) + { + if (n == 0) + return; + + if (action == null) + return; + + for (uint i = 0; i < n; i++) + action (i); + } + + /// + /// Executes the specified delegate times. + /// + /// + /// A that specifies the number of times to execute. + /// + /// + /// + /// An Action<ulong> delegate to execute. + /// + /// + /// The parameter is the zero-based count of iteration. + /// + /// + public static void Times (this ulong n, Action action) + { + if (n == 0) + return; + + if (action == null) + return; + + for (ulong i = 0; i < n; i++) + action (i); + } + + /// + /// Converts the specified byte array to the specified type value. + /// + /// + /// + /// A T converted from . + /// + /// + /// The default value of T if not converted. + /// + /// + /// + /// An array of to convert. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It specifies the byte order of . + /// + /// + /// + /// + /// The type of the return. + /// + /// + /// , , , + /// , , , + /// , , , + /// or . + /// + /// + /// + /// is . + /// + [Obsolete ("This method will be removed.")] + public static T To (this byte[] source, ByteOrder sourceOrder) + where T : struct + { + if (source == null) + throw new ArgumentNullException ("source"); + + if (source.Length == 0) + return default (T); + + var type = typeof (T); + var val = source.ToHostOrder (sourceOrder); + + return type == typeof (Boolean) + ? (T)(object) BitConverter.ToBoolean (val, 0) + : type == typeof (Char) + ? (T)(object) BitConverter.ToChar (val, 0) + : type == typeof (Double) + ? (T)(object) BitConverter.ToDouble (val, 0) + : type == typeof (Int16) + ? (T)(object) BitConverter.ToInt16 (val, 0) + : type == typeof (Int32) + ? (T)(object) BitConverter.ToInt32 (val, 0) + : type == typeof (Int64) + ? (T)(object) BitConverter.ToInt64 (val, 0) + : type == typeof (Single) + ? (T)(object) BitConverter.ToSingle (val, 0) + : type == typeof (UInt16) + ? (T)(object) BitConverter.ToUInt16 (val, 0) + : type == typeof (UInt32) + ? (T)(object) BitConverter.ToUInt32 (val, 0) + : type == typeof (UInt64) + ? (T)(object) BitConverter.ToUInt64 (val, 0) + : default (T); + } + + /// + /// Converts the specified value to a byte array. + /// + /// + /// An array of converted from . + /// + /// + /// A T to convert. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It specifies the byte order of the return. + /// + /// + /// + /// + /// The type of . + /// + /// + /// , , , + /// , , , + /// , , , + /// , or . + /// + /// + [Obsolete ("This method will be removed.")] + 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) { + if (!order.IsHostOrder ()) + Array.Reverse (bytes); + } + + return bytes; + } + + /// + /// Converts the order of elements in the specified byte array to + /// host (this computer architecture) byte order. + /// + /// + /// + /// 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. + /// + /// + /// It specifies the order of elements in . + /// + /// + /// + /// is . + /// + public static byte[] ToHostOrder (this byte[] source, ByteOrder sourceOrder) + { + if (source == null) + throw new ArgumentNullException ("source"); + + if (source.Length < 2) + return source; + + if (sourceOrder.IsHostOrder ()) + return source; + + return source.Reverse (); + } + + /// + /// Converts the specified array to a string. + /// + /// + /// + /// A converted by concatenating each element of + /// across . + /// + /// + /// An empty string if is an empty array. + /// + /// + /// + /// An array of T to convert. + /// + /// + /// A used to separate each element of + /// . + /// + /// + /// The type of elements in . + /// + /// + /// is . + /// + public static string ToString (this T[] array, string separator) + { + if (array == null) + 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); + var end = len - 1; + + for (var i = 0; i < end; i++) + buff.AppendFormat ("{0}{1}", array[i], separator); + + buff.Append (array[end].ToString ()); + return buff.ToString (); + } + + /// + /// Converts the specified string to a . + /// + /// + /// + /// A converted from . + /// + /// + /// if the conversion has failed. + /// + /// + /// + /// A to convert. + /// + public static Uri ToUri (this string value) + { + Uri ret; + Uri.TryCreate ( + value, value.MaybeUri () ? UriKind.Absolute : UriKind.Relative, out ret + ); + + return ret; + } + + /// + /// Sends the specified content data with the HTTP response. + /// + /// + /// A that represents the HTTP response + /// used to send the content data. + /// + /// + /// An array of that specifies the content data to send. + /// + /// + /// + /// is . + /// + /// + /// -or- + /// + /// + /// is . + /// + /// + [Obsolete ("This method will be removed.")] + public static void WriteContent ( + this HttpListenerResponse response, byte[] content + ) + { + if (response == null) + throw new ArgumentNullException ("response"); + + if (content == null) + throw new ArgumentNullException ("content"); + + var len = content.LongLength; + if (len == 0) { + response.Close (); + return; + } + + response.ContentLength64 = len; + + var output = response.OutputStream; + + if (len <= Int32.MaxValue) + output.Write (content, 0, (int) len); + else + output.WriteBytes (content, 1024); + + output.Close (); + } + + #endregion + } +} diff --git a/websocket-sharp-core/Fin.cs b/websocket-sharp-core/Fin.cs new file mode 100644 index 000000000..8965c378e --- /dev/null +++ b/websocket-sharp-core/Fin.cs @@ -0,0 +1,51 @@ +#region License +/* + * Fin.cs + * + * The MIT License + * + * Copyright (c) 2012-2015 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; + +namespace WebSocketSharp +{ + /// + /// Indicates whether a WebSocket frame is the final frame of a message. + /// + /// + /// The values of this enumeration are defined in + /// Section 5.2 of RFC 6455. + /// + internal enum Fin : byte + { + /// + /// Equivalent to numeric value 0. Indicates more frames of a message follow. + /// + More = 0x0, + /// + /// Equivalent to numeric value 1. Indicates the final frame of a message. + /// + Final = 0x1 + } +} diff --git a/websocket-sharp-core/HttpBase.cs b/websocket-sharp-core/HttpBase.cs new file mode 100644 index 000000000..a7dbd4026 --- /dev/null +++ b/websocket-sharp-core/HttpBase.cs @@ -0,0 +1,208 @@ +#region License +/* + * HttpBase.cs + * + * The MIT License + * + * Copyright (c) 2012-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.Generic; +using System.Collections.Specialized; +using System.IO; +using System.Text; +using System.Threading; +using WebSocketSharp.Net; + +namespace WebSocketSharp +{ + internal abstract class HttpBase + { + #region Private Fields + + private NameValueCollection _headers; + private const int _headersMaxLength = 8192; + private Version _version; + + #endregion + + #region Internal Fields + + internal byte[] EntityBodyData; + + #endregion + + #region Protected Fields + + protected const string CrLf = "\r\n"; + + #endregion + + #region Protected Constructors + + protected HttpBase (Version version, NameValueCollection headers) + { + _version = version; + _headers = headers; + } + + #endregion + + #region Public Properties + + public string EntityBody { + get { + if (EntityBodyData == null || EntityBodyData.LongLength == 0) + return String.Empty; + + Encoding enc = null; + + var contentType = _headers["Content-Type"]; + if (contentType != null && contentType.Length > 0) + enc = HttpUtility.GetEncoding (contentType); + + return (enc ?? Encoding.UTF8).GetString (EntityBodyData); + } + } + + public NameValueCollection Headers { + get { + return _headers; + } + } + + public Version ProtocolVersion { + get { + return _version; + } + } + + #endregion + + #region Private Methods + + private static byte[] readEntityBody (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."); + + return len > 1024 + ? stream.ReadBytes (len, 1024) + : len > 0 + ? stream.ReadBytes ((int) len) + : null; + } + + private static string[] readHeaders (Stream stream, int maxLength) + { + 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; + } + } + + if (!read) + throw new WebSocketException ("The length of header part is greater than the max length."); + + return Encoding.UTF8.GetString (buff.ToArray ()) + .Replace (CrLf + " ", " ") + .Replace (CrLf + "\t", " ") + .Split (new[] { CrLf }, StringSplitOptions.RemoveEmptyEntries); + } + + #endregion + + #region Protected Methods + + protected static T Read (Stream stream, Func parser, int millisecondsTimeout) + where T : HttpBase + { + var timeout = false; + var timer = new Timer ( + state => { + timeout = true; + stream.Close (); + }, + null, + millisecondsTimeout, + -1); + + T http = null; + Exception exception = null; + try { + http = parser (readHeaders (stream, _headersMaxLength)); + var contentLen = http.Headers["Content-Length"]; + if (contentLen != null && contentLen.Length > 0) + http.EntityBodyData = readEntityBody (stream, contentLen); + } + catch (Exception ex) { + exception = ex; + } + finally { + timer.Change (-1, -1); + 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 (msg != null) + throw new WebSocketException (msg, exception); + + return http; + } + + #endregion + + #region Public Methods + + public byte[] ToByteArray () + { + return Encoding.UTF8.GetBytes (ToString ()); + } + + #endregion + } +} diff --git a/websocket-sharp-core/HttpRequest.cs b/websocket-sharp-core/HttpRequest.cs new file mode 100644 index 000000000..fe74d5afb --- /dev/null +++ b/websocket-sharp-core/HttpRequest.cs @@ -0,0 +1,217 @@ +#region License +/* + * HttpRequest.cs + * + * The MIT License + * + * Copyright (c) 2012-2015 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Contributors +/* + * Contributors: + * - David Burhans + */ +#endregion + +using System; +using System.Collections.Specialized; +using System.IO; +using System.Text; +using WebSocketSharp.Net; + +namespace WebSocketSharp +{ + internal class HttpRequest : HttpBase + { + #region Private Fields + + private CookieCollection _cookies; + private string _method; + private string _uri; + + #endregion + + #region Private Constructors + + private HttpRequest (string method, string uri, Version version, NameValueCollection headers) + : base (version, headers) + { + _method = method; + _uri = uri; + } + + #endregion + + #region Internal Constructors + + internal HttpRequest (string method, string uri) + : this (method, uri, HttpVersion.Version11, new NameValueCollection ()) + { + Headers["User-Agent"] = "websocket-sharp/1.0"; + } + + #endregion + + #region Public Properties + + public AuthenticationResponse AuthenticationResponse { + get { + var res = Headers["Authorization"]; + return res != null && res.Length > 0 + ? AuthenticationResponse.Parse (res) + : null; + } + } + + public CookieCollection Cookies { + get { + if (_cookies == null) + _cookies = Headers.GetCookies (false); + + return _cookies; + } + } + + public string HttpMethod { + get { + return _method; + } + } + + public bool IsWebSocketRequest { + get { + return _method == "GET" + && ProtocolVersion > HttpVersion.Version10 + && Headers.Upgrades ("websocket"); + } + } + + public string RequestUri { + get { + return _uri; + } + } + + #endregion + + #region Internal Methods + + internal static HttpRequest CreateConnectRequest (Uri uri) + { + 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; + + return req; + } + + internal static HttpRequest CreateWebSocketRequest (Uri uri) + { + var req = new HttpRequest ("GET", uri.PathAndQuery); + var headers = req.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; + + headers["Upgrade"] = "websocket"; + headers["Connection"] = "Upgrade"; + + return req; + } + + internal HttpResponse GetResponse (Stream stream, int millisecondsTimeout) + { + var buff = ToByteArray (); + stream.Write (buff, 0, buff.Length); + + return Read (stream, HttpResponse.Parse, millisecondsTimeout); + } + + internal static HttpRequest Parse (string[] headerParts) + { + var requestLine = headerParts[0].Split (new[] { ' ' }, 3); + if (requestLine.Length != 3) + throw new ArgumentException ("Invalid request line: " + headerParts[0]); + + 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); + } + + internal static HttpRequest Read (Stream stream, int millisecondsTimeout) + { + return Read (stream, Parse, millisecondsTimeout); + } + + #endregion + + #region Public Methods + + public void SetCookies (CookieCollection cookies) + { + if (cookies == null || cookies.Count == 0) + 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 (); + } + } + + public override string ToString () + { + var output = new StringBuilder (64); + output.AppendFormat ("{0} {1} HTTP/{2}{3}", _method, _uri, ProtocolVersion, CrLf); + + 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 (); + } + + #endregion + } +} diff --git a/websocket-sharp-core/HttpResponse.cs b/websocket-sharp-core/HttpResponse.cs new file mode 100644 index 000000000..831b72783 --- /dev/null +++ b/websocket-sharp-core/HttpResponse.cs @@ -0,0 +1,209 @@ +#region License +/* + * HttpResponse.cs + * + * The MIT License + * + * Copyright (c) 2012-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.IO; +using System.Text; +using WebSocketSharp.Net; + +namespace WebSocketSharp +{ + internal class HttpResponse : HttpBase + { + #region Private Fields + + private string _code; + private string _reason; + + #endregion + + #region Private Constructors + + private HttpResponse (string code, string reason, Version version, NameValueCollection headers) + : base (version, headers) + { + _code = code; + _reason = reason; + } + + #endregion + + #region Internal Constructors + + internal HttpResponse (HttpStatusCode code) + : this (code, code.GetDescription ()) + { + } + + internal HttpResponse (HttpStatusCode code, string reason) + : this (((int) code).ToString (), reason, HttpVersion.Version11, new NameValueCollection ()) + { + Headers["Server"] = "websocket-sharp/1.0"; + } + + #endregion + + #region Public Properties + + public CookieCollection Cookies { + get { + return Headers.GetCookies (true); + } + } + + public bool HasConnectionClose { + get { + var comparison = StringComparison.OrdinalIgnoreCase; + return Headers.Contains ("Connection", "close", comparison); + } + } + + public bool IsProxyAuthenticationRequired { + get { + return _code == "407"; + } + } + + public bool IsRedirect { + get { + return _code == "301" || _code == "302"; + } + } + + public bool IsUnauthorized { + get { + return _code == "401"; + } + } + + public bool IsWebSocketResponse { + get { + return ProtocolVersion > HttpVersion.Version10 + && _code == "101" + && Headers.Upgrades ("websocket"); + } + } + + public string Reason { + get { + return _reason; + } + } + + public string StatusCode { + get { + return _code; + } + } + + #endregion + + #region Internal Methods + + internal static HttpResponse CreateCloseResponse (HttpStatusCode code) + { + var res = new HttpResponse (code); + res.Headers["Connection"] = "close"; + + return res; + } + + internal static HttpResponse CreateUnauthorizedResponse (string challenge) + { + var res = new HttpResponse (HttpStatusCode.Unauthorized); + res.Headers["WWW-Authenticate"] = challenge; + + return res; + } + + internal static HttpResponse CreateWebSocketResponse () + { + var res = new HttpResponse (HttpStatusCode.SwitchingProtocols); + + var headers = res.Headers; + headers["Upgrade"] = "websocket"; + headers["Connection"] = "Upgrade"; + + return res; + } + + internal static HttpResponse Parse (string[] headerParts) + { + var statusLine = headerParts[0].Split (new[] { ' ' }, 3); + if (statusLine.Length != 3) + throw new ArgumentException ("Invalid status line: " + headerParts[0]); + + 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); + } + + internal static HttpResponse Read (Stream stream, int millisecondsTimeout) + { + return Read (stream, Parse, millisecondsTimeout); + } + + #endregion + + #region Public Methods + + public void SetCookies (CookieCollection cookies) + { + if (cookies == null || cookies.Count == 0) + 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); + + 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 (); + } + + #endregion + } +} diff --git a/websocket-sharp-core/LogData.cs b/websocket-sharp-core/LogData.cs new file mode 100644 index 000000000..9c0843093 --- /dev/null +++ b/websocket-sharp-core/LogData.cs @@ -0,0 +1,149 @@ +#region License +/* + * LogData.cs + * + * The MIT License + * + * Copyright (c) 2013-2015 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.Diagnostics; +using System.Text; + +namespace WebSocketSharp +{ + /// + /// Represents a log data used by the class. + /// + public class LogData + { + #region Private Fields + + private StackFrame _caller; + private DateTime _date; + private LogLevel _level; + private string _message; + + #endregion + + #region Internal Constructors + + internal LogData (LogLevel level, StackFrame caller, string message) + { + _level = level; + _caller = caller; + _message = message ?? String.Empty; + _date = DateTime.Now; + } + + #endregion + + #region Public Properties + + /// + /// Gets the information of the logging method caller. + /// + /// + /// A that provides the information of the logging method caller. + /// + public StackFrame Caller { + get { + return _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. + /// + public DateTime Date { + get { + return _date; + } + } + + /// + /// Gets the logging level of the log data. + /// + /// + /// One of the enum values, indicates the logging level of the log data. + /// + public LogLevel Level { + get { + return _level; + } + } + + /// + /// Gets the message of the log data. + /// + /// + /// A that represents the message of the log data. + /// + public string Message { + get { + return _message; + } + } + + #endregion + + #region Public Methods + + /// + /// Returns a that represents the current . + /// + /// + /// A that represents the current . + /// + public override string ToString () + { + var header = String.Format ("{0}|{1,-5}|", _date, _level); + var method = _caller.GetMethod (); + var type = method.DeclaringType; +#if DEBUG + var lineNum = _caller.GetFileLineNumber (); + var headerAndCaller = + String.Format ("{0}{1}.{2}:{3}|", header, type.Name, method.Name, lineNum); +#else + var headerAndCaller = String.Format ("{0}{1}.{2}|", header, type.Name, method.Name); +#endif + var msgs = _message.Replace ("\r\n", "\n").TrimEnd ('\n').Split ('\n'); + if (msgs.Length <= 1) + return String.Format ("{0}{1}", headerAndCaller, _message); + + var buff = new StringBuilder (String.Format ("{0}{1}\n", headerAndCaller, msgs[0]), 64); + + var fmt = String.Format ("{{0,{0}}}{{1}}\n", header.Length); + for (var i = 1; i < msgs.Length; i++) + buff.AppendFormat (fmt, "", msgs[i]); + + buff.Length--; + return buff.ToString (); + } + + #endregion + } +} diff --git a/websocket-sharp-core/LogLevel.cs b/websocket-sharp-core/LogLevel.cs new file mode 100644 index 000000000..ef9967728 --- /dev/null +++ b/websocket-sharp-core/LogLevel.cs @@ -0,0 +1,63 @@ +#region License +/* + * LogLevel.cs + * + * The MIT License + * + * Copyright (c) 2013-2015 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; + +namespace WebSocketSharp +{ + /// + /// Specifies the logging level. + /// + public enum LogLevel + { + /// + /// Specifies the bottom logging level. + /// + Trace, + /// + /// Specifies the 2nd logging level from the bottom. + /// + Debug, + /// + /// Specifies the 3rd logging level from the bottom. + /// + Info, + /// + /// Specifies the 3rd logging level from the top. + /// + Warn, + /// + /// Specifies the 2nd logging level from the top. + /// + Error, + /// + /// Specifies the top logging level. + /// + Fatal + } +} diff --git a/websocket-sharp-core/Logger.cs b/websocket-sharp-core/Logger.cs new file mode 100644 index 000000000..17850e67e --- /dev/null +++ b/websocket-sharp-core/Logger.cs @@ -0,0 +1,330 @@ +#region License +/* + * Logger.cs + * + * The MIT License + * + * Copyright (c) 2013-2015 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.Diagnostics; +using System.IO; + +namespace WebSocketSharp +{ + /// + /// Provides a set of methods and properties for logging. + /// + /// + /// + /// If you output a log with lower than the value of the property, + /// 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. + /// + /// + /// If you would like to use the custom output action, you should set + /// the property to any Action<LogData, string> + /// delegate. + /// + /// + public class Logger + { + #region Private Fields + + private volatile string _file; + private volatile LogLevel _level; + private Action _output; + private object _sync; + + #endregion + + #region Public Constructors + + /// + /// Initializes a new instance of the class. + /// + /// + /// This constructor initializes the current logging level with . + /// + public Logger () + : this (LogLevel.Error, null, null) + { + } + + /// + /// Initializes a new instance of the class with + /// the specified logging . + /// + /// + /// One of the enum values. + /// + public Logger (LogLevel level) + : this (level, null, null) + { + } + + /// + /// Initializes a new instance of the class with + /// the specified logging , path to the log , + /// and action. + /// + /// + /// One of the enum values. + /// + /// + /// A that represents 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 + /// . + /// + public Logger (LogLevel level, string file, Action output) + { + _level = level; + _file = file; + _output = output ?? defaultOutput; + _sync = new object (); + } + + #endregion + + #region Public Properties + + /// + /// Gets or sets the current path to the log file. + /// + /// + /// A that represents the current path to the log file if any. + /// + public string File { + get { + return _file; + } + + set { + lock (_sync) { + _file = value; + Warn ( + String.Format ("The current path to the log file has been changed to {0}.", _file)); + } + } + } + + /// + /// Gets or sets the current logging level. + /// + /// + /// A log with lower than the value of this property cannot be outputted. + /// + /// + /// One of the enum values, specifies the current logging level. + /// + public LogLevel Level { + get { + return _level; + } + + set { + 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. + /// + /// + /// + /// 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 + /// the property. + /// + /// + /// If the value to set is , the current output action is changed to + /// the default output action. + /// + /// + public Action Output { + get { + return _output; + } + + set { + lock (_sync) { + _output = value ?? defaultOutput; + Warn ("The current output action has been changed."); + } + } + } + + #endregion + + #region Private Methods + + private static void defaultOutput (LogData data, string path) + { + var log = data.ToString (); + Console.WriteLine (log); + if (path != null && path.Length > 0) + writeToFile (log, path); + } + + private void output (string message, LogLevel level) + { + lock (_sync) { + if (_level > level) + return; + + LogData data = null; + try { + 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); + Console.WriteLine (data.ToString ()); + } + } + } + + private static void writeToFile (string value, string path) + { + using (var writer = new StreamWriter (path, true)) + using (var syncWriter = TextWriter.Synchronized (writer)) + syncWriter.WriteLine (value); + } + + #endregion + + #region Public Methods + + /// + /// Outputs as a log with . + /// + /// + /// If the current logging level is higher than , + /// this method doesn't output as a log. + /// + /// + /// A that represents the message to output as a log. + /// + public void Debug (string message) + { + if (_level > LogLevel.Debug) + return; + + output (message, LogLevel.Debug); + } + + /// + /// Outputs as a log with . + /// + /// + /// If the current logging level is higher than , + /// this method doesn't output as a log. + /// + /// + /// A that represents the message to output as a log. + /// + public void Error (string message) + { + if (_level > LogLevel.Error) + return; + + output (message, LogLevel.Error); + } + + /// + /// Outputs as a log with . + /// + /// + /// A that represents the message to output as a log. + /// + public void Fatal (string message) + { + output (message, LogLevel.Fatal); + } + + /// + /// Outputs as a log with . + /// + /// + /// If the current logging level is higher than , + /// this method doesn't output as a log. + /// + /// + /// A that represents the message to output as a log. + /// + public void Info (string message) + { + if (_level > LogLevel.Info) + return; + + output (message, LogLevel.Info); + } + + /// + /// Outputs as a log with . + /// + /// + /// If the current logging level is higher than , + /// this method doesn't output as a log. + /// + /// + /// A that represents the message to output as a log. + /// + public void Trace (string message) + { + if (_level > LogLevel.Trace) + return; + + output (message, LogLevel.Trace); + } + + /// + /// Outputs as a log with . + /// + /// + /// If the current logging level is higher than , + /// this method doesn't output as a log. + /// + /// + /// A that represents the message to output as a log. + /// + public void Warn (string message) + { + if (_level > LogLevel.Warn) + return; + + output (message, LogLevel.Warn); + } + + #endregion + } +} diff --git a/websocket-sharp-core/Mask.cs b/websocket-sharp-core/Mask.cs new file mode 100644 index 000000000..fcafac80c --- /dev/null +++ b/websocket-sharp-core/Mask.cs @@ -0,0 +1,51 @@ +#region License +/* + * Mask.cs + * + * The MIT License + * + * Copyright (c) 2012-2015 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; + +namespace WebSocketSharp +{ + /// + /// Indicates whether the payload data of a WebSocket frame is masked. + /// + /// + /// The values of this enumeration are defined in + /// Section 5.2 of RFC 6455. + /// + internal enum Mask : byte + { + /// + /// Equivalent to numeric value 0. Indicates not masked. + /// + Off = 0x0, + /// + /// Equivalent to numeric value 1. Indicates masked. + /// + On = 0x1 + } +} diff --git a/websocket-sharp-core/MessageEventArgs.cs b/websocket-sharp-core/MessageEventArgs.cs new file mode 100644 index 000000000..7940f98b7 --- /dev/null +++ b/websocket-sharp-core/MessageEventArgs.cs @@ -0,0 +1,183 @@ +#region License +/* + * MessageEventArgs.cs + * + * The MIT License + * + * 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 + * 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; + +namespace WebSocketSharp +{ + /// + /// Represents the event data for the event. + /// + /// + /// + /// That event occurs when the 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. + /// + /// + public class MessageEventArgs : EventArgs + { + #region Private Fields + + private string _data; + private bool _dataSet; + private Opcode _opcode; + private byte[] _rawData; + + #endregion + + #region Internal Constructors + + internal MessageEventArgs (WebSocketFrame frame) + { + _opcode = frame.Opcode; + _rawData = frame.PayloadData.ApplicationData; + } + + internal MessageEventArgs (Opcode opcode, byte[] rawData) + { + if ((ulong) rawData.LongLength > PayloadData.MaxLength) + throw new WebSocketException (CloseStatusCode.TooBig); + + _opcode = opcode; + _rawData = 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 if its type is + /// text or ping and if decoding it to a string has successfully done; + /// otherwise, . + /// + public string Data { + get { + setData (); + return _data; + } + } + + /// + /// Gets a value indicating whether the message type is binary. + /// + /// + /// true if the message type is binary; otherwise, false. + /// + public bool IsBinary { + get { + return _opcode == Opcode.Binary; + } + } + + /// + /// Gets a value indicating whether the message type is ping. + /// + /// + /// true if the message type is ping; otherwise, false. + /// + public bool IsPing { + get { + return _opcode == Opcode.Ping; + } + } + + /// + /// Gets a value indicating whether the message type is text. + /// + /// + /// true if the message type is text; otherwise, false. + /// + public bool IsText { + get { + return _opcode == Opcode.Text; + } + } + + /// + /// Gets the message data as an array of . + /// + /// + /// An array of that represents the message data. + /// + public byte[] RawData { + get { + setData (); + return _rawData; + } + } + + #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-core/Net/AuthenticationBase.cs b/websocket-sharp-core/Net/AuthenticationBase.cs new file mode 100644 index 000000000..107750499 --- /dev/null +++ b/websocket-sharp-core/Net/AuthenticationBase.cs @@ -0,0 +1,151 @@ +#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-core/Net/AuthenticationChallenge.cs b/websocket-sharp-core/Net/AuthenticationChallenge.cs new file mode 100644 index 000000000..3472204b9 --- /dev/null +++ b/websocket-sharp-core/Net/AuthenticationChallenge.cs @@ -0,0 +1,146 @@ +#region License +/* + * AuthenticationChallenge.cs + * + * The MIT License + * + * Copyright (c) 2013-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 class AuthenticationChallenge : AuthenticationBase + { + #region Private Constructors + + private AuthenticationChallenge (AuthenticationSchemes scheme, NameValueCollection parameters) + : base (scheme, parameters) + { + } + + #endregion + + #region Internal Constructors + + internal AuthenticationChallenge (AuthenticationSchemes scheme, string realm) + : base (scheme, new NameValueCollection ()) + { + Parameters["realm"] = realm; + if (scheme == AuthenticationSchemes.Digest) { + Parameters["nonce"] = CreateNonceValue (); + Parameters["algorithm"] = "MD5"; + Parameters["qop"] = "auth"; + } + } + + #endregion + + #region Public Properties + + public string Domain { + get { + return Parameters["domain"]; + } + } + + public string Stale { + get { + return Parameters["stale"]; + } + } + + #endregion + + #region Internal Methods + + internal static AuthenticationChallenge CreateBasicChallenge (string realm) + { + return new AuthenticationChallenge (AuthenticationSchemes.Basic, realm); + } + + internal static AuthenticationChallenge CreateDigestChallenge (string realm) + { + return new AuthenticationChallenge (AuthenticationSchemes.Digest, realm); + } + + 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; + } + + internal override string ToBasicString () + { + return String.Format ("Basic realm=\"{0}\"", Parameters["realm"]); + } + + internal override string ToDigestString () + { + var output = new StringBuilder (128); + + var domain = Parameters["domain"]; + if (domain != null) + output.AppendFormat ( + "Digest realm=\"{0}\", domain=\"{1}\", nonce=\"{2}\"", + Parameters["realm"], + domain, + Parameters["nonce"]); + else + output.AppendFormat ( + "Digest realm=\"{0}\", nonce=\"{1}\"", Parameters["realm"], Parameters["nonce"]); + + var opaque = Parameters["opaque"]; + if (opaque != null) + output.AppendFormat (", opaque=\"{0}\"", opaque); + + var stale = Parameters["stale"]; + if (stale != null) + output.AppendFormat (", stale={0}", stale); + + var algo = Parameters["algorithm"]; + if (algo != null) + output.AppendFormat (", algorithm={0}", algo); + + var qop = Parameters["qop"]; + if (qop != null) + output.AppendFormat (", qop=\"{0}\"", qop); + + return output.ToString (); + } + + #endregion + } +} diff --git a/websocket-sharp-core/Net/AuthenticationResponse.cs b/websocket-sharp-core/Net/AuthenticationResponse.cs new file mode 100644 index 000000000..0257d85b2 --- /dev/null +++ b/websocket-sharp-core/Net/AuthenticationResponse.cs @@ -0,0 +1,323 @@ +#region License +/* + * AuthenticationResponse.cs + * + * ParseBasicCredentials is derived from System.Net.HttpListenerContext.cs 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 + * + * 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.Security.Cryptography; +using System.Security.Principal; +using System.Text; + +namespace WebSocketSharp.Net +{ + internal class AuthenticationResponse : AuthenticationBase + { + #region Private Fields + + private uint _nonceCount; + + #endregion + + #region Private Constructors + + private AuthenticationResponse (AuthenticationSchemes scheme, NameValueCollection parameters) + : base (scheme, parameters) + { + } + + #endregion + + #region Internal Constructors + + internal AuthenticationResponse (NetworkCredential credentials) + : this (AuthenticationSchemes.Basic, new NameValueCollection (), credentials, 0) + { + } + + internal AuthenticationResponse ( + AuthenticationChallenge challenge, NetworkCredential credentials, uint nonceCount) + : this (challenge.Scheme, challenge.Parameters, credentials, nonceCount) + { + } + + internal AuthenticationResponse ( + AuthenticationSchemes scheme, + NameValueCollection parameters, + NetworkCredential credentials, + uint nonceCount) + : base (scheme, parameters) + { + Parameters["username"] = credentials.Username; + Parameters["password"] = credentials.Password; + Parameters["uri"] = credentials.Domain; + _nonceCount = nonceCount; + if (scheme == AuthenticationSchemes.Digest) + initAsDigest (); + } + + #endregion + + #region Internal Properties + + internal uint NonceCount { + get { + return _nonceCount < UInt32.MaxValue + ? _nonceCount + : 0; + } + } + + #endregion + + #region Public Properties + + public string Cnonce { + get { + return Parameters["cnonce"]; + } + } + + public string Nc { + get { + return Parameters["nc"]; + } + } + + public string Password { + get { + return Parameters["password"]; + } + } + + public string Response { + get { + return Parameters["response"]; + } + } + + public string Uri { + get { + return Parameters["uri"]; + } + } + + public string UserName { + get { + return Parameters["username"]; + } + } + + #endregion + + #region Private Methods + + 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) + { + return String.Format ( + "{0}:{1}:{2}", hash (createA1 (username, password, realm)), nonce, cnonce); + } + + private static string createA2 (string method, string uri) + { + return String.Format ("{0}:{1}", method, uri); + } + + private static string createA2 (string method, string uri, string entity) + { + return String.Format ("{0}:{1}:{2}", method, uri, hash (entity)); + } + + private static string hash (string value) + { + var src = Encoding.UTF8.GetBytes (value); + var md5 = MD5.Create (); + var hashed = md5.ComputeHash (src); + + var res = new StringBuilder (64); + foreach (var b in hashed) + res.Append (b.ToString ("x2")); + + return res.ToString (); + } + + private void initAsDigest () + { + 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); + } + else { + Parameters["qop"] = null; + } + } + + Parameters["method"] = "GET"; + Parameters["response"] = CreateRequestDigest (Parameters); + } + + #endregion + + #region Internal Methods + + internal static string CreateRequestDigest (NameValueCollection parameters) + { + var user = parameters["username"]; + var pass = parameters["password"]; + var realm = parameters["realm"]; + var nonce = parameters["nonce"]; + var uri = parameters["uri"]; + var algo = parameters["algorithm"]; + var qop = parameters["qop"]; + var cnonce = parameters["cnonce"]; + var nc = parameters["nc"]; + var method = parameters["method"]; + + var a1 = algo != null && algo.ToLower () == "md5-sess" + ? createA1 (user, pass, realm, nonce, cnonce) + : createA1 (user, pass, realm); + + var a2 = qop != null && qop.ToLower () == "auth-int" + ? createA2 (method, uri, parameters["entity"]) + : createA2 (method, uri); + + 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}", nonce, hash (a2)); + + return hash (String.Format ("{0}:{1}", secret, data)); + } + + 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; + } + catch { + } + + return null; + } + + internal static NameValueCollection ParseBasicCredentials (string value) + { + // Decode the basic-credentials (a Base64 encoded string). + var userPass = Encoding.Default.GetString (Convert.FromBase64String (value)); + + // 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 res = new NameValueCollection (); + res["username"] = user; + res["password"] = pass; + + return res; + } + + internal override string ToBasicString () + { + var userPass = String.Format ("{0}:{1}", Parameters["username"], Parameters["password"]); + var cred = Convert.ToBase64String (Encoding.UTF8.GetBytes (userPass)); + + return "Basic " + cred; + } + + internal override string ToDigestString () + { + var output = new StringBuilder (256); + output.AppendFormat ( + "Digest username=\"{0}\", realm=\"{1}\", nonce=\"{2}\", uri=\"{3}\", response=\"{4}\"", + Parameters["username"], + Parameters["realm"], + Parameters["nonce"], + Parameters["uri"], + Parameters["response"]); + + var opaque = Parameters["opaque"]; + if (opaque != null) + output.AppendFormat (", opaque=\"{0}\"", opaque); + + var algo = Parameters["algorithm"]; + if (algo != null) + output.AppendFormat (", algorithm={0}", algo); + + var qop = Parameters["qop"]; + if (qop != null) + output.AppendFormat ( + ", qop={0}, cnonce=\"{1}\", nc={2}", qop, Parameters["cnonce"], Parameters["nc"]); + + return output.ToString (); + } + + #endregion + + #region Public Methods + + 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; + } + + #endregion + } +} diff --git a/websocket-sharp-core/Net/AuthenticationSchemes.cs b/websocket-sharp-core/Net/AuthenticationSchemes.cs new file mode 100644 index 000000000..ab7721a15 --- /dev/null +++ b/websocket-sharp-core/Net/AuthenticationSchemes.cs @@ -0,0 +1,66 @@ +#region License +/* + * AuthenticationSchemes.cs + * + * 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-2016 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Atsushi Enomoto + */ +#endregion + +using System; + +namespace WebSocketSharp.Net +{ + /// + /// Specifies the scheme for authentication. + /// + public enum AuthenticationSchemes + { + /// + /// No authentication is allowed. + /// + None, + /// + /// Specifies digest authentication. + /// + Digest = 1, + /// + /// Specifies basic authentication. + /// + Basic = 8, + /// + /// Specifies anonymous authentication. + /// + Anonymous = 0x8000 + } +} diff --git a/websocket-sharp-core/Net/Chunk.cs b/websocket-sharp-core/Net/Chunk.cs new file mode 100644 index 000000000..7b6268b7f --- /dev/null +++ b/websocket-sharp-core/Net/Chunk.cs @@ -0,0 +1,91 @@ +#region License +/* + * Chunk.cs + * + * This code is derived from ChunkStream.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2003 Ximian, Inc (http://www.ximian.com) + * Copyright (c) 2014-2015 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: + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; + +namespace WebSocketSharp.Net +{ + internal class Chunk + { + #region Private Fields + + private byte[] _data; + private int _offset; + + #endregion + + #region Public Constructors + + public Chunk (byte[] data) + { + _data = data; + } + + #endregion + + #region Public Properties + + public int ReadLeft { + get { + return _data.Length - _offset; + } + } + + #endregion + + #region Public Methods + + public int Read (byte[] buffer, int offset, int count) + { + var left = _data.Length - _offset; + if (left == 0) + return left; + + if (count > left) + count = left; + + Buffer.BlockCopy (_data, _offset, buffer, offset, count); + _offset += count; + + return count; + } + + #endregion + } +} diff --git a/websocket-sharp-core/Net/ChunkStream.cs b/websocket-sharp-core/Net/ChunkStream.cs new file mode 100644 index 000000000..a5271b573 --- /dev/null +++ b/websocket-sharp-core/Net/ChunkStream.cs @@ -0,0 +1,360 @@ +#region License +/* + * ChunkStream.cs + * + * This code is derived from ChunkStream.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2003 Ximian, Inc (http://www.ximian.com) + * Copyright (c) 2012-2015 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: + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Net; +using System.Text; + +namespace WebSocketSharp.Net +{ + internal class ChunkStream + { + #region Private Fields + + private int _chunkRead; + private int _chunkSize; + private List _chunks; + private bool _gotIt; + private WebHeaderCollection _headers; + private StringBuilder _saved; + private bool _sawCr; + private InputChunkState _state; + private int _trailerState; + + #endregion + + #region Public Constructors + + 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 { + get { + return _headers; + } + } + + #endregion + + #region Public Properties + + public int ChunkLeft { + get { + return _chunkSize - _chunkRead; + } + } + + public bool WantMore { + get { + return _state != InputChunkState.End; + } + } + + #endregion + + #region Private Methods + + 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; + } + + 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) { + if (buffer[offset++] != 13) + throwProtocolViolation ("CR is expected."); + + _sawCr = true; + if (offset == length) + return InputChunkState.DataEnded; + } + + if (buffer[offset++] != 10) + throwProtocolViolation ("LF is expected."); + + return InputChunkState.None; + } + + 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."); + + break; + } + + if (b == 13) { + _sawCr = true; + continue; + } + + if (b == 10) + throwProtocolViolation ("LF is unexpected."); + + if (b == 32) // SP + _gotIt = true; + + if (!_gotIt) + _saved.Append ((char) b); + + if (_saved.Length > 20) + throwProtocolViolation ("The chunk size is too long."); + } + + if (!_sawCr || b != 10) + return InputChunkState.None; + + _chunkRead = 0; + try { + _chunkSize = Int32.Parse ( + removeChunkExtension (_saved.ToString ()), NumberStyles.HexNumber); + } + catch { + throwProtocolViolation ("The chunk size cannot be parsed."); + } + + if (_chunkSize == 0) { + _trailerState = 2; + return InputChunkState.Trailer; + } + + return InputChunkState.Data; + } + + 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 && _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 (b != 10) + throwProtocolViolation ("LF is expected."); + + _trailerState++; + continue; + } + + if (b == 13) { + _trailerState++; + continue; + } + + if (b == 10) + throwProtocolViolation ("LF is unexpected."); + + _trailerState = 0; + } + + if (_trailerState < 4) + return InputChunkState.Trailer; + + _saved.Length -= 2; + var reader = new StringReader (_saved.ToString ()); + + 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); + } + + private void write (byte[] buffer, ref 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; + + _saved.Length = 0; + _sawCr = false; + _gotIt = false; + } + + if (_state == InputChunkState.Data && offset < length) { + _state = writeData (buffer, ref offset, length); + if (_state == InputChunkState.Data) + return; + } + + if (_state == InputChunkState.DataEnded && offset < length) { + _state = seekCrLf (buffer, ref offset, length); + if (_state == InputChunkState.DataEnded) + return; + + _sawCr = false; + } + + if (_state == InputChunkState.Trailer && offset < length) { + _state = setTrailer (buffer, ref offset, length); + if (_state == InputChunkState.Trailer) + return; + + _saved.Length = 0; + } + + if (offset < length) + write (buffer, ref offset, 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)); + + offset += cnt; + _chunkRead += cnt; + + return _chunkRead == _chunkSize ? InputChunkState.DataEnded : InputChunkState.Data; + } + + #endregion + + #region Internal Methods + + internal void ResetBuffer () + { + _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); + } + + #endregion + + #region Public Methods + + public int Read (byte[] buffer, int offset, int count) + { + if (count <= 0) + return 0; + + return read (buffer, offset, count); + } + + public void Write (byte[] buffer, int offset, int count) + { + if (count <= 0) + return; + + write (buffer, ref offset, offset + count); + } + + #endregion + } +} diff --git a/websocket-sharp-core/Net/ChunkedRequestStream.cs b/websocket-sharp-core/Net/ChunkedRequestStream.cs new file mode 100644 index 000000000..913b505c3 --- /dev/null +++ b/websocket-sharp-core/Net/ChunkedRequestStream.cs @@ -0,0 +1,211 @@ +#region License +/* + * ChunkedRequestStream.cs + * + * This code is derived from ChunkedInputStream.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-2015 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: + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; +using System.IO; + +namespace WebSocketSharp.Net +{ + internal class ChunkedRequestStream : RequestStream + { + #region Private Fields + + private const int _bufferLength = 8192; + private HttpListenerContext _context; + private ChunkStream _decoder; + private bool _disposed; + private bool _noMoreData; + + #endregion + + #region Internal Constructors + + internal ChunkedRequestStream ( + Stream stream, byte[] buffer, int offset, int count, HttpListenerContext context) + : base (stream, buffer, offset, count) + { + _context = context; + _decoder = new ChunkStream ((WebHeaderCollection) context.Request.Headers); + } + + #endregion + + #region Internal Properties + + internal ChunkStream Decoder { + get { + return _decoder; + } + + set { + _decoder = value; + } + } + + #endregion + + #region Private Methods + + 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; + 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); + ares.Complete (ex); + } + } + + #endregion + + #region Public Methods + + public override IAsyncResult BeginRead ( + byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + 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."); + + 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) { + _noMoreData = nread == 0; + ares.Count = nread; + ares.Complete (); + + return ares; + } + + ares.Buffer = new byte[_bufferLength]; + ares.Offset = 0; + 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; + } + + public override void Close () + { + if (_disposed) + return; + + _disposed = true; + base.Close (); + } + + public override int EndRead (IAsyncResult asyncResult) + { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + if (asyncResult == null) + throw new ArgumentNullException ("asyncResult"); + + var ares = asyncResult as HttpStreamAsyncResult; + if (ares == null) + throw new ArgumentException ("A wrong IAsyncResult.", "asyncResult"); + + if (!ares.IsCompleted) + ares.AsyncWaitHandle.WaitOne (); + + if (ares.HasException) + throw new HttpListenerException (400, "I/O operation aborted."); + + return ares.Count; + } + + public override int Read (byte[] buffer, int offset, int count) + { + var ares = BeginRead (buffer, offset, count, null, null); + return EndRead (ares); + } + + #endregion + } +} diff --git a/websocket-sharp-core/Net/ClientSslConfiguration.cs b/websocket-sharp-core/Net/ClientSslConfiguration.cs new file mode 100644 index 000000000..800bcb30d --- /dev/null +++ b/websocket-sharp-core/Net/ClientSslConfiguration.cs @@ -0,0 +1,291 @@ +#region License +/* + * ClientSslConfiguration.cs + * + * The MIT License + * + * Copyright (c) 2014 liryna + * 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 + * 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; +using System.Net.Security; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; + +namespace WebSocketSharp.Net +{ + /// + /// Stores the parameters for the used by clients. + /// + public class ClientSslConfiguration + { + #region Private Fields + + 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. + /// + public ClientSslConfiguration () + { + _enabledSslProtocols = SslProtocols.Default; + } + + /// + /// Initializes a new instance of the class + /// with the specified . + /// + /// + /// A that represents the target host server name. + /// + public ClientSslConfiguration (string targetHost) + { + _targetHost = targetHost; + _enabledSslProtocols = SslProtocols.Default; + } + + /// + /// Copies the parameters from the specified to + /// a new instance of the class. + /// + /// + /// A from which to copy. + /// + /// + /// is . + /// + public ClientSslConfiguration (ClientSslConfiguration configuration) + { + 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 + + #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 during + /// authentication; otherwise, false. + /// + /// + /// The default value is false. + /// + /// + public bool CheckCertificateRevocation { + get { + return _checkCertRevocation; + } + + set { + _checkCertRevocation = value; + } + } + + /// + /// Gets or sets the certificates from which to select one to + /// supply to the server. + /// + /// + /// + /// A or . + /// + /// + /// That collection contains client certificates from which to select. + /// + /// + /// The default value is . + /// + /// + public X509CertificateCollection ClientCertificates { + get { + return _clientCerts; + } + + set { + _clientCerts = value; + } + } + + /// + /// Gets or sets the callback used to select the certificate to + /// supply to the server. + /// + /// + /// No certificate is supplied if the callback returns + /// . + /// + /// + /// + /// A delegate that + /// invokes the method called for selecting the certificate. + /// + /// + /// The default value is a delegate that invokes a method that + /// only returns . + /// + /// + public LocalCertificateSelectionCallback ClientCertificateSelectionCallback { + get { + if (_clientCertSelectionCallback == null) + _clientCertSelectionCallback = defaultSelectClientCertificate; + + return _clientCertSelectionCallback; + } + + set { + _clientCertSelectionCallback = value; + } + } + + /// + /// Gets or sets the protocols used for authentication. + /// + /// + /// + /// The enum values that represent + /// the protocols used for authentication. + /// + /// + /// 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. + /// + /// + /// The certificate is valid if the callback returns true. + /// + /// + /// + /// A delegate that + /// invokes the method called for validating the certificate. + /// + /// + /// The default value is a delegate that invokes a method that + /// only returns true. + /// + /// + public RemoteCertificateValidationCallback ServerCertificateValidationCallback { + get { + if (_serverCertValidationCallback == null) + _serverCertValidationCallback = defaultValidateServerCertificate; + + return _serverCertValidationCallback; + } + + set { + _serverCertValidationCallback = value; + } + } + + /// + /// Gets or sets the target host server name. + /// + /// + /// + /// A or + /// if not specified. + /// + /// + /// That string represents the name of the server that + /// will share a secure connection with a client. + /// + /// + public string TargetHost { + get { + return _targetHost; + } + + set { + _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-core/Net/Cookie.cs b/websocket-sharp-core/Net/Cookie.cs new file mode 100644 index 000000000..1c5a4bf2d --- /dev/null +++ b/websocket-sharp-core/Net/Cookie.cs @@ -0,0 +1,1016 @@ +#region License +/* + * Cookie.cs + * + * 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-2019 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: + * - Lawrence Pit + * - Gonzalo Paniagua Javier + * - Daniel Nauck + * - Sebastien Pouliot + */ +#endregion + +using System; +using System.Globalization; +using System.Text; + +namespace WebSocketSharp.Net +{ + /// + /// Provides a set of methods and properties used to manage an HTTP cookie. + /// + /// + /// + /// This class refers to the following specifications: + /// + /// + /// + /// + /// Netscape specification + /// + /// + /// + /// + /// RFC 2109 + /// + /// + /// + /// + /// RFC 2965 + /// + /// + /// + /// + /// RFC 6265 + /// + /// + /// + /// + /// This class cannot be inherited. + /// + /// + [Serializable] + public sealed class Cookie + { + #region Private Fields + + private string _comment; + 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[] _reservedCharsForValue; + private string _sameSite; + private bool _secure; + private DateTime _timeStamp; + private string _value; + private int _version; + + #endregion + + #region Static Constructor + + static Cookie () + { + _emptyPorts = new int[0]; + _reservedCharsForValue = new[] { ';', ',' }; + } + + #endregion + + #region Internal Constructors + + /// + /// Initializes a new instance of the class. + /// + internal Cookie () + { + init (String.Empty, String.Empty, String.Empty, String.Empty); + } + + #endregion + + #region Public Constructors + + /// + /// Initializes a new instance of the class with + /// the specified name and value. + /// + /// + /// + /// A that specifies the name of the cookie. + /// + /// + /// The name must be a token defined in + /// + /// RFC 2616. + /// + /// + /// + /// A that specifies the value of the cookie. + /// + /// + /// is . + /// + /// + /// + /// is an empty string. + /// + /// + /// - or - + /// + /// + /// starts with a dollar sign. + /// + /// + /// - or - + /// + /// + /// contains an invalid character. + /// + /// + /// - or - + /// + /// + /// is a string not enclosed in double quotes + /// that contains an invalid character. + /// + /// + public Cookie (string name, string value) + : this (name, value, String.Empty, String.Empty) + { + } + + /// + /// Initializes a new instance of the class with + /// the specified name, value, and path. + /// + /// + /// + /// A that specifies the name of the cookie. + /// + /// + /// The name must be a token defined in + /// + /// RFC 2616. + /// + /// + /// + /// A that specifies the value of the cookie. + /// + /// + /// A that specifies the value of the Path + /// attribute of the cookie. + /// + /// + /// is . + /// + /// + /// + /// is an empty string. + /// + /// + /// - or - + /// + /// + /// starts with a dollar sign. + /// + /// + /// - or - + /// + /// + /// contains an invalid character. + /// + /// + /// - or - + /// + /// + /// is a string not enclosed in double quotes + /// that contains an invalid character. + /// + /// + public Cookie (string name, string value, string path) + : this (name, value, path, String.Empty) + { + } + + /// + /// Initializes a new instance of the class with + /// the specified name, value, path, and domain. + /// + /// + /// + /// A that specifies the name of the cookie. + /// + /// + /// The name must be a token defined in + /// + /// RFC 2616. + /// + /// + /// + /// A that specifies the value of the cookie. + /// + /// + /// A that specifies the value of the Path + /// attribute of the cookie. + /// + /// + /// A that specifies the value of the Domain + /// attribute of the cookie. + /// + /// + /// is . + /// + /// + /// + /// is an empty string. + /// + /// + /// - or - + /// + /// + /// starts with a dollar sign. + /// + /// + /// - or - + /// + /// + /// contains an invalid character. + /// + /// + /// - or - + /// + /// + /// is a string not enclosed in double quotes + /// that contains an invalid character. + /// + /// + public Cookie (string name, string value, string path, string 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 + + #region Internal Properties + + internal bool ExactDomain { + get { + return _domain.Length == 0 || _domain[0] != '.'; + } + } + + internal int MaxAge { + get { + if (_expires == DateTime.MinValue) + return 0; + + var expires = _expires.Kind != DateTimeKind.Local + ? _expires.ToLocalTime () + : _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 ?? _emptyPorts; + } + } + + internal string SameSite { + get { + return _sameSite; + } + + set { + _sameSite = value; + } + } + + #endregion + + #region Public Properties + + /// + /// Gets the value of the Comment attribute 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; + } + + internal set { + _comment = value; + } + } + + /// + /// 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. + /// + /// + /// if not present. + /// + /// + /// The default value is . + /// + /// + public Uri CommentUri { + get { + return _commentUri; + } + + internal set { + _commentUri = value; + } + } + + /// + /// 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. + /// + /// + public bool Discard { + get { + return _discard; + } + + internal set { + _discard = value; + } + } + + /// + /// Gets or sets the value of the Domain attribute of the cookie. + /// + /// + /// + /// A that represents the domain name that + /// the cookie is valid for. + /// + /// + /// An empty string if this attribute is not needed. + /// + /// + public string Domain { + get { + return _domain; + } + + set { + _domain = value ?? String.Empty; + } + } + + /// + /// Gets or sets a value indicating whether the cookie has expired. + /// + /// + /// + /// true if the cookie has expired; otherwise, false. + /// + /// + /// The default value is false. + /// + /// + public bool Expired { + get { + return _expires != DateTime.MinValue && _expires <= DateTime.Now; + } + + set { + _expires = value ? DateTime.Now : DateTime.MinValue; + } + } + + /// + /// Gets or sets the value of the Expires attribute of the cookie. + /// + /// + /// + /// A that represents the date and time that + /// the cookie expires on. + /// + /// + /// if this attribute is not needed. + /// + /// + /// The default value is . + /// + /// + public DateTime Expires { + get { + return _expires; + } + + set { + _expires = value; + } + } + + /// + /// 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. + /// + /// + public bool HttpOnly { + get { + return _httpOnly; + } + + set { + _httpOnly = value; + } + } + + /// + /// Gets or sets 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 . + /// + /// + /// + /// The value specified for a set operation is an empty string. + /// + /// + /// - 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. + /// + /// + public string Name { + get { + return _name; + } + + set { + 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; + } + } + + /// + /// Gets or sets the value of the Path attribute of the cookie. + /// + /// + /// A that represents the subset of URI on + /// the origin server that the cookie applies to. + /// + public string Path { + get { + return _path; + } + + set { + _path = value ?? String.Empty; + } + } + + /// + /// Gets the value of the Port attribute of the cookie. + /// + /// + /// + /// A that represents the list of TCP ports + /// that the cookie applies to. + /// + /// + /// if not present. + /// + /// + /// The default value is . + /// + /// + public string Port { + get { + return _port; + } + + internal set { + int[] ports; + if (!tryCreatePorts (value, out ports)) + return; + + _port = value; + _ports = ports; + } + } + + /// + /// 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 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. + /// + /// + public bool Secure { + get { + return _secure; + } + + set { + _secure = value; + } + } + + /// + /// Gets the time when the cookie was issued. + /// + /// + /// A that represents the time when + /// the cookie was issued. + /// + public DateTime TimeStamp { + get { + return _timeStamp; + } + } + + /// + /// Gets or sets the value of the cookie. + /// + /// + /// A that represents the value of the cookie. + /// + /// + /// The value specified for a set operation is a string not enclosed in + /// double quotes that contains an invalid character. + /// + public string Value { + get { + return _value; + } + + set { + 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"); + } + } + + _value = value; + } + } + + /// + /// Gets the value of the Version attribute of the cookie. + /// + /// + /// + /// 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. + /// + /// + public int Version { + get { + return _version; + } + + internal set { + if (value < 0 || value > 1) + return; + + _version = value; + } + } + + #endregion + + #region Private Methods + + 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); + } + + private void init (string name, string value, string path, string domain) + { + _name = name; + _value = value; + _path = path; + _domain = domain; + + _expires = DateTime.MinValue; + _timeStamp = DateTime.Now; + } + + private string toResponseStringVersion0 () + { + var buff = new StringBuilder (64); + + buff.AppendFormat ("{0}={1}", _name, _value); + + if (_expires != DateTime.MinValue) { + buff.AppendFormat ( + "; Expires={0}", + _expires.ToUniversalTime ().ToString ( + "ddd, dd'-'MMM'-'yyyy HH':'mm':'ss 'GMT'", + CultureInfo.CreateSpecificCulture ("en-US") + ) + ); + } + + if (!_path.IsNullOrEmpty ()) + buff.AppendFormat ("; Path={0}", _path); + + if (!_domain.IsNullOrEmpty ()) + buff.AppendFormat ("; Domain={0}", _domain); + + if (!_sameSite.IsNullOrEmpty ()) + buff.AppendFormat ("; SameSite={0}", _sameSite); + + if (_secure) + buff.Append ("; Secure"); + + if (_httpOnly) + buff.Append ("; HttpOnly"); + + return buff.ToString (); + } + + private string toResponseStringVersion1 () + { + var buff = new StringBuilder (64); + + buff.AppendFormat ("{0}={1}; Version={2}", _name, _value, _version); + + if (_expires != DateTime.MinValue) + buff.AppendFormat ("; Max-Age={0}", MaxAge); + + if (!_path.IsNullOrEmpty ()) + buff.AppendFormat ("; Path={0}", _path); + + if (!_domain.IsNullOrEmpty ()) + buff.AppendFormat ("; Domain={0}", _domain); + + if (_port != null) { + if (_port != "\"\"") + buff.AppendFormat ("; Port={0}", _port); + else + buff.Append ("; Port"); + } + + if (_comment != null) + buff.AppendFormat ("; Comment={0}", HttpUtility.UrlEncode (_comment)); + + if (_commentUri != null) { + var url = _commentUri.OriginalString; + buff.AppendFormat ( + "; CommentURL={0}", !url.IsToken () ? url.Quote () : url + ); + } + + if (_discard) + buff.Append ("; Discard"); + + if (_secure) + buff.Append ("; Secure"); + + return buff.ToString (); + } + + private static bool tryCreatePorts (string value, out int[] result) + { + result = null; + + var arr = value.Trim ('"').Split (','); + var len = arr.Length; + var res = new int[len]; + + for (var i = 0; i < len; i++) { + var s = arr[i].Trim (); + if (s.Length == 0) { + res[i] = Int32.MinValue; + continue; + } + + if (!Int32.TryParse (s, out res[i])) + return false; + } + + result = res; + return true; + } + + #endregion + + #region Internal Methods + + 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) + return String.Empty; + + if (_version == 0) + return String.Format ("{0}={1}", _name, _value); + + var buff = new StringBuilder (64); + + buff.AppendFormat ("$Version={0}; {1}={2}", _version, _name, _value); + + if (!_path.IsNullOrEmpty ()) + buff.AppendFormat ("; $Path={0}", _path); + else if (uri != null) + buff.AppendFormat ("; $Path={0}", uri.GetAbsolutePath ()); + else + buff.Append ("; $Path=/"); + + if (!_domain.IsNullOrEmpty ()) { + if (uri == null || uri.Host != _domain) + buff.AppendFormat ("; $Domain={0}", _domain); + } + + if (_port != null) { + if (_port != "\"\"") + buff.AppendFormat ("; $Port={0}", _port); + else + buff.Append ("; $Port"); + } + + return buff.ToString (); + } + + /// + /// Returns a string that represents the current cookie instance. + /// + /// + /// A that is suitable for the Set-Cookie response + /// header. + /// + internal string ToResponseString () + { + return _name.Length == 0 + ? String.Empty + : _version == 0 + ? toResponseStringVersion0 () + : 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 + + #region Public Methods + + /// + /// Determines whether the current cookie instance is equal to + /// the specified instance. + /// + /// + /// + /// An instance to compare with + /// the current cookie instance. + /// + /// + /// An reference to a instance. + /// + /// + /// + /// true if the current cookie instance is equal to + /// ; otherwise, false. + /// + public override bool Equals (object comparand) + { + var cookie = comparand as Cookie; + 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; + } + + /// + /// Gets a hash code for the current cookie instance. + /// + /// + /// 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 + ); + } + + /// + /// Returns a string that represents the current cookie instance. + /// + /// + /// A that is suitable for the Cookie request header. + /// + public override string ToString () + { + return ToRequestString (null); + } + + #endregion + } +} diff --git a/websocket-sharp-core/Net/CookieCollection.cs b/websocket-sharp-core/Net/CookieCollection.cs new file mode 100644 index 000000000..8c0322bda --- /dev/null +++ b/websocket-sharp-core/Net/CookieCollection.cs @@ -0,0 +1,821 @@ +#region License +/* + * CookieCollection.cs + * + * 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-2019 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: + * - Lawrence Pit + * - Gonzalo Paniagua Javier + * - Sebastien Pouliot + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Text; + +namespace WebSocketSharp.Net +{ + /// + /// Provides a collection of instances of the class. + /// + [Serializable] + public class CookieCollection : ICollection + { + #region Private Fields + + private List _list; + private bool _readOnly; + private object _sync; + + #endregion + + #region Public Constructors + + /// + /// Initializes a new instance of the class. + /// + public CookieCollection () + { + _list = new List (); + _sync = ((ICollection) _list).SyncRoot; + } + + #endregion + + #region Internal Properties + + internal IList List { + get { + return _list; + } + } + + internal IEnumerable Sorted { + get { + var list = new List (_list); + if (list.Count > 1) + list.Sort (compareForSorted); + + return list; + } + } + + #endregion + + #region Public Properties + + /// + /// Gets the number of cookies in the collection. + /// + /// + /// An that represents the number of cookies in + /// the collection. + /// + public int Count { + get { + return _list.Count; + } + } + + /// + /// Gets a value indicating whether the collection is read-only. + /// + /// + /// + /// true if the collection is read-only; otherwise, false. + /// + /// + /// The default value is false. + /// + /// + public bool IsReadOnly { + get { + return _readOnly; + } + + internal set { + _readOnly = value; + } + } + + /// + /// 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. + /// + /// + public bool IsSynchronized { + get { + return false; + } + } + + /// + /// Gets the cookie at the specified index from the collection. + /// + /// + /// A at the specified index in the collection. + /// + /// + /// An that specifies the zero-based index of the cookie + /// to find. + /// + /// + /// is out of allowable range for the collection. + /// + public Cookie this[int index] { + get { + if (index < 0 || index >= _list.Count) + throw new ArgumentOutOfRangeException ("index"); + + return _list[index]; + } + } + + /// + /// Gets the cookie with the specified name from the collection. + /// + /// + /// + /// A with the specified name in the collection. + /// + /// + /// if not found. + /// + /// + /// + /// A that specifies the name of the cookie to find. + /// + /// + /// is . + /// + public Cookie this[string name] { + get { + if (name == null) + throw new ArgumentNullException ("name"); + + var caseInsensitive = StringComparison.InvariantCultureIgnoreCase; + + foreach (var cookie in Sorted) { + if (cookie.Name.Equals (name, caseInsensitive)) + return cookie; + } + + return null; + } + } + + /// + /// Gets an object used to synchronize access to the collection. + /// + /// + /// An used to synchronize access to the collection. + /// + public object SyncRoot { + get { + return _sync; + } + } + + #endregion + + #region Private Methods + + private void add (Cookie cookie) + { + var idx = search (cookie); + if (idx == -1) { + _list.Add (cookie); + return; + } + + _list[idx] = cookie; + } + + private static int compareForSort (Cookie x, Cookie y) + { + return (x.Name.Length + x.Value.Length) + - (y.Name.Length + y.Value.Length); + } + + private static int compareForSorted (Cookie x, Cookie y) + { + var ret = x.Version - y.Version; + return ret != 0 + ? ret + : (ret = x.Name.CompareTo (y.Name)) != 0 + ? ret + : y.Path.Length - x.Path.Length; + } + + private static CookieCollection parseRequest (string value) + { + var ret = new CookieCollection (); + + Cookie cookie = null; + var ver = 0; + + 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; + + var idx = pair.IndexOf ('='); + if (idx == -1) { + if (cookie == null) + continue; + + if (pair.Equals ("$port", caseInsensitive)) { + cookie.Port = "\"\""; + continue; + } + + continue; + } + + if (idx == 0) { + if (cookie != null) { + ret.add (cookie); + cookie = null; + } + + continue; + } + + 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; + + int num; + if (!Int32.TryParse (val.Unquote (), out num)) + continue; + + ver = num; + continue; + } + + if (name.Equals ("$path", caseInsensitive)) { + if (cookie == null) + continue; + + if (val.Length == 0) + continue; + + cookie.Path = val; + continue; + } + + if (name.Equals ("$domain", caseInsensitive)) { + if (cookie == null) + continue; + + if (val.Length == 0) + continue; + + 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) + ret.add (cookie); + + return ret; + } + + private static CookieCollection parseResponse (string value) + { + var ret = new CookieCollection (); + + Cookie cookie = null; + + 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; + + var idx = pair.IndexOf ('='); + 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; + } + + if (idx == 0) { + if (cookie != null) { + ret.add (cookie); + cookie = null; + } + + continue; + } + + 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; + + int num; + if (!Int32.TryParse (val.Unquote (), out num)) + continue; + + cookie.Version = num; + continue; + } + + 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 ()); + + 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 + ) + ) + continue; + + cookie.Expires = expires.ToLocalTime (); + continue; + } + + if (name.Equals ("max-age", caseInsensitive)) { + if (cookie == null) + continue; + + if (val.Length == 0) + continue; + + int num; + if (!Int32.TryParse (val.Unquote (), out num)) + continue; + + cookie.MaxAge = num; + continue; + } + + if (name.Equals ("path", caseInsensitive)) { + if (cookie == null) + continue; + + if (val.Length == 0) + continue; + + cookie.Path = val; + continue; + } + + if (name.Equals ("domain", caseInsensitive)) { + if (cookie == null) + continue; + + if (val.Length == 0) + continue; + + cookie.Domain = val; + continue; + } + + if (name.Equals ("port", caseInsensitive)) { + if (cookie == null) + continue; + + if (val.Length == 0) + continue; + + cookie.Port = val; + continue; + } + + if (name.Equals ("comment", caseInsensitive)) { + if (cookie == null) + continue; + + if (val.Length == 0) + continue; + + cookie.Comment = urlDecode (val, Encoding.UTF8); + continue; + } + + if (name.Equals ("commenturl", caseInsensitive)) { + if (cookie == null) + continue; + + if (val.Length == 0) + continue; + + cookie.CommentUri = val.Unquote ().ToUri (); + continue; + } + + if (name.Equals ("samesite", caseInsensitive)) { + if (cookie == null) + continue; + + if (val.Length == 0) + continue; + + cookie.SameSite = val.Unquote (); + continue; + } + + if (cookie != null) + ret.add (cookie); + + Cookie.TryCreate (name, val, out cookie); + } + + if (cookie != null) + ret.add (cookie); + + return ret; + } + + private int search (Cookie cookie) + { + for (var i = _list.Count - 1; i >= 0; i--) { + if (_list[i].EqualsWithoutValue (cookie)) + return i; + } + + return -1; + } + + private static string urlDecode (string s, Encoding encoding) + { + if (s.IndexOfAny (new[] { '%', '+' }) == -1) + return s; + + try { + return HttpUtility.UrlDecode (s, encoding); + } + catch { + return null; + } + } + + #endregion + + #region Internal Methods + + internal static CookieCollection Parse (string value, bool response) + { + 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 idx = search (cookie); + if (idx == -1) { + if (cookie.Expired) + return; + + _list.Add (cookie); + return; + } + + if (cookie.Expired) { + _list.RemoveAt (idx); + return; + } + + _list[idx] = cookie; + } + + internal void SetOrRemove (CookieCollection cookies) + { + foreach (var cookie in cookies._list) + SetOrRemove (cookie); + } + + internal void Sort () + { + if (_list.Count > 1) + _list.Sort (compareForSort); + } + + #endregion + + #region Public Methods + + /// + /// Adds the specified cookie to the collection. + /// + /// + /// A to add. + /// + /// + /// The collection is read-only. + /// + /// + /// is . + /// + public void Add (Cookie cookie) + { + if (_readOnly) { + var msg = "The collection is read-only."; + throw new InvalidOperationException (msg); + } + + if (cookie == null) + throw new ArgumentNullException ("cookie"); + + add (cookie); + } + + /// + /// Adds the specified cookies to the collection. + /// + /// + /// A that contains the cookies to add. + /// + /// + /// The collection is read-only. + /// + /// + /// is . + /// + 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 (var cookie in cookies._list) + add (cookie); + } + + /// + /// Removes all cookies from the collection. + /// + /// + /// 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 array of that specifies the destination of + /// the elements copied from the collection. + /// + /// + /// An that specifies the zero-based index in + /// the array at which copying starts. + /// + /// + /// is . + /// + /// + /// is less than zero. + /// + /// + /// The space from to the end of + /// is not enough to copy to. + /// + 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 (array.Length - index < _list.Count) { + var msg = "The available space of the array is not enough to copy to."; + throw new ArgumentException (msg); + } + + _list.CopyTo (array, index); + } + + /// + /// Gets the enumerator that iterates through the collection. + /// + /// + /// 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 removed; otherwise, + /// false. + /// + /// + /// false if the cookie is not found in the collection. + /// + /// + /// + /// A to remove. + /// + /// + /// The collection is read-only. + /// + /// + /// is . + /// + public bool Remove (Cookie cookie) + { + if (_readOnly) { + var msg = "The collection is read-only."; + throw new InvalidOperationException (msg); + } + + if (cookie == null) + throw new ArgumentNullException ("cookie"); + + var idx = search (cookie); + if (idx == -1) + return false; + + _list.RemoveAt (idx); + return true; + } + + #endregion + + #region Explicit Interface Implementations + + /// + /// Gets the enumerator that iterates through the collection. + /// + /// + /// An instance that can be used to iterate + /// through the collection. + /// + IEnumerator IEnumerable.GetEnumerator () + { + return _list.GetEnumerator (); + } + + #endregion + } +} diff --git a/websocket-sharp-core/Net/CookieException.cs b/websocket-sharp-core/Net/CookieException.cs new file mode 100644 index 000000000..2a5abe98a --- /dev/null +++ b/websocket-sharp-core/Net/CookieException.cs @@ -0,0 +1,165 @@ +#region License +/* + * CookieException.cs + * + * This code is derived from CookieException.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2012-2019 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: + * - Lawrence Pit + */ +#endregion + +using System; +using System.Runtime.Serialization; +using System.Security.Permissions; + +namespace WebSocketSharp.Net +{ + /// + /// The exception that is thrown when a gets an error. + /// + [Serializable] + public class CookieException : FormatException, ISerializable + { + #region Internal Constructors + + internal CookieException (string message) + : base (message) + { + } + + internal CookieException (string message, Exception innerException) + : base (message, innerException) + { + } + + #endregion + + #region Protected Constructors + + /// + /// Initializes a new instance of the class + /// with the serialized data. + /// + /// + /// A that holds the serialized object data. + /// + /// + /// A that specifies the source for + /// the deserialization. + /// + /// + /// is . + /// + protected CookieException ( + SerializationInfo serializationInfo, StreamingContext streamingContext + ) + : base (serializationInfo, streamingContext) + { + } + + #endregion + + #region Public Constructors + + /// + /// Initializes a new instance of the class. + /// + public CookieException () + : base () + { + } + + #endregion + + #region Public Methods + + /// + /// 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. + /// + /// + /// is . + /// + [ + SecurityPermission ( + SecurityAction.LinkDemand, + Flags = SecurityPermissionFlag.SerializationFormatter + ) + ] + public override void GetObjectData ( + SerializationInfo serializationInfo, StreamingContext streamingContext + ) + { + base.GetObjectData (serializationInfo, streamingContext); + } + + #endregion + + #region Explicit Interface Implementation + + /// + /// 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. + /// + /// + /// is . + /// + [ + SecurityPermission ( + SecurityAction.LinkDemand, + Flags = SecurityPermissionFlag.SerializationFormatter, + SerializationFormatter = true + ) + ] + void ISerializable.GetObjectData ( + SerializationInfo serializationInfo, StreamingContext streamingContext + ) + { + base.GetObjectData (serializationInfo, streamingContext); + } + + #endregion + } +} diff --git a/websocket-sharp-core/Net/EndPointListener.cs b/websocket-sharp-core/Net/EndPointListener.cs new file mode 100644 index 000000000..67fa26393 --- /dev/null +++ b/websocket-sharp-core/Net/EndPointListener.cs @@ -0,0 +1,515 @@ +#region License +/* + * EndPointListener.cs + * + * This code is derived from EndPointListener.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-2016 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Gonzalo Paniagua Javier + */ +#endregion + +#region Contributors +/* + * Contributors: + * - Liryna + * - Nicholas Devenish + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Threading; + +namespace WebSocketSharp.Net +{ + 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; + + #endregion + + #region Static Constructor + + static EndPointListener () + { + _defaultCertFolderPath = + Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData); + } + + #endregion + + #region Internal Constructors + + internal EndPointListener ( + IPEndPoint endpoint, + bool secure, + string certificateFolderPath, + ServerSslConfiguration sslConfig, + bool reuseAddress + ) + { + if (secure) { + var cert = + getCertificate (endpoint.Port, certificateFolderPath, sslConfig.ServerCertificate); + + if (cert == null) + throw new ArgumentException ("No server certificate could be found."); + + _secure = true; + _sslConfig = new ServerSslConfiguration (sslConfig); + _sslConfig.ServerCertificate = cert; + } + + _endpoint = endpoint; + _prefixes = new Dictionary (); + _unregistered = new Dictionary (); + _unregisteredSync = ((ICollection) _unregistered).SyncRoot; + _socket = + new Socket (endpoint.Address.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + + if (reuseAddress) + _socket.SetSocketOption (SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + + _socket.Bind (endpoint); + _socket.Listen (500); + _socket.BeginAccept (onAccept, this); + } + + #endregion + + #region Public Properties + + public IPAddress Address { + get { + return _endpoint.Address; + } + } + + public bool IsSecure { + get { + return _secure; + } + } + + public int Port { + get { + return _endpoint.Port; + } + } + + public ServerSslConfiguration SslConfiguration { + get { + return _sslConfig; + } + } + + #endregion + + #region Private Methods + + private static void addSpecial (List prefixes, HttpListenerPrefix prefix) + { + var path = prefix.Path; + foreach (var pref in prefixes) { + if (pref.Path == path) + throw new HttpListenerException (87, "The prefix is already in use."); + } + + prefixes.Add (prefix); + } + + 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); + } + + var rsa = new RSACryptoServiceProvider (); + rsa.ImportCspBlob (pvk); + + return rsa; + } + + private static X509Certificate2 getCertificate ( + int port, string folderPath, X509Certificate2 defaultCertificate + ) + { + if (folderPath == null || folderPath.Length == 0) + folderPath = _defaultCertFolderPath; + + try { + var cer = Path.Combine (folderPath, String.Format ("{0}.cer", port)); + var key = Path.Combine (folderPath, String.Format ("{0}.key", port)); + if (File.Exists (cer) && File.Exists (key)) { + var cert = new X509Certificate2 (cer); + cert.PrivateKey = createRSAFromFile (key); + + return cert; + } + } + catch { + } + + return defaultCertificate; + } + + private void leaveIfNoPrefix () + { + if (_prefixes.Count > 0) + return; + + var prefs = _unhandled; + if (prefs != null && prefs.Count > 0) + return; + + prefs = _all; + if (prefs != null && prefs.Count > 0) + return; + + EndPointManager.RemoveEndPoint (_endpoint); + } + + private static void onAccept (IAsyncResult asyncResult) + { + var lsnr = (EndPointListener) asyncResult.AsyncState; + + Socket sock = null; + try { + sock = lsnr._socket.EndAccept (asyncResult); + } + catch (SocketException) { + // TODO: Should log the error code when this class has a logging. + } + catch (ObjectDisposedException) { + return; + } + + try { + lsnr._socket.BeginAccept (onAccept, lsnr); + } + catch { + if (sock != null) + sock.Close (); + + return; + } + + if (sock == null) + return; + + processAccepted (sock, lsnr); + } + + 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; + } + + socket.Close (); + } + } + + 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; + } + } + + return false; + } + + private static HttpListener searchHttpListenerFromSpecial ( + string path, List prefixes + ) + { + if (prefixes == null) + return null; + + HttpListener bestMatch = null; + + var bestLen = -1; + foreach (var pref in prefixes) { + var prefPath = pref.Path; + + var len = prefPath.Length; + if (len < bestLen) + continue; + + if (path.StartsWith (prefPath)) { + bestLen = len; + bestMatch = pref.Listener; + } + } + + return bestMatch; + } + + #endregion + + #region Internal Methods + + internal static bool CertificateExists (int port, string folderPath) + { + if (folderPath == null || folderPath.Length == 0) + folderPath = _defaultCertFolderPath; + + 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); + } + + 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); + var pathSlash = path[path.Length - 1] != '/' ? path + "/" : path; + + if (host != null && host.Length > 0) { + var bestLen = -1; + foreach (var pref in _prefixes.Keys) { + if (dns) { + var prefHost = pref.Host; + if (Uri.CheckHostName (prefHost) == UriHostNameType.Dns && prefHost != host) + continue; + } + + if (pref.Port != port) + continue; + + var prefPath = pref.Path; + + var len = prefPath.Length; + if (len < bestLen) + continue; + + if (path.StartsWith (prefPath) || pathSlash.StartsWith (prefPath)) { + bestLen = len; + listener = _prefixes[pref]; + } + } + + if (bestLen != -1) + return true; + } + + var prefs = _unhandled; + listener = searchHttpListenerFromSpecial (path, prefs); + if (listener == null && pathSlash != path) + listener = searchHttpListenerFromSpecial (pathSlash, prefs); + + if (listener != null) + return true; + + prefs = _all; + listener = searchHttpListenerFromSpecial (path, prefs); + if (listener == null && pathSlash != path) + listener = searchHttpListenerFromSpecial (pathSlash, prefs); + + return listener != null; + } + + #endregion + + #region Public Methods + + public void AddPrefix (HttpListenerPrefix prefix, HttpListener listener) + { + List current, future; + if (prefix.Host == "*") { + do { + current = _unhandled; + future = current != null + ? new List (current) + : new List (); + + prefix.Listener = listener; + addSpecial (future, prefix); + } + while (Interlocked.CompareExchange (ref _unhandled, future, current) != current); + + return; + } + + if (prefix.Host == "+") { + do { + current = _all; + future = current != null + ? new List (current) + : new List (); + + prefix.Listener = listener; + addSpecial (future, prefix); + } + 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 ( + 87, String.Format ("There's another listener for {0}.", prefix) + ); + } + + return; + } + + prefs2 = new Dictionary (prefs); + prefs2[prefix] = listener; + } + while (Interlocked.CompareExchange (ref _prefixes, prefs2, prefs) != prefs); + } + + public void Close () + { + _socket.Close (); + + HttpConnection[] conns = null; + lock (_unregisteredSync) { + if (_unregistered.Count == 0) + return; + + var keys = _unregistered.Keys; + conns = new HttpConnection[keys.Count]; + keys.CopyTo (conns, 0); + _unregistered.Clear (); + } + + for (var i = conns.Length - 1; i >= 0; i--) + conns[i].Close (true); + } + + public void RemovePrefix (HttpListenerPrefix prefix, HttpListener listener) + { + 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. + } + while (Interlocked.CompareExchange (ref _unhandled, future, current) != current); + + leaveIfNoPrefix (); + 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. + } + while (Interlocked.CompareExchange (ref _all, future, current) != current); + + leaveIfNoPrefix (); + return; + } + + Dictionary prefs, prefs2; + do { + prefs = _prefixes; + if (!prefs.ContainsKey (prefix)) + break; + + prefs2 = new Dictionary (prefs); + prefs2.Remove (prefix); + } + while (Interlocked.CompareExchange (ref _prefixes, prefs2, prefs) != prefs); + + leaveIfNoPrefix (); + } + + #endregion + } +} diff --git a/websocket-sharp-core/Net/EndPointManager.cs b/websocket-sharp-core/Net/EndPointManager.cs new file mode 100644 index 000000000..c12349d56 --- /dev/null +++ b/websocket-sharp-core/Net/EndPointManager.cs @@ -0,0 +1,240 @@ +#region License +/* + * EndPointManager.cs + * + * This code is derived from EndPointManager.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-2016 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Gonzalo Paniagua Javier + */ +#endregion + +#region Contributors +/* + * Contributors: + * - Liryna + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Net; + +namespace WebSocketSharp.Net +{ + internal sealed class EndPointManager + { + #region Private Fields + + private static readonly Dictionary _endpoints; + + #endregion + + #region Static Constructor + + static EndPointManager () + { + _endpoints = new Dictionary (); + } + + #endregion + + #region Private Constructors + + private EndPointManager () + { + } + + #endregion + + #region Private Methods + + private static void addPrefix (string uriPrefix, HttpListener listener) + { + var pref = new HttpListenerPrefix (uriPrefix); + + var addr = convertToIPAddress (pref.Host); + if (addr == null) + throw new HttpListenerException (87, "Includes an invalid host."); + + if (!addr.IsLocal ()) + throw new HttpListenerException (87, "Includes an invalid host."); + + int port; + if (!Int32.TryParse (pref.Port, out port)) + throw new HttpListenerException (87, "Includes an invalid port."); + + if (!port.IsPortNumber ()) + throw new HttpListenerException (87, "Includes an invalid port."); + + var path = pref.Path; + if (path.IndexOf ('%') != -1) + throw new HttpListenerException (87, "Includes an invalid path."); + + if (path.IndexOf ("//", StringComparison.Ordinal) != -1) + throw new HttpListenerException (87, "Includes an invalid path."); + + var endpoint = new IPEndPoint (addr, port); + + EndPointListener lsnr; + if (_endpoints.TryGetValue (endpoint, out lsnr)) { + if (lsnr.IsSecure ^ pref.IsSecure) + throw new HttpListenerException (87, "Includes an invalid scheme."); + } + else { + lsnr = + new EndPointListener ( + endpoint, + pref.IsSecure, + listener.CertificateFolderPath, + listener.SslConfiguration, + listener.ReuseAddress + ); + + _endpoints.Add (endpoint, lsnr); + } + + lsnr.AddPrefix (pref, listener); + } + + 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 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; + + var endpoint = new IPEndPoint (addr, port); + + EndPointListener lsnr; + if (!_endpoints.TryGetValue (endpoint, out lsnr)) + return; + + if (lsnr.IsSecure ^ pref.IsSecure) + return; + + lsnr.RemovePrefix (pref, listener); + } + + #endregion + + #region Internal Methods + + internal static bool RemoveEndPoint (IPEndPoint endpoint) + { + lock (((ICollection) _endpoints).SyncRoot) { + EndPointListener lsnr; + if (!_endpoints.TryGetValue (endpoint, out lsnr)) + return false; + + _endpoints.Remove (endpoint); + lsnr.Close (); + + return true; + } + } + + #endregion + + #region Public Methods + + public static void AddListener (HttpListener listener) + { + var added = new List (); + lock (((ICollection) _endpoints).SyncRoot) { + try { + foreach (var pref in listener.Prefixes) { + addPrefix (pref, listener); + added.Add (pref); + } + } + catch { + foreach (var pref in added) + removePrefix (pref, listener); + + throw; + } + } + } + + public static void AddPrefix (string uriPrefix, HttpListener listener) + { + lock (((ICollection) _endpoints).SyncRoot) + addPrefix (uriPrefix, listener); + } + + public static void RemoveListener (HttpListener listener) + { + lock (((ICollection) _endpoints).SyncRoot) { + foreach (var pref in listener.Prefixes) + removePrefix (pref, listener); + } + } + + public static void RemovePrefix (string uriPrefix, HttpListener listener) + { + lock (((ICollection) _endpoints).SyncRoot) + removePrefix (uriPrefix, listener); + } + + #endregion + } +} diff --git a/websocket-sharp-core/Net/HttpBasicIdentity.cs b/websocket-sharp-core/Net/HttpBasicIdentity.cs new file mode 100644 index 000000000..d26b29f69 --- /dev/null +++ b/websocket-sharp-core/Net/HttpBasicIdentity.cs @@ -0,0 +1,82 @@ +#region License +/* + * HttpBasicIdentity.cs + * + * 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-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 + * 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: + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; +using System.Security.Principal; + +namespace WebSocketSharp.Net +{ + /// + /// Holds the username and password from an HTTP Basic authentication attempt. + /// + public class HttpBasicIdentity : GenericIdentity + { + #region Private Fields + + private string _password; + + #endregion + + #region Internal Constructors + + internal HttpBasicIdentity (string username, string password) + : base (username, "Basic") + { + _password = password; + } + + #endregion + + #region Public Properties + + /// + /// Gets the password from a basic authentication attempt. + /// + /// + /// A that represents the password. + /// + public virtual string Password { + get { + return _password; + } + } + + #endregion + } +} diff --git a/websocket-sharp-core/Net/HttpConnection.cs b/websocket-sharp-core/Net/HttpConnection.cs new file mode 100644 index 000000000..572d785c2 --- /dev/null +++ b/websocket-sharp-core/Net/HttpConnection.cs @@ -0,0 +1,597 @@ +#region License +/* + * HttpConnection.cs + * + * This code is derived from HttpConnection.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-2016 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Gonzalo Paniagua Javier + */ +#endregion + +#region Contributors +/* + * Contributors: + * - Liryna + * - Rohan Singh + */ +#endregion + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Security; +using System.Net.Sockets; +using System.Text; +using System.Threading; + +namespace WebSocketSharp.Net +{ + internal sealed class HttpConnection + { + #region Private Fields + + private byte[] _buffer; + private const int _bufferLength = 8192; + private HttpListenerContext _context; + private bool _contextRegistered; + private StringBuilder _currentLine; + private InputState _inputState; + private RequestStream _inputStream; + private HttpListener _lastListener; + private LineState _lineState; + private EndPointListener _listener; + private EndPoint _localEndPoint; + private ResponseStream _outputStream; + private int _position; + private EndPoint _remoteEndPoint; + private MemoryStream _requestBuffer; + private int _reuses; + private bool _secure; + private Socket _socket; + private Stream _stream; + private object _sync; + private int _timeout; + private Dictionary _timeoutCanceled; + private Timer _timer; + + #endregion + + #region Internal Constructors + + internal HttpConnection (Socket socket, EndPointListener listener) + { + _socket = socket; + _listener = listener; + + var netStream = new NetworkStream (socket, false); + if (listener.IsSecure) { + var sslConf = listener.SslConfiguration; + var sslStream = new SslStream ( + netStream, + false, + sslConf.ClientCertificateValidationCallback + ); + + sslStream.AuthenticateAsServer ( + sslConf.ServerCertificate, + sslConf.ClientCertificateRequired, + sslConf.EnabledSslProtocols, + sslConf.CheckCertificateRevocation + ); + + _secure = true; + _stream = sslStream; + } + else { + _stream = netStream; + } + + _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 (); + } + + #endregion + + #region Public Properties + + public bool IsClosed { + get { + return _socket == null; + } + } + + public bool IsLocal { + get { + return ((IPEndPoint) _remoteEndPoint).Address.IsLocal (); + } + } + + public bool IsSecure { + get { + return _secure; + } + } + + public IPEndPoint LocalEndPoint { + get { + return (IPEndPoint) _localEndPoint; + } + } + + public IPEndPoint RemoteEndPoint { + get { + return (IPEndPoint) _remoteEndPoint; + } + } + + public int Reuses { + get { + return _reuses; + } + } + + public Stream Stream { + get { + return _stream; + } + } + + #endregion + + #region Private Methods + + private void close () + { + lock (_sync) { + if (_socket == null) + return; + + disposeTimer (); + disposeRequestBuffer (); + disposeStream (); + closeSocket (); + } + + unregisterContext (); + removeConnection (); + } + + private void closeSocket () + { + try { + _socket.Shutdown (SocketShutdown.Both); + } + catch { + } + + _socket.Close (); + _socket = null; + } + + private void disposeRequestBuffer () + { + if (_requestBuffer == null) + return; + + _requestBuffer.Dispose (); + _requestBuffer = null; + } + + private void disposeStream () + { + if (_stream == null) + return; + + _inputStream = null; + _outputStream = null; + + _stream.Dispose (); + _stream = null; + } + + private void disposeTimer () + { + if (_timer == null) + return; + + try { + _timer.Change (Timeout.Infinite, Timeout.Infinite); + } + catch { + } + + _timer.Dispose (); + _timer = null; + } + + private void init () + { + _context = new HttpListenerContext (this); + _inputState = InputState.RequestLine; + _inputStream = null; + _lineState = LineState.None; + _outputStream = null; + _position = 0; + _requestBuffer = new MemoryStream (); + } + + private static void onRead (IAsyncResult asyncResult) + { + var conn = (HttpConnection) asyncResult.AsyncState; + if (conn._socket == null) + return; + + lock (conn._sync) { + if (conn._socket == null) + return; + + var nread = -1; + var len = 0; + try { + var current = conn._reuses; + if (!conn._timeoutCanceled[current]) { + conn._timer.Change (Timeout.Infinite, Timeout.Infinite); + conn._timeoutCanceled[current] = true; + } + + 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; + } + + 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; + } + + HttpListener lsnr; + if (!conn._listener.TrySearchHttpListener (conn._context.Request.Url, out lsnr)) { + conn.SendError (null, 404); + return; + } + + if (conn._lastListener != lsnr) { + conn.removeConnection (); + if (!lsnr.AddConnection (conn)) { + conn.close (); + return; + } + + conn._lastListener = lsnr; + } + + conn._context.Listener = lsnr; + if (!conn._context.Authenticate ()) + return; + + if (conn._context.Register ()) + conn._contextRegistered = true; + + return; + } + + conn._stream.BeginRead (conn._buffer, 0, _bufferLength, onRead, conn); + } + } + + private static void onTimeout (object state) + { + var conn = (HttpConnection) state; + var current = conn._reuses; + if (conn._socket == null) + return; + + lock (conn._sync) { + if (conn._socket == null) + return; + + if (conn._timeoutCanceled[current]) + return; + + conn.SendError (null, 408); + } + } + + // true -> Done processing. + // false -> Need more input. + private bool processInput (byte[] data, int length) + { + if (_currentLine == null) + _currentLine = new StringBuilder (64); + + var nread = 0; + try { + string line; + while ((line = readLineFrom (data, _position, length, out nread)) != null) { + _position += nread; + if (line.Length == 0) { + if (_inputState == InputState.RequestLine) + continue; + + if (_position > 32768) + _context.ErrorMessage = "Headers too long"; + + _currentLine = null; + return true; + } + + if (_inputState == InputState.RequestLine) { + _context.Request.SetRequestLine (line); + _inputState = InputState.Headers; + } + else { + _context.Request.AddHeader (line); + } + + if (_context.HasError) + return true; + } + } + catch (Exception ex) { + _context.ErrorMessage = ex.Message; + return true; + } + + _position += nread; + if (_position >= 32768) { + _context.ErrorMessage = "Headers too long"; + return true; + } + + return false; + } + + private string readLineFrom (byte[] buffer, int offset, int length, out int read) + { + 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); + } + + if (_lineState != LineState.Lf) + return null; + + var line = _currentLine.ToString (); + + _currentLine.Length = 0; + _lineState = LineState.None; + + return line; + } + + private void removeConnection () + { + if (_lastListener != null) + _lastListener.RemoveConnection (this); + else + _listener.RemoveConnection (this); + } + + private void unregisterContext () + { + if (!_contextRegistered) + return; + + _context.Unregister (); + _contextRegistered = false; + } + + #endregion + + #region Internal Methods + + internal void Close (bool force) + { + if (_socket == null) + return; + + lock (_sync) { + if (_socket == null) + return; + + if (force) { + if (_outputStream != null) + _outputStream.Close (true); + + close (); + return; + } + + GetResponseStream ().Close (false); + + if (_context.Response.CloseConnection) { + close (); + return; + } + + if (!_context.Request.FlushInput ()) { + close (); + return; + } + + disposeRequestBuffer (); + unregisterContext (); + init (); + + _reuses++; + BeginReadRequest (); + } + } + + #endregion + + #region Public Methods + + public void BeginReadRequest () + { + if (_buffer == null) + _buffer = new byte[_bufferLength]; + + if (_reuses == 1) + _timeout = 15000; + + try { + _timeoutCanceled.Add (_reuses, false); + _timer.Change (_timeout, Timeout.Infinite); + _stream.BeginRead (_buffer, 0, _bufferLength, onRead, this); + } + catch { + close (); + } + } + + public void Close () + { + Close (false); + } + + public RequestStream GetRequestStream (long contentLength, bool chunked) + { + 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; + disposeRequestBuffer (); + + _inputStream = chunked + ? new ChunkedRequestStream ( + _stream, buff, _position, cnt, _context + ) + : new RequestStream ( + _stream, buff, _position, cnt, contentLength + ); + + return _inputStream; + } + } + + public ResponseStream GetResponseStream () + { + // TODO: Can we get this stream before reading the input? + + 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-core/Net/HttpDigestIdentity.cs b/websocket-sharp-core/Net/HttpDigestIdentity.cs new file mode 100644 index 000000000..68ec86d9f --- /dev/null +++ b/websocket-sharp-core/Net/HttpDigestIdentity.cs @@ -0,0 +1,187 @@ +#region License +/* + * HttpDigestIdentity.cs + * + * The MIT License + * + * 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 + * 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.Security.Principal; + +namespace WebSocketSharp.Net +{ + /// + /// Holds the username and other parameters from + /// an HTTP Digest authentication attempt. + /// + public class HttpDigestIdentity : GenericIdentity + { + #region Private Fields + + private NameValueCollection _parameters; + + #endregion + + #region Internal Constructors + + internal HttpDigestIdentity (NameValueCollection parameters) + : base (parameters["username"], "Digest") + { + _parameters = parameters; + } + + #endregion + + #region Public Properties + + /// + /// Gets the algorithm parameter from a digest authentication attempt. + /// + /// + /// A that represents the algorithm parameter. + /// + public string Algorithm { + get { + return _parameters["algorithm"]; + } + } + + /// + /// Gets the cnonce parameter from a digest authentication attempt. + /// + /// + /// A that represents the cnonce parameter. + /// + public string Cnonce { + get { + return _parameters["cnonce"]; + } + } + + /// + /// Gets the nc parameter from a digest authentication attempt. + /// + /// + /// A that represents the nc parameter. + /// + public string Nc { + get { + return _parameters["nc"]; + } + } + + /// + /// Gets the nonce parameter from a digest authentication attempt. + /// + /// + /// A that represents the nonce parameter. + /// + public string Nonce { + get { + return _parameters["nonce"]; + } + } + + /// + /// Gets the opaque parameter from a digest authentication attempt. + /// + /// + /// A that represents the opaque parameter. + /// + public string Opaque { + get { + return _parameters["opaque"]; + } + } + + /// + /// Gets the qop parameter from a digest authentication attempt. + /// + /// + /// A that represents the qop parameter. + /// + public string Qop { + get { + return _parameters["qop"]; + } + } + + /// + /// Gets the realm parameter from a digest authentication attempt. + /// + /// + /// A that represents the realm parameter. + /// + public string Realm { + get { + return _parameters["realm"]; + } + } + + /// + /// Gets the response parameter from a digest authentication attempt. + /// + /// + /// A that represents the response parameter. + /// + public string Response { + get { + return _parameters["response"]; + } + } + + /// + /// Gets the uri parameter from a digest authentication attempt. + /// + /// + /// A that represents the uri parameter. + /// + public string Uri { + get { + return _parameters["uri"]; + } + } + + #endregion + + #region Internal Methods + + internal bool IsValid ( + string password, string realm, string method, string entity + ) + { + var copied = new NameValueCollection (_parameters); + copied["password"] = password; + copied["realm"] = realm; + copied["method"] = method; + copied["entity"] = entity; + + var expected = AuthenticationResponse.CreateRequestDigest (copied); + return _parameters["response"] == expected; + } + + #endregion + } +} diff --git a/websocket-sharp-core/Net/HttpHeaderInfo.cs b/websocket-sharp-core/Net/HttpHeaderInfo.cs new file mode 100644 index 000000000..717f8f46d --- /dev/null +++ b/websocket-sharp-core/Net/HttpHeaderInfo.cs @@ -0,0 +1,114 @@ +#region License +/* + * HttpHeaderInfo.cs + * + * The MIT License + * + * Copyright (c) 2013-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; + +namespace WebSocketSharp.Net +{ + internal class HttpHeaderInfo + { + #region Private Fields + + private string _name; + private HttpHeaderType _type; + + #endregion + + #region Internal Constructors + + internal HttpHeaderInfo (string name, HttpHeaderType type) + { + _name = name; + _type = type; + } + + #endregion + + #region Internal Properties + + internal bool IsMultiValueInRequest { + get { + return (_type & HttpHeaderType.MultiValueInRequest) == HttpHeaderType.MultiValueInRequest; + } + } + + internal bool IsMultiValueInResponse { + get { + return (_type & HttpHeaderType.MultiValueInResponse) == HttpHeaderType.MultiValueInResponse; + } + } + + #endregion + + #region Public Properties + + public bool IsRequest { + get { + return (_type & HttpHeaderType.Request) == HttpHeaderType.Request; + } + } + + public bool IsResponse { + get { + return (_type & HttpHeaderType.Response) == HttpHeaderType.Response; + } + } + + public string Name { + get { + return _name; + } + } + + public HttpHeaderType Type { + get { + return _type; + } + } + + #endregion + + #region Public Methods + + public bool IsMultiValue (bool response) + { + return (_type & HttpHeaderType.MultiValue) == HttpHeaderType.MultiValue + ? (response ? IsResponse : IsRequest) + : (response ? IsMultiValueInResponse : IsMultiValueInRequest); + } + + public bool IsRestricted (bool response) + { + return (_type & HttpHeaderType.Restricted) == HttpHeaderType.Restricted + ? (response ? IsResponse : IsRequest) + : false; + } + + #endregion + } +} diff --git a/websocket-sharp-core/Net/HttpHeaderType.cs b/websocket-sharp-core/Net/HttpHeaderType.cs new file mode 100644 index 000000000..113fb63b6 --- /dev/null +++ b/websocket-sharp-core/Net/HttpHeaderType.cs @@ -0,0 +1,44 @@ +#region License +/* + * HttpHeaderType.cs + * + * The MIT License + * + * Copyright (c) 2013-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; + +namespace WebSocketSharp.Net +{ + [Flags] + internal enum HttpHeaderType + { + Unspecified = 0, + Request = 1, + Response = 1 << 1, + Restricted = 1 << 2, + MultiValue = 1 << 3, + MultiValueInRequest = 1 << 4, + MultiValueInResponse = 1 << 5 + } +} diff --git a/websocket-sharp-core/Net/HttpListener.cs b/websocket-sharp-core/Net/HttpListener.cs new file mode 100644 index 000000000..07970e14d --- /dev/null +++ b/websocket-sharp-core/Net/HttpListener.cs @@ -0,0 +1,836 @@ +#region License +/* + * HttpListener.cs + * + * This code is derived from HttpListener.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-2016 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Gonzalo Paniagua Javier + */ +#endregion + +#region Contributors +/* + * Contributors: + * - Liryna + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Security.Principal; +using System.Threading; + +// TODO: Logging. +namespace WebSocketSharp.Net +{ + /// + /// Provides a simple, programmatically controlled HTTP listener. + /// + 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 static readonly string _defaultRealm; + private bool _disposed; + private bool _ignoreWriteExceptions; + private volatile bool _listening; + private Logger _logger; + private HttpListenerPrefixCollection _prefixes; + private string _realm; + private bool _reuseAddress; + private ServerSslConfiguration _sslConfig; + private Func _userCredFinder; + private List _waitQueue; + private object _waitQueueSync; + + #endregion + + #region Static Constructor + + static HttpListener () + { + _defaultRealm = "SECRET AREA"; + } + + #endregion + + #region Public Constructors + + /// + /// Initializes a new instance of the class. + /// + 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 (); + + _prefixes = new HttpListenerPrefixCollection (this); + + _waitQueue = new List (); + _waitQueueSync = ((ICollection) _waitQueue).SyncRoot; + } + + #endregion + + #region Internal Properties + + internal bool IsDisposed { + get { + return _disposed; + } + } + + internal bool ReuseAddress { + get { + return _reuseAddress; + } + + set { + _reuseAddress = value; + } + } + + #endregion + + #region Public Properties + + /// + /// 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 + /// . + /// + /// + /// This listener has been closed. + /// + public AuthenticationSchemes AuthenticationSchemes { + get { + CheckDisposed (); + return _authSchemes; + } + + set { + CheckDisposed (); + _authSchemes = value; + } + } + + /// + /// Gets or sets the delegate called to select 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. + /// + /// + /// A Func<, > + /// delegate that references the method used to select an authentication scheme. The default + /// value is . + /// + /// + /// This listener has been closed. + /// + public Func AuthenticationSchemeSelector { + get { + CheckDisposed (); + return _authSchemeSelector; + } + + set { + CheckDisposed (); + _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. + /// + /// + /// + /// 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). + /// + /// + /// If this property is or empty, the result of + /// System.Environment.GetFolderPath + /// () is used as the default path. + /// + /// + /// + /// 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 (); + return _certFolderPath; + } + + set { + CheckDisposed (); + _certFolderPath = value; + } + } + + /// + /// 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. + /// + /// + /// This listener has been closed. + /// + public bool IgnoreWriteExceptions { + get { + CheckDisposed (); + return _ignoreWriteExceptions; + } + + set { + CheckDisposed (); + _ignoreWriteExceptions = value; + } + } + + /// + /// Gets a value indicating whether the listener has been started. + /// + /// + /// true if the listener has been started; otherwise, false. + /// + public bool IsListening { + get { + return _listening; + } + } + + /// + /// Gets a value indicating whether the listener can be used with the current operating system. + /// + /// + /// true. + /// + public static bool IsSupported { + get { + return true; + } + } + + /// + /// 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. + /// + /// + /// A that provides the logging functions. + /// + public Logger Log { + get { + return _logger; + } + } + + /// + /// Gets the URI prefixes handled by the listener. + /// + /// + /// A that contains the URI prefixes. + /// + /// + /// This listener has been closed. + /// + public HttpListenerPrefixCollection Prefixes { + get { + CheckDisposed (); + return _prefixes; + } + } + + /// + /// Gets or sets the name of the realm associated with the listener. + /// + /// + /// If this property is or empty, "SECRET AREA" will be used as + /// the name of the realm. + /// + /// + /// A that represents the name of the realm. The default value is + /// . + /// + /// + /// This listener has been closed. + /// + public string Realm { + get { + CheckDisposed (); + return _realm; + } + + set { + CheckDisposed (); + _realm = value; + } + } + + /// + /// Gets or sets the SSL configuration used to authenticate the server and + /// optionally the client for secure connection. + /// + /// + /// A that represents the configuration used to + /// authenticate the server and optionally the client for secure connection. + /// + /// + /// This listener has been closed. + /// + public ServerSslConfiguration SslConfiguration { + get { + CheckDisposed (); + return _sslConfig ?? (_sslConfig = new ServerSslConfiguration ()); + } + + set { + CheckDisposed (); + _sslConfig = value; + } + } + + /// + /// Gets or sets a value indicating whether, when NTLM authentication is used, + /// the authentication information of first request is used to authenticate + /// additional requests on the same connection. + /// + /// + /// This property isn't currently supported and always throws + /// a . + /// + /// + /// true if the authentication information of first request is used; + /// otherwise, false. + /// + /// + /// Any use of this property. + /// + public bool UnsafeConnectionNtlmAuthentication { + get { + throw new NotSupportedException (); + } + + set { + throw new NotSupportedException (); + } + } + + /// + /// 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 + /// . + /// + /// + /// This listener has been closed. + /// + public Func UserCredentialsFinder { + get { + CheckDisposed (); + return _userCredFinder; + } + + set { + CheckDisposed (); + _userCredFinder = value; + } + } + + #endregion + + #region Private Methods + + private void cleanupConnections () + { + HttpConnection[] conns = null; + lock (_connectionsSync) { + if (_connections.Count == 0) + return; + + // Need to copy this since closing will call the RemoveConnection method. + var keys = _connections.Keys; + conns = new HttpConnection[keys.Count]; + keys.CopyTo (conns, 0); + _connections.Clear (); + } + + for (var i = conns.Length - 1; i >= 0; i--) + conns[i].Close (true); + } + + private void cleanupContextQueue (bool sendServiceUnavailable) + { + HttpListenerContext[] ctxs = null; + lock (_ctxQueueSync) { + if (_ctxQueue.Count == 0) + return; + + ctxs = _ctxQueue.ToArray (); + _ctxQueue.Clear (); + } + + if (!sendServiceUnavailable) + return; + + foreach (var ctx in ctxs) { + var res = ctx.Response; + res.StatusCode = (int) HttpStatusCode.ServiceUnavailable; + res.Close (); + } + } + + private void cleanupContextRegistry () + { + HttpListenerContext[] ctxs = null; + lock (_ctxRegistrySync) { + if (_ctxRegistry.Count == 0) + return; + + // Need to copy this since closing will call the UnregisterContext method. + var keys = _ctxRegistry.Keys; + 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); + } + + private void cleanupWaitQueue (Exception exception) + { + HttpListenerAsyncResult[] aress = null; + lock (_waitQueueSync) { + if (_waitQueue.Count == 0) + return; + + aress = _waitQueue.ToArray (); + _waitQueue.Clear (); + } + + foreach (var ares in aress) + ares.Complete (exception); + } + + private void close (bool force) + { + if (_listening) { + _listening = false; + EndPointManager.RemoveListener (this); + } + + lock (_ctxRegistrySync) + cleanupContextQueue (!force); + + cleanupContextRegistry (); + cleanupConnections (); + cleanupWaitQueue (new ObjectDisposedException (GetType ().ToString ())); + + _disposed = true; + } + + private HttpListenerAsyncResult getAsyncResultFromQueue () + { + if (_waitQueue.Count == 0) + return null; + + var ares = _waitQueue[0]; + _waitQueue.RemoveAt (0); + + return ares; + } + + private HttpListenerContext getContextFromQueue () + { + if (_ctxQueue.Count == 0) + return null; + + var ctx = _ctxQueue[0]; + _ctxQueue.RemoveAt (0); + + return ctx; + } + + #endregion + + #region Internal Methods + + internal bool AddConnection (HttpConnection connection) + { + if (!_listening) + return false; + + lock (_connectionsSync) { + if (!_listening) + return false; + + _connections[connection] = connection; + return true; + } + } + + internal HttpListenerAsyncResult BeginGetContext (HttpListenerAsyncResult asyncResult) + { + lock (_ctxRegistrySync) { + if (!_listening) + throw new HttpListenerException (995); + + var ctx = getContextFromQueue (); + if (ctx == null) + _waitQueue.Add (asyncResult); + else + asyncResult.Complete (ctx, true); + + return asyncResult; + } + } + + internal void CheckDisposed () + { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + } + + internal string GetRealm () + { + var realm = _realm; + return realm != null && realm.Length > 0 ? realm : _defaultRealm; + } + + internal Func GetUserCredentialsFinder () + { + return _userCredFinder; + } + + internal bool RegisterContext (HttpListenerContext context) + { + if (!_listening) + return false; + + lock (_ctxRegistrySync) { + if (!_listening) + return false; + + _ctxRegistry[context] = context; + + var ares = getAsyncResultFromQueue (); + if (ares == null) + _ctxQueue.Add (context); + else + ares.Complete (context); + + return true; + } + } + + internal void RemoveConnection (HttpConnection connection) + { + lock (_connectionsSync) + _connections.Remove (connection); + } + + internal AuthenticationSchemes SelectAuthenticationScheme (HttpListenerRequest request) + { + var selector = _authSchemeSelector; + if (selector == null) + return _authSchemes; + + try { + return selector (request); + } + catch { + return AuthenticationSchemes.None; + } + } + + internal void UnregisterContext (HttpListenerContext context) + { + lock (_ctxRegistrySync) + _ctxRegistry.Remove (context); + } + + #endregion + + #region Public Methods + + /// + /// Shuts down the listener immediately. + /// + public void Abort () + { + if (_disposed) + return; + + close (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. + /// + /// + /// An that represents the status of the asynchronous operation. + /// + /// + /// An delegate that references the method to invoke when + /// the asynchronous operation completes. + /// + /// + /// An that represents a user defined object to pass to + /// the delegate. + /// + /// + /// + /// This listener has no URI prefix on which listens. + /// + /// + /// -or- + /// + /// + /// This listener hasn't been started, or is currently stopped. + /// + /// + /// + /// This listener has been closed. + /// + public IAsyncResult BeginGetContext (AsyncCallback callback, Object state) + { + 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."); + + return BeginGetContext (new HttpListenerAsyncResult (callback, state)); + } + + /// + /// Shuts down the listener. + /// + public void Close () + { + if (_disposed) + return; + + close (false); + } + + /// + /// Ends an asynchronous operation to get an incoming request. + /// + /// + /// This method completes an asynchronous operation started by calling + /// the BeginGetContext method. + /// + /// + /// A that represents a request. + /// + /// + /// An obtained by calling the BeginGetContext method. + /// + /// + /// is . + /// + /// + /// wasn't obtained by calling the BeginGetContext method. + /// + /// + /// This method was already called for the specified . + /// + /// + /// This listener has been closed. + /// + public HttpListenerContext EndGetContext (IAsyncResult asyncResult) + { + CheckDisposed (); + 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."); + + ares.EndCalled = true; + if (!ares.IsCompleted) + ares.AsyncWaitHandle.WaitOne (); + + return ares.GetContext (); // This may throw an exception. + } + + /// + /// Gets an incoming request. + /// + /// + /// This method waits for an incoming request, and returns when a request is received. + /// + /// + /// A that represents a request. + /// + /// + /// + /// This listener has no URI prefix on which listens. + /// + /// + /// -or- + /// + /// + /// This listener hasn't been started, or is currently stopped. + /// + /// + /// + /// This listener has been closed. + /// + public HttpListenerContext GetContext () + { + 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."); + + var ares = BeginGetContext (new HttpListenerAsyncResult (null, null)); + ares.InGet = true; + + return EndGetContext (ares); + } + + /// + /// Starts receiving incoming requests. + /// + /// + /// This listener has been closed. + /// + public void Start () + { + CheckDisposed (); + if (_listening) + return; + + EndPointManager.AddListener (this); + _listening = true; + } + + /// + /// Stops receiving incoming requests. + /// + /// + /// This listener has been closed. + /// + public void Stop () + { + CheckDisposed (); + if (!_listening) + return; + + _listening = false; + EndPointManager.RemoveListener (this); + + lock (_ctxRegistrySync) + cleanupContextQueue (true); + + cleanupContextRegistry (); + cleanupConnections (); + cleanupWaitQueue (new HttpListenerException (995, "The listener is stopped.")); + } + + #endregion + + #region Explicit Interface Implementations + + /// + /// Releases all resources used by the listener. + /// + void IDisposable.Dispose () + { + if (_disposed) + return; + + close (true); + } + + #endregion + } +} diff --git a/websocket-sharp-core/Net/HttpListenerAsyncResult.cs b/websocket-sharp-core/Net/HttpListenerAsyncResult.cs new file mode 100644 index 000000000..a1c737421 --- /dev/null +++ b/websocket-sharp-core/Net/HttpListenerAsyncResult.cs @@ -0,0 +1,198 @@ +#region License +/* + * HttpListenerAsyncResult.cs + * + * This code is derived from ListenerAsyncResult.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Ximian, Inc. (http://www.ximian.com) + * 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 + * 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: + * - Gonzalo Paniagua Javier + */ +#endregion + +#region Contributors +/* + * Contributors: + * - Nicholas Devenish + */ +#endregion + +using System; +using System.Threading; + +namespace WebSocketSharp.Net +{ + internal class HttpListenerAsyncResult : IAsyncResult + { + #region Private Fields + + private AsyncCallback _callback; + private bool _completed; + private HttpListenerContext _context; + private bool _endCalled; + private Exception _exception; + private bool _inGet; + private object _state; + private object _sync; + private bool _syncCompleted; + private ManualResetEvent _waitHandle; + + #endregion + + #region Internal Constructors + + internal HttpListenerAsyncResult (AsyncCallback callback, object state) + { + _callback = callback; + _state = state; + _sync = new object (); + } + + #endregion + + #region Internal Properties + + internal bool EndCalled { + get { + return _endCalled; + } + + set { + _endCalled = value; + } + } + + internal bool InGet { + get { + return _inGet; + } + + set { + _inGet = value; + } + } + + #endregion + + #region Public Properties + + public object AsyncState { + get { + return _state; + } + } + + public WaitHandle AsyncWaitHandle { + get { + lock (_sync) + return _waitHandle ?? (_waitHandle = new ManualResetEvent (_completed)); + } + } + + public bool CompletedSynchronously { + get { + return _syncCompleted; + } + } + + public bool IsCompleted { + get { + lock (_sync) + return _completed; + } + } + + #endregion + + #region Private Methods + + private static void complete (HttpListenerAsyncResult asyncResult) + { + lock (asyncResult._sync) { + asyncResult._completed = true; + + var waitHandle = asyncResult._waitHandle; + if (waitHandle != null) + waitHandle.Set (); + } + + var callback = asyncResult._callback; + if (callback == null) + return; + + ThreadPool.QueueUserWorkItem ( + state => { + try { + callback (asyncResult); + } + catch { + } + }, + null + ); + } + + #endregion + + #region Internal Methods + + internal void Complete (Exception exception) + { + _exception = _inGet && (exception is ObjectDisposedException) + ? new HttpListenerException (995, "The listener is closed.") + : exception; + + complete (this); + } + + internal void Complete (HttpListenerContext context) + { + Complete (context, false); + } + + internal void Complete (HttpListenerContext context, bool syncCompleted) + { + _context = context; + _syncCompleted = syncCompleted; + + complete (this); + } + + internal HttpListenerContext GetContext () + { + if (_exception != null) + throw _exception; + + return _context; + } + + #endregion + } +} diff --git a/websocket-sharp-core/Net/HttpListenerContext.cs b/websocket-sharp-core/Net/HttpListenerContext.cs new file mode 100644 index 000000000..638078d4f --- /dev/null +++ b/websocket-sharp-core/Net/HttpListenerContext.cs @@ -0,0 +1,256 @@ +#region License +/* + * HttpListenerContext.cs + * + * This code 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) 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 + * 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: + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; +using System.Security.Principal; +using WebSocketSharp.Net.WebSockets; + +namespace WebSocketSharp.Net +{ + /// + /// Provides the access to the HTTP request and response objects used by + /// the . + /// + /// + /// 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 HttpListenerWebSocketContext _websocketContext; + + #endregion + + #region Internal Constructors + + internal HttpListenerContext (HttpConnection connection) + { + _connection = connection; + _errorStatus = 400; + _request = new HttpListenerRequest (this); + _response = new HttpListenerResponse (this); + } + + #endregion + + #region Internal Properties + + internal HttpConnection Connection { + get { + return _connection; + } + } + + internal string ErrorMessage { + get { + return _error; + } + + set { + _error = value; + } + } + + internal int ErrorStatus { + get { + return _errorStatus; + } + + set { + _errorStatus = value; + } + } + + internal bool HasError { + get { + return _error != null; + } + } + + internal HttpListener Listener { + get { + return _listener; + } + + set { + _listener = value; + } + } + + #endregion + + #region Public Properties + + /// + /// Gets the HTTP request object that represents a client request. + /// + /// + /// A that represents the client request. + /// + public HttpListenerRequest Request { + get { + return _request; + } + } + + /// + /// Gets the HTTP response object used to send a response to the client. + /// + /// + /// A that represents a response to the client request. + /// + public HttpListenerResponse Response { + get { + return _response; + } + } + + /// + /// Gets the client information (identity, authentication, and security roles). + /// + /// + /// A instance that represents the client information. + /// + public IPrincipal User { + get { + return _user; + } + } + + #endregion + + #region Internal Methods + + internal bool Authenticate () + { + var schm = _listener.SelectAuthenticationScheme (_request); + if (schm == AuthenticationSchemes.Anonymous) + return true; + + if (schm == AuthenticationSchemes.None) { + _response.Close (HttpStatusCode.Forbidden); + return false; + } + + var realm = _listener.GetRealm (); + var user = + HttpUtility.CreateUser ( + _request.Headers["Authorization"], + schm, + realm, + _request.HttpMethod, + _listener.GetUserCredentialsFinder () + ); + + if (user == null || !user.Identity.IsAuthenticated) { + _response.CloseWithAuthChallenge (new AuthenticationChallenge (schm, realm).ToString ()); + return false; + } + + _user = user; + return true; + } + + internal bool Register () + { + return _listener.RegisterContext (this); + } + + internal void Unregister () + { + _listener.UnregisterContext (this); + } + + #endregion + + #region Public Methods + + /// + /// Accepts a WebSocket handshake request. + /// + /// + /// A that represents + /// the WebSocket handshake request. + /// + /// + /// A that represents the subprotocol supported on + /// this WebSocket connection. + /// + /// + /// + /// is empty. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// + /// This method has already been called. + /// + public HttpListenerWebSocketContext AcceptWebSocket (string protocol) + { + if (_websocketContext != null) + throw new InvalidOperationException ("The accepting is already in progress."); + + if (protocol != null) { + if (protocol.Length == 0) + throw new ArgumentException ("An empty string.", "protocol"); + + if (!protocol.IsToken ()) + throw new ArgumentException ("Contains an invalid character.", "protocol"); + } + + _websocketContext = new HttpListenerWebSocketContext (this, protocol); + return _websocketContext; + } + + #endregion + } +} diff --git a/websocket-sharp-core/Net/HttpListenerException.cs b/websocket-sharp-core/Net/HttpListenerException.cs new file mode 100644 index 000000000..a52eeec03 --- /dev/null +++ b/websocket-sharp-core/Net/HttpListenerException.cs @@ -0,0 +1,127 @@ +#region License +/* + * HttpListenerException.cs + * + * This code is derived from System.Net.HttpListenerException.cs 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 + * + * 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: + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; +using System.ComponentModel; +using System.Runtime.Serialization; + +namespace WebSocketSharp.Net +{ + /// + /// The exception that is thrown when a gets an error + /// processing an HTTP request. + /// + [Serializable] + public class HttpListenerException : Win32Exception + { + #region Protected Constructors + + /// + /// Initializes a new instance of the class from + /// the specified and . + /// + /// + /// A that contains the serialized object data. + /// + /// + /// A that specifies the source for the deserialization. + /// + protected HttpListenerException ( + SerializationInfo serializationInfo, StreamingContext streamingContext) + : base (serializationInfo, streamingContext) + { + } + + #endregion + + #region Public Constructors + + /// + /// Initializes a new instance of the class. + /// + public HttpListenerException () + { + } + + /// + /// Initializes a new instance of the class + /// with the specified . + /// + /// + /// An that identifies the error. + /// + public HttpListenerException (int errorCode) + : base (errorCode) + { + } + + /// + /// Initializes a new instance of the class + /// with the specified and . + /// + /// + /// An that identifies the error. + /// + /// + /// A that describes the error. + /// + public HttpListenerException (int errorCode, string message) + : base (errorCode, message) + { + } + + #endregion + + #region Public Properties + + /// + /// Gets the error code that identifies the error that occurred. + /// + /// + /// An that identifies the error. + /// + public override int ErrorCode { + get { + return NativeErrorCode; + } + } + + #endregion + } +} diff --git a/websocket-sharp-core/Net/HttpListenerPrefix.cs b/websocket-sharp-core/Net/HttpListenerPrefix.cs new file mode 100644 index 000000000..960d02edf --- /dev/null +++ b/websocket-sharp-core/Net/HttpListenerPrefix.cs @@ -0,0 +1,228 @@ +#region License +/* + * HttpListenerPrefix.cs + * + * This code is derived from ListenerPrefix.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-2016 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Gonzalo Paniagua Javier + * - Oleg Mihailik + */ +#endregion + +using System; +using System.Net; + +namespace WebSocketSharp.Net +{ + internal sealed class HttpListenerPrefix + { + #region Private Fields + + private string _host; + private HttpListener _listener; + private string _original; + private string _path; + private string _port; + private string _prefix; + private bool _secure; + + #endregion + + #region Internal Constructors + + /// + /// Initializes a new instance of the class with + /// the specified . + /// + /// + /// This constructor must be called after calling the CheckPrefix method. + /// + /// + /// A that represents the URI prefix. + /// + internal HttpListenerPrefix (string uriPrefix) + { + _original = uriPrefix; + parse (uriPrefix); + } + + #endregion + + #region Public Properties + + public string Host { + get { + return _host; + } + } + + public bool IsSecure { + get { + return _secure; + } + } + + public HttpListener Listener { + get { + return _listener; + } + + set { + _listener = value; + } + } + + public string Original { + get { + return _original; + } + } + + public string Path { + get { + return _path; + } + } + + public string Port { + get { + return _port; + } + } + + #endregion + + #region Private Methods + + private void parse (string uriPrefix) + { + if (uriPrefix.StartsWith ("https")) + _secure = true; + + var len = uriPrefix.Length; + var startHost = uriPrefix.IndexOf (':') + 3; + var root = uriPrefix.IndexOf ('/', startHost + 1, len - startHost - 1); + + var colon = uriPrefix.LastIndexOf (':', root - 1, root - startHost - 1); + if (uriPrefix[root - 1] != ']' && colon > startHost) { + _host = uriPrefix.Substring (startHost, colon - startHost); + _port = uriPrefix.Substring (colon + 1, root - colon - 1); + } + else { + _host = uriPrefix.Substring (startHost, root - startHost); + _port = _secure ? "443" : "80"; + } + + _path = uriPrefix.Substring (root); + + _prefix = + String.Format ("http{0}://{1}:{2}{3}", _secure ? "s" : "", _host, _port, _path); + } + + #endregion + + #region Public Methods + + public static void CheckPrefix (string uriPrefix) + { + if (uriPrefix == null) + throw new ArgumentNullException ("uriPrefix"); + + var len = uriPrefix.Length; + if (len == 0) + throw new ArgumentException ("An empty string.", "uriPrefix"); + + if (!(uriPrefix.StartsWith ("http://") || uriPrefix.StartsWith ("https://"))) + throw new ArgumentException ("The scheme isn't 'http' or 'https'.", "uriPrefix"); + + var startHost = uriPrefix.IndexOf (':') + 3; + if (startHost >= len) + throw new ArgumentException ("No host is specified.", "uriPrefix"); + + if (uriPrefix[startHost] == ':') + throw new ArgumentException ("No host is specified.", "uriPrefix"); + + var root = uriPrefix.IndexOf ('/', startHost, len - startHost); + if (root == startHost) + throw new ArgumentException ("No host is specified.", "uriPrefix"); + + if (root == -1 || uriPrefix[len - 1] != '/') + throw new ArgumentException ("Ends without '/'.", "uriPrefix"); + + if (uriPrefix[root - 1] == ':') + throw new ArgumentException ("No port is specified.", "uriPrefix"); + + if (root == len - 2) + throw new ArgumentException ("No path is specified.", "uriPrefix"); + } + + /// + /// Determines whether this instance and the specified have the same value. + /// + /// + /// This method will be required to detect duplicates in any collection. + /// + /// + /// An to compare to this instance. + /// + /// + /// true if is a and + /// its value is the same as this instance; otherwise, false. + /// + public override bool Equals (Object obj) + { + var pref = obj as HttpListenerPrefix; + return pref != null && pref._prefix == _prefix; + } + + /// + /// Gets the hash code for this instance. + /// + /// + /// This method will be required to detect duplicates in any collection. + /// + /// + /// An that represents the hash code. + /// + public override int GetHashCode () + { + return _prefix.GetHashCode (); + } + + public override string ToString () + { + return _prefix; + } + + #endregion + } +} diff --git a/websocket-sharp-core/Net/HttpListenerPrefixCollection.cs b/websocket-sharp-core/Net/HttpListenerPrefixCollection.cs new file mode 100644 index 000000000..6373b8d65 --- /dev/null +++ b/websocket-sharp-core/Net/HttpListenerPrefixCollection.cs @@ -0,0 +1,278 @@ +#region License +/* + * HttpListenerPrefixCollection.cs + * + * This code is derived from HttpListenerPrefixCollection.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-2015 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: + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace WebSocketSharp.Net +{ + /// + /// Provides the collection used to store the URI prefixes for the . + /// + /// + /// The responds to the request which has a requested URI that + /// the prefixes most closely match. + /// + public class HttpListenerPrefixCollection : ICollection, IEnumerable, IEnumerable + { + #region Private Fields + + private HttpListener _listener; + private List _prefixes; + + #endregion + + #region Internal Constructors + + internal HttpListenerPrefixCollection (HttpListener listener) + { + _listener = listener; + _prefixes = new List (); + } + + #endregion + + #region Public Properties + + /// + /// Gets the number of prefixes in the collection. + /// + /// + /// An that represents the number of prefixes. + /// + public int Count { + get { + return _prefixes.Count; + } + } + + /// + /// Gets a value indicating whether the access to the collection is read-only. + /// + /// + /// Always returns false. + /// + public bool IsReadOnly { + get { + return false; + } + } + + /// + /// Gets a value indicating whether the access to the collection is synchronized. + /// + /// + /// Always returns false. + /// + public bool IsSynchronized { + get { + return false; + } + } + + #endregion + + #region Public Methods + + /// + /// Adds the specified 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 '/'. + /// + /// + /// is . + /// + /// + /// is invalid. + /// + /// + /// The 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); + } + + /// + /// Removes all URI prefixes from the collection. + /// + /// + /// The associated with this collection is closed. + /// + public void Clear () + { + _listener.CheckDisposed (); + _prefixes.Clear (); + if (_listener.IsListening) + EndPointManager.RemoveListener (_listener); + } + + /// + /// Returns a value indicating whether the collection contains the specified + /// . + /// + /// + /// true if the collection contains ; + /// otherwise, false. + /// + /// + /// A that represents the URI prefix to test. + /// + /// + /// is . + /// + /// + /// The associated with this collection is closed. + /// + public bool Contains (string uriPrefix) + { + _listener.CheckDisposed (); + if (uriPrefix == null) + throw new ArgumentNullException ("uriPrefix"); + + return _prefixes.Contains (uriPrefix); + } + + /// + /// Copies the contents of the collection to the specified . + /// + /// + /// An 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. + /// + 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. + /// + public void CopyTo (string[] array, int offset) + { + _listener.CheckDisposed (); + _prefixes.CopyTo (array, offset); + } + + /// + /// Gets the enumerator used to iterate through the . + /// + /// + /// An instance used to iterate + /// through the collection. + /// + public IEnumerator GetEnumerator () + { + return _prefixes.GetEnumerator (); + } + + /// + /// Removes the specified from the collection. + /// + /// + /// true if is successfully found and removed; + /// otherwise, false. + /// + /// + /// A that represents the URI prefix to remove. + /// + /// + /// is . + /// + /// + /// The 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) + EndPointManager.RemovePrefix (uriPrefix, _listener); + + return ret; + } + + #endregion + + #region Explicit Interface Implementations + + /// + /// Gets the enumerator used to iterate through the . + /// + /// + /// An instance used to iterate through the collection. + /// + IEnumerator IEnumerable.GetEnumerator () + { + return _prefixes.GetEnumerator (); + } + + #endregion + } +} diff --git a/websocket-sharp-core/Net/HttpListenerRequest.cs b/websocket-sharp-core/Net/HttpListenerRequest.cs new file mode 100644 index 000000000..953c9b956 --- /dev/null +++ b/websocket-sharp-core/Net/HttpListenerRequest.cs @@ -0,0 +1,910 @@ +#region License +/* + * HttpListenerRequest.cs + * + * This code is derived from HttpListenerRequest.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-2018 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: + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Globalization; +using System.IO; +using System.Security.Cryptography.X509Certificates; +using System.Text; + +namespace WebSocketSharp.Net +{ + /// + /// Represents an incoming request to a instance. + /// + /// + /// This class cannot be inherited. + /// + public sealed class HttpListenerRequest + { + #region Private Fields + + 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 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 + + #region Static Constructor + + static HttpListenerRequest () + { + _100continue = Encoding.ASCII.GetBytes ("HTTP/1.1 100 Continue\r\n\r\n"); + } + + #endregion + + #region Internal Constructors + + internal HttpListenerRequest (HttpListenerContext context) + { + _context = context; + + _connection = context.Connection; + _contentLength = -1; + _headers = new WebHeaderCollection (); + _requestTraceIdentifier = Guid.NewGuid (); + } + + #endregion + + #region Public Properties + + /// + /// Gets the media types that are acceptable for the client. + /// + /// + /// + /// 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 (',') + .Trim () + .ToList () + .ToArray (); + } + + return _acceptTypes; + } + } + + /// + /// Gets an error code that identifies a problem with the certificate + /// provided by the client. + /// + /// + /// An that represents an error code. + /// + /// + /// This property is not supported. + /// + public int ClientCertificateError { + get { + throw new NotSupportedException (); + } + } + + /// + /// Gets the encoding for the entity body data included in the request. + /// + /// + /// + /// A converted from the charset value of the + /// Content-Type header. + /// + /// + /// if the charset value is not available. + /// + /// + public Encoding ContentEncoding { + get { + if (_contentEncoding == null) + _contentEncoding = getContentEncoding () ?? Encoding.UTF8; + + return _contentEncoding; + } + } + + /// + /// Gets the length in bytes of the entity body data included in the + /// request. + /// + /// + /// + /// A converted from the value of the Content-Length + /// header. + /// + /// + /// -1 if the header is not present. + /// + /// + public long ContentLength64 { + get { + return _contentLength; + } + } + + /// + /// Gets the media type of the entity body data included in the request. + /// + /// + /// + /// A that represents the value of the Content-Type + /// header. + /// + /// + /// if the header is not present. + /// + /// + public string ContentType { + get { + return _headers["Content-Type"]; + } + } + + /// + /// Gets the cookies included in the request. + /// + /// + /// + /// A that contains the cookies. + /// + /// + /// An empty collection if not included. + /// + /// + public CookieCollection Cookies { + get { + if (_cookies == null) + _cookies = _headers.GetCookies (false); + + return _cookies; + } + } + + /// + /// Gets a value indicating whether the request has the entity body data. + /// + /// + /// true if the request has the entity body data; otherwise, + /// false. + /// + public bool HasEntityBody { + get { + return _contentLength > 0 || _chunked; + } + } + + /// + /// Gets the headers included in the request. + /// + /// + /// A that contains the headers. + /// + public NameValueCollection Headers { + get { + return _headers; + } + } + + /// + /// Gets the HTTP method specified by the client. + /// + /// + /// A that represents the HTTP method specified in + /// the request line. + /// + public string HttpMethod { + get { + return _httpMethod; + } + } + + /// + /// Gets a stream 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 { + if (_inputStream == null) + _inputStream = getInputStream () ?? Stream.Null; + + return _inputStream; + } + } + + /// + /// Gets a value indicating whether the client is authenticated. + /// + /// + /// true if the client is authenticated; otherwise, false. + /// + public bool IsAuthenticated { + get { + return _context.User != null; + } + } + + /// + /// Gets a value indicating whether the request is sent from the local + /// computer. + /// + /// + /// true if the request is sent from the same computer as the server; + /// otherwise, false. + /// + public bool IsLocal { + get { + return _connection.IsLocal; + } + } + + /// + /// Gets a value indicating whether a secure connection is used to send + /// the request. + /// + /// + /// true if the connection is secure; otherwise, false. + /// + public bool IsSecureConnection { + get { + return _connection.IsSecure; + } + } + + /// + /// Gets a value indicating whether the request is a WebSocket handshake + /// request. + /// + /// + /// true if the request is a WebSocket handshake request; otherwise, + /// false. + /// + public bool IsWebSocketRequest { + get { + return _httpMethod == "GET" + && _protocolVersion > HttpVersion.Version10 + && _headers.Upgrades ("websocket"); + } + } + + /// + /// Gets a value indicating whether a persistent connection is requested. + /// + /// + /// true if the request specifies that the connection is kept open; + /// otherwise, false. + /// + public bool KeepAlive { + get { + return _headers.KeepsAlive (_protocolVersion); + } + } + + /// + /// Gets the endpoint to which the request is sent. + /// + /// + /// A that represents the server IP + /// address and port number. + /// + public System.Net.IPEndPoint LocalEndPoint { + get { + return _connection.LocalEndPoint; + } + } + + /// + /// Gets the HTTP version specified by the client. + /// + /// + /// A that represents the HTTP version specified in + /// the request line. + /// + public Version ProtocolVersion { + get { + return _protocolVersion; + } + } + + /// + /// Gets the query string included in the request. + /// + /// + /// + /// A that contains the query + /// parameters. + /// + /// + /// An empty collection if not included. + /// + /// + public NameValueCollection QueryString { + get { + if (_queryString == null) { + var url = Url; + _queryString = QueryStringCollection.Parse ( + url != null ? url.Query : null, + Encoding.UTF8 + ); + } + + return _queryString; + } + } + + /// + /// Gets the raw URL specified by the client. + /// + /// + /// A that represents the request target specified in + /// the request line. + /// + public string RawUrl { + get { + return _rawUrl; + } + } + + /// + /// Gets the endpoint from which the request is sent. + /// + /// + /// A that represents the client IP + /// address and port number. + /// + public System.Net.IPEndPoint RemoteEndPoint { + get { + return _connection.RemoteEndPoint; + } + } + + /// + /// Gets the trace identifier of the request. + /// + /// + /// A that represents the trace identifier. + /// + public Guid RequestTraceIdentifier { + get { + return _requestTraceIdentifier; + } + } + + /// + /// Gets 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 ?? UserHostAddress, + IsWebSocketRequest, + IsSecureConnection + ); + + _urlSet = true; + } + + return _url; + } + } + + /// + /// Gets the URI of the resource from which the requested URL was obtained. + /// + /// + /// + /// A converted from the value of the Referer header. + /// + /// + /// if the header value is not available. + /// + /// + public Uri UrlReferrer { + get { + var val = _headers["Referer"]; + if (val == null) + return null; + + if (_urlReferrer == null) + _urlReferrer = val.ToUri (); + + return _urlReferrer; + } + } + + /// + /// Gets the user agent from which the request is originated. + /// + /// + /// + /// A that represents the value of the User-Agent + /// header. + /// + /// + /// if the header is not present. + /// + /// + public string UserAgent { + get { + return _headers["User-Agent"]; + } + } + + /// + /// Gets the IP address and port number to which the request is sent. + /// + /// + /// A that represents the server IP address and port + /// number. + /// + public string UserHostAddress { + get { + return _connection.LocalEndPoint.ToString (); + } + } + + /// + /// Gets the server host name requested by the client. + /// + /// + /// + /// A that represents the value of the Host header. + /// + /// + /// It includes the port number if provided. + /// + /// + /// if the header is not present. + /// + /// + public string UserHostName { + get { + return _userHostName; + } + } + + /// + /// Gets the natural languages that are acceptable for the client. + /// + /// + /// + /// 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 (',').Trim ().ToList ().ToArray (); + + return _userLanguages; + } + } + + #endregion + + #region Private Methods + + private void finishInitialization10 () + { + var transferEnc = _headers["Transfer-Encoding"]; + if (transferEnc != null) { + _context.ErrorMessage = "Invalid Transfer-Encoding header"; + return; + } + + if (_httpMethod == "POST") { + if (_contentLength == -1) { + _context.ErrorMessage = "Content-Length header required"; + return; + } + + if (_contentLength == 0) { + _context.ErrorMessage = "Invalid Content-Length header"; + return; + } + } + } + + private Encoding getContentEncoding () + { + var val = _headers["Content-Type"]; + if (val == null) + return null; + + Encoding ret; + HttpUtility.TryGetEncoding (val, out ret); + + return ret; + } + + private RequestStream getInputStream () + { + return _contentLength > 0 || _chunked + ? _connection.GetRequestStream (_contentLength, _chunked) + : null; + } + + #endregion + + #region Internal Methods + + internal void AddHeader (string headerField) + { + var start = headerField[0]; + if (start == ' ' || start == '\t') { + _context.ErrorMessage = "Invalid header field"; + return; + } + + var colon = headerField.IndexOf (':'); + if (colon < 1) { + _context.ErrorMessage = "Invalid header field"; + return; + } + + var name = headerField.Substring (0, colon).Trim (); + if (name.Length == 0 || !name.IsToken ()) { + _context.ErrorMessage = "Invalid header name"; + return; + } + + 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; + } + + if (val.Length == 0) { + _context.ErrorMessage = "Invalid Host header"; + return; + } + + _userHostName = val; + return; + } + + if (lower == "content-length") { + if (_contentLength > -1) { + _context.ErrorMessage = "Invalid Content-Length header"; + return; + } + + 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; + } + } + + internal void FinishInitialization () + { + if (_protocolVersion == HttpVersion.Version10) { + finishInitialization10 (); + return; + } + + if (_userHostName == null) { + _context.ErrorMessage = "Host header required"; + return; + } + + var transferEnc = _headers["Transfer-Encoding"]; + if (transferEnc != null) { + var comparison = StringComparison.OrdinalIgnoreCase; + if (!transferEnc.Equals ("chunked", comparison)) { + _context.ErrorMessage = String.Empty; + _context.ErrorStatus = 501; + + return; + } + + _chunked = true; + } + + if (_httpMethod == "POST" || _httpMethod == "PUT") { + if (_contentLength <= 0 && !_chunked) { + _context.ErrorMessage = String.Empty; + _context.ErrorStatus = 411; + + return; + } + } + + var expect = _headers["Expect"]; + if (expect != null) { + var comparison = StringComparison.OrdinalIgnoreCase; + if (!expect.Equals ("100-continue", comparison)) { + _context.ErrorMessage = "Invalid Expect header"; + return; + } + + var output = _connection.GetResponseStream (); + output.InternalWrite (_100continue, 0, _100continue.Length); + } + } + + internal bool FlushInput () + { + var input = InputStream; + if (input == Stream.Null) + return true; + + var len = 2048; + if (_contentLength > 0 && _contentLength < len) + len = (int) _contentLength; + + var buff = new byte[len]; + + while (true) { + try { + var ares = input.BeginRead (buff, 0, len, null, null); + if (!ares.IsCompleted) { + var timeout = 100; + if (!ares.AsyncWaitHandle.WaitOne (timeout)) + return false; + } + + if (input.EndRead (ares) <= 0) + return true; + } + catch { + return false; + } + } + } + + internal bool IsUpgradeRequest (string protocol) + { + return _headers.Upgrades (protocol); + } + + internal void SetRequestLine (string requestLine) + { + var parts = requestLine.Split (new[] { ' ' }, 3); + if (parts.Length < 3) { + _context.ErrorMessage = "Invalid request line (parts)"; + return; + } + + var method = parts[0]; + if (method.Length == 0) { + _context.ErrorMessage = "Invalid request line (method)"; + return; + } + + var target = parts[1]; + if (target.Length == 0) { + _context.ErrorMessage = "Invalid request line (target)"; + return; + } + + var rawVer = parts[2]; + if (rawVer.Length != 8) { + _context.ErrorMessage = "Invalid request line (version)"; + return; + } + + if (rawVer.IndexOf ("HTTP/") != 0) { + _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.Major < 1) { + _context.ErrorMessage = "Invalid request line (version)"; + return; + } + + if (!method.IsHttpMethod (ver)) { + _context.ErrorMessage = "Invalid request line (method)"; + return; + } + + _httpMethod = method; + _rawUrl = target; + _protocolVersion = ver; + } + + #endregion + + #region Public Methods + + /// + /// Begins getting the certificate provided by the client asynchronously. + /// + /// + /// An instance that indicates the status of the + /// operation. + /// + /// + /// An delegate that invokes the method called + /// when the operation is complete. + /// + /// + /// An that represents a user defined object to pass to + /// the callback delegate. + /// + /// + /// This method is not supported. + /// + public IAsyncResult BeginGetClientCertificate ( + AsyncCallback requestCallback, object state + ) + { + throw new NotSupportedException (); + } + + /// + /// Ends an asynchronous operation to get the certificate provided by the + /// client. + /// + /// + /// A that represents an X.509 certificate + /// provided by the client. + /// + /// + /// An instance returned when the operation + /// started. + /// + /// + /// This method is not supported. + /// + public X509Certificate2 EndGetClientCertificate (IAsyncResult asyncResult) + { + throw new NotSupportedException (); + } + + /// + /// Gets the certificate provided by the client. + /// + /// + /// A that represents an X.509 certificate + /// provided by the client. + /// + /// + /// This method is not supported. + /// + public X509Certificate2 GetClientCertificate () + { + throw new NotSupportedException (); + } + + /// + /// Returns a string that represents the current instance. + /// + /// + /// 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", _httpMethod, _rawUrl, _protocolVersion + ) + .Append (_headers.ToString ()); + + return buff.ToString (); + } + + #endregion + } +} diff --git a/websocket-sharp-core/Net/HttpListenerResponse.cs b/websocket-sharp-core/Net/HttpListenerResponse.cs new file mode 100644 index 000000000..516a3c974 --- /dev/null +++ b/websocket-sharp-core/Net/HttpListenerResponse.cs @@ -0,0 +1,1108 @@ +#region License +/* + * HttpListenerResponse.cs + * + * This code is derived from HttpListenerResponse.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-2015 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: + * - Gonzalo Paniagua Javier + */ +#endregion + +#region Contributors +/* + * Contributors: + * - Nicholas Devenish + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text; + +namespace WebSocketSharp.Net +{ + /// + /// Provides the access to a response to a request received by the . + /// + /// + /// The HttpListenerResponse 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; + + #endregion + + #region Internal Constructors + + internal HttpListenerResponse (HttpListenerContext context) + { + _context = context; + _keepAlive = true; + _statusCode = 200; + _statusDescription = "OK"; + _version = HttpVersion.Version11; + } + + #endregion + + #region Internal Properties + + internal bool CloseConnection { + get { + return _closeConnection; + } + + set { + _closeConnection = value; + } + } + + internal WebHeaderCollection FullHeaders { + get { + var headers = new WebHeaderCollection (HttpHeaderType.Response, true); + + if (_headers != null) + headers.Add (_headers); + + if (_contentType != null) { + headers.InternalSet ( + "Content-Type", + createContentTypeHeaderText (_contentType, _contentEncoding), + true + ); + } + + if (headers["Server"] == null) + headers.InternalSet ("Server", "websocket-sharp/1.0", true); + + if (headers["Date"] == null) { + headers.InternalSet ( + "Date", + DateTime.UtcNow.ToString ("r", CultureInfo.InvariantCulture), + true + ); + } + + if (_sendChunked) { + headers.InternalSet ("Transfer-Encoding", "chunked", true); + } + else { + headers.InternalSet ( + "Content-Length", + _contentLength.ToString (CultureInfo.InvariantCulture), + 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 + ); + + if (_context.Request.ProtocolVersion < HttpVersion.Version11) + headers.InternalSet ("Connection", "keep-alive", true); + } + + if (_location != null) + headers.InternalSet ("Location", _location, true); + + if (_cookies != null) { + foreach (var cookie in _cookies) { + headers.InternalSet ( + "Set-Cookie", + cookie.ToResponseString (), + true + ); + } + } + + return headers; + } + } + + internal bool HeadersSent { + get { + return _headersSent; + } + + set { + _headersSent = value; + } + } + + internal string StatusLine { + get { + return String.Format ( + "HTTP/{0} {1} {2}\r\n", + _version, + _statusCode, + _statusDescription + ); + } + } + + #endregion + + #region Public Properties + + /// + /// Gets or sets the encoding for the entity body data included in + /// the response. + /// + /// + /// + /// 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 instance is closed. + /// + public Encoding ContentEncoding { + get { + return _contentEncoding; + } + + set { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + 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. + /// + /// + /// + /// 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 is already being sent. + /// + /// + /// This instance is closed. + /// + public long ContentLength64 { + get { + return _contentLength; + } + + set { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + 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. + /// + /// + /// + /// 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 an empty string. + /// + /// + /// The response is already being sent. + /// + /// + /// This instance is closed. + /// + public string ContentType { + get { + return _contentType; + } + + set { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + 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"); + + _contentType = value; + } + } + + /// + /// Gets or sets the collection of cookies sent with the response. + /// + /// + /// A that contains the cookies sent with + /// the response. + /// + public CookieCollection Cookies { + get { + if (_cookies == null) + _cookies = new CookieCollection (); + + return _cookies; + } + + set { + _cookies = value; + } + } + + /// + /// Gets or sets the collection of HTTP headers sent to the client. + /// + /// + /// A that contains the headers sent to + /// the client. + /// + /// + /// The value specified for a set operation is not valid for a response. + /// + public WebHeaderCollection Headers { + get { + if (_headers == null) + _headers = new WebHeaderCollection (HttpHeaderType.Response, false); + + return _headers; + } + + set { + 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. + /// + /// + /// + /// true if the server requests a persistent connection; + /// otherwise, false. + /// + /// + /// The default value is true. + /// + /// + /// + /// The response is already being sent. + /// + /// + /// This instance is closed. + /// + public bool KeepAlive { + get { + return _keepAlive; + } + + set { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + if (_headersSent) { + var msg = "The response is already being sent."; + throw new InvalidOperationException (msg); + } + + _keepAlive = value; + } + } + + /// + /// Gets a stream instance to which the entity body data can be written. + /// + /// + /// A instance to which the entity body data can be + /// written. + /// + /// + /// This instance is closed. + /// + public Stream OutputStream { + get { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + if (_outputStream == null) + _outputStream = _context.Connection.GetResponseStream (); + + return _outputStream; + } + } + + /// + /// Gets or sets the HTTP version used for the response. + /// + /// + /// A that represents the HTTP version used for + /// the response. + /// + /// + /// The value specified for a set operation is . + /// + /// + /// + /// The value specified for a set operation does not have its Major + /// property set to 1. + /// + /// + /// -or- + /// + /// + /// The value specified for a set operation does not have its Minor + /// property set to either 0 or 1. + /// + /// + /// + /// The response is already being sent. + /// + /// + /// This instance is closed. + /// + public Version ProtocolVersion { + get { + return _version; + } + + set { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + if (_headersSent) { + var msg = "The response is already being sent."; + throw new InvalidOperationException (msg); + } + + if (value == null) + throw new ArgumentNullException ("value"); + + if (value.Major != 1) { + var msg = "Its Major property is not 1."; + throw new ArgumentException (msg, "value"); + } + + if (value.Minor < 0 || value.Minor > 1) { + var msg = "Its Minor property is not 0 or 1."; + throw new ArgumentException (msg, "value"); + } + + _version = value; + } + } + + /// + /// Gets or sets the URL to which the client is redirected to locate + /// a requested resource. + /// + /// + /// + /// 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 is an empty string. + /// + /// + /// -or- + /// + /// + /// The value specified for a set operation is not an absolute URL. + /// + /// + /// + /// The response is already being sent. + /// + /// + /// This instance is closed. + /// + public string RedirectLocation { + get { + return _location; + } + + set { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + if (_headersSent) { + var msg = "The response is already being sent."; + throw new InvalidOperationException (msg); + } + + if (value == null) { + _location = null; + return; + } + + if (value.Length == 0) + throw new ArgumentException ("An empty string.", "value"); + + Uri uri; + if (!Uri.TryCreate (value, UriKind.Absolute, out uri)) + throw new ArgumentException ("Not an absolute URL.", "value"); + + _location = value; + } + } + + /// + /// 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. + /// + /// + /// + /// The response is already being sent. + /// + /// + /// This instance is closed. + /// + public bool SendChunked { + get { + return _sendChunked; + } + + set { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + if (_headersSent) { + var msg = "The response is already being sent."; + throw new InvalidOperationException (msg); + } + + _sendChunked = value; + } + } + + /// + /// Gets or sets the HTTP status code returned to the client. + /// + /// + /// + /// An that represents the HTTP status code for + /// the response to the request. + /// + /// + /// The default value is 200. It is same as + /// . + /// + /// + /// + /// The response is already being sent. + /// + /// + /// This instance is closed. + /// + /// + /// + /// The value specified for a set operation is invalid. + /// + /// + /// Valid values are between 100 and 999 inclusive. + /// + /// + public int StatusCode { + get { + return _statusCode; + } + + set { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + 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 (); + } + } + + /// + /// Gets or sets the description of the HTTP status code returned to + /// the client. + /// + /// + /// + /// 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 is . + /// + /// + /// The value specified for a set operation contains an invalid character. + /// + /// + /// The response is already being sent. + /// + /// + /// This instance is closed. + /// + public string StatusDescription { + get { + return _statusDescription; + } + + set { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + 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 ()) { + var msg = "It contains an invalid character."; + throw new ArgumentException (msg, "value"); + } + + if (value.IndexOfAny (new[] { '\r', '\n' }) > -1) { + var msg = "It contains an invalid character."; + throw new ArgumentException (msg, "value"); + } + + _statusDescription = value; + } + } + + #endregion + + #region Private Methods + + private bool canSetCookie (Cookie cookie) + { + var found = findCookie (cookie).ToList (); + + if (found.Count == 0) + return true; + + var ver = cookie.Version; + + foreach (var c in found) { + if (c.Version == ver) + return true; + } + + return false; + } + + private void close (bool force) + { + _disposed = true; + _context.Connection.Close (force); + } + + private static string createContentTypeHeaderText ( + string value, Encoding encoding + ) + { + if (value.IndexOf ("charset=", StringComparison.Ordinal) > -1) + return value; + + if (encoding == null) + return value; + + return String.Format ("{0}; charset={1}", value, encoding.WebName); + } + + private IEnumerable findCookie (Cookie cookie) + { + if (_cookies == null || _cookies.Count == 0) + yield break; + + foreach (var c in _cookies) { + if (c.EqualsWithoutValueAndVersion (cookie)) + yield return c; + } + } + + #endregion + + #region Public Methods + + /// + /// Closes the connection to the client without sending a response. + /// + public void Abort () + { + if (_disposed) + return; + + close (true); + } + + /// + /// Appends the specified cookie to the cookies sent with the response. + /// + /// + /// A to append. + /// + /// + /// is . + /// + public void AppendCookie (Cookie cookie) + { + Cookies.Add (cookie); + } + + /// + /// 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. + /// + /// + /// A that represents the value of the header to + /// append. + /// + /// + /// is or empty. + /// + /// + /// + /// or contains + /// an invalid character. + /// + /// + /// -or- + /// + /// + /// is a restricted header name. + /// + /// + /// + /// The length of is greater than 65,535 + /// characters. + /// + /// + /// The header cannot be allowed to append to the current headers. + /// + public void AppendHeader (string name, string value) + { + Headers.Add (name, value); + } + + /// + /// Sends the response to the client and releases the resources used by + /// this instance. + /// + public void Close () + { + if (_disposed) + return; + + close (false); + } + + /// + /// 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 entity body data. + /// + /// + /// true if this method blocks execution while flushing the stream to + /// the client; otherwise, false. + /// + /// + /// is . + /// + /// + /// This instance is closed. + /// + public void Close (byte[] responseEntity, bool willBlock) + { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + if (responseEntity == null) + throw new ArgumentNullException ("responseEntity"); + + var len = responseEntity.Length; + var output = OutputStream; + + if (willBlock) { + output.Write (responseEntity, 0, len); + close (false); + + return; + } + + output.BeginWrite ( + responseEntity, + 0, + len, + ar => { + output.EndWrite (ar); + close (false); + }, + null + ); + } + + /// + /// Copies some properties from the specified response instance to + /// this instance. + /// + /// + /// A to copy. + /// + /// + /// is . + /// + public void CopyFrom (HttpListenerResponse templateResponse) + { + if (templateResponse == null) + throw new ArgumentNullException ("templateResponse"); + + var headers = templateResponse._headers; + + if (headers != null) { + if (_headers != null) + _headers.Clear (); + + Headers.Add (headers); + } + else { + _headers = null; + } + + _contentLength = templateResponse._contentLength; + _statusCode = templateResponse._statusCode; + _statusDescription = templateResponse._statusDescription; + _keepAlive = templateResponse._keepAlive; + _version = templateResponse._version; + } + + /// + /// Configures the response to redirect the client's request to + /// the specified URL. + /// + /// + /// This method sets the property to + /// , the property to + /// 302, and the property to "Found". + /// + /// + /// A that represents the absolute URL to which + /// the client is redirected to locate a requested resource. + /// + /// + /// is . + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is not an absolute URL. + /// + /// + /// + /// The response is already being sent. + /// + /// + /// This instance is closed. + /// + public void Redirect (string url) + { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + if (_headersSent) { + var msg = "The response is already being sent."; + throw new InvalidOperationException (msg); + } + + if (url == null) + throw new ArgumentNullException ("url"); + + if (url.Length == 0) + throw new ArgumentException ("An empty string.", "url"); + + Uri uri; + if (!Uri.TryCreate (url, UriKind.Absolute, out uri)) + throw new ArgumentException ("Not an absolute URL.", "url"); + + _location = url; + _statusCode = 302; + _statusDescription = "Found"; + } + + /// + /// Adds or updates a cookie in the cookies sent with the response. + /// + /// + /// A to set. + /// + /// + /// is . + /// + /// + /// already exists in the cookies but + /// it cannot be updated. + /// + public void SetCookie (Cookie cookie) + { + if (cookie == null) + throw new ArgumentNullException ("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 represents the name of the header to set. + /// + /// + /// A that represents the value of the header to set. + /// + /// + /// is or empty. + /// + /// + /// + /// or contains + /// an invalid character. + /// + /// + /// -or- + /// + /// + /// is a restricted header name. + /// + /// + /// + /// The length of is greater than 65,535 + /// characters. + /// + /// + /// The header cannot be allowed to set in the current headers. + /// + public void SetHeader (string name, string value) + { + Headers.Set (name, value); + } + + #endregion + + #region Explicit Interface Implementations + + /// + /// Releases all resources used by this instance. + /// + void IDisposable.Dispose () + { + if (_disposed) + return; + + close (true); + } + + #endregion + } +} diff --git a/websocket-sharp-core/Net/HttpRequestHeader.cs b/websocket-sharp-core/Net/HttpRequestHeader.cs new file mode 100644 index 000000000..08785db34 --- /dev/null +++ b/websocket-sharp-core/Net/HttpRequestHeader.cs @@ -0,0 +1,233 @@ +#region License +/* + * HttpRequestHeader.cs + * + * This code is derived from System.Net.HttpRequestHeader.cs of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * 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: + * - Gonzalo Paniagua Javier + */ +#endregion + +namespace WebSocketSharp.Net +{ + /// + /// Contains the HTTP headers 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. + /// + public enum HttpRequestHeader + { + /// + /// Indicates the Cache-Control header. + /// + CacheControl, + /// + /// Indicates the Connection header. + /// + Connection, + /// + /// Indicates the Date header. + /// + Date, + /// + /// Indicates the Keep-Alive header. + /// + KeepAlive, + /// + /// Indicates the Pragma header. + /// + Pragma, + /// + /// Indicates the Trailer header. + /// + Trailer, + /// + /// Indicates the Transfer-Encoding header. + /// + TransferEncoding, + /// + /// Indicates the Upgrade header. + /// + Upgrade, + /// + /// Indicates the Via header. + /// + Via, + /// + /// Indicates the Warning header. + /// + Warning, + /// + /// Indicates the Allow header. + /// + Allow, + /// + /// Indicates the Content-Length header. + /// + ContentLength, + /// + /// Indicates the Content-Type header. + /// + ContentType, + /// + /// Indicates the Content-Encoding header. + /// + ContentEncoding, + /// + /// Indicates the Content-Language header. + /// + ContentLanguage, + /// + /// Indicates the Content-Location header. + /// + ContentLocation, + /// + /// Indicates the Content-MD5 header. + /// + ContentMd5, + /// + /// Indicates the Content-Range header. + /// + ContentRange, + /// + /// Indicates the Expires header. + /// + Expires, + /// + /// Indicates the Last-Modified header. + /// + LastModified, + /// + /// Indicates the Accept header. + /// + Accept, + /// + /// Indicates the Accept-Charset header. + /// + AcceptCharset, + /// + /// Indicates the Accept-Encoding header. + /// + AcceptEncoding, + /// + /// Indicates the Accept-Language header. + /// + AcceptLanguage, + /// + /// Indicates the Authorization header. + /// + Authorization, + /// + /// Indicates the Cookie header. + /// + Cookie, + /// + /// Indicates the Expect header. + /// + Expect, + /// + /// Indicates the From header. + /// + From, + /// + /// Indicates the Host header. + /// + Host, + /// + /// Indicates the If-Match header. + /// + IfMatch, + /// + /// Indicates the If-Modified-Since header. + /// + IfModifiedSince, + /// + /// Indicates the If-None-Match header. + /// + IfNoneMatch, + /// + /// Indicates the If-Range header. + /// + IfRange, + /// + /// Indicates the If-Unmodified-Since header. + /// + IfUnmodifiedSince, + /// + /// Indicates the Max-Forwards header. + /// + MaxForwards, + /// + /// Indicates the Proxy-Authorization header. + /// + ProxyAuthorization, + /// + /// Indicates the Referer header. + /// + Referer, + /// + /// Indicates the Range header. + /// + Range, + /// + /// Indicates the TE header. + /// + Te, + /// + /// Indicates the Translate header. + /// + Translate, + /// + /// Indicates the User-Agent header. + /// + UserAgent, + /// + /// Indicates the Sec-WebSocket-Key header. + /// + SecWebSocketKey, + /// + /// Indicates the Sec-WebSocket-Extensions header. + /// + SecWebSocketExtensions, + /// + /// Indicates the Sec-WebSocket-Protocol header. + /// + SecWebSocketProtocol, + /// + /// Indicates the Sec-WebSocket-Version header. + /// + SecWebSocketVersion + } +} diff --git a/websocket-sharp-core/Net/HttpResponseHeader.cs b/websocket-sharp-core/Net/HttpResponseHeader.cs new file mode 100644 index 000000000..d8f36ed84 --- /dev/null +++ b/websocket-sharp-core/Net/HttpResponseHeader.cs @@ -0,0 +1,189 @@ +#region License +/* + * HttpResponseHeader.cs + * + * This code is derived from System.Net.HttpResponseHeader.cs of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * 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: + * - Gonzalo Paniagua Javier + */ +#endregion + +namespace WebSocketSharp.Net +{ + /// + /// Contains the HTTP headers 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. + /// + public enum HttpResponseHeader + { + /// + /// Indicates the Cache-Control header. + /// + CacheControl, + /// + /// Indicates the Connection header. + /// + Connection, + /// + /// Indicates the Date header. + /// + Date, + /// + /// Indicates the Keep-Alive header. + /// + KeepAlive, + /// + /// Indicates the Pragma header. + /// + Pragma, + /// + /// Indicates the Trailer header. + /// + Trailer, + /// + /// Indicates the Transfer-Encoding header. + /// + TransferEncoding, + /// + /// Indicates the Upgrade header. + /// + Upgrade, + /// + /// Indicates the Via header. + /// + Via, + /// + /// Indicates the Warning header. + /// + Warning, + /// + /// Indicates the Allow header. + /// + Allow, + /// + /// Indicates the Content-Length header. + /// + ContentLength, + /// + /// Indicates the Content-Type header. + /// + ContentType, + /// + /// Indicates the Content-Encoding header. + /// + ContentEncoding, + /// + /// Indicates the Content-Language header. + /// + ContentLanguage, + /// + /// Indicates the Content-Location header. + /// + ContentLocation, + /// + /// Indicates the Content-MD5 header. + /// + ContentMd5, + /// + /// Indicates the Content-Range header. + /// + ContentRange, + /// + /// Indicates the Expires header. + /// + Expires, + /// + /// Indicates the Last-Modified header. + /// + LastModified, + /// + /// Indicates the Accept-Ranges header. + /// + AcceptRanges, + /// + /// Indicates the Age header. + /// + Age, + /// + /// Indicates the ETag header. + /// + ETag, + /// + /// Indicates the Location header. + /// + Location, + /// + /// Indicates the Proxy-Authenticate header. + /// + ProxyAuthenticate, + /// + /// Indicates the Retry-After header. + /// + RetryAfter, + /// + /// Indicates the Server header. + /// + Server, + /// + /// Indicates the Set-Cookie header. + /// + SetCookie, + /// + /// Indicates the Vary header. + /// + Vary, + /// + /// Indicates the WWW-Authenticate header. + /// + WwwAuthenticate, + /// + /// Indicates the Sec-WebSocket-Extensions header. + /// + SecWebSocketExtensions, + /// + /// Indicates the Sec-WebSocket-Accept header. + /// + SecWebSocketAccept, + /// + /// Indicates the Sec-WebSocket-Protocol header. + /// + SecWebSocketProtocol, + /// + /// Indicates the Sec-WebSocket-Version header. + /// + SecWebSocketVersion + } +} diff --git a/websocket-sharp-core/Net/HttpStatusCode.cs b/websocket-sharp-core/Net/HttpStatusCode.cs new file mode 100644 index 000000000..123415f01 --- /dev/null +++ b/websocket-sharp-core/Net/HttpStatusCode.cs @@ -0,0 +1,359 @@ +#region License +/* + * HttpStatusCode.cs + * + * This code is derived from System.Net.HttpStatusCode.cs of Mono + * (http://www.mono-project.com). + * + * It was automatically generated from ECMA CLI XML Library Specification. + * Generator: libgen.xsl [1.0; (C) Sergey Chaban (serge@wildwestsoftware.com)] + * Created: Wed, 5 Sep 2001 06:32:05 UTC + * Source file: AllTypes.xml + * URL: http://msdn.microsoft.com/net/ecma/AllTypes.xml + * + * The MIT License + * + * Copyright (c) 2001 Ximian, Inc. (http://www.ximian.com) + * Copyright (c) 2012-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 + +namespace WebSocketSharp.Net +{ + /// + /// Contains the values of the HTTP status codes. + /// + /// + /// The HttpStatusCode enumeration contains the values of the HTTP status codes defined in + /// RFC 2616 for the HTTP/1.1. + /// + public enum HttpStatusCode + { + /// + /// 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. + /// + SwitchingProtocols = 101, + /// + /// 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. + /// + 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. + /// + 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. + /// + 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. + /// + 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. + /// + ResetContent = 205, + /// + /// 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. + /// + /// + /// MultipleChoices is a synonym for Ambiguous. + /// + /// + MultipleChoices = 300, + /// + /// + /// Equivalent to status code 300. + /// Indicates that the requested resource corresponds to any of multiple representations. + /// + /// + /// Ambiguous is a synonym for MultipleChoices. + /// + /// + 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. + /// + /// + /// MovedPermanently is a synonym for Moved. + /// + /// + 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. + /// + /// + /// Moved is a synonym for MovedPermanently. + /// + /// + Moved = 301, + /// + /// + /// Equivalent to status code 302. + /// Indicates that the requested resource is located temporarily under a different URI. + /// + /// + /// Found is a synonym for Redirect. + /// + /// + Found = 302, + /// + /// + /// Equivalent to status code 302. + /// Indicates that the requested resource is located temporarily under a different URI. + /// + /// + /// Redirect is a synonym for Found. + /// + /// + 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. + /// + /// + /// SeeOther is a synonym for RedirectMethod. + /// + /// + 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. + /// + /// + /// RedirectMethod is a synonym for SeeOther. + /// + /// + 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. + /// + NotModified = 304, + /// + /// 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. + /// + Unused = 306, + /// + /// + /// Equivalent to status code 307. + /// Indicates that the requested resource is located temporarily under a different URI. + /// + /// + /// TemporaryRedirect is a synonym for RedirectKeepVerb. + /// + /// + TemporaryRedirect = 307, + /// + /// + /// Equivalent to status code 307. + /// Indicates that the requested resource is located temporarily under a different URI. + /// + /// + /// RedirectKeepVerb is a synonym for TemporaryRedirect. + /// + /// + RedirectKeepVerb = 307, + /// + /// Equivalent to status code 400. + /// Indicates that the client's request couldn't 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. + /// + Unauthorized = 401, + /// + /// 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. + /// + Forbidden = 403, + /// + /// Equivalent to status code 404. + /// Indicates that the server hasn't 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. + /// + 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. + /// + NotAcceptable = 406, + /// + /// 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. + /// + RequestTimeout = 408, + /// + /// Equivalent to status code 409. + /// Indicates that the client's request couldn't 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. + /// + Gone = 410, + /// + /// 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. + /// + 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. + /// + RequestEntityTooLarge = 413, + /// + /// 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. + /// + 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. + /// + RequestedRangeNotSatisfiable = 416, + /// + /// Equivalent to status code 417. + /// Indicates that the expectation given in an Expect request header couldn't 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. + /// + InternalServerError = 500, + /// + /// Equivalent to status code 501. + /// Indicates that the server doesn't 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. + /// + 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. + /// + 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. + /// + GatewayTimeout = 504, + /// + /// Equivalent to status code 505. + /// Indicates that the server doesn't support the HTTP version used in the client's request. + /// + HttpVersionNotSupported = 505, + } +} diff --git a/websocket-sharp-core/Net/HttpStreamAsyncResult.cs b/websocket-sharp-core/Net/HttpStreamAsyncResult.cs new file mode 100644 index 000000000..44189303c --- /dev/null +++ b/websocket-sharp-core/Net/HttpStreamAsyncResult.cs @@ -0,0 +1,184 @@ +#region License +/* + * HttpStreamAsyncResult.cs + * + * This code is derived from HttpStreamAsyncResult.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-2015 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: + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; +using System.Threading; + +namespace WebSocketSharp.Net +{ + internal class HttpStreamAsyncResult : IAsyncResult + { + #region Private Fields + + private byte[] _buffer; + private AsyncCallback _callback; + private bool _completed; + private int _count; + private Exception _exception; + private int _offset; + private object _state; + private object _sync; + private int _syncRead; + private ManualResetEvent _waitHandle; + + #endregion + + #region Internal Constructors + + internal HttpStreamAsyncResult (AsyncCallback callback, object state) + { + _callback = callback; + _state = state; + _sync = new object (); + } + + #endregion + + #region Internal Properties + + internal byte[] Buffer { + get { + return _buffer; + } + + set { + _buffer = value; + } + } + + internal int Count { + get { + return _count; + } + + set { + _count = value; + } + } + + internal Exception Exception { + get { + return _exception; + } + } + + internal bool HasException { + get { + return _exception != null; + } + } + + internal int Offset { + get { + return _offset; + } + + set { + _offset = value; + } + } + + internal int SyncRead { + get { + return _syncRead; + } + + set { + _syncRead = value; + } + } + + #endregion + + #region Public Properties + + public object AsyncState { + get { + return _state; + } + } + + public WaitHandle AsyncWaitHandle { + get { + lock (_sync) + return _waitHandle ?? (_waitHandle = new ManualResetEvent (_completed)); + } + } + + public bool CompletedSynchronously { + get { + return _syncRead == _count; + } + } + + public bool IsCompleted { + get { + lock (_sync) + return _completed; + } + } + + #endregion + + #region Internal Methods + + internal void Complete () + { + lock (_sync) { + if (_completed) + return; + + _completed = true; + if (_waitHandle != null) + _waitHandle.Set (); + + if (_callback != null) + _callback.BeginInvoke (this, ar => _callback.EndInvoke (ar), null); + } + } + + internal void Complete (Exception exception) + { + _exception = exception; + Complete (); + } + + #endregion + } +} diff --git a/websocket-sharp-core/Net/HttpUtility.cs b/websocket-sharp-core/Net/HttpUtility.cs new file mode 100644 index 000000000..47ea7ee3a --- /dev/null +++ b/websocket-sharp-core/Net/HttpUtility.cs @@ -0,0 +1,1146 @@ +#region License +/* + * HttpUtility.cs + * + * 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-2019 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: + * - Patrik Torstensson + * - Wictor Wilén (decode/encode functions) + * - Tim Coleman + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Globalization; +using System.IO; +using System.Security.Principal; +using System.Text; + +namespace WebSocketSharp.Net +{ + internal static class HttpUtility + { + #region Private Fields + + private static Dictionary _entities; + 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 Dictionary getEntities () + { + lock (_sync) { + if (_entities == null) + initEntities (); + + return _entities; + } + } + + private static int getNumber (char c) + { + return c >= '0' && c <= '9' + ? c - '0' + : c >= 'A' && c <= 'F' + ? c - 'A' + 10 + : c >= 'a' && c <= 'f' + ? c - 'a' + 10 + : -1; + } + + private static int getNumber (byte[] bytes, int offset, int count) + { + var ret = 0; + + var end = offset + count - 1; + for (var i = offset; i <= end; i++) { + var num = getNumber ((char) bytes[i]); + if (num == -1) + return -1; + + ret = (ret << 4) + num; + } + + return ret; + } + + private static int getNumber (string s, int offset, int count) + { + var ret = 0; + + var end = offset + count - 1; + for (var i = offset; i <= end; i++) { + var num = getNumber (s[i]); + if (num == -1) + return -1; + + ret = (ret << 4) + num; + } + + return ret; + } + + private static string htmlDecode (string s) + { + var buff = new StringBuilder (); + + // 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 (); + } + + /// + /// 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 buff = new StringBuilder (); + + foreach (var c in s) { + buff.Append ( + c == '"' + ? """ + : c == '&' + ? "&" + : c == '<' + ? "<" + : c == '>' + ? ">" + : !minimal && c > 159 + ? String.Format ("&#{0};", (int) c) + : c.ToString () + ); + } + + 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 () + { + _entities = new Dictionary (); + _entities.Add ("nbsp", '\u00A0'); + _entities.Add ("iexcl", '\u00A1'); + _entities.Add ("cent", '\u00A2'); + _entities.Add ("pound", '\u00A3'); + _entities.Add ("curren", '\u00A4'); + _entities.Add ("yen", '\u00A5'); + _entities.Add ("brvbar", '\u00A6'); + _entities.Add ("sect", '\u00A7'); + _entities.Add ("uml", '\u00A8'); + _entities.Add ("copy", '\u00A9'); + _entities.Add ("ordf", '\u00AA'); + _entities.Add ("laquo", '\u00AB'); + _entities.Add ("not", '\u00AC'); + _entities.Add ("shy", '\u00AD'); + _entities.Add ("reg", '\u00AE'); + _entities.Add ("macr", '\u00AF'); + _entities.Add ("deg", '\u00B0'); + _entities.Add ("plusmn", '\u00B1'); + _entities.Add ("sup2", '\u00B2'); + _entities.Add ("sup3", '\u00B3'); + _entities.Add ("acute", '\u00B4'); + _entities.Add ("micro", '\u00B5'); + _entities.Add ("para", '\u00B6'); + _entities.Add ("middot", '\u00B7'); + _entities.Add ("cedil", '\u00B8'); + _entities.Add ("sup1", '\u00B9'); + _entities.Add ("ordm", '\u00BA'); + _entities.Add ("raquo", '\u00BB'); + _entities.Add ("frac14", '\u00BC'); + _entities.Add ("frac12", '\u00BD'); + _entities.Add ("frac34", '\u00BE'); + _entities.Add ("iquest", '\u00BF'); + _entities.Add ("Agrave", '\u00C0'); + _entities.Add ("Aacute", '\u00C1'); + _entities.Add ("Acirc", '\u00C2'); + _entities.Add ("Atilde", '\u00C3'); + _entities.Add ("Auml", '\u00C4'); + _entities.Add ("Aring", '\u00C5'); + _entities.Add ("AElig", '\u00C6'); + _entities.Add ("Ccedil", '\u00C7'); + _entities.Add ("Egrave", '\u00C8'); + _entities.Add ("Eacute", '\u00C9'); + _entities.Add ("Ecirc", '\u00CA'); + _entities.Add ("Euml", '\u00CB'); + _entities.Add ("Igrave", '\u00CC'); + _entities.Add ("Iacute", '\u00CD'); + _entities.Add ("Icirc", '\u00CE'); + _entities.Add ("Iuml", '\u00CF'); + _entities.Add ("ETH", '\u00D0'); + _entities.Add ("Ntilde", '\u00D1'); + _entities.Add ("Ograve", '\u00D2'); + _entities.Add ("Oacute", '\u00D3'); + _entities.Add ("Ocirc", '\u00D4'); + _entities.Add ("Otilde", '\u00D5'); + _entities.Add ("Ouml", '\u00D6'); + _entities.Add ("times", '\u00D7'); + _entities.Add ("Oslash", '\u00D8'); + _entities.Add ("Ugrave", '\u00D9'); + _entities.Add ("Uacute", '\u00DA'); + _entities.Add ("Ucirc", '\u00DB'); + _entities.Add ("Uuml", '\u00DC'); + _entities.Add ("Yacute", '\u00DD'); + _entities.Add ("THORN", '\u00DE'); + _entities.Add ("szlig", '\u00DF'); + _entities.Add ("agrave", '\u00E0'); + _entities.Add ("aacute", '\u00E1'); + _entities.Add ("acirc", '\u00E2'); + _entities.Add ("atilde", '\u00E3'); + _entities.Add ("auml", '\u00E4'); + _entities.Add ("aring", '\u00E5'); + _entities.Add ("aelig", '\u00E6'); + _entities.Add ("ccedil", '\u00E7'); + _entities.Add ("egrave", '\u00E8'); + _entities.Add ("eacute", '\u00E9'); + _entities.Add ("ecirc", '\u00EA'); + _entities.Add ("euml", '\u00EB'); + _entities.Add ("igrave", '\u00EC'); + _entities.Add ("iacute", '\u00ED'); + _entities.Add ("icirc", '\u00EE'); + _entities.Add ("iuml", '\u00EF'); + _entities.Add ("eth", '\u00F0'); + _entities.Add ("ntilde", '\u00F1'); + _entities.Add ("ograve", '\u00F2'); + _entities.Add ("oacute", '\u00F3'); + _entities.Add ("ocirc", '\u00F4'); + _entities.Add ("otilde", '\u00F5'); + _entities.Add ("ouml", '\u00F6'); + _entities.Add ("divide", '\u00F7'); + _entities.Add ("oslash", '\u00F8'); + _entities.Add ("ugrave", '\u00F9'); + _entities.Add ("uacute", '\u00FA'); + _entities.Add ("ucirc", '\u00FB'); + _entities.Add ("uuml", '\u00FC'); + _entities.Add ("yacute", '\u00FD'); + _entities.Add ("thorn", '\u00FE'); + _entities.Add ("yuml", '\u00FF'); + _entities.Add ("fnof", '\u0192'); + _entities.Add ("Alpha", '\u0391'); + _entities.Add ("Beta", '\u0392'); + _entities.Add ("Gamma", '\u0393'); + _entities.Add ("Delta", '\u0394'); + _entities.Add ("Epsilon", '\u0395'); + _entities.Add ("Zeta", '\u0396'); + _entities.Add ("Eta", '\u0397'); + _entities.Add ("Theta", '\u0398'); + _entities.Add ("Iota", '\u0399'); + _entities.Add ("Kappa", '\u039A'); + _entities.Add ("Lambda", '\u039B'); + _entities.Add ("Mu", '\u039C'); + _entities.Add ("Nu", '\u039D'); + _entities.Add ("Xi", '\u039E'); + _entities.Add ("Omicron", '\u039F'); + _entities.Add ("Pi", '\u03A0'); + _entities.Add ("Rho", '\u03A1'); + _entities.Add ("Sigma", '\u03A3'); + _entities.Add ("Tau", '\u03A4'); + _entities.Add ("Upsilon", '\u03A5'); + _entities.Add ("Phi", '\u03A6'); + _entities.Add ("Chi", '\u03A7'); + _entities.Add ("Psi", '\u03A8'); + _entities.Add ("Omega", '\u03A9'); + _entities.Add ("alpha", '\u03B1'); + _entities.Add ("beta", '\u03B2'); + _entities.Add ("gamma", '\u03B3'); + _entities.Add ("delta", '\u03B4'); + _entities.Add ("epsilon", '\u03B5'); + _entities.Add ("zeta", '\u03B6'); + _entities.Add ("eta", '\u03B7'); + _entities.Add ("theta", '\u03B8'); + _entities.Add ("iota", '\u03B9'); + _entities.Add ("kappa", '\u03BA'); + _entities.Add ("lambda", '\u03BB'); + _entities.Add ("mu", '\u03BC'); + _entities.Add ("nu", '\u03BD'); + _entities.Add ("xi", '\u03BE'); + _entities.Add ("omicron", '\u03BF'); + _entities.Add ("pi", '\u03C0'); + _entities.Add ("rho", '\u03C1'); + _entities.Add ("sigmaf", '\u03C2'); + _entities.Add ("sigma", '\u03C3'); + _entities.Add ("tau", '\u03C4'); + _entities.Add ("upsilon", '\u03C5'); + _entities.Add ("phi", '\u03C6'); + _entities.Add ("chi", '\u03C7'); + _entities.Add ("psi", '\u03C8'); + _entities.Add ("omega", '\u03C9'); + _entities.Add ("thetasym", '\u03D1'); + _entities.Add ("upsih", '\u03D2'); + _entities.Add ("piv", '\u03D6'); + _entities.Add ("bull", '\u2022'); + _entities.Add ("hellip", '\u2026'); + _entities.Add ("prime", '\u2032'); + _entities.Add ("Prime", '\u2033'); + _entities.Add ("oline", '\u203E'); + _entities.Add ("frasl", '\u2044'); + _entities.Add ("weierp", '\u2118'); + _entities.Add ("image", '\u2111'); + _entities.Add ("real", '\u211C'); + _entities.Add ("trade", '\u2122'); + _entities.Add ("alefsym", '\u2135'); + _entities.Add ("larr", '\u2190'); + _entities.Add ("uarr", '\u2191'); + _entities.Add ("rarr", '\u2192'); + _entities.Add ("darr", '\u2193'); + _entities.Add ("harr", '\u2194'); + _entities.Add ("crarr", '\u21B5'); + _entities.Add ("lArr", '\u21D0'); + _entities.Add ("uArr", '\u21D1'); + _entities.Add ("rArr", '\u21D2'); + _entities.Add ("dArr", '\u21D3'); + _entities.Add ("hArr", '\u21D4'); + _entities.Add ("forall", '\u2200'); + _entities.Add ("part", '\u2202'); + _entities.Add ("exist", '\u2203'); + _entities.Add ("empty", '\u2205'); + _entities.Add ("nabla", '\u2207'); + _entities.Add ("isin", '\u2208'); + _entities.Add ("notin", '\u2209'); + _entities.Add ("ni", '\u220B'); + _entities.Add ("prod", '\u220F'); + _entities.Add ("sum", '\u2211'); + _entities.Add ("minus", '\u2212'); + _entities.Add ("lowast", '\u2217'); + _entities.Add ("radic", '\u221A'); + _entities.Add ("prop", '\u221D'); + _entities.Add ("infin", '\u221E'); + _entities.Add ("ang", '\u2220'); + _entities.Add ("and", '\u2227'); + _entities.Add ("or", '\u2228'); + _entities.Add ("cap", '\u2229'); + _entities.Add ("cup", '\u222A'); + _entities.Add ("int", '\u222B'); + _entities.Add ("there4", '\u2234'); + _entities.Add ("sim", '\u223C'); + _entities.Add ("cong", '\u2245'); + _entities.Add ("asymp", '\u2248'); + _entities.Add ("ne", '\u2260'); + _entities.Add ("equiv", '\u2261'); + _entities.Add ("le", '\u2264'); + _entities.Add ("ge", '\u2265'); + _entities.Add ("sub", '\u2282'); + _entities.Add ("sup", '\u2283'); + _entities.Add ("nsub", '\u2284'); + _entities.Add ("sube", '\u2286'); + _entities.Add ("supe", '\u2287'); + _entities.Add ("oplus", '\u2295'); + _entities.Add ("otimes", '\u2297'); + _entities.Add ("perp", '\u22A5'); + _entities.Add ("sdot", '\u22C5'); + _entities.Add ("lceil", '\u2308'); + _entities.Add ("rceil", '\u2309'); + _entities.Add ("lfloor", '\u230A'); + _entities.Add ("rfloor", '\u230B'); + _entities.Add ("lang", '\u2329'); + _entities.Add ("rang", '\u232A'); + _entities.Add ("loz", '\u25CA'); + _entities.Add ("spades", '\u2660'); + _entities.Add ("clubs", '\u2663'); + _entities.Add ("hearts", '\u2665'); + _entities.Add ("diams", '\u2666'); + _entities.Add ("quot", '\u0022'); + _entities.Add ("amp", '\u0026'); + _entities.Add ("lt", '\u003C'); + _entities.Add ("gt", '\u003E'); + _entities.Add ("OElig", '\u0152'); + _entities.Add ("oelig", '\u0153'); + _entities.Add ("Scaron", '\u0160'); + _entities.Add ("scaron", '\u0161'); + _entities.Add ("Yuml", '\u0178'); + _entities.Add ("circ", '\u02C6'); + _entities.Add ("tilde", '\u02DC'); + _entities.Add ("ensp", '\u2002'); + _entities.Add ("emsp", '\u2003'); + _entities.Add ("thinsp", '\u2009'); + _entities.Add ("zwnj", '\u200C'); + _entities.Add ("zwj", '\u200D'); + _entities.Add ("lrm", '\u200E'); + _entities.Add ("rlm", '\u200F'); + _entities.Add ("ndash", '\u2013'); + _entities.Add ("mdash", '\u2014'); + _entities.Add ("lsquo", '\u2018'); + _entities.Add ("rsquo", '\u2019'); + _entities.Add ("sbquo", '\u201A'); + _entities.Add ("ldquo", '\u201C'); + _entities.Add ("rdquo", '\u201D'); + _entities.Add ("bdquo", '\u201E'); + _entities.Add ("dagger", '\u2020'); + _entities.Add ("Dagger", '\u2021'); + _entities.Add ("permil", '\u2030'); + _entities.Add ("lsaquo", '\u2039'); + _entities.Add ("rsaquo", '\u203A'); + _entities.Add ("euro", '\u20AC'); + } + + private static bool isAlphabet (char c) + { + return (c >= 'A' && c <= 'Z') + || (c >= 'a' && c <= 'z'); + } + + private static bool isNumeric (char c) + { + return c >= '0' && c <= '9'; + } + + private static bool isUnreserved (char c) + { + return c == '*' + || c == '-' + || c == '.' + || c == '_'; + } + + private static bool isUnreservedInRfc2396 (char c) + { + return c == '!' + || c == '\'' + || c == '(' + || c == ')' + || c == '*' + || c == '-' + || c == '.' + || c == '_' + || c == '~'; + } + + private static bool isUnreservedInRfc3986 (char c) + { + return c == '-' + || c == '.' + || c == '_' + || c == '~'; + } + + private static byte[] urlDecodeToBytes (byte[] bytes, int offset, int count) + { + using (var buff = new MemoryStream ()) { + var end = offset + count - 1; + for (var i = offset; i <= end; i++) { + var b = bytes[i]; + + var c = (char) b; + if (c == '%') { + if (i > end - 2) + break; + + var num = getNumber (bytes, i + 1, 2); + if (num == -1) + break; + + buff.WriteByte ((byte) num); + i += 2; + + continue; + } + + if (c == '+') { + buff.WriteByte ((byte) ' '); + continue; + } + + buff.WriteByte (b); + } + + buff.Close (); + return buff.ToArray (); + } + } + + private static void urlEncode (byte b, Stream output) + { + if (b > 31 && b < 127) { + var c = (char) b; + if (c == ' ') { + output.WriteByte ((byte) '+'); + return; + } + + if (isNumeric (c)) { + output.WriteByte (b); + return; + } + + if (isAlphabet (c)) { + output.WriteByte (b); + return; + } + + if (isUnreserved (c)) { + output.WriteByte (b); + return; + } + } + + var i = (int) b; + var bytes = new byte[] { + (byte) '%', + (byte) _hexChars[i >> 4], + (byte) _hexChars[i & 0x0F] + }; + + output.Write (bytes, 0, 3); + } + + private static byte[] urlEncodeToBytes (byte[] bytes, int offset, int count) + { + using (var buff = new MemoryStream ()) { + var end = offset + count - 1; + for (var i = offset; i <= end; i++) + urlEncode (bytes[i], buff); + + buff.Close (); + return buff.ToArray (); + } + } + + #endregion + + #region Internal Methods + + internal static Uri CreateRequestUrl ( + string requestUri, string host, bool websocketRequest, bool secure + ) + { + if (requestUri == null || requestUri.Length == 0) + return null; + + if (host == null || host.Length == 0) + return null; + + string schm = null; + string path = null; + + if (requestUri.IndexOf ('/') == 0) { + path = requestUri; + } + else if (requestUri.MaybeUri ()) { + Uri uri; + 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; + + host = uri.Authority; + path = uri.PathAndQuery; + } + else if (requestUri == "*") { + } + else { + // As the authority form. + host = requestUri; + } + + if (schm == null) { + schm = websocketRequest + ? (secure ? "wss" : "ws") + : (secure ? "https" : "http"); + } + + 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; + return Uri.TryCreate (url, UriKind.Absolute, out ret) ? ret : null; + } + + internal static IPrincipal CreateUser ( + string response, + AuthenticationSchemes scheme, + string realm, + string method, + Func credentialsFinder + ) + { + 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.IndexOf (scheme.ToString (), compType) != 0) + 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); + } + catch { + } + + if (cred == null) + return null; + + if (scheme == AuthenticationSchemes.Basic) { + var basicId = (HttpBasicIdentity) id; + 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 name = "charset="; + var compType = StringComparison.OrdinalIgnoreCase; + + foreach (var elm in contentType.SplitHeaderValue (';')) { + var part = elm.Trim (); + if (part.IndexOf (name, compType) != 0) + continue; + + var val = part.GetValue ('=', true); + if (val == null || val.Length == 0) + return null; + + return Encoding.GetEncoding (val); + } + + return null; + } + + internal static bool TryGetEncoding ( + string contentType, out Encoding result + ) + { + result = null; + + try { + result = GetEncoding (contentType); + } + catch { + return false; + } + + return result != null; + } + + #endregion + + #region Public Methods + + public static string HtmlAttributeEncode (string s) + { + if (s == null) + throw new ArgumentNullException ("s"); + + 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"); + + if (s.Length == 0) + return; + + output.Write (htmlEncode (s, true)); + } + + public static string HtmlDecode (string s) + { + if (s == null) + throw new ArgumentNullException ("s"); + + return s.Length > 0 ? htmlDecode (s) : s; + } + + public static void HtmlDecode (string s, TextWriter output) + { + if (s == null) + throw new ArgumentNullException ("s"); + + if (output == null) + throw new ArgumentNullException ("output"); + + if (s.Length == 0) + return; + + output.Write (htmlDecode (s)); + } + + public static string HtmlEncode (string s) + { + if (s == null) + throw new ArgumentNullException ("s"); + + return s.Length > 0 ? htmlEncode (s, false) : s; + } + + public static void HtmlEncode (string s, TextWriter output) + { + if (s == null) + throw new ArgumentNullException ("s"); + + if (output == null) + throw new ArgumentNullException ("output"); + + if (s.Length == 0) + return; + + output.Write (htmlEncode (s, false)); + } + + public static string UrlDecode (string s) + { + return UrlDecode (s, Encoding.UTF8); + } + + public static string UrlDecode (byte[] bytes, Encoding encoding) + { + if (bytes == null) + throw new ArgumentNullException ("bytes"); + + var len = bytes.Length; + return len > 0 + ? (encoding ?? Encoding.UTF8).GetString ( + urlDecodeToBytes (bytes, 0, len) + ) + : String.Empty; + } + + public static string UrlDecode (string s, Encoding encoding) + { + if (s == null) + throw new ArgumentNullException ("s"); + + if (s.Length == 0) + return s; + + var bytes = Encoding.ASCII.GetBytes (s); + return (encoding ?? Encoding.UTF8).GetString ( + urlDecodeToBytes (bytes, 0, bytes.Length) + ); + } + + public static string UrlDecode ( + byte[] bytes, int offset, int count, Encoding encoding + ) + { + 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"); + + return count > 0 + ? (encoding ?? Encoding.UTF8).GetString ( + urlDecodeToBytes (bytes, offset, count) + ) + : String.Empty; + } + + public static byte[] UrlDecodeToBytes (byte[] bytes) + { + if (bytes == null) + throw new ArgumentNullException ("bytes"); + + var len = bytes.Length; + return len > 0 + ? urlDecodeToBytes (bytes, 0, len) + : bytes; + } + + public static byte[] UrlDecodeToBytes (string s) + { + if (s == null) + throw new ArgumentNullException ("s"); + + if (s.Length == 0) + return new byte[0]; + + var bytes = Encoding.ASCII.GetBytes (s); + return urlDecodeToBytes (bytes, 0, bytes.Length); + } + + public static byte[] UrlDecodeToBytes (byte[] bytes, int offset, int count) + { + 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 bytes; + } + + if (offset < 0 || offset >= len) + throw new ArgumentOutOfRangeException ("offset"); + + if (count < 0 || count > len - offset) + throw new ArgumentOutOfRangeException ("count"); + + return count > 0 + ? urlDecodeToBytes (bytes, offset, count) + : new byte[0]; + } + + public static string UrlEncode (byte[] bytes) + { + if (bytes == null) + throw new ArgumentNullException ("bytes"); + + var len = bytes.Length; + return len > 0 + ? Encoding.ASCII.GetString (urlEncodeToBytes (bytes, 0, len)) + : String.Empty; + } + + public static string UrlEncode (string s) + { + return UrlEncode (s, Encoding.UTF8); + } + + public static string UrlEncode (string s, Encoding encoding) + { + if (s == null) + throw new ArgumentNullException ("s"); + + var len = s.Length; + if (len == 0) + return s; + + if (encoding == null) + encoding = Encoding.UTF8; + + var bytes = new byte[encoding.GetMaxByteCount (len)]; + var realLen = encoding.GetBytes (s, 0, len, bytes, 0); + + return Encoding.ASCII.GetString (urlEncodeToBytes (bytes, 0, realLen)); + } + + public static string UrlEncode (byte[] bytes, int offset, int count) + { + 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"); + + return count > 0 + ? Encoding.ASCII.GetString ( + urlEncodeToBytes (bytes, offset, count) + ) + : String.Empty; + } + + public static byte[] UrlEncodeToBytes (byte[] 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) + { + return UrlEncodeToBytes (s, Encoding.UTF8); + } + + public static byte[] UrlEncodeToBytes (string s, Encoding encoding) + { + if (s == null) + throw new ArgumentNullException ("s"); + + if (s.Length == 0) + return new byte[0]; + + var bytes = (encoding ?? Encoding.UTF8).GetBytes (s); + return urlEncodeToBytes (bytes, 0, bytes.Length); + } + + public static byte[] UrlEncodeToBytes (byte[] bytes, int offset, int count) + { + 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 bytes; + } + + if (offset < 0 || offset >= len) + throw new ArgumentOutOfRangeException ("offset"); + + if (count < 0 || count > len - offset) + throw new ArgumentOutOfRangeException ("count"); + + return count > 0 ? urlEncodeToBytes (bytes, offset, count) : new byte[0]; + } + + #endregion + } +} diff --git a/websocket-sharp-core/Net/HttpVersion.cs b/websocket-sharp-core/Net/HttpVersion.cs new file mode 100644 index 000000000..d20061e0b --- /dev/null +++ b/websocket-sharp-core/Net/HttpVersion.cs @@ -0,0 +1,73 @@ +#region License +/* + * HttpVersion.cs + * + * This code is derived from System.Net.HttpVersion.cs of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2012-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: + * - Lawrence Pit + */ +#endregion + +using System; + +namespace WebSocketSharp.Net +{ + /// + /// Provides the HTTP version numbers. + /// + public class HttpVersion + { + #region Public Fields + + /// + /// Provides a instance for the HTTP/1.0. + /// + public static readonly Version Version10 = new Version (1, 0); + + /// + /// Provides a instance for the HTTP/1.1. + /// + public static readonly Version Version11 = new Version (1, 1); + + #endregion + + #region Public Constructors + + /// + /// Initializes a new instance of the class. + /// + public HttpVersion () + { + } + + #endregion + } +} diff --git a/websocket-sharp-core/Net/InputChunkState.cs b/websocket-sharp-core/Net/InputChunkState.cs new file mode 100644 index 000000000..f50ad6b7a --- /dev/null +++ b/websocket-sharp-core/Net/InputChunkState.cs @@ -0,0 +1,52 @@ +#region License +/* + * InputChunkState.cs + * + * This code is derived from ChunkStream.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2003 Ximian, Inc (http://www.ximian.com) + * Copyright (c) 2014-2015 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: + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; + +namespace WebSocketSharp.Net +{ + internal enum InputChunkState + { + None, + Data, + DataEnded, + Trailer, + End + } +} diff --git a/websocket-sharp-core/Net/InputState.cs b/websocket-sharp-core/Net/InputState.cs new file mode 100644 index 000000000..9f566d246 --- /dev/null +++ b/websocket-sharp-core/Net/InputState.cs @@ -0,0 +1,49 @@ +#region License +/* + * InputState.cs + * + * This code is derived from HttpConnection.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-2015 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: + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; + +namespace WebSocketSharp.Net +{ + internal enum InputState + { + RequestLine, + Headers + } +} diff --git a/websocket-sharp-core/Net/LineState.cs b/websocket-sharp-core/Net/LineState.cs new file mode 100644 index 000000000..84e271a7b --- /dev/null +++ b/websocket-sharp-core/Net/LineState.cs @@ -0,0 +1,50 @@ +#region License +/* + * LineState.cs + * + * This code is derived from HttpConnection.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-2015 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: + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; + +namespace WebSocketSharp.Net +{ + internal enum LineState + { + None, + Cr, + Lf + } +} diff --git a/websocket-sharp-core/Net/NetworkCredential.cs b/websocket-sharp-core/Net/NetworkCredential.cs new file mode 100644 index 000000000..3ee52f402 --- /dev/null +++ b/websocket-sharp-core/Net/NetworkCredential.cs @@ -0,0 +1,209 @@ +#region License +/* + * NetworkCredential.cs + * + * The MIT License + * + * 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 + * 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; + +namespace WebSocketSharp.Net +{ + /// + /// Provides the credentials for the password-based authentication. + /// + public class NetworkCredential + { + #region Private Fields + + 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 + + #region Public Constructors + + /// + /// Initializes a new instance of the class with + /// the specified and . + /// + /// + /// A that represents the username associated with + /// the credentials. + /// + /// + /// A that represents the password for the username + /// associated with the credentials. + /// + /// + /// is . + /// + /// + /// is empty. + /// + public NetworkCredential (string username, string password) + : this (username, password, null, null) + { + } + + /// + /// Initializes a new instance of the class with + /// the specified , , + /// and . + /// + /// + /// A that represents the username associated with + /// the credentials. + /// + /// + /// A that represents the password for the username + /// associated with the credentials. + /// + /// + /// A that represents the domain associated with + /// the credentials. + /// + /// + /// An array of that represents the roles + /// associated with the credentials if any. + /// + /// + /// is . + /// + /// + /// is empty. + /// + public NetworkCredential ( + string username, string password, string domain, params string[] roles + ) + { + if (username == null) + throw new ArgumentNullException ("username"); + + if (username.Length == 0) + throw new ArgumentException ("An empty string.", "username"); + + _username = username; + _password = password; + _domain = domain; + _roles = roles; + } + + #endregion + + #region Public Properties + + /// + /// Gets the domain associated with the credentials. + /// + /// + /// This property returns an empty string if the domain was + /// initialized with . + /// + /// + /// A that represents the domain name + /// to which the username belongs. + /// + public string Domain { + get { + return _domain ?? String.Empty; + } + + internal set { + _domain = value; + } + } + + /// + /// Gets the password for the username associated with the credentials. + /// + /// + /// This property returns an empty string if the password was + /// initialized with . + /// + /// + /// A that represents the password. + /// + public string Password { + get { + return _password ?? String.Empty; + } + + internal set { + _password = value; + } + } + + /// + /// Gets the roles associated with the credentials. + /// + /// + /// This property returns an empty array if the roles were + /// initialized with . + /// + /// + /// An array of that represents the role names + /// to which the username belongs. + /// + public string[] Roles { + get { + return _roles ?? _noRoles; + } + + internal set { + _roles = value; + } + } + + /// + /// Gets the username associated with the credentials. + /// + /// + /// A that represents the username. + /// + public string Username { + get { + return _username; + } + + internal set { + _username = value; + } + } + + #endregion + } +} diff --git a/websocket-sharp-core/Net/QueryStringCollection.cs b/websocket-sharp-core/Net/QueryStringCollection.cs new file mode 100644 index 000000000..2e925e2d1 --- /dev/null +++ b/websocket-sharp-core/Net/QueryStringCollection.cs @@ -0,0 +1,150 @@ +#region License +/* + * QueryStringCollection.cs + * + * 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) 2018 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: + * - Patrik Torstensson + * - Wictor Wilén (decode/encode functions) + * - Tim Coleman + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; +using System.Collections.Specialized; +using System.Text; + +namespace WebSocketSharp.Net +{ + internal sealed class QueryStringCollection : NameValueCollection + { + #region Public Constructors + + public QueryStringCollection () + { + } + + public QueryStringCollection (int capacity) + : base (capacity) + { + } + + #endregion + + #region Private Methods + + private static string urlDecode (string s, Encoding encoding) + { + return s.IndexOfAny (new[] { '%', '+' }) > -1 + ? HttpUtility.UrlDecode (s, encoding) + : s; + } + + #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); + + var len = query.Length; + if (len == 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 (); + + var components = query.Split ('&'); + foreach (var component in components) { + len = component.Length; + if (len == 0) + continue; + + if (component == "=") + continue; + + var i = component.IndexOf ('='); + if (i < 0) { + ret.Add (null, urlDecode (component, encoding)); + continue; + } + + if (i == 0) { + ret.Add (null, urlDecode (component.Substring (1), encoding)); + continue; + } + + var name = urlDecode (component.Substring (0, i), encoding); + + var start = i + 1; + var val = start < len + ? urlDecode (component.Substring (start), encoding) + : String.Empty; + + ret.Add (name, val); + } + + return ret; + } + + public override string ToString () + { + var buff = new StringBuilder (); + + foreach (var key in AllKeys) + buff.AppendFormat ("{0}={1}&", key, this[key]); + + if (buff.Length > 0) + buff.Length--; + + return buff.ToString (); + } + + #endregion + } +} diff --git a/websocket-sharp-core/Net/ReadBufferState.cs b/websocket-sharp-core/Net/ReadBufferState.cs new file mode 100644 index 000000000..780a69b5a --- /dev/null +++ b/websocket-sharp-core/Net/ReadBufferState.cs @@ -0,0 +1,124 @@ +#region License +/* + * ReadBufferState.cs + * + * This code is derived from ChunkedInputStream.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-2015 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: + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; + +namespace WebSocketSharp.Net +{ + internal class ReadBufferState + { + #region Private Fields + + private HttpStreamAsyncResult _asyncResult; + private byte[] _buffer; + private int _count; + private int _initialCount; + private int _offset; + + #endregion + + #region Public Constructors + + public ReadBufferState ( + byte[] buffer, int offset, int count, HttpStreamAsyncResult asyncResult) + { + _buffer = buffer; + _offset = offset; + _count = count; + _initialCount = count; + _asyncResult = asyncResult; + } + + #endregion + + #region Public Properties + + public HttpStreamAsyncResult AsyncResult { + get { + return _asyncResult; + } + + set { + _asyncResult = value; + } + } + + public byte[] Buffer { + get { + return _buffer; + } + + set { + _buffer = value; + } + } + + public int Count { + get { + return _count; + } + + set { + _count = value; + } + } + + public int InitialCount { + get { + return _initialCount; + } + + set { + _initialCount = value; + } + } + + public int Offset { + get { + return _offset; + } + + set { + _offset = value; + } + } + + #endregion + } +} diff --git a/websocket-sharp-core/Net/RequestStream.cs b/websocket-sharp-core/Net/RequestStream.cs new file mode 100644 index 000000000..dd40b3784 --- /dev/null +++ b/websocket-sharp-core/Net/RequestStream.cs @@ -0,0 +1,267 @@ +#region License +/* + * RequestStream.cs + * + * This code is derived from RequestStream.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-2015 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: + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; +using System.IO; + +namespace WebSocketSharp.Net +{ + 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; + + #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 = stream; + _buffer = buffer; + _offset = offset; + _count = count; + _bodyLeft = contentLength; + } + + #endregion + + #region Public Properties + + public override bool CanRead { + get { + return true; + } + } + + public override bool CanSeek { + get { + return false; + } + } + + public override bool CanWrite { + get { + return false; + } + } + + public override long Length { + get { + throw new NotSupportedException (); + } + } + + public override long Position { + get { + throw new NotSupportedException (); + } + + set { + throw new NotSupportedException (); + } + } + + #endregion + + #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) + { + 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."); + + if (_bodyLeft == 0) + return -1; + + if (_count == 0 || count == 0) + return 0; + + if (count > _count) + count = _count; + + if (_bodyLeft > 0 && count > _bodyLeft) + count = (int) _bodyLeft; + + Buffer.BlockCopy (_buffer, _offset, buffer, offset, count); + _offset += count; + _count -= count; + if (_bodyLeft > 0) + _bodyLeft -= count; + + return count; + } + + #endregion + + #region Public Methods + + public override IAsyncResult BeginRead ( + byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + var nread = fillFromBuffer (buffer, offset, count); + if (nread > 0 || nread == -1) { + 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) + count = (int) _bodyLeft; + + return _stream.BeginRead (buffer, offset, count, callback, state); + } + + public override IAsyncResult BeginWrite ( + byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + throw new NotSupportedException (); + } + + public override void Close () + { + _disposed = true; + } + + public override int EndRead (IAsyncResult asyncResult) + { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + 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); + if (nread > 0 && _bodyLeft > 0) + _bodyLeft -= nread; + + return nread; + } + + public override void EndWrite (IAsyncResult asyncResult) + { + throw new NotSupportedException (); + } + + public override void Flush () + { + } + + public override int Read (byte[] buffer, int offset, int count) + { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + // 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). + return 0; + + if (nread > 0) + return nread; + + nread = _stream.Read (buffer, offset, count); + if (nread > 0 && _bodyLeft > 0) + _bodyLeft -= nread; + + return nread; + } + + public override long Seek (long offset, SeekOrigin origin) + { + throw new NotSupportedException (); + } + + public override void SetLength (long value) + { + throw new NotSupportedException (); + } + + public override void Write (byte[] buffer, int offset, int count) + { + throw new NotSupportedException (); + } + + #endregion + } +} diff --git a/websocket-sharp-core/Net/ResponseStream.cs b/websocket-sharp-core/Net/ResponseStream.cs new file mode 100644 index 000000000..0939dfbd3 --- /dev/null +++ b/websocket-sharp-core/Net/ResponseStream.cs @@ -0,0 +1,338 @@ +#region License +/* + * ResponseStream.cs + * + * This code is derived from ResponseStream.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-2015 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: + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; +using System.IO; +using System.Text; + +namespace WebSocketSharp.Net +{ + internal class ResponseStream : Stream + { + #region Private Fields + + private MemoryStream _body; + private static readonly byte[] _crlf = new byte[] { 13, 10 }; + private bool _disposed; + private HttpListenerResponse _response; + private bool _sendChunked; + private Stream _stream; + private Action _write; + private Action _writeBody; + private Action _writeChunked; + + #endregion + + #region Internal Constructors + + internal ResponseStream ( + Stream stream, HttpListenerResponse response, bool ignoreWriteExceptions) + { + _stream = stream; + _response = response; + + if (ignoreWriteExceptions) { + _write = writeWithoutThrowingException; + _writeChunked = writeChunkedWithoutThrowingException; + } + else { + _write = stream.Write; + _writeChunked = writeChunked; + } + + _body = new MemoryStream (); + } + + #endregion + + #region Public Properties + + public override bool CanRead { + get { + return false; + } + } + + public override bool CanSeek { + get { + return false; + } + } + + public override bool CanWrite { + get { + return !_disposed; + } + } + + public override long Length { + get { + throw new NotSupportedException (); + } + } + + public override long Position { + get { + throw new NotSupportedException (); + } + + set { + throw new NotSupportedException (); + } + } + + #endregion + + #region Private Methods + + private bool flush (bool closing) + { + if (!_response.HeadersSent) { + if (!flushHeaders (closing)) { + if (closing) + _response.CloseConnection = true; + + return false; + } + + _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; + if (len > Int32.MaxValue) { + _body.Position = 0; + var buffLen = 1024; + var buff = new byte[buffLen]; + var nread = 0; + while ((nread = _body.Read (buff, 0, buffLen)) > 0) + _writeBody (buff, 0, nread); + } + else if (len > 0) { + _writeBody (_body.GetBuffer (), 0, (int) len); + } + } + + _body = !closing ? new MemoryStream () : null; + } + + private bool flushHeaders (bool closing) + { + if (!_response.SendChunked) { + if (_response.ContentLength64 != _body.Length) + return false; + } + + var statusLine = _response.StatusLine; + var headers = _response.FullHeaders; + + var buff = new MemoryStream (); + var enc = Encoding.UTF8; + + using (var writer = new StreamWriter (buff, enc, 256)) { + writer.Write (statusLine); + writer.Write (headers.ToStringMultiValue (true)); + writer.Flush (); + + var start = enc.GetPreamble ().Length; + var len = buff.Length - start; + + if (len > 32768) + return false; + + _write (buff.GetBuffer (), start, (int) len); + } + + _response.CloseConnection = headers["Connection"] == "close"; + _response.HeadersSent = true; + + return true; + } + + private static byte[] getChunkSizeBytes (int size, bool final) + { + return Encoding.ASCII.GetBytes (String.Format ("{0:x}\r\n{1}", size, final ? "\r\n" : "")); + } + + 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); + } + + private void writeChunkedWithoutThrowingException (byte[] buffer, int offset, int count) + { + try { + writeChunked (buffer, offset, count); + } + catch { + } + } + + private void writeWithoutThrowingException (byte[] buffer, int offset, int count) + { + try { + _stream.Write (buffer, offset, count); + } + catch { + } + } + + #endregion + + #region Internal Methods + + internal void Close (bool force) + { + if (_disposed) + 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; + + _response.Abort (); + } + + _response = null; + _stream = null; + } + + internal void InternalWrite (byte[] buffer, int offset, int count) + { + _write (buffer, offset, count); + } + + #endregion + + #region Public Methods + + public override IAsyncResult BeginRead ( + 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) + { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + return _body.BeginWrite (buffer, offset, count, callback, state); + } + + public override void Close () + { + Close (false); + } + + protected override void Dispose (bool disposing) + { + Close (!disposing); + } + + public override int EndRead (IAsyncResult asyncResult) + { + throw new NotSupportedException (); + } + + public override void EndWrite (IAsyncResult asyncResult) + { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + _body.EndWrite (asyncResult); + } + + public override void Flush () + { + if (!_disposed && (_sendChunked || _response.SendChunked)) + flush (false); + } + + public override int Read (byte[] buffer, int offset, int count) + { + throw new NotSupportedException (); + } + + public override long Seek (long offset, SeekOrigin origin) + { + throw new NotSupportedException (); + } + + public override void SetLength (long value) + { + throw new NotSupportedException (); + } + + public override void Write (byte[] buffer, int offset, int count) + { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + _body.Write (buffer, offset, count); + } + + #endregion + } +} diff --git a/websocket-sharp-core/Net/ServerSslConfiguration.cs b/websocket-sharp-core/Net/ServerSslConfiguration.cs new file mode 100644 index 000000000..ad9b9e7c2 --- /dev/null +++ b/websocket-sharp-core/Net/ServerSslConfiguration.cs @@ -0,0 +1,245 @@ +#region License +/* + * ServerSslConfiguration.cs + * + * The MIT License + * + * Copyright (c) 2014 liryna + * 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 + * 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; +using System.Net.Security; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; + +namespace WebSocketSharp.Net +{ + /// + /// Stores the parameters for the used by servers. + /// + public class ServerSslConfiguration + { + #region Private Fields + + 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. + /// + public ServerSslConfiguration () + { + _enabledSslProtocols = SslProtocols.Default; + } + + /// + /// Initializes a new instance of the class + /// with the specified . + /// + /// + /// A that represents the certificate used to + /// authenticate the server. + /// + public ServerSslConfiguration (X509Certificate2 serverCertificate) + { + _serverCert = serverCertificate; + _enabledSslProtocols = SslProtocols.Default; + } + + /// + /// Copies the parameters from the specified to + /// a new instance of the class. + /// + /// + /// A from which to copy. + /// + /// + /// is . + /// + public ServerSslConfiguration (ServerSslConfiguration configuration) + { + if (configuration == null) + throw new ArgumentNullException ("configuration"); + + _checkCertRevocation = configuration._checkCertRevocation; + _clientCertRequired = configuration._clientCertRequired; + _clientCertValidationCallback = configuration._clientCertValidationCallback; + _enabledSslProtocols = configuration._enabledSslProtocols; + _serverCert = configuration._serverCert; + } + + #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 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 the client is asked for + /// a certificate for authentication. + /// + /// + /// + /// true if the client is asked for a certificate for + /// authentication; otherwise, false. + /// + /// + /// The default value is false. + /// + /// + public bool ClientCertificateRequired { + get { + return _clientCertRequired; + } + + set { + _clientCertRequired = value; + } + } + + /// + /// Gets or sets the callback used to validate the certificate + /// supplied by the client. + /// + /// + /// The certificate is valid if the callback returns true. + /// + /// + /// + /// A delegate that + /// invokes the method called for validating the certificate. + /// + /// + /// The default value is a delegate that invokes a method that + /// only returns true. + /// + /// + public RemoteCertificateValidationCallback ClientCertificateValidationCallback { + get { + if (_clientCertValidationCallback == null) + _clientCertValidationCallback = defaultValidateClientCertificate; + + return _clientCertValidationCallback; + } + + set { + _clientCertValidationCallback = value; + } + } + + /// + /// Gets or sets the protocols used for authentication. + /// + /// + /// + /// The enum values that represent + /// the protocols used for authentication. + /// + /// + /// The default value is . + /// + /// + public SslProtocols EnabledSslProtocols { + get { + return _enabledSslProtocols; + } + + set { + _enabledSslProtocols = value; + } + } + + /// + /// Gets or sets the certificate used to authenticate the server. + /// + /// + /// + /// A or + /// if not specified. + /// + /// + /// That instance represents an X.509 certificate. + /// + /// + public X509Certificate2 ServerCertificate { + get { + return _serverCert; + } + + set { + _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-core/Net/WebHeaderCollection.cs b/websocket-sharp-core/Net/WebHeaderCollection.cs new file mode 100644 index 000000000..8423d2f17 --- /dev/null +++ b/websocket-sharp-core/Net/WebHeaderCollection.cs @@ -0,0 +1,1459 @@ +#region License +/* + * WebHeaderCollection.cs + * + * This code is derived from WebHeaderCollection.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2003 Ximian, Inc. (http://www.ximian.com) + * Copyright (c) 2007 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2012-2015 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: + * - Lawrence Pit + * - Gonzalo Paniagua Javier + * - Miguel de Icaza + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Runtime.InteropServices; +using System.Runtime.Serialization; +using System.Security.Permissions; +using System.Text; + +namespace WebSocketSharp.Net +{ + /// + /// Provides a collection of the HTTP headers associated with a request or response. + /// + [Serializable] + [ComVisible (true)] + public class WebHeaderCollection : NameValueCollection, ISerializable + { + #region Private Fields + + private static readonly Dictionary _headers; + private bool _internallyUsed; + private HttpHeaderType _state; + + #endregion + + #region Static Constructor + + static WebHeaderCollection () + { + _headers = + new Dictionary (StringComparer.InvariantCultureIgnoreCase) { + { + "Accept", + new HttpHeaderInfo ( + "Accept", + HttpHeaderType.Request | HttpHeaderType.Restricted | HttpHeaderType.MultiValue) + }, + { + "AcceptCharset", + new HttpHeaderInfo ( + "Accept-Charset", + HttpHeaderType.Request | HttpHeaderType.MultiValue) + }, + { + "AcceptEncoding", + new HttpHeaderInfo ( + "Accept-Encoding", + HttpHeaderType.Request | HttpHeaderType.MultiValue) + }, + { + "AcceptLanguage", + new HttpHeaderInfo ( + "Accept-Language", + HttpHeaderType.Request | HttpHeaderType.MultiValue) + }, + { + "AcceptRanges", + new HttpHeaderInfo ( + "Accept-Ranges", + HttpHeaderType.Response | HttpHeaderType.MultiValue) + }, + { + "Age", + new HttpHeaderInfo ( + "Age", + HttpHeaderType.Response) + }, + { + "Allow", + new HttpHeaderInfo ( + "Allow", + HttpHeaderType.Request | HttpHeaderType.Response | HttpHeaderType.MultiValue) + }, + { + "Authorization", + new HttpHeaderInfo ( + "Authorization", + HttpHeaderType.Request | HttpHeaderType.MultiValue) + }, + { + "CacheControl", + new HttpHeaderInfo ( + "Cache-Control", + HttpHeaderType.Request | HttpHeaderType.Response | HttpHeaderType.MultiValue) + }, + { + "Connection", + new HttpHeaderInfo ( + "Connection", + HttpHeaderType.Request | + HttpHeaderType.Response | + HttpHeaderType.Restricted | + HttpHeaderType.MultiValue) + }, + { + "ContentEncoding", + new HttpHeaderInfo ( + "Content-Encoding", + HttpHeaderType.Request | HttpHeaderType.Response | HttpHeaderType.MultiValue) + }, + { + "ContentLanguage", + new HttpHeaderInfo ( + "Content-Language", + HttpHeaderType.Request | HttpHeaderType.Response | HttpHeaderType.MultiValue) + }, + { + "ContentLength", + new HttpHeaderInfo ( + "Content-Length", + HttpHeaderType.Request | HttpHeaderType.Response | HttpHeaderType.Restricted) + }, + { + "ContentLocation", + new HttpHeaderInfo ( + "Content-Location", + HttpHeaderType.Request | HttpHeaderType.Response) + }, + { + "ContentMd5", + new HttpHeaderInfo ( + "Content-MD5", + HttpHeaderType.Request | HttpHeaderType.Response) + }, + { + "ContentRange", + new HttpHeaderInfo ( + "Content-Range", + HttpHeaderType.Request | HttpHeaderType.Response) + }, + { + "ContentType", + new HttpHeaderInfo ( + "Content-Type", + HttpHeaderType.Request | HttpHeaderType.Response | HttpHeaderType.Restricted) + }, + { + "Cookie", + new HttpHeaderInfo ( + "Cookie", + HttpHeaderType.Request) + }, + { + "Cookie2", + new HttpHeaderInfo ( + "Cookie2", + HttpHeaderType.Request) + }, + { + "Date", + new HttpHeaderInfo ( + "Date", + HttpHeaderType.Request | HttpHeaderType.Response | HttpHeaderType.Restricted) + }, + { + "Expect", + new HttpHeaderInfo ( + "Expect", + HttpHeaderType.Request | HttpHeaderType.Restricted | HttpHeaderType.MultiValue) + }, + { + "Expires", + new HttpHeaderInfo ( + "Expires", + HttpHeaderType.Request | HttpHeaderType.Response) + }, + { + "ETag", + new HttpHeaderInfo ( + "ETag", + HttpHeaderType.Response) + }, + { + "From", + new HttpHeaderInfo ( + "From", + HttpHeaderType.Request) + }, + { + "Host", + new HttpHeaderInfo ( + "Host", + HttpHeaderType.Request | HttpHeaderType.Restricted) + }, + { + "IfMatch", + new HttpHeaderInfo ( + "If-Match", + HttpHeaderType.Request | HttpHeaderType.MultiValue) + }, + { + "IfModifiedSince", + new HttpHeaderInfo ( + "If-Modified-Since", + HttpHeaderType.Request | HttpHeaderType.Restricted) + }, + { + "IfNoneMatch", + new HttpHeaderInfo ( + "If-None-Match", + HttpHeaderType.Request | HttpHeaderType.MultiValue) + }, + { + "IfRange", + new HttpHeaderInfo ( + "If-Range", + HttpHeaderType.Request) + }, + { + "IfUnmodifiedSince", + new HttpHeaderInfo ( + "If-Unmodified-Since", + HttpHeaderType.Request) + }, + { + "KeepAlive", + new HttpHeaderInfo ( + "Keep-Alive", + HttpHeaderType.Request | HttpHeaderType.Response | HttpHeaderType.MultiValue) + }, + { + "LastModified", + new HttpHeaderInfo ( + "Last-Modified", + HttpHeaderType.Request | HttpHeaderType.Response) + }, + { + "Location", + new HttpHeaderInfo ( + "Location", + HttpHeaderType.Response) + }, + { + "MaxForwards", + new HttpHeaderInfo ( + "Max-Forwards", + HttpHeaderType.Request) + }, + { + "Pragma", + new HttpHeaderInfo ( + "Pragma", + HttpHeaderType.Request | HttpHeaderType.Response) + }, + { + "ProxyAuthenticate", + new HttpHeaderInfo ( + "Proxy-Authenticate", + HttpHeaderType.Response | HttpHeaderType.MultiValue) + }, + { + "ProxyAuthorization", + new HttpHeaderInfo ( + "Proxy-Authorization", + HttpHeaderType.Request) + }, + { + "ProxyConnection", + new HttpHeaderInfo ( + "Proxy-Connection", + HttpHeaderType.Request | HttpHeaderType.Response | HttpHeaderType.Restricted) + }, + { + "Public", + new HttpHeaderInfo ( + "Public", + HttpHeaderType.Response | HttpHeaderType.MultiValue) + }, + { + "Range", + new HttpHeaderInfo ( + "Range", + HttpHeaderType.Request | HttpHeaderType.Restricted | HttpHeaderType.MultiValue) + }, + { + "Referer", + new HttpHeaderInfo ( + "Referer", + HttpHeaderType.Request | HttpHeaderType.Restricted) + }, + { + "RetryAfter", + new HttpHeaderInfo ( + "Retry-After", + HttpHeaderType.Response) + }, + { + "SecWebSocketAccept", + new HttpHeaderInfo ( + "Sec-WebSocket-Accept", + HttpHeaderType.Response | HttpHeaderType.Restricted) + }, + { + "SecWebSocketExtensions", + new HttpHeaderInfo ( + "Sec-WebSocket-Extensions", + HttpHeaderType.Request | + HttpHeaderType.Response | + HttpHeaderType.Restricted | + HttpHeaderType.MultiValueInRequest) + }, + { + "SecWebSocketKey", + new HttpHeaderInfo ( + "Sec-WebSocket-Key", + HttpHeaderType.Request | HttpHeaderType.Restricted) + }, + { + "SecWebSocketProtocol", + new HttpHeaderInfo ( + "Sec-WebSocket-Protocol", + HttpHeaderType.Request | HttpHeaderType.Response | HttpHeaderType.MultiValueInRequest) + }, + { + "SecWebSocketVersion", + new HttpHeaderInfo ( + "Sec-WebSocket-Version", + HttpHeaderType.Request | + HttpHeaderType.Response | + HttpHeaderType.Restricted | + HttpHeaderType.MultiValueInResponse) + }, + { + "Server", + new HttpHeaderInfo ( + "Server", + HttpHeaderType.Response) + }, + { + "SetCookie", + new HttpHeaderInfo ( + "Set-Cookie", + HttpHeaderType.Response | HttpHeaderType.MultiValue) + }, + { + "SetCookie2", + new HttpHeaderInfo ( + "Set-Cookie2", + HttpHeaderType.Response | HttpHeaderType.MultiValue) + }, + { + "Te", + new HttpHeaderInfo ( + "TE", + HttpHeaderType.Request) + }, + { + "Trailer", + new HttpHeaderInfo ( + "Trailer", + HttpHeaderType.Request | HttpHeaderType.Response) + }, + { + "TransferEncoding", + new HttpHeaderInfo ( + "Transfer-Encoding", + HttpHeaderType.Request | + HttpHeaderType.Response | + HttpHeaderType.Restricted | + HttpHeaderType.MultiValue) + }, + { + "Translate", + new HttpHeaderInfo ( + "Translate", + HttpHeaderType.Request) + }, + { + "Upgrade", + new HttpHeaderInfo ( + "Upgrade", + HttpHeaderType.Request | HttpHeaderType.Response | HttpHeaderType.MultiValue) + }, + { + "UserAgent", + new HttpHeaderInfo ( + "User-Agent", + HttpHeaderType.Request | HttpHeaderType.Restricted) + }, + { + "Vary", + new HttpHeaderInfo ( + "Vary", + HttpHeaderType.Response | HttpHeaderType.MultiValue) + }, + { + "Via", + new HttpHeaderInfo ( + "Via", + HttpHeaderType.Request | HttpHeaderType.Response | HttpHeaderType.MultiValue) + }, + { + "Warning", + new HttpHeaderInfo ( + "Warning", + HttpHeaderType.Request | HttpHeaderType.Response | HttpHeaderType.MultiValue) + }, + { + "WwwAuthenticate", + new HttpHeaderInfo ( + "WWW-Authenticate", + HttpHeaderType.Response | HttpHeaderType.Restricted | HttpHeaderType.MultiValue) + } + }; + } + + #endregion + + #region Internal Constructors + + internal WebHeaderCollection (HttpHeaderType state, bool internallyUsed) + { + _state = state; + _internallyUsed = internallyUsed; + } + + #endregion + + #region Protected Constructors + + /// + /// Initializes a new instance of the class from + /// the specified and . + /// + /// + /// A that contains the serialized object data. + /// + /// + /// A that specifies the source for the deserialization. + /// + /// + /// is . + /// + /// + /// An element with the specified name isn't found in . + /// + protected WebHeaderCollection ( + SerializationInfo serializationInfo, StreamingContext streamingContext) + { + if (serializationInfo == null) + throw new ArgumentNullException ("serializationInfo"); + + try { + _internallyUsed = serializationInfo.GetBoolean ("InternallyUsed"); + _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 ())); + } + } + catch (SerializationException ex) { + throw new ArgumentException (ex.Message, "serializationInfo", ex); + } + } + + #endregion + + #region Public Constructors + + /// + /// Initializes a new instance of the class. + /// + public WebHeaderCollection () + { + } + + #endregion + + #region Internal Properties + + internal HttpHeaderType State { + get { + return _state; + } + } + + #endregion + + #region Public Properties + + /// + /// Gets all header names in the collection. + /// + /// + /// An array of that contains all header names in the collection. + /// + public override string[] AllKeys { + get { + return base.AllKeys; + } + } + + /// + /// Gets the number of headers in the collection. + /// + /// + /// An that represents the number of headers in the collection. + /// + public override int Count { + get { + return base.Count; + } + } + + /// + /// Gets or sets the specified request in the collection. + /// + /// + /// A that represents the value of the request . + /// + /// + /// One of the enum values, represents + /// the request header to get or set. + /// + /// + /// + /// is a restricted header. + /// + /// + /// -or- + /// + /// + /// contains invalid characters. + /// + /// + /// + /// The length of is greater than 65,535 characters. + /// + /// + /// The current instance doesn't allow + /// the request . + /// + public string this[HttpRequestHeader header] { + get { + return Get (Convert (header)); + } + + set { + Add (header, value); + } + } + + /// + /// Gets or sets the specified response in the collection. + /// + /// + /// A that represents the value of the response . + /// + /// + /// One of the enum values, represents + /// the response header to get or set. + /// + /// + /// + /// is a restricted header. + /// + /// + /// -or- + /// + /// + /// contains invalid characters. + /// + /// + /// + /// The length of is greater than 65,535 characters. + /// + /// + /// The current instance doesn't allow + /// the response . + /// + public string this[HttpResponseHeader header] { + get { + return Get (Convert (header)); + } + + set { + Add (header, value); + } + } + + /// + /// Gets a collection of header names in the collection. + /// + /// + /// A that contains + /// all header names in the collection. + /// + public override NameObjectCollectionBase.KeysCollection Keys { + get { + return base.Keys; + } + } + + #endregion + + #region Private Methods + + private void add (string name, string value, bool ignoreRestricted) + { + var act = ignoreRestricted + ? (Action ) addWithoutCheckingNameAndRestricted + : addWithoutCheckingName; + + doWithCheckingState (act, checkName (name), value, true); + } + + private void addWithoutCheckingName (string name, string value) + { + doWithoutCheckingName (base.Add, name, value); + } + + private void addWithoutCheckingNameAndRestricted (string name, string value) + { + base.Add (name, checkValue (value)); + } + + private static int checkColonSeparated (string header) + { + var idx = header.IndexOf (':'); + if (idx == -1) + throw new ArgumentException ("No colon could be found.", "header"); + + return idx; + } + + 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; + } + + private static string checkName (string name) + { + if (name == null || name.Length == 0) + throw new ArgumentNullException ("name"); + + name = name.Trim (); + if (!IsHeaderName (name)) + throw new ArgumentException ("Contains invalid characters.", "name"); + + return name; + } + + private void checkRestricted (string name) + { + if (!_internallyUsed && isRestricted (name, true)) + throw new ArgumentException ("This header must be modified with the appropiate property."); + } + + private void checkState (bool response) + { + if (_state == HttpHeaderType.Unspecified) + return; + + if (response && _state == HttpHeaderType.Request) + throw new InvalidOperationException ( + "This collection has already been used to store the request headers."); + + if (!response && _state == HttpHeaderType.Response) + throw new InvalidOperationException ( + "This collection has already been used to store the response headers."); + } + + private static string checkValue (string value) + { + if (value == null || value.Length == 0) + 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"); + + return value; + } + + private static string convert (string key) + { + HttpHeaderInfo info; + return _headers.TryGetValue (key, out info) ? info.Name : String.Empty; + } + + 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); + } + + private void doWithCheckingState ( + Action action, string name, string value, bool response, bool setState) + { + checkState (response); + action (name, value); + if (setState && _state == HttpHeaderType.Unspecified) + _state = response ? HttpHeaderType.Response : HttpHeaderType.Request; + } + + private void doWithoutCheckingName (Action action, string name, string value) + { + checkRestricted (name); + action (name, checkValue (value)); + } + + private static HttpHeaderInfo getHeaderInfo (string name) + { + foreach (var info in _headers.Values) + if (info.Name.Equals (name, StringComparison.InvariantCultureIgnoreCase)) + return info; + + return null; + } + + private static bool isRestricted (string name, bool response) + { + var info = getHeaderInfo (name); + return info != null && info.IsRestricted (response); + } + + private void removeWithoutCheckingName (string name, string unuse) + { + checkRestricted (name); + base.Remove (name); + } + + private void setWithoutCheckingName (string name, string value) + { + doWithoutCheckingName (base.Set, name, value); + } + + #endregion + + #region Internal Methods + + internal static string Convert (HttpRequestHeader header) + { + return convert (header.ToString ()); + } + + internal static string Convert (HttpResponseHeader header) + { + return convert (header.ToString ()); + } + + internal void InternalRemove (string name) + { + base.Remove (name); + } + + internal void InternalSet (string header, bool response) + { + var pos = checkColonSeparated (header); + InternalSet (header.Substring (0, pos), header.Substring (pos + 1), response); + } + + internal void InternalSet (string name, string value, bool response) + { + value = checkValue (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 (); + } + + internal static bool IsHeaderValue (string value) + { + return value.IsText (); + } + + internal static bool IsMultiValue (string headerName, bool response) + { + if (headerName == null || headerName.Length == 0) + return false; + + var info = getHeaderInfo (headerName); + return info != null && info.IsMultiValue (response); + } + + 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 (); + } + + #endregion + + #region Protected Methods + + /// + /// Adds a header to the collection without checking if the header is on + /// the restricted header list. + /// + /// + /// 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. + /// + /// + /// The length of is greater than 65,535 characters. + /// + /// + /// The current instance doesn't allow + /// the . + /// + protected void AddWithoutValidate (string headerName, string headerValue) + { + add (headerName, headerValue, true); + } + + #endregion + + #region Public Methods + + /// + /// Adds the specified to the collection. + /// + /// + /// A that represents the header with the name and value separated by + /// a colon (':'). + /// + /// + /// is , empty, or the name part of + /// is empty. + /// + /// + /// + /// doesn't contain a colon. + /// + /// + /// -or- + /// + /// + /// is a restricted header. + /// + /// + /// -or- + /// + /// + /// The name or value part of contains invalid characters. + /// + /// + /// + /// The length of the value part of is greater than 65,535 characters. + /// + /// + /// The current instance doesn't allow + /// the . + /// + public void Add (string header) + { + if (header == null || header.Length == 0) + throw new ArgumentNullException ("header"); + + var pos = checkColonSeparated (header); + add (header.Substring (0, pos), header.Substring (pos + 1), false); + } + + /// + /// Adds the specified request with + /// the specified to the collection. + /// + /// + /// One of the enum values, represents + /// the request header to add. + /// + /// + /// A that represents the value of the header to add. + /// + /// + /// + /// is a restricted header. + /// + /// + /// -or- + /// + /// + /// contains invalid characters. + /// + /// + /// + /// The length of is greater than 65,535 characters. + /// + /// + /// The current instance doesn't allow + /// the request . + /// + public void Add (HttpRequestHeader header, string value) + { + doWithCheckingState (addWithoutCheckingName, Convert (header), value, false, true); + } + + /// + /// Adds the specified response with + /// the specified to the collection. + /// + /// + /// One of the enum values, represents + /// the response header to add. + /// + /// + /// A that represents the value of the header to add. + /// + /// + /// + /// is a restricted header. + /// + /// + /// -or- + /// + /// + /// contains invalid characters. + /// + /// + /// + /// The length of is greater than 65,535 characters. + /// + /// + /// The current instance doesn't allow + /// the response . + /// + public void Add (HttpResponseHeader header, string value) + { + doWithCheckingState (addWithoutCheckingName, Convert (header), value, true, true); + } + + /// + /// Adds a header with the specified and + /// to the collection. + /// + /// + /// 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 current instance doesn't allow + /// the header . + /// + public override void Add (string name, string value) + { + add (name, value, false); + } + + /// + /// Removes all headers from the collection. + /// + public override void Clear () + { + base.Clear (); + _state = HttpHeaderType.Unspecified; + } + + /// + /// Get the value of the header at the specified in the collection. + /// + /// + /// A that receives the value of the header. + /// + /// + /// An that represents the zero-based index of the header to find. + /// + /// + /// is out of allowable range of indexes for the collection. + /// + public override string Get (int index) + { + return base.Get (index); + } + + /// + /// Get the value of the header with the specified in the collection. + /// + /// + /// A that receives the value of the header if found; + /// otherwise, . + /// + /// + /// A that represents the name of the header to find. + /// + public override string Get (string name) + { + return base.Get (name); + } + + /// + /// Gets the enumerator used to iterate through the collection. + /// + /// + /// An instance used to iterate through the collection. + /// + public override IEnumerator GetEnumerator () + { + return base.GetEnumerator (); + } + + /// + /// Get the name of the header at the specified in the collection. + /// + /// + /// A that receives the header name. + /// + /// + /// An that represents the zero-based index of the header to find. + /// + /// + /// is out of allowable range of indexes for the collection. + /// + public override string GetKey (int index) + { + return base.GetKey (index); + } + + /// + /// Gets an array of header values stored in the specified position of + /// the collection. + /// + /// + /// An array of that receives the header values if found; + /// otherwise, . + /// + /// + /// An that represents the zero-based index of the header to find. + /// + /// + /// 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 . + /// + /// + /// An array of that receives the header values if found; + /// otherwise, . + /// + /// + /// A that represents the name of the header to find. + /// + public override string[] GetValues (string header) + { + var vals = base.GetValues (header); + return vals != null && vals.Length > 0 ? vals : null; + } + + /// + /// Populates the specified with the data needed to serialize + /// the . + /// + /// + /// A that holds the serialized object data. + /// + /// + /// A that specifies the destination for the serialization. + /// + /// + /// is . + /// + [SecurityPermission ( + SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)] + public override void GetObjectData ( + SerializationInfo serializationInfo, StreamingContext streamingContext) + { + if (serializationInfo == null) + throw new ArgumentNullException ("serializationInfo"); + + serializationInfo.AddValue ("InternallyUsed", _internallyUsed); + 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)); + }); + } + + /// + /// Determines whether the specified header can be set for the request. + /// + /// + /// true if the header is restricted; otherwise, false. + /// + /// + /// A that represents the name of the header to test. + /// + /// + /// is or empty. + /// + /// + /// contains invalid characters. + /// + public static bool IsRestricted (string headerName) + { + return isRestricted (checkName (headerName), false); + } + + /// + /// Determines whether the specified header can be set for the request or the response. + /// + /// + /// true if the header is restricted; otherwise, false. + /// + /// + /// A that represents the name of the header to test. + /// + /// + /// true if does the test for the response; for the request, false. + /// + /// + /// is or empty. + /// + /// + /// contains invalid characters. + /// + public static bool IsRestricted (string headerName, bool response) + { + return isRestricted (checkName (headerName), response); + } + + /// + /// Implements the interface and raises the deserialization event + /// when the deserialization is complete. + /// + /// + /// An that represents the source of the deserialization event. + /// + public override void OnDeserialization (object sender) + { + } + + /// + /// Removes the specified request from the collection. + /// + /// + /// One of the enum values, represents + /// the request header to remove. + /// + /// + /// is a restricted header. + /// + /// + /// The current instance doesn't allow + /// the request . + /// + public void Remove (HttpRequestHeader header) + { + doWithCheckingState (removeWithoutCheckingName, Convert (header), null, false, false); + } + + /// + /// Removes the specified response from the collection. + /// + /// + /// One of the enum values, represents + /// the response header to remove. + /// + /// + /// is a restricted header. + /// + /// + /// The current instance doesn't allow + /// the response . + /// + public void Remove (HttpResponseHeader header) + { + doWithCheckingState (removeWithoutCheckingName, Convert (header), null, true, false); + } + + /// + /// Removes the specified header from the collection. + /// + /// + /// A that represents the name of the header to remove. + /// + /// + /// is or empty. + /// + /// + /// + /// contains invalid characters. + /// + /// + /// -or- + /// + /// + /// is a restricted header name. + /// + /// + /// + /// The current instance doesn't allow + /// the header . + /// + public override void Remove (string name) + { + doWithCheckingState (removeWithoutCheckingName, checkName (name), null, false); + } + + /// + /// Sets the specified request to the specified value. + /// + /// + /// One of the enum values, represents + /// the request header to set. + /// + /// + /// A that represents the value of the request header to set. + /// + /// + /// + /// is a restricted header. + /// + /// + /// -or- + /// + /// + /// contains invalid characters. + /// + /// + /// + /// The length of is greater than 65,535 characters. + /// + /// + /// The current instance doesn't allow + /// the request . + /// + public void Set (HttpRequestHeader header, string value) + { + doWithCheckingState (setWithoutCheckingName, Convert (header), value, false, true); + } + + /// + /// Sets the specified response to the specified value. + /// + /// + /// One of the enum values, represents + /// the response header to set. + /// + /// + /// A that represents the value of the response header to set. + /// + /// + /// + /// is a restricted header. + /// + /// + /// -or- + /// + /// + /// contains invalid characters. + /// + /// + /// + /// The length of is greater than 65,535 characters. + /// + /// + /// The current instance doesn't allow + /// the response . + /// + public void Set (HttpResponseHeader header, string value) + { + doWithCheckingState (setWithoutCheckingName, Convert (header), value, true, true); + } + + /// + /// Sets the specified header to the specified value. + /// + /// + /// A that represents the name of the header to set. + /// + /// + /// A that represents the value of the header to set. + /// + /// + /// is or empty. + /// + /// + /// + /// or contains invalid characters. + /// + /// + /// -or- + /// + /// + /// is a restricted header name. + /// + /// + /// + /// The length of is greater than 65,535 characters. + /// + /// + /// The current instance doesn't allow + /// the header . + /// + public override void Set (string name, string value) + { + doWithCheckingState (setWithoutCheckingName, checkName (name), value, true); + } + + /// + /// Converts the current to an array of . + /// + /// + /// An array of that receives the converted current + /// . + /// + public byte[] ToByteArray () + { + return Encoding.UTF8.GetBytes (ToString ()); + } + + /// + /// Returns a that represents the current + /// . + /// + /// + /// A that represents the current . + /// + public override string ToString () + { + var buff = new StringBuilder (); + Count.Times (i => buff.AppendFormat ("{0}: {1}\r\n", GetKey (i), Get (i))); + + return buff.Append ("\r\n").ToString (); + } + + #endregion + + #region Explicit Interface Implementations + + /// + /// Populates the specified with the data needed to serialize + /// the current . + /// + /// + /// A that holds the serialized object data. + /// + /// + /// A that specifies the destination for the serialization. + /// + /// + /// is . + /// + [SecurityPermission ( + SecurityAction.LinkDemand, + Flags = SecurityPermissionFlag.SerializationFormatter, + SerializationFormatter = true)] + void ISerializable.GetObjectData ( + SerializationInfo serializationInfo, StreamingContext streamingContext) + { + GetObjectData (serializationInfo, streamingContext); + } + + #endregion + } +} diff --git a/websocket-sharp-core/Net/WebSockets/HttpListenerWebSocketContext.cs b/websocket-sharp-core/Net/WebSockets/HttpListenerWebSocketContext.cs new file mode 100644 index 000000000..eed49ce1c --- /dev/null +++ b/websocket-sharp-core/Net/WebSockets/HttpListenerWebSocketContext.cs @@ -0,0 +1,394 @@ +#region License +/* + * HttpListenerWebSocketContext.cs + * + * The MIT License + * + * Copyright (c) 2012-2018 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.Generic; +using System.Collections.Specialized; +using System.IO; +using System.Security.Principal; + +namespace WebSocketSharp.Net.WebSockets +{ + /// + /// Provides the access to the information in a WebSocket handshake request to + /// a instance. + /// + public class HttpListenerWebSocketContext : WebSocketContext + { + #region Private Fields + + private HttpListenerContext _context; + private WebSocket _websocket; + + #endregion + + #region Internal Constructors + + internal HttpListenerWebSocketContext ( + HttpListenerContext context, string protocol + ) + { + _context = context; + _websocket = new WebSocket (this, protocol); + } + + #endregion + + #region Internal Properties + + internal Logger Log { + get { + return _context.Listener.Log; + } + } + + internal Stream Stream { + get { + return _context.Connection.Stream; + } + } + + #endregion + + #region Public Properties + + /// + /// Gets the HTTP cookies included in the handshake request. + /// + /// + /// + /// A that contains + /// the cookies. + /// + /// + /// An empty collection if not included. + /// + /// + public override CookieCollection CookieCollection { + get { + return _context.Request.Cookies; + } + } + + /// + /// Gets the HTTP headers included in the handshake request. + /// + /// + /// A that contains the headers. + /// + public override NameValueCollection Headers { + get { + return _context.Request.Headers; + } + } + + /// + /// Gets the value of the Host header included in the handshake request. + /// + /// + /// + /// 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.UserHostName; + } + } + + /// + /// Gets a value indicating whether the client is authenticated. + /// + /// + /// true if the client is authenticated; otherwise, false. + /// + public override bool IsAuthenticated { + get { + return _context.Request.IsAuthenticated; + } + } + + /// + /// Gets a value indicating whether the handshake request is sent from + /// the local computer. + /// + /// + /// true if the handshake request is sent from the same computer + /// as the server; otherwise, false. + /// + public override bool IsLocal { + get { + return _context.Request.IsLocal; + } + } + + /// + /// Gets a value indicating whether a secure connection is used to send + /// the handshake request. + /// + /// + /// true if the connection is secure; otherwise, false. + /// + public override bool IsSecureConnection { + get { + return _context.Request.IsSecureConnection; + } + } + + /// + /// Gets a value indicating whether the request is a WebSocket handshake + /// request. + /// + /// + /// true if the request is a WebSocket handshake request; otherwise, + /// false. + /// + public override bool IsWebSocketRequest { + get { + return _context.Request.IsWebSocketRequest; + } + } + + /// + /// Gets the value of the Origin header included in the handshake request. + /// + /// + /// + /// A that represents the value of the Origin header. + /// + /// + /// if the header is not present. + /// + /// + public override string Origin { + get { + return _context.Request.Headers["Origin"]; + } + } + + /// + /// Gets the query string included in the handshake request. + /// + /// + /// + /// A that contains the query + /// parameters. + /// + /// + /// An empty collection if not included. + /// + /// + public override NameValueCollection QueryString { + get { + return _context.Request.QueryString; + } + } + + /// + /// Gets the URI requested by the client. + /// + /// + /// + /// A that represents the URI parsed from the request. + /// + /// + /// if the URI cannot be parsed. + /// + /// + public override Uri RequestUri { + get { + return _context.Request.Url; + } + } + + /// + /// Gets the value of the Sec-WebSocket-Key header included in + /// the handshake request. + /// + /// + /// + /// 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 the header is not present. + /// + /// + public override string SecWebSocketKey { + get { + return _context.Request.Headers["Sec-WebSocket-Key"]; + } + } + + /// + /// Gets the names of the subprotocols from the Sec-WebSocket-Protocol + /// header included in the handshake request. + /// + /// + /// + /// 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 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 handshake request. + /// + /// + /// + /// A that represents the WebSocket protocol + /// version specified by the client. + /// + /// + /// if the header is not present. + /// + /// + public override string SecWebSocketVersion { + get { + return _context.Request.Headers["Sec-WebSocket-Version"]; + } + } + + /// + /// Gets the endpoint to which the handshake request is sent. + /// + /// + /// A that represents the server IP + /// address and port number. + /// + public override System.Net.IPEndPoint ServerEndPoint { + get { + return _context.Request.LocalEndPoint; + } + } + + /// + /// Gets 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 { + return _context.User; + } + } + + /// + /// Gets the endpoint from which the handshake request is sent. + /// + /// + /// A that represents the client IP + /// address and port number. + /// + public override System.Net.IPEndPoint UserEndPoint { + get { + return _context.Request.RemoteEndPoint; + } + } + + /// + /// Gets the WebSocket instance used for two-way communication between + /// the client and server. + /// + /// + /// A . + /// + public override WebSocket WebSocket { + get { + return _websocket; + } + } + + #endregion + + #region Internal Methods + + internal void Close () + { + _context.Connection.Close (true); + } + + internal void Close (HttpStatusCode code) + { + _context.Response.Close (code); + } + + #endregion + + #region Public Methods + + /// + /// Returns a string that represents the current instance. + /// + /// + /// A that contains the request line and headers + /// included in the handshake request. + /// + public override string ToString () + { + return _context.Request.ToString (); + } + + #endregion + } +} diff --git a/websocket-sharp-core/Net/WebSockets/TcpListenerWebSocketContext.cs b/websocket-sharp-core/Net/WebSockets/TcpListenerWebSocketContext.cs new file mode 100644 index 000000000..519da7896 --- /dev/null +++ b/websocket-sharp-core/Net/WebSockets/TcpListenerWebSocketContext.cs @@ -0,0 +1,518 @@ +#region License +/* + * TcpListenerWebSocketContext.cs + * + * The MIT License + * + * Copyright (c) 2012-2018 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Contributors +/* + * Contributors: + * - Liryna + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.IO; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Principal; +using System.Text; + +namespace WebSocketSharp.Net.WebSockets +{ + /// + /// Provides the access to the information in a WebSocket handshake request to + /// a instance. + /// + internal class TcpListenerWebSocketContext : WebSocketContext + { + #region Private Fields + + private Logger _log; + private NameValueCollection _queryString; + private HttpRequest _request; + private Uri _requestUri; + private bool _secure; + private System.Net.EndPoint _serverEndPoint; + private Stream _stream; + private TcpClient _tcpClient; + private IPrincipal _user; + private System.Net.EndPoint _userEndPoint; + private WebSocket _websocket; + + #endregion + + #region Internal Constructors + + internal TcpListenerWebSocketContext ( + TcpClient tcpClient, + string protocol, + bool secure, + ServerSslConfiguration sslConfig, + Logger log + ) + { + _tcpClient = tcpClient; + _secure = secure; + _log = log; + + var netStream = tcpClient.GetStream (); + if (secure) { + var sslStream = new SslStream ( + netStream, + false, + sslConfig.ClientCertificateValidationCallback + ); + + sslStream.AuthenticateAsServer ( + sslConfig.ServerCertificate, + sslConfig.ClientCertificateRequired, + sslConfig.EnabledSslProtocols, + sslConfig.CheckCertificateRevocation + ); + + _stream = sslStream; + } + else { + _stream = netStream; + } + + var sock = tcpClient.Client; + _serverEndPoint = sock.LocalEndPoint; + _userEndPoint = sock.RemoteEndPoint; + + _request = HttpRequest.Read (_stream, 90000); + _websocket = new WebSocket (this, protocol); + } + + #endregion + + #region Internal Properties + + internal Logger Log { + get { + return _log; + } + } + + internal Stream Stream { + get { + return _stream; + } + } + + #endregion + + #region Public Properties + + /// + /// Gets the HTTP cookies included in the handshake request. + /// + /// + /// + /// A that contains + /// the cookies. + /// + /// + /// An empty collection if not included. + /// + /// + public override CookieCollection CookieCollection { + get { + return _request.Cookies; + } + } + + /// + /// Gets the HTTP headers included in the handshake request. + /// + /// + /// A that contains the headers. + /// + public override NameValueCollection Headers { + get { + return _request.Headers; + } + } + + /// + /// Gets the value of the Host header included in the handshake request. + /// + /// + /// + /// A that represents the server host name requested + /// by the client. + /// + /// + /// It includes the port number if provided. + /// + /// + public override string Host { + get { + return _request.Headers["Host"]; + } + } + + /// + /// Gets a value indicating whether the client is authenticated. + /// + /// + /// true if the client is authenticated; otherwise, false. + /// + public override bool IsAuthenticated { + get { + return _user != null; + } + } + + /// + /// Gets a value indicating whether the handshake request is sent from + /// the local computer. + /// + /// + /// true if the handshake request is sent from the same computer + /// as the server; otherwise, false. + /// + public override bool IsLocal { + get { + return UserEndPoint.Address.IsLocal (); + } + } + + /// + /// Gets a value indicating whether a secure connection is used to send + /// the handshake request. + /// + /// + /// true if the connection is secure; otherwise, false. + /// + public override bool IsSecureConnection { + get { + return _secure; + } + } + + /// + /// Gets a value indicating whether the request is a WebSocket handshake + /// request. + /// + /// + /// true if the request is a WebSocket handshake request; otherwise, + /// false. + /// + public override bool IsWebSocketRequest { + get { + return _request.IsWebSocketRequest; + } + } + + /// + /// Gets the value of the Origin header included in the handshake request. + /// + /// + /// + /// A that represents the value of the Origin header. + /// + /// + /// if the header is not present. + /// + /// + public override string Origin { + get { + return _request.Headers["Origin"]; + } + } + + /// + /// Gets the query string included in the handshake request. + /// + /// + /// + /// A that contains the query + /// parameters. + /// + /// + /// An empty collection if not included. + /// + /// + public override NameValueCollection QueryString { + get { + if (_queryString == null) { + var uri = RequestUri; + _queryString = QueryStringCollection.Parse ( + uri != null ? uri.Query : null, + Encoding.UTF8 + ); + } + + return _queryString; + } + } + + /// + /// Gets the URI requested by the client. + /// + /// + /// + /// A that represents the URI parsed from the request. + /// + /// + /// if the URI cannot be parsed. + /// + /// + public override Uri RequestUri { + get { + if (_requestUri == null) { + _requestUri = HttpUtility.CreateRequestUrl ( + _request.RequestUri, + _request.Headers["Host"], + _request.IsWebSocketRequest, + _secure + ); + } + + return _requestUri; + } + } + + /// + /// Gets the value of the Sec-WebSocket-Key header included in + /// the handshake request. + /// + /// + /// + /// 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 the header is not present. + /// + /// + public override string SecWebSocketKey { + get { + return _request.Headers["Sec-WebSocket-Key"]; + } + } + + /// + /// Gets the names of the subprotocols from the Sec-WebSocket-Protocol + /// header included in the handshake request. + /// + /// + /// + /// 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 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 handshake request. + /// + /// + /// + /// A that represents the WebSocket protocol + /// version specified by the client. + /// + /// + /// if the header is not present. + /// + /// + public override string SecWebSocketVersion { + get { + return _request.Headers["Sec-WebSocket-Version"]; + } + } + + /// + /// Gets the endpoint to which the handshake request is sent. + /// + /// + /// A that represents the server IP + /// address and port number. + /// + public override System.Net.IPEndPoint ServerEndPoint { + get { + return (System.Net.IPEndPoint) _serverEndPoint; + } + } + + /// + /// Gets 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 { + return _user; + } + } + + /// + /// Gets the endpoint from which the handshake request is sent. + /// + /// + /// A that represents the client IP + /// address and port number. + /// + public override System.Net.IPEndPoint UserEndPoint { + get { + return (System.Net.IPEndPoint) _userEndPoint; + } + } + + /// + /// Gets the WebSocket instance used for two-way communication between + /// the client and server. + /// + /// + /// A . + /// + public override WebSocket WebSocket { + get { + return _websocket; + } + } + + #endregion + + #region Private Methods + + private HttpRequest sendAuthenticationChallenge (string challenge) + { + var res = HttpResponse.CreateUnauthorizedResponse (challenge); + var bytes = res.ToByteArray (); + _stream.Write (bytes, 0, bytes.Length); + + return HttpRequest.Read (_stream, 15000); + } + + #endregion + + #region Internal Methods + + internal bool Authenticate ( + AuthenticationSchemes scheme, + string realm, + Func credentialsFinder + ) + { + var chal = new AuthenticationChallenge (scheme, realm).ToString (); + + var retry = -1; + Func auth = null; + auth = + () => { + retry++; + if (retry > 99) + return false; + + var user = HttpUtility.CreateUser ( + _request.Headers["Authorization"], + scheme, + realm, + _request.HttpMethod, + credentialsFinder + ); + + if (user != null && user.Identity.IsAuthenticated) { + _user = user; + return true; + } + + _request = sendAuthenticationChallenge (chal); + return auth (); + }; + + return auth (); + } + + internal void Close () + { + _stream.Close (); + _tcpClient.Close (); + } + + internal void Close (HttpStatusCode code) + { + var res = HttpResponse.CreateCloseResponse (code); + var bytes = res.ToByteArray (); + _stream.Write (bytes, 0, bytes.Length); + + _stream.Close (); + _tcpClient.Close (); + } + + #endregion + + #region Public Methods + + /// + /// Returns a string that represents the current instance. + /// + /// + /// A that contains the request line and headers + /// included in the handshake request. + /// + public override string ToString () + { + return _request.ToString (); + } + + #endregion + } +} diff --git a/websocket-sharp-core/Net/WebSockets/WebSocketContext.cs b/websocket-sharp-core/Net/WebSockets/WebSocketContext.cs new file mode 100644 index 000000000..6921891f7 --- /dev/null +++ b/websocket-sharp-core/Net/WebSockets/WebSocketContext.cs @@ -0,0 +1,224 @@ +#region License +/* + * WebSocketContext.cs + * + * The MIT License + * + * Copyright (c) 2012-2018 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.Generic; +using System.Collections.Specialized; +using System.Security.Principal; + +namespace WebSocketSharp.Net.WebSockets +{ + /// + /// Exposes the access to the information in a WebSocket handshake request. + /// + /// + /// This class is an abstract class. + /// + public abstract class WebSocketContext + { + #region Protected Constructors + + /// + /// Initializes a new instance of the class. + /// + protected WebSocketContext () + { + } + + #endregion + + #region Public Properties + + /// + /// Gets the HTTP cookies included in the handshake request. + /// + /// + /// A that contains + /// the cookies. + /// + public abstract CookieCollection CookieCollection { get; } + + /// + /// Gets the HTTP headers included in the handshake request. + /// + /// + /// A that contains the headers. + /// + public abstract NameValueCollection Headers { get; } + + /// + /// Gets the value of the Host header included in the handshake request. + /// + /// + /// A that represents the server host name requested + /// by the client. + /// + public abstract string Host { get; } + + /// + /// Gets a value indicating whether the client is authenticated. + /// + /// + /// true if the client is authenticated; otherwise, false. + /// + public abstract bool IsAuthenticated { get; } + + /// + /// Gets a value indicating whether the handshake request is sent from + /// the local computer. + /// + /// + /// 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 a secure connection is used to send + /// the handshake request. + /// + /// + /// true if the connection is secure; otherwise, false. + /// + public abstract bool IsSecureConnection { get; } + + /// + /// Gets a value indicating whether the request is a WebSocket handshake + /// request. + /// + /// + /// 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 handshake request. + /// + /// + /// A that represents the value of the Origin header. + /// + public abstract string Origin { get; } + + /// + /// Gets the query string included in the handshake request. + /// + /// + /// A that contains the query parameters. + /// + public abstract NameValueCollection QueryString { get; } + + /// + /// Gets the URI requested by the client. + /// + /// + /// 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 handshake request. + /// + /// + /// + /// 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 names of the subprotocols from the Sec-WebSocket-Protocol + /// header included in the handshake request. + /// + /// + /// + /// 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 handshake request. + /// + /// + /// A that represents the WebSocket protocol + /// version specified by the client. + /// + public abstract string SecWebSocketVersion { get; } + + /// + /// Gets the endpoint to which the handshake request is sent. + /// + /// + /// A that represents the server IP + /// address and port number. + /// + public abstract System.Net.IPEndPoint ServerEndPoint { get; } + + /// + /// Gets the client information. + /// + /// + /// A instance that represents identity, + /// authentication, and security roles for the client. + /// + public abstract IPrincipal User { get; } + + /// + /// Gets the endpoint from which the handshake request is sent. + /// + /// + /// A that represents the client IP + /// address and port number. + /// + public abstract System.Net.IPEndPoint UserEndPoint { get; } + + /// + /// Gets the WebSocket instance used for two-way communication between + /// the client and server. + /// + /// + /// A . + /// + public abstract WebSocket WebSocket { get; } + + #endregion + } +} diff --git a/websocket-sharp-core/Opcode.cs b/websocket-sharp-core/Opcode.cs new file mode 100644 index 000000000..5a8c632e0 --- /dev/null +++ b/websocket-sharp-core/Opcode.cs @@ -0,0 +1,68 @@ +#region License +/* + * Opcode.cs + * + * The MIT License + * + * 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 + * 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; + +namespace WebSocketSharp +{ + /// + /// Indicates the WebSocket frame type. + /// + /// + /// The values of this enumeration are defined in + /// + /// Section 5.2 of RFC 6455. + /// + internal enum Opcode : byte + { + /// + /// Equivalent to numeric value 0. Indicates continuation frame. + /// + Cont = 0x0, + /// + /// Equivalent to numeric value 1. Indicates text frame. + /// + Text = 0x1, + /// + /// Equivalent to numeric value 2. Indicates binary frame. + /// + Binary = 0x2, + /// + /// Equivalent to numeric value 8. Indicates connection close frame. + /// + Close = 0x8, + /// + /// Equivalent to numeric value 9. Indicates ping frame. + /// + Ping = 0x9, + /// + /// Equivalent to numeric value 10. Indicates pong frame. + /// + Pong = 0xa + } +} diff --git a/websocket-sharp-core/PayloadData.cs b/websocket-sharp-core/PayloadData.cs new file mode 100644 index 000000000..9e40b9404 --- /dev/null +++ b/websocket-sharp-core/PayloadData.cs @@ -0,0 +1,208 @@ +#region License +/* + * PayloadData.cs + * + * The MIT License + * + * Copyright (c) 2012-2019 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; +using System.Collections.Generic; + +namespace WebSocketSharp +{ + internal class PayloadData : IEnumerable + { + #region Private Fields + + private byte[] _data; + private long _extDataLength; + private long _length; + + #endregion + + #region Public Fields + + /// + /// Represents the empty payload data. + /// + public static readonly PayloadData Empty; + + /// + /// Represents the allowable max length of payload data. + /// + /// + /// + /// A will occur when the length of + /// incoming payload data is greater than the value of this field. + /// + /// + /// If you would like to change the value of this field, it must be + /// a number between and + /// inclusive. + /// + /// + public static readonly ulong MaxLength; + + #endregion + + #region Static Constructor + + static PayloadData () + { + Empty = new PayloadData (WebSocket.EmptyBytes, 0); + MaxLength = Int64.MaxValue; + } + + #endregion + + #region Internal Constructors + + internal PayloadData (byte[] data) + : this (data, data.LongLength) + { + } + + internal PayloadData (byte[] data, long length) + { + _data = data; + _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; + } + + set { + _extDataLength = value; + } + } + + internal bool HasReservedCode { + get { + return _length >= 2 && Code.IsReserved (); + } + } + + internal string Reason { + get { + if (_length <= 2) + return String.Empty; + + var raw = _data.SubArray (2, _length - 2); + + string reason; + return raw.TryGetUTF8DecodedString (out reason) + ? reason + : String.Empty; + } + } + + #endregion + + #region Public Properties + + public byte[] ApplicationData { + get { + return _extDataLength > 0 + ? _data.SubArray (_extDataLength, _length - _extDataLength) + : _data; + } + } + + public byte[] ExtensionData { + get { + return _extDataLength > 0 + ? _data.SubArray (0, _extDataLength) + : WebSocket.EmptyBytes; + } + } + + public ulong Length { + get { + return (ulong) _length; + } + } + + #endregion + + #region Internal Methods + + internal void Mask (byte[] key) + { + for (long i = 0; i < _length; i++) + _data[i] = (byte) (_data[i] ^ key[i % 4]); + } + + #endregion + + #region Public Methods + + public IEnumerator GetEnumerator () + { + foreach (var b in _data) + yield return b; + } + + public byte[] ToArray () + { + return _data; + } + + public override string ToString () + { + return BitConverter.ToString (_data); + } + + #endregion + + #region Explicit Interface Implementations + + IEnumerator IEnumerable.GetEnumerator () + { + return GetEnumerator (); + } + + #endregion + } +} diff --git a/websocket-sharp-core/Rsv.cs b/websocket-sharp-core/Rsv.cs new file mode 100644 index 000000000..8a10567c5 --- /dev/null +++ b/websocket-sharp-core/Rsv.cs @@ -0,0 +1,51 @@ +#region License +/* + * Rsv.cs + * + * The MIT License + * + * Copyright (c) 2012-2015 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; + +namespace WebSocketSharp +{ + /// + /// 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. + /// + internal enum Rsv : byte + { + /// + /// Equivalent to numeric value 0. Indicates zero. + /// + Off = 0x0, + /// + /// Equivalent to numeric value 1. Indicates non-zero. + /// + On = 0x1 + } +} diff --git a/websocket-sharp-core/Server/HttpRequestEventArgs.cs b/websocket-sharp-core/Server/HttpRequestEventArgs.cs new file mode 100644 index 000000000..ee76cbab3 --- /dev/null +++ b/websocket-sharp-core/Server/HttpRequestEventArgs.cs @@ -0,0 +1,255 @@ +#region License +/* + * HttpRequestEventArgs.cs + * + * The MIT License + * + * Copyright (c) 2012-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 + * 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.IO; +using System.Security.Principal; +using System.Text; +using WebSocketSharp.Net; + +namespace WebSocketSharp.Server +{ + /// + /// Represents the event data for the HTTP request events of + /// the . + /// + /// + /// + /// An HTTP request event occurs when the + /// receives an HTTP request. + /// + /// + /// You should access the property if you would + /// like to get the request data sent from a client. + /// + /// + /// 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 HttpListenerContext _context; + private string _docRootPath; + + #endregion + + #region Internal Constructors + + internal HttpRequestEventArgs ( + HttpListenerContext context, string documentRootPath + ) + { + _context = context; + _docRootPath = documentRootPath; + } + + #endregion + + #region Public Properties + + /// + /// Gets the request data sent from a client. + /// + /// + /// A that provides the methods and + /// properties for the request data. + /// + public HttpListenerRequest Request { + get { + return _context.Request; + } + } + + /// + /// Gets the response data to return to the client. + /// + /// + /// A that provides the methods and + /// properties for the response data. + /// + public HttpListenerResponse Response { + get { + return _context.Response; + } + } + + /// + /// Gets the information for the client. + /// + /// + /// + /// A instance or + /// if not authenticated. + /// + /// + /// That instance describes the identity, authentication scheme, + /// and security roles for the client. + /// + /// + 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 . + /// + /// + /// + /// An array of or + /// if it fails. + /// + /// + /// That array receives the contents of the file. + /// + /// + /// + /// A that represents a virtual path to + /// find the file from the document folder. + /// + /// + /// is . + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// contains "..". + /// + /// + 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.IndexOf ("..") > -1) + throw new ArgumentException ("It contains '..'.", "path"); + + byte[] contents; + tryReadFile (createFilePath (path), out contents); + + return contents; + } + + /// + /// Tries to read the specified file from the document folder of + /// the . + /// + /// + /// true if it succeeds to read; otherwise, false. + /// + /// + /// A that represents a virtual path to + /// find the file from the document folder. + /// + /// + /// + /// When this method returns, an array of or + /// if it fails. + /// + /// + /// That array receives the contents of the file. + /// + /// + /// + /// is . + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// contains "..". + /// + /// + 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.IndexOf ("..") > -1) + throw new ArgumentException ("It contains '..'.", "path"); + + return tryReadFile (createFilePath (path), out contents); + } + + #endregion + } +} diff --git a/websocket-sharp-core/Server/HttpServer.cs b/websocket-sharp-core/Server/HttpServer.cs new file mode 100644 index 000000000..56925ac6d --- /dev/null +++ b/websocket-sharp-core/Server/HttpServer.cs @@ -0,0 +1,1652 @@ +#region License +/* + * HttpServer.cs + * + * A simple HTTP server that allows to accept WebSocket handshake requests. + * + * The MIT License + * + * 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 + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Contributors +/* + * Contributors: + * - Juan Manuel Lallana + * - Liryna + * - Rohan Singh + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +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; + +namespace WebSocketSharp.Server +{ + /// + /// Provides a simple HTTP server that 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 _docRootPath; + private string _hostname; + private HttpListener _listener; + private Logger _log; + private int _port; + private Thread _receiveThread; + private bool _secure; + private WebSocketServiceManager _services; + private volatile ServerState _state; + private object _sync; + + #endregion + + #region Public Constructors + + /// + /// Initializes a new instance of the class. + /// + /// + /// The new instance listens for incoming requests on + /// and port 80. + /// + public HttpServer () + { + init ("*", System.Net.IPAddress.Any, 80, false); + } + + /// + /// Initializes a new instance of the class with + /// the specified . + /// + /// + /// + /// The new instance listens for incoming requests on + /// and . + /// + /// + /// It provides secure connections if is 443. + /// + /// + /// + /// An that represents the number of the port + /// on which to listen. + /// + /// + /// is less than 1 or greater than 65535. + /// + public HttpServer (int port) + : this (port, port == 443) + { + } + + /// + /// Initializes a new instance of the class with + /// the specified . + /// + /// + /// + /// The new instance listens for incoming requests on the IP address of the + /// host of and the 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. + /// + /// + /// The new instance provides secure connections if the scheme of + /// is https. + /// + /// + /// + /// A that represents the HTTP URL of the server. + /// + /// + /// is . + /// + /// + /// + /// is empty. + /// + /// + /// -or- + /// + /// + /// is invalid. + /// + /// + public HttpServer (string url) + { + if (url == null) + throw new ArgumentNullException ("url"); + + if (url.Length == 0) + throw new ArgumentException ("An empty string.", "url"); + + Uri uri; + string msg; + if (!tryCreateUri (url, out uri, out msg)) + throw new ArgumentException (msg, "url"); + + var host = uri.GetDnsSafeHost (true); + + var addr = host.ToIPAddress (); + 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 new instance listens for incoming requests on + /// and . + /// + /// + /// An that represents the number of the port + /// on which to listen. + /// + /// + /// A : true if the new instance provides + /// secure connections; otherwise, false. + /// + /// + /// is less than 1 or greater than 65535. + /// + public HttpServer (int port, bool secure) + { + 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 new instance listens for incoming requests on + /// and . + /// + /// + /// It provides secure connections if is 443. + /// + /// + /// + /// A that represents + /// the local IP address on which to listen. + /// + /// + /// An that represents the number of the port + /// on which to listen. + /// + /// + /// is . + /// + /// + /// is not a local IP address. + /// + /// + /// is less than 1 or greater than 65535. + /// + public HttpServer (System.Net.IPAddress address, int port) + : this (address, port, port == 443) + { + } + + /// + /// Initializes a new instance of the class with + /// the specified , , + /// and . + /// + /// + /// The new instance listens for incoming requests on + /// and . + /// + /// + /// A that represents + /// the local IP address on which to listen. + /// + /// + /// An that represents the number of the port + /// on which to listen. + /// + /// + /// A : true if the new instance provides + /// secure connections; otherwise, false. + /// + /// + /// is . + /// + /// + /// is not a local IP address. + /// + /// + /// 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"); + + if (!port.IsPortNumber ()) { + var msg = "Less than 1 or greater than 65535."; + throw new ArgumentOutOfRangeException ("port", msg); + } + + init (address.ToString (true), address, port, secure); + } + + #endregion + + #region Public Properties + + /// + /// Gets the 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 { + return _address; + } + } + + /// + /// Gets or sets the scheme used to authenticate the clients. + /// + /// + /// The set operation does nothing if the server has already + /// started or it is shutting down. + /// + /// + /// + /// One of the + /// enum values. + /// + /// + /// It represents the scheme used to authenticate the clients. + /// + /// + /// The default value is + /// . + /// + /// + public AuthenticationSchemes AuthenticationSchemes { + get { + return _listener.AuthenticationSchemes; + } + + set { + string msg; + if (!canSet (out msg)) { + _log.Warn (msg); + return; + } + + lock (_sync) { + if (!canSet (out msg)) { + _log.Warn (msg); + return; + } + + _listener.AuthenticationSchemes = value; + } + } + } + + /// + /// Gets or sets the path to the document folder of the server. + /// + /// + /// + /// '/' or '\' is trimmed from the end of the value if any. + /// + /// + /// The set operation does nothing if the server has already + /// started or it is shutting down. + /// + /// + /// + /// + /// A that represents a path to the folder + /// from which to find the requested file. + /// + /// + /// The default value is "./Public". + /// + /// + /// + /// The value specified for a set operation is . + /// + /// + /// + /// The value specified for a set operation is an empty string. + /// + /// + /// -or- + /// + /// + /// The value specified for a set operation is an invalid path string. + /// + /// + /// -or- + /// + /// + /// The value specified for a set operation is an absolute root. + /// + /// + 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 (); + + string full = null; + try { + full = Path.GetFullPath (value); + } + catch (Exception ex) { + throw new ArgumentException ("An invalid path string.", "value", ex); + } + + 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"); + + 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"); + + string msg; + if (!canSet (out msg)) { + _log.Warn (msg); + return; + } + + lock (_sync) { + if (!canSet (out msg)) { + _log.Warn (msg); + return; + } + + _docRootPath = value; + } + } + } + + /// + /// Gets a value indicating whether the server has started. + /// + /// + /// true if the server has started; otherwise, false. + /// + public bool IsListening { + get { + return _state == ServerState.Start; + } + } + + /// + /// Gets a value indicating whether secure connections are provided. + /// + /// + /// true if this instance provides secure connections; otherwise, + /// false. + /// + public bool IsSecure { + get { + return _secure; + } + } + + /// + /// Gets or sets a value indicating whether the server cleans up + /// the inactive sessions periodically. + /// + /// + /// The set operation does nothing if the server has already + /// started or it is shutting down. + /// + /// + /// + /// true if the server cleans up the inactive sessions + /// every 60 seconds; otherwise, false. + /// + /// + /// The default value is true. + /// + /// + public bool KeepClean { + get { + return _services.KeepClean; + } + + set { + _services.KeepClean = value; + } + } + + /// + /// Gets the logging function for the server. + /// + /// + /// The default logging level is . + /// + /// + /// A that provides the logging function. + /// + public Logger Log { + get { + return _log; + } + } + + /// + /// Gets the port of the server. + /// + /// + /// An that represents the number of the port + /// on which to listen for incoming requests. + /// + public int Port { + get { + return _port; + } + } + + /// + /// Gets or sets the realm used for authentication. + /// + /// + /// + /// "SECRET AREA" is used as the realm if the value is + /// or an empty string. + /// + /// + /// The set operation does nothing if the server has + /// already started or it is shutting down. + /// + /// + /// + /// + /// A or by default. + /// + /// + /// That string represents the name of the realm. + /// + /// + public string Realm { + get { + return _listener.Realm; + } + + set { + string msg; + if (!canSet (out msg)) { + _log.Warn (msg); + return; + } + + lock (_sync) { + if (!canSet (out msg)) { + _log.Warn (msg); + return; + } + + _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. + /// + /// + /// + /// You should set this property to true if you would + /// like to resolve to wait for socket in TIME_WAIT state. + /// + /// + /// The set operation does nothing if the server has already + /// started or it is shutting down. + /// + /// + /// + /// + /// 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 { + return _listener.ReuseAddress; + } + + set { + string msg; + if (!canSet (out msg)) { + _log.Warn (msg); + return; + } + + lock (_sync) { + if (!canSet (out msg)) { + _log.Warn (msg); + return; + } + + _listener.ReuseAddress = value; + } + } + } + + /// + /// Gets the configuration for secure connection. + /// + /// + /// This configuration will be referenced when attempts to start, + /// so it must be configured before the start method is called. + /// + /// + /// A that represents + /// the configuration used to provide secure connections. + /// + /// + /// This instance does not provide secure connections. + /// + public ServerSslConfiguration SslConfiguration { + get { + if (!_secure) { + var msg = "This instance does not provide secure connections."; + throw new InvalidOperationException (msg); + } + + return _listener.SslConfiguration; + } + } + + /// + /// Gets or sets the delegate used to find the credentials + /// for an identity. + /// + /// + /// + /// No credentials are found if the method invoked by + /// the delegate returns or + /// the value is . + /// + /// + /// The set operation does nothing if the server has + /// already started or it is shutting down. + /// + /// + /// + /// + /// A Func<, + /// > delegate or + /// if not needed. + /// + /// + /// That delegate invokes the method called for finding + /// the credentials used to authenticate a client. + /// + /// + /// The default value is . + /// + /// + public Func UserCredentialsFinder { + get { + return _listener.UserCredentialsFinder; + } + + set { + string msg; + if (!canSet (out msg)) { + _log.Warn (msg); + return; + } + + lock (_sync) { + if (!canSet (out msg)) { + _log.Warn (msg); + return; + } + + _listener.UserCredentialsFinder = value; + } + } + } + + /// + /// Gets or sets the time to wait for the response to the WebSocket Ping or + /// Close. + /// + /// + /// The set operation does nothing if the server has already started or + /// it is shutting down. + /// + /// + /// + /// A 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 { + _services.WaitTime = value; + } + } + + /// + /// Gets the management function for the WebSocket services + /// provided by the server. + /// + /// + /// A that manages + /// the WebSocket services provided by the server. + /// + public WebSocketServiceManager WebSocketServices { + get { + return _services; + } + } + + #endregion + + #region Public Events + + /// + /// Occurs when the server receives an HTTP CONNECT request. + /// + public event EventHandler OnConnect; + + /// + /// Occurs when the server receives an HTTP DELETE request. + /// + public event EventHandler OnDelete; + + /// + /// Occurs when the server receives an HTTP GET request. + /// + public event EventHandler OnGet; + + /// + /// Occurs when the server receives an HTTP HEAD request. + /// + public event EventHandler OnHead; + + /// + /// Occurs when the server receives an HTTP OPTIONS request. + /// + public event EventHandler OnOptions; + + /// + /// Occurs when the server receives an HTTP POST request. + /// + public event EventHandler OnPost; + + /// + /// Occurs when the server receives an HTTP PUT request. + /// + public event EventHandler OnPut; + + /// + /// Occurs when the server receives an HTTP TRACE request. + /// + public event EventHandler OnTrace; + + #endregion + + #region Private Methods + + private void abort () + { + lock (_sync) { + if (_state != ServerState.Start) + return; + + _state = ServerState.ShuttingDown; + } + + try { + try { + _services.Stop (1006, String.Empty); + } + finally { + _listener.Abort (); + } + } + catch { + } + + _state = ServerState.Stop; + } + + private bool canSet (out string message) + { + message = null; + + if (_state == ServerState.Start) { + message = "The server has already started."; + return false; + } + + if (_state == ServerState.ShuttingDown) { + message = "The server is shutting down."; + return false; + } + + return true; + } + + private bool checkCertificate (out string message) + { + message = null; + + var byUser = _listener.SslConfiguration.ServerCertificate != null; + + var path = _listener.CertificateFolderPath; + var withPort = EndPointListener.CertificateExists (_port, path); + + if (!(byUser || withPort)) { + message = "There is no server certificate for secure connection."; + return false; + } + + if (byUser && withPort) + _log.Warn ("The server certificate associated with the port is used."); + + return true; + } + + private string createFilePath (string childPath) + { + childPath = childPath.TrimStart ('/', '\\'); + return new StringBuilder (_docRootPath, 32) + .AppendFormat ("/{0}", childPath) + .ToString () + .Replace ('\\', '/'); + } + + private static HttpListener createListener ( + string hostname, int port, bool secure + ) + { + var lsnr = new HttpListener (); + + var schm = secure ? "https" : "http"; + var pref = String.Format ("{0}://{1}:{2}/", schm, hostname, port); + lsnr.Prefixes.Add (pref); + + return lsnr; + } + + private void init ( + string hostname, System.Net.IPAddress address, int port, bool secure + ) + { + _hostname = hostname; + _address = address; + _port = port; + _secure = secure; + + _docRootPath = "./Public"; + _listener = createListener (_hostname, _port, _secure); + _log = _listener.Log; + _services = new WebSocketServiceManager (_log); + _sync = new object (); + } + + private void processRequest (HttpListenerContext context) + { + var method = context.Request.HttpMethod; + var evt = method == "GET" + ? OnGet + : method == "HEAD" + ? OnHead + : method == "POST" + ? OnPost + : method == "PUT" + ? OnPut + : method == "DELETE" + ? OnDelete + : method == "CONNECT" + ? OnConnect + : method == "OPTIONS" + ? OnOptions + : method == "TRACE" + ? OnTrace + : null; + + if (evt != null) + evt (this, new HttpRequestEventArgs (context, _docRootPath)); + else + context.Response.StatusCode = 501; // Not Implemented + + 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 (path, out host)) { + context.Close (HttpStatusCode.NotImplemented); + return; + } + + host.StartSession (context); + } + + private void receiveRequest () + { + while (true) { + HttpListenerContext ctx = null; + try { + ctx = _listener.GetContext (); + ThreadPool.QueueUserWorkItem ( + state => { + try { + if (ctx.Request.IsUpgradeRequest ("websocket")) { + processRequest (ctx.AcceptWebSocket (null)); + return; + } + + processRequest (ctx); + } + catch (Exception ex) { + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + + ctx.Connection.Close (true); + } + } + ); + } + catch (HttpListenerException) { + _log.Info ("The underlying listener is stopped."); + break; + } + catch (InvalidOperationException) { + _log.Info ("The underlying listener is stopped."); + break; + } + catch (Exception ex) { + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + + if (ctx != null) + ctx.Connection.Close (true); + + break; + } + } + + if (_state != ServerState.ShuttingDown) + abort (); + } + + private void start () + { + if (_state == ServerState.Start) { + _log.Info ("The server has already started."); + return; + } + + if (_state == ServerState.ShuttingDown) { + _log.Warn ("The server is shutting down."); + return; + } + + lock (_sync) { + if (_state == ServerState.Start) { + _log.Info ("The server has already started."); + return; + } + + if (_state == ServerState.ShuttingDown) { + _log.Warn ("The server is shutting down."); + return; + } + + _services.Start (); + + try { + startReceiving (); + } + catch { + _services.Stop (1011, String.Empty); + throw; + } + + _state = ServerState.Start; + } + } + + private void startReceiving () + { + try { + _listener.Start (); + } + catch (Exception ex) { + var msg = "The underlying listener has failed to start."; + throw new InvalidOperationException (msg, ex); + } + + _receiveThread = new Thread (new ThreadStart (receiveRequest)); + _receiveThread.IsBackground = true; + _receiveThread.Start (); + } + + private void stop (ushort code, string reason) + { + if (_state == ServerState.Ready) { + _log.Info ("The server is not started."); + return; + } + + if (_state == ServerState.ShuttingDown) { + _log.Info ("The server is shutting down."); + return; + } + + if (_state == ServerState.Stop) { + _log.Info ("The server has already stopped."); + return; + } + + lock (_sync) { + if (_state == ServerState.ShuttingDown) { + _log.Info ("The server is shutting down."); + return; + } + + if (_state == ServerState.Stop) { + _log.Info ("The server has already stopped."); + return; + } + + _state = ServerState.ShuttingDown; + } + + try { + var threw = false; + try { + _services.Stop (code, reason); + } + catch { + threw = true; + throw; + } + finally { + try { + stopReceiving (5000); + } + catch { + if (!threw) + throw; + } + } + } + finally { + _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 + ) + { + result = null; + message = null; + + var uri = uriString.ToUri (); + if (uri == null) { + message = "An invalid URI string."; + return false; + } + + if (!uri.IsAbsoluteUri) { + message = "A relative URI."; + return false; + } + + var schm = uri.Scheme; + if (!(schm == "http" || schm == "https")) { + message = "The scheme part is not 'http' or 'https'."; + return false; + } + + if (uri.PathAndQuery != "/") { + message = "It includes either or both path and query components."; + return false; + } + + if (uri.Fragment.Length > 0) { + message = "It includes the fragment component."; + return false; + } + + if (uri.Port == 0) { + message = "The port part is zero."; + return false; + } + + result = uri; + return true; + } + + #endregion + + #region Public Methods + + /// + /// Adds a WebSocket service with the specified behavior, path, + /// and delegate. + /// + /// + /// + /// A that represents an absolute path to + /// the service to add. + /// + /// + /// / is trimmed from the end of the string if present. + /// + /// + /// + /// + /// A Func<TBehavior> delegate. + /// + /// + /// It invokes the method called when creating a new session + /// instance for the service. + /// + /// + /// The method must create a new instance of the specified + /// behavior class and return it. + /// + /// + /// + /// + /// The type of the behavior for the service. + /// + /// + /// It must inherit the class. + /// + /// + /// + /// + /// is . + /// + /// + /// -or- + /// + /// + /// is . + /// + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is not an absolute path. + /// + /// + /// -or- + /// + /// + /// includes either or both + /// query and fragment components. + /// + /// + /// -or- + /// + /// + /// is already in use. + /// + /// + [Obsolete ("This method will be removed. Use added one instead.")] + public void AddWebSocketService ( + string path, Func creator + ) + where TBehavior : WebSocketBehavior + { + if (path == null) + throw new ArgumentNullException ("path"); + + if (creator == null) + throw new ArgumentNullException ("creator"); + + if (path.Length == 0) + throw new ArgumentException ("An empty string.", "path"); + + if (path[0] != '/') + throw new ArgumentException ("Not an absolute path.", "path"); + + if (path.IndexOfAny (new[] { '?', '#' }) > -1) { + var msg = "It includes either or both query and fragment components."; + throw new ArgumentException (msg, "path"); + } + + _services.Add (path, creator); + } + + /// + /// Adds a WebSocket service with the specified behavior and path. + /// + /// + /// + /// A that represents an absolute path to + /// the service to add. + /// + /// + /// / is trimmed from the end of the string if present. + /// + /// + /// + /// + /// The type of the behavior for the service. + /// + /// + /// It must inherit the class. + /// + /// + /// And also, it must have a public parameterless constructor. + /// + /// + /// + /// is . + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is not an absolute path. + /// + /// + /// -or- + /// + /// + /// includes either or both + /// query and fragment components. + /// + /// + /// -or- + /// + /// + /// is already in use. + /// + /// + public void AddWebSocketService (string path) + where TBehaviorWithNew : WebSocketBehavior, new () + { + _services.AddService (path, null); + } + + /// + /// Adds a WebSocket service with the specified behavior, path, + /// and delegate. + /// + /// + /// + /// A that represents an absolute path to + /// the service to add. + /// + /// + /// / is trimmed from the end of the string if present. + /// + /// + /// + /// + /// An Action<TBehaviorWithNew> delegate or + /// if not needed. + /// + /// + /// The delegate invokes the method called when initializing + /// a new session instance for the service. + /// + /// + /// + /// + /// The type of the behavior for the service. + /// + /// + /// It must inherit the class. + /// + /// + /// And also, it must have a public parameterless constructor. + /// + /// + /// + /// is . + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is not an absolute path. + /// + /// + /// -or- + /// + /// + /// includes either or both + /// query and fragment components. + /// + /// + /// -or- + /// + /// + /// is already in use. + /// + /// + public void AddWebSocketService ( + string path, Action initializer + ) + where TBehaviorWithNew : WebSocketBehavior, new () + { + _services.AddService (path, initializer); + } + + /// + /// Gets the contents of the specified file from the document + /// folder of the server. + /// + /// + /// + /// An array of or + /// if it fails. + /// + /// + /// That array represents the contents of the file. + /// + /// + /// + /// A that represents a virtual path to + /// find the file from the document folder. + /// + /// + /// is . + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// contains "..". + /// + /// + [Obsolete ("This method will be removed.")] + public byte[] GetFile (string path) + { + if (path == null) + throw new ArgumentNullException ("path"); + + if (path.Length == 0) + throw new ArgumentException ("An empty string.", "path"); + + if (path.IndexOf ("..") > -1) + throw new ArgumentException ("It contains '..'.", "path"); + + path = createFilePath (path); + return File.Exists (path) ? File.ReadAllBytes (path) : null; + } + + /// + /// Removes a WebSocket service with the specified path. + /// + /// + /// The service is stopped with close status 1001 (going away) + /// if it has already started. + /// + /// + /// true if the service is successfully found and removed; + /// otherwise, false. + /// + /// + /// + /// A that represents an absolute path to + /// the service to remove. + /// + /// + /// / is trimmed from the end of the string if present. + /// + /// + /// + /// is . + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is not an absolute path. + /// + /// + /// -or- + /// + /// + /// includes either or both + /// query and fragment components. + /// + /// + public bool RemoveWebSocketService (string path) + { + return _services.RemoveService (path); + } + + /// + /// Starts receiving incoming requests. + /// + /// + /// This method does nothing if the server has already started or + /// it is shutting down. + /// + /// + /// + /// There is no server certificate for secure connection. + /// + /// + /// -or- + /// + /// + /// The underlying has failed to start. + /// + /// + public void Start () + { + if (_secure) { + string msg; + if (!checkCertificate (out msg)) + throw new InvalidOperationException (msg); + } + + start (); + } + + /// + /// Stops receiving incoming requests. + /// + public void Stop () + { + stop (1001, String.Empty); + } + + /// + /// Stops receiving incoming requests and closes each connection. + /// + /// + /// + /// A that represents the status code indicating + /// the reason for the WebSocket connection close. + /// + /// + /// The status codes are defined in + /// + /// Section 7.4 of RFC 6455. + /// + /// + /// + /// + /// A that represents the reason for the WebSocket + /// connection close. + /// + /// + /// The size must be 123 bytes or less in UTF-8. + /// + /// + /// + /// + /// is less than 1000 or greater than 4999. + /// + /// + /// -or- + /// + /// + /// The size of is greater than 123 bytes. + /// + /// + /// + /// + /// is 1010 (mandatory extension). + /// + /// + /// -or- + /// + /// + /// is 1005 (no status) and there is reason. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + [Obsolete ("This method will be removed.")] + public void Stop (ushort code, string reason) + { + if (!code.IsCloseStatusCode ()) { + var msg = "Less than 1000 or greater than 4999."; + throw new ArgumentOutOfRangeException ("code", msg); + } + + if (code == 1010) { + var msg = "1010 cannot be used."; + throw new ArgumentException (msg, "code"); + } + + if (!reason.IsNullOrEmpty ()) { + 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); + } + } + + stop (code, reason); + } + + /// + /// Stops receiving incoming requests and closes each connection. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It represents the status code indicating the reason for the WebSocket + /// connection close. + /// + /// + /// + /// + /// A that represents the reason for the WebSocket + /// connection close. + /// + /// + /// The size must be 123 bytes or less in UTF-8. + /// + /// + /// + /// The size of is greater than 123 bytes. + /// + /// + /// + /// is + /// . + /// + /// + /// -or- + /// + /// + /// is + /// and there is reason. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + [Obsolete ("This method will be removed.")] + public void Stop (CloseStatusCode code, string reason) + { + if (code == CloseStatusCode.MandatoryExtension) { + var msg = "MandatoryExtension cannot be used."; + throw new ArgumentException (msg, "code"); + } + + if (!reason.IsNullOrEmpty ()) { + 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); + } + } + + stop ((ushort) code, reason); + } + + #endregion + } +} diff --git a/websocket-sharp-core/Server/IWebSocketSession.cs b/websocket-sharp-core/Server/IWebSocketSession.cs new file mode 100644 index 000000000..296b5bf5a --- /dev/null +++ b/websocket-sharp-core/Server/IWebSocketSession.cs @@ -0,0 +1,91 @@ +#region License +/* + * IWebSocketSession.cs + * + * The MIT License + * + * Copyright (c) 2013-2018 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 WebSocketSharp.Net.WebSockets; + +namespace WebSocketSharp.Server +{ + /// + /// Exposes the access to the information in a WebSocket session. + /// + public interface IWebSocketSession + { + #region Properties + + /// + /// Gets the current state of the WebSocket connection for the session. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It indicates the current state of the connection. + /// + /// + WebSocketState ConnectionState { get; } + + /// + /// Gets the information in the WebSocket handshake request. + /// + /// + /// A instance that provides the access to + /// the information in the handshake request. + /// + WebSocketContext Context { get; } + + /// + /// Gets the unique ID of the session. + /// + /// + /// A that represents the unique ID of the session. + /// + string ID { get; } + + /// + /// Gets the name of the WebSocket subprotocol for the session. + /// + /// + /// A that represents the name of the subprotocol + /// if present. + /// + string Protocol { get; } + + /// + /// Gets the time that the session has started. + /// + /// + /// A that represents the time that the session + /// has started. + /// + DateTime StartTime { get; } + + #endregion + } +} diff --git a/websocket-sharp-core/Server/ServerState.cs b/websocket-sharp-core/Server/ServerState.cs new file mode 100644 index 000000000..2d7582920 --- /dev/null +++ b/websocket-sharp-core/Server/ServerState.cs @@ -0,0 +1,40 @@ +#region License +/* + * ServerState.cs + * + * The MIT License + * + * Copyright (c) 2013-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; + +namespace WebSocketSharp.Server +{ + internal enum ServerState + { + Ready, + Start, + ShuttingDown, + Stop + } +} diff --git a/websocket-sharp-core/Server/WebSocketBehavior.cs b/websocket-sharp-core/Server/WebSocketBehavior.cs new file mode 100644 index 000000000..b5e8ffeb7 --- /dev/null +++ b/websocket-sharp-core/Server/WebSocketBehavior.cs @@ -0,0 +1,1204 @@ +#region License +/* + * WebSocketBehavior.cs + * + * The MIT License + * + * 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 + * 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.IO; +using WebSocketSharp.Net; +using WebSocketSharp.Net.WebSockets; + +namespace WebSocketSharp.Server +{ + /// + /// Exposes a set of methods and properties used to define the behavior of + /// a WebSocket service provided by the or + /// . + /// + /// + /// 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; + + #endregion + + #region Protected Constructors + + /// + /// Initializes a new instance of the class. + /// + protected WebSocketBehavior () + { + _startTime = DateTime.MaxValue; + } + + #endregion + + #region Protected Properties + + /// + /// Gets the HTTP headers included in a WebSocket handshake request. + /// + /// + /// + /// A that contains the headers. + /// + /// + /// if the session has not started yet. + /// + /// + protected NameValueCollection Headers { + get { + return _context != null ? _context.Headers : null; + } + } + + /// + /// Gets the logging function. + /// + /// + /// + /// A that provides the logging function. + /// + /// + /// if the session has not started yet. + /// + /// + [Obsolete ("This property will be removed.")] + protected Logger Log { + get { + return _websocket != null ? _websocket.Log : null; + } + } + + /// + /// Gets the query string included in a WebSocket handshake request. + /// + /// + /// + /// A that contains the query + /// parameters. + /// + /// + /// An empty collection if not included. + /// + /// + /// if the session has not started yet. + /// + /// + protected NameValueCollection QueryString { + get { + return _context != null ? _context.QueryString : null; + } + } + + /// + /// Gets the management function for the sessions in the service. + /// + /// + /// + /// A that manages the sessions in + /// the service. + /// + /// + /// if the session has not started yet. + /// + /// + protected WebSocketSessionManager Sessions { + get { + return _sessions; + } + } + + #endregion + + #region Public Properties + + /// + /// Gets the current state of the WebSocket connection for a session. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It indicates the current state of the connection. + /// + /// + /// if the session has not + /// started yet. + /// + /// + public WebSocketState ConnectionState { + get { + return _websocket != null + ? _websocket.ReadyState + : WebSocketState.Connecting; + } + } + + /// + /// Gets the information in a WebSocket handshake request to the service. + /// + /// + /// + /// A instance that provides the access to + /// the information in the handshake request. + /// + /// + /// if the session has not started yet. + /// + /// + public WebSocketContext Context { + get { + return _context; + } + } + + /// + /// Gets or sets the delegate used to validate the HTTP cookies included in + /// a WebSocket handshake request to the service. + /// + /// + /// + /// A Func<CookieCollection, CookieCollection, bool> delegate + /// or if not needed. + /// + /// + /// The delegate invokes the method called when the WebSocket instance + /// for a session validates the handshake request. + /// + /// + /// 1st parameter passed to the method + /// contains the cookies to validate if present. + /// + /// + /// 2nd parameter passed to the method + /// receives the cookies to send to the client. + /// + /// + /// The method must return true if the cookies are valid. + /// + /// + /// The default value is . + /// + /// + public Func CookiesValidator { + get { + return _cookiesValidator; + } + + set { + _cookiesValidator = value; + } + } + + /// + /// Gets or sets a value indicating whether the WebSocket instance for + /// a session emits the message event when receives a ping. + /// + /// + /// + /// true if the WebSocket instance emits the message event + /// when receives a ping; otherwise, false. + /// + /// + /// The default value is false. + /// + /// + public bool EmitOnPing { + get { + return _websocket != null ? _websocket.EmitOnPing : _emitOnPing; + } + + set { + if (_websocket != null) { + _websocket.EmitOnPing = value; + return; + } + + _emitOnPing = value; + } + } + + /// + /// Gets the unique ID of a session. + /// + /// + /// + /// A that represents the unique ID of the session. + /// + /// + /// if the session has not started yet. + /// + /// + public string ID { + get { + return _id; + } + } + + /// + /// Gets or sets a value indicating whether the service ignores + /// the Sec-WebSocket-Extensions header included in a WebSocket + /// handshake request. + /// + /// + /// + /// true if the service ignores the extensions requested + /// from a client; otherwise, false. + /// + /// + /// The default value is false. + /// + /// + public bool IgnoreExtensions { + get { + return _ignoreExtensions; + } + + set { + _ignoreExtensions = value; + } + } + + /// + /// Gets or sets the delegate used to validate the Origin header included in + /// a WebSocket handshake request to the service. + /// + /// + /// + /// A Func<string, bool> delegate or + /// if not needed. + /// + /// + /// The delegate invokes the method called when the WebSocket instance + /// for a session validates the handshake request. + /// + /// + /// The parameter passed to the method is the value + /// of the Origin header or if the header is not + /// present. + /// + /// + /// The method must return true if the header value is valid. + /// + /// + /// The default value is . + /// + /// + public Func OriginValidator { + get { + return _originValidator; + } + + set { + _originValidator = value; + } + } + + /// + /// Gets or sets the name of the WebSocket subprotocol for the service. + /// + /// + /// + /// A that represents the name of the subprotocol. + /// + /// + /// The value specified for a set must be a token defined in + /// + /// RFC 2616. + /// + /// + /// The default value is an empty string. + /// + /// + /// + /// The set operation is not available if the session has already started. + /// + /// + /// The value specified for a set operation is not a token. + /// + public string Protocol { + get { + return _websocket != null + ? _websocket.Protocol + : (_protocol ?? String.Empty); + } + + set { + if (ConnectionState != WebSocketState.Connecting) { + var msg = "The session has already started."; + throw new InvalidOperationException (msg); + } + + if (value == null || value.Length == 0) { + _protocol = null; + return; + } + + if (!value.IsToken ()) + throw new ArgumentException ("Not a token.", "value"); + + _protocol = value; + } + } + + /// + /// Gets the time that a session has started. + /// + /// + /// + /// A that represents the time that the session + /// has started. + /// + /// + /// if the session has not started yet. + /// + /// + public DateTime StartTime { + get { + return _startTime; + } + } + + #endregion + + #region Private Methods + + private string checkHandshakeRequest (WebSocketContext context) + { + if (_originValidator != null) { + if (!_originValidator (context.Origin)) + return "It includes no Origin header or an invalid one."; + } + + if (_cookiesValidator != null) { + var req = context.CookieCollection; + var res = context.WebSocket.CookieCollection; + if (!_cookiesValidator (req, res)) + return "It includes no cookie or an invalid one."; + } + + return null; + } + + private void onClose (object sender, CloseEventArgs e) + { + if (_id == null) + return; + + _sessions.Remove (_id); + OnClose (e); + } + + private void onError (object sender, ErrorEventArgs e) + { + OnError (e); + } + + private void onMessage (object sender, MessageEventArgs e) + { + OnMessage (e); + } + + private void onOpen (object sender, EventArgs e) + { + _id = _sessions.Add (this); + if (_id == null) { + _websocket.Close (CloseStatusCode.Away); + return; + } + + _startTime = DateTime.Now; + OnOpen (); + } + + #endregion + + #region Internal Methods + + internal void Start (WebSocketContext context, WebSocketSessionManager sessions) + { + if (_websocket != null) { + _websocket.Log.Error ("A session instance cannot be reused."); + context.WebSocket.Close (HttpStatusCode.ServiceUnavailable); + + return; + } + + _context = context; + _sessions = sessions; + + _websocket = context.WebSocket; + _websocket.CustomHandshakeRequestChecker = checkHandshakeRequest; + _websocket.EmitOnPing = _emitOnPing; + _websocket.IgnoreExtensions = _ignoreExtensions; + _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.InternalAccept (); + } + + #endregion + + #region Protected Methods + + /// + /// Closes the WebSocket connection for a session. + /// + /// + /// This method does nothing if the current state of the connection is + /// Closing or Closed. + /// + /// + /// The session has not started yet. + /// + protected void Close () + { + if (_websocket == null) { + var msg = "The session has not started yet."; + throw new InvalidOperationException (msg); + } + + _websocket.Close (); + } + + /// + /// Closes the WebSocket connection for a session with the specified + /// code and reason. + /// + /// + /// This method does nothing if the current state of the connection is + /// Closing or Closed. + /// + /// + /// + /// A that represents the status code indicating + /// the reason for the close. + /// + /// + /// The status codes are defined in + /// + /// Section 7.4 of RFC 6455. + /// + /// + /// + /// + /// A that represents the reason for the close. + /// + /// + /// The size must be 123 bytes or less in UTF-8. + /// + /// + /// + /// The session has not started yet. + /// + /// + /// + /// is less than 1000 or greater than 4999. + /// + /// + /// -or- + /// + /// + /// The size of is greater than 123 bytes. + /// + /// + /// + /// + /// is 1010 (mandatory extension). + /// + /// + /// -or- + /// + /// + /// is 1005 (no status) and there is reason. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + protected void Close (ushort code, string reason) + { + if (_websocket == null) { + var msg = "The session has not started yet."; + throw new InvalidOperationException (msg); + } + + _websocket.Close (code, reason); + } + + /// + /// Closes the WebSocket connection for a session with the specified + /// code and reason. + /// + /// + /// This method does nothing if the current state of the connection is + /// Closing or Closed. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It represents the status code indicating the reason for the close. + /// + /// + /// + /// + /// A that represents the reason for the close. + /// + /// + /// The size must be 123 bytes or less in UTF-8. + /// + /// + /// + /// The session has not started yet. + /// + /// + /// The size of is greater than 123 bytes. + /// + /// + /// + /// is + /// . + /// + /// + /// -or- + /// + /// + /// is + /// and there is reason. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + protected void Close (CloseStatusCode code, string reason) + { + if (_websocket == null) { + var msg = "The session has not started yet."; + throw new InvalidOperationException (msg); + } + + _websocket.Close (code, reason); + } + + /// + /// Closes the WebSocket connection for a session asynchronously. + /// + /// + /// + /// This method does not wait for the close to be complete. + /// + /// + /// This method does nothing if the current state of the connection is + /// Closing or Closed. + /// + /// + /// + /// The session has not started yet. + /// + protected void CloseAsync () + { + if (_websocket == null) { + 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 code and reason. + /// + /// + /// + /// This method does not wait for the close to be complete. + /// + /// + /// This method does nothing if the current state of the connection is + /// Closing or Closed. + /// + /// + /// + /// + /// A that represents the status code indicating + /// the reason for the close. + /// + /// + /// The status codes are defined in + /// + /// Section 7.4 of RFC 6455. + /// + /// + /// + /// + /// A that represents the reason for the close. + /// + /// + /// The size must be 123 bytes or less in UTF-8. + /// + /// + /// + /// The session has not started yet. + /// + /// + /// + /// is less than 1000 or greater than 4999. + /// + /// + /// -or- + /// + /// + /// The size of is greater than 123 bytes. + /// + /// + /// + /// + /// is 1010 (mandatory extension). + /// + /// + /// -or- + /// + /// + /// is 1005 (no status) and there is reason. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + protected void CloseAsync (ushort code, string reason) + { + if (_websocket == null) { + var msg = "The session has not started yet."; + throw new InvalidOperationException (msg); + } + + _websocket.CloseAsync (code, reason); + } + + /// + /// Closes the WebSocket connection for a session asynchronously with + /// the specified code and reason. + /// + /// + /// + /// This method does not wait for the close to be complete. + /// + /// + /// This method does nothing if the current state of the connection is + /// Closing or Closed. + /// + /// + /// + /// + /// One of the enum values. + /// + /// + /// It represents the status code indicating the reason for the close. + /// + /// + /// + /// + /// A that represents the reason for the close. + /// + /// + /// The size must be 123 bytes or less in UTF-8. + /// + /// + /// + /// The session has not started yet. + /// + /// + /// + /// is + /// . + /// + /// + /// -or- + /// + /// + /// is + /// and there is reason. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// The size of is greater than 123 bytes. + /// + protected void CloseAsync (CloseStatusCode code, string reason) + { + if (_websocket == null) { + var msg = "The session has not started yet."; + throw new InvalidOperationException (msg); + } + + _websocket.CloseAsync (code, reason); + } + + /// + /// Calls the method with the specified message. + /// + /// + /// A that represents the error message. + /// + /// + /// An instance that represents the cause of + /// the error if present. + /// + /// + /// is . + /// + /// + /// is an empty string. + /// + [Obsolete ("This method will be removed.")] + protected void Error (string message, Exception exception) + { + if (message == null) + throw new ArgumentNullException ("message"); + + if (message.Length == 0) + throw new ArgumentException ("An empty string.", "message"); + + OnError (new ErrorEventArgs (message, exception)); + } + + /// + /// Called when the WebSocket connection for a session has been closed. + /// + /// + /// A that represents the event data passed + /// from a event. + /// + protected virtual void OnClose (CloseEventArgs e) + { + } + + /// + /// Called when the WebSocket instance for a session gets an error. + /// + /// + /// A that represents the event data passed + /// from a event. + /// + protected virtual void OnError (ErrorEventArgs e) + { + } + + /// + /// Called when the WebSocket instance for a session receives a message. + /// + /// + /// A that represents the event data passed + /// from a event. + /// + protected virtual void OnMessage (MessageEventArgs e) + { + } + + /// + /// Called when the WebSocket connection for a session has been established. + /// + protected virtual void OnOpen () + { + } + + /// + /// Sends the specified data to a client using the WebSocket connection. + /// + /// + /// An array of that represents the binary data to send. + /// + /// + /// The current state of the connection is not Open. + /// + /// + /// is . + /// + protected void Send (byte[] data) + { + if (_websocket == null) { + var msg = "The current state of the connection is not Open."; + throw new InvalidOperationException (msg); + } + + _websocket.Send (data); + } + + /// + /// Sends the specified file to a client using the WebSocket connection. + /// + /// + /// + /// A that specifies the file to send. + /// + /// + /// The file is sent as the binary data. + /// + /// + /// + /// The current state of the connection is not Open. + /// + /// + /// is . + /// + /// + /// + /// The file does not exist. + /// + /// + /// -or- + /// + /// + /// The file could not be opened. + /// + /// + protected void Send (FileInfo fileInfo) + { + if (_websocket == null) { + var msg = "The current state of the connection is not Open."; + throw new InvalidOperationException (msg); + } + + _websocket.Send (fileInfo); + } + + /// + /// Sends the specified data to a client using the WebSocket connection. + /// + /// + /// A that represents the text data to send. + /// + /// + /// The current state of the connection is not Open. + /// + /// + /// is . + /// + /// + /// could not be UTF-8-encoded. + /// + protected void Send (string data) + { + if (_websocket == null) { + var msg = "The current state of the connection is not Open."; + throw new InvalidOperationException (msg); + } + + _websocket.Send (data); + } + + /// + /// Sends the data from the specified stream to a client using + /// the WebSocket connection. + /// + /// + /// + /// A instance from which to read the data to send. + /// + /// + /// The data is sent as the binary data. + /// + /// + /// + /// An that specifies the number of bytes to send. + /// + /// + /// The current state of the connection is not Open. + /// + /// + /// is . + /// + /// + /// + /// cannot be read. + /// + /// + /// -or- + /// + /// + /// is less than 1. + /// + /// + /// -or- + /// + /// + /// No data could be read from . + /// + /// + protected void Send (Stream stream, int length) + { + if (_websocket == null) { + var msg = "The current state of the connection is not Open."; + throw new InvalidOperationException (msg); + } + + _websocket.Send (stream, length); + } + + /// + /// Sends the specified data to a client asynchronously using + /// the WebSocket connection. + /// + /// + /// This method does not wait for the send to be complete. + /// + /// + /// An array of that represents the binary data to send. + /// + /// + /// + /// An Action<bool> delegate or + /// if not needed. + /// + /// + /// The delegate invokes the method called when the send is complete. + /// + /// + /// true is passed to the method if the send has done with + /// no error; otherwise, false. + /// + /// + /// + /// The current state of the connection is not Open. + /// + /// + /// is . + /// + protected void SendAsync (byte[] data, Action completed) + { + if (_websocket == null) { + var msg = "The current state of the connection is not Open."; + throw new InvalidOperationException (msg); + } + + _websocket.SendAsync (data, completed); + } + + /// + /// Sends the specified file to a client asynchronously using + /// the WebSocket connection. + /// + /// + /// This method does not wait for the send to be complete. + /// + /// + /// + /// A that specifies the file to send. + /// + /// + /// The file is sent as the binary data. + /// + /// + /// + /// + /// An Action<bool> delegate or + /// if not needed. + /// + /// + /// The delegate invokes the method called when the send is complete. + /// + /// + /// true is passed to the method if the send has done with + /// no error; otherwise, false. + /// + /// + /// + /// The current state of the connection is not Open. + /// + /// + /// is . + /// + /// + /// + /// The file does not exist. + /// + /// + /// -or- + /// + /// + /// The file could not be opened. + /// + /// + protected void SendAsync (FileInfo fileInfo, Action completed) + { + if (_websocket == null) { + var msg = "The current state of the connection is not Open."; + throw new InvalidOperationException (msg); + } + + _websocket.SendAsync (fileInfo, completed); + } + + /// + /// Sends the specified data to a client asynchronously using + /// the WebSocket connection. + /// + /// + /// This method does not wait for the send to be complete. + /// + /// + /// A that represents the text data to send. + /// + /// + /// + /// An Action<bool> delegate or + /// if not needed. + /// + /// + /// The delegate invokes the method called when the send is complete. + /// + /// + /// true is passed to the method if the send has done with + /// no error; otherwise, false. + /// + /// + /// + /// The current state of the connection is not Open. + /// + /// + /// is . + /// + /// + /// could not be UTF-8-encoded. + /// + protected void SendAsync (string data, Action completed) + { + if (_websocket == null) { + var msg = "The current state of the connection is not Open."; + throw new InvalidOperationException (msg); + } + + _websocket.SendAsync (data, completed); + } + + /// + /// Sends the data from the specified stream to a client asynchronously + /// using the WebSocket connection. + /// + /// + /// This method does not wait for the send to be complete. + /// + /// + /// + /// A instance from which to read the data to send. + /// + /// + /// The data is sent as the binary data. + /// + /// + /// + /// An that specifies the number of bytes to send. + /// + /// + /// + /// An Action<bool> delegate or + /// if not needed. + /// + /// + /// The delegate invokes the method called when the send is complete. + /// + /// + /// true is passed to the method if the send has done with + /// no error; otherwise, false. + /// + /// + /// + /// The current state of the connection is not Open. + /// + /// + /// is . + /// + /// + /// + /// cannot be read. + /// + /// + /// -or- + /// + /// + /// is less than 1. + /// + /// + /// -or- + /// + /// + /// No data could be read from . + /// + /// + protected void SendAsync (Stream stream, int length, Action completed) + { + if (_websocket == null) { + var msg = "The current state of the connection is not Open."; + throw new InvalidOperationException (msg); + } + + _websocket.SendAsync (stream, length, completed); + } + + #endregion + } +} diff --git a/websocket-sharp-core/Server/WebSocketServer.cs b/websocket-sharp-core/Server/WebSocketServer.cs new file mode 100644 index 000000000..be7bca768 --- /dev/null +++ b/websocket-sharp-core/Server/WebSocketServer.cs @@ -0,0 +1,1518 @@ +#region License +/* + * WebSocketServer.cs + * + * The MIT License + * + * Copyright (c) 2012-2015 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Contributors +/* + * Contributors: + * - Juan Manuel Lallana + * - Jonas Hovgaard + * - Liryna + * - Rohan Singh + */ +#endregion + +using System; +using System.Collections.Generic; +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; + +namespace WebSocketSharp.Server +{ + /// + /// Provides a WebSocket protocol server. + /// + /// + /// This class can provide multiple WebSocket services. + /// + public class WebSocketServer + { + #region Private Fields + + private System.Net.IPAddress _address; + private bool _allowForwardedRequest; + private AuthenticationSchemes _authSchemes; + private static readonly string _defaultRealm; + private bool _dnsStyle; + private string _hostname; + private TcpListener _listener; + 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 + + #region Public Constructors + + /// + /// Initializes a new instance of the class. + /// + /// + /// The new instance listens for incoming handshake requests on + /// and port 80. + /// + public WebSocketServer () + { + var addr = System.Net.IPAddress.Any; + init (addr.ToString (), addr, 80, false); + } + + /// + /// Initializes a new instance of the class + /// with the specified . + /// + /// + /// + /// The new instance listens for incoming handshake requests on + /// and . + /// + /// + /// It provides secure connections if is 443. + /// + /// + /// + /// An that represents the number of the port + /// on which to listen. + /// + /// + /// is less than 1 or greater than 65535. + /// + public WebSocketServer (int port) + : this (port, port == 443) + { + } + + /// + /// Initializes a new instance of the class + /// with the specified . + /// + /// + /// + /// The new instance listens for incoming handshake requests on + /// the IP address of the host of and + /// the 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. + /// + /// + /// The new instance provides secure connections if the scheme of + /// is wss. + /// + /// + /// + /// A that represents the WebSocket URL of the server. + /// + /// + /// is . + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is invalid. + /// + /// + public WebSocketServer (string url) + { + if (url == null) + throw new ArgumentNullException ("url"); + + if (url.Length == 0) + throw new ArgumentException ("An empty 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 == 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 . + /// + /// + /// The new instance listens for incoming handshake requests on + /// and . + /// + /// + /// An that represents the number of the port + /// on which to listen. + /// + /// + /// A : true if the new instance provides + /// secure connections; otherwise, false. + /// + /// + /// is less than 1 or greater than 65535. + /// + public WebSocketServer (int port, bool secure) + { + if (!port.IsPortNumber ()) { + var msg = "Less than 1 or greater than 65535."; + throw new ArgumentOutOfRangeException ("port", msg); + } + + var addr = System.Net.IPAddress.Any; + init (addr.ToString (), addr, port, secure); + } + + /// + /// Initializes a new instance of the class + /// with the specified and . + /// + /// + /// + /// The new instance listens for incoming handshake requests on + /// and . + /// + /// + /// It provides secure connections if is 443. + /// + /// + /// + /// A that represents the local + /// IP address on which to listen. + /// + /// + /// An that represents the number of the port + /// on which to listen. + /// + /// + /// is . + /// + /// + /// is not a local IP address. + /// + /// + /// is less than 1 or greater than 65535. + /// + public WebSocketServer (System.Net.IPAddress address, int port) + : this (address, port, port == 443) + { + } + + /// + /// Initializes a new instance of the class + /// with the specified , , + /// and . + /// + /// + /// The new instance listens for incoming handshake requests on + /// and . + /// + /// + /// A that represents the local + /// IP address on which to listen. + /// + /// + /// An that represents the number of the port + /// on which to listen. + /// + /// + /// A : true if the new instance provides + /// secure connections; otherwise, false. + /// + /// + /// is . + /// + /// + /// is not a local IP address. + /// + /// + /// 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"); + + if (!port.IsPortNumber ()) { + var msg = "Less than 1 or greater than 65535."; + throw new ArgumentOutOfRangeException ("port", msg); + } + + init (address.ToString (), address, port, secure); + } + + #endregion + + #region Public Properties + + /// + /// Gets the 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 { + return _address; + } + } + + /// + /// Gets or sets a value indicating whether the server accepts every + /// handshake request without checking the request URI. + /// + /// + /// The set operation does nothing if the server has already started or + /// it is shutting down. + /// + /// + /// + /// true if the server accepts every handshake request without + /// checking the request URI; otherwise, false. + /// + /// + /// The default value is false. + /// + /// + public bool AllowForwardedRequest { + get { + return _allowForwardedRequest; + } + + set { + string msg; + if (!canSet (out msg)) { + _log.Warn (msg); + return; + } + + lock (_sync) { + if (!canSet (out msg)) { + _log.Warn (msg); + return; + } + + _allowForwardedRequest = value; + } + } + } + + /// + /// Gets or sets the scheme used to authenticate the clients. + /// + /// + /// The set operation does nothing if the server has already started or + /// it is shutting down. + /// + /// + /// + /// One of the + /// enum values. + /// + /// + /// It represents the scheme used to authenticate the clients. + /// + /// + /// The default value is + /// . + /// + /// + public AuthenticationSchemes AuthenticationSchemes { + get { + return _authSchemes; + } + + set { + string msg; + if (!canSet (out msg)) { + _log.Warn (msg); + return; + } + + lock (_sync) { + if (!canSet (out msg)) { + _log.Warn (msg); + return; + } + + _authSchemes = value; + } + } + } + + /// + /// Gets a value indicating whether the server has started. + /// + /// + /// true if the server has started; otherwise, false. + /// + public bool IsListening { + get { + return _state == ServerState.Start; + } + } + + /// + /// Gets a value indicating whether secure connections are provided. + /// + /// + /// true if this instance provides secure connections; otherwise, + /// false. + /// + public bool IsSecure { + get { + return _secure; + } + } + + /// + /// Gets or sets a value indicating whether the server cleans up + /// the inactive sessions periodically. + /// + /// + /// The set operation does nothing if the server has already started or + /// it is shutting down. + /// + /// + /// + /// true if the server cleans up the inactive sessions every + /// 60 seconds; otherwise, false. + /// + /// + /// The default value is true. + /// + /// + public bool KeepClean { + get { + return _services.KeepClean; + } + + set { + _services.KeepClean = value; + } + } + + /// + /// Gets the logging function for the server. + /// + /// + /// The default logging level is . + /// + /// + /// A that provides the logging function. + /// + public Logger Log { + get { + return _log; + } + } + + /// + /// Gets the port of the server. + /// + /// + /// An that represents the number of the port + /// on which to listen for incoming handshake requests. + /// + public int Port { + get { + return _port; + } + } + + /// + /// Gets or sets the realm used for authentication. + /// + /// + /// + /// "SECRET AREA" is used as the realm if the value is + /// or an empty string. + /// + /// + /// The set operation does nothing if the server has + /// already started or it is shutting down. + /// + /// + /// + /// + /// A or by default. + /// + /// + /// That string represents the name of the realm. + /// + /// + public string Realm { + get { + return _realm; + } + + set { + string msg; + if (!canSet (out msg)) { + _log.Warn (msg); + return; + } + + lock (_sync) { + if (!canSet (out msg)) { + _log.Warn (msg); + return; + } + + _realm = value; + } + } + } + + /// + /// Gets or sets a value indicating whether the server is allowed to + /// be bound to an address that is already in use. + /// + /// + /// + /// You should set this property to true if you would + /// like to resolve to wait for socket in TIME_WAIT state. + /// + /// + /// The set operation does nothing if the server has already + /// started or it is shutting down. + /// + /// + /// + /// + /// 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 { + return _reuseAddress; + } + + set { + string msg; + if (!canSet (out msg)) { + _log.Warn (msg); + return; + } + + lock (_sync) { + if (!canSet (out msg)) { + _log.Warn (msg); + return; + } + + _reuseAddress = value; + } + } + } + + /// + /// Gets the configuration for secure connection. + /// + /// + /// This configuration will be referenced when attempts to start, + /// so it must be configured before the start method is called. + /// + /// + /// A that represents + /// the configuration used to provide secure connections. + /// + /// + /// This instance does not provide secure connections. + /// + public ServerSslConfiguration SslConfiguration { + get { + if (!_secure) { + var msg = "This instance does not provide secure connections."; + throw new InvalidOperationException (msg); + } + + return getSslConfiguration (); + } + } + + /// + /// Gets or sets the delegate used to find the credentials + /// for an identity. + /// + /// + /// + /// No credentials are found if the method invoked by + /// the delegate returns or + /// the value is . + /// + /// + /// The set operation does nothing if the server has + /// already started or it is shutting down. + /// + /// + /// + /// + /// A Func<, + /// > delegate or + /// if not needed. + /// + /// + /// That delegate invokes the method called for finding + /// the credentials used to authenticate a client. + /// + /// + /// The default value is . + /// + /// + public Func UserCredentialsFinder { + get { + return _userCredFinder; + } + + set { + string msg; + if (!canSet (out msg)) { + _log.Warn (msg); + return; + } + + lock (_sync) { + if (!canSet (out msg)) { + _log.Warn (msg); + return; + } + + _userCredFinder = value; + } + } + } + + /// + /// Gets or sets the time to wait for the response to the WebSocket Ping or + /// Close. + /// + /// + /// The set operation does nothing if the server has already started or + /// it is shutting down. + /// + /// + /// + /// A 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 { + _services.WaitTime = value; + } + } + + /// + /// Gets the management function for the WebSocket services + /// provided by the server. + /// + /// + /// A that manages + /// the WebSocket services provided by the server. + /// + public WebSocketServiceManager WebSocketServices { + get { + return _services; + } + } + + #endregion + + #region Private Methods + + private void abort () + { + lock (_sync) { + if (_state != ServerState.Start) + return; + + _state = ServerState.ShuttingDown; + } + + try { + try { + _listener.Stop (); + } + finally { + _services.Stop (1006, String.Empty); + } + } + catch { + } + + _state = ServerState.Stop; + } + + private bool authenticateClient (TcpListenerWebSocketContext context) + { + if (_authSchemes == AuthenticationSchemes.Anonymous) + return true; + + if (_authSchemes == AuthenticationSchemes.None) + return false; + + return context.Authenticate (_authSchemes, _realmInUse, _userCredFinder); + } + + private bool canSet (out string message) + { + message = null; + + if (_state == ServerState.Start) { + message = "The server has already started."; + return false; + } + + if (_state == ServerState.ShuttingDown) { + message = "The server is shutting down."; + return false; + } + + return true; + } + + private bool checkHostNameForRequest (string name) + { + return !_dnsStyle + || Uri.CheckHostName (name) != UriHostNameType.Dns + || name == _hostname; + } + + private static bool checkSslConfiguration ( + ServerSslConfiguration configuration, out string message + ) + { + message = null; + + if (configuration.ServerCertificate == null) { + message = "There is no server certificate for secure connection."; + return false; + } + + return true; + } + + private string getRealm () + { + 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; + + _authSchemes = AuthenticationSchemes.Anonymous; + _dnsStyle = Uri.CheckHostName (hostname) == UriHostNameType.Dns; + _listener = new TcpListener (address, port); + _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) { + context.Close (HttpStatusCode.BadRequest); + return; + } + + if (!_allowForwardedRequest) { + if (uri.Port != _port) { + context.Close (HttpStatusCode.BadRequest); + return; + } + + if (!checkHostNameForRequest (uri.DnsSafeHost)) { + 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 (path, out host)) { + context.Close (HttpStatusCode.NotImplemented); + return; + } + + host.StartSession (context); + } + + private void receiveRequest () + { + while (true) { + TcpClient cl = null; + try { + cl = _listener.AcceptTcpClient (); + ThreadPool.QueueUserWorkItem ( + state => { + try { + var ctx = new TcpListenerWebSocketContext ( + cl, null, _secure, _sslConfigInUse, _log + ); + + processRequest (ctx); + } + catch (Exception ex) { + _log.Error (ex.Message); + _log.Debug (ex.ToString ()); + + cl.Close (); + } + } + ); + } + catch (SocketException ex) { + if (_state == ServerState.ShuttingDown) { + _log.Info ("The underlying listener is stopped."); + break; + } + + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + + break; + } + catch (Exception ex) { + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + + if (cl != null) + cl.Close (); + + break; + } + } + + if (_state != ServerState.ShuttingDown) + abort (); + } + + private void start (ServerSslConfiguration sslConfig) + { + if (_state == ServerState.Start) { + _log.Info ("The server has already started."); + return; + } + + if (_state == ServerState.ShuttingDown) { + _log.Warn ("The server is shutting down."); + return; + } + + lock (_sync) { + if (_state == ServerState.Start) { + _log.Info ("The server has already started."); + return; + } + + if (_state == ServerState.ShuttingDown) { + _log.Warn ("The server is shutting down."); + return; + } + + _sslConfigInUse = sslConfig; + _realmInUse = getRealm (); + + _services.Start (); + try { + startReceiving (); + } + catch { + _services.Stop (1011, String.Empty); + throw; + } + + _state = ServerState.Start; + } + } + + private void startReceiving () + { + if (_reuseAddress) { + _listener.Server.SetSocketOption ( + SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true + ); + } + + try { + _listener.Start (); + } + catch (Exception ex) { + var msg = "The underlying listener has failed to start."; + throw new InvalidOperationException (msg, ex); + } + + _receiveThread = new Thread (new ThreadStart (receiveRequest)); + _receiveThread.IsBackground = true; + _receiveThread.Start (); + } + + private void stop (ushort code, string reason) + { + if (_state == ServerState.Ready) { + _log.Info ("The server is not started."); + return; + } + + if (_state == ServerState.ShuttingDown) { + _log.Info ("The server is shutting down."); + return; + } + + if (_state == ServerState.Stop) { + _log.Info ("The server has already stopped."); + return; + } + + lock (_sync) { + if (_state == ServerState.ShuttingDown) { + _log.Info ("The server is shutting down."); + return; + } + + if (_state == ServerState.Stop) { + _log.Info ("The server has already stopped."); + return; + } + + _state = ServerState.ShuttingDown; + } + + try { + var threw = false; + try { + stopReceiving (5000); + } + catch { + threw = true; + throw; + } + finally { + try { + _services.Stop (code, reason); + } + catch { + if (!threw) + throw; + } + } + } + finally { + _state = ServerState.Stop; + } + } + + private void stopReceiving (int millisecondsTimeout) + { + try { + _listener.Stop (); + } + catch (Exception ex) { + var msg = "The underlying listener has failed to stop."; + throw new InvalidOperationException (msg, ex); + } + + _receiveThread.Join (millisecondsTimeout); + } + + 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 = "It includes either or both path and query components."; + + return false; + } + + return true; + } + + #endregion + + #region Public Methods + + /// + /// Adds a WebSocket service with the specified behavior, path, + /// and delegate. + /// + /// + /// + /// A that represents an absolute path to + /// the service to add. + /// + /// + /// / is trimmed from the end of the string if present. + /// + /// + /// + /// + /// A Func<TBehavior> delegate. + /// + /// + /// It invokes the method called when creating a new session + /// instance for the service. + /// + /// + /// The method must create a new instance of the specified + /// behavior class and return it. + /// + /// + /// + /// + /// The type of the behavior for the service. + /// + /// + /// It must inherit the class. + /// + /// + /// + /// + /// is . + /// + /// + /// -or- + /// + /// + /// is . + /// + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is not an absolute path. + /// + /// + /// -or- + /// + /// + /// includes either or both + /// query and fragment components. + /// + /// + /// -or- + /// + /// + /// is already in use. + /// + /// + [Obsolete ("This method will be removed. Use added one instead.")] + public void AddWebSocketService ( + string path, Func creator + ) + where TBehavior : WebSocketBehavior + { + if (path == null) + throw new ArgumentNullException ("path"); + + if (creator == null) + throw new ArgumentNullException ("creator"); + + if (path.Length == 0) + throw new ArgumentException ("An empty string.", "path"); + + if (path[0] != '/') + throw new ArgumentException ("Not an absolute path.", "path"); + + if (path.IndexOfAny (new[] { '?', '#' }) > -1) { + var msg = "It includes either or both query and fragment components."; + throw new ArgumentException (msg, "path"); + } + + _services.Add (path, creator); + } + + /// + /// Adds a WebSocket service with the specified behavior and path. + /// + /// + /// + /// A that represents an absolute path to + /// the service to add. + /// + /// + /// / is trimmed from the end of the string if present. + /// + /// + /// + /// + /// The type of the behavior for the service. + /// + /// + /// It must inherit the class. + /// + /// + /// And also, it must have a public parameterless constructor. + /// + /// + /// + /// is . + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is not an absolute path. + /// + /// + /// -or- + /// + /// + /// includes either or both + /// query and fragment components. + /// + /// + /// -or- + /// + /// + /// is already in use. + /// + /// + public void AddWebSocketService (string path) + where TBehaviorWithNew : WebSocketBehavior, new () + { + _services.AddService (path, null); + } + + /// + /// Adds a WebSocket service with the specified behavior, path, + /// and delegate. + /// + /// + /// + /// A that represents an absolute path to + /// the service to add. + /// + /// + /// / is trimmed from the end of the string if present. + /// + /// + /// + /// + /// An Action<TBehaviorWithNew> delegate or + /// if not needed. + /// + /// + /// The delegate invokes the method called when initializing + /// a new session instance for the service. + /// + /// + /// + /// + /// The type of the behavior for the service. + /// + /// + /// It must inherit the class. + /// + /// + /// And also, it must have a public parameterless constructor. + /// + /// + /// + /// is . + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is not an absolute path. + /// + /// + /// -or- + /// + /// + /// includes either or both + /// query and fragment components. + /// + /// + /// -or- + /// + /// + /// is already in use. + /// + /// + public void AddWebSocketService ( + string path, Action initializer + ) + where TBehaviorWithNew : WebSocketBehavior, new () + { + _services.AddService (path, initializer); + } + + /// + /// Removes a WebSocket service with the specified path. + /// + /// + /// The service is stopped with close status 1001 (going away) + /// if it has already started. + /// + /// + /// true if the service is successfully found and removed; + /// otherwise, false. + /// + /// + /// + /// A that represents an absolute path to + /// the service to remove. + /// + /// + /// / is trimmed from the end of the string if present. + /// + /// + /// + /// is . + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is not an absolute path. + /// + /// + /// -or- + /// + /// + /// includes either or both + /// query and fragment components. + /// + /// + public bool RemoveWebSocketService (string path) + { + return _services.RemoveService (path); + } + + /// + /// Starts receiving incoming handshake requests. + /// + /// + /// This method does nothing if the server has already started or + /// it is shutting down. + /// + /// + /// + /// There is no server certificate for secure connection. + /// + /// + /// -or- + /// + /// + /// The underlying has failed to start. + /// + /// + public void Start () + { + ServerSslConfiguration sslConfig = null; + + if (_secure) { + sslConfig = new ServerSslConfiguration (getSslConfiguration ()); + + string msg; + if (!checkSslConfiguration (sslConfig, out msg)) + throw new InvalidOperationException (msg); + } + + start (sslConfig); + } + + /// + /// Stops receiving incoming handshake requests. + /// + /// + /// The underlying has failed to stop. + /// + public void Stop () + { + stop (1001, String.Empty); + } + + /// + /// Stops receiving incoming handshake requests and closes each connection + /// with the specified code and reason. + /// + /// + /// + /// A that represents the status code indicating + /// the reason for the close. + /// + /// + /// The status codes are defined in + /// + /// Section 7.4 of RFC 6455. + /// + /// + /// + /// + /// A that represents the reason for the close. + /// + /// + /// The size must be 123 bytes or less in UTF-8. + /// + /// + /// + /// + /// is less than 1000 or greater than 4999. + /// + /// + /// -or- + /// + /// + /// The size of is greater than 123 bytes. + /// + /// + /// + /// + /// is 1010 (mandatory extension). + /// + /// + /// -or- + /// + /// + /// is 1005 (no status) and there is reason. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// The underlying has failed to stop. + /// + [Obsolete ("This method will be removed.")] + public void Stop (ushort code, string reason) + { + if (!code.IsCloseStatusCode ()) { + var msg = "Less than 1000 or greater than 4999."; + throw new ArgumentOutOfRangeException ("code", msg); + } + + if (code == 1010) { + var msg = "1010 cannot be used."; + throw new ArgumentException (msg, "code"); + } + + if (!reason.IsNullOrEmpty ()) { + 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); + } + } + + stop (code, reason); + } + + /// + /// Stops receiving incoming handshake requests and closes each connection + /// with the specified code and reason. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It represents the status code indicating the reason for the close. + /// + /// + /// + /// + /// A that represents the reason for the close. + /// + /// + /// The size must be 123 bytes or less in UTF-8. + /// + /// + /// + /// + /// is + /// . + /// + /// + /// -or- + /// + /// + /// is + /// and there is reason. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// The size of is greater than 123 bytes. + /// + /// + /// The underlying has failed to stop. + /// + [Obsolete ("This method will be removed.")] + public void Stop (CloseStatusCode code, string reason) + { + if (code == CloseStatusCode.MandatoryExtension) { + var msg = "MandatoryExtension cannot be used."; + throw new ArgumentException (msg, "code"); + } + + if (!reason.IsNullOrEmpty ()) { + 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); + } + } + + stop ((ushort) code, reason); + } + + #endregion + } +} diff --git a/websocket-sharp-core/Server/WebSocketServiceHost.cs b/websocket-sharp-core/Server/WebSocketServiceHost.cs new file mode 100644 index 000000000..1da76427a --- /dev/null +++ b/websocket-sharp-core/Server/WebSocketServiceHost.cs @@ -0,0 +1,224 @@ +#region License +/* + * WebSocketServiceHost.cs + * + * The MIT License + * + * Copyright (c) 2012-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 + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Contributors +/* + * Contributors: + * - Juan Manuel Lallana + */ +#endregion + +using System; +using WebSocketSharp.Net.WebSockets; + +namespace WebSocketSharp.Server +{ + /// + /// Exposes the methods and properties used to access the information in + /// a WebSocket service provided by the or + /// . + /// + /// + /// 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 + /// with the specified and . + /// + /// + /// A that represents the absolute path to the service. + /// + /// + /// A that represents the logging function for the service. + /// + protected WebSocketServiceHost (string path, Logger log) + { + _path = path; + _log = log; + + _sessions = new WebSocketSessionManager (log); + } + + #endregion + + #region Internal Properties + + internal ServerState State { + get { + 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; + } + } + + #endregion + + #region Public Properties + + /// + /// Gets or sets a value indicating whether the service cleans up + /// the inactive sessions periodically. + /// + /// + /// The set operation does nothing if the service has already started or + /// it is shutting down. + /// + /// + /// true if the service cleans up the inactive sessions every + /// 60 seconds; otherwise, false. + /// + public bool KeepClean { + get { + return _sessions.KeepClean; + } + + set { + _sessions.KeepClean = value; + } + } + + /// + /// Gets the path to the service. + /// + /// + /// A that represents the absolute path to + /// the service. + /// + public string Path { + get { + return _path; + } + } + + /// + /// Gets the management function for the sessions in the service. + /// + /// + /// A that manages the sessions in + /// the service. + /// + public WebSocketSessionManager Sessions { + get { + return _sessions; + } + } + + /// + /// Gets the of the behavior of the service. + /// + /// + /// A that represents the type of the behavior of + /// the service. + /// + public abstract Type BehaviorType { get; } + + /// + /// Gets or sets the time to wait for the response to the WebSocket Ping or + /// Close. + /// + /// + /// The set operation does nothing if the service has already started or + /// it is shutting down. + /// + /// + /// A to wait for the response. + /// + /// + /// The value specified for a set operation is zero or less. + /// + public TimeSpan WaitTime { + get { + return _sessions.WaitTime; + } + + set { + _sessions.WaitTime = value; + } + } + + #endregion + + #region Internal Methods + + internal void Start () + { + _sessions.Start (); + } + + internal void StartSession (WebSocketContext context) + { + CreateSession ().Start (context, _sessions); + } + + internal void Stop (ushort code, string reason) + { + _sessions.Stop (code, reason); + } + + #endregion + + #region Protected Methods + + /// + /// Creates a new session for the service. + /// + /// + /// A instance that represents + /// the new session. + /// + protected abstract WebSocketBehavior CreateSession (); + + #endregion + } +} diff --git a/websocket-sharp-core/Server/WebSocketServiceHost`1.cs b/websocket-sharp-core/Server/WebSocketServiceHost`1.cs new file mode 100644 index 000000000..d4ca6a2d1 --- /dev/null +++ b/websocket-sharp-core/Server/WebSocketServiceHost`1.cs @@ -0,0 +1,102 @@ +#region License +/* + * WebSocketServiceHost`1.cs + * + * The MIT License + * + * Copyright (c) 2015-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 + * 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; + +namespace WebSocketSharp.Server +{ + internal class WebSocketServiceHost : WebSocketServiceHost + where TBehavior : WebSocketBehavior + { + #region Private Fields + + private Func _creator; + + #endregion + + #region Internal Constructors + + internal WebSocketServiceHost ( + string path, Func creator, Logger log + ) + : this (path, creator, null, log) + { + } + + internal WebSocketServiceHost ( + string path, + Func creator, + Action initializer, + Logger log + ) + : base (path, log) + { + _creator = createCreator (creator, initializer); + } + + #endregion + + #region Public Properties + + public override Type BehaviorType { + get { + return typeof (TBehavior); + } + } + + #endregion + + #region Private Methods + + private Func createCreator ( + Func creator, Action initializer + ) + { + if (initializer == null) + return creator; + + return () => { + var ret = creator (); + initializer (ret); + + return ret; + }; + } + + #endregion + + #region Protected Methods + + protected override WebSocketBehavior CreateSession () + { + return _creator (); + } + + #endregion + } +} diff --git a/websocket-sharp-core/Server/WebSocketServiceManager.cs b/websocket-sharp-core/Server/WebSocketServiceManager.cs new file mode 100644 index 000000000..ee1256fcf --- /dev/null +++ b/websocket-sharp-core/Server/WebSocketServiceManager.cs @@ -0,0 +1,1078 @@ +#region License +/* + * WebSocketServiceManager.cs + * + * The MIT License + * + * Copyright (c) 2012-2015 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; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using WebSocketSharp.Net; + +namespace WebSocketSharp.Server +{ + /// + /// Provides the management function for the WebSocket services. + /// + /// + /// This class manages the WebSocket services provided by + /// the or . + /// + public class WebSocketServiceManager + { + #region Private Fields + + private volatile bool _clean; + private Dictionary _hosts; + private Logger _log; + private volatile ServerState _state; + private object _sync; + private TimeSpan _waitTime; + + #endregion + + #region Internal Constructors + + internal WebSocketServiceManager (Logger log) + { + _log = log; + + _clean = true; + _hosts = new Dictionary (); + _state = ServerState.Ready; + _sync = ((ICollection) _hosts).SyncRoot; + _waitTime = TimeSpan.FromSeconds (1); + } + + #endregion + + #region Public Properties + + /// + /// Gets the number of the WebSocket services. + /// + /// + /// An that represents the number of the services. + /// + public int Count { + get { + lock (_sync) + return _hosts.Count; + } + } + + /// + /// Gets the host instances for the WebSocket services. + /// + /// + /// + /// An IEnumerable<WebSocketServiceHost> instance. + /// + /// + /// It provides an enumerator which supports the iteration over + /// the collection of the host instances. + /// + /// + public IEnumerable Hosts { + get { + lock (_sync) + return _hosts.Values.ToList (); + } + } + + /// + /// Gets the host instance for a WebSocket service with the specified path. + /// + /// + /// + /// A instance or + /// if not found. + /// + /// + /// The host instance provides the function to access + /// the information in the service. + /// + /// + /// + /// + /// A that represents an absolute path to + /// the service to find. + /// + /// + /// / is trimmed from the end of the string if present. + /// + /// + /// + /// is . + /// + /// + /// + /// is empty. + /// + /// + /// -or- + /// + /// + /// is not an absolute path. + /// + /// + /// -or- + /// + /// + /// includes either or both + /// query and fragment components. + /// + /// + 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] != '/') + throw new ArgumentException ("Not an absolute path.", "path"); + + if (path.IndexOfAny (new[] { '?', '#' }) > -1) { + var msg = "It includes either or both query and fragment components."; + throw new ArgumentException (msg, "path"); + } + + WebSocketServiceHost host; + InternalTryGetServiceHost (path, out host); + + return host; + } + } + + /// + /// Gets or sets a value indicating whether the inactive sessions in + /// the WebSocket services are cleaned up periodically. + /// + /// + /// The set operation does nothing if the server has already started or + /// it is shutting down. + /// + /// + /// true if the inactive sessions are cleaned up every 60 seconds; + /// otherwise, false. + /// + public bool KeepClean { + get { + return _clean; + } + + set { + string msg; + if (!canSet (out msg)) { + _log.Warn (msg); + return; + } + + lock (_sync) { + if (!canSet (out msg)) { + _log.Warn (msg); + return; + } + + foreach (var host in _hosts.Values) + host.KeepClean = value; + + _clean = value; + } + } + } + + /// + /// Gets the paths for the WebSocket services. + /// + /// + /// + /// An IEnumerable<string> instance. + /// + /// + /// It provides an enumerator which supports the iteration over + /// the collection of the paths. + /// + /// + public IEnumerable Paths { + get { + lock (_sync) + return _hosts.Keys.ToList (); + } + } + + /// + /// Gets the total number of the sessions in the WebSocket services. + /// + /// + /// An that represents the total number of + /// the sessions in the services. + /// + [Obsolete ("This property will be removed.")] + public int SessionCount { + get { + var cnt = 0; + foreach (var host in Hosts) { + if (_state != ServerState.Start) + break; + + cnt += host.Sessions.Count; + } + + return cnt; + } + } + + /// + /// Gets or sets the time to wait for the response to the WebSocket Ping or + /// Close. + /// + /// + /// The set operation does nothing if the server has already started or + /// it is shutting down. + /// + /// + /// A to wait for the response. + /// + /// + /// The value specified for a set operation is zero or less. + /// + public TimeSpan WaitTime { + get { + return _waitTime; + } + + set { + if (value <= TimeSpan.Zero) + throw new ArgumentOutOfRangeException ("value", "Zero or less."); + + string msg; + if (!canSet (out msg)) { + _log.Warn (msg); + return; + } + + lock (_sync) { + if (!canSet (out msg)) { + _log.Warn (msg); + return; + } + + foreach (var host in _hosts.Values) + host.WaitTime = value; + + _waitTime = value; + } + } + } + + #endregion + + #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) { + _log.Error ("The server is shutting down."); + break; + } + + host.Sessions.Broadcast (opcode, data, cache); + } + + if (completed != null) + completed (); + } + catch (Exception ex) { + _log.Error (ex.Message); + _log.Debug (ex.ToString ()); + } + finally { + cache.Clear (); + } + } + + private void broadcast (Opcode opcode, Stream stream, Action completed) + { + var cache = new Dictionary (); + + try { + foreach (var host in Hosts) { + if (_state != ServerState.Start) { + _log.Error ("The server is shutting down."); + break; + } + + host.Sessions.Broadcast (opcode, stream, cache); + } + + if (completed != null) + completed (); + } + catch (Exception ex) { + _log.Error (ex.Message); + _log.Debug (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) { + _log.Error ("The server is shutting down."); + break; + } + + var res = host.Sessions.Broadping (frameAsBytes, timeout); + ret.Add (host.Path, res); + } + + return ret; + } + + private bool canSet (out string message) + { + message = null; + + if (_state == ServerState.Start) { + message = "The server has already started."; + return false; + } + + if (_state == ServerState.ShuttingDown) { + message = "The server is shutting down."; + return false; + } + + return true; + } + + #endregion + + #region Internal Methods + + internal void Add (string path, Func creator) + where TBehavior : WebSocketBehavior + { + path = path.TrimSlashFromEnd (); + + lock (_sync) { + WebSocketServiceHost host; + if (_hosts.TryGetValue (path, out host)) + throw new ArgumentException ("Already in use.", "path"); + + host = new WebSocketServiceHost ( + path, creator, null, _log + ); + + 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 + ) + { + path = path.TrimSlashFromEnd (); + + lock (_sync) + return _hosts.TryGetValue (path, out host); + } + + internal void Start () + { + lock (_sync) { + foreach (var host in _hosts.Values) + host.Start (); + + _state = ServerState.Start; + } + } + + internal void Stop (ushort code, string reason) + { + lock (_sync) { + _state = ServerState.ShuttingDown; + + foreach (var host in _hosts.Values) + host.Stop (code, reason); + + _state = ServerState.Stop; + } + } + + #endregion + + #region Public Methods + + /// + /// Adds a WebSocket service with the specified behavior, path, + /// and delegate. + /// + /// + /// + /// A that represents an absolute path to + /// the service to add. + /// + /// + /// / is trimmed from the end of the string if present. + /// + /// + /// + /// + /// An Action<TBehavior> delegate or + /// if not needed. + /// + /// + /// The delegate invokes the method called when initializing + /// a new session instance for the service. + /// + /// + /// + /// + /// The type of the behavior for the service. + /// + /// + /// It must inherit the class. + /// + /// + /// And also, it must have a public parameterless constructor. + /// + /// + /// + /// is . + /// + /// + /// + /// is empty. + /// + /// + /// -or- + /// + /// + /// is not an absolute path. + /// + /// + /// -or- + /// + /// + /// includes either or both + /// query and fragment components. + /// + /// + /// -or- + /// + /// + /// is already in use. + /// + /// + public void AddService ( + string path, Action initializer + ) + where TBehavior : WebSocketBehavior, new () + { + if (path == null) + throw new ArgumentNullException ("path"); + + if (path.Length == 0) + throw new ArgumentException ("An empty string.", "path"); + + if (path[0] != '/') + throw new ArgumentException ("Not an absolute path.", "path"); + + if (path.IndexOfAny (new[] { '?', '#' }) > -1) { + var msg = "It includes either or both query and fragment components."; + throw new ArgumentException (msg, "path"); + } + + path = path.TrimSlashFromEnd (); + + lock (_sync) { + WebSocketServiceHost host; + if (_hosts.TryGetValue (path, out host)) + throw new ArgumentException ("Already in use.", "path"); + + host = new WebSocketServiceHost ( + path, () => new TBehavior (), initializer, _log + ); + + if (!_clean) + host.KeepClean = false; + + if (_waitTime != host.WaitTime) + host.WaitTime = _waitTime; + + if (_state == ServerState.Start) + host.Start (); + + _hosts.Add (path, host); + } + } + + /// + /// Sends to every client in the WebSocket services. + /// + /// + /// An array of that represents the binary data to send. + /// + /// + /// The current state of the manager is not Start. + /// + /// + /// is . + /// + [Obsolete ("This method will be removed.")] + public void Broadcast (byte[] data) + { + if (_state != ServerState.Start) { + var msg = "The current state of the manager is not Start."; + throw new InvalidOperationException (msg); + } + + if (data == null) + throw new ArgumentNullException ("data"); + + if (data.LongLength <= WebSocket.FragmentLength) + broadcast (Opcode.Binary, data, null); + else + broadcast (Opcode.Binary, new MemoryStream (data), null); + } + + /// + /// Sends to every client in the WebSocket services. + /// + /// + /// A that represents the text data to send. + /// + /// + /// The current state of the manager is not Start. + /// + /// + /// is . + /// + /// + /// could not be UTF-8-encoded. + /// + [Obsolete ("This method will be removed.")] + public void Broadcast (string data) + { + if (_state != ServerState.Start) { + var msg = "The current state of the manager is not Start."; + 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"); + } + + if (bytes.LongLength <= WebSocket.FragmentLength) + broadcast (Opcode.Text, bytes, null); + else + broadcast (Opcode.Text, new MemoryStream (bytes), null); + } + + /// + /// Sends asynchronously to every client in + /// the WebSocket services. + /// + /// + /// This method does not wait for the send to be complete. + /// + /// + /// An array of that represents the binary data to send. + /// + /// + /// + /// An delegate or + /// if not needed. + /// + /// + /// The delegate invokes the method called when the send is complete. + /// + /// + /// + /// The current state of the manager is not Start. + /// + /// + /// is . + /// + [Obsolete ("This method will be removed.")] + public void BroadcastAsync (byte[] data, Action completed) + { + if (_state != ServerState.Start) { + var msg = "The current state of the manager is not Start."; + throw new InvalidOperationException (msg); + } + + if (data == null) + throw new ArgumentNullException ("data"); + + if (data.LongLength <= WebSocket.FragmentLength) + broadcastAsync (Opcode.Binary, data, completed); + else + broadcastAsync (Opcode.Binary, new MemoryStream (data), completed); + } + + /// + /// Sends asynchronously to every client in + /// the WebSocket services. + /// + /// + /// This method does not wait for the send to be complete. + /// + /// + /// A that represents the text data to send. + /// + /// + /// + /// An delegate or + /// if not needed. + /// + /// + /// The delegate invokes the method called when the send is complete. + /// + /// + /// + /// The current state of the manager is not Start. + /// + /// + /// is . + /// + /// + /// could not be UTF-8-encoded. + /// + [Obsolete ("This method will be removed.")] + public void BroadcastAsync (string data, Action completed) + { + if (_state != ServerState.Start) { + var msg = "The current state of the manager is not Start."; + 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"); + } + + if (bytes.LongLength <= WebSocket.FragmentLength) + broadcastAsync (Opcode.Text, bytes, completed); + else + broadcastAsync (Opcode.Text, new MemoryStream (bytes), completed); + } + + /// + /// Sends the data from asynchronously to + /// every client in the WebSocket services. + /// + /// + /// + /// The data is sent as the binary data. + /// + /// + /// This method does not wait for the send to be complete. + /// + /// + /// + /// A instance from which to read the data to send. + /// + /// + /// An that specifies the number of bytes to send. + /// + /// + /// + /// An delegate or + /// if not needed. + /// + /// + /// The delegate invokes the method called when the send is complete. + /// + /// + /// + /// The current state of the manager is not Start. + /// + /// + /// is . + /// + /// + /// + /// cannot be read. + /// + /// + /// -or- + /// + /// + /// is less than 1. + /// + /// + /// -or- + /// + /// + /// No data could be read from . + /// + /// + [Obsolete ("This method will be removed.")] + public void BroadcastAsync (Stream stream, int length, Action completed) + { + if (_state != ServerState.Start) { + var msg = "The current state of the manager 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) { + _log.Warn ( + String.Format ( + "Only {0} byte(s) of data could be read from the stream.", + len + ) + ); + } + + if (len <= WebSocket.FragmentLength) + broadcastAsync (Opcode.Binary, bytes, completed); + else + broadcastAsync (Opcode.Binary, new MemoryStream (bytes), completed); + } + + /// + /// Sends a ping to every client in the WebSocket services. + /// + /// + /// + /// A Dictionary<string, Dictionary<string, bool>>. + /// + /// + /// It represents a collection of pairs of a service path and another + /// collection of pairs of a session ID and a value indicating whether + /// a pong has been received from the client within a time. + /// + /// + /// + /// The current state of the manager is not Start. + /// + [Obsolete ("This method will be removed.")] + public Dictionary> Broadping () + { + if (_state != ServerState.Start) { + var msg = "The current state of the manager is not Start."; + throw new InvalidOperationException (msg); + } + + return broadping (WebSocketFrame.EmptyPingBytes, _waitTime); + } + + /// + /// Sends a ping with to every client in + /// the WebSocket services. + /// + /// + /// + /// A Dictionary<string, Dictionary<string, bool>>. + /// + /// + /// It represents a collection of pairs of a service path and another + /// collection of pairs of a session ID and a value indicating whether + /// a pong has been received from the client within a time. + /// + /// + /// + /// + /// A that represents the message to send. + /// + /// + /// The size must be 125 bytes or less in UTF-8. + /// + /// + /// + /// The current state of the manager is not Start. + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// The size of is greater than 125 bytes. + /// + [Obsolete ("This method will be removed.")] + public Dictionary> Broadping (string message) + { + if (_state != ServerState.Start) { + var msg = "The current state of the manager is not Start."; + throw new InvalidOperationException (msg); + } + + if (message.IsNullOrEmpty ()) + return broadping (WebSocketFrame.EmptyPingBytes, _waitTime); + + byte[] bytes; + if (!message.TryGetUTF8EncodedBytes (out bytes)) { + var msg = "It could not be UTF-8-encoded."; + throw new ArgumentException (msg, "message"); + } + + if (bytes.Length > 125) { + var msg = "Its size is greater than 125 bytes."; + throw new ArgumentOutOfRangeException ("message", msg); + } + + var frame = WebSocketFrame.CreatePingFrame (bytes, false); + return broadping (frame.ToArray (), _waitTime); + } + + /// + /// Removes all WebSocket services managed by the manager. + /// + /// + /// A service is stopped with close status 1001 (going away) + /// if it has already started. + /// + public void Clear () + { + List hosts = null; + + lock (_sync) { + hosts = _hosts.Values.ToList (); + _hosts.Clear (); + } + + foreach (var host in hosts) { + if (host.State == ServerState.Start) + host.Stop (1001, String.Empty); + } + } + + /// + /// Removes a WebSocket service with the specified path. + /// + /// + /// The service is stopped with close status 1001 (going away) + /// if it has already started. + /// + /// + /// true if the service is successfully found and removed; + /// otherwise, false. + /// + /// + /// + /// A that represents an absolute path to + /// the service to remove. + /// + /// + /// / is trimmed from the end of the string if present. + /// + /// + /// + /// is . + /// + /// + /// + /// is empty. + /// + /// + /// -or- + /// + /// + /// is not an absolute path. + /// + /// + /// -or- + /// + /// + /// includes either or both + /// query and fragment components. + /// + /// + public bool RemoveService (string path) + { + if (path == null) + throw new ArgumentNullException ("path"); + + if (path.Length == 0) + throw new ArgumentException ("An empty string.", "path"); + + if (path[0] != '/') + throw new ArgumentException ("Not an absolute path.", "path"); + + if (path.IndexOfAny (new[] { '?', '#' }) > -1) { + var msg = "It includes either or both query and fragment components."; + throw new ArgumentException (msg, "path"); + } + + path = path.TrimSlashFromEnd (); + + WebSocketServiceHost host; + lock (_sync) { + if (!_hosts.TryGetValue (path, out host)) + return false; + + _hosts.Remove (path); + } + + if (host.State == ServerState.Start) + host.Stop (1001, String.Empty); + + return true; + } + + /// + /// Tries to get the host instance for a WebSocket service with + /// the specified path. + /// + /// + /// true if the service is successfully found; otherwise, + /// false. + /// + /// + /// + /// A that represents an absolute path to + /// the service to find. + /// + /// + /// / is trimmed from the end of the string if present. + /// + /// + /// + /// + /// When this method returns, a + /// instance or if not found. + /// + /// + /// The host instance provides the function to access + /// the information in the service. + /// + /// + /// + /// is . + /// + /// + /// + /// is empty. + /// + /// + /// -or- + /// + /// + /// is not an absolute path. + /// + /// + /// -or- + /// + /// + /// includes either or both + /// query and fragment components. + /// + /// + public bool TryGetServiceHost (string path, out WebSocketServiceHost host) + { + if (path == null) + throw new ArgumentNullException ("path"); + + if (path.Length == 0) + throw new ArgumentException ("An empty string.", "path"); + + if (path[0] != '/') + throw new ArgumentException ("Not an absolute path.", "path"); + + if (path.IndexOfAny (new[] { '?', '#' }) > -1) { + var msg = "It includes either or both query and fragment components."; + throw new ArgumentException (msg, "path"); + } + + return InternalTryGetServiceHost (path, out host); + } + + #endregion + } +} diff --git a/websocket-sharp-core/Server/WebSocketSessionManager.cs b/websocket-sharp-core/Server/WebSocketSessionManager.cs new file mode 100644 index 000000000..f7144b0ce --- /dev/null +++ b/websocket-sharp-core/Server/WebSocketSessionManager.cs @@ -0,0 +1,1695 @@ +#region License +/* + * WebSocketSessionManager.cs + * + * The MIT License + * + * Copyright (c) 2012-2015 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; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Timers; + +namespace WebSocketSharp.Server +{ + /// + /// Provides the management function for the sessions in a WebSocket service. + /// + /// + /// This class manages the sessions in a WebSocket service provided by + /// the or . + /// + public class WebSocketSessionManager + { + #region Private Fields + + private volatile bool _clean; + private object _forSweep; + private Logger _log; + private Dictionary _sessions; + private volatile ServerState _state; + private volatile bool _sweeping; + private System.Timers.Timer _sweepTimer; + private object _sync; + private TimeSpan _waitTime; + + #endregion + + #region Internal Constructors + + internal WebSocketSessionManager (Logger log) + { + _log = log; + + _clean = true; + _forSweep = new object (); + _sessions = new Dictionary (); + _state = ServerState.Ready; + _sync = ((ICollection) _sessions).SyncRoot; + _waitTime = TimeSpan.FromSeconds (1); + + setSweepTimer (60000); + } + + #endregion + + #region Internal Properties + + internal ServerState State { + get { + return _state; + } + } + + #endregion + + #region Public Properties + + /// + /// Gets the IDs for the active sessions in the WebSocket service. + /// + /// + /// + /// An IEnumerable<string> 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)) { + if (res.Value) + yield return res.Key; + } + } + } + + /// + /// Gets the number of the sessions in the WebSocket service. + /// + /// + /// An that represents the number of the sessions. + /// + public int Count { + get { + lock (_sync) + return _sessions.Count; + } + } + + /// + /// Gets the IDs for the sessions in the WebSocket service. + /// + /// + /// + /// An IEnumerable<string> 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.Start) + return Enumerable.Empty (); + + lock (_sync) { + if (_state != ServerState.Start) + return Enumerable.Empty (); + + return _sessions.Keys.ToList (); + } + } + } + + /// + /// Gets the IDs for the inactive sessions in the WebSocket service. + /// + /// + /// + /// An IEnumerable<string> 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)) { + if (!res.Value) + yield return res.Key; + } + } + } + + /// + /// Gets the session instance with . + /// + /// + /// + /// A instance or + /// if not found. + /// + /// + /// The session instance provides the function to access the information + /// in the session. + /// + /// + /// + /// A that represents the ID of the session to find. + /// + /// + /// is . + /// + /// + /// is an empty string. + /// + 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); + + return session; + } + } + + /// + /// Gets or sets a value indicating whether the inactive sessions in + /// the WebSocket service are cleaned up periodically. + /// + /// + /// The set operation does nothing if the service has already started or + /// it is shutting down. + /// + /// + /// true if the inactive sessions are cleaned up every 60 seconds; + /// otherwise, false. + /// + public bool KeepClean { + get { + return _clean; + } + + set { + string msg; + if (!canSet (out msg)) { + _log.Warn (msg); + return; + } + + lock (_sync) { + if (!canSet (out msg)) { + _log.Warn (msg); + return; + } + + _clean = value; + } + } + } + + /// + /// Gets the session instances in the WebSocket service. + /// + /// + /// + /// An IEnumerable<IWebSocketSession> instance. + /// + /// + /// It provides an enumerator which supports the iteration over + /// the collection of the session instances. + /// + /// + public IEnumerable Sessions { + get { + if (_state != ServerState.Start) + return Enumerable.Empty (); + + lock (_sync) { + if (_state != ServerState.Start) + return Enumerable.Empty (); + + return _sessions.Values.ToList (); + } + } + } + + /// + /// Gets or sets the time to wait for the response to the WebSocket Ping or + /// Close. + /// + /// + /// The set operation does nothing if the service has already started or + /// it is shutting down. + /// + /// + /// A to wait for the response. + /// + /// + /// The value specified for a set operation is zero or less. + /// + public TimeSpan WaitTime { + get { + return _waitTime; + } + + set { + if (value <= TimeSpan.Zero) + throw new ArgumentOutOfRangeException ("value", "Zero or less."); + + string msg; + if (!canSet (out msg)) { + _log.Warn (msg); + return; + } + + lock (_sync) { + if (!canSet (out msg)) { + _log.Warn (msg); + return; + } + + _waitTime = value; + } + } + } + + #endregion + + #region Private Methods + + private void broadcast (Opcode opcode, byte[] data, Action completed) + { + var cache = new Dictionary (); + + try { + foreach (var session in Sessions) { + if (_state != ServerState.Start) { + _log.Error ("The service is shutting down."); + break; + } + + session.Context.WebSocket.Send (opcode, data, cache); + } + + if (completed != null) + completed (); + } + catch (Exception ex) { + _log.Error (ex.Message); + _log.Debug (ex.ToString ()); + } + finally { + cache.Clear (); + } + } + + private void broadcast (Opcode opcode, Stream stream, Action completed) + { + var cache = new Dictionary (); + + try { + foreach (var session in Sessions) { + if (_state != ServerState.Start) { + _log.Error ("The service is shutting down."); + break; + } + + session.Context.WebSocket.Send (opcode, stream, cache); + } + + if (completed != null) + completed (); + } + catch (Exception ex) { + _log.Error (ex.Message); + _log.Debug (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) + { + var ret = new Dictionary (); + + foreach (var session in Sessions) { + if (_state != ServerState.Start) { + _log.Error ("The service is shutting down."); + break; + } + + var res = session.Context.WebSocket.Ping (frameAsBytes, _waitTime); + ret.Add (session.ID, res); + } + + return ret; + } + + private bool canSet (out string message) + { + message = null; + + if (_state == ServerState.Start) { + message = "The service has already started."; + return false; + } + + if (_state == ServerState.ShuttingDown) { + message = "The service is shutting down."; + return false; + } + + return true; + } + + private static string createID () + { + return Guid.NewGuid ().ToString ("N"); + } + + private void setSweepTimer (double interval) + { + _sweepTimer = new System.Timers.Timer (interval); + _sweepTimer.Elapsed += (sender, e) => Sweep (); + } + + private void stop (PayloadData payloadData, bool send) + { + var bytes = send + ? WebSocketFrame.CreateCloseFrame (payloadData, false).ToArray () + : null; + + lock (_sync) { + _state = ServerState.ShuttingDown; + + _sweepTimer.Enabled = false; + foreach (var session in _sessions.Values.ToList ()) + session.Context.WebSocket.Close (payloadData, bytes); + + _state = ServerState.Stop; + } + } + + private bool tryGetSession (string id, out IWebSocketSession session) + { + session = null; + + if (_state != ServerState.Start) + return false; + + lock (_sync) { + if (_state != ServerState.Start) + return false; + + return _sessions.TryGetValue (id, out session); + } + } + + #endregion + + #region Internal Methods + + internal string Add (IWebSocketSession session) + { + lock (_sync) { + if (_state != ServerState.Start) + return null; + + var id = createID (); + _sessions.Add (id, session); + + return id; + } + } + + internal void Broadcast ( + Opcode opcode, byte[] data, Dictionary cache + ) + { + foreach (var session in Sessions) { + if (_state != ServerState.Start) { + _log.Error ("The service is shutting down."); + break; + } + + session.Context.WebSocket.Send (opcode, data, cache); + } + } + + internal void Broadcast ( + Opcode opcode, Stream stream, Dictionary cache + ) + { + foreach (var session in Sessions) { + if (_state != ServerState.Start) { + _log.Error ("The service is shutting down."); + break; + } + + session.Context.WebSocket.Send (opcode, stream, cache); + } + } + + internal Dictionary Broadping ( + byte[] frameAsBytes, TimeSpan timeout + ) + { + var ret = new Dictionary (); + + foreach (var session in Sessions) { + if (_state != ServerState.Start) { + _log.Error ("The service is shutting down."); + break; + } + + var res = session.Context.WebSocket.Ping (frameAsBytes, timeout); + ret.Add (session.ID, res); + } + + return ret; + } + + internal bool Remove (string id) + { + lock (_sync) + return _sessions.Remove (id); + } + + internal void Start () + { + lock (_sync) { + _sweepTimer.Enabled = _clean; + _state = ServerState.Start; + } + } + + internal void Stop (ushort code, string reason) + { + if (code == 1005) { // == no status + stop (PayloadData.Empty, true); + return; + } + + stop (new PayloadData (code, reason), !code.IsReserved ()); + } + + #endregion + + #region Public Methods + + /// + /// Sends to every client in the WebSocket service. + /// + /// + /// An array of that represents the binary data to send. + /// + /// + /// The current state of the manager is not Start. + /// + /// + /// is . + /// + public void Broadcast (byte[] data) + { + if (_state != ServerState.Start) { + var msg = "The current state of the manager is not Start."; + throw new InvalidOperationException (msg); + } + + if (data == null) + throw new ArgumentNullException ("data"); + + if (data.LongLength <= WebSocket.FragmentLength) + broadcast (Opcode.Binary, data, null); + else + broadcast (Opcode.Binary, new MemoryStream (data), null); + } + + /// + /// Sends to every client in the WebSocket service. + /// + /// + /// A that represents the text data to send. + /// + /// + /// The current state of the manager is not Start. + /// + /// + /// is . + /// + /// + /// could not be UTF-8-encoded. + /// + public void Broadcast (string data) + { + if (_state != ServerState.Start) { + var msg = "The current state of the manager is not Start."; + 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"); + } + + if (bytes.LongLength <= WebSocket.FragmentLength) + broadcast (Opcode.Text, bytes, null); + else + broadcast (Opcode.Text, new MemoryStream (bytes), null); + } + + /// + /// Sends the data from to every client in + /// the WebSocket service. + /// + /// + /// The data is sent as the binary data. + /// + /// + /// A instance from which to read the data to send. + /// + /// + /// An that specifies the number of bytes to send. + /// + /// + /// The current state of the manager is not Start. + /// + /// + /// is . + /// + /// + /// + /// cannot be read. + /// + /// + /// -or- + /// + /// + /// is less than 1. + /// + /// + /// -or- + /// + /// + /// No data could be read from . + /// + /// + public void Broadcast (Stream stream, int length) + { + if (_state != ServerState.Start) { + var msg = "The current state of the manager 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) { + _log.Warn ( + String.Format ( + "Only {0} byte(s) of data could be read from the stream.", + len + ) + ); + } + + if (len <= WebSocket.FragmentLength) + broadcast (Opcode.Binary, bytes, null); + else + broadcast (Opcode.Binary, new MemoryStream (bytes), null); + } + + /// + /// Sends asynchronously to every client in + /// the WebSocket service. + /// + /// + /// This method does not wait for the send to be complete. + /// + /// + /// An array of that represents the binary data to send. + /// + /// + /// + /// An delegate or + /// if not needed. + /// + /// + /// The delegate invokes the method called when the send is complete. + /// + /// + /// + /// The current state of the manager is not Start. + /// + /// + /// is . + /// + public void BroadcastAsync (byte[] data, Action completed) + { + if (_state != ServerState.Start) { + var msg = "The current state of the manager is not Start."; + throw new InvalidOperationException (msg); + } + + if (data == null) + throw new ArgumentNullException ("data"); + + if (data.LongLength <= WebSocket.FragmentLength) + broadcastAsync (Opcode.Binary, data, completed); + else + broadcastAsync (Opcode.Binary, new MemoryStream (data), completed); + } + + /// + /// Sends asynchronously to every client in + /// the WebSocket service. + /// + /// + /// This method does not wait for the send to be complete. + /// + /// + /// A that represents the text data to send. + /// + /// + /// + /// An delegate or + /// if not needed. + /// + /// + /// The delegate invokes the method called when the send is complete. + /// + /// + /// + /// The current state of the manager is not Start. + /// + /// + /// is . + /// + /// + /// could not be UTF-8-encoded. + /// + public void BroadcastAsync (string data, Action completed) + { + if (_state != ServerState.Start) { + var msg = "The current state of the manager is not Start."; + 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"); + } + + if (bytes.LongLength <= WebSocket.FragmentLength) + broadcastAsync (Opcode.Text, bytes, completed); + else + broadcastAsync (Opcode.Text, new MemoryStream (bytes), completed); + } + + /// + /// Sends the data from asynchronously to + /// every client in the WebSocket service. + /// + /// + /// + /// The data is sent as the binary data. + /// + /// + /// This method does not wait for the send to be complete. + /// + /// + /// + /// A instance from which to read the data to send. + /// + /// + /// An that specifies the number of bytes to send. + /// + /// + /// + /// An delegate or + /// if not needed. + /// + /// + /// The delegate invokes the method called when the send is complete. + /// + /// + /// + /// The current state of the manager is not Start. + /// + /// + /// is . + /// + /// + /// + /// cannot be read. + /// + /// + /// -or- + /// + /// + /// is less than 1. + /// + /// + /// -or- + /// + /// + /// No data could be read from . + /// + /// + public void BroadcastAsync (Stream stream, int length, Action completed) + { + if (_state != ServerState.Start) { + var msg = "The current state of the manager 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) { + _log.Warn ( + String.Format ( + "Only {0} byte(s) of data could be read from the stream.", + len + ) + ); + } + + if (len <= WebSocket.FragmentLength) + broadcastAsync (Opcode.Binary, bytes, completed); + else + broadcastAsync (Opcode.Binary, new MemoryStream (bytes), completed); + } + + /// + /// Sends a ping to every client in the WebSocket service. + /// + /// + /// + /// A Dictionary<string, bool>. + /// + /// + /// It represents a collection of pairs of a session ID and + /// a value indicating whether a pong has been received from + /// the client within a time. + /// + /// + /// + /// The current state of the manager is not Start. + /// + [Obsolete ("This method will be removed.")] + public Dictionary Broadping () + { + if (_state != ServerState.Start) { + var msg = "The current state of the manager is not Start."; + throw new InvalidOperationException (msg); + } + + return Broadping (WebSocketFrame.EmptyPingBytes, _waitTime); + } + + /// + /// Sends a ping with to every client in + /// the WebSocket service. + /// + /// + /// + /// A Dictionary<string, bool>. + /// + /// + /// It represents a collection of pairs of a session ID and + /// a value indicating whether a pong has been received from + /// the client within a time. + /// + /// + /// + /// + /// A that represents the message to send. + /// + /// + /// The size must be 125 bytes or less in UTF-8. + /// + /// + /// + /// The current state of the manager is not Start. + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// The size of is greater than 125 bytes. + /// + [Obsolete ("This method will be removed.")] + public Dictionary Broadping (string message) + { + if (_state != ServerState.Start) { + var msg = "The current state of the manager is not Start."; + throw new InvalidOperationException (msg); + } + + if (message.IsNullOrEmpty ()) + return Broadping (WebSocketFrame.EmptyPingBytes, _waitTime); + + byte[] bytes; + if (!message.TryGetUTF8EncodedBytes (out bytes)) { + var msg = "It could not be UTF-8-encoded."; + throw new ArgumentException (msg, "message"); + } + + if (bytes.Length > 125) { + var msg = "Its size is greater than 125 bytes."; + throw new ArgumentOutOfRangeException ("message", msg); + } + + var frame = WebSocketFrame.CreatePingFrame (bytes, false); + return Broadping (frame.ToArray (), _waitTime); + } + + /// + /// Closes the specified session. + /// + /// + /// A that represents the ID of the session to close. + /// + /// + /// is . + /// + /// + /// is an empty string. + /// + /// + /// The session could not be found. + /// + public void CloseSession (string id) + { + IWebSocketSession session; + if (!TryGetSession (id, out session)) { + var msg = "The session could not be found."; + throw new InvalidOperationException (msg); + } + + session.Context.WebSocket.Close (); + } + + /// + /// Closes the specified session with and + /// . + /// + /// + /// A that represents the ID of the session to close. + /// + /// + /// + /// A that represents the status code indicating + /// the reason for the close. + /// + /// + /// The status codes are defined in + /// + /// Section 7.4 of RFC 6455. + /// + /// + /// + /// + /// A that represents the reason for the close. + /// + /// + /// The size must be 123 bytes or less in UTF-8. + /// + /// + /// + /// is . + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is 1010 (mandatory extension). + /// + /// + /// -or- + /// + /// + /// is 1005 (no status) and there is + /// . + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// The session could not be found. + /// + /// + /// + /// is less than 1000 or greater than 4999. + /// + /// + /// -or- + /// + /// + /// The size of is greater than 123 bytes. + /// + /// + public void CloseSession (string id, ushort code, string reason) + { + IWebSocketSession session; + if (!TryGetSession (id, out session)) { + var msg = "The session could not be found."; + throw new InvalidOperationException (msg); + } + + session.Context.WebSocket.Close (code, reason); + } + + /// + /// Closes the specified session with and + /// . + /// + /// + /// A that represents the ID of the session to close. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It represents the status code indicating the reason for the close. + /// + /// + /// + /// + /// A that represents the reason for the close. + /// + /// + /// The size must be 123 bytes or less in UTF-8. + /// + /// + /// + /// is . + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is + /// . + /// + /// + /// -or- + /// + /// + /// is + /// and there is + /// . + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// The session could not be found. + /// + /// + /// The size of is greater than 123 bytes. + /// + public void CloseSession (string id, CloseStatusCode code, string reason) + { + IWebSocketSession session; + if (!TryGetSession (id, out session)) { + var msg = "The session could not be found."; + throw new InvalidOperationException (msg); + } + + session.Context.WebSocket.Close (code, reason); + } + + /// + /// Sends a ping to the client using the specified session. + /// + /// + /// true if the send has done with no error and a pong has been + /// received from the client within a time; otherwise, false. + /// + /// + /// A that represents the ID of the session. + /// + /// + /// is . + /// + /// + /// is an empty string. + /// + /// + /// The session could not be found. + /// + public bool PingTo (string id) + { + IWebSocketSession session; + if (!TryGetSession (id, out session)) { + var msg = "The session could not be found."; + throw new InvalidOperationException (msg); + } + + return session.Context.WebSocket.Ping (); + } + + /// + /// Sends a ping with to the client using + /// the specified session. + /// + /// + /// true if the send has done with no error and a pong has been + /// received from the client within a time; otherwise, false. + /// + /// + /// + /// A that represents the message to send. + /// + /// + /// The size must be 125 bytes or less in UTF-8. + /// + /// + /// + /// A that represents the ID of the session. + /// + /// + /// is . + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// The session could not be found. + /// + /// + /// The size of is greater than 125 bytes. + /// + public bool PingTo (string message, string id) + { + IWebSocketSession session; + if (!TryGetSession (id, out session)) { + var msg = "The session could not be found."; + throw new InvalidOperationException (msg); + } + + return session.Context.WebSocket.Ping (message); + } + + /// + /// Sends to the client using the specified session. + /// + /// + /// An array of that represents the binary data to send. + /// + /// + /// A that represents the ID of the session. + /// + /// + /// + /// is . + /// + /// + /// -or- + /// + /// + /// is . + /// + /// + /// + /// is an empty string. + /// + /// + /// + /// The session could not be found. + /// + /// + /// -or- + /// + /// + /// The current state of the WebSocket connection is not Open. + /// + /// + public void SendTo (byte[] data, string id) + { + IWebSocketSession session; + if (!TryGetSession (id, out session)) { + var msg = "The session could not be found."; + throw new InvalidOperationException (msg); + } + + session.Context.WebSocket.Send (data); + } + + /// + /// Sends to the client using the specified session. + /// + /// + /// A that represents the text data to send. + /// + /// + /// A that represents the ID of the session. + /// + /// + /// + /// is . + /// + /// + /// -or- + /// + /// + /// is . + /// + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// + /// The session could not be found. + /// + /// + /// -or- + /// + /// + /// The current state of the WebSocket connection is not Open. + /// + /// + public void SendTo (string data, string id) + { + IWebSocketSession session; + if (!TryGetSession (id, out session)) { + var msg = "The session could not be found."; + throw new InvalidOperationException (msg); + } + + session.Context.WebSocket.Send (data); + } + + /// + /// Sends the data from to the client using + /// the specified session. + /// + /// + /// The data is sent as the binary data. + /// + /// + /// A instance from which to read the data to send. + /// + /// + /// An that specifies the number of bytes to send. + /// + /// + /// A that represents the ID of the session. + /// + /// + /// + /// is . + /// + /// + /// -or- + /// + /// + /// is . + /// + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// cannot be read. + /// + /// + /// -or- + /// + /// + /// is less than 1. + /// + /// + /// -or- + /// + /// + /// No data could be read from . + /// + /// + /// + /// + /// The session could not be found. + /// + /// + /// -or- + /// + /// + /// The current state of the WebSocket connection 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.Context.WebSocket.Send (stream, length); + } + + /// + /// Sends asynchronously to the client using + /// the specified session. + /// + /// + /// This method does not wait for the send to be complete. + /// + /// + /// An array of that represents the binary data to send. + /// + /// + /// A that represents the ID of the session. + /// + /// + /// + /// An Action<bool> delegate or + /// if not needed. + /// + /// + /// The delegate invokes the method called when the send is complete. + /// + /// + /// true is passed to the method if the send has done with + /// no error; otherwise, false. + /// + /// + /// + /// + /// is . + /// + /// + /// -or- + /// + /// + /// is . + /// + /// + /// + /// is an empty string. + /// + /// + /// + /// The session could not be found. + /// + /// + /// -or- + /// + /// + /// The current state of the WebSocket connection is not Open. + /// + /// + public void SendToAsync (byte[] data, string id, Action completed) + { + IWebSocketSession session; + if (!TryGetSession (id, out session)) { + var msg = "The session could not be found."; + throw new InvalidOperationException (msg); + } + + session.Context.WebSocket.SendAsync (data, completed); + } + + /// + /// Sends asynchronously to the client using + /// the specified session. + /// + /// + /// This method does not wait for the send to be complete. + /// + /// + /// A that represents the text data to send. + /// + /// + /// A that represents the ID of the session. + /// + /// + /// + /// An Action<bool> delegate or + /// if not needed. + /// + /// + /// The delegate invokes the method called when the send is complete. + /// + /// + /// true is passed to the method if the send has done with + /// no error; otherwise, false. + /// + /// + /// + /// + /// is . + /// + /// + /// -or- + /// + /// + /// is . + /// + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// + /// The session could not be found. + /// + /// + /// -or- + /// + /// + /// The current state of the WebSocket connection is not Open. + /// + /// + public void SendToAsync (string data, string id, Action completed) + { + IWebSocketSession session; + if (!TryGetSession (id, out session)) { + var msg = "The session could not be found."; + throw new InvalidOperationException (msg); + } + + session.Context.WebSocket.SendAsync (data, completed); + } + + /// + /// Sends the data from asynchronously to + /// the client using the specified session. + /// + /// + /// + /// The data is sent as the binary data. + /// + /// + /// This method does not wait for the send to be complete. + /// + /// + /// + /// A instance from which to read the data to send. + /// + /// + /// An that specifies the number of bytes to send. + /// + /// + /// A that represents the ID of the session. + /// + /// + /// + /// An Action<bool> delegate or + /// if not needed. + /// + /// + /// The delegate invokes the method called when the send is complete. + /// + /// + /// true is passed to the method if the send has done with + /// no error; otherwise, false. + /// + /// + /// + /// + /// is . + /// + /// + /// -or- + /// + /// + /// is . + /// + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// cannot be read. + /// + /// + /// -or- + /// + /// + /// is less than 1. + /// + /// + /// -or- + /// + /// + /// No data could be read from . + /// + /// + /// + /// + /// The session could not be found. + /// + /// + /// -or- + /// + /// + /// The current state of the WebSocket connection is not Open. + /// + /// + public void SendToAsync ( + Stream stream, int length, string id, Action completed + ) + { + IWebSocketSession session; + if (!TryGetSession (id, out session)) { + var msg = "The session could not be found."; + throw new InvalidOperationException (msg); + } + + session.Context.WebSocket.SendAsync (stream, length, completed); + } + + /// + /// Cleans up the inactive sessions in the WebSocket service. + /// + public void Sweep () + { + if (_sweeping) { + _log.Info ("The sweeping is already in progress."); + return; + } + + lock (_forSweep) { + if (_sweeping) { + _log.Info ("The sweeping is already in progress."); + return; + } + + _sweeping = true; + } + + foreach (var id in InactiveIDs) { + if (_state != ServerState.Start) + break; + + lock (_sync) { + if (_state != ServerState.Start) + break; + + IWebSocketSession session; + if (_sessions.TryGetValue (id, out session)) { + var state = session.ConnectionState; + if (state == WebSocketState.Open) + session.Context.WebSocket.Close (CloseStatusCode.Abnormal); + else if (state == WebSocketState.Closing) + continue; + else + _sessions.Remove (id); + } + } + } + + _sweeping = false; + } + + /// + /// Tries to get the session instance with . + /// + /// + /// true if the session is successfully found; otherwise, + /// false. + /// + /// + /// A that represents the ID of the session to find. + /// + /// + /// + /// When this method returns, a + /// instance or if not found. + /// + /// + /// The session instance provides the function to access + /// the information in the session. + /// + /// + /// + /// is . + /// + /// + /// is an empty string. + /// + public bool TryGetSession (string id, out IWebSocketSession session) + { + if (id == null) + throw new ArgumentNullException ("id"); + + if (id.Length == 0) + throw new ArgumentException ("An empty string.", "id"); + + return tryGetSession (id, out session); + } + + #endregion + } +} diff --git a/websocket-sharp-core/WebSocket.cs b/websocket-sharp-core/WebSocket.cs new file mode 100644 index 000000000..011dee00d --- /dev/null +++ b/websocket-sharp-core/WebSocket.cs @@ -0,0 +1,4093 @@ +#region License +/* + * WebSocket.cs + * + * This code is derived from WebSocket.java + * (http://github.com/adamac/Java-WebSocket-client). + * + * The MIT License + * + * Copyright (c) 2009 Adam MacBeth + * Copyright (c) 2010-2016 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Contributors +/* + * Contributors: + * - Frank Razenberg + * - David Wood + * - Liryna + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics; +using System.IO; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using WebSocketSharp.Net; +using WebSocketSharp.Net.WebSockets; + +namespace WebSocketSharp +{ + /// + /// Implements the WebSocket interface. + /// + /// + /// + /// This class provides a set of methods and properties for two-way + /// communication using the WebSocket protocol. + /// + /// + /// The WebSocket protocol is defined in + /// RFC 6455. + /// + /// + public class WebSocket : IDisposable + { + #region Private Fields + + private AuthenticationChallenge _authChallenge; + private string _base64Key; + private bool _client; + private Action _closeContext; + private CompressionMethod _compression; + private WebSocketContext _context; + private CookieCollection _cookies; + private NetworkCredential _credentials; + private bool _emitOnPing; + private bool _enableRedirection; + private string _extensions; + private bool _extensionsRequested; + private object _forMessageEventQueue; + private object _forPing; + private object _forSend; + private object _forState; + private MemoryStream _fragmentsBuffer; + private bool _fragmentsCompressed; + private Opcode _fragmentsOpcode; + private const string _guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + private Func _handshakeRequestChecker; + private bool _ignoreExtensions; + private bool _inContinuation; + private volatile bool _inMessage; + private volatile Logger _logger; + private static readonly int _maxRetryCountForConnect; + private Action _message; + private Queue _messageEventQueue; + private uint _nonceCount; + private string _origin; + private ManualResetEvent _pongReceived; + private bool _preAuth; + private string _protocol; + private string[] _protocols; + private bool _protocolsRequested; + private NetworkCredential _proxyCredentials; + private Uri _proxyUri; + private volatile WebSocketState _readyState; + private ManualResetEvent _receivingExited; + private int _retryCountForConnect; + private bool _secure; + private ClientSslConfiguration _sslConfig; + private Stream _stream; + private TcpClient _tcpClient; + private Uri _uri; + private const string _version = "13"; + private TimeSpan _waitTime; + + #endregion + + #region Internal Fields + + /// + /// Represents the empty array of used internally. + /// + internal static readonly byte[] EmptyBytes; + + /// + /// Represents the length used to determine whether the data should be fragmented in sending. + /// + /// + /// + /// The data will be fragmented if that length is greater than the value of this field. + /// + /// + /// If you would like to change the value, you must set it to a value between 125 and + /// Int32.MaxValue - 14 inclusive. + /// + /// + internal static readonly int FragmentLength; + + /// + /// Represents the random number generator used internally. + /// + internal static readonly RandomNumberGenerator RandomNumber; + + #endregion + + #region Static Constructor + + static WebSocket () + { + _maxRetryCountForConnect = 10; + EmptyBytes = new byte[0]; + FragmentLength = 1016; + RandomNumber = new RNGCryptoServiceProvider (); + } + + #endregion + + #region Internal Constructors + + // As server + internal WebSocket (HttpListenerWebSocketContext context, string protocol) + { + _context = context; + _protocol = protocol; + + _closeContext = context.Close; + _logger = context.Log; + _message = messages; + _secure = context.IsSecureConnection; + _stream = context.Stream; + _waitTime = TimeSpan.FromSeconds (1); + + init (); + } + + // As server + internal WebSocket (TcpListenerWebSocketContext context, string protocol) + { + _context = context; + _protocol = protocol; + + _closeContext = context.Close; + _logger = context.Log; + _message = messages; + _secure = context.IsSecureConnection; + _stream = context.Stream; + _waitTime = TimeSpan.FromSeconds (1); + + init (); + } + + #endregion + + #region Public Constructors + + /// + /// Initializes a new instance of the class with + /// and optionally . + /// + /// + /// + /// A that specifies the URL to which to connect. + /// + /// + /// The scheme of the URL must be ws or wss. + /// + /// + /// The new instance uses a secure connection if the scheme is wss. + /// + /// + /// + /// + /// An array of that specifies the names of + /// the subprotocols if necessary. + /// + /// + /// Each value of the array must be a token defined in + /// + /// RFC 2616. + /// + /// + /// + /// is . + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is an invalid WebSocket URL string. + /// + /// + /// -or- + /// + /// + /// contains a value that is not a token. + /// + /// + /// -or- + /// + /// + /// contains a value twice. + /// + /// + public WebSocket (string url, params string[] protocols) + { + if (url == null) + throw new ArgumentNullException ("url"); + + if (url.Length == 0) + throw new ArgumentException ("An empty string.", "url"); + + string msg; + if (!url.TryCreateWebSocketUri (out _uri, out msg)) + throw new ArgumentException (msg, "url"); + + if (protocols != null && protocols.Length > 0) { + if (!checkProtocols (protocols, out msg)) + throw new ArgumentException (msg, "protocols"); + + _protocols = protocols; + } + + _base64Key = CreateBase64Key (); + _client = true; + _logger = new Logger (); + _message = messagec; + _secure = _uri.Scheme == "wss"; + _waitTime = TimeSpan.FromSeconds (5); + + init (); + } + + #endregion + + #region Internal Properties + + internal CookieCollection CookieCollection { + get { + return _cookies; + } + } + + // As server + internal Func CustomHandshakeRequestChecker { + get { + return _handshakeRequestChecker; + } + + set { + _handshakeRequestChecker = value; + } + } + + internal bool HasMessage { + get { + lock (_forMessageEventQueue) + return _messageEventQueue.Count > 0; + } + } + + // As server + internal bool IgnoreExtensions { + get { + return _ignoreExtensions; + } + + set { + _ignoreExtensions = value; + } + } + + internal bool IsConnected { + get { + return _readyState == WebSocketState.Open || _readyState == WebSocketState.Closing; + } + } + + #endregion + + #region Public Properties + + /// + /// Gets or sets the compression method used to compress a message. + /// + /// + /// The set operation does nothing if the connection has already been + /// established or it is closing. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It specifies the compression method used to compress a message. + /// + /// + /// The default value is . + /// + /// + /// + /// The set operation is not available if this instance is not a client. + /// + public CompressionMethod Compression { + get { + return _compression; + } + + set { + string msg = null; + + if (!_client) { + msg = "This instance is not a client."; + throw new InvalidOperationException (msg); + } + + if (!canSet (out msg)) { + _logger.Warn (msg); + return; + } + + lock (_forState) { + if (!canSet (out msg)) { + _logger.Warn (msg); + return; + } + + _compression = value; + } + } + } + + /// + /// Gets the HTTP cookies included in the handshake request/response. + /// + /// + /// + /// An + /// instance. + /// + /// + /// It provides an enumerator which supports the iteration over + /// the collection of the cookies. + /// + /// + public IEnumerable Cookies { + get { + lock (_cookies.SyncRoot) { + foreach (Cookie cookie in _cookies) + yield return cookie; + } + } + } + + /// + /// Gets the credentials for the HTTP authentication (Basic/Digest). + /// + /// + /// + /// A that represents the credentials + /// used to authenticate the client. + /// + /// + /// The default value is . + /// + /// + public NetworkCredential Credentials { + get { + return _credentials; + } + } + + /// + /// Gets or sets a value indicating whether a event + /// is emitted when a ping is received. + /// + /// + /// + /// true if this instance emits a event + /// when receives a ping; otherwise, false. + /// + /// + /// The default value is false. + /// + /// + public bool EmitOnPing { + get { + return _emitOnPing; + } + + set { + _emitOnPing = value; + } + } + + /// + /// Gets or sets a value indicating whether the URL redirection for + /// the handshake request is allowed. + /// + /// + /// The set operation does nothing if the connection has already been + /// established or it is closing. + /// + /// + /// + /// true if this instance allows the URL redirection for + /// the handshake request; otherwise, false. + /// + /// + /// The default value is false. + /// + /// + /// + /// The set operation is not available if this instance is not a client. + /// + public bool EnableRedirection { + get { + return _enableRedirection; + } + + set { + string msg = null; + + if (!_client) { + msg = "This instance is not a client."; + throw new InvalidOperationException (msg); + } + + if (!canSet (out msg)) { + _logger.Warn (msg); + return; + } + + lock (_forState) { + if (!canSet (out msg)) { + _logger.Warn (msg); + return; + } + + _enableRedirection = value; + } + } + } + + /// + /// Gets the extensions selected by server. + /// + /// + /// A that will be a list of the extensions + /// negotiated between client and server, or an empty string if + /// not specified or selected. + /// + public string Extensions { + get { + return _extensions ?? String.Empty; + } + } + + /// + /// Gets a value indicating whether the connection is alive. + /// + /// + /// The get operation returns the value by using a ping/pong + /// if the current state of the connection is Open. + /// + /// + /// true if the connection is alive; otherwise, false. + /// + public bool IsAlive { + get { + return ping (EmptyBytes); + } + } + + /// + /// Gets a value indicating whether a secure connection is used. + /// + /// + /// true if this instance uses a secure connection; otherwise, + /// false. + /// + public bool IsSecure { + get { + return _secure; + } + } + + /// + /// Gets the logging function. + /// + /// + /// The default logging level is . + /// + /// + /// A that provides the logging function. + /// + public Logger Log { + get { + return _logger; + } + + internal set { + _logger = value; + } + } + + /// + /// Gets or sets the value of the HTTP Origin header to send with + /// the handshake request. + /// + /// + /// + /// The HTTP Origin header is defined in + /// + /// Section 7 of RFC 6454. + /// + /// + /// This instance sends the Origin header if this property has any. + /// + /// + /// The set operation does nothing if the connection has already been + /// established or it is closing. + /// + /// + /// + /// + /// A that represents the value of the Origin + /// header to send. + /// + /// + /// The syntax is <scheme>://<host>[:<port>]. + /// + /// + /// The default value is . + /// + /// + /// + /// The set operation is not available if this instance is not a client. + /// + /// + /// + /// The value specified for a set operation is not an absolute URI string. + /// + /// + /// -or- + /// + /// + /// The value specified for a set operation includes the path segments. + /// + /// + public string Origin { + get { + return _origin; + } + + set { + string msg = null; + + if (!_client) { + msg = "This instance is not a client."; + throw new InvalidOperationException (msg); + } + + if (!value.IsNullOrEmpty ()) { + Uri uri; + if (!Uri.TryCreate (value, UriKind.Absolute, out uri)) { + msg = "Not an absolute URI string."; + throw new ArgumentException (msg, "value"); + } + + if (uri.Segments.Length > 1) { + msg = "It includes the path segments."; + throw new ArgumentException (msg, "value"); + } + } + + if (!canSet (out msg)) { + _logger.Warn (msg); + return; + } + + lock (_forState) { + if (!canSet (out msg)) { + _logger.Warn (msg); + return; + } + + _origin = !value.IsNullOrEmpty () ? value.TrimEnd ('/') : value; + } + } + } + + /// + /// Gets the name of subprotocol selected by the server. + /// + /// + /// + /// A that will be one of the names of + /// subprotocols specified by client. + /// + /// + /// An empty string if not specified or selected. + /// + /// + public string Protocol { + get { + return _protocol ?? String.Empty; + } + + internal set { + _protocol = value; + } + } + + /// + /// Gets the current state of the connection. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It indicates the current state of the connection. + /// + /// + /// The default value is . + /// + /// + public WebSocketState ReadyState { + get { + return _readyState; + } + } + + /// + /// Gets the configuration for secure connection. + /// + /// + /// This configuration will be referenced when attempts to connect, + /// so it must be configured before any connect method is called. + /// + /// + /// A that represents + /// the configuration used to establish a secure connection. + /// + /// + /// + /// This instance is not a client. + /// + /// + /// This instance does not use a secure connection. + /// + /// + public ClientSslConfiguration SslConfiguration { + get { + if (!_client) { + var msg = "This instance is not a client."; + throw new InvalidOperationException (msg); + } + + if (!_secure) { + var msg = "This instance does not use a secure connection."; + throw new InvalidOperationException (msg); + } + + return getSslConfiguration (); + } + } + + /// + /// Gets the URL to which to connect. + /// + /// + /// A that represents the URL to which to connect. + /// + public Uri Url { + get { + return _client ? _uri : _context.RequestUri; + } + } + + /// + /// Gets or sets the time to wait for the response to the ping or close. + /// + /// + /// The set operation does nothing if the connection has already been + /// established or it is closing. + /// + /// + /// + /// A to wait for the response. + /// + /// + /// The default value is the same as 5 seconds if this instance is + /// a client. + /// + /// + /// + /// The value specified for a set operation is zero or less. + /// + public TimeSpan WaitTime { + get { + return _waitTime; + } + + set { + if (value <= TimeSpan.Zero) + throw new ArgumentOutOfRangeException ("value", "Zero or less."); + + string msg; + if (!canSet (out msg)) { + _logger.Warn (msg); + return; + } + + lock (_forState) { + if (!canSet (out msg)) { + _logger.Warn (msg); + return; + } + + _waitTime = value; + } + } + } + + #endregion + + #region Public Events + + /// + /// Occurs when the WebSocket connection has been closed. + /// + public event EventHandler OnClose; + + /// + /// Occurs when the gets an error. + /// + public event EventHandler OnError; + + /// + /// Occurs when the receives a message. + /// + public event EventHandler OnMessage; + + /// + /// Occurs when the WebSocket connection has been established. + /// + public event EventHandler OnOpen; + + #endregion + + #region Private Methods + + // As server + private bool accept () + { + if (_readyState == WebSocketState.Open) { + var msg = "The handshake request has already been accepted."; + _logger.Warn (msg); + + return false; + } + + lock (_forState) { + if (_readyState == WebSocketState.Open) { + var msg = "The handshake request has already been accepted."; + _logger.Warn (msg); + + return false; + } + + if (_readyState == WebSocketState.Closing) { + var msg = "The close process has set in."; + _logger.Error (msg); + + msg = "An interruption has occurred while attempting to accept."; + error (msg, null); + + return false; + } + + if (_readyState == WebSocketState.Closed) { + var msg = "The connection has been closed."; + _logger.Error (msg); + + msg = "An interruption has occurred while attempting to accept."; + error (msg, null); + + return false; + } + + try { + if (!acceptHandshake ()) + return false; + } + catch (Exception ex) { + _logger.Fatal (ex.Message); + _logger.Debug (ex.ToString ()); + + var msg = "An exception has occurred while attempting to accept."; + fatal (msg, ex); + + return false; + } + + _readyState = WebSocketState.Open; + return true; + } + } + + // As server + private bool acceptHandshake () + { + _logger.Debug ( + String.Format ( + "A handshake request from {0}:\n{1}", _context.UserEndPoint, _context + ) + ); + + string msg; + if (!checkHandshakeRequest (_context, out msg)) { + _logger.Error (msg); + + refuseHandshake ( + CloseStatusCode.ProtocolError, + "A handshake error has occurred while attempting to accept." + ); + + return false; + } + + if (!customCheckHandshakeRequest (_context, out msg)) { + _logger.Error (msg); + + refuseHandshake ( + CloseStatusCode.PolicyViolation, + "A handshake error has occurred while attempting to accept." + ); + + return false; + } + + _base64Key = _context.Headers["Sec-WebSocket-Key"]; + + if (_protocol != null) { + var vals = _context.SecWebSocketProtocols; + processSecWebSocketProtocolClientHeader (vals); + } + + if (!_ignoreExtensions) { + var val = _context.Headers["Sec-WebSocket-Extensions"]; + processSecWebSocketExtensionsClientHeader (val); + } + + return sendHttpResponse (createHandshakeResponse ()); + } + + private bool canSet (out string message) + { + message = null; + + if (_readyState == WebSocketState.Open) { + message = "The connection has already been established."; + return false; + } + + if (_readyState == WebSocketState.Closing) { + message = "The connection is closing."; + return false; + } + + return true; + } + + // As server + private bool checkHandshakeRequest ( + WebSocketContext context, out string message + ) + { + message = null; + + if (!context.IsWebSocketRequest) { + message = "Not a handshake request."; + return false; + } + + if (context.RequestUri == null) { + message = "It specifies an invalid Request-URI."; + return false; + } + + var headers = context.Headers; + + var key = headers["Sec-WebSocket-Key"]; + if (key == null) { + message = "It includes no Sec-WebSocket-Key header."; + return false; + } + + if (key.Length == 0) { + message = "It includes an invalid Sec-WebSocket-Key header."; + return false; + } + + var version = headers["Sec-WebSocket-Version"]; + if (version == null) { + message = "It includes no Sec-WebSocket-Version header."; + return false; + } + + if (version != _version) { + message = "It includes an invalid Sec-WebSocket-Version header."; + return false; + } + + var protocol = headers["Sec-WebSocket-Protocol"]; + if (protocol != null && protocol.Length == 0) { + message = "It includes an invalid Sec-WebSocket-Protocol header."; + return false; + } + + if (!_ignoreExtensions) { + var extensions = headers["Sec-WebSocket-Extensions"]; + if (extensions != null && extensions.Length == 0) { + message = "It includes an invalid Sec-WebSocket-Extensions header."; + return false; + } + } + + return true; + } + + // As client + private bool checkHandshakeResponse (HttpResponse response, out string message) + { + message = null; + + if (response.IsRedirect) { + message = "Indicates the redirection."; + return false; + } + + if (response.IsUnauthorized) { + message = "Requires the authentication."; + return false; + } + + if (!response.IsWebSocketResponse) { + message = "Not a WebSocket handshake response."; + return false; + } + + var headers = response.Headers; + if (!validateSecWebSocketAcceptHeader (headers["Sec-WebSocket-Accept"])) { + message = "Includes no Sec-WebSocket-Accept header, or it has an invalid value."; + return false; + } + + if (!validateSecWebSocketProtocolServerHeader (headers["Sec-WebSocket-Protocol"])) { + message = "Includes no Sec-WebSocket-Protocol header, or it has an invalid value."; + return false; + } + + if (!validateSecWebSocketExtensionsServerHeader (headers["Sec-WebSocket-Extensions"])) { + message = "Includes an invalid Sec-WebSocket-Extensions header."; + return false; + } + + if (!validateSecWebSocketVersionServerHeader (headers["Sec-WebSocket-Version"])) { + message = "Includes an invalid Sec-WebSocket-Version header."; + return false; + } + + return true; + } + + private static bool checkProtocols (string[] protocols, out string message) + { + message = null; + + Func cond = protocol => protocol.IsNullOrEmpty () + || !protocol.IsToken (); + + if (protocols.Contains (cond)) { + message = "It contains a value that is not a token."; + return false; + } + + if (protocols.ContainsTwice ()) { + message = "It contains a value twice."; + return false; + } + + return true; + } + + private bool checkReceivedFrame (WebSocketFrame frame, out string message) + { + message = null; + + var masked = frame.IsMasked; + if (_client && masked) { + message = "A frame from the server is masked."; + return false; + } + + if (!_client && !masked) { + message = "A frame from a client is not masked."; + return false; + } + + if (_inContinuation && frame.IsData) { + message = "A data frame has been received while receiving continuation frames."; + return false; + } + + if (frame.IsCompressed && _compression == CompressionMethod.None) { + message = "A compressed frame has been received without any agreement for it."; + return false; + } + + if (frame.Rsv2 == Rsv.On) { + message = "The RSV2 of a frame is non-zero without any negotiation for it."; + return false; + } + + if (frame.Rsv3 == Rsv.On) { + message = "The RSV3 of a frame is non-zero without any negotiation for it."; + return false; + } + + return true; + } + + private void close (ushort code, string reason) + { + if (_readyState == WebSocketState.Closing) { + _logger.Info ("The closing is already in progress."); + return; + } + + if (_readyState == WebSocketState.Closed) { + _logger.Info ("The connection has already been closed."); + return; + } + + if (code == 1005) { // == no status + close (PayloadData.Empty, true, true, false); + return; + } + + var send = !code.IsReserved (); + close (new PayloadData (code, reason), send, send, false); + } + + private void close ( + PayloadData payloadData, bool send, bool receive, bool received + ) + { + lock (_forState) { + if (_readyState == WebSocketState.Closing) { + _logger.Info ("The closing is already in progress."); + return; + } + + if (_readyState == WebSocketState.Closed) { + _logger.Info ("The connection has already been closed."); + return; + } + + send = send && _readyState == WebSocketState.Open; + receive = send && receive; + + _readyState = WebSocketState.Closing; + } + + _logger.Trace ("Begin closing the connection."); + + var res = closeHandshake (payloadData, send, receive, received); + releaseResources (); + + _logger.Trace ("End closing the connection."); + + _readyState = WebSocketState.Closed; + + var e = new CloseEventArgs (payloadData, res); + + try { + OnClose.Emit (this, e); + } + catch (Exception ex) { + _logger.Error (ex.Message); + _logger.Debug (ex.ToString ()); + } + } + + private void closeAsync (ushort code, string reason) + { + if (_readyState == WebSocketState.Closing) { + _logger.Info ("The closing is already in progress."); + return; + } + + if (_readyState == WebSocketState.Closed) { + _logger.Info ("The connection has already been closed."); + return; + } + + if (code == 1005) { // == no status + closeAsync (PayloadData.Empty, true, true, false); + return; + } + + var send = !code.IsReserved (); + closeAsync (new PayloadData (code, reason), send, send, false); + } + + private void closeAsync ( + PayloadData payloadData, bool send, bool receive, bool received + ) + { + Action closer = close; + closer.BeginInvoke ( + payloadData, send, receive, received, ar => closer.EndInvoke (ar), null + ); + } + + private bool closeHandshake (byte[] frameAsBytes, bool receive, bool received) + { + var sent = frameAsBytes != null && sendBytes (frameAsBytes); + + var wait = !received && sent && receive && _receivingExited != null; + if (wait) + received = _receivingExited.WaitOne (_waitTime); + + var ret = sent && received; + + _logger.Debug ( + String.Format ( + "Was clean?: {0}\n sent: {1}\n received: {2}", ret, sent, received + ) + ); + + return ret; + } + + private bool closeHandshake ( + PayloadData payloadData, bool send, bool receive, bool received + ) + { + var sent = false; + if (send) { + var frame = WebSocketFrame.CreateCloseFrame (payloadData, _client); + sent = sendBytes (frame.ToArray ()); + + if (_client) + frame.Unmask (); + } + + var wait = !received && sent && receive && _receivingExited != null; + if (wait) + received = _receivingExited.WaitOne (_waitTime); + + var ret = sent && received; + + _logger.Debug ( + String.Format ( + "Was clean?: {0}\n sent: {1}\n received: {2}", ret, sent, received + ) + ); + + return ret; + } + + // As client + private bool connect () + { + if (_readyState == WebSocketState.Open) { + var msg = "The connection has already been established."; + _logger.Warn (msg); + + return false; + } + + lock (_forState) { + if (_readyState == WebSocketState.Open) { + var msg = "The connection has already been established."; + _logger.Warn (msg); + + return false; + } + + if (_readyState == WebSocketState.Closing) { + var msg = "The close process has set in."; + _logger.Error (msg); + + msg = "An interruption has occurred while attempting to connect."; + error (msg, null); + + return false; + } + + if (_retryCountForConnect > _maxRetryCountForConnect) { + var msg = "An opportunity for reconnecting has been lost."; + _logger.Error (msg); + + msg = "An interruption has occurred while attempting to connect."; + error (msg, null); + + return false; + } + + _readyState = WebSocketState.Connecting; + + try { + doHandshake (); + } + catch (Exception ex) { + _retryCountForConnect++; + + _logger.Fatal (ex.Message); + _logger.Debug (ex.ToString ()); + + var msg = "An exception has occurred while attempting to connect."; + fatal (msg, ex); + + return false; + } + + _retryCountForConnect = 1; + _readyState = WebSocketState.Open; + + return true; + } + } + + // As client + private string createExtensions () + { + var buff = new StringBuilder (80); + + if (_compression != CompressionMethod.None) { + var str = _compression.ToExtensionString ( + "server_no_context_takeover", "client_no_context_takeover"); + + buff.AppendFormat ("{0}, ", str); + } + + var len = buff.Length; + if (len > 2) { + buff.Length = len - 2; + return buff.ToString (); + } + + return null; + } + + // As server + private HttpResponse createHandshakeFailureResponse (HttpStatusCode code) + { + var ret = HttpResponse.CreateCloseResponse (code); + ret.Headers["Sec-WebSocket-Version"] = _version; + + return ret; + } + + // As client + private HttpRequest createHandshakeRequest () + { + var ret = HttpRequest.CreateWebSocketRequest (_uri); + + var headers = ret.Headers; + if (!_origin.IsNullOrEmpty ()) + headers["Origin"] = _origin; + + headers["Sec-WebSocket-Key"] = _base64Key; + + _protocolsRequested = _protocols != null; + if (_protocolsRequested) + headers["Sec-WebSocket-Protocol"] = _protocols.ToString (", "); + + _extensionsRequested = _compression != CompressionMethod.None; + if (_extensionsRequested) + headers["Sec-WebSocket-Extensions"] = createExtensions (); + + headers["Sec-WebSocket-Version"] = _version; + + AuthenticationResponse authRes = null; + if (_authChallenge != null && _credentials != null) { + authRes = new AuthenticationResponse (_authChallenge, _credentials, _nonceCount); + _nonceCount = authRes.NonceCount; + } + else if (_preAuth) { + authRes = new AuthenticationResponse (_credentials); + } + + if (authRes != null) + headers["Authorization"] = authRes.ToString (); + + if (_cookies.Count > 0) + ret.SetCookies (_cookies); + + return ret; + } + + // As server + private HttpResponse createHandshakeResponse () + { + var ret = HttpResponse.CreateWebSocketResponse (); + + var headers = ret.Headers; + headers["Sec-WebSocket-Accept"] = CreateResponseKey (_base64Key); + + if (_protocol != null) + headers["Sec-WebSocket-Protocol"] = _protocol; + + if (_extensions != null) + headers["Sec-WebSocket-Extensions"] = _extensions; + + if (_cookies.Count > 0) + ret.SetCookies (_cookies); + + return ret; + } + + // As server + private bool customCheckHandshakeRequest ( + WebSocketContext context, out string message + ) + { + message = null; + + if (_handshakeRequestChecker == null) + return true; + + message = _handshakeRequestChecker (context); + return message == null; + } + + private MessageEventArgs dequeueFromMessageEventQueue () + { + lock (_forMessageEventQueue) + return _messageEventQueue.Count > 0 ? _messageEventQueue.Dequeue () : null; + } + + // As client + private void doHandshake () + { + setClientStream (); + var res = sendHandshakeRequest (); + + string msg; + if (!checkHandshakeResponse (res, out msg)) + throw new WebSocketException (CloseStatusCode.ProtocolError, msg); + + if (_protocolsRequested) + _protocol = res.Headers["Sec-WebSocket-Protocol"]; + + if (_extensionsRequested) + processSecWebSocketExtensionsServerHeader (res.Headers["Sec-WebSocket-Extensions"]); + + processCookies (res.Cookies); + } + + private void enqueueToMessageEventQueue (MessageEventArgs e) + { + lock (_forMessageEventQueue) + _messageEventQueue.Enqueue (e); + } + + private void error (string message, Exception exception) + { + try { + OnError.Emit (this, new ErrorEventArgs (message, exception)); + } + catch (Exception ex) { + _logger.Error (ex.Message); + _logger.Debug (ex.ToString ()); + } + } + + private void fatal (string message, Exception exception) + { + var code = exception is WebSocketException + ? ((WebSocketException) exception).Code + : CloseStatusCode.Abnormal; + + fatal (message, (ushort) code); + } + + private void fatal (string message, ushort code) + { + var payload = new PayloadData (code, message); + close (payload, !code.IsReserved (), false, false); + } + + private void fatal (string message, CloseStatusCode code) + { + fatal (message, (ushort) code); + } + + private ClientSslConfiguration getSslConfiguration () + { + if (_sslConfig == null) + _sslConfig = new ClientSslConfiguration (_uri.DnsSafeHost); + + return _sslConfig; + } + + private void init () + { + _compression = CompressionMethod.None; + _cookies = new CookieCollection (); + _forPing = new object (); + _forSend = new object (); + _forState = new object (); + _messageEventQueue = new Queue (); + _forMessageEventQueue = ((ICollection) _messageEventQueue).SyncRoot; + _readyState = WebSocketState.Connecting; + } + + private void message () + { + MessageEventArgs e = null; + lock (_forMessageEventQueue) { + if (_inMessage || _messageEventQueue.Count == 0 || _readyState != WebSocketState.Open) + return; + + _inMessage = true; + e = _messageEventQueue.Dequeue (); + } + + _message (e); + } + + private void messagec (MessageEventArgs e) + { + do { + try { + OnMessage.Emit (this, e); + } + catch (Exception ex) { + _logger.Error (ex.ToString ()); + error ("An error has occurred during an OnMessage event.", ex); + } + + lock (_forMessageEventQueue) { + if (_messageEventQueue.Count == 0 || _readyState != WebSocketState.Open) { + _inMessage = false; + break; + } + + e = _messageEventQueue.Dequeue (); + } + } + while (true); + } + + private void messages (MessageEventArgs e) + { + try { + OnMessage.Emit (this, e); + } + catch (Exception ex) { + _logger.Error (ex.ToString ()); + error ("An error has occurred during an OnMessage event.", ex); + } + + lock (_forMessageEventQueue) { + if (_messageEventQueue.Count == 0 || _readyState != WebSocketState.Open) { + _inMessage = false; + return; + } + + e = _messageEventQueue.Dequeue (); + } + + ThreadPool.QueueUserWorkItem (state => messages (e)); + } + + private void open () + { + _inMessage = true; + startReceiving (); + try { + OnOpen.Emit (this, EventArgs.Empty); + } + catch (Exception ex) { + _logger.Error (ex.ToString ()); + error ("An error has occurred during the OnOpen event.", ex); + } + + MessageEventArgs e = null; + lock (_forMessageEventQueue) { + if (_messageEventQueue.Count == 0 || _readyState != WebSocketState.Open) { + _inMessage = false; + return; + } + + e = _messageEventQueue.Dequeue (); + } + + _message.BeginInvoke (e, ar => _message.EndInvoke (ar), null); + } + + private bool ping (byte[] data) + { + if (_readyState != WebSocketState.Open) + return false; + + var pongReceived = _pongReceived; + if (pongReceived == null) + return false; + + lock (_forPing) { + try { + pongReceived.Reset (); + if (!send (Fin.Final, Opcode.Ping, data, false)) + return false; + + return pongReceived.WaitOne (_waitTime); + } + catch (ObjectDisposedException) { + return false; + } + } + } + + private bool processCloseFrame (WebSocketFrame frame) + { + var payload = frame.PayloadData; + close (payload, !payload.HasReservedCode, false, true); + + return false; + } + + // As client + private void processCookies (CookieCollection cookies) + { + if (cookies.Count == 0) + return; + + _cookies.SetOrRemove (cookies); + } + + private bool processDataFrame (WebSocketFrame frame) + { + enqueueToMessageEventQueue ( + frame.IsCompressed + ? new MessageEventArgs ( + frame.Opcode, frame.PayloadData.ApplicationData.Decompress (_compression)) + : new MessageEventArgs (frame)); + + return true; + } + + private bool processFragmentFrame (WebSocketFrame frame) + { + if (!_inContinuation) { + // Must process first fragment. + if (frame.IsContinuation) + return true; + + _fragmentsOpcode = frame.Opcode; + _fragmentsCompressed = frame.IsCompressed; + _fragmentsBuffer = new MemoryStream (); + _inContinuation = true; + } + + _fragmentsBuffer.WriteBytes (frame.PayloadData.ApplicationData, 1024); + if (frame.IsFinal) { + using (_fragmentsBuffer) { + var data = _fragmentsCompressed + ? _fragmentsBuffer.DecompressToArray (_compression) + : _fragmentsBuffer.ToArray (); + + enqueueToMessageEventQueue (new MessageEventArgs (_fragmentsOpcode, data)); + } + + _fragmentsBuffer = null; + _inContinuation = false; + } + + return true; + } + + private bool processPingFrame (WebSocketFrame frame) + { + _logger.Trace ("A ping was received."); + + var pong = WebSocketFrame.CreatePongFrame (frame.PayloadData, _client); + + lock (_forState) { + if (_readyState != WebSocketState.Open) { + _logger.Error ("The connection is closing."); + return true; + } + + if (!sendBytes (pong.ToArray ())) + return false; + } + + _logger.Trace ("A pong to this ping has been sent."); + + if (_emitOnPing) { + if (_client) + pong.Unmask (); + + enqueueToMessageEventQueue (new MessageEventArgs (frame)); + } + + return true; + } + + private bool processPongFrame (WebSocketFrame frame) + { + _logger.Trace ("A pong was received."); + + try { + _pongReceived.Set (); + } + catch (NullReferenceException ex) { + _logger.Error (ex.Message); + _logger.Debug (ex.ToString ()); + + return false; + } + catch (ObjectDisposedException ex) { + _logger.Error (ex.Message); + _logger.Debug (ex.ToString ()); + + return false; + } + + _logger.Trace ("It has been signaled."); + + return true; + } + + private bool processReceivedFrame (WebSocketFrame frame) + { + string msg; + if (!checkReceivedFrame (frame, out msg)) + throw new WebSocketException (CloseStatusCode.ProtocolError, msg); + + frame.Unmask (); + return frame.IsFragment + ? processFragmentFrame (frame) + : frame.IsData + ? processDataFrame (frame) + : frame.IsPing + ? processPingFrame (frame) + : frame.IsPong + ? processPongFrame (frame) + : frame.IsClose + ? processCloseFrame (frame) + : processUnsupportedFrame (frame); + } + + // As server + private void processSecWebSocketExtensionsClientHeader (string value) + { + if (value == null) + return; + + var buff = new StringBuilder (80); + var comp = false; + + foreach (var elm in value.SplitHeaderValue (',')) { + var extension = elm.Trim (); + if (extension.Length == 0) + continue; + + if (!comp) { + if (extension.IsCompressionExtension (CompressionMethod.Deflate)) { + _compression = CompressionMethod.Deflate; + + buff.AppendFormat ( + "{0}, ", + _compression.ToExtensionString ( + "client_no_context_takeover", "server_no_context_takeover" + ) + ); + + comp = true; + } + } + } + + var len = buff.Length; + if (len <= 2) + return; + + buff.Length = len - 2; + _extensions = buff.ToString (); + } + + // As client + private void processSecWebSocketExtensionsServerHeader (string value) + { + if (value == null) { + _compression = CompressionMethod.None; + return; + } + + _extensions = value; + } + + // As server + private void processSecWebSocketProtocolClientHeader ( + IEnumerable values + ) + { + if (values.Contains (val => val == _protocol)) + return; + + _protocol = null; + } + + private bool processUnsupportedFrame (WebSocketFrame frame) + { + _logger.Fatal ("An unsupported frame:" + frame.PrintToString (false)); + fatal ("There is no way to handle it.", CloseStatusCode.PolicyViolation); + + return false; + } + + // As server + private void refuseHandshake (CloseStatusCode code, string reason) + { + _readyState = WebSocketState.Closing; + + var res = createHandshakeFailureResponse (HttpStatusCode.BadRequest); + sendHttpResponse (res); + + releaseServerResources (); + + _readyState = WebSocketState.Closed; + + var e = new CloseEventArgs ((ushort) code, reason, false); + + try { + OnClose.Emit (this, e); + } + catch (Exception ex) { + _logger.Error (ex.Message); + _logger.Debug (ex.ToString ()); + } + } + + // As client + private void releaseClientResources () + { + if (_stream != null) { + _stream.Dispose (); + _stream = null; + } + + if (_tcpClient != null) { + _tcpClient.Close (); + _tcpClient = null; + } + } + + private void releaseCommonResources () + { + if (_fragmentsBuffer != null) { + _fragmentsBuffer.Dispose (); + _fragmentsBuffer = null; + _inContinuation = false; + } + + if (_pongReceived != null) { + _pongReceived.Close (); + _pongReceived = null; + } + + if (_receivingExited != null) { + _receivingExited.Close (); + _receivingExited = null; + } + } + + private void releaseResources () + { + if (_client) + releaseClientResources (); + else + releaseServerResources (); + + releaseCommonResources (); + } + + // As server + private void releaseServerResources () + { + if (_closeContext == null) + return; + + _closeContext (); + _closeContext = null; + _stream = null; + _context = null; + } + + private bool send (Opcode opcode, Stream stream) + { + lock (_forSend) { + var src = stream; + var compressed = false; + var sent = false; + try { + if (_compression != CompressionMethod.None) { + stream = stream.Compress (_compression); + compressed = true; + } + + sent = send (opcode, stream, compressed); + if (!sent) + error ("A send has been interrupted.", null); + } + catch (Exception ex) { + _logger.Error (ex.ToString ()); + error ("An error has occurred during a send.", ex); + } + finally { + if (compressed) + stream.Dispose (); + + src.Dispose (); + } + + return sent; + } + } + + private bool send (Opcode opcode, Stream stream, bool compressed) + { + var len = stream.Length; + if (len == 0) + return send (Fin.Final, opcode, EmptyBytes, false); + + var quo = len / FragmentLength; + var rem = (int) (len % FragmentLength); + + byte[] buff = null; + if (quo == 0) { + buff = new byte[rem]; + return stream.Read (buff, 0, rem) == rem + && send (Fin.Final, opcode, buff, compressed); + } + + if (quo == 1 && rem == 0) { + buff = new byte[FragmentLength]; + return stream.Read (buff, 0, FragmentLength) == FragmentLength + && send (Fin.Final, opcode, buff, compressed); + } + + /* Send fragments */ + + // Begin + buff = new byte[FragmentLength]; + var sent = stream.Read (buff, 0, FragmentLength) == FragmentLength + && send (Fin.More, opcode, buff, compressed); + + if (!sent) + return false; + + var n = rem == 0 ? quo - 2 : quo - 1; + for (long i = 0; i < n; i++) { + sent = stream.Read (buff, 0, FragmentLength) == FragmentLength + && send (Fin.More, Opcode.Cont, buff, false); + + if (!sent) + return false; + } + + // End + if (rem == 0) + rem = FragmentLength; + else + buff = new byte[rem]; + + return stream.Read (buff, 0, rem) == rem + && send (Fin.Final, Opcode.Cont, buff, false); + } + + private bool send (Fin fin, Opcode opcode, byte[] data, bool compressed) + { + lock (_forState) { + if (_readyState != WebSocketState.Open) { + _logger.Error ("The connection is closing."); + return false; + } + + var frame = new WebSocketFrame (fin, opcode, data, compressed, _client); + return sendBytes (frame.ToArray ()); + } + } + + private void sendAsync (Opcode opcode, Stream stream, Action completed) + { + Func sender = send; + sender.BeginInvoke ( + opcode, + stream, + ar => { + try { + var sent = sender.EndInvoke (ar); + if (completed != null) + completed (sent); + } + catch (Exception ex) { + _logger.Error (ex.ToString ()); + error ( + "An error has occurred during the callback for an async send.", + ex + ); + } + }, + null + ); + } + + private bool sendBytes (byte[] bytes) + { + try { + _stream.Write (bytes, 0, bytes.Length); + } + catch (Exception ex) { + _logger.Error (ex.Message); + _logger.Debug (ex.ToString ()); + + return false; + } + + return true; + } + + // As client + private HttpResponse sendHandshakeRequest () + { + var req = createHandshakeRequest (); + var res = sendHttpRequest (req, 90000); + if (res.IsUnauthorized) { + var chal = res.Headers["WWW-Authenticate"]; + _logger.Warn (String.Format ("Received an authentication requirement for '{0}'.", chal)); + if (chal.IsNullOrEmpty ()) { + _logger.Error ("No authentication challenge is specified."); + return res; + } + + _authChallenge = AuthenticationChallenge.Parse (chal); + if (_authChallenge == null) { + _logger.Error ("An invalid authentication challenge is specified."); + return res; + } + + if (_credentials != null && + (!_preAuth || _authChallenge.Scheme == AuthenticationSchemes.Digest)) { + if (res.HasConnectionClose) { + releaseClientResources (); + setClientStream (); + } + + var authRes = new AuthenticationResponse (_authChallenge, _credentials, _nonceCount); + _nonceCount = authRes.NonceCount; + req.Headers["Authorization"] = authRes.ToString (); + res = sendHttpRequest (req, 15000); + } + } + + if (res.IsRedirect) { + var url = res.Headers["Location"]; + _logger.Warn (String.Format ("Received a redirection to '{0}'.", url)); + if (_enableRedirection) { + if (url.IsNullOrEmpty ()) { + _logger.Error ("No url to redirect is located."); + return res; + } + + Uri uri; + string msg; + if (!url.TryCreateWebSocketUri (out uri, out msg)) { + _logger.Error ("An invalid url to redirect is located: " + msg); + return res; + } + + releaseClientResources (); + + _uri = uri; + _secure = uri.Scheme == "wss"; + + setClientStream (); + return sendHandshakeRequest (); + } + } + + return res; + } + + // As client + private HttpResponse sendHttpRequest (HttpRequest request, int millisecondsTimeout) + { + _logger.Debug ("A request to the server:\n" + request.ToString ()); + var res = request.GetResponse (_stream, millisecondsTimeout); + _logger.Debug ("A response to this request:\n" + res.ToString ()); + + return res; + } + + // As server + private bool sendHttpResponse (HttpResponse response) + { + _logger.Debug ( + String.Format ( + "A response to {0}:\n{1}", _context.UserEndPoint, response + ) + ); + + return sendBytes (response.ToByteArray ()); + } + + // As client + private void sendProxyConnectRequest () + { + var req = HttpRequest.CreateConnectRequest (_uri); + var res = sendHttpRequest (req, 90000); + if (res.IsProxyAuthenticationRequired) { + var chal = res.Headers["Proxy-Authenticate"]; + _logger.Warn ( + String.Format ("Received a proxy authentication requirement for '{0}'.", chal)); + + if (chal.IsNullOrEmpty ()) + throw new WebSocketException ("No proxy authentication challenge is specified."); + + var authChal = AuthenticationChallenge.Parse (chal); + if (authChal == null) + throw new WebSocketException ("An invalid proxy authentication challenge is specified."); + + if (_proxyCredentials != null) { + if (res.HasConnectionClose) { + releaseClientResources (); + _tcpClient = new TcpClient (_proxyUri.DnsSafeHost, _proxyUri.Port); + _stream = _tcpClient.GetStream (); + } + + var authRes = new AuthenticationResponse (authChal, _proxyCredentials, 0); + req.Headers["Proxy-Authorization"] = authRes.ToString (); + res = sendHttpRequest (req, 15000); + } + + if (res.IsProxyAuthenticationRequired) + throw new WebSocketException ("A proxy authentication is required."); + } + + if (res.StatusCode[0] != '2') + throw new WebSocketException ( + "The proxy has failed a connection to the requested host and port."); + } + + // As client + private void setClientStream () + { + if (_proxyUri != null) { + _tcpClient = new TcpClient (_proxyUri.DnsSafeHost, _proxyUri.Port); + _stream = _tcpClient.GetStream (); + sendProxyConnectRequest (); + } + else { + _tcpClient = new TcpClient (_uri.DnsSafeHost, _uri.Port); + _stream = _tcpClient.GetStream (); + } + + if (_secure) { + var conf = getSslConfiguration (); + var host = conf.TargetHost; + if (host != _uri.DnsSafeHost) + throw new WebSocketException ( + CloseStatusCode.TlsHandshakeFailure, "An invalid host name is specified."); + + try { + var sslStream = new SslStream ( + _stream, + false, + conf.ServerCertificateValidationCallback, + conf.ClientCertificateSelectionCallback); + + sslStream.AuthenticateAsClient ( + host, + conf.ClientCertificates, + conf.EnabledSslProtocols, + conf.CheckCertificateRevocation); + + _stream = sslStream; + } + catch (Exception ex) { + throw new WebSocketException (CloseStatusCode.TlsHandshakeFailure, ex); + } + } + } + + private void startReceiving () + { + if (_messageEventQueue.Count > 0) + _messageEventQueue.Clear (); + + _pongReceived = new ManualResetEvent (false); + _receivingExited = new ManualResetEvent (false); + + Action receive = null; + receive = + () => + WebSocketFrame.ReadFrameAsync ( + _stream, + false, + frame => { + if (!processReceivedFrame (frame) || _readyState == WebSocketState.Closed) { + var exited = _receivingExited; + if (exited != null) + exited.Set (); + + return; + } + + // Receive next asap because the Ping or Close needs a response to it. + receive (); + + if (_inMessage || !HasMessage || _readyState != WebSocketState.Open) + return; + + message (); + }, + ex => { + _logger.Fatal (ex.ToString ()); + fatal ("An exception has occurred while receiving.", ex); + } + ); + + receive (); + } + + // As client + private bool validateSecWebSocketAcceptHeader (string value) + { + return value != null && value == CreateResponseKey (_base64Key); + } + + // As client + private bool validateSecWebSocketExtensionsServerHeader (string value) + { + if (value == null) + return true; + + if (value.Length == 0) + return false; + + if (!_extensionsRequested) + return false; + + var comp = _compression != CompressionMethod.None; + foreach (var e in value.SplitHeaderValue (',')) { + var ext = e.Trim (); + if (comp && ext.IsCompressionExtension (_compression)) { + if (!ext.Contains ("server_no_context_takeover")) { + _logger.Error ("The server hasn't sent back 'server_no_context_takeover'."); + return false; + } + + if (!ext.Contains ("client_no_context_takeover")) + _logger.Warn ("The server hasn't sent back 'client_no_context_takeover'."); + + var method = _compression.ToExtensionString (); + var invalid = + ext.SplitHeaderValue (';').Contains ( + t => { + t = t.Trim (); + return t != method + && t != "server_no_context_takeover" + && t != "client_no_context_takeover"; + } + ); + + if (invalid) + return false; + } + else { + return false; + } + } + + return true; + } + + // As client + private bool validateSecWebSocketProtocolServerHeader (string value) + { + if (value == null) + return !_protocolsRequested; + + if (value.Length == 0) + return false; + + return _protocolsRequested && _protocols.Contains (p => p == value); + } + + // As client + private bool validateSecWebSocketVersionServerHeader (string value) + { + return value == null || value == _version; + } + + #endregion + + #region Internal Methods + + // As server + internal void Close (HttpResponse response) + { + _readyState = WebSocketState.Closing; + + sendHttpResponse (response); + releaseServerResources (); + + _readyState = WebSocketState.Closed; + } + + // As server + internal void Close (HttpStatusCode code) + { + Close (createHandshakeFailureResponse (code)); + } + + // As server + internal void Close (PayloadData payloadData, byte[] frameAsBytes) + { + lock (_forState) { + if (_readyState == WebSocketState.Closing) { + _logger.Info ("The closing is already in progress."); + return; + } + + if (_readyState == WebSocketState.Closed) { + _logger.Info ("The connection has already been closed."); + return; + } + + _readyState = WebSocketState.Closing; + } + + _logger.Trace ("Begin closing the connection."); + + var sent = frameAsBytes != null && sendBytes (frameAsBytes); + var received = sent && _receivingExited != null + ? _receivingExited.WaitOne (_waitTime) + : false; + + var res = sent && received; + + _logger.Debug ( + String.Format ( + "Was clean?: {0}\n sent: {1}\n received: {2}", res, sent, received + ) + ); + + releaseServerResources (); + releaseCommonResources (); + + _logger.Trace ("End closing the connection."); + + _readyState = WebSocketState.Closed; + + var e = new CloseEventArgs (payloadData, res); + + try { + OnClose.Emit (this, e); + } + catch (Exception ex) { + _logger.Error (ex.Message); + _logger.Debug (ex.ToString ()); + } + } + + // As client + internal static string CreateBase64Key () + { + var src = new byte[16]; + RandomNumber.GetBytes (src); + + return Convert.ToBase64String (src); + } + + internal static string CreateResponseKey (string base64Key) + { + var buff = new StringBuilder (base64Key, 64); + buff.Append (_guid); + SHA1 sha1 = new SHA1CryptoServiceProvider (); + var src = sha1.ComputeHash (buff.ToString ().GetUTF8EncodedBytes ()); + + return Convert.ToBase64String (src); + } + + // As server + internal void InternalAccept () + { + try { + if (!acceptHandshake ()) + return; + } + catch (Exception ex) { + _logger.Fatal (ex.Message); + _logger.Debug (ex.ToString ()); + + var msg = "An exception has occurred while attempting to accept."; + fatal (msg, ex); + + return; + } + + _readyState = WebSocketState.Open; + + open (); + } + + // As server + internal bool Ping (byte[] frameAsBytes, TimeSpan timeout) + { + if (_readyState != WebSocketState.Open) + return false; + + var pongReceived = _pongReceived; + if (pongReceived == null) + return false; + + lock (_forPing) { + try { + pongReceived.Reset (); + + lock (_forState) { + if (_readyState != WebSocketState.Open) + return false; + + if (!sendBytes (frameAsBytes)) + return false; + } + + return pongReceived.WaitOne (timeout); + } + catch (ObjectDisposedException) { + return false; + } + } + } + + // As server + internal void Send ( + Opcode opcode, byte[] data, Dictionary cache + ) + { + lock (_forSend) { + lock (_forState) { + if (_readyState != WebSocketState.Open) { + _logger.Error ("The connection is closing."); + return; + } + + byte[] found; + if (!cache.TryGetValue (_compression, out found)) { + found = new WebSocketFrame ( + Fin.Final, + opcode, + data.Compress (_compression), + _compression != CompressionMethod.None, + false + ) + .ToArray (); + + cache.Add (_compression, found); + } + + sendBytes (found); + } + } + } + + // As server + internal void Send ( + Opcode opcode, Stream stream, Dictionary cache + ) + { + lock (_forSend) { + Stream found; + if (!cache.TryGetValue (_compression, out found)) { + found = stream.Compress (_compression); + cache.Add (_compression, found); + } + else { + found.Position = 0; + } + + send (opcode, found, _compression != CompressionMethod.None); + } + } + + #endregion + + #region Public Methods + + /// + /// Accepts the handshake request. + /// + /// + /// This method does nothing if the handshake request has already been + /// accepted. + /// + /// + /// + /// This instance is a client. + /// + /// + /// -or- + /// + /// + /// The close process is in progress. + /// + /// + /// -or- + /// + /// + /// The connection has already been closed. + /// + /// + public void Accept () + { + if (_client) { + var msg = "This instance is a client."; + throw new InvalidOperationException (msg); + } + + if (_readyState == WebSocketState.Closing) { + var msg = "The close process is in progress."; + throw new InvalidOperationException (msg); + } + + if (_readyState == WebSocketState.Closed) { + var msg = "The connection has already been closed."; + throw new InvalidOperationException (msg); + } + + if (accept ()) + open (); + } + + /// + /// Accepts the handshake request asynchronously. + /// + /// + /// + /// This method does not wait for the accept process to be complete. + /// + /// + /// This method does nothing if the handshake request has already been + /// accepted. + /// + /// + /// + /// + /// This instance is a client. + /// + /// + /// -or- + /// + /// + /// The close process is in progress. + /// + /// + /// -or- + /// + /// + /// The connection has already been closed. + /// + /// + public void AcceptAsync () + { + if (_client) { + var msg = "This instance is a client."; + throw new InvalidOperationException (msg); + } + + if (_readyState == WebSocketState.Closing) { + var msg = "The close process is in progress."; + throw new InvalidOperationException (msg); + } + + if (_readyState == WebSocketState.Closed) { + var msg = "The connection has already been closed."; + throw new InvalidOperationException (msg); + } + + Func acceptor = accept; + acceptor.BeginInvoke ( + ar => { + if (acceptor.EndInvoke (ar)) + open (); + }, + null + ); + } + + /// + /// Closes the connection. + /// + /// + /// This method does nothing if the current state of the connection is + /// Closing or Closed. + /// + public void Close () + { + close (1005, String.Empty); + } + + /// + /// Closes the connection with the specified code. + /// + /// + /// This method does nothing if the current state of the connection is + /// Closing or Closed. + /// + /// + /// + /// A that represents the status code indicating + /// the reason for the close. + /// + /// + /// The status codes are defined in + /// + /// Section 7.4 of RFC 6455. + /// + /// + /// + /// is less than 1000 or greater than 4999. + /// + /// + /// + /// is 1011 (server error). + /// It cannot be used by clients. + /// + /// + /// -or- + /// + /// + /// is 1010 (mandatory extension). + /// It cannot be used by servers. + /// + /// + public void Close (ushort code) + { + if (!code.IsCloseStatusCode ()) { + var msg = "Less than 1000 or greater than 4999."; + throw new ArgumentOutOfRangeException ("code", msg); + } + + if (_client && code == 1011) { + var msg = "1011 cannot be used."; + throw new ArgumentException (msg, "code"); + } + + if (!_client && code == 1010) { + var msg = "1010 cannot be used."; + throw new ArgumentException (msg, "code"); + } + + close (code, String.Empty); + } + + /// + /// Closes the connection with the specified code. + /// + /// + /// This method does nothing if the current state of the connection is + /// Closing or Closed. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It represents the status code indicating the reason for the close. + /// + /// + /// + /// + /// is + /// . + /// It cannot be used by clients. + /// + /// + /// -or- + /// + /// + /// is + /// . + /// It cannot be used by servers. + /// + /// + public void Close (CloseStatusCode code) + { + if (_client && code == CloseStatusCode.ServerError) { + var msg = "ServerError cannot be used."; + throw new ArgumentException (msg, "code"); + } + + if (!_client && code == CloseStatusCode.MandatoryExtension) { + var msg = "MandatoryExtension cannot be used."; + throw new ArgumentException (msg, "code"); + } + + close ((ushort) code, String.Empty); + } + + /// + /// Closes the connection with the specified code and reason. + /// + /// + /// This method does nothing if the current state of the connection is + /// Closing or Closed. + /// + /// + /// + /// A that represents the status code indicating + /// the reason for the close. + /// + /// + /// The status codes are defined in + /// + /// Section 7.4 of RFC 6455. + /// + /// + /// + /// + /// A that represents the reason for the close. + /// + /// + /// The size must be 123 bytes or less in UTF-8. + /// + /// + /// + /// + /// is less than 1000 or greater than 4999. + /// + /// + /// -or- + /// + /// + /// The size of is greater than 123 bytes. + /// + /// + /// + /// + /// is 1011 (server error). + /// It cannot be used by clients. + /// + /// + /// -or- + /// + /// + /// is 1010 (mandatory extension). + /// It cannot be used by servers. + /// + /// + /// -or- + /// + /// + /// is 1005 (no status) and there is reason. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + public void Close (ushort code, string reason) + { + if (!code.IsCloseStatusCode ()) { + var msg = "Less than 1000 or greater than 4999."; + throw new ArgumentOutOfRangeException ("code", msg); + } + + if (_client && code == 1011) { + var msg = "1011 cannot be used."; + throw new ArgumentException (msg, "code"); + } + + if (!_client && code == 1010) { + var msg = "1010 cannot be used."; + throw new ArgumentException (msg, "code"); + } + + if (reason.IsNullOrEmpty ()) { + close (code, String.Empty); + return; + } + + if (code == 1005) { + var msg = "1005 cannot be used."; + throw new ArgumentException (msg, "code"); + } + + byte[] bytes; + if (!reason.TryGetUTF8EncodedBytes (out bytes)) { + var msg = "It could not be UTF-8-encoded."; + throw new ArgumentException (msg, "reason"); + } + + if (bytes.Length > 123) { + var msg = "Its size is greater than 123 bytes."; + throw new ArgumentOutOfRangeException ("reason", msg); + } + + close (code, reason); + } + + /// + /// Closes the connection with the specified code and reason. + /// + /// + /// This method does nothing if the current state of the connection is + /// Closing or Closed. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It represents the status code indicating the reason for the close. + /// + /// + /// + /// + /// A that represents the reason for the close. + /// + /// + /// The size must be 123 bytes or less in UTF-8. + /// + /// + /// + /// + /// is + /// . + /// It cannot be used by clients. + /// + /// + /// -or- + /// + /// + /// is + /// . + /// It cannot be used by servers. + /// + /// + /// -or- + /// + /// + /// is + /// and there is reason. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// The size of is greater than 123 bytes. + /// + public void Close (CloseStatusCode code, string reason) + { + if (_client && code == CloseStatusCode.ServerError) { + var msg = "ServerError cannot be used."; + throw new ArgumentException (msg, "code"); + } + + if (!_client && code == CloseStatusCode.MandatoryExtension) { + var msg = "MandatoryExtension cannot be used."; + throw new ArgumentException (msg, "code"); + } + + if (reason.IsNullOrEmpty ()) { + close ((ushort) code, String.Empty); + return; + } + + if (code == CloseStatusCode.NoStatus) { + var msg = "NoStatus cannot be used."; + throw new ArgumentException (msg, "code"); + } + + byte[] bytes; + if (!reason.TryGetUTF8EncodedBytes (out bytes)) { + var msg = "It could not be UTF-8-encoded."; + throw new ArgumentException (msg, "reason"); + } + + if (bytes.Length > 123) { + var msg = "Its size is greater than 123 bytes."; + throw new ArgumentOutOfRangeException ("reason", msg); + } + + close ((ushort) code, reason); + } + + /// + /// Closes the connection asynchronously. + /// + /// + /// + /// This method does not wait for the close to be complete. + /// + /// + /// This method does nothing if the current state of the connection is + /// Closing or Closed. + /// + /// + public void CloseAsync () + { + closeAsync (1005, String.Empty); + } + + /// + /// Closes the connection asynchronously with the specified code. + /// + /// + /// + /// This method does not wait for the close to be complete. + /// + /// + /// This method does nothing if the current state of the connection is + /// Closing or Closed. + /// + /// + /// + /// + /// A that represents the status code indicating + /// the reason for the close. + /// + /// + /// The status codes are defined in + /// + /// Section 7.4 of RFC 6455. + /// + /// + /// + /// is less than 1000 or greater than 4999. + /// + /// + /// + /// is 1011 (server error). + /// It cannot be used by clients. + /// + /// + /// -or- + /// + /// + /// is 1010 (mandatory extension). + /// It cannot be used by servers. + /// + /// + public void CloseAsync (ushort code) + { + if (!code.IsCloseStatusCode ()) { + var msg = "Less than 1000 or greater than 4999."; + throw new ArgumentOutOfRangeException ("code", msg); + } + + if (_client && code == 1011) { + var msg = "1011 cannot be used."; + throw new ArgumentException (msg, "code"); + } + + if (!_client && code == 1010) { + var msg = "1010 cannot be used."; + throw new ArgumentException (msg, "code"); + } + + closeAsync (code, String.Empty); + } + + /// + /// Closes the connection asynchronously with the specified code. + /// + /// + /// + /// This method does not wait for the close to be complete. + /// + /// + /// This method does nothing if the current state of the connection is + /// Closing or Closed. + /// + /// + /// + /// + /// One of the enum values. + /// + /// + /// It represents the status code indicating the reason for the close. + /// + /// + /// + /// + /// is + /// . + /// It cannot be used by clients. + /// + /// + /// -or- + /// + /// + /// is + /// . + /// It cannot be used by servers. + /// + /// + public void CloseAsync (CloseStatusCode code) + { + if (_client && code == CloseStatusCode.ServerError) { + var msg = "ServerError cannot be used."; + throw new ArgumentException (msg, "code"); + } + + if (!_client && code == CloseStatusCode.MandatoryExtension) { + var msg = "MandatoryExtension cannot be used."; + throw new ArgumentException (msg, "code"); + } + + closeAsync ((ushort) code, String.Empty); + } + + /// + /// Closes the connection asynchronously with the specified code and reason. + /// + /// + /// + /// This method does not wait for the close to be complete. + /// + /// + /// This method does nothing if the current state of the connection is + /// Closing or Closed. + /// + /// + /// + /// + /// A that represents the status code indicating + /// the reason for the close. + /// + /// + /// The status codes are defined in + /// + /// Section 7.4 of RFC 6455. + /// + /// + /// + /// + /// A that represents the reason for the close. + /// + /// + /// The size must be 123 bytes or less in UTF-8. + /// + /// + /// + /// + /// is less than 1000 or greater than 4999. + /// + /// + /// -or- + /// + /// + /// The size of is greater than 123 bytes. + /// + /// + /// + /// + /// is 1011 (server error). + /// It cannot be used by clients. + /// + /// + /// -or- + /// + /// + /// is 1010 (mandatory extension). + /// It cannot be used by servers. + /// + /// + /// -or- + /// + /// + /// is 1005 (no status) and there is reason. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + public void CloseAsync (ushort code, string reason) + { + if (!code.IsCloseStatusCode ()) { + var msg = "Less than 1000 or greater than 4999."; + throw new ArgumentOutOfRangeException ("code", msg); + } + + if (_client && code == 1011) { + var msg = "1011 cannot be used."; + throw new ArgumentException (msg, "code"); + } + + if (!_client && code == 1010) { + var msg = "1010 cannot be used."; + throw new ArgumentException (msg, "code"); + } + + if (reason.IsNullOrEmpty ()) { + closeAsync (code, String.Empty); + return; + } + + if (code == 1005) { + var msg = "1005 cannot be used."; + throw new ArgumentException (msg, "code"); + } + + byte[] bytes; + if (!reason.TryGetUTF8EncodedBytes (out bytes)) { + var msg = "It could not be UTF-8-encoded."; + throw new ArgumentException (msg, "reason"); + } + + if (bytes.Length > 123) { + var msg = "Its size is greater than 123 bytes."; + throw new ArgumentOutOfRangeException ("reason", msg); + } + + closeAsync (code, reason); + } + + /// + /// Closes the connection asynchronously with the specified code and reason. + /// + /// + /// + /// This method does not wait for the close to be complete. + /// + /// + /// This method does nothing if the current state of the connection is + /// Closing or Closed. + /// + /// + /// + /// + /// One of the enum values. + /// + /// + /// It represents the status code indicating the reason for the close. + /// + /// + /// + /// + /// A that represents the reason for the close. + /// + /// + /// The size must be 123 bytes or less in UTF-8. + /// + /// + /// + /// + /// is + /// . + /// It cannot be used by clients. + /// + /// + /// -or- + /// + /// + /// is + /// . + /// It cannot be used by servers. + /// + /// + /// -or- + /// + /// + /// is + /// and there is reason. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// The size of is greater than 123 bytes. + /// + public void CloseAsync (CloseStatusCode code, string reason) + { + if (_client && code == CloseStatusCode.ServerError) { + var msg = "ServerError cannot be used."; + throw new ArgumentException (msg, "code"); + } + + if (!_client && code == CloseStatusCode.MandatoryExtension) { + var msg = "MandatoryExtension cannot be used."; + throw new ArgumentException (msg, "code"); + } + + if (reason.IsNullOrEmpty ()) { + closeAsync ((ushort) code, String.Empty); + return; + } + + if (code == CloseStatusCode.NoStatus) { + var msg = "NoStatus cannot be used."; + throw new ArgumentException (msg, "code"); + } + + byte[] bytes; + if (!reason.TryGetUTF8EncodedBytes (out bytes)) { + var msg = "It could not be UTF-8-encoded."; + throw new ArgumentException (msg, "reason"); + } + + if (bytes.Length > 123) { + var msg = "Its size is greater than 123 bytes."; + throw new ArgumentOutOfRangeException ("reason", msg); + } + + closeAsync ((ushort) code, reason); + } + + /// + /// Establishes a connection. + /// + /// + /// This method does nothing if the connection has already been established. + /// + /// + /// + /// This instance is not a client. + /// + /// + /// -or- + /// + /// + /// The close process is in progress. + /// + /// + /// -or- + /// + /// + /// A series of reconnecting has failed. + /// + /// + public void Connect () + { + if (!_client) { + var msg = "This instance is not a client."; + throw new InvalidOperationException (msg); + } + + if (_readyState == WebSocketState.Closing) { + var msg = "The close process is in progress."; + throw new InvalidOperationException (msg); + } + + if (_retryCountForConnect > _maxRetryCountForConnect) { + var msg = "A series of reconnecting has failed."; + throw new InvalidOperationException (msg); + } + + if (connect ()) + open (); + } + + /// + /// Establishes a connection asynchronously. + /// + /// + /// + /// This method does not wait for the connect process to be complete. + /// + /// + /// This method does nothing if the connection has already been + /// established. + /// + /// + /// + /// + /// This instance is not a client. + /// + /// + /// -or- + /// + /// + /// The close process is in progress. + /// + /// + /// -or- + /// + /// + /// A series of reconnecting has failed. + /// + /// + public void ConnectAsync () + { + if (!_client) { + var msg = "This instance is not a client."; + throw new InvalidOperationException (msg); + } + + if (_readyState == WebSocketState.Closing) { + var msg = "The close process is in progress."; + throw new InvalidOperationException (msg); + } + + if (_retryCountForConnect > _maxRetryCountForConnect) { + var msg = "A series of reconnecting has failed."; + throw new InvalidOperationException (msg); + } + + Func connector = connect; + connector.BeginInvoke ( + ar => { + if (connector.EndInvoke (ar)) + open (); + }, + null + ); + } + + /// + /// Sends a ping using the WebSocket connection. + /// + /// + /// true if the send has done with no error and a pong has been + /// received within a time; otherwise, false. + /// + public bool Ping () + { + return ping (EmptyBytes); + } + + /// + /// Sends a ping with using the WebSocket + /// connection. + /// + /// + /// true if the send has done with no error and a pong has been + /// received within a time; otherwise, false. + /// + /// + /// + /// A that represents the message to send. + /// + /// + /// The size must be 125 bytes or less in UTF-8. + /// + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// The size of is greater than 125 bytes. + /// + public bool Ping (string message) + { + if (message.IsNullOrEmpty ()) + return ping (EmptyBytes); + + byte[] bytes; + if (!message.TryGetUTF8EncodedBytes (out bytes)) { + var msg = "It could not be UTF-8-encoded."; + throw new ArgumentException (msg, "message"); + } + + if (bytes.Length > 125) { + var msg = "Its size is greater than 125 bytes."; + throw new ArgumentOutOfRangeException ("message", msg); + } + + return ping (bytes); + } + + /// + /// Sends the specified data using the WebSocket connection. + /// + /// + /// An array of that represents the binary data to send. + /// + /// + /// The current state of the connection is not Open. + /// + /// + /// is . + /// + public void Send (byte[] data) + { + if (_readyState != WebSocketState.Open) { + var msg = "The current state of the connection is not Open."; + throw new InvalidOperationException (msg); + } + + if (data == null) + throw new ArgumentNullException ("data"); + + send (Opcode.Binary, new MemoryStream (data)); + } + + /// + /// Sends the specified file using the WebSocket connection. + /// + /// + /// + /// A that specifies the file to send. + /// + /// + /// The file is sent as the binary data. + /// + /// + /// + /// The current state of the connection is not Open. + /// + /// + /// is . + /// + /// + /// + /// The file does not exist. + /// + /// + /// -or- + /// + /// + /// The file could not be opened. + /// + /// + public void Send (FileInfo fileInfo) + { + if (_readyState != WebSocketState.Open) { + var msg = "The current state of the connection is not Open."; + throw new InvalidOperationException (msg); + } + + if (fileInfo == null) + throw new ArgumentNullException ("fileInfo"); + + if (!fileInfo.Exists) { + var msg = "The file does not exist."; + throw new ArgumentException (msg, "fileInfo"); + } + + FileStream stream; + if (!fileInfo.TryOpenRead (out stream)) { + var msg = "The file could not be opened."; + throw new ArgumentException (msg, "fileInfo"); + } + + send (Opcode.Binary, stream); + } + + /// + /// Sends the specified data using the WebSocket connection. + /// + /// + /// A that represents the text data to send. + /// + /// + /// The current state of the connection is not Open. + /// + /// + /// is . + /// + /// + /// could not be UTF-8-encoded. + /// + public void Send (string data) + { + if (_readyState != WebSocketState.Open) { + var msg = "The current state of the connection is not Open."; + throw new InvalidOperationException (msg); + } + + if (data == null) + throw new ArgumentNullException ("data"); + + byte[] bytes; + if (!data.TryGetUTF8EncodedBytes (out bytes)) { + var msg = "It could not be UTF-8-encoded."; + throw new ArgumentException (msg, "data"); + } + + send (Opcode.Text, new MemoryStream (bytes)); + } + + /// + /// Sends the data from the specified stream using the WebSocket connection. + /// + /// + /// + /// A instance from which to read the data to send. + /// + /// + /// The data is sent as the binary data. + /// + /// + /// + /// An that specifies the number of bytes to send. + /// + /// + /// The current state of the connection is not Open. + /// + /// + /// is . + /// + /// + /// + /// cannot be read. + /// + /// + /// -or- + /// + /// + /// is less than 1. + /// + /// + /// -or- + /// + /// + /// No data could be read from . + /// + /// + public void Send (Stream stream, int length) + { + if (_readyState != WebSocketState.Open) { + var msg = "The current state of the connection is not Open."; + throw new InvalidOperationException (msg); + } + + if (stream == null) + throw new ArgumentNullException ("stream"); + + if (!stream.CanRead) { + var msg = "It cannot be read."; + throw new ArgumentException (msg, "stream"); + } + + if (length < 1) { + var msg = "Less than 1."; + throw new ArgumentException (msg, "length"); + } + + var bytes = stream.ReadBytes (length); + + var len = bytes.Length; + if (len == 0) { + var msg = "No data could be read from it."; + throw new ArgumentException (msg, "stream"); + } + + if (len < length) { + _logger.Warn ( + String.Format ( + "Only {0} byte(s) of data could be read from the stream.", + len + ) + ); + } + + send (Opcode.Binary, new MemoryStream (bytes)); + } + + /// + /// Sends the specified data asynchronously using the WebSocket connection. + /// + /// + /// This method does not wait for the send to be complete. + /// + /// + /// An array of that represents the binary data to send. + /// + /// + /// + /// An Action<bool> delegate or + /// if not needed. + /// + /// + /// The delegate invokes the method called when the send is complete. + /// + /// + /// true is passed to the method if the send has done with + /// no error; otherwise, false. + /// + /// + /// + /// The current state of the connection is not Open. + /// + /// + /// is . + /// + public void SendAsync (byte[] data, Action completed) + { + if (_readyState != WebSocketState.Open) { + var msg = "The current state of the connection is not Open."; + throw new InvalidOperationException (msg); + } + + if (data == null) + throw new ArgumentNullException ("data"); + + sendAsync (Opcode.Binary, new MemoryStream (data), completed); + } + + /// + /// Sends the specified file asynchronously using the WebSocket connection. + /// + /// + /// This method does not wait for the send to be complete. + /// + /// + /// + /// A that specifies the file to send. + /// + /// + /// The file is sent as the binary data. + /// + /// + /// + /// + /// An Action<bool> delegate or + /// if not needed. + /// + /// + /// The delegate invokes the method called when the send is complete. + /// + /// + /// true is passed to the method if the send has done with + /// no error; otherwise, false. + /// + /// + /// + /// The current state of the connection is not Open. + /// + /// + /// is . + /// + /// + /// + /// The file does not exist. + /// + /// + /// -or- + /// + /// + /// The file could not be opened. + /// + /// + public void SendAsync (FileInfo fileInfo, Action completed) + { + if (_readyState != WebSocketState.Open) { + var msg = "The current state of the connection is not Open."; + throw new InvalidOperationException (msg); + } + + if (fileInfo == null) + throw new ArgumentNullException ("fileInfo"); + + if (!fileInfo.Exists) { + var msg = "The file does not exist."; + throw new ArgumentException (msg, "fileInfo"); + } + + FileStream stream; + if (!fileInfo.TryOpenRead (out stream)) { + var msg = "The file could not be opened."; + throw new ArgumentException (msg, "fileInfo"); + } + + sendAsync (Opcode.Binary, stream, completed); + } + + /// + /// Sends the specified data asynchronously using the WebSocket connection. + /// + /// + /// This method does not wait for the send to be complete. + /// + /// + /// A that represents the text data to send. + /// + /// + /// + /// An Action<bool> delegate or + /// if not needed. + /// + /// + /// The delegate invokes the method called when the send is complete. + /// + /// + /// true is passed to the method if the send has done with + /// no error; otherwise, false. + /// + /// + /// + /// The current state of the connection is not Open. + /// + /// + /// is . + /// + /// + /// could not be UTF-8-encoded. + /// + public void SendAsync (string data, Action completed) + { + if (_readyState != WebSocketState.Open) { + var msg = "The current state of the connection is not Open."; + throw new InvalidOperationException (msg); + } + + if (data == null) + throw new ArgumentNullException ("data"); + + byte[] bytes; + if (!data.TryGetUTF8EncodedBytes (out bytes)) { + var msg = "It could not be UTF-8-encoded."; + throw new ArgumentException (msg, "data"); + } + + sendAsync (Opcode.Text, new MemoryStream (bytes), completed); + } + + /// + /// Sends the data from the specified stream asynchronously using + /// the WebSocket connection. + /// + /// + /// This method does not wait for the send to be complete. + /// + /// + /// + /// A instance from which to read the data to send. + /// + /// + /// The data is sent as the binary data. + /// + /// + /// + /// An that specifies the number of bytes to send. + /// + /// + /// + /// An Action<bool> delegate or + /// if not needed. + /// + /// + /// The delegate invokes the method called when the send is complete. + /// + /// + /// true is passed to the method if the send has done with + /// no error; otherwise, false. + /// + /// + /// + /// The current state of the connection is not Open. + /// + /// + /// is . + /// + /// + /// + /// cannot be read. + /// + /// + /// -or- + /// + /// + /// is less than 1. + /// + /// + /// -or- + /// + /// + /// No data could be read from . + /// + /// + public void SendAsync (Stream stream, int length, Action completed) + { + if (_readyState != WebSocketState.Open) { + var msg = "The current state of the connection is not Open."; + throw new InvalidOperationException (msg); + } + + if (stream == null) + throw new ArgumentNullException ("stream"); + + if (!stream.CanRead) { + var msg = "It cannot be read."; + throw new ArgumentException (msg, "stream"); + } + + if (length < 1) { + var msg = "Less than 1."; + throw new ArgumentException (msg, "length"); + } + + var bytes = stream.ReadBytes (length); + + var len = bytes.Length; + if (len == 0) { + var msg = "No data could be read from it."; + throw new ArgumentException (msg, "stream"); + } + + if (len < length) { + _logger.Warn ( + String.Format ( + "Only {0} byte(s) of data could be read from the stream.", + len + ) + ); + } + + sendAsync (Opcode.Binary, new MemoryStream (bytes), completed); + } + + /// + /// Sets an HTTP cookie to send with the handshake request. + /// + /// + /// This method does nothing if the connection has already been + /// established or it is closing. + /// + /// + /// A that represents the cookie to send. + /// + /// + /// This instance is not a client. + /// + /// + /// is . + /// + public void SetCookie (Cookie cookie) + { + string msg = null; + + if (!_client) { + msg = "This instance is not a client."; + throw new InvalidOperationException (msg); + } + + if (cookie == null) + throw new ArgumentNullException ("cookie"); + + if (!canSet (out msg)) { + _logger.Warn (msg); + return; + } + + lock (_forState) { + if (!canSet (out msg)) { + _logger.Warn (msg); + return; + } + + lock (_cookies.SyncRoot) + _cookies.SetOrRemove (cookie); + } + } + + /// + /// Sets the credentials for the HTTP authentication (Basic/Digest). + /// + /// + /// This method does nothing if the connection has already been + /// established or it is closing. + /// + /// + /// + /// A that represents the username associated with + /// the credentials. + /// + /// + /// or an empty string if initializes + /// the credentials. + /// + /// + /// + /// + /// A that represents the password for the username + /// associated with the credentials. + /// + /// + /// or an empty string if not necessary. + /// + /// + /// + /// true if sends the credentials for the Basic authentication in + /// advance with the first handshake request; otherwise, false. + /// + /// + /// This instance is not a client. + /// + /// + /// + /// contains an invalid character. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + public void SetCredentials (string username, string password, bool preAuth) + { + string msg = null; + + if (!_client) { + msg = "This instance is not a client."; + throw new InvalidOperationException (msg); + } + + if (!username.IsNullOrEmpty ()) { + if (username.Contains (':') || !username.IsText ()) { + msg = "It contains an invalid character."; + throw new ArgumentException (msg, "username"); + } + } + + if (!password.IsNullOrEmpty ()) { + if (!password.IsText ()) { + msg = "It contains an invalid character."; + throw new ArgumentException (msg, "password"); + } + } + + if (!canSet (out msg)) { + _logger.Warn (msg); + return; + } + + lock (_forState) { + if (!canSet (out msg)) { + _logger.Warn (msg); + return; + } + + if (username.IsNullOrEmpty ()) { + _credentials = null; + _preAuth = false; + + return; + } + + _credentials = new NetworkCredential ( + username, password, _uri.PathAndQuery + ); + + _preAuth = preAuth; + } + } + + /// + /// Sets the URL of the HTTP proxy server through which to connect and + /// the credentials for the HTTP proxy authentication (Basic/Digest). + /// + /// + /// This method does nothing if the connection has already been + /// established or it is closing. + /// + /// + /// + /// A that represents the URL of the proxy server + /// through which to connect. + /// + /// + /// The syntax is http://<host>[:<port>]. + /// + /// + /// or an empty string if initializes the URL and + /// the credentials. + /// + /// + /// + /// + /// A that represents the username associated with + /// the credentials. + /// + /// + /// or an empty string if the credentials are not + /// necessary. + /// + /// + /// + /// + /// A that represents the password for the username + /// associated with the credentials. + /// + /// + /// or an empty string if not necessary. + /// + /// + /// + /// This instance is not a client. + /// + /// + /// + /// is not an absolute URI string. + /// + /// + /// -or- + /// + /// + /// The scheme of is not http. + /// + /// + /// -or- + /// + /// + /// includes the path segments. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + public void SetProxy (string url, string username, string password) + { + string msg = null; + + if (!_client) { + msg = "This instance is not a client."; + throw new InvalidOperationException (msg); + } + + Uri uri = null; + + if (!url.IsNullOrEmpty ()) { + if (!Uri.TryCreate (url, UriKind.Absolute, out uri)) { + msg = "Not an absolute URI string."; + throw new ArgumentException (msg, "url"); + } + + if (uri.Scheme != "http") { + msg = "The scheme part is not http."; + throw new ArgumentException (msg, "url"); + } + + if (uri.Segments.Length > 1) { + msg = "It includes the path segments."; + throw new ArgumentException (msg, "url"); + } + } + + if (!username.IsNullOrEmpty ()) { + if (username.Contains (':') || !username.IsText ()) { + msg = "It contains an invalid character."; + throw new ArgumentException (msg, "username"); + } + } + + if (!password.IsNullOrEmpty ()) { + if (!password.IsText ()) { + msg = "It contains an invalid character."; + throw new ArgumentException (msg, "password"); + } + } + + if (!canSet (out msg)) { + _logger.Warn (msg); + return; + } + + lock (_forState) { + if (!canSet (out msg)) { + _logger.Warn (msg); + return; + } + + if (url.IsNullOrEmpty ()) { + _proxyUri = null; + _proxyCredentials = null; + + return; + } + + _proxyUri = uri; + _proxyCredentials = !username.IsNullOrEmpty () + ? new NetworkCredential ( + username, + password, + String.Format ( + "{0}:{1}", _uri.DnsSafeHost, _uri.Port + ) + ) + : null; + } + } + + #endregion + + #region Explicit Interface Implementations + + /// + /// Closes the connection and releases all associated resources. + /// + /// + /// + /// This method closes the connection with close status 1001 (going away). + /// + /// + /// And this method does nothing if the current state of the connection is + /// Closing or Closed. + /// + /// + void IDisposable.Dispose () + { + close (1001, String.Empty); + } + + #endregion + } +} diff --git a/websocket-sharp-core/WebSocketException.cs b/websocket-sharp-core/WebSocketException.cs new file mode 100644 index 000000000..81d7c8081 --- /dev/null +++ b/websocket-sharp-core/WebSocketException.cs @@ -0,0 +1,109 @@ +#region License +/* + * WebSocketException.cs + * + * The MIT License + * + * 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 + * 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; + +namespace WebSocketSharp +{ + /// + /// The exception that is thrown when a fatal error occurs in + /// the WebSocket communication. + /// + public class WebSocketException : Exception + { + #region Private Fields + + private CloseStatusCode _code; + + #endregion + + #region Internal Constructors + + internal WebSocketException () + : this (CloseStatusCode.Abnormal, null, null) + { + } + + internal WebSocketException (Exception innerException) + : this (CloseStatusCode.Abnormal, null, innerException) + { + } + + internal WebSocketException (string message) + : this (CloseStatusCode.Abnormal, message, null) + { + } + + internal WebSocketException (CloseStatusCode code) + : this (code, null, null) + { + } + + internal WebSocketException (string message, Exception innerException) + : this (CloseStatusCode.Abnormal, message, innerException) + { + } + + internal WebSocketException (CloseStatusCode code, Exception innerException) + : this (code, null, innerException) + { + } + + internal WebSocketException (CloseStatusCode code, string message) + : this (code, message, null) + { + } + + internal WebSocketException ( + CloseStatusCode code, string message, Exception innerException + ) + : base (message ?? code.GetMessage (), innerException) + { + _code = code; + } + + #endregion + + #region Public Properties + + /// + /// Gets the status code indicating the cause of the exception. + /// + /// + /// One of the enum values that represents + /// the status code indicating the cause of the exception. + /// + public CloseStatusCode Code { + get { + return _code; + } + } + + #endregion + } +} diff --git a/websocket-sharp-core/WebSocketFrame.cs b/websocket-sharp-core/WebSocketFrame.cs new file mode 100644 index 000000000..ba0de3cd8 --- /dev/null +++ b/websocket-sharp-core/WebSocketFrame.cs @@ -0,0 +1,895 @@ +#region License +/* + * WebSocketFrame.cs + * + * The MIT License + * + * Copyright (c) 2012-2019 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Contributors +/* + * Contributors: + * - Chris Swiedler + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace WebSocketSharp +{ + 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 ping frame, + /// so it can only be used to send a ping from the server. + /// + internal static readonly byte[] EmptyPingBytes; + + #endregion + + #region Static Constructor + + static WebSocketFrame () + { + EmptyPingBytes = CreatePingFrame (false).ToArray (); + } + + #endregion + + #region Private Constructors + + private WebSocketFrame () + { + } + + #endregion + + #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 + ) + : this (fin, opcode, new PayloadData (data), compressed, mask) + { + } + + internal WebSocketFrame ( + Fin fin, + Opcode opcode, + PayloadData payloadData, + bool compressed, + bool mask + ) + { + _fin = fin; + _opcode = opcode; + + _rsv1 = opcode.IsData () && compressed ? Rsv.On : Rsv.Off; + _rsv2 = Rsv.Off; + _rsv3 = Rsv.Off; + + var len = payloadData.Length; + if (len < 126) { + _payloadLength = (byte) len; + _extPayloadLength = WebSocket.EmptyBytes; + } + else if (len < 0x010000) { + _payloadLength = (byte) 126; + _extPayloadLength = ((ushort) len).InternalToByteArray (ByteOrder.Big); + } + else { + _payloadLength = (byte) 127; + _extPayloadLength = len.InternalToByteArray (ByteOrder.Big); + } + + if (mask) { + _mask = Mask.On; + _maskingKey = createMaskingKey (); + payloadData.Mask (_maskingKey); + } + else { + _mask = Mask.Off; + _maskingKey = WebSocket.EmptyBytes; + } + + _payloadData = payloadData; + } + + #endregion + + #region Internal Properties + + internal ulong ExactPayloadLength { + get { + return _payloadLength < 126 + ? _payloadLength + : _payloadLength == 126 + ? _extPayloadLength.ToUInt16 (ByteOrder.Big) + : _extPayloadLength.ToUInt64 (ByteOrder.Big); + } + } + + internal int ExtendedPayloadLengthWidth { + get { + return _payloadLength < 126 + ? 0 + : _payloadLength == 126 + ? 2 + : 8; + } + } + + #endregion + + #region Public Properties + + public byte[] ExtendedPayloadLength { + get { + return _extPayloadLength; + } + } + + public Fin Fin { + get { + return _fin; + } + } + + public bool IsBinary { + get { + return _opcode == Opcode.Binary; + } + } + + public bool IsClose { + get { + return _opcode == Opcode.Close; + } + } + + public bool IsCompressed { + get { + return _rsv1 == Rsv.On; + } + } + + public bool IsContinuation { + get { + return _opcode == Opcode.Cont; + } + } + + public bool IsControl { + get { + return _opcode >= Opcode.Close; + } + } + + public bool IsData { + get { + return _opcode == Opcode.Text || _opcode == Opcode.Binary; + } + } + + public bool IsFinal { + get { + return _fin == Fin.Final; + } + } + + public bool IsFragment { + get { + return _fin == Fin.More || _opcode == Opcode.Cont; + } + } + + public bool IsMasked { + get { + return _mask == Mask.On; + } + } + + public bool IsPing { + get { + return _opcode == Opcode.Ping; + } + } + + public bool IsPong { + get { + return _opcode == Opcode.Pong; + } + } + + public bool IsText { + get { + return _opcode == Opcode.Text; + } + } + + public ulong Length { + get { + return 2 + + (ulong) (_extPayloadLength.Length + _maskingKey.Length) + + _payloadData.Length; + } + } + + public Mask Mask { + get { + return _mask; + } + } + + public byte[] MaskingKey { + get { + return _maskingKey; + } + } + + public Opcode Opcode { + get { + return _opcode; + } + } + + public PayloadData PayloadData { + get { + return _payloadData; + } + } + + public byte PayloadLength { + get { + return _payloadLength; + } + } + + public Rsv Rsv1 { + get { + return _rsv1; + } + } + + public Rsv Rsv2 { + get { + return _rsv2; + } + } + + public Rsv Rsv3 { + get { + return _rsv3; + } + } + + #endregion + + #region Private Methods + + private static byte[] createMaskingKey () + { + var key = new byte[4]; + WebSocket.RandomNumber.GetBytes (key); + + return key; + } + + private static string dump (WebSocketFrame frame) + { + 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 + ); + + var buff = new StringBuilder (64); + + Func> linePrinter = + () => { + long lineCnt = 0; + return (arg1, arg2, arg3, arg4) => { + buff.AppendFormat ( + lineFmt, ++lineCnt, arg1, arg2, arg3, arg4 + ); + }; + }; + + var printLine = linePrinter (); + var bytes = frame.ToArray (); + + buff.AppendFormat (headerFmt, String.Empty); + + 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 + ); + } + } + + buff.AppendFormat (footerFmt, String.Empty); + return buff.ToString (); + } + + private static string print (WebSocketFrame frame) + { + // Payload Length + var payloadLen = frame._payloadLength; + + // Extended Payload Length + var extPayloadLen = payloadLen > 125 + ? frame.ExactPayloadLength.ToString () + : String.Empty; + + // Masking Key + var maskingKey = BitConverter.ToString (frame._maskingKey); + + // Payload Data + var payload = payloadLen == 0 + ? String.Empty + : payloadLen > 125 + ? "---" + : !frame.IsText + || frame.IsFragment + || frame.IsMasked + || frame.IsCompressed + ? frame._payloadData.ToString () + : utf8Decode (frame._payloadData.ApplicationData); + + 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) { + var msg = "The header part of a frame could not be read."; + throw new WebSocketException (msg); + } + + // FIN + var fin = (header[0] & 0x80) == 0x80 ? Fin.Final : Fin.More; + + // RSV1 + var rsv1 = (header[0] & 0x40) == 0x40 ? Rsv.On : Rsv.Off; + + // RSV2 + var rsv2 = (header[0] & 0x20) == 0x20 ? Rsv.On : Rsv.Off; + + // RSV3 + var rsv3 = (header[0] & 0x10) == 0x10 ? Rsv.On : Rsv.Off; + + // Opcode + var opcode = (byte) (header[0] & 0x0f); + + // MASK + var mask = (header[1] & 0x80) == 0x80 ? Mask.On : Mask.Off; + + // Payload Length + var payloadLen = (byte) (header[1] & 0x7f); + + if (!opcode.IsSupported ()) { + var msg = "A frame has an unsupported opcode."; + throw new WebSocketException (CloseStatusCode.ProtocolError, msg); + } + + if (!opcode.IsData () && rsv1 == Rsv.On) { + var msg = "A non data frame is compressed."; + throw new WebSocketException (CloseStatusCode.ProtocolError, msg); + } + + if (opcode.IsControl ()) { + if (fin == Fin.More) { + var msg = "A control frame is fragmented."; + throw new WebSocketException (CloseStatusCode.ProtocolError, msg); + } + + if (payloadLen > 125) { + var msg = "A control frame has too long payload length."; + throw new WebSocketException (CloseStatusCode.ProtocolError, msg); + } + } + + var frame = new WebSocketFrame (); + frame._fin = fin; + frame._rsv1 = rsv1; + frame._rsv2 = rsv2; + frame._rsv3 = rsv3; + frame._opcode = (Opcode) opcode; + frame._mask = mask; + frame._payloadLength = payloadLen; + + return frame; + } + + private static WebSocketFrame readExtendedPayloadLength ( + Stream stream, WebSocketFrame frame + ) + { + var len = frame.ExtendedPayloadLengthWidth; + if (len == 0) { + frame._extPayloadLength = WebSocket.EmptyBytes; + return frame; + } + + var bytes = stream.ReadBytes (len); + 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; + } + + private static void readExtendedPayloadLengthAsync ( + Stream stream, + WebSocketFrame frame, + Action completed, + Action error + ) + { + var len = frame.ExtendedPayloadLengthWidth; + if (len == 0) { + frame._extPayloadLength = WebSocket.EmptyBytes; + completed (frame); + + return; + } + + stream.ReadBytesAsync ( + len, + bytes => { + 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 + ); + } + + private static WebSocketFrame readHeader (Stream stream) + { + return processHeader (stream.ReadBytes (2)); + } + + private static void readHeaderAsync ( + Stream stream, Action completed, Action error + ) + { + stream.ReadBytesAsync ( + 2, bytes => completed (processHeader (bytes)), error + ); + } + + private static WebSocketFrame readMaskingKey ( + Stream stream, WebSocketFrame frame + ) + { + if (!frame.IsMasked) { + frame._maskingKey = WebSocket.EmptyBytes; + return frame; + } + + var len = 4; + var bytes = stream.ReadBytes (len); + + if (bytes.Length != len) { + var msg = "The masking key of a frame could not be read."; + throw new WebSocketException (msg); + } + + frame._maskingKey = bytes; + return frame; + } + + private static void readMaskingKeyAsync ( + Stream stream, + WebSocketFrame frame, + Action completed, + Action error + ) + { + if (!frame.IsMasked) { + frame._maskingKey = WebSocket.EmptyBytes; + completed (frame); + + return; + } + + var len = 4; + + stream.ReadBytesAsync ( + len, + bytes => { + if (bytes.Length != len) { + var msg = "The masking key of a frame could not be read."; + throw new WebSocketException (msg); + } + + frame._maskingKey = bytes; + completed (frame); + }, + error + ); + } + + private static WebSocketFrame readPayloadData ( + Stream stream, WebSocketFrame frame + ) + { + var exactLen = frame.ExactPayloadLength; + if (exactLen > PayloadData.MaxLength) { + var msg = "A frame has too long payload length."; + throw new WebSocketException (CloseStatusCode.TooBig, msg); + } + + if (exactLen == 0) { + frame._payloadData = PayloadData.Empty; + return frame; + } + + var len = (long) exactLen; + var bytes = frame._payloadLength < 127 + ? stream.ReadBytes ((int) exactLen) + : stream.ReadBytes (len, 1024); + + if (bytes.LongLength != len) { + var msg = "The payload data of a frame could not be read."; + throw new WebSocketException (msg); + } + + frame._payloadData = new PayloadData (bytes, len); + return frame; + } + + private static void readPayloadDataAsync ( + Stream stream, + WebSocketFrame frame, + Action completed, + Action error + ) + { + var exactLen = frame.ExactPayloadLength; + if (exactLen > PayloadData.MaxLength) { + var msg = "A frame has too long payload length."; + throw new WebSocketException (CloseStatusCode.TooBig, msg); + } + + if (exactLen == 0) { + frame._payloadData = PayloadData.Empty; + completed (frame); + + return; + } + + var len = (long) exactLen; + Action comp = + bytes => { + if (bytes.LongLength != len) { + var msg = "The payload data of a frame could not be read."; + throw new WebSocketException (msg); + } + + frame._payloadData = new PayloadData (bytes, len); + completed (frame); + }; + + if (frame._payloadLength < 127) { + stream.ReadBytesAsync ((int) exactLen, comp, error); + return; + } + + stream.ReadBytesAsync (len, 1024, comp, error); + } + + private static string utf8Decode (byte[] bytes) + { + try { + return Encoding.UTF8.GetString (bytes); + } + catch { + return null; + } + } + + #endregion + + #region Internal Methods + + internal static WebSocketFrame CreateCloseFrame ( + PayloadData payloadData, bool 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 + ); + } + + internal static WebSocketFrame CreatePingFrame (byte[] data, bool 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); + + if (unmask) + frame.Unmask (); + + return frame; + } + + internal static void ReadFrameAsync ( + Stream stream, + bool unmask, + Action completed, + Action error + ) + { + readHeaderAsync ( + stream, + frame => + readExtendedPayloadLengthAsync ( + stream, + frame, + frame1 => + readMaskingKeyAsync ( + stream, + frame1, + frame2 => + readPayloadDataAsync ( + stream, + frame2, + frame3 => { + if (unmask) + frame3.Unmask (); + + completed (frame3); + }, + error + ), + error + ), + error + ), + error + ); + } + + internal void Unmask () + { + if (_mask == Mask.Off) + return; + + _mask = Mask.Off; + _payloadData.Mask (_maskingKey); + _maskingKey = WebSocket.EmptyBytes; + } + + #endregion + + #region Public Methods + + public IEnumerator GetEnumerator () + { + foreach (var b in ToArray ()) + 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 + ); + + if (_payloadLength > 125) + buff.Write (_extPayloadLength, 0, _payloadLength == 126 ? 2 : 8); + + if (_mask == Mask.On) + buff.Write (_maskingKey, 0, 4); + + if (_payloadLength > 0) { + var bytes = _payloadData.ToArray (); + + if (_payloadLength < 127) + buff.Write (bytes, 0, bytes.Length); + else + buff.WriteBytes (bytes, 1024); + } + + buff.Close (); + return buff.ToArray (); + } + } + + public override string ToString () + { + return BitConverter.ToString (ToArray ()); + } + + #endregion + + #region Explicit Interface Implementations + + IEnumerator IEnumerable.GetEnumerator () + { + return GetEnumerator (); + } + + #endregion + } +} diff --git a/websocket-sharp-core/WebSocketState.cs b/websocket-sharp-core/WebSocketState.cs new file mode 100644 index 000000000..2cbcd688d --- /dev/null +++ b/websocket-sharp-core/WebSocketState.cs @@ -0,0 +1,65 @@ +#region License +/* + * WebSocketState.cs + * + * The MIT License + * + * Copyright (c) 2010-2016 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; + +namespace WebSocketSharp +{ + /// + /// Indicates the state of a WebSocket connection. + /// + /// + /// The values of this enumeration are defined in + /// + /// The WebSocket API. + /// + public enum WebSocketState : ushort + { + /// + /// Equivalent to numeric value 0. Indicates that the connection has not + /// yet been established. + /// + Connecting = 0, + /// + /// Equivalent to numeric value 1. Indicates that the connection has + /// been established, and the communication is possible. + /// + Open = 1, + /// + /// Equivalent to numeric value 2. Indicates that the connection is + /// going through the closing handshake, or the close method has + /// been invoked. + /// + Closing = 2, + /// + /// Equivalent to numeric value 3. Indicates that the connection has + /// been closed or could not be established. + /// + Closed = 3 + } +} diff --git a/websocket-sharp-core/websocket-sharp-core.csproj b/websocket-sharp-core/websocket-sharp-core.csproj new file mode 100644 index 000000000..a648dc001 --- /dev/null +++ b/websocket-sharp-core/websocket-sharp-core.csproj @@ -0,0 +1,8 @@ + + + + netstandard2.1 + websocket_sharp_core + + + diff --git a/websocket-sharp-core/websocket-sharp.snk b/websocket-sharp-core/websocket-sharp.snk new file mode 100644 index 000000000..a2546f385 Binary files /dev/null and b/websocket-sharp-core/websocket-sharp.snk differ diff --git a/websocket-sharp.sln b/websocket-sharp.sln index 3c20e06a0..3cafe6b91 100644 --- a/websocket-sharp.sln +++ b/websocket-sharp.sln @@ -1,64 +1,32 @@  -Microsoft Visual Studio Solution File, Format Version 10.00 -# Visual Studio 2008 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "websocket-sharp", "websocket-sharp\websocket-sharp.csproj", "{B357BAC7-529E-4D81-A0D2-71041B19C8DE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example", "Example\Example.csproj", "{52805AEC-EFB1-4F42-BB8E-3ED4E692C568}" -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}" +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29613.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "websocket-sharp-core", "websocket-sharp-core\websocket-sharp-core.csproj", "{37AC9B85-1759-470F-922D-F71AC49ED4BA}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU Debug_Ubuntu|Any CPU = Debug_Ubuntu|Any CPU + Debug|Any CPU = Debug|Any CPU Release_Ubuntu|Any CPU = Release_Ubuntu|Any CPU + Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {390E2568-57B7-4D17-91E5-C29336368CCF}.Debug_Ubuntu|Any CPU.ActiveCfg = Debug_Ubuntu|Any CPU - {390E2568-57B7-4D17-91E5-C29336368CCF}.Debug_Ubuntu|Any CPU.Build.0 = Debug_Ubuntu|Any CPU - {390E2568-57B7-4D17-91E5-C29336368CCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {390E2568-57B7-4D17-91E5-C29336368CCF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {390E2568-57B7-4D17-91E5-C29336368CCF}.Release_Ubuntu|Any CPU.ActiveCfg = Release_Ubuntu|Any CPU - {390E2568-57B7-4D17-91E5-C29336368CCF}.Release_Ubuntu|Any CPU.Build.0 = Release_Ubuntu|Any CPU - {390E2568-57B7-4D17-91E5-C29336368CCF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {390E2568-57B7-4D17-91E5-C29336368CCF}.Release|Any CPU.Build.0 = Release|Any CPU - {52805AEC-EFB1-4F42-BB8E-3ED4E692C568}.Debug_Ubuntu|Any CPU.ActiveCfg = Debug_Ubuntu|Any CPU - {52805AEC-EFB1-4F42-BB8E-3ED4E692C568}.Debug_Ubuntu|Any CPU.Build.0 = Debug_Ubuntu|Any CPU - {52805AEC-EFB1-4F42-BB8E-3ED4E692C568}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {52805AEC-EFB1-4F42-BB8E-3ED4E692C568}.Debug|Any CPU.Build.0 = Debug|Any CPU - {52805AEC-EFB1-4F42-BB8E-3ED4E692C568}.Release_Ubuntu|Any CPU.ActiveCfg = Release_Ubuntu|Any CPU - {52805AEC-EFB1-4F42-BB8E-3ED4E692C568}.Release_Ubuntu|Any CPU.Build.0 = Release_Ubuntu|Any CPU - {52805AEC-EFB1-4F42-BB8E-3ED4E692C568}.Release|Any CPU.ActiveCfg = Release|Any CPU - {52805AEC-EFB1-4F42-BB8E-3ED4E692C568}.Release|Any CPU.Build.0 = Release|Any CPU - {B357BAC7-529E-4D81-A0D2-71041B19C8DE}.Debug_Ubuntu|Any CPU.ActiveCfg = Debug_Ubuntu|Any CPU - {B357BAC7-529E-4D81-A0D2-71041B19C8DE}.Debug_Ubuntu|Any CPU.Build.0 = Debug_Ubuntu|Any CPU - {B357BAC7-529E-4D81-A0D2-71041B19C8DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B357BAC7-529E-4D81-A0D2-71041B19C8DE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B357BAC7-529E-4D81-A0D2-71041B19C8DE}.Release_Ubuntu|Any CPU.ActiveCfg = Release_Ubuntu|Any CPU - {B357BAC7-529E-4D81-A0D2-71041B19C8DE}.Release_Ubuntu|Any CPU.Build.0 = Release_Ubuntu|Any CPU - {B357BAC7-529E-4D81-A0D2-71041B19C8DE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B357BAC7-529E-4D81-A0D2-71041B19C8DE}.Release|Any CPU.Build.0 = Release|Any CPU - {B81A24C8-25BB-42B2-AF99-1E1EACCE74C7}.Debug_Ubuntu|Any CPU.ActiveCfg = Debug_Ubuntu|Any CPU - {B81A24C8-25BB-42B2-AF99-1E1EACCE74C7}.Debug_Ubuntu|Any CPU.Build.0 = Debug_Ubuntu|Any CPU - {B81A24C8-25BB-42B2-AF99-1E1EACCE74C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B81A24C8-25BB-42B2-AF99-1E1EACCE74C7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B81A24C8-25BB-42B2-AF99-1E1EACCE74C7}.Release_Ubuntu|Any CPU.ActiveCfg = Release_Ubuntu|Any CPU - {B81A24C8-25BB-42B2-AF99-1E1EACCE74C7}.Release_Ubuntu|Any CPU.Build.0 = Release_Ubuntu|Any CPU - {B81A24C8-25BB-42B2-AF99-1E1EACCE74C7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B81A24C8-25BB-42B2-AF99-1E1EACCE74C7}.Release|Any CPU.Build.0 = Release|Any CPU - {C648BA25-77E5-4A40-A97F-D0AA37B9FB26}.Debug_Ubuntu|Any CPU.ActiveCfg = Debug_Ubuntu|Any CPU - {C648BA25-77E5-4A40-A97F-D0AA37B9FB26}.Debug_Ubuntu|Any CPU.Build.0 = Debug_Ubuntu|Any CPU - {C648BA25-77E5-4A40-A97F-D0AA37B9FB26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C648BA25-77E5-4A40-A97F-D0AA37B9FB26}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C648BA25-77E5-4A40-A97F-D0AA37B9FB26}.Release_Ubuntu|Any CPU.ActiveCfg = Release_Ubuntu|Any CPU - {C648BA25-77E5-4A40-A97F-D0AA37B9FB26}.Release_Ubuntu|Any CPU.Build.0 = Release_Ubuntu|Any CPU - {C648BA25-77E5-4A40-A97F-D0AA37B9FB26}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C648BA25-77E5-4A40-A97F-D0AA37B9FB26}.Release|Any CPU.Build.0 = Release|Any CPU + {37AC9B85-1759-470F-922D-F71AC49ED4BA}.Debug_Ubuntu|Any CPU.ActiveCfg = Debug|Any CPU + {37AC9B85-1759-470F-922D-F71AC49ED4BA}.Debug_Ubuntu|Any CPU.Build.0 = Debug|Any CPU + {37AC9B85-1759-470F-922D-F71AC49ED4BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {37AC9B85-1759-470F-922D-F71AC49ED4BA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {37AC9B85-1759-470F-922D-F71AC49ED4BA}.Release_Ubuntu|Any CPU.ActiveCfg = Release|Any CPU + {37AC9B85-1759-470F-922D-F71AC49ED4BA}.Release_Ubuntu|Any CPU.Build.0 = Release|Any CPU + {37AC9B85-1759-470F-922D-F71AC49ED4BA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {37AC9B85-1759-470F-922D-F71AC49ED4BA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {49A21385-B502-4EA4-906D-B9EA7F6613C7} EndGlobalSection GlobalSection(MonoDevelopProperties) = preSolution StartupItem = websocket-sharp\websocket-sharp.csproj diff --git a/websocket-sharp/websocket-sharp.csproj b/websocket-sharp/websocket-sharp.csproj index 0860c0313..8d4960b73 100644 --- a/websocket-sharp/websocket-sharp.csproj +++ b/websocket-sharp/websocket-sharp.csproj @@ -1,5 +1,5 @@ - + Debug AnyCPU @@ -12,6 +12,11 @@ v3.5 true websocket-sharp.snk + + + + + 3.5 true