From fa35b949e057272e6513d18a3c677099bc2cb361 Mon Sep 17 00:00:00 2001 From: Nick Beaugeard Date: Tue, 28 Jan 2020 09:59:28 +1100 Subject: [PATCH] Updates --- .vs/websocket-sharp/DesignTimeBuild/.dtbcache | Bin 0 -> 183294 bytes .../v16/Server/sqlite3/db.lock | 0 .../v16/Server/sqlite3/storage.ide | Bin 0 -> 1028096 bytes Example/Example.csproj | 7 +- Example1/Example1.csproj | 7 +- Example2/Example2.csproj | 7 +- Example3/Example3.csproj | 7 +- websocket-sharp-core/AssemblyInfo.cs | 26 + websocket-sharp-core/ByteOrder.cs | 47 + websocket-sharp-core/CloseEventArgs.cs | 113 + websocket-sharp-core/CloseStatusCode.cs | 120 + websocket-sharp-core/CompressionMethod.cs | 52 + websocket-sharp-core/ErrorEventArgs.cs | 109 + websocket-sharp-core/Ext.cs | 2268 +++++++++ websocket-sharp-core/Fin.cs | 51 + websocket-sharp-core/HttpBase.cs | 208 + websocket-sharp-core/HttpRequest.cs | 217 + websocket-sharp-core/HttpResponse.cs | 209 + websocket-sharp-core/LogData.cs | 149 + websocket-sharp-core/LogLevel.cs | 63 + websocket-sharp-core/Logger.cs | 330 ++ websocket-sharp-core/Mask.cs | 51 + websocket-sharp-core/MessageEventArgs.cs | 183 + .../Net/AuthenticationBase.cs | 151 + .../Net/AuthenticationChallenge.cs | 146 + .../Net/AuthenticationResponse.cs | 323 ++ .../Net/AuthenticationSchemes.cs | 66 + websocket-sharp-core/Net/Chunk.cs | 91 + websocket-sharp-core/Net/ChunkStream.cs | 360 ++ .../Net/ChunkedRequestStream.cs | 211 + .../Net/ClientSslConfiguration.cs | 291 ++ websocket-sharp-core/Net/Cookie.cs | 1016 ++++ websocket-sharp-core/Net/CookieCollection.cs | 821 ++++ websocket-sharp-core/Net/CookieException.cs | 165 + websocket-sharp-core/Net/EndPointListener.cs | 515 +++ websocket-sharp-core/Net/EndPointManager.cs | 240 + websocket-sharp-core/Net/HttpBasicIdentity.cs | 82 + websocket-sharp-core/Net/HttpConnection.cs | 597 +++ .../Net/HttpDigestIdentity.cs | 187 + websocket-sharp-core/Net/HttpHeaderInfo.cs | 114 + websocket-sharp-core/Net/HttpHeaderType.cs | 44 + websocket-sharp-core/Net/HttpListener.cs | 836 ++++ .../Net/HttpListenerAsyncResult.cs | 198 + .../Net/HttpListenerContext.cs | 256 ++ .../Net/HttpListenerException.cs | 127 + .../Net/HttpListenerPrefix.cs | 228 + .../Net/HttpListenerPrefixCollection.cs | 278 ++ .../Net/HttpListenerRequest.cs | 910 ++++ .../Net/HttpListenerResponse.cs | 1108 +++++ websocket-sharp-core/Net/HttpRequestHeader.cs | 233 + .../Net/HttpResponseHeader.cs | 189 + websocket-sharp-core/Net/HttpStatusCode.cs | 359 ++ .../Net/HttpStreamAsyncResult.cs | 184 + websocket-sharp-core/Net/HttpUtility.cs | 1146 +++++ websocket-sharp-core/Net/HttpVersion.cs | 73 + websocket-sharp-core/Net/InputChunkState.cs | 52 + websocket-sharp-core/Net/InputState.cs | 49 + websocket-sharp-core/Net/LineState.cs | 50 + websocket-sharp-core/Net/NetworkCredential.cs | 209 + .../Net/QueryStringCollection.cs | 150 + websocket-sharp-core/Net/ReadBufferState.cs | 124 + websocket-sharp-core/Net/RequestStream.cs | 267 ++ websocket-sharp-core/Net/ResponseStream.cs | 338 ++ .../Net/ServerSslConfiguration.cs | 245 + .../Net/WebHeaderCollection.cs | 1459 ++++++ .../HttpListenerWebSocketContext.cs | 394 ++ .../WebSockets/TcpListenerWebSocketContext.cs | 518 +++ .../Net/WebSockets/WebSocketContext.cs | 224 + websocket-sharp-core/Opcode.cs | 68 + websocket-sharp-core/PayloadData.cs | 208 + websocket-sharp-core/Rsv.cs | 51 + .../Server/HttpRequestEventArgs.cs | 255 + websocket-sharp-core/Server/HttpServer.cs | 1652 +++++++ .../Server/IWebSocketSession.cs | 91 + websocket-sharp-core/Server/ServerState.cs | 40 + .../Server/WebSocketBehavior.cs | 1204 +++++ .../Server/WebSocketServer.cs | 1518 ++++++ .../Server/WebSocketServiceHost.cs | 224 + .../Server/WebSocketServiceHost`1.cs | 102 + .../Server/WebSocketServiceManager.cs | 1078 +++++ .../Server/WebSocketSessionManager.cs | 1695 +++++++ websocket-sharp-core/WebSocket.cs | 4093 +++++++++++++++++ websocket-sharp-core/WebSocketException.cs | 109 + websocket-sharp-core/WebSocketFrame.cs | 895 ++++ websocket-sharp-core/WebSocketState.cs | 65 + .../websocket-sharp-core.csproj | 8 + websocket-sharp-core/websocket-sharp.snk | Bin 0 -> 596 bytes websocket-sharp.sln | 74 +- websocket-sharp/websocket-sharp.csproj | 7 +- 89 files changed, 33227 insertions(+), 58 deletions(-) create mode 100644 .vs/websocket-sharp/DesignTimeBuild/.dtbcache create mode 100644 .vs/websocket-sharp/v16/Server/sqlite3/db.lock create mode 100644 .vs/websocket-sharp/v16/Server/sqlite3/storage.ide create mode 100644 websocket-sharp-core/AssemblyInfo.cs create mode 100644 websocket-sharp-core/ByteOrder.cs create mode 100644 websocket-sharp-core/CloseEventArgs.cs create mode 100644 websocket-sharp-core/CloseStatusCode.cs create mode 100644 websocket-sharp-core/CompressionMethod.cs create mode 100644 websocket-sharp-core/ErrorEventArgs.cs create mode 100644 websocket-sharp-core/Ext.cs create mode 100644 websocket-sharp-core/Fin.cs create mode 100644 websocket-sharp-core/HttpBase.cs create mode 100644 websocket-sharp-core/HttpRequest.cs create mode 100644 websocket-sharp-core/HttpResponse.cs create mode 100644 websocket-sharp-core/LogData.cs create mode 100644 websocket-sharp-core/LogLevel.cs create mode 100644 websocket-sharp-core/Logger.cs create mode 100644 websocket-sharp-core/Mask.cs create mode 100644 websocket-sharp-core/MessageEventArgs.cs create mode 100644 websocket-sharp-core/Net/AuthenticationBase.cs create mode 100644 websocket-sharp-core/Net/AuthenticationChallenge.cs create mode 100644 websocket-sharp-core/Net/AuthenticationResponse.cs create mode 100644 websocket-sharp-core/Net/AuthenticationSchemes.cs create mode 100644 websocket-sharp-core/Net/Chunk.cs create mode 100644 websocket-sharp-core/Net/ChunkStream.cs create mode 100644 websocket-sharp-core/Net/ChunkedRequestStream.cs create mode 100644 websocket-sharp-core/Net/ClientSslConfiguration.cs create mode 100644 websocket-sharp-core/Net/Cookie.cs create mode 100644 websocket-sharp-core/Net/CookieCollection.cs create mode 100644 websocket-sharp-core/Net/CookieException.cs create mode 100644 websocket-sharp-core/Net/EndPointListener.cs create mode 100644 websocket-sharp-core/Net/EndPointManager.cs create mode 100644 websocket-sharp-core/Net/HttpBasicIdentity.cs create mode 100644 websocket-sharp-core/Net/HttpConnection.cs create mode 100644 websocket-sharp-core/Net/HttpDigestIdentity.cs create mode 100644 websocket-sharp-core/Net/HttpHeaderInfo.cs create mode 100644 websocket-sharp-core/Net/HttpHeaderType.cs create mode 100644 websocket-sharp-core/Net/HttpListener.cs create mode 100644 websocket-sharp-core/Net/HttpListenerAsyncResult.cs create mode 100644 websocket-sharp-core/Net/HttpListenerContext.cs create mode 100644 websocket-sharp-core/Net/HttpListenerException.cs create mode 100644 websocket-sharp-core/Net/HttpListenerPrefix.cs create mode 100644 websocket-sharp-core/Net/HttpListenerPrefixCollection.cs create mode 100644 websocket-sharp-core/Net/HttpListenerRequest.cs create mode 100644 websocket-sharp-core/Net/HttpListenerResponse.cs create mode 100644 websocket-sharp-core/Net/HttpRequestHeader.cs create mode 100644 websocket-sharp-core/Net/HttpResponseHeader.cs create mode 100644 websocket-sharp-core/Net/HttpStatusCode.cs create mode 100644 websocket-sharp-core/Net/HttpStreamAsyncResult.cs create mode 100644 websocket-sharp-core/Net/HttpUtility.cs create mode 100644 websocket-sharp-core/Net/HttpVersion.cs create mode 100644 websocket-sharp-core/Net/InputChunkState.cs create mode 100644 websocket-sharp-core/Net/InputState.cs create mode 100644 websocket-sharp-core/Net/LineState.cs create mode 100644 websocket-sharp-core/Net/NetworkCredential.cs create mode 100644 websocket-sharp-core/Net/QueryStringCollection.cs create mode 100644 websocket-sharp-core/Net/ReadBufferState.cs create mode 100644 websocket-sharp-core/Net/RequestStream.cs create mode 100644 websocket-sharp-core/Net/ResponseStream.cs create mode 100644 websocket-sharp-core/Net/ServerSslConfiguration.cs create mode 100644 websocket-sharp-core/Net/WebHeaderCollection.cs create mode 100644 websocket-sharp-core/Net/WebSockets/HttpListenerWebSocketContext.cs create mode 100644 websocket-sharp-core/Net/WebSockets/TcpListenerWebSocketContext.cs create mode 100644 websocket-sharp-core/Net/WebSockets/WebSocketContext.cs create mode 100644 websocket-sharp-core/Opcode.cs create mode 100644 websocket-sharp-core/PayloadData.cs create mode 100644 websocket-sharp-core/Rsv.cs create mode 100644 websocket-sharp-core/Server/HttpRequestEventArgs.cs create mode 100644 websocket-sharp-core/Server/HttpServer.cs create mode 100644 websocket-sharp-core/Server/IWebSocketSession.cs create mode 100644 websocket-sharp-core/Server/ServerState.cs create mode 100644 websocket-sharp-core/Server/WebSocketBehavior.cs create mode 100644 websocket-sharp-core/Server/WebSocketServer.cs create mode 100644 websocket-sharp-core/Server/WebSocketServiceHost.cs create mode 100644 websocket-sharp-core/Server/WebSocketServiceHost`1.cs create mode 100644 websocket-sharp-core/Server/WebSocketServiceManager.cs create mode 100644 websocket-sharp-core/Server/WebSocketSessionManager.cs create mode 100644 websocket-sharp-core/WebSocket.cs create mode 100644 websocket-sharp-core/WebSocketException.cs create mode 100644 websocket-sharp-core/WebSocketFrame.cs create mode 100644 websocket-sharp-core/WebSocketState.cs create mode 100644 websocket-sharp-core/websocket-sharp-core.csproj create mode 100644 websocket-sharp-core/websocket-sharp.snk diff --git a/.vs/websocket-sharp/DesignTimeBuild/.dtbcache b/.vs/websocket-sharp/DesignTimeBuild/.dtbcache new file mode 100644 index 0000000000000000000000000000000000000000..4e286051d4697dba673c940ddb799c478aa5c001 GIT binary patch literal 183294 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 C_?H?0 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..88827beea9f406b10b2b03a5906413a0984dab10 GIT binary patch literal 1028096 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a2546f385ff1c28221f1071dd2fbc1870d728eb2 GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONaL0000f7yF6+!;$wZ%ZuV+(mw^0ZDpvQV{)eT z)hzOzAo>fyaBB95ev6PlIrVnPNZ`ABvU}~*5T_asaF`E&^h;9-(xvYSmgrHTIc_N+d4!r0J+k@O7F^N!rZ!?-v-48IrNAb7!} z-N_==YbBhM_zb%3vcPBeECpwQOr<(i6$!ynrQ~`QXjiMFwVBazO>@JrfgI51ivGl6 zRjei2J2>~wP7}hz?AO{)>lh*();UW|5NUR0!Ji$O!OQ_E@UT|!^m$J!!@8AkltT%j z@^-wS`8NxnHzsxMW{zi$Sgj3%PBZ_5qAu2Olh=Q!lDnQk=0}kkW2jUYzmul4bgJIL zlK?W_`r?N;Z(!Kc`z;*hfOxrjI{zF803Sg&r~jaQ?Y?Fa8?Az+3a++ht8(mR9-o|j zE@`6|n4@2Af4?zHoP|-_#;rVGqar>7nH%|Nigv24jWsE z$LIC>T!MfxCrw6m*qpp=DPHF19B*63GF@WFjU6vWTmP4b`Oq(FIRY0(-TN1xl?|1D z-Sj2XD0j7$83O-B-{7UHg(P)vanCZOjten@X*^b4ZsM;$Wnsvy271`V8$NH_bgN{j zPL(SOf2I?l4(?fnuwJ_P05C5xZ~dI4vhml_=<z#%yL*k|m=6 literal 0 HcmV?d00001 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