From 8d7e806d145a1e7c9684e03a25d32b3d5b9b8158 Mon Sep 17 00:00:00 2001 From: EonaCat Date: Tue, 19 Mar 2024 18:02:29 +0100 Subject: [PATCH] Updated webSockets --- EonaCat.Network/EonaCat.Network.csproj | 96 +- EonaCat.Network/System/BlockChain/Block.cs | 54 - .../System/BlockChain/BlockChain.cs | 106 - .../System/BlockChain/ClientServerExample.cs | 75 - .../System/BlockChain/P2PClient.cs | 83 - .../System/BlockChain/P2PServer.cs | 51 - .../System/BlockChain/Transaction.cs | 16 - EonaCat.Network/System/Sockets/RemoteInfo.cs | 8 +- .../System/Sockets/Tcp/SocketTcpServer.cs | 9 +- .../System/Sockets/Web/ByteOrder.cs | 12 - .../System/Sockets/Web/CloseStatusCode.cs | 85 - .../System/Sockets/Web/CompressionMethod.cs | 12 - .../Core/Authentication/AuthenticationBase.cs | 129 - .../Authentication/AuthenticationChallenge.cs | 136 - .../Authentication/AuthenticationResponse.cs | 359 -- .../Authentication/AuthenticationSchemes.cs | 31 - .../Core/Authentication/NetworkCredential.cs | 113 - .../System/Sockets/Web/Core/Chunks/Chunk.cs | 56 - .../Sockets/Web/Core/Chunks/ChunkStream.cs | 398 --- .../Web/Core/Chunks/ChunkedRequestStream.cs | 208 -- .../Core/Collections/QueryStringCollection.cs | 41 - .../Core/Collections/WebHeaderCollection.cs | 599 ---- .../Web/Core/Context/HttpListenerWSContext.cs | 111 - .../Web/Core/Context/TcpListenerWSContext.cs | 230 -- .../Sockets/Web/Core/Context/WSContext.cs | 112 - .../System/Sockets/Web/Core/Cookies/Cookie.cs | 590 ---- .../Web/Core/Cookies/CookieCollection.cs | 545 --- .../Web/Core/Cookies/CookieException.cs | 81 - .../Web/Core/Endpoints/EndPointListener.cs | 532 --- .../Web/Core/Endpoints/EndPointManager.cs | 232 -- .../Web/Core/Http/HttpBasicIdentity.cs | 31 - .../Sockets/Web/Core/Http/HttpConnection.cs | 633 ---- .../Web/Core/Http/HttpDigestIdentity.cs | 95 - .../Sockets/Web/Core/Http/HttpHeaderInfo.cs | 74 - .../Sockets/Web/Core/Http/HttpHeaderType.cs | 49 - .../Sockets/Web/Core/Http/HttpListener.cs | 643 ---- .../Web/Core/Http/HttpListenerAsyncResult.cs | 162 - .../Web/Core/Http/HttpListenerContext.cs | 152 - .../Web/Core/Http/HttpListenerException.cs | 56 - .../Web/Core/Http/HttpListenerPrefix.cs | 167 - .../Core/Http/HttpListenerPrefixCollection.cs | 154 - .../Web/Core/Http/HttpListenerRequest.cs | 415 --- .../Web/Core/Http/HttpListenerResponse.cs | 647 ---- .../Web/Core/Http/HttpRequestHeader.cs | 101 - .../Web/Core/Http/HttpResponseHeader.cs | 79 - .../Sockets/Web/Core/Http/HttpStatusCode.cs | 103 - .../Web/Core/Http/HttpStreamAsyncResult.cs | 84 - .../Sockets/Web/Core/Http/HttpUtility.cs | 1363 -------- .../Sockets/Web/Core/Http/HttpVersion.cs | 30 - .../Sockets/Web/Core/InputChunkState.cs | 14 - .../System/Sockets/Web/Core/InputState.cs | 11 - .../System/Sockets/Web/Core/LineState.cs | 20 - .../System/Sockets/Web/Core/Logger.cs | 229 -- .../Sockets/Web/Core/ReadBufferState.cs | 28 - .../Web/Core/SSLConfig/SSLConfigClient.cs | 113 - .../Web/Core/SSLConfig/SSLConfigServer.cs | 71 - .../Sockets/Web/Core/Stream/RequestStream.cs | 237 -- .../Sockets/Web/Core/Stream/ResponseStream.cs | 286 -- .../Sockets/Web/Endpoints/WelcomeEndpoint.cs | 17 - .../Sockets/Web/EventArgs/CloseEventArgs.cs | 60 - .../Sockets/Web/EventArgs/ErrorEventArgs.cs | 25 - .../Sockets/Web/EventArgs/MessageEventArgs.cs | 72 - .../System/Sockets/Web/Extensions.cs | 1650 --------- .../System/Sockets/Web/FinalFrame.cs | 12 - EonaCat.Network/System/Sockets/Web/Mask.cs | 12 - .../System/Sockets/Web/OperationCode.cs | 20 - EonaCat.Network/System/Sockets/Web/Payload.cs | 155 - .../System/Sockets/Web/ReservedBits.cs | 12 - .../Web/Server/HttpRequestEventArgs.cs | 102 - .../System/Sockets/Web/Server/HttpServer.cs | 885 ----- .../System/Sockets/Web/Server/IWSSession.cs | 20 - .../System/Sockets/Web/Server/ServerState.cs | 13 - .../System/Sockets/Web/Server/WSEndpoint.cs | 209 -- .../Sockets/Web/Server/WSEndpointHost.cs | 66 - .../Sockets/Web/Server/WSEndpointManager.cs | 467 --- .../System/Sockets/Web/Server/WSServer.cs | 786 ----- .../Sockets/Web/Server/WSSessionManager.cs | 871 ----- .../Web/Server/WebSocketEndpointHost.cs | 55 - .../System/Sockets/Web/WSClient.cs | 2980 ----------------- .../System/Sockets/Web/WSException.cs | 55 - EonaCat.Network/System/Sockets/Web/WSFrame.cs | 641 ---- EonaCat.Network/System/Sockets/Web/WSState.cs | 16 - EonaCat.Network/System/Sockets/Web/WebBase.cs | 162 - .../System/Sockets/Web/WebRequest.cs | 172 - .../System/Sockets/Web/WebResponse.cs | 143 - .../WebSockets/Buffer/BufferValidator.cs | 49 + .../Buffer/ISegmentBufferManager.cs | 17 + .../Buffer/SegmentBufferDeflector.cs | 93 + .../WebSockets/Buffer/SegmentBufferManager.cs | 273 ++ .../Buffer/UnableToAllocateBufferException.cs | 16 + .../Buffer/UnableToCreateMemoryException .cs | 16 + .../WebSockets/Client/AsyncWebSocketClient.cs | 1332 ++++++++ .../AsyncWebSocketClientConfiguration.cs | 81 + .../IAsyncWebSocketClientMessageDispatcher.cs | 19 + ...etClientMessageDispatcherImplementation.cs | 113 + .../Client/WebSocketClientHandshaker.cs | 414 +++ .../Exceptions/WebSocketException.cs | 21 + .../Exceptions/WebSocketHandshakeException.cs | 21 + .../Extensions/IWebSocketExtension.cs | 21 + .../IWebSocketExtensionNegotiator.cs | 11 + .../Parameters/AbsentableValueParameter.cs | 34 + .../Parameters/AgreedExtensionParameter.cs | 28 + .../Parameters/AgreedSingleParameter.cs | 21 + .../Parameters/AgreedValuableParameter.cs | 29 + .../Parameters/ExtensionParameter.cs | 28 + .../Parameters/ExtensionParameterType.cs | 14 + .../Extensions/Parameters/SingleParameter.cs | 21 + .../Parameters/ValuableParameter.cs | 31 + .../PMCE/DeflateCompression.cs | 81 + .../PMCE/PerMessageCompressionExtension.cs | 97 + ...erMessageCompressionExtensionNegotiator.cs | 135 + ...erMessageCompressionExtensionParameters.cs | 203 ++ .../WebSocketExtensionOfferDescription.cs | 22 + .../Framing/BinaryFragmentationFrame.cs | 46 + .../Sockets/WebSockets/Framing/BinaryFrame.cs | 51 + .../Framing/Builder/IFrameBuilder.cs | 23 + .../Framing/Builder/WebSocketFrameBuilder.cs | 437 +++ .../Sockets/WebSockets/Framing/CloseFrame.cs | 41 + .../WebSockets/Framing/ControlFrame.cs | 9 + .../Sockets/WebSockets/Framing/DataFrame.cs | 9 + .../Sockets/WebSockets/Framing/Frame.cs | 15 + .../WebSockets/Framing/Header/Header.cs | 32 + .../Sockets/WebSockets/Framing/OpCode.cs | 27 + .../Sockets/WebSockets/Framing/PingFrame.cs | 39 + .../Sockets/WebSockets/Framing/PongFrame.cs | 39 + .../Sockets/WebSockets/Framing/TextFrame.cs | 39 + .../Sockets/WebSockets/Helpers/Consts.cs | 45 + .../Helpers/HttpKnownHeaderNames.cs | 144 + .../WebSockets/Helpers/KeepAliveTracker.cs | 172 + .../Helpers/StringBuilderExtensions.cs | 35 + .../WebSockets/Helpers/TplExtensions.cs | 14 + .../WebSockets/Helpers/WebSocketHelpers.cs | 77 + .../WebSockets/Server/AsyncWebSocketServer.cs | 346 ++ .../AsyncWebSocketServerConfiguration.cs | 80 + .../Server/AsyncWebSocketSession.cs | 1218 +++++++ .../Module/AsyncWebSocketRouteResolver.cs | 34 + .../Module/AsyncWebSocketServerModule.cs | 119 + .../AsyncWebSocketServerModuleCatalog.cs | 28 + .../IAsyncWebSocketServerMessageDispatcher.cs | 19 + .../Server/WebSocketServerHandshaker.cs | 459 +++ .../SubProtocols/IWebSocketSubProtocol.cs | 9 + .../IWebSocketSubProtocolNegotiator.cs | 11 + .../WebSocketSubProtocolRequestDescription.cs | 22 + .../Sockets/WebSockets/WebSocketCloseCode.cs | 29 + .../Sockets/WebSockets/WebSocketState.cs | 14 + .../System/Tools/CertificateInfoHelper.cs | 3 + EonaCat.Network/System/Tools/FileReader.cs | 6 +- .../System/Web/EonaCatWebserver.cs | 1 + LICENSE | 344 +- 149 files changed, 7164 insertions(+), 20948 deletions(-) delete mode 100644 EonaCat.Network/System/BlockChain/Block.cs delete mode 100644 EonaCat.Network/System/BlockChain/BlockChain.cs delete mode 100644 EonaCat.Network/System/BlockChain/ClientServerExample.cs delete mode 100644 EonaCat.Network/System/BlockChain/P2PClient.cs delete mode 100644 EonaCat.Network/System/BlockChain/P2PServer.cs delete mode 100644 EonaCat.Network/System/BlockChain/Transaction.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/ByteOrder.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/CloseStatusCode.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/CompressionMethod.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationBase.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationChallenge.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationResponse.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationSchemes.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Authentication/NetworkCredential.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Chunks/Chunk.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Chunks/ChunkStream.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Chunks/ChunkedRequestStream.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Collections/QueryStringCollection.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Collections/WebHeaderCollection.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Context/HttpListenerWSContext.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Context/TcpListenerWSContext.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Context/WSContext.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Cookies/Cookie.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Cookies/CookieCollection.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Cookies/CookieException.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Endpoints/EndPointListener.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Endpoints/EndPointManager.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpBasicIdentity.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpConnection.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpDigestIdentity.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpHeaderInfo.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpHeaderType.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpListener.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerAsyncResult.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerContext.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerException.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerPrefix.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerPrefixCollection.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerRequest.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerResponse.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpRequestHeader.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpResponseHeader.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpStatusCode.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpStreamAsyncResult.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpUtility.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpVersion.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/InputChunkState.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/InputState.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/LineState.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Logger.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/ReadBufferState.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/SSLConfig/SSLConfigClient.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/SSLConfig/SSLConfigServer.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Stream/RequestStream.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Core/Stream/ResponseStream.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Endpoints/WelcomeEndpoint.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/EventArgs/CloseEventArgs.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/EventArgs/ErrorEventArgs.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/EventArgs/MessageEventArgs.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Extensions.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/FinalFrame.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Mask.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/OperationCode.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Payload.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/ReservedBits.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Server/HttpRequestEventArgs.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Server/HttpServer.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Server/IWSSession.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Server/ServerState.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Server/WSEndpoint.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Server/WSEndpointHost.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Server/WSEndpointManager.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Server/WSServer.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Server/WSSessionManager.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/Server/WebSocketEndpointHost.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/WSClient.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/WSException.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/WSFrame.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/WSState.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/WebBase.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/WebRequest.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/WebResponse.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Buffer/BufferValidator.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Buffer/ISegmentBufferManager.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Buffer/SegmentBufferDeflector.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Buffer/SegmentBufferManager.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Buffer/UnableToAllocateBufferException.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Buffer/UnableToCreateMemoryException .cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Client/AsyncWebSocketClient.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Client/AsyncWebSocketClientConfiguration.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Client/IAsyncWebSocketClientMessageDispatcher.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Client/InternalAsyncWebSocketClientMessageDispatcherImplementation.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Client/WebSocketClientHandshaker.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Exceptions/WebSocketException.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Exceptions/WebSocketHandshakeException.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Extensions/IWebSocketExtension.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Extensions/IWebSocketExtensionNegotiator.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/AbsentableValueParameter.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/AgreedExtensionParameter.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/AgreedSingleParameter.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/AgreedValuableParameter.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/ExtensionParameter.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/ExtensionParameterType.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/SingleParameter.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/ValuableParameter.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Extensions/PerMessageExtensions/PMCE/DeflateCompression.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Extensions/PerMessageExtensions/PMCE/PerMessageCompressionExtension.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Extensions/PerMessageExtensions/PMCE/PerMessageCompressionExtensionNegotiator.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Extensions/PerMessageExtensions/PMCE/PerMessageCompressionExtensionParameters.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Extensions/WebSocketExtensionOfferDescription.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Framing/BinaryFragmentationFrame.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Framing/BinaryFrame.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Framing/Builder/IFrameBuilder.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Framing/Builder/WebSocketFrameBuilder.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Framing/CloseFrame.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Framing/ControlFrame.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Framing/DataFrame.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Framing/Frame.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Framing/Header/Header.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Framing/OpCode.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Framing/PingFrame.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Framing/PongFrame.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Framing/TextFrame.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Helpers/Consts.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Helpers/HttpKnownHeaderNames.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Helpers/KeepAliveTracker.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Helpers/StringBuilderExtensions.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Helpers/TplExtensions.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Helpers/WebSocketHelpers.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Server/AsyncWebSocketServer.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Server/AsyncWebSocketServerConfiguration.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Server/AsyncWebSocketSession.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Server/Module/AsyncWebSocketRouteResolver.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Server/Module/AsyncWebSocketServerModule.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Server/Module/AsyncWebSocketServerModuleCatalog.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Server/Module/IAsyncWebSocketServerMessageDispatcher.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/Server/WebSocketServerHandshaker.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/SubProtocols/IWebSocketSubProtocol.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/SubProtocols/IWebSocketSubProtocolNegotiator.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/SubProtocols/WebSocketSubProtocolRequestDescription.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/WebSocketCloseCode.cs create mode 100644 EonaCat.Network/System/Sockets/WebSockets/WebSocketState.cs diff --git a/EonaCat.Network/EonaCat.Network.csproj b/EonaCat.Network/EonaCat.Network.csproj index d9a3590..97a170b 100644 --- a/EonaCat.Network/EonaCat.Network.csproj +++ b/EonaCat.Network/EonaCat.Network.csproj @@ -1,28 +1,27 @@  - - Latest - - netstandard2.1; - net6.0; - net7.0; - - true - EonaCat.Network - EonaCat (Jeroen Saey) - EonaCat (Jeroen Saey) - EonaCat.Network - EonaCat (Jeroen Saey) - https://www.nuget.org/packages/EonaCat.Network/ - EonaCat, Network, .NET Standard, EonaCatHelpers, Jeroen, Saey, Protocol, Quic, UDP, TCP, Web, Server - - EonaCat Networking library with Quic, TCP, UDP, WebSockets and a Webserver - 1.1.4 - 1.1.4 - 1.1.4 - icon.png - + Latest + net48;netstandard2.1;net6.0;net7.0;net8.0 + true + EonaCat.Network + EonaCat (Jeroen Saey) + EonaCat (Jeroen Saey) + EonaCat.Network + EonaCat (Jeroen Saey) + https://www.nuget.org/packages/EonaCat.Network/ + EonaCat, Network, .NET Standard, EonaCatHelpers, Jeroen, Saey, Protocol, Quic, UDP, TCP, Web, Server + + EonaCat Networking library with Quic, TCP, UDP, WebSockets and a Webserver + 1.1.5 + 1.1.5 + 1.1.5 + icon.png + README.md + LICENSE + + + false True README.md @@ -30,29 +29,36 @@ EonaCat.Network True LICENSE + - - - - True - \ - - - True - \ - - - True - \ - + + + + True + \ + + + True + \ + + + True + \ + - - - - - - - - - + + + + + + + + + + + + + + + diff --git a/EonaCat.Network/System/BlockChain/Block.cs b/EonaCat.Network/System/BlockChain/Block.cs deleted file mode 100644 index dc93b1d..0000000 --- a/EonaCat.Network/System/BlockChain/Block.cs +++ /dev/null @@ -1,54 +0,0 @@ -using EonaCat.Json; -using System; -using System.Collections.Generic; -using System.Security.Cryptography; -using System.Text; - -namespace EonaCat.BlockChain -{ - public class Block - { - public int Index { get; set; } - private DateTime TimeStamp { get; set; } - public string PreviousHash { get; set; } - public string Hash { get; set; } - public IList Transactions { get; set; } - private int Nonce { get; set; } - - public Block(DateTime timeStamp, string previousHash, IList transactions) - { - Index = 0; - TimeStamp = timeStamp; - PreviousHash = previousHash; - Transactions = transactions; - } - - /// - /// Calculate the hash of the block - /// - /// - public string CalculateHash() - { - var sha256 = SHA256.Create(); - - var inputBytes = Encoding.ASCII.GetBytes($"{TimeStamp}-{PreviousHash ?? ""}-{JsonHelper.ToJson(Transactions)}-{Nonce}"); - var outputBytes = sha256.ComputeHash(inputBytes); - - return Convert.ToBase64String(outputBytes); - } - - /// - /// Mine the block - /// - /// - public void Mine(int difficulty) - { - var leadingZeros = new string('0', difficulty); - while (Hash == null || Hash.Substring(0, difficulty) != leadingZeros) - { - Nonce++; - Hash = CalculateHash(); - } - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/BlockChain/BlockChain.cs b/EonaCat.Network/System/BlockChain/BlockChain.cs deleted file mode 100644 index e1c3779..0000000 --- a/EonaCat.Network/System/BlockChain/BlockChain.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace EonaCat.BlockChain -{ - public class BlockChain - { - public IList PendingTransactions = new List(); - public IList Chain { set; get; } - private int Difficulty { set; get; } = 2; - private readonly int _reward = 1; //1 cryptocurrency - - public void InitializeChain() - { - Chain = new List(); - AddGenesisBlock(); - } - - private Block CreateGenesisBlock() - { - Block block = new Block(DateTime.Now, null, PendingTransactions); - block.Mine(Difficulty); - PendingTransactions = new List(); - return block; - } - - private void AddGenesisBlock() - { - Chain.Add(CreateGenesisBlock()); - } - - private Block GetLatestBlock() - { - return Chain[Chain.Count - 1]; - } - - public void CreateTransaction(Transaction transaction) - { - PendingTransactions.Add(transaction); - } - - public void ProcessPendingTransactions(string minerAddress) - { - Block block = new Block(DateTime.Now, GetLatestBlock().Hash, PendingTransactions); - AddBlock(block); - - PendingTransactions = new List(); - CreateTransaction(new Transaction(null, minerAddress, _reward)); - } - - private void AddBlock(Block block) - { - Block latestBlock = GetLatestBlock(); - block.Index = latestBlock.Index + 1; - block.PreviousHash = latestBlock.Hash; - //block.Hash = block.CalculateHash(); - block.Mine(Difficulty); - Chain.Add(block); - } - - public bool IsValid() - { - for (int i = 1; i < Chain.Count; i++) - { - Block currentBlock = Chain[i]; - Block previousBlock = Chain[i - 1]; - - if (currentBlock.Hash != currentBlock.CalculateHash()) - { - return false; - } - - if (currentBlock.PreviousHash != previousBlock.Hash) - { - return false; - } - } - return true; - } - - public int GetBalance(string address) - { - int balance = 0; - - for (int i = 0; i < Chain.Count; i++) - { - for (int j = 0; j < Chain[i].Transactions.Count; j++) - { - var transaction = Chain[i].Transactions[j]; - - if (transaction.FromAddress == address) - { - balance -= transaction.Amount; - } - - if (transaction.ToAddress == address) - { - balance += transaction.Amount; - } - } - } - - return balance; - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/BlockChain/ClientServerExample.cs b/EonaCat.Network/System/BlockChain/ClientServerExample.cs deleted file mode 100644 index 1dacfad..0000000 --- a/EonaCat.Network/System/BlockChain/ClientServerExample.cs +++ /dev/null @@ -1,75 +0,0 @@ -using EonaCat.Json; -using System; - -namespace EonaCat.BlockChain -{ - public static class ClientServerExample - { - public static int ServerPort; - private static P2PServer _server; - private static readonly P2PClient Client = new P2PClient(); - public static BlockChain BlockChain = new BlockChain(); - private static string _clientUsername = "Unknown"; - - public static void Run(int serverPort, string clientUsername) - { - BlockChain.InitializeChain(); - - ServerPort = serverPort; - _clientUsername = clientUsername; - - if (ServerPort > 0) - { - _server = new P2PServer(); - _server.Start(); - } - - if (_clientUsername != "Unknown") - { - Console.WriteLine($"Current user is {_clientUsername}"); - } - - Console.WriteLine("========================="); - Console.WriteLine("EonaCat BlockChain"); - Console.WriteLine("1. Connect to a server"); - Console.WriteLine("2. Add a transaction"); - Console.WriteLine("3. Display BlockChain"); - Console.WriteLine("4. Exit"); - Console.WriteLine("========================="); - - int selection = 0; - while (selection != 4) - { - switch (selection) - { - case 1: - Console.WriteLine("Please enter the server URL"); - string serverUrl = Console.ReadLine(); - Client.Connect($"{serverUrl}/BlockChain"); - break; - - case 2: - Console.WriteLine("Please enter the receiver name"); - string receiverName = Console.ReadLine(); - Console.WriteLine("Please enter the amount"); - string amount = Console.ReadLine(); - BlockChain.CreateTransaction(new Transaction(_clientUsername, receiverName, int.Parse(amount))); - BlockChain.ProcessPendingTransactions(_clientUsername); - Client.Broadcast(JsonHelper.ToJson(BlockChain)); - break; - - case 3: - Console.WriteLine("BlockChain"); - Console.WriteLine(JsonHelper.ToJson(BlockChain, Formatting.Indented)); - break; - } - - Console.WriteLine("Please select an action"); - string action = Console.ReadLine(); - selection = int.Parse(action); - } - - Client.Close(); - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/BlockChain/P2PClient.cs b/EonaCat.Network/System/BlockChain/P2PClient.cs deleted file mode 100644 index 3a4ba3a..0000000 --- a/EonaCat.Network/System/BlockChain/P2PClient.cs +++ /dev/null @@ -1,83 +0,0 @@ -using EonaCat.Json; -using EonaCat.Network; -using System; -using System.Collections.Generic; - -namespace EonaCat.BlockChain -{ - public class P2PClient - { - private readonly IDictionary wsDict = new Dictionary(); - - public void Connect(string url) - { - if (!wsDict.ContainsKey(url)) - { - WSClient webSocket = new WSClient(url); - webSocket.OnMessageReceived += (sender, e) => - { - if (e.Data == "Hi Client") - { - Console.WriteLine(e.Data); - } - else - { - var newChain = JsonHelper.ToObject(e.Data); - if (!newChain.IsValid() || newChain.Chain.Count <= ClientServerExample.BlockChain.Chain.Count) - { - return; - } - - var newTransactions = new List(); - newTransactions.AddRange(newChain.PendingTransactions); - newTransactions.AddRange(ClientServerExample.BlockChain.PendingTransactions); - - newChain.PendingTransactions = newTransactions; - ClientServerExample.BlockChain = newChain; - } - }; - webSocket.Connect(); - webSocket.Send("Hi Server"); - webSocket.Send(JsonHelper.ToJson(ClientServerExample.BlockChain)); - wsDict.Add(url, webSocket); - } - } - - public void Send(string url, string data) - { - foreach (var item in wsDict) - { - if (item.Key == url) - { - item.Value.Send(data); - } - } - } - - public void Broadcast(string data) - { - foreach (var item in wsDict) - { - item.Value.Send(data); - } - } - - public IList GetServers() - { - IList servers = new List(); - foreach (var item in wsDict) - { - servers.Add(item.Key); - } - return servers; - } - - public void Close() - { - foreach (var item in wsDict) - { - item.Value.Close(); - } - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/BlockChain/P2PServer.cs b/EonaCat.Network/System/BlockChain/P2PServer.cs deleted file mode 100644 index 824e79d..0000000 --- a/EonaCat.Network/System/BlockChain/P2PServer.cs +++ /dev/null @@ -1,51 +0,0 @@ -using EonaCat.Json; -using EonaCat.Network; -using System; -using System.Collections.Generic; - -namespace EonaCat.BlockChain -{ - public class P2PServer : WSEndpoint - { - private bool _chainSynched; - private WSServer _wss; - - public void Start(string webSocketAddress = null) - { - webSocketAddress ??= $"ws://127.0.0.1:{ClientServerExample.ServerPort}"; - _wss = new WSServer(webSocketAddress); - _wss.AddEndpoint("/BlockChain"); - _wss.Start(); - Console.WriteLine($"Started server at {webSocketAddress}"); - } - - protected override void OnMessage(MessageEventArgs e) - { - if (e.Data == "Hi Server") - { - Console.WriteLine(e.Data); - Send("Hi Client"); - } - else - { - BlockChain newChain = JsonHelper.ToObject(e.Data); - - if (newChain.IsValid() && newChain.Chain.Count > ClientServerExample.BlockChain.Chain.Count) - { - List newTransactions = new List(); - newTransactions.AddRange(newChain.PendingTransactions); - newTransactions.AddRange(ClientServerExample.BlockChain.PendingTransactions); - - newChain.PendingTransactions = newTransactions; - ClientServerExample.BlockChain = newChain; - } - - if (!_chainSynched) - { - Send(JsonHelper.ToJson(ClientServerExample.BlockChain)); - _chainSynched = true; - } - } - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/BlockChain/Transaction.cs b/EonaCat.Network/System/BlockChain/Transaction.cs deleted file mode 100644 index a34fa96..0000000 --- a/EonaCat.Network/System/BlockChain/Transaction.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace EonaCat.BlockChain -{ - public class Transaction - { - public string FromAddress { get; set; } - public string ToAddress { get; set; } - public int Amount { get; set; } - - public Transaction(string fromAddress, string toAddress, int amount) - { - FromAddress = fromAddress; - ToAddress = toAddress; - Amount = amount; - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/RemoteInfo.cs b/EonaCat.Network/System/Sockets/RemoteInfo.cs index 26508b7..6780127 100644 --- a/EonaCat.Network/System/Sockets/RemoteInfo.cs +++ b/EonaCat.Network/System/Sockets/RemoteInfo.cs @@ -1,11 +1,11 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System.Net; +using System.Net; using System.Net.Sockets; namespace EonaCat.Network { + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + public class RemoteInfo { public bool IsTcp { get; set; } diff --git a/EonaCat.Network/System/Sockets/Tcp/SocketTcpServer.cs b/EonaCat.Network/System/Sockets/Tcp/SocketTcpServer.cs index 75297b1..767cda4 100644 --- a/EonaCat.Network/System/Sockets/Tcp/SocketTcpServer.cs +++ b/EonaCat.Network/System/Sockets/Tcp/SocketTcpServer.cs @@ -104,7 +104,7 @@ namespace EonaCat.Network { try { - int received = await socket.ReceiveAsync(new Memory(buffer), SocketFlags.None, cancellationToken).ConfigureAwait(false); + int received = await ReceiveAsync(socket, buffer, cancellationToken).ConfigureAwait(false); if (received > 0) { byte[] data = new byte[received]; @@ -139,6 +139,13 @@ namespace EonaCat.Network }); } + private Task ReceiveAsync(Socket socket, byte[] buffer, CancellationToken cancellationToken) + { + return Task.Factory.FromAsync(socket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, null, socket), + socket.EndReceive); + } + + /// /// Send data to socket /// diff --git a/EonaCat.Network/System/Sockets/Web/ByteOrder.cs b/EonaCat.Network/System/Sockets/Web/ByteOrder.cs deleted file mode 100644 index 5e784d6..0000000 --- a/EonaCat.Network/System/Sockets/Web/ByteOrder.cs +++ /dev/null @@ -1,12 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -namespace EonaCat.Network -{ - public enum ByteOrder - { - Little, - - Big - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/CloseStatusCode.cs b/EonaCat.Network/System/Sockets/Web/CloseStatusCode.cs deleted file mode 100644 index 9763359..0000000 --- a/EonaCat.Network/System/Sockets/Web/CloseStatusCode.cs +++ /dev/null @@ -1,85 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -namespace EonaCat.Network -{ - public enum CloseStatusCode : ushort - { - //1000: Normal Closure - //Indicates a normal closure, meaning that the purpose for which the connection was established has been fulfilled. - - //1001: Going Away - //The endpoint is going away, such as a server going down or a browser tab is closing. - - //1002: Protocol Error - //Indicates that the endpoint is terminating the connection due to a protocol error. - - //1003: Unsupported Data - //Indicates that the endpoint is terminating the connection because it received data that it cannot process. - - //1005: No Status Received - //Reserved. It indicates that no status code was received. - - //1006: Abnormal Closure - //Reserved. It is a reserved value and should not be set as a status code in a Close control frame by an endpoint. - - //1007: Invalid frame payload data - //Indicates that an endpoint is terminating the connection because it has received data within a message that was not consistent with the type of the message. - - //1008: Policy Violation - //Indicates that an endpoint is terminating the connection because it has received a message that violates its policy. - - //1009: Message Too Big - //Indicates that an endpoint is terminating the connection because it has received a message that is too big for it to process. - - //1010: Missing Extension - //Indicates that an endpoint is terminating the connection because it has received a message that requires negotiation of an extension, and the client did not offer that extension. - - //1011: Internal Error - //Indicates that a server is terminating the connection because it encountered an unexpected condition that prevented it from fulfilling the request. - - //1012: Service Restart - //Reserved. It indicates that the service is restarting. - - //1013: Try Again Later - //Reserved. It indicates that the server is temporarily unable to process the request. - - //1014: Bad Gateway - //Reserved. It indicates that an intermediate server failed to fulfill a request. - - //1015: TLS Handshake Failure - //Reserved. It indicates that the connection was closed due to a failure to perform a TLS handshake. - - Normal = 1000, - - Away = 1001, - - ProtocolError = 1002, - - UnsupportedData = 1003, - - Undefined = 1004, - - NoStatus = 1005, - - Abnormal = 1006, - - InvalidData = 1007, - - PolicyViolation = 1008, - - TooBig = 1009, - - MissingExtension = 1010, - - ServerError = 1011, - - ServiceRestart = 1012, - - TryAgainLater = 1013, - - BadGateway = 1014, - - TlsHandshakeFailure = 1015 - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/CompressionMethod.cs b/EonaCat.Network/System/Sockets/Web/CompressionMethod.cs deleted file mode 100644 index ada0444..0000000 --- a/EonaCat.Network/System/Sockets/Web/CompressionMethod.cs +++ /dev/null @@ -1,12 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -namespace EonaCat.Network -{ - public enum CompressionMethod : byte - { - None, - - Deflate - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationBase.cs b/EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationBase.cs deleted file mode 100644 index 35c4d75..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationBase.cs +++ /dev/null @@ -1,129 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.Collections.Specialized; -using System.Text; - -namespace EonaCat.Network -{ - /// - /// Represents the base class for authentication in the EonaCat network library. - /// - internal abstract class AuthenticationBase - { - /// - /// Gets or sets the collection of authentication parameters. - /// - internal NameValueCollection Parameters; - - /// - /// Initializes a new instance of the class. - /// - /// The authentication scheme. - /// The collection of authentication parameters. - protected AuthenticationBase(AuthenticationSchemes scheme, NameValueCollection parameters) - { - Scheme = scheme; - Parameters = parameters; - } - - /// - /// Gets the algorithm used for authentication. - /// - public string Algorithm => Parameters["algorithm"]; - - /// - /// Gets the nonce value for authentication. - /// - public string Nonce => Parameters["nonce"]; - - /// - /// Gets the opaque value for authentication. - /// - public string Opaque => Parameters["opaque"]; - - /// - /// Gets the quality of protection for authentication. - /// - public string Qop => Parameters["qop"]; - - /// - /// Gets the realm for authentication. - /// - public string Realm => Parameters["realm"]; - - /// - /// Gets the authentication scheme. - /// - public AuthenticationSchemes Scheme { get; } - - /// - /// Creates a nonce value for authentication. - /// - /// A string representing the generated nonce value. - internal static string CreateNonceValue() - { - var buffer = new byte[16]; - var random = new Random(); - random.NextBytes(buffer); - - var result = new StringBuilder(32); - foreach (var currentByte in buffer) - { - result.Append(currentByte.ToString("x2")); - } - - return result.ToString(); - } - - /// - /// Parses the authentication parameters from the specified string value. - /// - /// The string containing authentication parameters. - /// A collection of authentication parameters. - internal static NameValueCollection ParseParameters(string value) - { - var result = 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; - - result.Add(name, val); - } - - return result; - } - - /// - /// Gets the authentication string for Basic authentication. - /// - /// A string representing the Basic authentication. - internal abstract string ToBasicString(); - - /// - /// Gets the authentication string for Digest authentication. - /// - /// A string representing the Digest authentication. - internal abstract string ToDigestString(); - - /// - /// Returns a string representation of the authentication information. - /// - /// A string representation of the authentication information. - public override string ToString() - { - return Scheme == AuthenticationSchemes.Basic - ? ToBasicString() - : Scheme == AuthenticationSchemes.Digest - ? ToDigestString() - : string.Empty; - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationChallenge.cs b/EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationChallenge.cs deleted file mode 100644 index ad9eaf9..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationChallenge.cs +++ /dev/null @@ -1,136 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System.Collections.Specialized; -using System.Text; - -namespace EonaCat.Network -{ - /// - /// Represents an authentication challenge in the EonaCat network library. - /// - internal class AuthenticationChallenge : AuthenticationBase - { - private const string BASIC = "basic"; - private const string DIGEST = "digest"; - private const int DIGEST_SIZE = 128; - - /// - /// Initializes a new instance of the class for Basic or Digest authentication. - /// - /// The authentication scheme. - /// The authentication realm. - 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"; - } - } - - /// - /// Initializes a new instance of the class. - /// - /// The authentication scheme. - /// The collection of authentication parameters. - private AuthenticationChallenge(AuthenticationSchemes scheme, NameValueCollection parameters) - : base(scheme, parameters) - { - } - - /// - /// Gets the domain for Digest authentication. - /// - public string Domain => Parameters["domain"]; - - /// - /// Gets the stale parameter for Digest authentication. - /// - public string Stale => Parameters["stale"]; - - /// - /// Creates a Basic authentication challenge with the specified realm. - /// - /// The authentication realm. - /// An instance of for Basic authentication. - - internal static AuthenticationChallenge CreateBasicChallenge(string realm) - { - return new AuthenticationChallenge(AuthenticationSchemes.Basic, realm); - } - - /// - /// Creates a Digest authentication challenge with the specified realm. - /// - /// The authentication realm. - /// An instance of for Digest authentication. - internal static AuthenticationChallenge CreateDigestChallenge(string realm) - { - return new AuthenticationChallenge(AuthenticationSchemes.Digest, realm); - } - - /// - /// Parses an authentication challenge from the specified string value. - /// - /// The string containing the authentication challenge. - /// An instance of if successful; otherwise, null. - internal static AuthenticationChallenge Parse(string value) - { - var challenge = value.Split(new[] { ' ' }, 2); - if (challenge.Length != 2) - { - return null; - } - - var scheme = challenge[0].ToLower(); - return scheme == BASIC - ? new AuthenticationChallenge( - AuthenticationSchemes.Basic, ParseParameters(challenge[1])) - : scheme == DIGEST - ? new AuthenticationChallenge( - AuthenticationSchemes.Digest, ParseParameters(challenge[1])) - : null; - } - - /// - /// Gets the Basic authentication string representation of the authentication challenge. - /// - /// A string representing the Basic authentication challenge. - internal override string ToBasicString() - { - return string.Format($"Basic realm=\"{{0}}\"", Parameters["realm"]); - } - - internal override string ToDigestString() - { - var output = new StringBuilder(DIGEST_SIZE); - var realm = Parameters["realm"]; - var nonce = Parameters["nonce"]; - - if (realm != null) - { - output.AppendFormat("Digest realm=\"{0}\", nonce=\"{1}\"", realm, nonce); - - AppendIfNotNull(output, "domain", Parameters["domain"]); - AppendIfNotNull(output, "opaque", Parameters["opaque"]); - AppendIfNotNull(output, "stale", Parameters["stale"]); - AppendIfNotNull(output, "algorithm", Parameters["algorithm"]); - AppendIfNotNull(output, "qop", Parameters["qop"]); - } - - return output.ToString(); - } - - private static void AppendIfNotNull(StringBuilder output, string key, string value) - { - if (value != null) - { - output.AppendFormat(", {0}=\"{1}\"", key, value); - } - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationResponse.cs b/EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationResponse.cs deleted file mode 100644 index 64ccdf0..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationResponse.cs +++ /dev/null @@ -1,359 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.Collections.Specialized; -using System.Security.Cryptography; -using System.Security.Principal; -using System.Text; - -namespace EonaCat.Network -{ - /// - /// Represents an authentication response in the EonaCat network library. - /// - internal class AuthenticationResponse : AuthenticationBase - { - private const string BASIC = "basic"; - private const string DIGEST = "digest"; - private uint _nonceCount; - - /// - /// Initializes a new instance of the class for Basic authentication. - /// - /// The network credentials. - internal AuthenticationResponse(NetworkCredential credentials) - : this(AuthenticationSchemes.Basic, new NameValueCollection(), credentials, 0) - { - } - - /// - /// Initializes a new instance of the class for Digest authentication. - /// - /// The authentication challenge. - /// The network credentials. - /// The nonce count. - internal AuthenticationResponse( - AuthenticationChallenge challenge, NetworkCredential credentials, uint nonceCount) - : this(challenge.Scheme, challenge.Parameters, credentials, nonceCount) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The authentication scheme. - /// The collection of authentication parameters. - /// The network credentials. - /// The nonce count. - 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(); - } - } - - /// - /// Initializes a new instance of the class. - /// - /// The authentication scheme. - /// The collection of authentication parameters. - private AuthenticationResponse(AuthenticationSchemes scheme, NameValueCollection parameters) - : base(scheme, parameters) - { - } - - /// - /// Gets the cnonce value for Digest authentication. - /// - public string Cnonce => Parameters["cnonce"]; - - /// - /// Gets the nonce count for Digest authentication. - /// - public string Nc => Parameters["nc"]; - - /// - /// Gets the password for authentication. - /// - public string Password => Parameters["password"]; - - /// - /// Gets the response value for authentication. - /// - public string Response => Parameters["response"]; - - /// - /// Gets the URI for authentication. - /// - public string Uri => Parameters["uri"]; - - /// - /// Gets the username for authentication. - /// - public string UserName => Parameters["username"]; - - /// - /// Gets the nonce count. - /// - internal uint NonceCount => _nonceCount < uint.MaxValue - ? _nonceCount - : 0; - - /// - /// Converts the authentication response to an identity. - /// - /// An instance of . - public IIdentity ToIdentity() - { - var scheme = Scheme; - return scheme == AuthenticationSchemes.Basic - ? new HttpBasicIdentity(Parameters["username"], Parameters["password"]) - : scheme == AuthenticationSchemes.Digest - ? new HttpDigestIdentity(Parameters) - : null; - } - - /// - /// Creates the request digest for Digest authentication. - /// - /// The authentication parameters. - /// The request digest. - 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)); - } - - /// - /// Parses an authentication response from the specified string value. - /// - /// The string containing the authentication response. - /// An instance of if successful; otherwise, null. - internal static AuthenticationResponse Parse(string value) - { - try - { - var cred = value.Split(new[] { ' ' }, 2); - if (cred.Length != 2) - { - return null; - } - - var scheme = cred[0].ToLower(); - return scheme == BASIC - ? new AuthenticationResponse( - AuthenticationSchemes.Basic, ParseBasicCredentials(cred[1])) - : scheme == DIGEST - ? new AuthenticationResponse( - AuthenticationSchemes.Digest, ParseParameters(cred[1])) - : null; - } - catch - { - } - - return null; - } - - /// - /// Parses the basic credentials from the specified string value. - /// - /// The string containing basic credentials. - /// A collection of basic credentials. - 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 - { - ["username"] = user, - ["password"] = pass - }; - - return res; - } - - /// - /// Gets the Basic authentication string representation of the authentication response. - /// - /// A string representing the Basic authentication response. - 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; - } - - /// - /// Gets the Digest authentication string representation of the authentication response. - /// - /// A string representing the Digest authentication response. - internal override string ToDigestString() - { - var output = new StringBuilder(256); - output.AppendFormat($"Digest username=\"{Parameters["username"]}\", realm=\"{Parameters["realm"]}\", nonce=\"{Parameters["nonce"]}\", uri=\"{Parameters["uri"]}\", response=\"{Parameters["response"]}\""); - - var opaque = Parameters["opaque"]; - if (opaque != null) - { - output.AppendFormat($", opaque=\"{opaque}\""); - } - - var algorithm = Parameters["algorithm"]; - if (algorithm != null) - { - output.AppendFormat($", algorithm={algorithm}"); - } - - var qop = Parameters["qop"]; - if (qop != null) - { - output.AppendFormat($", qop={qop}, cnonce=\"{Parameters["cnonce"]}\", nc={Parameters["nc"]}"); - } - - return output.ToString(); - } - - /// - /// Creates the A1 value for Digest authentication. - /// - /// The username. - /// The password. - /// The realm. - /// The A1 value. - private static string CreateA1(string username, string password, string realm) - { - return $"{username}:{realm}:{password}"; - } - - /// - /// Creates the A1 value for Digest authentication with cnonce and nonce. - /// - /// The username. - /// The password. - /// The realm. - /// The nonce. - /// The cnonce. - /// The A1 value. - private static string CreateA1( - string username, string password, string realm, string nonce, string cnonce) - { - return $"{Hash(CreateA1(username, password, realm))}:{nonce}:{cnonce}"; - } - - /// - /// Creates the A2 value for Digest authentication. - /// - /// The HTTP method. - /// The URI. - /// The A2 value. - private static string CreateA2(string method, string uri) - { - return $"{method}:{uri}"; - } - - /// - /// Creates the A2 value for Digest authentication with an entity. - /// - /// The HTTP method. - /// The URI. - /// The entity. - /// The A2 value. - private static string CreateA2(string method, string uri, string entity) - { - return $"{method}:{uri}:{Hash(entity)}"; - } - - /// - /// Computes the MD5 hash of the given value. - /// - /// The value to hash. - /// The MD5 hash. - private static string Hash(string value) - { - var source = Encoding.UTF8.GetBytes(value); - var md5 = MD5.Create(); - var hashed = md5.ComputeHash(source); - - var result = new StringBuilder(64); - foreach (var currentByte in hashed) - { - result.Append(currentByte.ToString("x2")); - } - - return result.ToString(); - } - - /// - /// Initializes the authentication as Digest. - /// - 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); - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationSchemes.cs b/EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationSchemes.cs deleted file mode 100644 index be5cbc0..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationSchemes.cs +++ /dev/null @@ -1,31 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -namespace EonaCat.Network -{ - /// - /// Enumerates the possible authentication schemes in the EonaCat network library. - /// - public enum AuthenticationSchemes - { - /// - /// No authentication scheme. - /// - None, - - /// - /// Digest authentication scheme. - /// - Digest = 1, - - /// - /// Basic authentication scheme. - /// - Basic = 8, - - /// - /// Anonymous authentication scheme. - /// - Anonymous = 0x8000 - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Authentication/NetworkCredential.cs b/EonaCat.Network/System/Sockets/Web/Core/Authentication/NetworkCredential.cs deleted file mode 100644 index ce6e467..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Authentication/NetworkCredential.cs +++ /dev/null @@ -1,113 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; - -namespace EonaCat.Network -{ - /// - /// Represents network credentials used for authentication in the EonaCat network library. - /// - public class NetworkCredential - { - private static readonly string[] _noRoles; - private string _domain; - private string _password; - private string[] _roles; - - static NetworkCredential() - { - _noRoles = new string[0]; - } - - /// - /// Initializes a new instance of the class with the specified username and password. - /// - /// The username. - /// The password. - public NetworkCredential(string username, string password) - : this(username, password, null, null) - { - } - - /// - /// Initializes a new instance of the class with the specified username, password, domain, and roles. - /// - /// The username. - /// The password. - /// The domain. - /// The roles. - public NetworkCredential( - string username, string password, string domain, params string[] roles - ) - { - if (username == null) - { - throw new ArgumentNullException(nameof(username)); - } - - if (username.Length == 0) - { - throw new ArgumentException("An empty string.", nameof(username)); - } - - Username = username; - _password = password; - _domain = domain; - _roles = roles; - } - - /// - /// Gets or sets the domain associated with the network credentials. - /// - public string Domain - { - get - { - return _domain ?? string.Empty; - } - - internal set - { - _domain = value; - } - } - - /// - /// Gets or sets the password associated with the network credentials. - /// - public string Password - { - get - { - return _password ?? string.Empty; - } - - internal set - { - _password = value; - } - } - - /// - /// Gets or sets the roles associated with the network credentials. - /// - public string[] Roles - { - get - { - return _roles ?? _noRoles; - } - - internal set - { - _roles = value; - } - } - - /// - /// Gets the username associated with the network credentials. - /// - public string Username { get; internal set; } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Chunks/Chunk.cs b/EonaCat.Network/System/Sockets/Web/Core/Chunks/Chunk.cs deleted file mode 100644 index 3942cfb..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Chunks/Chunk.cs +++ /dev/null @@ -1,56 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; - -namespace EonaCat.Network -{ - /// - /// Represents a chunk of data in the EonaCat network library. - /// - internal class WebChunk - { - private readonly byte[] _data; - private int _offset; - - /// - /// Initializes a new instance of the class with the specified data. - /// - /// The byte array representing the data. - public WebChunk(byte[] data) - { - _data = data; - } - - /// - /// Gets the remaining bytes to read in the chunk. - /// - public int ReadLeft => _data.Length - _offset; - - /// - /// Reads a specified number of bytes from the chunk into a buffer. - /// - /// The destination buffer. - /// The zero-based byte offset in the buffer at which to begin storing the data. - /// The maximum number of bytes to read. - /// The actual number of bytes read. - 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; - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Chunks/ChunkStream.cs b/EonaCat.Network/System/Sockets/Web/Core/Chunks/ChunkStream.cs deleted file mode 100644 index 0ba330c..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Chunks/ChunkStream.cs +++ /dev/null @@ -1,398 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Net; -using System.Text; - -namespace EonaCat.Network -{ - /// - /// Represents a stream for handling chunked data in the EonaCat network library. - /// - internal class ChunkStream - { - private readonly List _chunks; - private readonly StringBuilder _saved; - private int _chunkRead; - private int _chunkSize; - private bool _foundSPCode; - private bool _gotChunck; - private InputChunkState _state; - private int _trailerState; - - /// - /// Initializes a new instance of the class with the specified headers. - /// - /// The web headers associated with the chunk stream. - public ChunkStream(WebHeaderCollection headers) - { - Headers = headers; - _chunkSize = -1; - _chunks = new List(); - _saved = new StringBuilder(); - } - - /// - /// Initializes a new instance of the class with the specified buffer, offset, count, and headers. - /// - /// The byte array containing chunked data. - /// The offset in the buffer at which to begin reading. - /// The number of bytes to read from the buffer. - /// The web headers associated with the chunk stream. - public ChunkStream(byte[] buffer, int offset, int count, WebHeaderCollection headers) - : this(headers) - { - Write(buffer, offset, count); - } - - /// - /// Gets the number of bytes left in the current chunk. - /// - public int ChunkLeft => _chunkSize - _chunkRead; - - /// - /// Gets a value indicating whether more data is expected. - /// - public bool WantMore => _state != InputChunkState.End; - - /// - /// Gets the web headers associated with the chunk stream. - /// - internal WebHeaderCollection Headers { get; } - - /// - /// Reads a specified amount of data from the chunk stream. - /// - /// The destination buffer. - /// The zero-based byte offset in the buffer at which to begin storing the data. - /// The maximum number of bytes to read. - /// The total number of bytes read into the buffer. - public int Read(byte[] buffer, int offset, int count) - { - if (count <= 0) - { - return 0; - } - - return read(buffer, offset, count); - } - - /// - /// Writes a specified amount of data to the chunk stream. - /// - /// The byte array containing data to be written to the chunk stream. - /// The offset in the buffer at which to begin writing. - /// The number of bytes to write to the chunk stream. - public void Write(byte[] buffer, int offset, int count) - { - if (count <= 0) - { - return; - } - - Write(buffer, ref offset, offset + count); - } - - /// - /// Resets the internal buffer and state of the chunk stream. - /// - internal void ResetBuffer() - { - _chunkRead = 0; - _chunkSize = -1; - _chunks.Clear(); - } - - /// - /// Writes a specified amount of data to the chunk stream and reads it back. - /// - /// The byte array containing data to be written to the chunk stream. - /// The offset in the buffer at which to begin writing. - /// The number of bytes to write to the chunk stream. - /// The number of bytes to read back from the chunk stream. - /// The number of bytes read from the chunk stream. - internal int WriteAndReadBack(byte[] buffer, int offset, int writeCount, int readCount) - { - Write(buffer, offset, writeCount); - return Read(buffer, offset, readCount); - } - - private static string RemoveChunkExtension(string value) - { - var index = value.IndexOf(';'); - return index > -1 ? value.Substring(0, index) : value; - } - - private static void ThrowProtocolViolation(string message) - { - throw new WebException($"EonaCat Network: {message}", null, WebExceptionStatus.ServerProtocolViolation, null); - } - - 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 InputChunkState SeekCrLf(byte[] buffer, ref int offset, int length) - { - if (!_gotChunck) - { - if (buffer[offset++] != 13) - { - ThrowProtocolViolation("CR is expected."); - } - - _gotChunck = 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 currentByte = 0; - while (offset < length) - { - currentByte = buffer[offset++]; - if (_gotChunck) - { - if (currentByte != 10) - { - ThrowProtocolViolation("LF is expected."); - } - - break; - } - - if (currentByte == 13) - { - _gotChunck = true; - continue; - } - - if (currentByte == 10) - { - ThrowProtocolViolation("LF is unexpected."); - } - - if (currentByte == 32) // SP - { - _foundSPCode = true; - } - - if (!_foundSPCode) - { - _saved.Append((char)currentByte); - } - - if (_saved.Length > 20) - { - ThrowProtocolViolation("The chunk size is too long."); - } - } - - if (!_gotChunck || currentByte != 10) - { - return InputChunkState.None; - } - - _chunkRead = 0; - try - { - _chunkSize = int.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 currentByte = buffer[offset++]; - _saved.Append((char)currentByte); - if (_saved.Length > 4196) - { - ThrowProtocolViolation("The trailer is too long."); - } - - if (_trailerState == 1 || _trailerState == 3) - { - if (currentByte != 10) - { - ThrowProtocolViolation("LF is expected."); - } - - _trailerState++; - continue; - } - - if (currentByte == 13) - { - _trailerState++; - continue; - } - - if (currentByte == 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 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; - _gotChunck = false; - _foundSPCode = 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; - } - - _gotChunck = 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 WebChunk(data)); - - offset += cnt; - _chunkRead += cnt; - - return _chunkRead == _chunkSize ? InputChunkState.DataEnded : InputChunkState.Data; - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Chunks/ChunkedRequestStream.cs b/EonaCat.Network/System/Sockets/Web/Core/Chunks/ChunkedRequestStream.cs deleted file mode 100644 index d62b77d..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Chunks/ChunkedRequestStream.cs +++ /dev/null @@ -1,208 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.IO; - -namespace EonaCat.Network -{ - /// - /// Represents a stream for handling chunked requests in the EonaCat network library. - /// - internal class ChunkedRequestStream : RequestStream - { - private const int _bufferLength = 8192; - private readonly HttpListenerContext _context; - private bool _disposed; - private bool _noMoreData; - - /// - /// Initializes a new instance of the class with the specified stream, buffer, offset, count, and context. - /// - /// The underlying stream. - /// The byte array used for buffering. - /// The offset in the buffer at which to begin reading. - /// The maximum number of bytes to read. - /// The HTTP listener context. - 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); - } - - /// - /// Gets or sets the chunk stream decoder associated with the chunked request stream. - /// - internal ChunkStream Decoder { get; set; } - - /// - /// Begins an asynchronous read operation from the stream. - /// - /// The destination buffer. - /// The zero-based byte offset in the buffer at which to begin storing the data. - /// The maximum number of bytes to read. - /// An optional asynchronous callback, to be called when the read is complete. - /// A user-provided object that distinguishes this particular asynchronous read request from other requests. - /// An that represents the asynchronous read operation. - 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(nameof(buffer)); - } - - if (offset < 0) - { - throw new ArgumentOutOfRangeException(nameof(offset), "A negative value."); - } - - if (count < 0) - { - throw new ArgumentOutOfRangeException(nameof(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 result = new HttpStreamAsyncResult(callback, state); - if (_noMoreData) - { - result.Complete(); - return result; - } - - var nread = Decoder.Read(buffer, offset, count); - offset += nread; - count -= nread; - if (count == 0) - { - result.Count = nread; - result.Complete(); - - return result; - } - - if (!Decoder.WantMore) - { - _noMoreData = nread == 0; - result.Count = nread; - result.Complete(); - - return result; - } - - result.Buffer = new byte[_bufferLength]; - result.Offset = 0; - result.Count = _bufferLength; - - var readBufferState = new ReadBufferState(buffer, offset, count, result); - readBufferState.InitialCount += nread; - base.BeginRead(result.Buffer, result.Offset, result.Count, OnRead, readBufferState); - - return result; - } - - /// - /// Closes the stream. - /// - public override void Close() - { - if (_disposed) - { - return; - } - - _disposed = true; - base.Close(); - } - - /// - /// Ends an asynchronous read operation from the stream. - /// - /// The result of the asynchronous operation. - /// The number of bytes read from the stream, between zero (0) and the number of bytes you requested. Streams return zero (0) only at the end of the stream, otherwise, they should block until at least one byte is available. - public override int EndRead(IAsyncResult asyncResult) - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().ToString()); - } - - if (asyncResult == null) - { - throw new ArgumentNullException(nameof(asyncResult)); - } - - if (asyncResult is not HttpStreamAsyncResult result) - { - throw new ArgumentException("A wrong IAsyncResult.", nameof(asyncResult)); - } - - if (!result.IsCompleted) - { - result.AsyncWaitHandle.WaitOne(); - } - - if (result.HasException) - { - throw new HttpListenerException(400, "I/O operation aborted."); - } - - return result.Count; - } - - /// - /// Reads a sequence of bytes from the stream and advances the position within the stream by the number of bytes read. - /// - /// The byte array to read data into. - /// The zero-based byte offset in buffer at which to begin storing the data. - /// The maximum number of bytes to be read from the stream. - /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached. - public override int Read(byte[] buffer, int offset, int count) - { - var result = BeginRead(buffer, offset, count, null, null); - return EndRead(result); - } - - private void OnRead(IAsyncResult asyncResult) - { - var readBufferState = (ReadBufferState)asyncResult.AsyncState; - var result = readBufferState.AsyncResult; - try - { - var nread = base.EndRead(asyncResult); - Decoder.Write(result.Buffer, result.Offset, nread); - nread = Decoder.Read(readBufferState.Buffer, readBufferState.Offset, readBufferState.Count); - readBufferState.Offset += nread; - readBufferState.Count -= nread; - if (readBufferState.Count == 0 || !Decoder.WantMore || nread == 0) - { - _noMoreData = !Decoder.WantMore && nread == 0; - result.Count = readBufferState.InitialCount - readBufferState.Count; - result.Complete(); - - return; - } - - result.Offset = 0; - result.Count = Math.Min(_bufferLength, Decoder.ChunkLeft + 6); - base.BeginRead(result.Buffer, result.Offset, result.Count, OnRead, readBufferState); - } - catch (Exception ex) - { - _context.Connection.SendError(ex.Message, 400); - result.Complete(ex); - } - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Collections/QueryStringCollection.cs b/EonaCat.Network/System/Sockets/Web/Core/Collections/QueryStringCollection.cs deleted file mode 100644 index 692a5d1..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Collections/QueryStringCollection.cs +++ /dev/null @@ -1,41 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System.Collections.Specialized; -using System.Text; - -namespace EonaCat.Network -{ - /// - /// Represents a collection of query string parameters in the EonaCat network library. - /// - internal sealed class QueryStringCollection : NameValueCollection - { - /// - /// Converts the collection to its string representation. - /// - /// A string representation of the query string parameters. - public override string ToString() - { - var count = Count; - if (count == 0) - { - return string.Empty; - } - - var output = new StringBuilder(); - var keys = AllKeys; - foreach (var key in keys) - { - output.AppendFormat($"{key}={this[key]}&"); - } - - if (output.Length > 0) - { - output.Length--; - } - - return output.ToString(); - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Collections/WebHeaderCollection.cs b/EonaCat.Network/System/Sockets/Web/Core/Collections/WebHeaderCollection.cs deleted file mode 100644 index 6a872ed..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Collections/WebHeaderCollection.cs +++ /dev/null @@ -1,599 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -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 EonaCat.Network -{ - /// - /// Represents a collection of HTTP headers in the EonaCat Network library. - /// - [Serializable] - [ComVisible(true)] - public class WebHeaderCollection : NameValueCollection, ISerializable - { - private const int BUFFER_SIZE = 65535; - private static readonly Dictionary _headers; - private readonly bool _internallyUsed; - private HttpHeaderType _state; - - 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) } - }; - } - - public WebHeaderCollection() - { - } - - /// - /// Initializes a new instance of the WebHeaderCollection class with the specified parameters. - /// - /// The HTTP header type. - /// A boolean indicating whether the collection is internally used. - internal WebHeaderCollection(HttpHeaderType state, bool internallyUsed) - { - _state = state; - _internallyUsed = internallyUsed; - } - - /// - /// Initializes a new instance of the WebHeaderCollection class with serialization information. - /// - /// The SerializationInfo containing the data needed to serialize the WebHeaderCollection. - /// The StreamingContext containing the source and destination of the serialized stream associated with the WebHeaderCollection. - protected WebHeaderCollection( - SerializationInfo serializationInfo, StreamingContext streamingContext) - { - if (serializationInfo == null) - { - throw new ArgumentNullException(nameof(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, nameof(serializationInfo), ex); - } - } - - public override string[] AllKeys => base.AllKeys; - public override int Count => base.Count; - public override KeysCollection Keys => base.Keys; - internal HttpHeaderType State => _state; - - public string this[HttpRequestHeader header] - { - get - { - return Get(Convert(header)); - } - - set - { - Add(header, value); - } - } - - public string this[HttpResponseHeader header] - { - get - { - return Get(Convert(header)); - } - - set - { - Add(header, value); - } - } - - public static bool IsRestricted(string headerName) - { - return isRestricted(CheckName(headerName), false); - } - - public static bool IsRestricted(string headerName, bool response) - { - return isRestricted(CheckName(headerName), response); - } - - public void Add(string header) - { - if (header == null || header.Length == 0) - { - throw new ArgumentNullException(nameof(header)); - } - - var pos = checkColonSeparated(header); - add(header.Substring(0, pos), header.Substring(pos + 1), false); - } - - public void Add(HttpRequestHeader header, string value) - { - DoWithCheckingState(addWithoutCheckingName, Convert(header), value, false, true); - } - - public void Add(HttpResponseHeader header, string value) - { - DoWithCheckingState(addWithoutCheckingName, Convert(header), value, true, true); - } - - public override void Add(string name, string value) - { - add(name, value, false); - } - - public override void Clear() - { - base.Clear(); - _state = HttpHeaderType.Unspecified; - } - - public override string Get(int index) - { - return base.Get(index); - } - - public override string Get(string name) - { - return base.Get(name); - } - - public override IEnumerator GetEnumerator() - { - return base.GetEnumerator(); - } - - public override string GetKey(int index) - { - return base.GetKey(index); - } - - [SecurityPermission( - SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)] - public override void GetObjectData( - SerializationInfo serializationInfo, StreamingContext streamingContext) - { - if (serializationInfo == null) - { - throw new ArgumentNullException(nameof(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)); - }); - } - - [SecurityPermission( - SecurityAction.LinkDemand, - Flags = SecurityPermissionFlag.SerializationFormatter, - SerializationFormatter = true)] - void ISerializable.GetObjectData( - SerializationInfo serializationInfo, StreamingContext streamingContext) - { - GetObjectData(serializationInfo, streamingContext); - } - - public override string[] GetValues(int index) - { - var vals = base.GetValues(index); - return vals != null && vals.Length > 0 ? vals : null; - } - - public override string[] GetValues(string header) - { - var vals = base.GetValues(header); - return vals != null && vals.Length > 0 ? vals : null; - } - - public override void OnDeserialization(object sender) - { - } - - public void Remove(HttpRequestHeader header) - { - DoWithCheckingState(removeWithoutCheckingName, Convert(header), null, false, false); - } - - public void Remove(HttpResponseHeader header) - { - DoWithCheckingState(removeWithoutCheckingName, Convert(header), null, true, false); - } - - public override void Remove(string name) - { - DoWithCheckingState(removeWithoutCheckingName, CheckName(name), null, false); - } - - public void Set(HttpRequestHeader header, string value) - { - DoWithCheckingState(setWithoutCheckingName, Convert(header), value, false, true); - } - - public void Set(HttpResponseHeader header, string value) - { - DoWithCheckingState(setWithoutCheckingName, Convert(header), value, true, true); - } - - public override void Set(string name, string value) - { - DoWithCheckingState(setWithoutCheckingName, CheckName(name), value, true); - } - - /// - /// Returns a byte array representing the WebHeaderCollection in UTF-8 encoding. - /// - /// A byte array representing the WebHeaderCollection. - public byte[] ToByteArray() - { - return Encoding.UTF8.GetBytes(ToString()); - } - - /// - /// Returns a string representation of the WebHeaderCollection. - /// - /// A string representing the WebHeaderCollection. - public override string ToString() - { - var buff = new StringBuilder(); - Count.Times(i => buff.AppendFormat($"{GetKey(i)}: {Get(i)}\r\n")); - - return buff.Append("\r\n").ToString(); - } - - /// - /// Converts the specified HttpRequestHeader to a string. - /// - /// The HttpRequestHeader to convert. - /// A string representing the converted HttpRequestHeader. - internal static string Convert(HttpRequestHeader header) - { - return Convert(header.ToString()); - } - - internal static string Convert(HttpResponseHeader header) - { - return Convert(header.ToString()); - } - - 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 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 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($"{key}: {val}\r\n"); - } - } - else - { - buff.AppendFormat($"{key}: {Get(i)}\r\n"); - } - }); - - return buff.Append("\r\n").ToString(); - } - - protected void AddWithoutValidate(string headerName, string headerValue) - { - add(headerName, headerValue, true); - } - - private static int checkColonSeparated(string header) - { - var idx = header.IndexOf(':'); - if (idx == -1) - { - throw new ArgumentException("No colon could be found.", nameof(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(nameof(name)); - } - - name = name.Trim(); - if (!IsHeaderName(name)) - { - throw new ArgumentException("Contains invalid characters.", nameof(name)); - } - - return name; - } - - private static string CheckValue(string value) - { - if (value == null || value.Length == 0) - { - return string.Empty; - } - - value = value.Trim(); - if (value.Length > BUFFER_SIZE) - { - throw new ArgumentOutOfRangeException(nameof(value), "Greater than 65,535 characters."); - } - - if (!IsHeaderValue(value)) - { - throw new ArgumentException("Contains invalid characters.", nameof(value)); - } - - return value; - } - - private static string Convert(string key) - { - return _headers.TryGetValue(key, out HttpHeaderInfo info) ? info.Name : string.Empty; - } - - 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 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 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 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 void removeWithoutCheckingName(string name, string unuse) - { - CheckRestricted(name); - base.Remove(name); - } - - private void setWithoutCheckingName(string name, string value) - { - DoWithoutCheckingName(base.Set, name, value); - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Context/HttpListenerWSContext.cs b/EonaCat.Network/System/Sockets/Web/Core/Context/HttpListenerWSContext.cs deleted file mode 100644 index 543fcfe..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Context/HttpListenerWSContext.cs +++ /dev/null @@ -1,111 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.IO; -using System.Security.Principal; - -namespace EonaCat.Network -{ - /// - /// Represents the context of a WebSocket connection within an . - /// - /// - /// This class provides access to various properties and methods for interacting with the WebSocket connection - /// within the context of an HTTP request handled by an . - /// - /// - public class HttpListenerWSContext : WSContext - { - private readonly HttpListenerContext _context; - private readonly WSClient _websocket; - - /// - /// Initializes a new instance of the class. - /// - /// The associated with the WebSocket connection. - /// The WebSocket protocol negotiated during the connection. - internal HttpListenerWSContext(HttpListenerContext context, string protocol) - { - _context = context; - _websocket = new WSClient(this, protocol); - } - - public override CookieCollection CookieCollection => _context.Request.Cookies; - - public override NameValueCollection Headers => _context.Request.Headers; - - public override string Host => _context.Request.Headers["Host"]; - - public override bool IsAuthenticated => _context.User != null; - - public override bool IsLocal => _context.Request.IsLocal; - - public override bool IsSecureConnection => _context.Connection.IsSecure; - - public override bool IsWebSocketRequest => _context.Request.IsWebSocketRequest; - - public override string Origin => _context.Request.Headers["Origin"]; - - public override NameValueCollection QueryString => _context.Request.QueryString; - - public override Uri RequestUri => _context.Request.Url; - - public override string SecWebSocketKey => _context.Request.Headers["Sec-WebSocket-Key"]; - - public override IEnumerable SecWebSocketProtocols - { - get - { - var protocols = _context.Request.Headers["Sec-WebSocket-Protocol"]; - if (protocols != null) - { - foreach (var protocol in protocols.Split(',')) - { - yield return protocol.Trim(); - } - } - } - } - - public override string SecWebSocketVersion => _context.Request.Headers["Sec-WebSocket-Version"]; - - public override System.Net.IPEndPoint ServerEndPoint => _context.Connection.LocalEndPoint; - - public override IPrincipal User => _context.User; - - public override System.Net.IPEndPoint UserEndPoint => _context.Connection.RemoteEndPoint; - - public override WSClient WebSocket => _websocket; - - /// - /// Gets the stream of the underlying TCP connection. - /// - internal Stream Stream => _context.Connection.Stream; - - /// - public override string ToString() - { - return _context.Request.ToString(); - } - - /// - /// Closes the WebSocket connection. - /// - internal void Close() - { - _context.Connection.Close(true); - } - - /// - /// Closes the WebSocket connection with the specified HTTP status code. - /// - /// The HTTP status code indicating the reason for closure. - internal void Close(HttpStatusCode code) - { - _context.Response.Close(code); - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Context/TcpListenerWSContext.cs b/EonaCat.Network/System/Sockets/Web/Core/Context/TcpListenerWSContext.cs deleted file mode 100644 index 6d53f66..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Context/TcpListenerWSContext.cs +++ /dev/null @@ -1,230 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -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 EonaCat.Network -{ - /// - /// Represents the context of a WebSocket connection within a . - /// - /// - /// This internal class provides access to various properties and methods for interacting with the WebSocket connection - /// within the context of a TCP listener. - /// - internal class TcpListenerWSContext : WSContext - { - private readonly bool _secure; - private readonly TcpClient _tcpClient; - private readonly Uri _uri; - private readonly WSClient _websocket; - private CookieCollection _cookies; - private NameValueCollection _queryString; - private WebRequest _request; - private IPrincipal _user; - - /// - /// Initializes a new instance of the class. - /// - /// The associated with the WebSocket connection. - /// The WebSocket protocol negotiated during the connection. - /// A boolean indicating whether the connection is secure. - /// The SSL config for secure connections. - internal TcpListenerWSContext( - TcpClient tcpClient, - string protocol, - bool secure, - SSLConfigServer sslConfig - ) - { - _tcpClient = tcpClient; - _secure = secure; - - var netStream = tcpClient.GetStream(); - if (secure) - { - var sslStream = - new SslStream(netStream, false, sslConfig.ClientCertificateValidationCallback); - - sslStream.AuthenticateAsServer( - sslConfig.Certificate, - sslConfig.IsClientCertificateRequired, - sslConfig.SslProtocols, - sslConfig.CheckForCertificateRevocation - ); - - Stream = sslStream; - } - else - { - Stream = netStream; - } - - _request = WebRequest.Read(Stream, 90000); - _uri = - HttpUtility.CreateRequestUrl( - _request.RequestUri, _request.Headers["Host"], _request.IsWebSocketRequest, secure - ); - - _websocket = new WSClient(this, protocol); - } - - public override CookieCollection CookieCollection => _cookies ??= _request.Cookies; - - public override NameValueCollection Headers => _request.Headers; - - public override string Host => _request.Headers["Host"]; - - public override bool IsAuthenticated => _user != null; - - public override bool IsLocal => UserEndPoint.Address.IsLocal(); - - public override bool IsSecureConnection => _secure; - - public override bool IsWebSocketRequest => _request.IsWebSocketRequest; - - public override string Origin => _request.Headers["Origin"]; - - public override NameValueCollection QueryString => _queryString ??= - HttpUtility.InternalParseQueryString( - _uri?.Query, Encoding.UTF8 - ) - ; - - public override Uri RequestUri => _uri; - - public override string SecWebSocketKey => _request.Headers["Sec-WebSocket-Key"]; - - public override IEnumerable SecWebSocketProtocols - { - get - { - var protocols = _request.Headers["Sec-WebSocket-Protocol"]; - if (protocols != null) - { - foreach (var protocol in protocols.Split(',')) - { - yield return protocol.Trim(); - } - } - } - } - - public override string SecWebSocketVersion => _request.Headers["Sec-WebSocket-Version"]; - - public override System.Net.IPEndPoint ServerEndPoint => (System.Net.IPEndPoint)_tcpClient.Client.LocalEndPoint; - - public override IPrincipal User => _user; - - public override System.Net.IPEndPoint UserEndPoint => (System.Net.IPEndPoint)_tcpClient.Client.RemoteEndPoint; - - public override WSClient WebSocket => _websocket; - - /// - /// Gets the stream of the underlying TCP connection. - /// - internal Stream Stream { get; } - - /// - public override string ToString() - { - return _request.ToString(); - } - - /// - /// Authenticates the WebSocket connection based on the specified authentication scheme. - /// - /// The authentication scheme to use. - /// The authentication realm. - /// A function to find network credentials based on identity. - /// True if authentication is successful; otherwise, false. - internal bool Authenticate( - AuthenticationSchemes scheme, - string realm, - Func credentialsFinder - ) - { - if (scheme == AuthenticationSchemes.Anonymous) - { - return true; - } - - if (scheme == AuthenticationSchemes.None) - { - Close(HttpStatusCode.Forbidden); - return false; - } - - var chal = new AuthenticationChallenge(scheme, realm).ToString(); - - var retry = -1; - Func auth = null; - auth = - () => - { - retry++; - if (retry > 99) - { - Close(HttpStatusCode.Forbidden); - return false; - } - - var user = - HttpUtility.CreateUser( - _request.Headers["Authorization"], - scheme, - realm, - _request.HttpMethod, - credentialsFinder - ); - - if (user == null || !user.Identity.IsAuthenticated) - { - SendAuthenticationChallenge(chal); - return auth(); - } - - _user = user; - return true; - }; - - return auth(); - } - - /// - /// Closes the WebSocket connection. - /// - internal void Close() - { - Stream.Close(); - _tcpClient.Close(); - } - - /// - /// Closes the WebSocket connection with the specified HTTP status code. - /// - /// The HTTP status code indicating the reason for closure. - internal void Close(HttpStatusCode code) - { - _websocket.Close(WebResponse.CreateCloseResponse(code)); - } - - /// - /// Sends an authentication challenge to the WebSocket client. - /// - /// The authentication challenge. - internal void SendAuthenticationChallenge(string challenge) - { - var buff = WebResponse.CreateUnauthorizedResponse(challenge).ToByteArray(); - Stream.Write(buff, 0, buff.Length); - _request = WebRequest.Read(Stream, 15000); - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Context/WSContext.cs b/EonaCat.Network/System/Sockets/Web/Core/Context/WSContext.cs deleted file mode 100644 index 5b84bb4..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Context/WSContext.cs +++ /dev/null @@ -1,112 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Security.Principal; - -namespace EonaCat.Network -{ - /// - /// Represents the context of a WebSocket connection. - /// - /// - /// This abstract class defines properties and methods for accessing information related to a WebSocket connection, - /// such as headers, cookies, authentication status, and more. - /// - public abstract class WSContext - { - /// - /// Initializes a new instance of the class. - /// - protected WSContext() - { - } - - /// - /// Gets the collection of cookies associated with the WebSocket connection. - /// - public abstract CookieCollection CookieCollection { get; } - - /// - /// Gets the collection of headers associated with the WebSocket connection. - /// - public abstract NameValueCollection Headers { get; } - - /// - /// Gets the host information from the WebSocket connection. - /// - public abstract string Host { get; } - - /// - /// Gets a value indicating whether the WebSocket connection is authenticated. - /// - public abstract bool IsAuthenticated { get; } - - /// - /// Gets a value indicating whether the WebSocket connection is local. - /// - public abstract bool IsLocal { get; } - - /// - /// Gets a value indicating whether the WebSocket connection is secure. - /// - public abstract bool IsSecureConnection { get; } - - /// - /// Gets a value indicating whether the request is a WebSocket request. - /// - public abstract bool IsWebSocketRequest { get; } - - /// - /// Gets the origin of the WebSocket connection. - /// - public abstract string Origin { get; } - - /// - /// Gets the query string information from the WebSocket connection. - /// - public abstract NameValueCollection QueryString { get; } - - /// - /// Gets the URI of the WebSocket request. - /// - public abstract Uri RequestUri { get; } - - /// - /// Gets the value of the 'Sec-WebSocket-Key' header from the WebSocket connection. - /// - public abstract string SecWebSocketKey { get; } - - /// - /// Gets the protocols specified in the 'Sec-WebSocket-Protocol' header from the WebSocket connection. - /// - public abstract IEnumerable SecWebSocketProtocols { get; } - - /// - /// Gets the value of the 'Sec-WebSocket-Version' header from the WebSocket connection. - /// - public abstract string SecWebSocketVersion { get; } - - /// - /// Gets the local endpoint of the WebSocket server. - /// - public abstract System.Net.IPEndPoint ServerEndPoint { get; } - - /// - /// Gets the user associated with the WebSocket connection. - /// - public abstract IPrincipal User { get; } - - /// - /// Gets the remote endpoint of the WebSocket user. - /// - public abstract System.Net.IPEndPoint UserEndPoint { get; } - - /// - /// Gets the WebSocket instance associated with the context. - /// - public abstract WSClient WebSocket { get; } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Cookies/Cookie.cs b/EonaCat.Network/System/Sockets/Web/Core/Cookies/Cookie.cs deleted file mode 100644 index 2419fdb..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Cookies/Cookie.cs +++ /dev/null @@ -1,590 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.Globalization; -using System.Text; - -namespace EonaCat.Network -{ - /// - /// Represents an HTTP cookie. - /// - [Serializable] - public sealed class Cookie - { - private static readonly char[] _reservedCharsForName; - private static readonly char[] _reservedCharsForValue; - private readonly DateTime _timestamp; - private string _comment; - private Uri _commentUri; - private bool _discard; - private string _domain; - private DateTime _expires; - private bool _httpOnly; - private string _name; - private string _path; - private string _port; - private int[] _ports; - private bool _secure; - private string _value; - private int _version; - - static Cookie() - { - _reservedCharsForName = new[] { ' ', '=', ';', ',', '\n', '\r', '\t' }; - _reservedCharsForValue = new[] { ';', ',' }; - } - - /// - /// Initializes a new instance of the class. - /// - public Cookie() - { - _comment = string.Empty; - _domain = string.Empty; - _expires = DateTime.MinValue; - _name = string.Empty; - _path = string.Empty; - _port = string.Empty; - _ports = new int[0]; - _timestamp = DateTime.Now; - _value = string.Empty; - _version = 0; - } - - /// - /// Initializes a new instance of the class with the specified name and value. - /// - /// The name of the cookie. - /// The value of the cookie. - public Cookie(string name, string value) - : this() - { - Name = name; - Value = value; - } - - /// - /// Initializes a new instance of the class with the specified name, value, and path. - /// - /// The name of the cookie. - /// The value of the cookie. - /// The path for which the cookie is valid. - public Cookie(string name, string value, string path) - : this(name, value) - { - Path = path; - } - - /// - /// Initializes a new instance of the class with the specified name, value, path, and domain. - /// - /// The name of the cookie. - /// The value of the cookie. - /// The path for which the cookie is valid. - /// The domain to which the cookie belongs. - public Cookie(string name, string value, string path, string domain) - : this(name, value, path) - { - Domain = domain; - } - - public string Comment - { - get - { - return _comment; - } - - set - { - _comment = value ?? string.Empty; - } - } - - public Uri CommentUri - { - get - { - return _commentUri; - } - - set - { - _commentUri = value; - } - } - - public bool Discard - { - get - { - return _discard; - } - - set - { - _discard = value; - } - } - - public string Domain - { - get - { - return _domain; - } - - set - { - if (value.IsNullOrEmpty()) - { - _domain = string.Empty; - ExactDomain = true; - } - else - { - _domain = value; - ExactDomain = value[0] != '.'; - } - } - } - - public bool Expired - { - get - { - return _expires != DateTime.MinValue && _expires <= DateTime.Now; - } - - set - { - _expires = value ? DateTime.Now : DateTime.MinValue; - } - } - - public DateTime Expires - { - get - { - return _expires; - } - - set - { - _expires = value; - } - } - - public bool HttpOnly - { - get - { - return _httpOnly; - } - - set - { - _httpOnly = value; - } - } - - public string Name - { - get - { - return _name; - } - - set - { - if (!canSetName(value, out string message)) - { - throw new CookieException(message); - } - - _name = value; - } - } - - public string Path - { - get - { - return _path; - } - - set - { - _path = value ?? string.Empty; - } - } - - public string Port - { - get - { - return _port; - } - - set - { - if (value.IsNullOrEmpty()) - { - _port = string.Empty; - _ports = new int[0]; - - return; - } - - if (!value.IsEnclosedIn('"')) - { - throw new CookieException( - "The value specified for the Port attribute isn't enclosed in double quotes."); - } - - if (!tryCreatePorts(value, out _ports, out string err)) - { - throw new CookieException( - string.Format( - "The value specified for the Port attribute contains an invalid value: {0}", err)); - } - - _port = value; - } - } - - public bool Secure - { - get - { - return _secure; - } - - set - { - _secure = value; - } - } - - public DateTime TimeStamp => _timestamp; - - public string Value - { - get - { - return _value; - } - - set - { - if (!canSetValue(value, out string message)) - { - throw new CookieException(message); - } - - _value = value.Length > 0 ? value : "\"\""; - } - } - - public int Version - { - get - { - return _version; - } - - set - { - if (value < 0 || value > 1) - { - throw new ArgumentOutOfRangeException(nameof(value), "Not 0 or 1."); - } - - _version = value; - } - } - - internal bool ExactDomain - { - get; set; - } - - 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; - } - } - - internal int[] Ports => _ports; - - /// - public override bool Equals(object comparand) - { - return comparand is Cookie cookie && - _name.Equals(cookie.Name, StringComparison.InvariantCultureIgnoreCase) && - _value.Equals(cookie.Value, StringComparison.InvariantCulture) && - _path.Equals(cookie.Path, StringComparison.InvariantCulture) && - _domain.Equals(cookie.Domain, StringComparison.InvariantCultureIgnoreCase) && - _version == cookie.Version; - } - - /// - public override int GetHashCode() - { - return hash( - StringComparer.InvariantCultureIgnoreCase.GetHashCode(_name), - _value.GetHashCode(), - _path.GetHashCode(), - StringComparer.InvariantCultureIgnoreCase.GetHashCode(_domain), - _version); - } - - /// - public override string ToString() - { - return ToRequestString(null); - } - - // From client to server - internal string ToRequestString(Uri uri) - { - if (_name.Length == 0) - { - return string.Empty; - } - - if (_version == 0) - { - return string.Format("{0}={1}", _name, _value); - } - - var output = new StringBuilder(64); - output.AppendFormat("$Version={0}; {1}={2}", _version, _name, _value); - - if (!_path.IsNullOrEmpty()) - { - output.AppendFormat("; $Path={0}", _path); - } - else if (uri != null) - { - output.AppendFormat("; $Path={0}", uri.GetAbsolutePath()); - } - else - { - output.Append("; $Path=/"); - } - - var appendDomain = uri == null || uri.Host != _domain; - if (appendDomain && !_domain.IsNullOrEmpty()) - { - output.AppendFormat("; $Domain={0}", _domain); - } - - if (!_port.IsNullOrEmpty()) - { - if (_port == "\"\"") - { - output.Append("; $Port"); - } - else - { - output.AppendFormat("; $Port={0}", _port); - } - } - - return output.ToString(); - } - - // From server to client - internal string ToResponseString() - { - return _name.Length > 0 - ? (_version == 0 ? ToResponseStringVersion0() : ToResponseStringVersion1()) - : string.Empty; - } - - private static bool canSetName(string name, out string message) - { - if (name.IsNullOrEmpty()) - { - message = "The value specified for the Name is null or empty."; - return false; - } - - if (name[0] == '$' || name.Contains(_reservedCharsForName)) - { - message = "The value specified for the Name contains an invalid character."; - return false; - } - - message = string.Empty; - return true; - } - - private static bool canSetValue(string value, out string message) - { - if (value == null) - { - message = "The value specified for the Value is null."; - return false; - } - - if (value.Contains(_reservedCharsForValue) && !value.IsEnclosedIn('"')) - { - message = "The value specified for the Value contains an invalid character."; - return false; - } - - message = string.Empty; - return true; - } - - 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 static bool tryCreatePorts(string value, out int[] result, out string parseError) - { - var ports = value.Trim('"').Split(','); - var len = ports.Length; - var res = new int[len]; - for (var i = 0; i < len; i++) - { - res[i] = int.MinValue; - - var port = ports[i].Trim(); - if (port.Length == 0) - { - continue; - } - - if (!int.TryParse(port, out res[i])) - { - result = new int[0]; - parseError = port; - - return false; - } - } - - result = res; - parseError = string.Empty; - - return true; - } - - private string ToResponseStringVersion0() - { - var output = new StringBuilder(64); - output.AppendFormat("{0}={1}", _name, _value); - - if (_expires != DateTime.MinValue) - { - output.AppendFormat( - "; Expires={0}", - _expires.ToUniversalTime().ToString( - "ddd, dd'-'MMM'-'yyyy HH':'mm':'ss 'GMT'", - CultureInfo.CreateSpecificCulture("en-US"))); - } - - if (!_path.IsNullOrEmpty()) - { - output.AppendFormat("; Path={0}", _path); - } - - if (!_domain.IsNullOrEmpty()) - { - output.AppendFormat("; Domain={0}", _domain); - } - - if (_secure) - { - output.Append("; Secure"); - } - - if (_httpOnly) - { - output.Append("; HttpOnly"); - } - - return output.ToString(); - } - - private string ToResponseStringVersion1() - { - var output = new StringBuilder(64); - output.AppendFormat("{0}={1}; Version={2}", _name, _value, _version); - - if (_expires != DateTime.MinValue) - { - output.AppendFormat("; Max-Age={0}", MaxAge); - } - - if (!_path.IsNullOrEmpty()) - { - output.AppendFormat("; Path={0}", _path); - } - - if (!_domain.IsNullOrEmpty()) - { - output.AppendFormat("; Domain={0}", _domain); - } - - if (!_port.IsNullOrEmpty()) - { - if (_port == "\"\"") - { - output.Append("; Port"); - } - else - { - output.AppendFormat("; Port={0}", _port); - } - } - - if (!_comment.IsNullOrEmpty()) - { - output.AppendFormat("; Comment={0}", _comment.UrlEncode()); - } - - if (_commentUri != null) - { - var url = _commentUri.OriginalString; - output.AppendFormat("; CommentURL={0}", url.IsToken() ? url : url.Quote()); - } - - if (_discard) - { - output.Append("; Discard"); - } - - if (_secure) - { - output.Append("; Secure"); - } - - return output.ToString(); - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Cookies/CookieCollection.cs b/EonaCat.Network/System/Sockets/Web/Core/Cookies/CookieCollection.cs deleted file mode 100644 index 14b0ee8..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Cookies/CookieCollection.cs +++ /dev/null @@ -1,545 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Globalization; -using System.Text; - -namespace EonaCat.Network -{ - /// - /// Represents a collection of HTTP cookies. - /// - [Serializable] - public class CookieCollection : ICollection, IEnumerable - { - private readonly List _list; - private object _locker; - - /// - /// Initializes a new instance of the class. - /// - public CookieCollection() - { - _list = new List(); - } - - /// - /// Gets the number of cookies in the collection. - /// - public int Count => _list.Count; - - /// - /// Gets a value indicating whether the collection is read-only. Always returns true. - /// - public bool IsReadOnly => true; - - /// - /// Gets a value indicating whether access to the collection is synchronized (thread-safe). Always returns false. - /// - public bool IsSynchronized => false; - - /// - /// Gets an object that can be used to synchronize access to the collection. - /// - public object SyncRoot => _locker ??= ((ICollection)_list).SyncRoot; - - internal IList List => _list; - - internal IEnumerable Sorted - { - get - { - var list = new List(_list); - if (list.Count > 1) - { - list.Sort(compareCookieWithinSorted); - } - - return list; - } - } - - /// - /// Gets or sets the cookie at the specified index. - /// - /// The index of the cookie to get or set. - /// The cookie at the specified index. - public Cookie this[int index] - { - get - { - if (index < 0 || index >= _list.Count) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } - - return _list[index]; - } - } - - /// - /// Gets the cookie with the specified name. - /// - /// The name of the cookie to retrieve. - /// The cookie with the specified name, or null if the cookie is not found. - public Cookie this[string name] - { - get - { - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } - - foreach (var cookie in Sorted) - { - if (cookie.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) - { - return cookie; - } - } - - return null; - } - } - - /// - /// Adds the specified cookie to the collection, updating it if it already exists. - /// - /// The cookie to add or update. - public void Add(Cookie cookie) - { - if (cookie == null) - { - throw new ArgumentNullException(nameof(cookie)); - } - - var pos = searchCookie(cookie); - if (pos == -1) - { - _list.Add(cookie); - return; - } - - _list[pos] = cookie; - } - - /// - /// Adds the cookies from the specified to this collection, updating existing cookies. - /// - /// The to add or update from. - public void Add(CookieCollection cookies) - { - if (cookies == null) - { - throw new ArgumentNullException(nameof(cookies)); - } - - foreach (Cookie cookie in cookies) - { - Add(cookie); - } - } - - /// - /// Copies the cookies in the collection to the specified array, starting at the specified index. - /// - /// The destination array. - /// The index in the destination array at which copying begins. - public void CopyTo(Array array, int index) - { - if (array == null) - { - throw new ArgumentNullException(nameof(array)); - } - - if (index < 0) - { - throw new ArgumentOutOfRangeException(nameof(index), "Less than zero."); - } - - if (array.Rank > 1) - { - throw new ArgumentException("Multidimensional.", nameof(array)); - } - - if (array.Length - index < _list.Count) - { - throw new ArgumentException( - "The number of elements in this collection is greater than the available space of the destination array."); - } - - if (!array.GetType().GetElementType().IsAssignableFrom(typeof(Cookie))) - { - throw new InvalidCastException( - "The elements in this collection cannot be cast automatically to the type of the destination array."); - } ((IList)_list).CopyTo(array, index); - } - - /// - /// Copies the cookies in the collection to the specified array, starting at the specified index. - /// - /// The destination array. - /// The index in the destination array at which copying begins. - public void CopyTo(Cookie[] array, int index) - { - if (array == null) - { - throw new ArgumentNullException(nameof(array)); - } - - if (index < 0) - { - throw new ArgumentOutOfRangeException(nameof(index), "Less than zero."); - } - - if (array.Length - index < _list.Count) - { - throw new ArgumentException( - "The number of elements in this collection is greater than the available space of the destination array."); - } - - _list.CopyTo(array, index); - } - - /// - /// Returns an enumerator that iterates through the collection. - /// - /// An enumerator for the collection. - public IEnumerator GetEnumerator() - { - return _list.GetEnumerator(); - } - - /// - /// Parses the specified cookie string, creating a . - /// - /// The cookie string to parse. - /// True if parsing a response header; otherwise, false. - /// A instance representing the parsed cookies. - internal static CookieCollection Parse(string value, bool response) - { - return response - ? parseResponse(value) - : parseRequest(value); - } - - internal void SetOrRemove(Cookie cookie) - { - var pos = searchCookie(cookie); - if (pos == -1) - { - if (!cookie.Expired) - { - _list.Add(cookie); - } - - return; - } - - if (!cookie.Expired) - { - _list[pos] = cookie; - return; - } - - _list.RemoveAt(pos); - } - - internal void SetOrRemove(CookieCollection cookies) - { - foreach (Cookie cookie in cookies) - { - SetOrRemove(cookie); - } - } - - internal void Sort() - { - if (_list.Count > 1) - { - _list.Sort(compareCookieWithinSort); - } - } - - private static int compareCookieWithinSort(Cookie x, Cookie y) - { - return (x.Name.Length + x.Value.Length) - (y.Name.Length + y.Value.Length); - } - - private static int compareCookieWithinSorted(Cookie x, Cookie y) - { - var ret = 0; - return (ret = x.Version - y.Version) != 0 - ? ret - : (ret = x.Name.CompareTo(y.Name)) != 0 - ? ret - : y.Path.Length - x.Path.Length; - } - - private static CookieCollection parseRequest(string value) - { - var cookies = new CookieCollection(); - - Cookie cookie = null; - var ver = 0; - var pairs = splitCookieHeaderValue(value); - for (var i = 0; i < pairs.Length; i++) - { - var pair = pairs[i].Trim(); - if (pair.Length == 0) - { - continue; - } - - if (pair.StartsWith("$version", StringComparison.InvariantCultureIgnoreCase)) - { - ver = int.Parse(pair.GetValue('=', true)); - } - else if (pair.StartsWith("$path", StringComparison.InvariantCultureIgnoreCase)) - { - if (cookie != null) - { - cookie.Path = pair.GetValue('='); - } - } - else if (pair.StartsWith("$domain", StringComparison.InvariantCultureIgnoreCase)) - { - if (cookie != null) - { - cookie.Domain = pair.GetValue('='); - } - } - else if (pair.StartsWith("$port", StringComparison.InvariantCultureIgnoreCase)) - { - var port = pair.Equals("$port", StringComparison.InvariantCultureIgnoreCase) - ? "\"\"" - : pair.GetValue('='); - - if (cookie != null) - { - cookie.Port = port; - } - } - else - { - if (cookie != null) - { - cookies.Add(cookie); - } - - string name; - string val = string.Empty; - - var pos = pair.IndexOf('='); - if (pos == -1) - { - name = pair; - } - else if (pos == pair.Length - 1) - { - name = pair.Substring(0, pos).TrimEnd(' '); - } - else - { - name = pair.Substring(0, pos).TrimEnd(' '); - val = pair.Substring(pos + 1).TrimStart(' '); - } - - cookie = new Cookie(name, val); - if (ver != 0) - { - cookie.Version = ver; - } - } - } - - if (cookie != null) - { - cookies.Add(cookie); - } - - return cookies; - } - - private static CookieCollection parseResponse(string value) - { - var cookies = new CookieCollection(); - - Cookie cookie = null; - var pairs = splitCookieHeaderValue(value); - for (var i = 0; i < pairs.Length; i++) - { - var pair = pairs[i].Trim(); - if (pair.Length == 0) - { - continue; - } - - if (pair.StartsWith("version", StringComparison.InvariantCultureIgnoreCase)) - { - if (cookie != null) - { - cookie.Version = int.Parse(pair.GetValue('=', true)); - } - } - else if (pair.StartsWith("expires", StringComparison.InvariantCultureIgnoreCase)) - { - var buff = new StringBuilder(pair.GetValue('='), 32); - if (i < pairs.Length - 1) - { - buff.AppendFormat(", {0}", pairs[++i].Trim()); - } - - if (!DateTime.TryParseExact( - buff.ToString(), - new[] { "ddd, dd'-'MMM'-'yyyy HH':'mm':'ss 'GMT'", "r" }, - CultureInfo.CreateSpecificCulture("en-US"), - DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, - out DateTime expires)) - { - expires = DateTime.Now; - } - - if (cookie != null && cookie.Expires == DateTime.MinValue) - { - cookie.Expires = expires.ToLocalTime(); - } - } - else if (pair.StartsWith("max-age", StringComparison.InvariantCultureIgnoreCase)) - { - var max = int.Parse(pair.GetValue('=', true)); - var expires = DateTime.Now.AddSeconds(max); - if (cookie != null) - { - cookie.Expires = expires; - } - } - else if (pair.StartsWith("path", StringComparison.InvariantCultureIgnoreCase)) - { - if (cookie != null) - { - cookie.Path = pair.GetValue('='); - } - } - else if (pair.StartsWith("domain", StringComparison.InvariantCultureIgnoreCase)) - { - if (cookie != null) - { - cookie.Domain = pair.GetValue('='); - } - } - else if (pair.StartsWith("port", StringComparison.InvariantCultureIgnoreCase)) - { - var port = pair.Equals("port", StringComparison.InvariantCultureIgnoreCase) - ? "\"\"" - : pair.GetValue('='); - - if (cookie != null) - { - cookie.Port = port; - } - } - else if (pair.StartsWith("comment", StringComparison.InvariantCultureIgnoreCase)) - { - if (cookie != null) - { - cookie.Comment = pair.GetValue('=').UrlDecode(); - } - } - else if (pair.StartsWith("commenturl", StringComparison.InvariantCultureIgnoreCase)) - { - if (cookie != null) - { - cookie.CommentUri = pair.GetValue('=', true).ToUri(); - } - } - else if (pair.StartsWith("discard", StringComparison.InvariantCultureIgnoreCase)) - { - if (cookie != null) - { - cookie.Discard = true; - } - } - else if (pair.StartsWith("secure", StringComparison.InvariantCultureIgnoreCase)) - { - if (cookie != null) - { - cookie.Secure = true; - } - } - else if (pair.StartsWith("httponly", StringComparison.InvariantCultureIgnoreCase)) - { - if (cookie != null) - { - cookie.HttpOnly = true; - } - } - else - { - if (cookie != null) - { - cookies.Add(cookie); - } - - string name; - string val = string.Empty; - - var pos = pair.IndexOf('='); - if (pos == -1) - { - name = pair; - } - else if (pos == pair.Length - 1) - { - name = pair.Substring(0, pos).TrimEnd(' '); - } - else - { - name = pair.Substring(0, pos).TrimEnd(' '); - val = pair.Substring(pos + 1).TrimStart(' '); - } - - cookie = new Cookie(name, val); - } - } - - if (cookie != null) - { - cookies.Add(cookie); - } - - return cookies; - } - - private static string[] splitCookieHeaderValue(string value) - { - return new List(value.SplitHeaderValue(',', ';')).ToArray(); - } - - private int searchCookie(Cookie cookie) - { - var name = cookie.Name; - var path = cookie.Path; - var domain = cookie.Domain; - var ver = cookie.Version; - - for (var i = _list.Count - 1; i >= 0; i--) - { - var c = _list[i]; - if (c.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase) && - c.Path.Equals(path, StringComparison.InvariantCulture) && - c.Domain.Equals(domain, StringComparison.InvariantCultureIgnoreCase) && - c.Version == ver) - { - return i; - } - } - - return -1; - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Cookies/CookieException.cs b/EonaCat.Network/System/Sockets/Web/Core/Cookies/CookieException.cs deleted file mode 100644 index ccd19b1..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Cookies/CookieException.cs +++ /dev/null @@ -1,81 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.Runtime.Serialization; -using System.Security.Permissions; - -namespace EonaCat.Network -{ - /// - /// Represents an exception specific to EonaCat Network cookies. - /// - [Serializable] - public class CookieException : FormatException, ISerializable - { - /// - /// Initializes a new instance of the class. - /// - public CookieException() - : base() - { - } - - /// - /// Initializes a new instance of the class with a specified error message. - /// - /// The error message that explains the reason for the exception. - internal CookieException(string message) - : base($"EonaCat Network: {message}") - { - } - - /// - /// Initializes a new instance of the class with a specified error message - /// and a reference to the inner exception that is the cause of this exception. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception. - internal CookieException(string message, Exception innerException) - : base($"EonaCat Network: {message}", innerException) - { - } - - /// - /// Initializes a new instance of the class. - /// - protected CookieException( - SerializationInfo serializationInfo, StreamingContext streamingContext) - : base(serializationInfo, streamingContext) - { - } - - /// - /// Populates a with the data needed to serialize the exception. - /// - /// The to populate with data. - /// The destination (see ) for this serialization. - [SecurityPermission( - SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)] - public override void GetObjectData( - SerializationInfo serializationInfo, StreamingContext streamingContext) - { - base.GetObjectData(serializationInfo, streamingContext); - } - - /// - /// Populates a with the data needed to serialize the exception. - /// - /// The to populate with data. - /// The destination (see ) for this serialization. - [SecurityPermission( - SecurityAction.LinkDemand, - Flags = SecurityPermissionFlag.SerializationFormatter, - SerializationFormatter = true)] - void ISerializable.GetObjectData( - SerializationInfo serializationInfo, StreamingContext streamingContext) - { - base.GetObjectData(serializationInfo, streamingContext); - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Endpoints/EndPointListener.cs b/EonaCat.Network/System/Sockets/Web/Core/Endpoints/EndPointListener.cs deleted file mode 100644 index d624e99..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Endpoints/EndPointListener.cs +++ /dev/null @@ -1,532 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -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 EonaCat.Network -{ - /// - /// Represents an endpoint listener for managing HTTP connections. - /// - internal sealed class EndPointListener - { - private static readonly string _defaultCertFolderPath; - private readonly IPEndPoint _endpoint; - private readonly Socket _socket; - private readonly Dictionary _unregistered; - private readonly object _unregisteredSync; - private List _all; // host == '+' - private Dictionary _prefixes; - private List _unhandled; // host == '*' - - static EndPointListener() - { - _defaultCertFolderPath = - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); - } - - internal EndPointListener( - IPEndPoint endpoint, - bool secure, - string certificateFolderPath, - SSLConfigServer sslConfig, - bool reuseAddress - ) - { - if (secure) - { - var cert = - getCertificate(endpoint.Port, certificateFolderPath, sslConfig.Certificate); - - if (cert == null) - { - throw new ArgumentException("No server certificate could be found."); - } - - IsSecure = true; - SSL = new SSLConfigServer(sslConfig); - SSL.Certificate = 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); - } - - /// - /// Gets the IP address of the endpoint. - /// - public IPAddress Address => _endpoint.Address; - - /// - /// Gets a value indicating whether the endpoint is secure. - /// - public bool IsSecure { get; } - - /// - /// Gets the port number of the endpoint. - /// - public int Port => _endpoint.Port; - - /// - /// Gets the SSL config for the secure endpoint. - /// - public SSLConfigServer SSL { get; } - - 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) - { - [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(); - } - - 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 && !_prefixes[pref].AllowForwardedRequest) - { - 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; - } - - 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 static void onAccept(IAsyncResult asyncResult) - { - var lsnr = (EndPointListener)asyncResult.AsyncState; - - Socket sock = null; - try - { - sock = lsnr._socket.EndAccept(asyncResult); - } - catch (SocketException) - { - // Do nothing - } - catch (ObjectDisposedException) - { - return; - } - - try - { - lsnr._socket.BeginAccept(onAccept, lsnr); - } - catch - { - 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; - } - - 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); - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Endpoints/EndPointManager.cs b/EonaCat.Network/System/Sockets/Web/Core/Endpoints/EndPointManager.cs deleted file mode 100644 index f2aa2a2..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Endpoints/EndPointManager.cs +++ /dev/null @@ -1,232 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Net; - -namespace EonaCat.Network -{ - /// - /// Manages HTTP endpoint listeners and their prefixes. - /// - internal sealed class EndPointManager - { - private static readonly Dictionary _endpoints; - - /// - /// Initializes static members of the class. - /// - static EndPointManager() - { - _endpoints = new Dictionary(); - } - - /// - /// Prevents a default instance of the class from being created. - /// - private EndPointManager() - { - } - - /// - /// Adds an HTTP listener and its associated prefixes. - /// - /// The HTTP listener to be added. - 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; - } - } - } - - /// - /// Adds an HTTP listener prefix. - /// - /// The URI prefix to be added. - /// The HTTP listener associated with the prefix. - public static void AddPrefix(string uriPrefix, HttpListener listener) - { - lock (((ICollection)_endpoints).SyncRoot) - { - addPrefix(uriPrefix, listener); - } - } - - /// - /// Removes an HTTP listener and its associated prefixes. - /// - /// The HTTP listener to be removed. - public static void RemoveListener(HttpListener listener) - { - lock (((ICollection)_endpoints).SyncRoot) - { - foreach (var pref in listener.Prefixes) - { - removePrefix(pref, listener); - } - } - } - - /// - /// Removes an HTTP listener prefix. - /// - /// The URI prefix to be removed. - /// The HTTP listener associated with the prefix. - public static void RemovePrefix(string uriPrefix, HttpListener listener) - { - lock (((ICollection)_endpoints).SyncRoot) - { - removePrefix(uriPrefix, listener); - } - } - - /// - /// Removes an endpoint and closes its associated listener. - /// - /// The endpoint to be removed. - /// true if the endpoint is successfully removed; otherwise, false. - internal static bool RemoveEndPoint(IPEndPoint endpoint) - { - lock (((ICollection)_endpoints).SyncRoot) - { - if (!_endpoints.TryGetValue(endpoint, out EndPointListener lsnr)) - { - return false; - } - - _endpoints.Remove(endpoint); - lsnr.Close(); - - return true; - } - } - - private static void addPrefix(string uriPrefix, HttpListener listener) - { - var pref = new HttpListenerPrefix(uriPrefix); - - var addr = convertToIPAddress(pref.Host); - if (!addr.IsLocal()) - { - throw new HttpListenerException(87, "Includes an invalid host."); - } - - if (!int.TryParse(pref.Port, out int 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); - - if (_endpoints.TryGetValue(endpoint, out EndPointListener lsnr)) - { - if (lsnr.IsSecure ^ pref.IsSecure) - { - throw new HttpListenerException(87, "Includes an invalid scheme."); - } - } - else - { - lsnr = - new EndPointListener( - endpoint, - pref.IsSecure, - listener.CertificateFolderPath, - listener.SSL, - listener.ReuseAddress - ); - - _endpoints.Add(endpoint, lsnr); - } - - lsnr.AddPrefix(pref, listener); - } - - private static IPAddress convertToIPAddress(string hostname) - { - return hostname == "*" || hostname == "+" ? IPAddress.Any : hostname.ToIPAddress(); - } - - private static void removePrefix(string uriPrefix, HttpListener listener) - { - var pref = new HttpListenerPrefix(uriPrefix); - - var addr = convertToIPAddress(pref.Host); - if (!addr.IsLocal()) - { - return; - } - - if (!int.TryParse(pref.Port, out int 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); - - if (!_endpoints.TryGetValue(endpoint, out EndPointListener lsnr)) - { - return; - } - - if (lsnr.IsSecure ^ pref.IsSecure) - { - return; - } - - lsnr.RemovePrefix(pref, listener); - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpBasicIdentity.cs b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpBasicIdentity.cs deleted file mode 100644 index b770b76..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpBasicIdentity.cs +++ /dev/null @@ -1,31 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System.Security.Principal; - -namespace EonaCat.Network -{ - /// - /// Represents a basic HTTP identity with a username and password. - /// - public class HttpBasicIdentity : GenericIdentity - { - private readonly string _password; - - /// - /// Initializes a new instance of the class. - /// - /// The username associated with the identity. - /// The password associated with the identity. - internal HttpBasicIdentity(string username, string password) - : base(username, "Basic") - { - _password = password; - } - - /// - /// Gets the password associated with the identity. - /// - public virtual string Password => _password; - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpConnection.cs b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpConnection.cs deleted file mode 100644 index 42b604b..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpConnection.cs +++ /dev/null @@ -1,633 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -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 EonaCat.Network -{ - /// - /// Represents an HTTP connection. - /// - internal sealed class HttpConnection - { - private const int _bufferLength = 8192; - private const int TIMEOUT_CONTINUE = 15000; - private const int TIMEOUT_INITIAL = 90000; - private readonly EndPointListener _listener; - private readonly object _lock; - private readonly Dictionary _timeoutCanceled; - private byte[] _buffer; - private HttpListenerContext _context; - private bool _contextRegistered; - private StringBuilder _currentLine; - private InputState _inputState; - private RequestStream _inputStream; - private HttpListener _lastListener; - private LineState _lineState; - private ResponseStream _outputStream; - private int _position; - private MemoryStream _requestBuffer; - private Socket _socket; - private int _timeout; - private Timer _timer; - - /// - /// Initializes a new instance of the class. - /// - /// The socket associated with the connection. - /// The endpoint listener. - internal HttpConnection(Socket socket, EndPointListener listener) - { - _socket = socket; - _listener = listener; - IsSecure = listener.IsSecure; - - var networkStream = new NetworkStream(socket, false); - if (IsSecure) - { - var conf = listener.SSL; - var sslStream = new SslStream(networkStream, false, conf.ClientCertificateValidationCallback); - sslStream.AuthenticateAsServer( - conf.Certificate, - conf.IsClientCertificateRequired, - conf.SslProtocols, - conf.CheckForCertificateRevocation - ); - - Stream = sslStream; - } - else - { - Stream = networkStream; - } - - _lock = new object(); - _timeout = TIMEOUT_INITIAL; - _timeoutCanceled = new Dictionary(); - _timer = new Timer(OnTimeout, this, Timeout.Infinite, Timeout.Infinite); - - Setup(); - } - - /// - /// Gets a value indicating whether the connection is closed. - /// - public bool IsClosed => _socket == null; - - /// - /// Gets a value indicating whether the connection is secure. - /// - public bool IsSecure { get; } - - /// - /// Gets the local endpoint. - /// - public IPEndPoint LocalEndPoint => (IPEndPoint)_socket.LocalEndPoint; - - /// - /// Gets the remote endpoint. - /// - public IPEndPoint RemoteEndPoint => (IPEndPoint)_socket.RemoteEndPoint; - - /// - /// Gets or sets the number of reuses. - /// - public int Reuses { get; private set; } - - /// - /// Gets the network stream associated with the connection. - /// - public Stream Stream { get; private set; } - - /// - /// Initiates reading the request. - /// - public void BeginReadRequest() - { - _buffer ??= new byte[_bufferLength]; - - if (Reuses == 1) - { - _timeout = TIMEOUT_CONTINUE; - } - - try - { - _timeoutCanceled.Add(Reuses, false); - _timer.Change(_timeout, Timeout.Infinite); - Stream.BeginRead(_buffer, 0, _bufferLength, OnRead, this); - } - catch - { - close(); - } - } - - /// - /// Closes the connection. - /// - public void Close() - { - Close(false); - } - - /// - /// Gets the request stream. - /// - /// The length of the content. - /// True if chunked, false otherwise. - /// The request stream. - public RequestStream GetRequestStream(long contentLength, bool chunked) - { - if (_inputStream != null || _socket == null) - { - return _inputStream; - } - - lock (_lock) - { - if (_socket == null) - { - return _inputStream; - } - - var buff = _requestBuffer.GetBuffer(); - var len = (int)_requestBuffer.Length; - DisposeRequestBuffer(); - if (chunked) - { - _context.Response.SendInChunks = true; - _inputStream = - new ChunkedRequestStream(Stream, buff, _position, len - _position, _context); - } - else - { - _inputStream = - new RequestStream(Stream, buff, _position, len - _position, contentLength); - } - - return _inputStream; - } - } - - /// - /// Gets the response stream. - /// - /// The response stream. - public ResponseStream GetResponseStream() - { - if (_outputStream != null || _socket == null) - { - return _outputStream; - } - - lock (_lock) - { - if (_socket == null) - { - return _outputStream; - } - - var lsnr = _context.Listener; - var ignore = lsnr == null || lsnr.IgnoreWriteExceptions; - _outputStream = new ResponseStream(Stream, _context.Response, ignore); - - return _outputStream; - } - } - - /// - /// Sends an error response. - /// - public void SendError() - { - SendError(_context.ErrorMessage, _context.ErrorStatus); - } - - /// - /// Sends an error response with the specified message and status code. - /// - /// The error message. - /// The HTTP status code. - public void SendError(string message, int status) - { - if (_socket == null) - { - return; - } - - lock (_lock) - { - if (_socket == null) - { - return; - } - - try - { - var httpResponse = _context.Response; - httpResponse.StatusCode = status; - httpResponse.ContentType = "text/html"; - - var content = new StringBuilder(64); - content.AppendFormat("EonaCat.Network Error

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

", message); - } - else - { - content.Append(""); - } - - var encoding = Encoding.UTF8; - var entity = encoding.GetBytes(content.ToString()); - httpResponse.ContentEncoding = encoding; - httpResponse.ContentLength64 = entity.LongLength; - - httpResponse.Close(entity, true); - } - catch - { - Close(true); - } - } - } - - /// - /// Closes the connection. - /// - /// True to force close, false otherwise. - internal void Close(bool force) - { - if (_socket == null) - { - return; - } - - lock (_lock) - { - if (_socket == null) - { - return; - } - - if (!force) - { - GetResponseStream().Close(false); - if (!_context.Response.CloseConnection && _context.Request.FlushInput()) - { - // Don't close. Keep working. - Reuses++; - DisposeRequestBuffer(); - UnregisterContext(); - Setup(); - BeginReadRequest(); - return; - } - } - else - { - _outputStream?.Close(true); - } - - close(); - } - } - - private static void OnRead(IAsyncResult asyncResult) - { - var httpConnection = (HttpConnection)asyncResult.AsyncState; - if (httpConnection._socket == null) - { - return; - } - - lock (httpConnection._lock) - { - if (httpConnection._socket == null) - { - return; - } - - var nread = -1; - var len = 0; - try - { - var current = httpConnection.Reuses; - if (!httpConnection._timeoutCanceled[current]) - { - httpConnection._timer.Change(Timeout.Infinite, Timeout.Infinite); - httpConnection._timeoutCanceled[current] = true; - } - - nread = httpConnection.Stream.EndRead(asyncResult); - httpConnection._requestBuffer.Write(httpConnection._buffer, 0, nread); - len = (int)httpConnection._requestBuffer.Length; - } - catch (Exception exception) - { - if (httpConnection._requestBuffer != null && httpConnection._requestBuffer.Length > 0) - { - httpConnection.SendError(exception.Message, 400); - return; - } - - httpConnection.close(); - return; - } - - if (nread <= 0) - { - httpConnection.close(); - return; - } - - if (httpConnection.ProcessInput(httpConnection._requestBuffer.GetBuffer(), len)) - { - if (!httpConnection._context.HasError) - { - httpConnection._context.Request.FinishInitialization(); - } - - if (httpConnection._context.HasError) - { - httpConnection.SendError(); - return; - } - - if (!httpConnection._listener.TrySearchHttpListener(httpConnection._context.Request.Url, out HttpListener httpListener)) - { - httpConnection.SendError(null, 404); - return; - } - - if (httpConnection._lastListener != httpListener) - { - httpConnection.RemoveConnection(); - if (!httpListener.AddConnection(httpConnection)) - { - httpConnection.close(); - return; - } - - httpConnection._lastListener = httpListener; - } - - httpConnection._context.Listener = httpListener; - if (!httpConnection._context.Authenticate()) - { - return; - } - - if (httpConnection._context.Register()) - { - httpConnection._contextRegistered = true; - } - - return; - } - - httpConnection.Stream.BeginRead(httpConnection._buffer, 0, _bufferLength, OnRead, httpConnection); - } - } - - private static void OnTimeout(object state) - { - var httpConnection = (HttpConnection)state; - var current = httpConnection.Reuses; - if (httpConnection._socket == null) - { - return; - } - - lock (httpConnection._lock) - { - if (httpConnection._socket == null) - { - return; - } - - if (httpConnection._timeoutCanceled[current]) - { - return; - } - - httpConnection.SendError(null, 408); - } - } - - private void close() - { - lock (_lock) - { - 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 bool ProcessInput(byte[] data, int length) - { - _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 exception) - { - _context.ErrorMessage = exception.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.LineFeed; i++) - { - read++; - - var b = buffer[i]; - if (b == 13) - { - _lineState = LineState.CarriageReturn; - } - else if (b == 10) - { - _lineState = LineState.LineFeed; - } - else - { - _currentLine.Append((char)b); - } - } - - if (_lineState != LineState.LineFeed) - { - 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 Setup() - { - _context = new HttpListenerContext(this); - _inputState = InputState.RequestLine; - _inputStream = null; - _lineState = LineState.None; - _outputStream = null; - _position = 0; - _requestBuffer = new MemoryStream(); - } - - private void UnregisterContext() - { - if (!_contextRegistered) - { - return; - } - - _context.Unregister(); - _contextRegistered = false; - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpDigestIdentity.cs b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpDigestIdentity.cs deleted file mode 100644 index b07ed95..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpDigestIdentity.cs +++ /dev/null @@ -1,95 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System.Collections.Specialized; -using System.Security.Principal; - -namespace EonaCat.Network -{ - /// - /// Represents an HTTP Digest identity. - /// - public class HttpDigestIdentity : GenericIdentity - { - private readonly NameValueCollection _parameters; - - /// - /// Initializes a new instance of the class. - /// - /// The collection of parameters. - internal HttpDigestIdentity(NameValueCollection parameters) - : base(parameters["username"], "Digest") - { - _parameters = parameters; - } - - /// - /// Gets the algorithm used for digest authentication. - /// - public string Algorithm => _parameters["algorithm"]; - - /// - /// Gets the client nonce. - /// - public string Cnonce => _parameters["cnonce"]; - - /// - /// Gets the nonce count. - /// - public string Nc => _parameters["nc"]; - - /// - /// Gets the nonce value. - /// - public string Nonce => _parameters["nonce"]; - - /// - /// Gets the opaque value. - /// - public string Opaque => _parameters["opaque"]; - - /// - /// Gets the quality of protection. - /// - public string Qop => _parameters["qop"]; - - /// - /// Gets the realm. - /// - public string Realm => _parameters["realm"]; - - /// - /// Gets the response value. - /// - public string Response => _parameters["response"]; - - /// - /// Gets the URI. - /// - public string Uri => _parameters["uri"]; - - /// - /// Validates the HTTP Digest identity. - /// - /// The password. - /// The realm. - /// The HTTP method. - /// The entity. - /// True if the identity is valid, false otherwise. - internal bool IsValid( - string password, string realm, string method, string entity - ) - { - var copied = new NameValueCollection(_parameters) - { - ["password"] = password, - ["realm"] = realm, - ["method"] = method, - ["entity"] = entity - }; - - var expected = AuthenticationResponse.CreateRequestDigest(copied); - return _parameters["response"] == expected; - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpHeaderInfo.cs b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpHeaderInfo.cs deleted file mode 100644 index e2a2788..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpHeaderInfo.cs +++ /dev/null @@ -1,74 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -namespace EonaCat.Network -{ - /// - /// Represents information about an HTTP header. - /// - internal class HttpHeaderInfo - { - /// - /// Initializes a new instance of the class. - /// - /// The name of the header. - /// The type of the header. - internal HttpHeaderInfo(string name, HttpHeaderType type) - { - Name = name; - Type = type; - } - - /// - /// Gets a value indicating whether the header is for a request. - /// - public bool IsRequest => (Type & HttpHeaderType.Request) == HttpHeaderType.Request; - - /// - /// Gets a value indicating whether the header is for a response. - /// - public bool IsResponse => (Type & HttpHeaderType.Response) == HttpHeaderType.Response; - - /// - /// Gets the name of the header. - /// - public string Name { get; } - - /// - /// Gets the type of the header. - /// - public HttpHeaderType Type { get; } - - /// - /// Gets a value indicating whether the header is multi-value in a request. - /// - internal bool IsMultiValueInRequest => (Type & HttpHeaderType.MultiValueInRequest) == HttpHeaderType.MultiValueInRequest; - - /// - /// Gets a value indicating whether the header is multi-value in a response. - /// - internal bool IsMultiValueInResponse => (Type & HttpHeaderType.MultiValueInResponse) == HttpHeaderType.MultiValueInResponse; - - /// - /// Gets a value indicating whether the header is multi-value. - /// - /// True if checking for a response; false for a request. - /// True if the header is multi-value, false otherwise. - public bool IsMultiValue(bool response) - { - return (Type & HttpHeaderType.MultiValue) == HttpHeaderType.MultiValue - ? (response ? IsResponse : IsRequest) - : (response ? IsMultiValueInResponse : IsMultiValueInRequest); - } - - /// - /// Gets a value indicating whether the header is restricted. - /// - /// True if checking for a response; false for a request. - /// True if the header is restricted, false otherwise. - public bool IsRestricted(bool response) - { - return (Type & HttpHeaderType.Restricted) == HttpHeaderType.Restricted && (response ? IsResponse : IsRequest); - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpHeaderType.cs b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpHeaderType.cs deleted file mode 100644 index 40b51d1..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpHeaderType.cs +++ /dev/null @@ -1,49 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; - -namespace EonaCat.Network -{ - /// - /// Represents the type of an HTTP header. - /// - [Flags] - internal enum HttpHeaderType - { - /// - /// No specific header type specified. - /// - Unspecified = 0, - - /// - /// Header used in an HTTP request. - /// - Request = 1, - - /// - /// Header used in an HTTP response. - /// - Response = 1 << 1, - - /// - /// Header with restricted use. - /// - Restricted = 1 << 2, - - /// - /// Header that can have multiple values. - /// - MultiValue = 1 << 3, - - /// - /// Multi-value header used in an HTTP request. - /// - MultiValueInRequest = 1 << 4, - - /// - /// Multi-value header used in an HTTP response. - /// - MultiValueInResponse = 1 << 5 - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListener.cs b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListener.cs deleted file mode 100644 index adc7e25..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListener.cs +++ /dev/null @@ -1,643 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Security.Principal; - -namespace EonaCat.Network -{ - /// - /// Represents an HTTP listener that listens for incoming requests. - /// - public sealed class HttpListener : IDisposable - { - private static readonly string _defaultRealm; - private readonly Dictionary _connections; - private readonly object _connectionsSync; - private readonly object _contextQueueLock; - private readonly Dictionary _contextRegistry; - private readonly object _contextRegistryLock; - private readonly HttpListenerPrefixCollection _prefixes; - private readonly List _waitQueue; - private readonly object _waitQueueLock; - private readonly List contextQueue; - private bool _allowForwardedRequest; - private AuthenticationSchemes _authSchemes; - private Func _authSchemeSelector; - private string _certFolderPath; - private bool _ignoreWriteExceptions; - private volatile bool _listening; - private string _realm; - private SSLConfigServer _sslConfig; - private Func _userCredFinder; - - static HttpListener() - { - _defaultRealm = "SECRET AREA"; - } - - /// - /// Initializes a new instance of the class. - /// - public HttpListener() - { - _authSchemes = AuthenticationSchemes.Anonymous; - - _connections = new Dictionary(); - _connectionsSync = ((ICollection)_connections).SyncRoot; - - contextQueue = new List(); - _contextQueueLock = ((ICollection)contextQueue).SyncRoot; - - _contextRegistry = new Dictionary(); - _contextRegistryLock = ((ICollection)_contextRegistry).SyncRoot; - - _prefixes = new HttpListenerPrefixCollection(this); - - _waitQueue = new List(); - _waitQueueLock = ((ICollection)_waitQueue).SyncRoot; - } - - public static bool IsSupported => true; - - /// - /// 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 { _allowForwardedRequest = value; } - } - - /// - /// Gets or sets the authentication schemes used by this listener. - /// - public AuthenticationSchemes AuthenticationSchemes - { - get - { - CheckDisposed(); - return _authSchemes; - } - - set - { - CheckDisposed(); - _authSchemes = value; - } - } - - public Func AuthenticationSchemeSelector - { - get - { - CheckDisposed(); - return _authSchemeSelector; - } - - set - { - CheckDisposed(); - _authSchemeSelector = value; - } - } - - public string CertificateFolderPath - { - get - { - CheckDisposed(); - return _certFolderPath; - } - - set - { - CheckDisposed(); - _certFolderPath = value; - } - } - - public bool IgnoreWriteExceptions - { - get - { - CheckDisposed(); - return _ignoreWriteExceptions; - } - - set - { - CheckDisposed(); - _ignoreWriteExceptions = value; - } - } - - public bool IsListening => _listening; - - public HttpListenerPrefixCollection Prefixes - { - get - { - CheckDisposed(); - return _prefixes; - } - } - - public string Realm - { - get - { - CheckDisposed(); - return _realm; - } - - set - { - CheckDisposed(); - _realm = value; - } - } - - public SSLConfigServer SSL - { - get - { - CheckDisposed(); - return _sslConfig ??= new SSLConfigServer(); - } - - set - { - CheckDisposed(); - _sslConfig = value; - } - } - - public bool UnsafeConnectionNtlmAuthentication - { - get - { - throw new NotSupportedException(); - } - - set - { - throw new NotSupportedException(); - } - } - - public Func UserCredentialsFinder - { - get - { - CheckDisposed(); - return _userCredFinder; - } - - set - { - CheckDisposed(); - _userCredFinder = value; - } - } - - internal bool IsDisposed { get; private set; } - - internal bool ReuseAddress { get; set; } - - /// - /// Aborts the listener and releases all resources associated with it. - /// - public void Abort() - { - if (IsDisposed) - { - return; - } - - close(true); - } - - /// - /// Begins asynchronously getting an HTTP context from the listener. - /// - /// The method to call when the operation completes. - /// A user-defined object that contains information about the asynchronous operation. - /// An that represents the asynchronous operation. - 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)); - } - - /// - /// Closes the listener. - /// - public void Close() - { - if (IsDisposed) - { - return; - } - - close(false); - } - - /// - /// Disposes of the resources used by the . - /// - void IDisposable.Dispose() - { - if (IsDisposed) - { - return; - } - - close(true); - } - - /// - /// Ends an asynchronous operation to get an HTTP context from the listener. - /// - /// The reference to the pending asynchronous request to finish. - /// An that represents the context of the asynchronous operation. - public HttpListenerContext EndGetContext(IAsyncResult asyncResult) - { - CheckDisposed(); - if (asyncResult == null) - { - throw new ArgumentNullException(nameof(asyncResult)); - } - - if (asyncResult is not HttpListenerAsyncResult result) - { - throw new ArgumentException("A wrong IAsyncResult.", nameof(asyncResult)); - } - - if (result.EndCalled) - { - throw new InvalidOperationException("This IAsyncResult cannot be reused."); - } - - result.EndCalled = true; - if (!result.IsCompleted) - { - result.AsyncWaitHandle.WaitOne(); - } - - return result.GetContext(); - } - - /// - /// Gets the next available HTTP context from the listener. - /// - /// An that represents the context of the HTTP request. - 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 result = BeginGetContext(new HttpListenerAsyncResult(null, null)); - result.InGet = true; - - return EndGetContext(result); - } - - /// - /// Starts listening for incoming requests. - /// - public void Start() - { - CheckDisposed(); - if (_listening) - { - return; - } - - EndPointManager.AddListener(this); - _listening = true; - } - - /// - /// Stops listening for incoming requests. - /// - public void Stop() - { - CheckDisposed(); - if (!_listening) - { - return; - } - - _listening = false; - EndPointManager.RemoveListener(this); - - lock (_contextRegistryLock) - { - CleanupContextQueue(true); - } - - CleanupContextRegistry(); - CleanupConnections(); - CleanupWaitQueue(new HttpListenerException(995, "The listener is closed.")); - } - - 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 (_contextRegistryLock) - { - if (!_listening) - { - throw new HttpListenerException(995); - } - - var context = GetContextFromQueue(); - if (context == null) - { - _waitQueue.Add(asyncResult); - } - else - { - asyncResult.Complete(context, true); - } - - return asyncResult; - } - } - - internal void CheckDisposed() - { - if (IsDisposed) - { - 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 (_contextRegistryLock) - { - if (!_listening) - { - return false; - } - - _contextRegistry[context] = context; - - var result = GetAsyncResultFromQueue(); - if (result == null) - { - contextQueue.Add(context); - } - else - { - result.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 (_contextRegistryLock) - { - _contextRegistry.Remove(context); - } - } - - private void CleanupConnections() - { - HttpConnection[] httpConnections = null; - lock (_connectionsSync) - { - if (_connections.Count == 0) - { - return; - } - - // Need to copy this since closing will call the RemoveConnection method. - var keys = _connections.Keys; - httpConnections = new HttpConnection[keys.Count]; - keys.CopyTo(httpConnections, 0); - _connections.Clear(); - } - - for (var i = httpConnections.Length - 1; i >= 0; i--) - { - httpConnections[i].Close(true); - } - } - - private void CleanupContextQueue(bool sendServiceUnavailable) - { - HttpListenerContext[] httpContextQueues = null; - lock (_contextQueueLock) - { - if (contextQueue.Count == 0) - { - return; - } - - httpContextQueues = contextQueue.ToArray(); - contextQueue.Clear(); - } - - if (!sendServiceUnavailable) - { - return; - } - - foreach (var currentContext in httpContextQueues) - { - var response = currentContext.Response; - response.StatusCode = (int)HttpStatusCode.ServiceUnavailable; - response.Close(); - } - } - - private void CleanupContextRegistry() - { - HttpListenerContext[] contexts = null; - lock (_contextRegistryLock) - { - if (_contextRegistry.Count == 0) - { - return; - } - - // Need to copy this since closing will call the UnregisterContext method. - var keys = _contextRegistry.Keys; - contexts = new HttpListenerContext[keys.Count]; - keys.CopyTo(contexts, 0); - _contextRegistry.Clear(); - } - - for (var i = contexts.Length - 1; i >= 0; i--) - { - contexts[i].Connection.Close(true); - } - } - - private void CleanupWaitQueue(Exception exception) - { - HttpListenerAsyncResult[] results = null; - lock (_waitQueueLock) - { - if (_waitQueue.Count == 0) - { - return; - } - - results = _waitQueue.ToArray(); - _waitQueue.Clear(); - } - - foreach (var currentResult in results) - { - currentResult.Complete(exception); - } - } - - private void close(bool force) - { - if (_listening) - { - _listening = false; - EndPointManager.RemoveListener(this); - } - - lock (_contextRegistryLock) - { - CleanupContextQueue(!force); - } - - CleanupContextRegistry(); - CleanupConnections(); - CleanupWaitQueue(new ObjectDisposedException(GetType().ToString())); - - IsDisposed = true; - } - - private HttpListenerAsyncResult GetAsyncResultFromQueue() - { - if (_waitQueue.Count == 0) - { - return null; - } - - var ares = _waitQueue[0]; - _waitQueue.RemoveAt(0); - - return ares; - } - - private HttpListenerContext GetContextFromQueue() - { - if (contextQueue.Count == 0) - { - return null; - } - - var context = contextQueue[0]; - contextQueue.RemoveAt(0); - - return context; - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerAsyncResult.cs b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerAsyncResult.cs deleted file mode 100644 index 0def279..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerAsyncResult.cs +++ /dev/null @@ -1,162 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.Threading; - -namespace EonaCat.Network -{ - /// - /// Represents the result of an asynchronous operation for an . - /// - internal class HttpListenerAsyncResult : IAsyncResult - { - private readonly AsyncCallback _callback; - private readonly object _locker; - private bool _completed; - private HttpListenerContext _context; - private Exception _exception; - private ManualResetEvent _waitHandle; - - /// - /// Initializes a new instance of the class. - /// - /// The method to call when the operation completes. - /// A user-defined object that contains information about the asynchronous operation. - internal HttpListenerAsyncResult(AsyncCallback callback, object state) - { - _callback = callback; - AsyncState = state; - _locker = new object(); - } - - /// - /// Gets the user-defined object that contains information about the asynchronous operation. - /// - public object AsyncState { get; } - - /// - /// Gets a that is used to wait for an asynchronous operation to complete. - /// - public WaitHandle AsyncWaitHandle - { - get - { - lock (_locker) - { - return _waitHandle ??= new ManualResetEvent(_completed); - } - } - } - - /// - /// Gets a value indicating whether the asynchronous operation completed synchronously. - /// - public bool CompletedSynchronously { get; private set; } - - /// - /// Gets a value indicating whether the asynchronous operation has completed. - /// - public bool IsCompleted - { - get - { - lock (_locker) - { - return _completed; - } - } - } - - /// - /// Gets or sets a value indicating whether the method has been called. - /// - internal bool EndCalled { get; set; } - - /// - /// Gets or sets a value indicating whether the asynchronous operation is in progress. - /// - internal bool InGet { get; set; } - - /// - /// Completes the asynchronous operation with the specified exception. - /// - /// The exception that occurred during the asynchronous operation. - internal void Complete(Exception exception) - { - _exception = InGet && (exception is ObjectDisposedException) - ? new HttpListenerException(995, "The listener is closed.") - : exception; - - complete(this); - } - - /// - /// Completes the asynchronous operation with the specified . - /// - /// The representing the result of the asynchronous operation. - internal void Complete(HttpListenerContext context) - { - Complete(context, false); - } - - /// - /// Completes the asynchronous operation with the specified . - /// - /// The representing the result of the asynchronous operation. - /// A value indicating whether the completion was synchronous. - internal void Complete(HttpListenerContext context, bool syncCompleted) - { - _context = context; - CompletedSynchronously = syncCompleted; - - complete(this); - } - - /// - /// Gets the result of the asynchronous operation. - /// - /// The representing the result of the asynchronous operation. - internal HttpListenerContext GetContext() - { - if (_exception != null) - { - throw _exception; - } - - return _context; - } - - // Private method to complete the asynchronous operation - private static void complete(HttpListenerAsyncResult asyncResult) - { - lock (asyncResult._locker) - { - asyncResult._completed = true; - - var waitHandle = asyncResult._waitHandle; - waitHandle?.Set(); - } - - var callback = asyncResult._callback; - if (callback == null) - { - return; - } - - ThreadPool.QueueUserWorkItem( - state => - { - try - { - callback(asyncResult); - } - catch - { - } - }, - null - ); - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerContext.cs b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerContext.cs deleted file mode 100644 index b725dee..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerContext.cs +++ /dev/null @@ -1,152 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.Security.Principal; - -namespace EonaCat.Network -{ - /// - /// Represents the context for an HTTP listener. - /// - public sealed class HttpListenerContext - { - private HttpListenerWSContext _websocketContext; - - /// - /// Initializes a new instance of the class. - /// - /// The underlying for the context. - internal HttpListenerContext(HttpConnection connection) - { - Connection = connection; - ErrorStatus = 400; - Request = new HttpListenerRequest(this); - Response = new HttpListenerResponse(this); - } - - /// - /// Gets the associated with the context. - /// - public HttpListenerRequest Request { get; } - - /// - /// Gets the associated with the context. - /// - public HttpListenerResponse Response { get; } - - /// - /// Gets or sets the associated with the user. - /// - public IPrincipal User { get; private set; } - - /// - /// Gets the underlying for the context. - /// - internal HttpConnection Connection { get; } - - /// - /// Gets or sets the error message associated with the context. - /// - internal string ErrorMessage { get; set; } - - /// - /// Gets or sets the error status associated with the context. - /// - internal int ErrorStatus { get; set; } - - /// - /// Gets a value indicating whether an error has occurred. - /// - internal bool HasError => ErrorMessage != null; - - /// - /// Gets or sets the associated with the context. - /// - internal HttpListener Listener { get; set; } - - /// - /// Accepts a WebSocket connection with the specified protocol. - /// - /// The WebSocket subprotocol to negotiate. - /// The for the WebSocket connection. - public HttpListenerWSContext AcceptWebSocket(string protocol) - { - if (_websocketContext != null) - { - throw new InvalidOperationException("Accepting already in progress."); - } - - if (protocol != null) - { - if (protocol.Length == 0) - { - throw new ArgumentException("Empty string.", nameof(protocol)); - } - - if (!protocol.IsToken()) - { - throw new ArgumentException("Contains invalid characters", nameof(protocol)); - } - } - - _websocketContext = new HttpListenerWSContext(this, protocol); - return _websocketContext; - } - - /// - /// Authenticates the user based on the specified authentication scheme. - /// - /// true if authentication is successful; otherwise, false. - 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; - } - - /// - /// Registers the context with the associated . - /// - /// true if registration is successful; otherwise, false. - internal bool Register() - { - return Listener.RegisterContext(this); - } - - /// - /// Unregisters the context from the associated . - /// - internal void Unregister() - { - Listener.UnregisterContext(this); - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerException.cs b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerException.cs deleted file mode 100644 index a4cd719..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerException.cs +++ /dev/null @@ -1,56 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.ComponentModel; -using System.Runtime.Serialization; - -namespace EonaCat.Network -{ - /// - /// Represents an exception that occurs in the class. - /// - [Serializable] - public class HttpListenerException : Win32Exception - { - /// - /// Initializes a new instance of the class with no error message. - /// - public HttpListenerException() - { - } - - /// - /// Initializes a new instance of the class with the specified error code. - /// - /// The Win32 error code associated with this exception. - public HttpListenerException(int errorCode) - : base(errorCode) - { - } - - /// - /// Initializes a new instance of the class with the specified error code and message. - /// - /// The Win32 error code associated with this exception. - /// The error message that explains the reason for the exception. - public HttpListenerException(int errorCode, string message) - : base(errorCode, $"EonaCat Network: {message}") - { - } - - /// - /// Initializes a new instance of the class. - /// - protected HttpListenerException( - SerializationInfo serializationInfo, StreamingContext streamingContext) - : base(serializationInfo, streamingContext) - { - } - - /// - /// Gets the Win32 error code associated with this exception. - /// - public override int ErrorCode => NativeErrorCode; - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerPrefix.cs b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerPrefix.cs deleted file mode 100644 index 1270378..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerPrefix.cs +++ /dev/null @@ -1,167 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; - -namespace EonaCat.Network -{ - /// - /// Represents a URI prefix for an . - /// - internal sealed class HttpListenerPrefix - { - private string _prefix; - - /// - /// Initializes a new instance of the class with the specified URI prefix. - /// - /// The URI prefix. - internal HttpListenerPrefix(string uriPrefix) - { - Original = uriPrefix; - parse(uriPrefix); - } - - /// - /// Gets the host specified in the URI prefix. - /// - public string Host { get; private set; } - - /// - /// Gets a value indicating whether the URI prefix is for a secure connection (https). - /// - public bool IsSecure { get; private set; } - - /// - /// Gets or sets the associated . - /// - public HttpListener Listener { get; set; } - - /// - /// Gets the original URI prefix. - /// - public string Original { get; } - - /// - /// Gets the path specified in the URI prefix. - /// - public string Path { get; private set; } - - /// - /// Gets the port specified in the URI prefix. - /// - public string Port { get; private set; } - - /// - /// Checks if the specified URI prefix is valid. - /// - /// The URI prefix to check. - public static void CheckPrefix(string uriPrefix) - { - if (uriPrefix == null) - { - throw new ArgumentNullException(nameof(uriPrefix)); - } - - var len = uriPrefix.Length; - if (len == 0) - { - throw new ArgumentException("An empty string.", nameof(uriPrefix)); - } - - if (!(uriPrefix.StartsWith("http://") || uriPrefix.StartsWith("https://"))) - { - throw new ArgumentException("The scheme isn't 'http' or 'https'.", nameof(uriPrefix)); - } - - var startHost = uriPrefix.IndexOf(':') + 3; - if (startHost >= len) - { - throw new ArgumentException("No host is specified.", nameof(uriPrefix)); - } - - if (uriPrefix[startHost] == ':') - { - throw new ArgumentException("No host is specified.", nameof(uriPrefix)); - } - - var root = uriPrefix.IndexOf('/', startHost, len - startHost); - if (root == startHost) - { - throw new ArgumentException("No host is specified.", nameof(uriPrefix)); - } - - if (root == -1 || uriPrefix[len - 1] != '/') - { - throw new ArgumentException("Ends without '/'.", nameof(uriPrefix)); - } - - if (uriPrefix[root - 1] == ':') - { - throw new ArgumentException("No port is specified.", nameof(uriPrefix)); - } - - if (root == len - 2) - { - throw new ArgumentException("No path is specified.", nameof(uriPrefix)); - } - } - - /// - /// Determines whether the specified object is equal to the current object. - /// - /// The object to compare with the current object. - /// true if the specified object is equal to the current object; otherwise, false. - public override bool Equals(object obj) - { - return obj is HttpListenerPrefix pref && pref._prefix == _prefix; - } - - /// - /// Serves as the default hash function. - /// - /// A hash code for the current object. - public override int GetHashCode() - { - return _prefix.GetHashCode(); - } - - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. - public override string ToString() - { - return _prefix; - } - - private void parse(string uriPrefix) - { - if (uriPrefix.StartsWith("https")) - { - IsSecure = 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 = IsSecure ? "443" : "80"; - } - - Path = uriPrefix.Substring(root); - - _prefix = - string.Format("http{0}://{1}:{2}{3}", IsSecure ? "s" : "", Host, Port, Path); - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerPrefixCollection.cs b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerPrefixCollection.cs deleted file mode 100644 index 5204118..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerPrefixCollection.cs +++ /dev/null @@ -1,154 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.Collections; -using System.Collections.Generic; - -namespace EonaCat.Network -{ - /// - /// Represents a collection of URI prefixes for an . - /// - public class HttpListenerPrefixCollection : ICollection, IEnumerable, IEnumerable - { - private readonly HttpListener _listener; - private readonly List _prefixes; - - /// - /// Initializes a new instance of the class. - /// - /// The associated . - internal HttpListenerPrefixCollection(HttpListener listener) - { - _listener = listener; - _prefixes = new List(); - } - - /// - /// Gets the number of URI prefixes in the collection. - /// - public int Count => _prefixes.Count; - - /// - /// Gets a value indicating whether the collection is read-only. - /// - public bool IsReadOnly => false; - - /// - /// Gets a value indicating whether access to the collection is synchronized (thread-safe). - /// - public bool IsSynchronized => false; - - /// - /// Adds a URI prefix to the collection. - /// - /// The URI prefix to add. - 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. - /// - public void Clear() - { - _listener.CheckDisposed(); - _prefixes.Clear(); - if (_listener.IsListening) - { - EndPointManager.RemoveListener(_listener); - } - } - - /// - /// Determines whether the collection contains a specific URI prefix. - /// - /// The URI prefix to locate. - /// true if the collection contains the specified URI prefix; otherwise, false. - public bool Contains(string uriPrefix) - { - _listener.CheckDisposed(); - if (uriPrefix == null) - { - throw new ArgumentNullException(nameof(uriPrefix)); - } - - return _prefixes.Contains(uriPrefix); - } - - /// - /// Copies the URI prefixes to an array, starting at a particular array index. - /// - /// The one-dimensional array that is the destination of the elements copied from the collection. - /// The zero-based index in the array at which copying begins. - public void CopyTo(Array array, int offset) - { - _listener.CheckDisposed(); - ((ICollection)_prefixes).CopyTo(array, offset); - } - - /// - /// Copies the URI prefixes to an array, starting at a particular array index. - /// - /// The one-dimensional array that is the destination of the elements copied from the collection. - /// The zero-based index in the array at which copying begins. - public void CopyTo(string[] array, int offset) - { - _listener.CheckDisposed(); - _prefixes.CopyTo(array, offset); - } - - /// - /// Returns an enumerator that iterates through the collection. - /// - /// An enumerator that can be used to iterate through the collection. - public IEnumerator GetEnumerator() - { - return _prefixes.GetEnumerator(); - } - - /// - /// Returns an enumerator that iterates through the collection. - /// - /// An enumerator that can be used to iterate through the collection. - IEnumerator IEnumerable.GetEnumerator() - { - return _prefixes.GetEnumerator(); - } - - /// - /// Removes the specified URI prefix from the collection. - /// - /// The URI prefix to remove. - /// true if the URI prefix is successfully removed; otherwise, false. This method also returns false if the URI prefix was not found in the collection. - public bool Remove(string uriPrefix) - { - _listener.CheckDisposed(); - if (uriPrefix == null) - { - throw new ArgumentNullException(nameof(uriPrefix)); - } - - var ret = _prefixes.Remove(uriPrefix); - if (ret && _listener.IsListening) - { - EndPointManager.RemovePrefix(uriPrefix, _listener); - } - - return ret; - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerRequest.cs b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerRequest.cs deleted file mode 100644 index 78f691d..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerRequest.cs +++ /dev/null @@ -1,415 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Globalization; -using System.IO; -using System.Text; - -namespace EonaCat.Network -{ - /// - /// Represents an HTTP listener request. - /// - public sealed class HttpListenerRequest - { - private static readonly byte[] _100continue; - private readonly HttpListenerContext _context; - private readonly WebHeaderCollection _headers; - private bool _chunked; - private Encoding _contentEncoding; - private bool _contentLengthSet; - private CookieCollection _cookies; - private Stream _inputStream; - private bool _keepAlive; - private bool _keepAliveSet; - private NameValueCollection _queryString; - private string _uri; - private Version _version; - private bool _websocketRequest; - private bool _websocketRequestSet; - - static HttpListenerRequest() - { - _100continue = Encoding.ASCII.GetBytes("HTTP/1.1 100 Continue\r\n\r\n"); - } - - internal HttpListenerRequest(HttpListenerContext context) - { - _context = context; - ContentLength64 = -1; - _headers = new WebHeaderCollection(); - RequestTraceIdentifier = Guid.NewGuid(); - } - - /// - /// Gets the accepted types for the request. - /// - public string[] AcceptTypes { get; private set; } - - /// - /// Gets the content encoding of the request. - /// - public Encoding ContentEncoding => _contentEncoding ??= Encoding.Default; - - /// - /// Gets the content length of the request. - /// - public long ContentLength64 { get; private set; } - - /// - /// Gets the content type of the request. - /// - public string ContentType => _headers["Content-Type"]; - - /// - /// Gets the collection of cookies in the request. - /// - public CookieCollection Cookies => _cookies ??= _headers.GetCookies(false); - - /// - /// Gets a value indicating whether the request has an entity body. - /// - public bool HasEntityBody => ContentLength64 > 0 || _chunked; - - /// - /// Gets the collection of headers in the request. - /// - public NameValueCollection Headers => _headers; - - /// - /// Gets the HTTP method of the request. - /// - public string HttpMethod { get; private set; } - - /// - /// Gets the input stream of the request. - /// - public Stream InputStream => _inputStream ??= HasEntityBody - ? _context.Connection.GetRequestStream(ContentLength64, _chunked) - : Stream.Null; - - /// - /// Gets a value indicating whether the user is authenticated. - /// - public bool IsAuthenticated => _context.User != null; - - /// - /// Gets a value indicating whether the request is from a local address. - /// - public bool IsLocal => RemoteEndPoint.Address.IsLocal(); - - /// - /// Gets a value indicating whether the connection is secure. - /// - public bool IsSecureConnection => _context.Connection.IsSecure; - - /// - /// Gets a value indicating whether the request is a WebSocket request. - /// - public bool IsWebSocketRequest - { - get - { - if (!_websocketRequestSet) - { - _websocketRequest = HttpMethod == "GET" && - _version > HttpVersion.Version10 && - _headers.Contains("Upgrade", "websocket") && - _headers.Contains("Connection", "Upgrade"); - - _websocketRequestSet = true; - } - - return _websocketRequest; - } - } - - /// - /// Gets a value indicating whether the connection should be kept alive. - /// - public bool KeepAlive - { - get - { - if (!_keepAliveSet) - { - string keepAlive; - _keepAlive = _version > HttpVersion.Version10 || - _headers.Contains("Connection", "keep-alive") || - ((keepAlive = _headers["Keep-Alive"]) != null && keepAlive != "closed"); - - _keepAliveSet = true; - } - - return _keepAlive; - } - } - - /// - /// Gets the local endpoint of the connection. - /// - public System.Net.IPEndPoint LocalEndPoint => _context.Connection.LocalEndPoint; - - /// - /// Gets the protocol version of the request. - /// - public Version ProtocolVersion => _version; - - /// - /// Gets the query string in the request. - /// - public NameValueCollection QueryString => _queryString ??= HttpUtility.InternalParseQueryString(Url.Query, ContentEncoding); - - /// - /// Gets the raw URL of the request. - /// - public string RawUrl => Url.PathAndQuery; - - /// - /// Gets the remote endpoint of the connection. - /// - public System.Net.IPEndPoint RemoteEndPoint => _context.Connection.RemoteEndPoint; - - /// - /// Gets the trace identifier for the request. - /// - public Guid RequestTraceIdentifier { get; } - - /// - /// Gets the URL of the request. - /// - public Uri Url { get; private set; } - - /// - /// Gets the URL referrer of the request. - /// - public Uri UrlReferrer { get; private set; } - - /// - /// Gets the user agent of the request. - /// - public string UserAgent => _headers["User-Agent"]; - - /// - /// Gets the user host address of the request. - /// - public string UserHostAddress => LocalEndPoint.ToString(); - - /// - /// Gets the user host name of the request. - /// - public string UserHostName => _headers["Host"]; - - /// - /// Gets the user languages of the request. - /// - public string[] UserLanguages { get; private set; } - - public override string ToString() - { - var buff = new StringBuilder(64); - buff.AppendFormat("{0} {1} HTTP/{2}\r\n", HttpMethod, _uri, _version); - buff.Append(_headers.ToString()); - - return buff.ToString(); - } - - internal void AddHeader(string header) - { - var colon = header.IndexOf(':'); - if (colon == -1) - { - _context.ErrorMessage = "Invalid header"; - return; - } - - var name = header.Substring(0, colon).Trim(); - var val = header.Substring(colon + 1).Trim(); - _headers.InternalSet(name, val, false); - - var lower = name.ToLower(CultureInfo.InvariantCulture); - if (lower == "accept") - { - AcceptTypes = new List(val.SplitHeaderValue(',')).ToArray(); - return; - } - - if (lower == "accept-language") - { - UserLanguages = val.Split(','); - return; - } - - if (lower == "content-length") - { - if (long.TryParse(val, out long len) && len >= 0) - { - ContentLength64 = len; - _contentLengthSet = true; - } - else - { - _context.ErrorMessage = "Invalid Content-Length header"; - } - - return; - } - - if (lower == "content-type") - { - try - { - _contentEncoding = HttpUtility.GetEncoding(val); - } - catch - { - _context.ErrorMessage = "Invalid Content-Type header"; - } - - return; - } - - if (lower == "referer") - { - UrlReferrer = val.ToUri(); - } - } - - internal void FinishInitialization() - { - var host = _headers["Host"]; - var nohost = host == null || host.Length == 0; - if (_version > HttpVersion.Version10 && nohost) - { - _context.ErrorMessage = "Invalid Host header"; - return; - } - - if (nohost) - { - host = UserHostAddress; - } - - Url = HttpUtility.CreateRequestUrl(_uri, host, IsWebSocketRequest, IsSecureConnection); - if (Url == null) - { - _context.ErrorMessage = "Invalid request url"; - return; - } - - var enc = Headers["Transfer-Encoding"]; - if (_version > HttpVersion.Version10 && enc != null && enc.Length > 0) - { - _chunked = enc.ToLower() == "chunked"; - if (!_chunked) - { - _context.ErrorMessage = string.Empty; - _context.ErrorStatus = 501; - - return; - } - } - - if (!_chunked && !_contentLengthSet) - { - var method = HttpMethod.ToLower(); - if (method == "post" || method == "put") - { - _context.ErrorMessage = string.Empty; - _context.ErrorStatus = 411; - - return; - } - } - - var expect = Headers["Expect"]; - if (expect != null && expect.Length > 0 && expect.ToLower() == "100-continue") - { - var output = _context.Connection.GetResponseStream(); - output.InternalWrite(_100continue, 0, _100continue.Length); - } - } - - // Returns true is the stream could be reused. - internal bool FlushInput() - { - if (!HasEntityBody) - { - return true; - } - - var len = 2048; - if (ContentLength64 > 0) - { - len = (int)Math.Min(ContentLength64, len); - } - - var buff = new byte[len]; - while (true) - { - try - { - var result = InputStream.BeginRead(buff, 0, len, null, null); - if (!result.IsCompleted && !result.AsyncWaitHandle.WaitOne(100)) - { - return false; - } - - if (InputStream.EndRead(result) <= 0) - { - return true; - } - } - catch - { - return false; - } - } - } - - internal void SetRequestLine(string requestLine) - { - var parts = requestLine.Split(new[] { ' ' }, 3); - if (parts.Length != 3) - { - _context.ErrorMessage = "Invalid request line (parts)"; - return; - } - - HttpMethod = parts[0]; - if (!HttpMethod.IsToken()) - { - _context.ErrorMessage = "Invalid request line (method)"; - return; - } - - _uri = parts[1]; - - var ver = parts[2]; - if (ver.Length != 8 || - !ver.StartsWith("HTTP/") || - !tryCreateVersion(ver.Substring(5), out _version) || - _version.Major < 1) - { - _context.ErrorMessage = "Invalid request line (version)"; - } - } - - private static bool tryCreateVersion(string version, out Version result) - { - try - { - result = new Version(version); - return true; - } - catch - { - result = null; - return false; - } - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerResponse.cs b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerResponse.cs deleted file mode 100644 index 3f97e31..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerResponse.cs +++ /dev/null @@ -1,647 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Text; - -namespace EonaCat.Network -{ - /// - /// Represents an HTTP listener response. - /// - public sealed class HttpListenerResponse : IDisposable - { - private readonly HttpListenerContext _context; - private Encoding _contentEncoding; - private long _contentLength; - private string _contentType; - private CookieCollection _cookies; - private bool _disposed; - private WebHeaderCollection _headers; - private bool _keepAlive; - private string _location; - private ResponseStream _outputStream; - private bool _sendInChunks; - private int _statusCode; - private string _statusDescription; - private Version _version; - - /// - /// Initializes a new instance of the class. - /// - /// The HTTP listener context associated with the response. - internal HttpListenerResponse(HttpListenerContext context) - { - _context = context; - _keepAlive = true; - _statusCode = 200; - _statusDescription = "OK"; - _version = HttpVersion.Version11; - } - - /// - /// Gets or sets the encoding for the response content. - /// - public Encoding ContentEncoding - { - get - { - return _contentEncoding; - } - - set - { - checkDisposed(); - _contentEncoding = value; - } - } - - /// - /// Gets or sets the content length of the response content. - /// - public long ContentLength64 - { - get - { - return _contentLength; - } - - set - { - checkDisposedOrHeadersSent(); - if (value < 0) - { - throw new ArgumentOutOfRangeException("Less than zero.", "value"); - } - - _contentLength = value; - } - } - - /// - /// Gets or sets the content type of the response content. - /// - public string ContentType - { - get - { - return _contentType; - } - - set - { - checkDisposed(); - if (value != null && value.Length == 0) - { - throw new ArgumentException("An empty string.", nameof(value)); - } - - _contentType = value; - } - } - - /// - /// Gets or sets the collection of cookies in the response. - /// - public CookieCollection Cookies - { - get - { - return _cookies ??= new CookieCollection(); - } - - set - { - _cookies = value; - } - } - - /// - /// Gets or sets the collection of headers in the response. - /// - public WebHeaderCollection Headers - { - get - { - return _headers ??= new WebHeaderCollection(HttpHeaderType.Response, false); - } - - set - { - if (value != null && value.State != HttpHeaderType.Response) - { - throw new InvalidOperationException( - "The specified headers aren't valid for a response."); - } - - _headers = value; - } - } - - /// - /// Gets or sets a value indicating whether the connection should be kept alive. - /// - public bool KeepAlive - { - get - { - return _keepAlive; - } - - set - { - checkDisposedOrHeadersSent(); - _keepAlive = value; - } - } - - /// - /// Gets the output stream for writing the response content. - /// - public Stream OutputStream - { - get - { - checkDisposed(); - return _outputStream ??= _context.Connection.GetResponseStream(); - } - } - - /// - /// Gets or sets the HTTP protocol version of the response. - /// - public Version ProtocolVersion - { - get - { - return _version; - } - - set - { - checkDisposedOrHeadersSent(); - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - - if (value.Major != 1 || (value.Minor != 0 && value.Minor != 1)) - { - throw new ArgumentException("Not 1.0 or 1.1.", nameof(value)); - } - - _version = value; - } - } - - /// - /// Gets or sets the redirect location for the response. - /// - public string RedirectLocation - { - get - { - return _location; - } - - set - { - checkDisposed(); - if (value == null) - { - _location = null; - return; - } - - if (!value.MaybeUri() || !Uri.TryCreate(value, UriKind.Absolute, out Uri uri)) - { - throw new ArgumentException("Not an absolute URL.", nameof(value)); - } - - _location = value; - } - } - - /// - /// Gets or sets a value indicating whether the response should be sent in chunks. - /// - public bool SendInChunks - { - get - { - return _sendInChunks; - } - - set - { - checkDisposedOrHeadersSent(); - _sendInChunks = value; - } - } - - /// - /// Gets or sets the HTTP status code for the response. - /// - public int StatusCode - { - get - { - return _statusCode; - } - - set - { - checkDisposedOrHeadersSent(); - if (value < 100 || value > 999) - { - throw new System.Net.ProtocolViolationException( - "A value isn't between 100 and 999 inclusive."); - } - - _statusCode = value; - _statusDescription = value.GetStatusDescription(); - } - } - - /// - /// Gets or sets the status description for the response. - /// - public string StatusDescription - { - get - { - return _statusDescription; - } - - set - { - checkDisposedOrHeadersSent(); - if (value == null || value.Length == 0) - { - _statusDescription = _statusCode.GetStatusDescription(); - return; - } - - if (!value.IsText() || value.IndexOfAny(new[] { '\r', '\n' }) > -1) - { - throw new ArgumentException("Contains invalid characters.", nameof(value)); - } - - _statusDescription = value; - } - } - - internal bool CloseConnection { get; set; } - - internal bool HeadersSent { get; set; } - - /// - /// Aborts the response. - /// - public void Abort() - { - if (_disposed) - { - return; - } - - close(true); - } - - /// - /// Appends a header to the response. - /// - /// The name of the header. - /// The value of the header. - public void AddHeader(string name, string value) - { - Headers.Set(name, value); - } - - /// - /// Appends a cookie to the response. - /// - /// The cookie to append. - public void AppendCookie(Cookie cookie) - { - Cookies.Add(cookie); - } - - /// - /// Appends a header to the response. - /// - /// The name of the header. - /// The value of the header. - public void AppendHeader(string name, string value) - { - Headers.Add(name, value); - } - - /// - /// Closes the response. - /// - public void Close() - { - if (_disposed) - { - return; - } - - close(false); - } - - /// - /// Closes the response with the specified response entity and blocking behavior. - /// - /// The response entity. - /// A value indicating whether the operation will block. - public void Close(byte[] responseEntity, bool willBlock) - { - checkDisposed(); - if (responseEntity == null) - { - throw new ArgumentNullException(nameof(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 headers and other properties from a template response. - /// - /// The template response to copy from. - public void CopyFrom(HttpListenerResponse templateResponse) - { - if (templateResponse == null) - { - throw new ArgumentNullException(nameof(templateResponse)); - } - - if (templateResponse._headers != null) - { - _headers?.Clear(); - - Headers.Add(templateResponse._headers); - } - else if (_headers != null) - { - _headers = null; - } - - _contentLength = templateResponse._contentLength; - _statusCode = templateResponse._statusCode; - _statusDescription = templateResponse._statusDescription; - _keepAlive = templateResponse._keepAlive; - _version = templateResponse._version; - } - - /// - void IDisposable.Dispose() - { - if (_disposed) - { - return; - } - - close(true); - } - - /// - /// Redirects the response to the specified URL. - /// - /// The URL to redirect to. - public void Redirect(string url) - { - checkDisposedOrHeadersSent(); - if (url == null) - { - throw new ArgumentNullException(nameof(url)); - } - - if (!url.MaybeUri() || !Uri.TryCreate(url, UriKind.Absolute, out Uri uri)) - { - throw new ArgumentException("Not an absolute URL.", nameof(url)); - } - - _location = url; - _statusCode = 302; - _statusDescription = "Found"; - } - - /// - /// Sets a cookie in the response. - /// - /// The cookie to set. - public void SetCookie(Cookie cookie) - { - if (cookie == null) - { - throw new ArgumentNullException(nameof(cookie)); - } - - if (!canAddOrUpdate(cookie)) - { - throw new ArgumentException("Cannot be replaced.", nameof(cookie)); - } - - Cookies.Add(cookie); - } - - /// - /// Writes headers to the specified destination. - /// - /// The destination stream to write headers to. - /// The collection of headers written. - internal WebHeaderCollection WriteHeadersTo(MemoryStream destination) - { - var headers = new WebHeaderCollection(HttpHeaderType.Response, true); - if (_headers != null) - { - headers.Add(_headers); - } - - if (_contentType != null) - { - var type = _contentType.IndexOf("charset=", StringComparison.Ordinal) == -1 && - _contentEncoding != null - ? string.Format("{0}; charset={1}", _contentType, _contentEncoding.WebName) - : _contentType; - - headers.InternalSet("Content-Type", type, true); - } - - if (headers["Server"] == null) - { - headers.InternalSet("Server", $"EonaCat.Network/{Constants.Version}", true); - } - - var prov = CultureInfo.InvariantCulture; - if (headers["Date"] == null) - { - headers.InternalSet("Date", DateTime.UtcNow.ToString("r", prov), true); - } - - if (!_sendInChunks) - { - headers.InternalSet("Content-Length", _contentLength.ToString(prov), true); - } - else - { - headers.InternalSet("Transfer-Encoding", "chunked", true); - } - - /* - * Apache forces closing the connection for these status codes: - * - 400 Bad Request - * - 408 Request Timeout - * - 411 Length Required - * - 413 Request Entity Too Large - * - 414 Request-Uri Too Long - * - 500 Internal Server Error - * - 503 Service Unavailable - */ - var closeConn = !_context.Request.KeepAlive || - !_keepAlive || - _statusCode == 400 || - _statusCode == 408 || - _statusCode == 411 || - _statusCode == 413 || - _statusCode == 414 || - _statusCode == 500 || - _statusCode == 503; - - var reuses = _context.Connection.Reuses; - if (closeConn || reuses >= 100) - { - headers.InternalSet("Connection", "close", true); - } - else - { - headers.InternalSet( - "Keep-Alive", string.Format("timeout=15,max={0}", 100 - reuses), true); - - if (_context.Request.ProtocolVersion < HttpVersion.Version11) - { - headers.InternalSet("Connection", "keep-alive", true); - } - } - - if (_location != null) - { - headers.InternalSet("Location", _location, true); - } - - if (_cookies != null) - { - foreach (Cookie cookie in _cookies) - { - headers.InternalSet("Set-Cookie", cookie.ToResponseString(), true); - } - } - - var enc = _contentEncoding ?? Encoding.Default; - var writer = new StreamWriter(destination, enc, 256); - writer.Write("HTTP/{0} {1} {2}\r\n", _version, _statusCode, _statusDescription); - writer.Write(headers.ToStringMultiValue(true)); - writer.Flush(); - - // Assumes that the destination was at position 0. - destination.Position = enc.GetPreamble().Length; - - return headers; - } - - private bool canAddOrUpdate(Cookie cookie) - { - if (_cookies == null || _cookies.Count == 0) - { - return true; - } - - 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 checkDisposed() - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().ToString()); - } - } - - private void checkDisposedOrHeadersSent() - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().ToString()); - } - - if (HeadersSent) - { - throw new InvalidOperationException("Cannot be changed after the headers are sent."); - } - } - - private void close(bool force) - { - _disposed = true; - _context.Connection.Close(force); - } - - private IEnumerable findCookie(Cookie cookie) - { - var name = cookie.Name; - var domain = cookie.Domain; - var path = cookie.Path; - if (_cookies != null) - { - foreach (Cookie c in _cookies) - { - if (c.Name.Equals(name, StringComparison.OrdinalIgnoreCase) && - c.Domain.Equals(domain, StringComparison.OrdinalIgnoreCase) && - c.Path.Equals(path, StringComparison.Ordinal)) - { - yield return c; - } - } - } - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpRequestHeader.cs b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpRequestHeader.cs deleted file mode 100644 index d80d379..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpRequestHeader.cs +++ /dev/null @@ -1,101 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -namespace EonaCat.Network -{ - /// - /// Represents HTTP request headers. - /// - public enum HttpRequestHeader - { - CacheControl, - - Connection, - - Date, - - KeepAlive, - - Pragma, - - Trailer, - - TransferEncoding, - - Upgrade, - - Via, - - Warning, - - Allow, - - ContentLength, - - ContentType, - - ContentEncoding, - - ContentLanguage, - - ContentLocation, - - ContentMd5, - - ContentRange, - - Expires, - - LastModified, - - Accept, - - AcceptCharset, - - AcceptEncoding, - - AcceptLanguage, - - Authorization, - - Cookie, - - Expect, - - From, - - Host, - - IfMatch, - - IfModifiedSince, - - IfNoneMatch, - - IfRange, - - IfUnmodifiedSince, - - MaxForwards, - - ProxyAuthorization, - - Referer, - - Range, - - Te, - - Translate, - - UserAgent, - - SecWebSocketKey, - - SecWebSocketExtensions, - - SecWebSocketProtocol, - - SecWebSocketVersion - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpResponseHeader.cs b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpResponseHeader.cs deleted file mode 100644 index a17222a..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpResponseHeader.cs +++ /dev/null @@ -1,79 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -namespace EonaCat.Network -{ - /// - /// Represents HTTP response headers. - /// - public enum HttpResponseHeader - { - CacheControl, - - Connection, - - Date, - - KeepAlive, - - Pragma, - - Trailer, - - TransferEncoding, - - Upgrade, - - Via, - - Warning, - - Allow, - - ContentLength, - - ContentType, - - ContentEncoding, - - ContentLanguage, - - ContentLocation, - - ContentMd5, - - ContentRange, - - Expires, - - LastModified, - - AcceptRanges, - - Age, - - ETag, - - Location, - - ProxyAuthenticate, - - RetryAfter, - - Server, - - SetCookie, - - Vary, - - WwwAuthenticate, - - SecWebSocketExtensions, - - SecWebSocketAccept, - - SecWebSocketProtocol, - - SecWebSocketVersion - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpStatusCode.cs b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpStatusCode.cs deleted file mode 100644 index fa19756..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpStatusCode.cs +++ /dev/null @@ -1,103 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -namespace EonaCat.Network -{ - /// - /// Represents HTTP status codes. - /// - public enum HttpStatusCode - { - Continue = 100, - - SwitchingProtocols = 101, - - OK = 200, - - Created = 201, - - Accepted = 202, - - NonAuthoritativeInformation = 203, - - NoContent = 204, - - ResetContent = 205, - - PartialContent = 206, - - MultipleChoices = 300, - - Ambiguous = 300, - - MovedPermanently = 301, - - Moved = 301, - - Found = 302, - - Redirect = 302, - - SeeOther = 303, - - RedirectMethod = 303, - - NotModified = 304, - - UseProxy = 305, - - Unused = 306, - - TemporaryRedirect = 307, - - RedirectKeepVerb = 307, - - BadRequest = 400, - - Unauthorized = 401, - - PaymentRequired = 402, - - Forbidden = 403, - - NotFound = 404, - - MethodNotAllowed = 405, - - NotAcceptable = 406, - - ProxyAuthenticationRequired = 407, - - RequestTimeout = 408, - - Conflict = 409, - - Gone = 410, - - LengthRequired = 411, - - PreconditionFailed = 412, - - RequestEntityTooLarge = 413, - - RequestUriTooLong = 414, - - UnsupportedMediaType = 415, - - RequestedRangeNotSatisfiable = 416, - - ExpectationFailed = 417, - - InternalServerError = 500, - - NotImplemented = 501, - - BadGateway = 502, - - ServiceUnavailable = 503, - - GatewayTimeout = 504, - - HttpVersionNotSupported = 505, - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpStreamAsyncResult.cs b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpStreamAsyncResult.cs deleted file mode 100644 index 3f726bf..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpStreamAsyncResult.cs +++ /dev/null @@ -1,84 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.Threading; - -namespace EonaCat.Network -{ - internal class HttpStreamAsyncResult : IAsyncResult - { - private readonly AsyncCallback _callback; - private readonly object _locker; - private bool _isCompleted; - private ManualResetEvent _waitHandle; - - internal HttpStreamAsyncResult(AsyncCallback callback, object state) - { - _callback = callback; - AsyncState = state; - _locker = new object(); - } - - public object AsyncState { get; } - - public WaitHandle AsyncWaitHandle - { - get - { - lock (_locker) - { - return _waitHandle ??= new ManualResetEvent(_isCompleted); - } - } - } - - public bool CompletedSynchronously => SyncRead == Count; - - public bool IsCompleted - { - get - { - lock (_locker) - { - return _isCompleted; - } - } - } - - internal byte[] Buffer { get; set; } - - internal int Count { get; set; } - - internal Exception Exception { get; private set; } - - internal bool HasException => Exception != null; - - internal int Offset { get; set; } - - internal int SyncRead { get; set; } - - internal void Complete() - { - lock (_locker) - { - if (_isCompleted) - { - return; - } - - _isCompleted = true; - - _waitHandle?.Set(); - - _callback?.Invoke(this); - } - } - - internal void Complete(Exception exception) - { - Exception = exception; - Complete(); - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpUtility.cs b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpUtility.cs deleted file mode 100644 index 7da87d7..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpUtility.cs +++ /dev/null @@ -1,1363 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -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 EonaCat.Network -{ - internal sealed class HttpUtility - { - private static readonly char[] _hexChars = "0123456789abcdef".ToCharArray(); - private static readonly object _locker = new object(); - private static Dictionary _entities; - - public static string HtmlAttributeEncode(string s) - { - if (s == null || s.Length == 0 || !s.Contains('&', '"', '<', '>')) - { - return s; - } - - var output = new StringBuilder(); - foreach (var c in s) - { - output.Append( - c == '&' - ? "&" - : c == '"' - ? """ - : c == '<' - ? "<" - : c == '>' - ? ">" - : c.ToString()); - } - - return output.ToString(); - } - - public static void HtmlAttributeEncode(string s, TextWriter output) - { - if (output == null) - { - throw new ArgumentNullException(nameof(output)); - } - - output.Write(HtmlAttributeEncode(s)); - } - - public static string HtmlDecode(string s) - { - if (s == null || s.Length == 0 || !s.Contains('&')) - { - return s; - } - - var entity = new StringBuilder(); - var output = new StringBuilder(); - - // 0 -> nothing, - // 1 -> right after '&' - // 2 -> between '&' and ';' but no '#' - // 3 -> '#' found after '&' and getting numbers - var state = 0; - - var number = 0; - var haveTrailingDigits = false; - foreach (var c in s) - { - if (state == 0) - { - if (c == '&') - { - entity.Append(c); - state = 1; - } - else - { - output.Append(c); - } - - continue; - } - - if (c == '&') - { - state = 1; - if (haveTrailingDigits) - { - entity.Append(number.ToString(CultureInfo.InvariantCulture)); - haveTrailingDigits = false; - } - - output.Append(entity.ToString()); - entity.Length = 0; - entity.Append('&'); - - continue; - } - - if (state == 1) - { - if (c == ';') - { - state = 0; - output.Append(entity.ToString()); - output.Append(c); - entity.Length = 0; - } - else - { - number = 0; - if (c != '#') - { - state = 2; - } - else - { - state = 3; - } - - entity.Append(c); - } - } - else if (state == 2) - { - entity.Append(c); - if (c == ';') - { - var key = entity.ToString(); - var entities = getEntities(); - if (key.Length > 1 && entities.ContainsKey(key.Substring(1, key.Length - 2))) - { - key = entities[key.Substring(1, key.Length - 2)].ToString(); - } - - output.Append(key); - state = 0; - entity.Length = 0; - } - } - else if (state == 3) - { - if (c == ';') - { - if (number > 65535) - { - output.Append("&#"); - output.Append(number.ToString(CultureInfo.InvariantCulture)); - output.Append(";"); - } - else - { - output.Append((char)number); - } - - state = 0; - entity.Length = 0; - haveTrailingDigits = false; - } - else if (char.IsDigit(c)) - { - number = number * 10 + (c - '0'); - haveTrailingDigits = true; - } - else - { - state = 2; - if (haveTrailingDigits) - { - entity.Append(number.ToString(CultureInfo.InvariantCulture)); - haveTrailingDigits = false; - } - - entity.Append(c); - } - } - } - - if (entity.Length > 0) - { - output.Append(entity.ToString()); - } - else if (haveTrailingDigits) - { - output.Append(number.ToString(CultureInfo.InvariantCulture)); - } - - return output.ToString(); - } - - public static void HtmlDecode(string s, TextWriter output) - { - if (output == null) - { - throw new ArgumentNullException(nameof(output)); - } - - output.Write(HtmlDecode(s)); - } - - public static string HtmlEncode(string s) - { - if (s == null || s.Length == 0) - { - return s; - } - - var needEncode = false; - foreach (var c in s) - { - if (c == '&' || c == '"' || c == '<' || c == '>' || c > 159) - { - needEncode = true; - break; - } - } - - if (!needEncode) - { - return s; - } - - var output = new StringBuilder(); - foreach (var c in s) - { - if (c == '&') - { - output.Append("&"); - } - else if (c == '"') - { - output.Append("""); - } - else if (c == '<') - { - output.Append("<"); - } - else if (c == '>') - { - output.Append(">"); - } - else if (c > 159) - { - // MS starts encoding with &# from 160 and stops at 255. - // We don't do that. One reason is the 65308/65310 unicode - // characters that look like '<' and '>'. - output.Append("&#"); - output.Append(((int)c).ToString(CultureInfo.InvariantCulture)); - output.Append(";"); - } - else - { - output.Append(c); - } - } - - return output.ToString(); - } - - public static void HtmlEncode(string s, TextWriter output) - { - if (output == null) - { - throw new ArgumentNullException(nameof(output)); - } - - output.Write(HtmlEncode(s)); - } - - public static NameValueCollection ParseQueryString(string query) - { - return ParseQueryString(query, Encoding.UTF8); - } - - public static NameValueCollection ParseQueryString(string query, Encoding encoding) - { - if (query == null) - { - throw new ArgumentNullException(nameof(query)); - } - - return InternalParseQueryString(query, encoding ?? Encoding.UTF8); - } - - public static string UrlDecode(string s) - { - return UrlDecode(s, Encoding.UTF8); - } - - public static string UrlDecode(string s, Encoding encoding) - { - if (s == null || s.Length == 0 || !s.Contains('%', '+')) - { - return s; - } - - encoding ??= Encoding.UTF8; - - var buff = new List(); - var len = s.Length; - for (var i = 0; i < len; i++) - { - var c = s[i]; - if (c == '%' && i + 2 < len && s[i + 1] != '%') - { - int xchar; - if (s[i + 1] == 'u' && i + 5 < len) - { - // Unicode hex sequence. - xchar = getChar(s, i + 2, 4); - if (xchar != -1) - { - writeCharBytes((char)xchar, buff, encoding); - i += 5; - } - else - { - writeCharBytes('%', buff, encoding); - } - } - else if ((xchar = getChar(s, i + 1, 2)) != -1) - { - writeCharBytes((char)xchar, buff, encoding); - i += 2; - } - else - { - writeCharBytes('%', buff, encoding); - } - - continue; - } - - if (c == '+') - { - writeCharBytes(' ', buff, encoding); - continue; - } - - writeCharBytes(c, buff, encoding); - } - - return encoding.GetString(buff.ToArray()); - } - - public static string UrlDecode(byte[] bytes, Encoding encoding) - { - int len; - return bytes == null - ? null - : (len = bytes.Length) == 0 - ? string.Empty - : InternalUrlDecode(bytes, 0, len, encoding ?? Encoding.UTF8); - } - - public static string UrlDecode(byte[] bytes, int offset, int count, Encoding encoding) - { - if (bytes == null) - { - return null; - } - - var len = bytes.Length; - if (len == 0 || count == 0) - { - return string.Empty; - } - - if (offset < 0 || offset >= len) - { - throw new ArgumentOutOfRangeException(nameof(offset)); - } - - if (count < 0 || count > len - offset) - { - throw new ArgumentOutOfRangeException(nameof(count)); - } - - return InternalUrlDecode(bytes, offset, count, encoding ?? Encoding.UTF8); - } - - public static byte[] UrlDecodeToBytes(byte[] bytes) - { - int len; - return bytes != null && (len = bytes.Length) > 0 - ? InternalUrlDecodeToBytes(bytes, 0, len) - : bytes; - } - - public static byte[] UrlDecodeToBytes(string s) - { - return UrlDecodeToBytes(s, Encoding.UTF8); - } - - public static byte[] UrlDecodeToBytes(string s, Encoding encoding) - { - if (s == null) - { - return null; - } - - if (s.Length == 0) - { - return new byte[0]; - } - - var bytes = (encoding ?? Encoding.UTF8).GetBytes(s); - return InternalUrlDecodeToBytes(bytes, 0, bytes.Length); - } - - public static byte[] UrlDecodeToBytes(byte[] bytes, int offset, int count) - { - int len; - if (bytes == null || (len = bytes.Length) == 0) - { - return bytes; - } - - if (count == 0) - { - return new byte[0]; - } - - if (offset < 0 || offset >= len) - { - throw new ArgumentOutOfRangeException(nameof(offset)); - } - - if (count < 0 || count > len - offset) - { - throw new ArgumentOutOfRangeException(nameof(count)); - } - - return InternalUrlDecodeToBytes(bytes, offset, count); - } - - public static string UrlEncode(byte[] bytes) - { - int len; - return bytes == null - ? null - : (len = bytes.Length) == 0 - ? string.Empty - : Encoding.ASCII.GetString(InternalUrlEncodeToBytes(bytes, 0, len)); - } - - public static string UrlEncode(string s) - { - return UrlEncode(s, Encoding.UTF8); - } - - public static string UrlEncode(string s, Encoding encoding) - { - int len; - if (s == null || (len = s.Length) == 0) - { - return s; - } - - var needEncode = false; - foreach (var c in s) - { - if ((c < '0') || (c < 'A' && c > '9') || (c > 'Z' && c < 'a') || (c > 'z')) - { - if (notEncoded(c)) - { - continue; - } - - needEncode = true; - break; - } - } - - if (!needEncode) - { - return s; - } - - encoding ??= Encoding.UTF8; - - // Avoided GetByteCount call. - var bytes = new byte[encoding.GetMaxByteCount(len)]; - var realLen = encoding.GetBytes(s, 0, len, bytes, 0); - - return Encoding.ASCII.GetString(InternalUrlEncodeToBytes(bytes, 0, realLen)); - } - - public static string UrlEncode(byte[] bytes, int offset, int count) - { - var encoded = UrlEncodeToBytes(bytes, offset, count); - return encoded == null - ? null - : encoded.Length == 0 - ? string.Empty - : Encoding.ASCII.GetString(encoded); - } - - public static byte[] UrlEncodeToBytes(byte[] bytes) - { - int len; - return bytes != null && (len = bytes.Length) > 0 - ? InternalUrlEncodeToBytes(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) - { - return null; - } - - if (s.Length == 0) - { - return new byte[0]; - } - - var bytes = (encoding ?? Encoding.UTF8).GetBytes(s); - return InternalUrlEncodeToBytes(bytes, 0, bytes.Length); - } - - public static byte[] UrlEncodeToBytes(byte[] bytes, int offset, int count) - { - int len; - if (bytes == null || (len = bytes.Length) == 0) - { - return bytes; - } - - if (count == 0) - { - return new byte[0]; - } - - if (offset < 0 || offset >= len) - { - throw new ArgumentOutOfRangeException(nameof(offset)); - } - - if (count < 0 || count > len - offset) - { - throw new ArgumentOutOfRangeException(nameof(count)); - } - - return InternalUrlEncodeToBytes(bytes, offset, count); - } - - public static string UrlEncodeUnicode(string s) - { - return s != null && s.Length > 0 - ? Encoding.ASCII.GetString(InternalUrlEncodeUnicodeToBytes(s)) - : s; - } - - public static byte[] UrlEncodeUnicodeToBytes(string s) - { - return s == null - ? null - : s.Length == 0 - ? new byte[0] - : InternalUrlEncodeUnicodeToBytes(s); - } - - public static string UrlPathEncode(string s) - { - if (s == null || s.Length == 0) - { - return s; - } - - using (var res = new MemoryStream()) - { - foreach (var c in s) - { - urlPathEncode(c, res); - } - - res.Close(); - return Encoding.ASCII.GetString(res.ToArray()); - } - } - - internal static Uri CreateRequestUrl( - string requestUri, string host, bool websocketRequest, bool secure) - { - if (requestUri == null || requestUri.Length == 0 || host == null || host.Length == 0) - { - return null; - } - - string schm = null; - string path = null; - if (requestUri.StartsWith("/")) - { - path = requestUri; - } - else if (requestUri.MaybeUri()) - { - var valid = Uri.TryCreate(requestUri, UriKind.Absolute, out Uri uri) && - (((schm = uri.Scheme).StartsWith("http") && !websocketRequest) || - (schm.StartsWith("ws") && websocketRequest)); - - if (!valid) - { - return null; - } - - host = uri.Authority; - path = uri.PathAndQuery; - } - else if (requestUri == "*") - { - } - else - { - // As authority form - host = requestUri; - } - - schm ??= (websocketRequest ? "ws" : "http") + (secure ? "s" : string.Empty); - - var colon = host.IndexOf(':'); - if (colon == -1) - { - host = string.Format("{0}:{1}", host, schm == "http" || schm == "ws" ? 80 : 443); - } - - var url = string.Format("{0}://{1}{2}", schm, host, path); - - if (!Uri.TryCreate(url, UriKind.Absolute, out Uri res)) - { - return null; - } - - return res; - } - - internal static IPrincipal CreateUser( - string response, - AuthenticationSchemes scheme, - string realm, - string method, - Func credentialsFinder - ) - { - if (response == null || response.Length == 0) - { - return null; - } - - if (credentialsFinder == null) - { - return null; - } - - if (!(scheme == AuthenticationSchemes.Basic || scheme == AuthenticationSchemes.Digest)) - { - return null; - } - - if (scheme == AuthenticationSchemes.Digest) - { - if (realm == null || realm.Length == 0) - { - return null; - } - - if (method == null || method.Length == 0) - { - return null; - } - } - - if (!response.StartsWith(scheme.ToString(), StringComparison.OrdinalIgnoreCase)) - { - 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 - && ((HttpBasicIdentity)id).Password != cred.Password - ) - { - return null; - } - - if (scheme == AuthenticationSchemes.Digest - && !((HttpDigestIdentity)id).IsValid(cred.Password, realm, method, null) - ) - { - return null; - } - - return new GenericPrincipal(id, cred.Roles); - } - - internal static Encoding GetEncoding(string contentType) - { - var parts = contentType.Split(';'); - foreach (var p in parts) - { - var part = p.Trim(); - if (part.StartsWith("charset", StringComparison.OrdinalIgnoreCase)) - { - return Encoding.GetEncoding(part.GetValue('=', true)); - } - } - - return null; - } - - internal static NameValueCollection InternalParseQueryString(string query, Encoding encoding) - { - int len; - if (query == null || (len = query.Length) == 0 || (len == 1 && query[0] == '?')) - { - return new NameValueCollection(1); - } - - if (query[0] == '?') - { - query = query.Substring(1); - } - - var res = new QueryStringCollection(); - var components = query.Split('&'); - foreach (var component in components) - { - var i = component.IndexOf('='); - if (i > -1) - { - var name = UrlDecode(component.Substring(0, i), encoding); - var val = component.Length > i + 1 - ? UrlDecode(component.Substring(i + 1), encoding) - : string.Empty; - - res.Add(name, val); - } - else - { - res.Add(null, UrlDecode(component, encoding)); - } - } - - return res; - } - - internal static string InternalUrlDecode( - byte[] bytes, int offset, int count, Encoding encoding) - { - var output = new StringBuilder(); - using (var acc = new MemoryStream()) - { - var end = count + offset; - for (var i = offset; i < end; i++) - { - if (bytes[i] == '%' && i + 2 < count && bytes[i + 1] != '%') - { - int xchar; - if (bytes[i + 1] == (byte)'u' && i + 5 < end) - { - if (acc.Length > 0) - { - output.Append(getChars(acc, encoding)); - acc.SetLength(0); - } - - xchar = getChar(bytes, i + 2, 4); - if (xchar != -1) - { - output.Append((char)xchar); - i += 5; - - continue; - } - } - else if ((xchar = getChar(bytes, i + 1, 2)) != -1) - { - acc.WriteByte((byte)xchar); - i += 2; - - continue; - } - } - - if (acc.Length > 0) - { - output.Append(getChars(acc, encoding)); - acc.SetLength(0); - } - - if (bytes[i] == '+') - { - output.Append(' '); - continue; - } - - output.Append((char)bytes[i]); - } - - if (acc.Length > 0) - { - output.Append(getChars(acc, encoding)); - } - } - - return output.ToString(); - } - - internal static byte[] InternalUrlDecodeToBytes(byte[] bytes, int offset, int count) - { - using (var res = new MemoryStream()) - { - var end = offset + count; - for (var i = offset; i < end; i++) - { - var c = (char)bytes[i]; - if (c == '+') - { - c = ' '; - } - else if (c == '%' && i < end - 2) - { - var xchar = getChar(bytes, i + 1, 2); - if (xchar != -1) - { - c = (char)xchar; - i += 2; - } - } - - res.WriteByte((byte)c); - } - - res.Close(); - return res.ToArray(); - } - } - - internal static byte[] InternalUrlEncodeToBytes(byte[] bytes, int offset, int count) - { - using (var res = new MemoryStream()) - { - var end = offset + count; - for (var i = offset; i < end; i++) - { - urlEncode((char)bytes[i], res, false); - } - - res.Close(); - return res.ToArray(); - } - } - - internal static byte[] InternalUrlEncodeUnicodeToBytes(string s) - { - using (var res = new MemoryStream()) - { - foreach (var c in s) - { - urlEncode(c, res, true); - } - - res.Close(); - return res.ToArray(); - } - } - - private static void GenerateHtmlEntityRefrencesDictionary() - { - // Build the dictionary of HTML entity references. - _entities = new Dictionary - { - { "nbsp", '\u00A0' }, - { "iexcl", '\u00A1' }, - { "cent", '\u00A2' }, - { "pound", '\u00A3' }, - { "curren", '\u00A4' }, - { "yen", '\u00A5' }, - { "brvbar", '\u00A6' }, - { "sect", '\u00A7' }, - { "uml", '\u00A8' }, - { "copy", '\u00A9' }, - { "ordf", '\u00AA' }, - { "laquo", '\u00AB' }, - { "not", '\u00AC' }, - { "shy", '\u00AD' }, - { "reg", '\u00AE' }, - { "macr", '\u00AF' }, - { "deg", '\u00B0' }, - { "plusmn", '\u00B1' }, - { "sup2", '\u00B2' }, - { "sup3", '\u00B3' }, - { "acute", '\u00B4' }, - { "micro", '\u00B5' }, - { "para", '\u00B6' }, - { "middot", '\u00B7' }, - { "cedil", '\u00B8' }, - { "sup1", '\u00B9' }, - { "ordm", '\u00BA' }, - { "raquo", '\u00BB' }, - { "frac14", '\u00BC' }, - { "frac12", '\u00BD' }, - { "frac34", '\u00BE' }, - { "iquest", '\u00BF' }, - { "Agrave", '\u00C0' }, - { "Aacute", '\u00C1' }, - { "Acirc", '\u00C2' }, - { "Atilde", '\u00C3' }, - { "Auml", '\u00C4' }, - { "Aring", '\u00C5' }, - { "AElig", '\u00C6' }, - { "Ccedil", '\u00C7' }, - { "Egrave", '\u00C8' }, - { "Eacute", '\u00C9' }, - { "Ecirc", '\u00CA' }, - { "Euml", '\u00CB' }, - { "Igrave", '\u00CC' }, - { "Iacute", '\u00CD' }, - { "Icirc", '\u00CE' }, - { "Iuml", '\u00CF' }, - { "ETH", '\u00D0' }, - { "Ntilde", '\u00D1' }, - { "Ograve", '\u00D2' }, - { "Oacute", '\u00D3' }, - { "Ocirc", '\u00D4' }, - { "Otilde", '\u00D5' }, - { "Ouml", '\u00D6' }, - { "times", '\u00D7' }, - { "Oslash", '\u00D8' }, - { "Ugrave", '\u00D9' }, - { "Uacute", '\u00DA' }, - { "Ucirc", '\u00DB' }, - { "Uuml", '\u00DC' }, - { "Yacute", '\u00DD' }, - { "THORN", '\u00DE' }, - { "szlig", '\u00DF' }, - { "agrave", '\u00E0' }, - { "aacute", '\u00E1' }, - { "acirc", '\u00E2' }, - { "atilde", '\u00E3' }, - { "auml", '\u00E4' }, - { "aring", '\u00E5' }, - { "aelig", '\u00E6' }, - { "ccedil", '\u00E7' }, - { "egrave", '\u00E8' }, - { "eacute", '\u00E9' }, - { "ecirc", '\u00EA' }, - { "euml", '\u00EB' }, - { "igrave", '\u00EC' }, - { "iacute", '\u00ED' }, - { "icirc", '\u00EE' }, - { "iuml", '\u00EF' }, - { "eth", '\u00F0' }, - { "ntilde", '\u00F1' }, - { "ograve", '\u00F2' }, - { "oacute", '\u00F3' }, - { "ocirc", '\u00F4' }, - { "otilde", '\u00F5' }, - { "ouml", '\u00F6' }, - { "divide", '\u00F7' }, - { "oslash", '\u00F8' }, - { "ugrave", '\u00F9' }, - { "uacute", '\u00FA' }, - { "ucirc", '\u00FB' }, - { "uuml", '\u00FC' }, - { "yacute", '\u00FD' }, - { "thorn", '\u00FE' }, - { "yuml", '\u00FF' }, - { "fnof", '\u0192' }, - { "Alpha", '\u0391' }, - { "Beta", '\u0392' }, - { "Gamma", '\u0393' }, - { "Delta", '\u0394' }, - { "Epsilon", '\u0395' }, - { "Zeta", '\u0396' }, - { "Eta", '\u0397' }, - { "Theta", '\u0398' }, - { "Iota", '\u0399' }, - { "Kappa", '\u039A' }, - { "Lambda", '\u039B' }, - { "Mu", '\u039C' }, - { "Nu", '\u039D' }, - { "Xi", '\u039E' }, - { "Omicron", '\u039F' }, - { "Pi", '\u03A0' }, - { "Rho", '\u03A1' }, - { "Sigma", '\u03A3' }, - { "Tau", '\u03A4' }, - { "Upsilon", '\u03A5' }, - { "Phi", '\u03A6' }, - { "Chi", '\u03A7' }, - { "Psi", '\u03A8' }, - { "Omega", '\u03A9' }, - { "alpha", '\u03B1' }, - { "beta", '\u03B2' }, - { "gamma", '\u03B3' }, - { "delta", '\u03B4' }, - { "epsilon", '\u03B5' }, - { "zeta", '\u03B6' }, - { "eta", '\u03B7' }, - { "theta", '\u03B8' }, - { "iota", '\u03B9' }, - { "kappa", '\u03BA' }, - { "lambda", '\u03BB' }, - { "mu", '\u03BC' }, - { "nu", '\u03BD' }, - { "xi", '\u03BE' }, - { "omicron", '\u03BF' }, - { "pi", '\u03C0' }, - { "rho", '\u03C1' }, - { "sigmaf", '\u03C2' }, - { "sigma", '\u03C3' }, - { "tau", '\u03C4' }, - { "upsilon", '\u03C5' }, - { "phi", '\u03C6' }, - { "chi", '\u03C7' }, - { "psi", '\u03C8' }, - { "omega", '\u03C9' }, - { "thetasym", '\u03D1' }, - { "upsih", '\u03D2' }, - { "piv", '\u03D6' }, - { "bull", '\u2022' }, - { "hellip", '\u2026' }, - { "prime", '\u2032' }, - { "Prime", '\u2033' }, - { "oline", '\u203E' }, - { "frasl", '\u2044' }, - { "weierp", '\u2118' }, - { "image", '\u2111' }, - { "real", '\u211C' }, - { "trade", '\u2122' }, - { "alefsym", '\u2135' }, - { "larr", '\u2190' }, - { "uarr", '\u2191' }, - { "rarr", '\u2192' }, - { "darr", '\u2193' }, - { "harr", '\u2194' }, - { "crarr", '\u21B5' }, - { "lArr", '\u21D0' }, - { "uArr", '\u21D1' }, - { "rArr", '\u21D2' }, - { "dArr", '\u21D3' }, - { "hArr", '\u21D4' }, - { "forall", '\u2200' }, - { "part", '\u2202' }, - { "exist", '\u2203' }, - { "empty", '\u2205' }, - { "nabla", '\u2207' }, - { "isin", '\u2208' }, - { "notin", '\u2209' }, - { "ni", '\u220B' }, - { "prod", '\u220F' }, - { "sum", '\u2211' }, - { "minus", '\u2212' }, - { "lowast", '\u2217' }, - { "radic", '\u221A' }, - { "prop", '\u221D' }, - { "infin", '\u221E' }, - { "ang", '\u2220' }, - { "and", '\u2227' }, - { "or", '\u2228' }, - { "cap", '\u2229' }, - { "cup", '\u222A' }, - { "int", '\u222B' }, - { "there4", '\u2234' }, - { "sim", '\u223C' }, - { "cong", '\u2245' }, - { "asymp", '\u2248' }, - { "ne", '\u2260' }, - { "equiv", '\u2261' }, - { "le", '\u2264' }, - { "ge", '\u2265' }, - { "sub", '\u2282' }, - { "sup", '\u2283' }, - { "nsub", '\u2284' }, - { "sube", '\u2286' }, - { "supe", '\u2287' }, - { "oplus", '\u2295' }, - { "otimes", '\u2297' }, - { "perp", '\u22A5' }, - { "sdot", '\u22C5' }, - { "lceil", '\u2308' }, - { "rceil", '\u2309' }, - { "lfloor", '\u230A' }, - { "rfloor", '\u230B' }, - { "lang", '\u2329' }, - { "rang", '\u232A' }, - { "loz", '\u25CA' }, - { "spades", '\u2660' }, - { "clubs", '\u2663' }, - { "hearts", '\u2665' }, - { "diams", '\u2666' }, - { "quot", '\u0022' }, - { "amp", '\u0026' }, - { "lt", '\u003C' }, - { "gt", '\u003E' }, - { "OElig", '\u0152' }, - { "oelig", '\u0153' }, - { "Scaron", '\u0160' }, - { "scaron", '\u0161' }, - { "Yuml", '\u0178' }, - { "circ", '\u02C6' }, - { "tilde", '\u02DC' }, - { "ensp", '\u2002' }, - { "emsp", '\u2003' }, - { "thinsp", '\u2009' }, - { "zwnj", '\u200C' }, - { "zwj", '\u200D' }, - { "lrm", '\u200E' }, - { "rlm", '\u200F' }, - { "ndash", '\u2013' }, - { "mdash", '\u2014' }, - { "lsquo", '\u2018' }, - { "rsquo", '\u2019' }, - { "sbquo", '\u201A' }, - { "ldquo", '\u201C' }, - { "rdquo", '\u201D' }, - { "bdquo", '\u201E' }, - { "dagger", '\u2020' }, - { "Dagger", '\u2021' }, - { "permil", '\u2030' }, - { "lsaquo", '\u2039' }, - { "rsaquo", '\u203A' }, - { "euro", '\u20AC' } - }; - } - - private static int getChar(byte[] bytes, int offset, int length) - { - var val = 0; - var end = length + offset; - for (var i = offset; i < end; i++) - { - var current = getInt(bytes[i]); - if (current == -1) - { - return -1; - } - - val = (val << 4) + current; - } - - return val; - } - - private static int getChar(string s, int offset, int length) - { - var val = 0; - var end = length + offset; - for (var i = offset; i < end; i++) - { - var c = s[i]; - if (c > 127) - { - return -1; - } - - var current = getInt((byte)c); - if (current == -1) - { - return -1; - } - - val = (val << 4) + current; - } - - return val; - } - - private static char[] getChars(MemoryStream buffer, Encoding encoding) - { - return encoding.GetChars(buffer.GetBuffer(), 0, (int)buffer.Length); - } - - private static Dictionary getEntities() - { - lock (_locker) - { - if (_entities == null) - { - GenerateHtmlEntityRefrencesDictionary(); - } - - return _entities; - } - } - - private static int getInt(byte b) - { - var c = (char)b; - return c >= '0' && c <= '9' - ? c - '0' - : c >= 'a' && c <= 'f' - ? c - 'a' + 10 - : c >= 'A' && c <= 'F' - ? c - 'A' + 10 - : -1; - } - - private static bool notEncoded(char c) - { - return c == '!' || - c == '\'' || - c == '(' || - c == ')' || - c == '*' || - c == '-' || - c == '.' || - c == '_'; - } - - private static void urlEncode(char c, Stream result, bool unicode) - { - if (c > 255) - { - result.WriteByte((byte)'%'); - result.WriteByte((byte)'u'); - - var i = (int)c; - var idx = i >> 12; - result.WriteByte((byte)_hexChars[idx]); - - idx = (i >> 8) & 0x0F; - result.WriteByte((byte)_hexChars[idx]); - - idx = (i >> 4) & 0x0F; - result.WriteByte((byte)_hexChars[idx]); - - idx = i & 0x0F; - result.WriteByte((byte)_hexChars[idx]); - - return; - } - - if (c > ' ' && notEncoded(c)) - { - result.WriteByte((byte)c); - return; - } - - if (c == ' ') - { - result.WriteByte((byte)'+'); - return; - } - - if ((c < '0') || - (c < 'A' && c > '9') || - (c > 'Z' && c < 'a') || - (c > 'z')) - { - if (unicode && c > 127) - { - result.WriteByte((byte)'%'); - result.WriteByte((byte)'u'); - result.WriteByte((byte)'0'); - result.WriteByte((byte)'0'); - } - else - { - result.WriteByte((byte)'%'); - } - - var i = (int)c; - var idx = i >> 4; - result.WriteByte((byte)_hexChars[idx]); - - idx = i & 0x0F; - result.WriteByte((byte)_hexChars[idx]); - - return; - } - - result.WriteByte((byte)c); - } - - private static void urlPathEncode(char c, Stream result) - { - if (c < 33 || c > 126) - { - var bytes = Encoding.UTF8.GetBytes(c.ToString()); - foreach (var b in bytes) - { - result.WriteByte((byte)'%'); - - var i = (int)b; - var idx = i >> 4; - result.WriteByte((byte)_hexChars[idx]); - - idx = i & 0x0F; - result.WriteByte((byte)_hexChars[idx]); - } - - return; - } - - if (c == ' ') - { - result.WriteByte((byte)'%'); - result.WriteByte((byte)'2'); - result.WriteByte((byte)'0'); - - return; - } - - result.WriteByte((byte)c); - } - - private static void writeCharBytes(char c, IList buffer, Encoding encoding) - { - if (c > 255) - { - foreach (var b in encoding.GetBytes(new[] { c })) - { - buffer.Add(b); - } - - return; - } - - buffer.Add((byte)c); - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpVersion.cs b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpVersion.cs deleted file mode 100644 index dd1521f..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Http/HttpVersion.cs +++ /dev/null @@ -1,30 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; - -namespace EonaCat.Network -{ - /// - /// Represents HTTP versions. - /// - public class HttpVersion - { - /// - /// Gets the HTTP version 1.0. - /// - public static readonly Version Version10 = new Version(1, 0); - - /// - /// Gets the HTTP version 1.1. - /// - public static readonly Version Version11 = new Version(1, 1); - - /// - /// Initializes a new instance of the class. - /// - public HttpVersion() - { - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/InputChunkState.cs b/EonaCat.Network/System/Sockets/Web/Core/InputChunkState.cs deleted file mode 100644 index b3ce4e3..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/InputChunkState.cs +++ /dev/null @@ -1,14 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -namespace EonaCat.Network -{ - internal enum InputChunkState - { - None, - Data, - DataEnded, - Trailer, - End - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/InputState.cs b/EonaCat.Network/System/Sockets/Web/Core/InputState.cs deleted file mode 100644 index ba8d311..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/InputState.cs +++ /dev/null @@ -1,11 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -namespace EonaCat.Network -{ - internal enum InputState - { - RequestLine, - Headers - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/LineState.cs b/EonaCat.Network/System/Sockets/Web/Core/LineState.cs deleted file mode 100644 index 6b1ebb7..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/LineState.cs +++ /dev/null @@ -1,20 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -namespace EonaCat.Network -{ - internal enum LineState - { - None, - - /// - /// Moves the cursor to the beginning of the current line - /// - CarriageReturn, - - /// - /// Moves the cursor down to the next line and then to the beginning of the line - /// - LineFeed - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Logger.cs b/EonaCat.Network/System/Sockets/Web/Core/Logger.cs deleted file mode 100644 index 938d3e2..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Logger.cs +++ /dev/null @@ -1,229 +0,0 @@ -using EonaCat.Logger; -using EonaCat.Logger.GrayLog; -using EonaCat.Logger.Managers; -using EonaCat.Logger.SplunkServer; -using EonaCat.Logger.Syslog; -using System; -using System.Collections.Generic; - -namespace EonaCat.Network -{ - internal class GrayLogSettings - { - internal GrayLogSettings(string facility, string source, string version = "1.1") - { - Facility = facility; - Source = source; - Version = version; - } - - internal string Facility { get; } - internal string Source { get; } - internal string Version { get; } - } - - internal class Logger - { - private static readonly LogManager _logManager; - - static Logger() - { - _logManager = new LogManager(new LoggerSettings()); - _logManager.OnException += LogManager_OnException; - } - - internal static bool DisableConsole { get; set; } - internal static bool IsLoggingDirectorySet => !string.IsNullOrWhiteSpace(LoggingDirectory); - internal static bool IsLoggingEnabled { get; set; } - internal static string LoggingDirectory { get; private set; } - private static bool HasBeenSetup { get; set; } - - internal static void AddGrayLogServer(string hostname, int port) - { - _logManager.Settings.GrayLogServers.Add(new GrayLogServer(hostname, port)); - } - - internal static void AddSplunkServer(string splunkHecUrl, string splunkHecToken, bool disableSSL = false) - { - var splunkServer = new SplunkServer(splunkHecUrl, splunkHecToken); - if (disableSSL) - { - splunkServer.DisableSSLValidation(); - } - _logManager.Settings.SplunkServers.Add(splunkServer); - } - - internal static void AddSyslogServer(string ipAddress, int port) - { - _logManager.Settings.SysLogServers.Add(new SyslogServer(ipAddress, port)); - } - - internal static void Critical(string message, bool? writeToConsole = null, bool? sendToSysLogServers = null, bool? sendToSplunkServers = null, string? customSplunkSourceType = null, bool? sendToGrayLogServers = null, GrayLogSettings grayLogSettings = null) - { - Write(message, ELogType.CRITICAL, writeToConsole, sendToSysLogServers, sendToSplunkServers, customSplunkSourceType, sendToGrayLogServers, grayLogSettings); - } - - internal static void Debug(string message, bool? writeToConsole = null, bool? sendToSysLogServers = null, bool? sendToSplunkServers = null, string? customSplunkSourceType = null, bool? sendToGrayLogServers = null, GrayLogSettings grayLogSettings = null) - { - Write(message, ELogType.DEBUG, writeToConsole, sendToSysLogServers, sendToSplunkServers, customSplunkSourceType, sendToGrayLogServers, grayLogSettings); - } - - internal static void Error(Exception exception, string message = null, bool isCriticalException = false, bool? writeToConsole = null, bool? sendToSysLogServers = null, bool? sendToSplunkServers = null, string? customSplunkSourceType = null, bool? sendToGrayLogServers = null, GrayLogSettings grayLogSettings = null) - { - if (!IsLoggingEnabled) - { - return; - } - - if (DisableConsole) - { - writeToConsole = false; - } - - if (!HasBeenSetup) - { - Setup(); - } - - if (grayLogSettings != null) - { - _logManager.Write(exception, message, criticalException: isCriticalException, writeToConsole: writeToConsole, sendToSysLogServers: sendToSysLogServers, sendToSplunkServers: sendToSplunkServers, customSplunkSourceType: customSplunkSourceType, sendToGrayLogServers: sendToGrayLogServers, grayLogFacility: grayLogSettings.Facility, grayLogSource: grayLogSettings.Source, grayLogVersion: grayLogSettings.Version); - } - else - { - _logManager.Write(exception, message, criticalException: isCriticalException, writeToConsole: writeToConsole, sendToSysLogServers: sendToSysLogServers, sendToSplunkServers: sendToSplunkServers, customSplunkSourceType: customSplunkSourceType, sendToGrayLogServers: sendToGrayLogServers); - } - } - - internal static void Error(string message = null, bool? writeToConsole = null, bool? sendToSysLogServers = null, bool? sendToSplunkServers = null, string? customSplunkSourceType = null, bool? sendToGrayLogServers = null, string grayLogFacility = null, string grayLogSource = null, string grayLogVersion = "1.1", bool isCriticalException = false) - { - if (!IsLoggingEnabled) - { - return; - } - - if (DisableConsole) - { - writeToConsole = false; - } - - if (!HasBeenSetup) - { - Setup(); - } - - if (isCriticalException) - { - _logManager.Write(message, ELogType.CRITICAL, writeToConsole); - } - else - { - _logManager.Write(message, ELogType.ERROR, writeToConsole); - } - } - - internal static void GrayLogState(bool state) - { - _logManager.Settings.SendToGrayLogServers = state; - } - - internal static void Info(string message, bool? writeToConsole = null, bool? sendToSysLogServers = null, bool? sendToSplunkServers = null, string? customSplunkSourceType = null, bool? sendToGrayLogServers = null, GrayLogSettings grayLogSettings = null) - { - Write(message, ELogType.INFO, writeToConsole, sendToSysLogServers, sendToSplunkServers, customSplunkSourceType, sendToGrayLogServers, grayLogSettings); - } - - internal static bool RemoveGrayLogServer(GrayLogServer grayLogServer) - { - return _logManager.Settings.GrayLogServers.Remove(grayLogServer); - } - - internal static bool RemoveSplunkServer(SplunkServer splunkServer) - { - return _logManager.Settings.SplunkServers.Remove(splunkServer); - } - - internal static bool RemoveSyslogServer(SyslogServer syslogServer) - { - return _logManager.Settings.SysLogServers.Remove(syslogServer); - } - - internal static void Setup(string loggingDirectory = null) - { - LoggingDirectory = loggingDirectory; - _logManager.Settings.FileLoggerOptions.FileNamePrefix = "EonaCat.Network"; - - if (IsLoggingDirectorySet) - { - _logManager.Settings.FileLoggerOptions.LogDirectory = LoggingDirectory; - } - _logManager.Settings.UseLocalTime = true; - _logManager.Settings.FileLoggerOptions.UseLocalTime = true; - _logManager.Settings.SysLogServers = new List(); - _logManager.Settings.SplunkServers = new List(); - _logManager.Settings.GrayLogServers = new List(); - _logManager.StartNewLogAsync(); - HasBeenSetup = true; - } - - internal static void SplunkState(bool state) - { - _logManager.Settings.SendToSplunkServers = state; - } - - internal static void SysLogState(bool state) - { - _logManager.Settings.SendToSyslogServers = state; - } - - internal static void Trace(string message, bool? writeToConsole = null, bool? sendToSysLogServers = null, bool? sendToSplunkServers = null, string? customSplunkSourceType = null, bool? sendToGrayLogServers = null, GrayLogSettings grayLogSettings = null) - { - Write(message, ELogType.TRACE, writeToConsole, sendToSysLogServers, sendToSplunkServers, customSplunkSourceType, sendToGrayLogServers, grayLogSettings); - } - - internal static void Traffic(string message, bool? writeToConsole = null, bool? sendToSysLogServers = null, bool? sendToSplunkServers = null, string? customSplunkSourceType = null, bool? sendToGrayLogServers = null, GrayLogSettings grayLogSettings = null) - { - Write(message, ELogType.TRAFFIC, writeToConsole, sendToSysLogServers, sendToSplunkServers, customSplunkSourceType, sendToGrayLogServers, grayLogSettings); - } - - internal static void Warning(string message, bool? writeToConsole = null, bool? sendToSysLogServers = null, bool? sendToSplunkServers = null, string? customSplunkSourceType = null, bool? sendToGrayLogServers = null, GrayLogSettings grayLogSettings = null) - { - Write(message, ELogType.WARNING, writeToConsole, sendToSysLogServers, sendToSplunkServers, customSplunkSourceType, sendToGrayLogServers, grayLogSettings); - } - - private static void LogManager_OnException(object? sender, ErrorMessage e) - { - Console.WriteLine(e.Message); - if (e.Exception != null) - { - Console.WriteLine(e.Exception); - } - } - - private static void Write(string message, ELogType logType, bool? writeToConsole = null, bool? sendToSysLogServers = null, bool? sendToSplunkServers = null, string? customSplunkSourceType = null, bool? sendToGrayLogServers = null, GrayLogSettings grayLogSettings = null) - { - if (!IsLoggingEnabled) - { - return; - } - - if (DisableConsole) - { - writeToConsole = false; - } - - if (!HasBeenSetup) - { - Setup(); - } - - if (grayLogSettings != null) - { - _logManager.Write(message, logType, writeToConsole, sendToSysLogServers, sendToSplunkServers, customSplunkSourceType, sendToGrayLogServers, grayLogSettings.Facility, grayLogSettings.Source, grayLogSettings.Version); - } - else - { - _logManager.Write(message, logType, writeToConsole, sendToSysLogServers, sendToSplunkServers, customSplunkSourceType, sendToGrayLogServers); - } - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/ReadBufferState.cs b/EonaCat.Network/System/Sockets/Web/Core/ReadBufferState.cs deleted file mode 100644 index 432afbf..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/ReadBufferState.cs +++ /dev/null @@ -1,28 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -namespace EonaCat.Network -{ - internal class ReadBufferState - { - public ReadBufferState( - byte[] buffer, int offset, int count, HttpStreamAsyncResult asyncResult) - { - Buffer = buffer; - Offset = offset; - Count = count; - InitialCount = count; - AsyncResult = asyncResult; - } - - public HttpStreamAsyncResult AsyncResult { get; set; } - - public byte[] Buffer { get; set; } - - public int Count { get; set; } - - public int InitialCount { get; set; } - - public int Offset { get; set; } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/SSLConfig/SSLConfigClient.cs b/EonaCat.Network/System/Sockets/Web/Core/SSLConfig/SSLConfigClient.cs deleted file mode 100644 index f041a50..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/SSLConfig/SSLConfigClient.cs +++ /dev/null @@ -1,113 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.Net.Security; -using System.Security.Authentication; -using System.Security.Cryptography.X509Certificates; - -namespace EonaCat.Network -{ - public class SSLConfigClient - { - private X509CertificateCollection _clientCertificates; - private LocalCertificateSelectionCallback _clientCertSelectionCallback; - private RemoteCertificateValidationCallback _serverCertValidationCallback; - - public SSLConfigClient() - { - SslProtocols = SslProtocols.None; - } - - public SSLConfigClient(string targetHost) - { - TargetHost = targetHost; - SslProtocols = SslProtocols.None; - } - - public SSLConfigClient(SSLConfigClient sslConfig) - { - if (sslConfig == null) - { - throw new ArgumentNullException(nameof(sslConfig)); - } - - CheckForCertificateRevocation = sslConfig.CheckForCertificateRevocation; - _clientCertSelectionCallback = sslConfig._clientCertSelectionCallback; - _clientCertificates = sslConfig._clientCertificates; - SslProtocols = sslConfig.SslProtocols; - _serverCertValidationCallback = sslConfig._serverCertValidationCallback; - TargetHost = sslConfig.TargetHost; - } - - public X509CertificateCollection Certificates - { - get - { - _clientCertificates ??= new X509CertificateCollection(); - return _clientCertificates; - } - - set - { - _clientCertificates = value; - } - } - - public bool CheckForCertificateRevocation { get; set; } - - public LocalCertificateSelectionCallback ClientCertificateSelectionCallback - { - get - { - _clientCertSelectionCallback ??= SelectClientCertificate; - - return _clientCertSelectionCallback; - } - - set - { - _clientCertSelectionCallback = value; - } - } - - public RemoteCertificateValidationCallback ServerCertificateValidationCallback - { - get - { - _serverCertValidationCallback ??= ValidateServerCertificate; - - return _serverCertValidationCallback; - } - - set - { - _serverCertValidationCallback = value; - } - } - - public SslProtocols SslProtocols { get; set; } - public string TargetHost { get; set; } - - private static X509Certificate SelectClientCertificate( - object sender, - string targetHost, - X509CertificateCollection clientCertificates, - X509Certificate serverCertificate, - string[] acceptableIssuers - ) - { - return null; - } - - private static bool ValidateServerCertificate( - object sender, - X509Certificate certificate, - X509Chain chain, - SslPolicyErrors sslPolicyErrors - ) - { - return true; - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/SSLConfig/SSLConfigServer.cs b/EonaCat.Network/System/Sockets/Web/Core/SSLConfig/SSLConfigServer.cs deleted file mode 100644 index 628bfa4..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/SSLConfig/SSLConfigServer.cs +++ /dev/null @@ -1,71 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.Net.Security; -using System.Security.Authentication; -using System.Security.Cryptography.X509Certificates; - -namespace EonaCat.Network -{ - public class SSLConfigServer - { - private RemoteCertificateValidationCallback _clientCertificationValidationCallback; - - public SSLConfigServer() - { - SslProtocols = SslProtocols.None; - } - - public SSLConfigServer(X509Certificate2 certificate) - { - Certificate = certificate; - SslProtocols = SslProtocols.None; - } - - public SSLConfigServer(SSLConfigServer sslConfig) - { - if (sslConfig == null) - { - throw new ArgumentNullException(nameof(sslConfig)); - } - - CheckForCertificateRevocation = sslConfig.CheckForCertificateRevocation; - IsClientCertificateRequired = sslConfig.IsClientCertificateRequired; - _clientCertificationValidationCallback = sslConfig._clientCertificationValidationCallback; - SslProtocols = sslConfig.SslProtocols; - Certificate = sslConfig.Certificate; - } - - public X509Certificate2 Certificate { get; set; } - public bool CheckForCertificateRevocation { get; set; } - - public RemoteCertificateValidationCallback ClientCertificateValidationCallback - { - get - { - _clientCertificationValidationCallback ??= ValidateClientCertificate; - - return _clientCertificationValidationCallback; - } - - set - { - _clientCertificationValidationCallback = value; - } - } - - public bool IsClientCertificateRequired { get; set; } - public SslProtocols SslProtocols { get; set; } - - private static bool ValidateClientCertificate( - object sender, - X509Certificate certificate, - X509Chain chain, - SslPolicyErrors sslPolicyErrors - ) - { - return true; - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Stream/RequestStream.cs b/EonaCat.Network/System/Sockets/Web/Core/Stream/RequestStream.cs deleted file mode 100644 index 3fd614d..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Stream/RequestStream.cs +++ /dev/null @@ -1,237 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.IO; - -namespace EonaCat.Network -{ - internal class RequestStream : Stream - { - private readonly byte[] _buffer; - private readonly Stream _stream; - private long _bodyLeft; - private int _count; - private bool _disposed; - private int _offset; - - 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; - } - - public override bool CanRead => true; - - public override bool CanSeek => false; - - public override bool CanWrite => false; - - public override long Length => throw new NotSupportedException(); - - public override long Position - { - get - { - throw new NotSupportedException(); - } - - set - { - throw new NotSupportedException(); - } - } - - 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 result = new HttpStreamAsyncResult(callback, state); - result.Buffer = buffer; - result.Offset = offset; - result.Count = count; - result.SyncRead = nread > 0 ? nread : 0; - result.Complete(); - return result; - } - - // 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(nameof(asyncResult)); - } - - if (asyncResult is HttpStreamAsyncResult) - { - var result = (HttpStreamAsyncResult)asyncResult; - if (!result.IsCompleted) - { - result.AsyncWaitHandle.WaitOne(); - } - - return result.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(); - } - - // 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(nameof(buffer)); - } - - if (offset < 0) - { - throw new ArgumentOutOfRangeException(nameof(offset), "A negative value."); - } - - if (count < 0) - { - throw new ArgumentOutOfRangeException(nameof(count), "A negative value."); - } - - var bufferLength = buffer.Length; - if (offset + count > bufferLength) - { - 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; - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Stream/ResponseStream.cs b/EonaCat.Network/System/Sockets/Web/Core/Stream/ResponseStream.cs deleted file mode 100644 index 13ea0cc..0000000 --- a/EonaCat.Network/System/Sockets/Web/Core/Stream/ResponseStream.cs +++ /dev/null @@ -1,286 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.IO; -using System.Text; - -namespace EonaCat.Network -{ - internal class ResponseStream : Stream - { - private static readonly byte[] _crlf = new byte[] { 13, 10 }; - private readonly Action _write; - private readonly Action _writeChunked; - private MemoryStream _body; - private bool _disposed; - private HttpListenerResponse _response; - private bool _sendChunked; - private Stream _stream; - private Action _writeBody; - - 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(); - } - - public override bool CanRead => false; - - public override bool CanSeek => false; - - public override bool CanWrite => !_disposed; - - public override long Length => throw new NotSupportedException(); - - public override long Position - { - get - { - throw new NotSupportedException(); - } - - set - { - throw new NotSupportedException(); - } - } - - 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); - } - - 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.SendInChunks)) - { - 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); - } - - 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); - } - - protected override void Dispose(bool disposing) - { - Close(!disposing); - } - - private static byte[] getChunkSizeBytes(int size, bool final) - { - return Encoding.ASCII.GetBytes(string.Format("{0:x}\r\n{1}", size, final ? "\r\n" : "")); - } - - private bool flush(bool closing) - { - if (!_response.HeadersSent) - { - if (!flushHeaders(closing)) - { - if (closing) - { - _response.CloseConnection = true; - } - - return false; - } - - _sendChunked = _response.SendInChunks; - _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 > int.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) - { - using (var buff = new MemoryStream()) - { - var headers = _response.WriteHeadersTo(buff); - var start = buff.Position; - var len = buff.Length - start; - if (len > 32768) - { - return false; - } - - if (!_response.SendInChunks && _response.ContentLength64 != _body.Length) - { - return false; - } - - _write(buff.GetBuffer(), (int)start, (int)len); - _response.CloseConnection = headers["Connection"] == "close"; - _response.HeadersSent = true; - } - - return true; - } - - 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 - { - } - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Endpoints/WelcomeEndpoint.cs b/EonaCat.Network/System/Sockets/Web/Endpoints/WelcomeEndpoint.cs deleted file mode 100644 index bee44a3..0000000 --- a/EonaCat.Network/System/Sockets/Web/Endpoints/WelcomeEndpoint.cs +++ /dev/null @@ -1,17 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; - -namespace EonaCat.Network -{ - public class WelcomeEndpoint : WSEndpoint - - { - protected override void OnMessage(MessageEventArgs e) - { - base.OnMessage(e); - Console.WriteLine($"Received message: {e.Data}"); - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/EventArgs/CloseEventArgs.cs b/EonaCat.Network/System/Sockets/Web/EventArgs/CloseEventArgs.cs deleted file mode 100644 index 98d6858..0000000 --- a/EonaCat.Network/System/Sockets/Web/EventArgs/CloseEventArgs.cs +++ /dev/null @@ -1,60 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; - -namespace EonaCat.Network -{ - public class CloseEventArgs : EventArgs - { - internal CloseEventArgs() - { - Payload = Payload.Empty; - } - - internal CloseEventArgs(ushort code) - : this(code, null) - { - } - - internal CloseEventArgs(CloseStatusCode code) - : this((ushort)code, null) - { - } - - internal CloseEventArgs(Payload payload) - { - Payload = payload; - } - - internal CloseEventArgs(ushort code, string reason) - { - Payload = new Payload(code, reason); - } - - internal CloseEventArgs(CloseStatusCode code, string reason) - : this((ushort)code, reason) - { - } - - /// - /// Code - /// - public ushort Code => Payload.Code; - - /// - /// Reason - /// - public string Reason => Payload.Reason ?? string.Empty; - - /// - /// Determnines if both the client and server requests where handled - /// - public bool WasClean { get; internal set; } - - /// - /// Payload - /// - internal Payload Payload { get; } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/EventArgs/ErrorEventArgs.cs b/EonaCat.Network/System/Sockets/Web/EventArgs/ErrorEventArgs.cs deleted file mode 100644 index deac474..0000000 --- a/EonaCat.Network/System/Sockets/Web/EventArgs/ErrorEventArgs.cs +++ /dev/null @@ -1,25 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; - -namespace EonaCat.Network -{ - public class ErrorEventArgs : EventArgs - { - internal ErrorEventArgs(string message) - : this(message, null) - { - } - - internal ErrorEventArgs(string message, Exception exception) - { - Message = message; - Exception = exception; - } - - public Exception Exception { get; } - - public string Message { get; } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/EventArgs/MessageEventArgs.cs b/EonaCat.Network/System/Sockets/Web/EventArgs/MessageEventArgs.cs deleted file mode 100644 index 2935f8f..0000000 --- a/EonaCat.Network/System/Sockets/Web/EventArgs/MessageEventArgs.cs +++ /dev/null @@ -1,72 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; - -namespace EonaCat.Network -{ - public class MessageEventArgs : EventArgs - { - private readonly byte[] _rawData; - private string _data; - private bool _dataSet; - - internal MessageEventArgs(WSFrame frame) - { - Opcode = frame.Opcode; - _rawData = frame.Payload.ApplicationData; - } - - internal MessageEventArgs(OperationCode opcode, byte[] rawData) - { - if ((ulong)rawData.LongLength > Payload.MaxLength) - { - throw new WSException(CloseStatusCode.TooBig); - } - - Opcode = opcode; - _rawData = rawData; - } - - public string Data - { - get - { - setData(); - return _data; - } - } - - public bool IsBinary => Opcode == OperationCode.Binary; - public bool IsPing => Opcode == OperationCode.Ping; - public bool IsText => Opcode == OperationCode.Text; - - public byte[] RawData - { - get - { - setData(); - return _rawData; - } - } - - internal OperationCode Opcode { get; } - - private void setData() - { - if (_dataSet) - { - return; - } - - if (Opcode == OperationCode.Binary) - { - _dataSet = true; - return; - } - - _data = _rawData.UTF8Decode(); - _dataSet = true; - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Extensions.cs b/EonaCat.Network/System/Sockets/Web/Extensions.cs deleted file mode 100644 index 77f673b..0000000 --- a/EonaCat.Network/System/Sockets/Web/Extensions.cs +++ /dev/null @@ -1,1650 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Net.Sockets; -using System.Text; - -namespace EonaCat.Network -{ - public static class Ext - { - private const string _tspecials = "()<>@,;:\\\"/[]?={} \t"; - private const int BUFFER_SIZE = 1024; - private static readonly byte[] _last = new byte[] { 0x00 }; - private static readonly int _retry = 5; - - public static bool Contains(this string value, params char[] chars) - { - return chars == null || chars.Length == 0 -|| value != null && value.Length != 0 -&& value.IndexOfAny(chars) > -1; - } - - public static bool Contains(this NameValueCollection collection, string name) - { - return collection != null && collection.Count > 0 && collection[name] != null; - } - - public static bool Contains(this NameValueCollection collection, string name, string value) - { - if (collection == null || collection.Count == 0) - { - return false; - } - - var vals = collection[name]; - if (vals == null) - { - return false; - } - - foreach (var val in vals.Split(',')) - { - if (val.Trim().Equals(value, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - return false; - } - - public static void Emit(this EventHandler eventHandler, object sender, EventArgs e) - { - eventHandler?.Invoke(sender, e); - } - - public static void Emit( - this EventHandler eventHandler, object sender, TEventArgs e) - where TEventArgs : EventArgs - { - eventHandler?.Invoke(sender, e); - } - - public static CookieCollection GetCookies(this NameValueCollection headers, bool response) - { - var name = response ? "Set-Cookie" : "Cookie"; - return headers != null && headers.Contains(name) - ? CookieCollection.Parse(headers[name], response) - : new CookieCollection(); - } - - public static string GetDescription(this HttpStatusCode code) - { - return ((int)code).GetStatusDescription(); - } - - 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; - } - - public static bool IsCloseStatusCode(this ushort value) - { - return value > 999 && value < 5000; - } - - public static bool IsEnclosedIn(this string value, char c) - { - return value != null - && value.Length > 1 - && value[0] == c - && value[value.Length - 1] == c; - } - - 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)); - } - - /// - /// An extension method to determine if an IP address is internal - /// Class A Private IP Range: 10.0.0.0 ? 10.255.255.255 - /// Class B Private IP Range: 172.16.0.0 ? 172.31.255.255 - /// Class C Private IP Range: 192.168.0.0 ? 192.168.255.25 - /// - /// The IP address that will be tested - /// Returns true if the IP is internal, false if it is external - public static bool IsInternal(this System.Net.IPAddress address) - { - byte[] bytes = address.GetAddressBytes(); - switch (bytes[0]) - { - case 10: - return true; - - case 172: - return bytes[1] < 32 && bytes[1] >= 16; - - case 192: - return bytes[1] == 168; - - default: - return false; - } - } - - public static bool IsLocal(this System.Net.IPAddress address) - { - if (address == null) - { - return false; - } - - 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; - } - } - - if (address.IsInternal()) - 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; - } - - public static bool IsNullOrEmpty(this string value) - { - return value == null || value.Length == 0; - } - - 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; - } - - public static bool IsUpgradeTo(this HttpListenerRequest request, string protocol) - { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } - - if (protocol == null) - { - throw new ArgumentNullException(nameof(protocol)); - } - - if (protocol.Length == 0) - { - throw new ArgumentException("An empty string.", nameof(protocol)); - } - - return request.Headers.Contains("Upgrade", protocol) && - request.Headers.Contains("Connection", "Upgrade"); - } - - public static bool MaybeUri(this string value) - { - if (value == null || 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(); - } - - public static T[] SubArray(this T[] array, int startIndex, int length) - { - int len; - if (array == null || (len = array.Length) == 0) - { - return new T[0]; - } - - if (startIndex < 0 || length <= 0 || startIndex + length > len) - { - return new T[0]; - } - - if (startIndex == 0 && length == len) - { - return array; - } - - var subArray = new T[length]; - Array.Copy(array, startIndex, subArray, 0, length); - - return subArray; - } - - public static T[] SubArray(this T[] array, long startIndex, long length) - { - long len; - if (array == null || (len = array.LongLength) == 0) - { - return new T[0]; - } - - if (startIndex < 0 || length <= 0 || startIndex + length > len) - { - return new T[0]; - } - - if (startIndex == 0 && length == len) - { - return array; - } - - var subArray = new T[length]; - Array.Copy(array, startIndex, subArray, 0, length); - - return subArray; - } - - public static void Times(this int n, Action action) - { - if (n > 0 && action != null) - { - ((ulong)n).times(action); - } - } - - public static void Times(this long n, Action action) - { - if (n > 0 && action != null) - { - ((ulong)n).times(action); - } - } - - public static void Times(this uint n, Action action) - { - if (n > 0 && action != null) - { - ((ulong)n).times(action); - } - } - - public static void Times(this ulong n, Action action) - { - if (n > 0 && action != null) - { - n.times(action); - } - } - - public static void Times(this int n, Action action) - { - if (n > 0 && action != null) - { - for (int i = 0; i < n; i++) - { - action(i); - } - } - } - - public static void Times(this long n, Action action) - { - if (n > 0 && action != null) - { - for (long i = 0; i < n; i++) - { - action(i); - } - } - } - - public static void Times(this uint n, Action action) - { - if (n > 0 && action != null) - { - for (uint i = 0; i < n; i++) - { - action(i); - } - } - } - - public static void Times(this ulong n, Action action) - { - if (n > 0 && action != null) - { - for (ulong i = 0; i < n; i++) - { - action(i); - } - } - } - - public static T To(this byte[] source, ByteOrder sourceOrder) - where T : struct - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (source.Length == 0) - { - return default(T); - } - - var type = typeof(T); - var buff = source.ToHostOrder(sourceOrder); - - return type == typeof(bool) - ? (T)(object)BitConverter.ToBoolean(buff, 0) - : type == typeof(char) - ? (T)(object)BitConverter.ToChar(buff, 0) - : type == typeof(double) - ? (T)(object)BitConverter.ToDouble(buff, 0) - : type == typeof(short) - ? (T)(object)BitConverter.ToInt16(buff, 0) - : type == typeof(int) - ? (T)(object)BitConverter.ToInt32(buff, 0) - : type == typeof(long) - ? (T)(object)BitConverter.ToInt64(buff, 0) - : type == typeof(float) - ? (T)(object)BitConverter.ToSingle(buff, 0) - : type == typeof(ushort) - ? (T)(object)BitConverter.ToUInt16(buff, 0) - : type == typeof(uint) - ? (T)(object)BitConverter.ToUInt32(buff, 0) - : type == typeof(ulong) - ? (T)(object)BitConverter.ToUInt64(buff, 0) - : default(T); - } - - public static byte[] ToByteArray(this T value, ByteOrder order) - where T : struct - { - var type = typeof(T); - var bytes = type == typeof(bool) - ? BitConverter.GetBytes((bool)(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(short) - ? BitConverter.GetBytes((short)(object)value) - : type == typeof(int) - ? BitConverter.GetBytes((int)(object)value) - : type == typeof(long) - ? BitConverter.GetBytes((long)(object)value) - : type == typeof(float) - ? BitConverter.GetBytes((float)(object)value) - : type == typeof(ushort) - ? BitConverter.GetBytes((ushort)(object)value) - : type == typeof(uint) - ? BitConverter.GetBytes((uint)(object)value) - : type == typeof(ulong) - ? BitConverter.GetBytes((ulong)(object)value) - : WSClient.EmptyBytes; - - if (bytes.Length > 1 && !order.IsHostOrder()) - { - Array.Reverse(bytes); - } - - return bytes; - } - - public static byte[] ToHostOrder(this byte[] source, ByteOrder sourceOrder) - { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } - - return source.Length > 1 && !sourceOrder.IsHostOrder() ? source.Reverse() : source; - } - - public static string ToString(this T[] array, string separator) - { - if (array == null) - { - throw new ArgumentNullException(nameof(array)); - } - - var len = array.Length; - if (len == 0) - { - return string.Empty; - } - - separator ??= string.Empty; - - var buff = new StringBuilder(64); - (len - 1).Times(i => buff.AppendFormat("{0}{1}", array[i].ToString(), separator)); - - buff.Append(array[len - 1].ToString()); - return buff.ToString(); - } - - public static Uri ToUri(this string value) - { - Uri.TryCreate( - value, value.MaybeUri() ? UriKind.Absolute : UriKind.Relative, out Uri ret - ); - - return ret; - } - - public static string UrlDecode(this string value) - { - return value != null && value.Length > 0 - ? HttpUtility.UrlDecode(value) - : value; - } - - public static string UrlEncode(this string value) - { - return value != null && value.Length > 0 - ? HttpUtility.UrlEncode(value) - : value; - } - - public static void WriteContent(this HttpListenerResponse response, byte[] content) - { - if (response == null) - { - throw new ArgumentNullException(nameof(response)); - } - - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } - - var len = content.LongLength; - if (len == 0) - { - response.Close(); - return; - } - - response.ContentLength64 = len; - var output = response.OutputStream; - if (len <= int.MaxValue) - { - output.Write(content, 0, (int)len); - } - else - { - output.WriteBytes(content, BUFFER_SIZE); - } - - output.Close(); - } - - internal static byte[] Append(this ushort code, string reason) - { - var ret = code.InternalToByteArray(ByteOrder.Big); - if (reason != null && reason.Length > 0) - { - var buff = new List(ret); - buff.AddRange(Encoding.UTF8.GetBytes(reason)); - ret = buff.ToArray(); - } - - return ret; - } - - internal static void Close(this HttpListenerResponse response, HttpStatusCode code) - { - response.StatusCode = (int)code; - response.OutputStream.Close(); - } - - internal static void CloseWithAuthChallenge( - this HttpListenerResponse response, string challenge) - { - response.Headers.InternalSet("WWW-Authenticate", challenge, true); - response.Close(HttpStatusCode.Unauthorized); - } - - internal static byte[] Compress(this byte[] data, CompressionMethod method) - { - 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(); - } - - 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 ((nread = source.Read(buff, 0, bufferLength)) > 0) - { - 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) - { - completed?.Invoke(); - - return; - } - - destination.Write(buff, 0, nread); - source.BeginRead(buff, 0, bufferLength, callback, null); - } - catch (Exception ex) - { - error?.Invoke(ex); - } - }; - - try - { - source.BeginRead(buff, 0, bufferLength, callback, null); - } - catch (Exception ex) - { - error?.Invoke(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(); - } - - internal static bool EqualsWith(this int value, char c, Action action) - { - action(value); - return value == c - 0; - } - - internal static string GetAbsolutePath(this Uri uri) - { - if (uri.IsAbsoluteUri) - { - return uri.AbsolutePath; - } - - var original = uri.OriginalString; - if (original[0] != '/') - { - return null; - } - - var idx = original.IndexOfAny(new[] { '?', '#' }); - return idx > 0 ? original.Substring(0, idx) : original; - } - - internal static string 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.MissingExtension - ? "WebSocket client didn't receive expected extension(s)." - : code == CloseStatusCode.ServerError - ? "WebSocket server got an internal error." - : code == CloseStatusCode.TlsHandshakeFailure - ? "An error has occurred during a TLS handshake." - : string.Empty; - } - - internal static string GetName(this string nameAndValue, char separator) - { - var idx = nameAndValue.IndexOf(separator); - return idx > 0 ? nameAndValue.Substring(0, idx).Trim() : null; - } - - internal static string GetValue(this string nameAndValue, char separator) - { - var idx = nameAndValue.IndexOf(separator); - return idx > -1 && idx < nameAndValue.Length - 1 - ? nameAndValue.Substring(idx + 1).Trim() - : null; - } - - 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 bytes = BitConverter.GetBytes(value); - if (!order.IsHostOrder()) - { - Array.Reverse(bytes); - } - - return bytes; - } - - internal static byte[] InternalToByteArray(this ulong value, ByteOrder order) - { - var bytes = BitConverter.GetBytes(value); - if (!order.IsHostOrder()) - { - Array.Reverse(bytes); - } - - return bytes; - } - - 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 OperationCode opcode) - { - return opcode >= OperationCode.Close; - } - - internal static bool IsData(this byte opcode) - { - return opcode == 0x1 || opcode == 0x2; - } - - internal static bool IsData(this OperationCode opcode) - { - return opcode == OperationCode.Text || opcode == OperationCode.Binary; - } - - internal static bool IsPortNumber(this int value) - { - return value > 0 && value < 65536; - } - - internal static bool IsReserved(this ushort code) - { - return code == (ushort)CloseStatusCode.Undefined - || code == (ushort)CloseStatusCode.NoStatus - || code == (ushort)CloseStatusCode.Abnormal - || code == (ushort)CloseStatusCode.TlsHandshakeFailure; - } - - 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(OperationCode), 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".Contains(c)) - { - return false; - } - - if (c == '\n') - { - i++; - if (i == len) - { - break; - } - - c = value[i]; - if (!" \t".Contains(c)) - { - 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 >= 0x7f) - { - return false; - } - - if (_tspecials.Contains(c)) - { - return false; - } - } - - return true; - } - - 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; - try - { - var nread = 0; - while (length > 0) - { - nread = stream.Read(buff, offset, length); - if (nread == 0) - { - break; - } - - offset += nread; - length -= nread; - } - } - catch - { - } - - return buff.SubArray(0, offset); - } - - internal static byte[] ReadBytes(this Stream stream, long length, int bufferLength) - { - using (var dest = new MemoryStream()) - { - try - { - var buff = new byte[bufferLength]; - var nread = 0; - while (length > 0) - { - if (length < bufferLength) - { - bufferLength = (int)length; - } - - nread = stream.Read(buff, 0, bufferLength); - if (nread == 0) - { - break; - } - - dest.Write(buff, 0, nread); - length -= nread; - } - } - catch - { - } - - 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 && retry < _retry) - { - retry++; - stream.BeginRead(buff, offset, length, callback, null); - - return; - } - - if (nread == length) - { - completed?.Invoke(buff.SubArray(0, offset + nread)); - - return; - } - - retry = 0; - - offset += nread; - length -= nread; - - stream.BeginRead(buff, offset, length, callback, null); - } - catch (Exception ex) - { - error?.Invoke(ex); - } - }; - - try - { - stream.BeginRead(buff, offset, length, callback, null); - } - catch (Exception ex) - { - error?.Invoke(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) - { - dest.Write(buff, 0, nread); - } - - if (nread == 0 && retry < _retry) - { - retry++; - read(len); - - return; - } - - if (nread == 0 || nread == len) - { - if (completed != null) - { - dest.Close(); - completed(dest.ToArray()); - } - - dest.Dispose(); - return; - } - - retry = 0; - read(len - nread); - } - catch (Exception ex) - { - dest.Dispose(); - error?.Invoke(ex); - } - }, - null - ); - }; - - try - { - read(length); - } - catch (Exception ex) - { - dest.Dispose(); - error?.Invoke(ex); - } - } - - internal static string RemovePrefix(this string value, params string[] prefixes) - { - var idx = 0; - foreach (var prefix in prefixes) - { - if (value.StartsWith(prefix)) - { - idx = prefix.Length; - break; - } - } - - return idx > 0 ? value.Substring(idx) : value; - } - - internal static T[] Reverse(this T[] array) - { - var len = array.Length; - var reverse = new T[len]; - - var end = len - 1; - for (var i = 0; i <= end; i++) - { - reverse[i] = array[end - i]; - } - - return reverse; - } - - internal static IEnumerable SplitHeaderValue( - this string value, params char[] separators) - { - var len = value.Length; - var seps = new string(separators); - - var buff = new StringBuilder(32); - var escaped = false; - var quoted = false; - - for (var i = 0; i < len; i++) - { - var c = value[i]; - if (c == '"') - { - if (escaped) - { - escaped = !escaped; - } - else - { - quoted = !quoted; - } - } - else if (c == '\\') - { - if (i < len - 1 && value[i + 1] == '"') - { - escaped = true; - } - } - else if (seps.Contains(c)) - { - if (!quoted) - { - yield return buff.ToString(); - buff.Length = 0; - - continue; - } - } - else - { - } - - buff.Append(c); - } - - if (buff.Length > 0) - { - yield return buff.ToString(); - } - } - - internal static byte[] ToByteArray(this Stream stream) - { - using (var output = new MemoryStream()) - { - stream.Position = 0; - stream.CopyTo(output, BUFFER_SIZE); - output.Close(); - - return output.ToArray(); - } - } - - internal static CompressionMethod ToCompressionMethod(this string value) - { - foreach (CompressionMethod method in Enum.GetValues(typeof(CompressionMethod))) - { - 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 m = string.Format("permessage-{0}", method.ToString().ToLower()); - if (parameters == null || parameters.Length == 0) - { - return m; - } - - return string.Format("{0}; {1}", m, parameters.ToString("; ")); - } - - internal static System.Net.IPAddress ToIPAddress(this string value, bool useIPv6 = false) - { - if (value == null || value.Length == 0) - { - return null; - } - - if (System.Net.IPAddress.TryParse(value, out System.Net.IPAddress addr)) - { - return addr; - } - - try - { - var addrs = System.Net.Dns.GetHostAddresses(value); - - if (useIPv6) - { - // Find the first IPv6 address in the list, if available - var ipv6Addr = addrs.FirstOrDefault(a => a.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6); - if (ipv6Addr != null) - { - return ipv6Addr; - } - } - - // If IPv6 is not requested or not available, return the first IPv4 address - return addrs.FirstOrDefault(a => a.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork); - } - 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 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 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 scheme = uri.Scheme; - if (!(scheme == "ws" || scheme == "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( - $"{scheme}://{uri.Host}:{(scheme == "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 < 0) - { - return value; - } - - var end = value.LastIndexOf('"'); - var len = end - start - 1; - - return len < 0 - ? value - : len == 0 - ? string.Empty - : value.Substring(start + 1, len).Replace("\\\"", "\""); - } - - internal static string UTF8Decode(this byte[] bytes) - { - try - { - return Encoding.UTF8.GetString(bytes); - } - catch - { - return null; - } - } - - internal static byte[] UTF8Encode(this string s) - { - return Encoding.UTF8.GetBytes(s); - } - - internal static void WriteBytes(this Stream stream, byte[] bytes, int bufferLength) - { - using (var input = new MemoryStream(bytes)) - { - input.CopyTo(stream, bufferLength); - } - } - - internal static void WriteBytesAsync( - this Stream stream, byte[] bytes, int bufferLength, Action completed, Action error) - { - var input = new MemoryStream(bytes); - input.CopyToAsync( - stream, - bufferLength, - () => - { - completed?.Invoke(); - - input.Dispose(); - }, - ex => - { - input.Dispose(); - error?.Invoke(ex); - }); - } - - private static byte[] compress(this byte[] data) - { - if (data.LongLength == 0) - { - 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, BUFFER_SIZE); - 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, BUFFER_SIZE); - output.Position = 0; - - return output; - } - } - - private static byte[] decompressToArray(this Stream stream) - { - using (var output = stream.decompress()) - { - output.Close(); - return output.ToArray(); - } - } - - private static void times(this ulong n, Action action) - { - for (ulong i = 0; i < n; i++) - { - action(); - } - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/FinalFrame.cs b/EonaCat.Network/System/Sockets/Web/FinalFrame.cs deleted file mode 100644 index 1b1f32e..0000000 --- a/EonaCat.Network/System/Sockets/Web/FinalFrame.cs +++ /dev/null @@ -1,12 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -namespace EonaCat.Network -{ - internal enum FinalFrame : byte - { - More = 0x0, - - Final = 0x1 - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Mask.cs b/EonaCat.Network/System/Sockets/Web/Mask.cs deleted file mode 100644 index 14a2d0f..0000000 --- a/EonaCat.Network/System/Sockets/Web/Mask.cs +++ /dev/null @@ -1,12 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -namespace EonaCat.Network -{ - internal enum Mask : byte - { - Off = 0x0, - - On = 0x1 - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/OperationCode.cs b/EonaCat.Network/System/Sockets/Web/OperationCode.cs deleted file mode 100644 index cdcb124..0000000 --- a/EonaCat.Network/System/Sockets/Web/OperationCode.cs +++ /dev/null @@ -1,20 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -namespace EonaCat.Network -{ - internal enum OperationCode : byte - { - Continue = 0x0, - - Text = 0x1, - - Binary = 0x2, - - Close = 0x8, - - Ping = 0x9, - - Pong = 0xa - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Payload.cs b/EonaCat.Network/System/Sockets/Web/Payload.cs deleted file mode 100644 index fc0937c..0000000 --- a/EonaCat.Network/System/Sockets/Web/Payload.cs +++ /dev/null @@ -1,155 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.Collections; -using System.Collections.Generic; - -namespace EonaCat.Network -{ - internal class Payload : IEnumerable - { - private ushort _code; - private bool _codeSet; - private readonly byte[] _data; - private readonly long _length; - private string _reason; - private bool _reasonSet; - - public static readonly Payload Empty; - - public static readonly ulong MaxLength; - - static Payload() - { - Empty = new Payload(); - MaxLength = long.MaxValue; - } - - internal Payload() - { - _code = (ushort)CloseStatusCode.NoStatus; - _reason = string.Empty; - - _data = WSClient.EmptyBytes; - - _codeSet = true; - _reasonSet = true; - } - - internal Payload(Payload original) - { - _code = original._code; - _codeSet = original._codeSet; - ExtensionDataLength = original.ExtensionDataLength; - _length = original._length; - _reason = original._reason; - _reasonSet = original._reasonSet; - - _data = new byte[_length]; - original._data.CopyTo(_data, 0); - } - - internal Payload(byte[] data) - : this(data, data.LongLength) - { - } - - internal Payload(byte[] data, long length) - { - _data = data; - _length = length; - } - - internal Payload(ushort code, string reason) - { - _code = code; - _reason = reason ?? string.Empty; - - _data = code.Append(reason); - _length = _data.LongLength; - - _codeSet = true; - _reasonSet = true; - } - - internal ushort Code - { - get - { - if (!_codeSet) - { - _code = _length > 1 - ? _data.SubArray(0, 2).ToUInt16(ByteOrder.Big) - : (ushort)CloseStatusCode.NoStatus; - - _codeSet = true; - } - - return _code; - } - } - - internal long ExtensionDataLength { get; set; } - - internal bool HasReservedCode => _length > 1 && Code.IsReserved(); - - internal string Reason - { - get - { - if (!_reasonSet) - { - _reason = _length > 2 - ? _data.SubArray(2, _length - 2).UTF8Decode() - : string.Empty; - - _reasonSet = true; - } - - return _reason; - } - } - - public byte[] ApplicationData => ExtensionDataLength > 0 - ? _data.SubArray(ExtensionDataLength, _length - ExtensionDataLength) - : _data; - - public byte[] ExtensionData => ExtensionDataLength > 0 - ? _data.SubArray(0, ExtensionDataLength) - : WSClient.EmptyBytes; - - public ulong Length => (ulong)_length; - - internal void Mask(byte[] key) - { - for (long i = 0; i < _length; i++) - { - _data[i] = (byte)(_data[i] ^ key[i % 4]); - } - } - - public IEnumerator GetEnumerator() - { - foreach (var b in _data) - { - yield return b; - } - } - - public byte[] ToArray() - { - return _data; - } - - public override string ToString() - { - return BitConverter.ToString(_data); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/ReservedBits.cs b/EonaCat.Network/System/Sockets/Web/ReservedBits.cs deleted file mode 100644 index d49e872..0000000 --- a/EonaCat.Network/System/Sockets/Web/ReservedBits.cs +++ /dev/null @@ -1,12 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -namespace EonaCat.Network -{ - internal enum ReservedBits : byte - { - Off = 0x0, - - On = 0x1 - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Server/HttpRequestEventArgs.cs b/EonaCat.Network/System/Sockets/Web/Server/HttpRequestEventArgs.cs deleted file mode 100644 index 98bd225..0000000 --- a/EonaCat.Network/System/Sockets/Web/Server/HttpRequestEventArgs.cs +++ /dev/null @@ -1,102 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.IO; -using System.Security.Principal; -using System.Text; - -namespace EonaCat.Network -{ - public class HttpRequestEventArgs : EventArgs - { - private readonly HttpListenerContext _context; - private readonly string _docRootPath; - - internal HttpRequestEventArgs( - HttpListenerContext context, string documentRootPath - ) - { - _context = context; - _docRootPath = documentRootPath; - } - - public HttpListenerRequest Request => _context.Request; - - public HttpListenerResponse Response => _context.Response; - - public IPrincipal User => _context.User; - - public byte[] ReadFile(string path) - { - if (path == null) - { - throw new ArgumentNullException(nameof(path)); - } - - if (path.Length == 0) - { - throw new ArgumentException("An empty string.", nameof(path)); - } - - if (path.IndexOf("..") > -1) - { - throw new ArgumentException("It contains '..'.", nameof(path)); - } - - tryReadFile(CreateFilePath(path), out byte[] contents); - - return contents; - } - - public bool TryReadFile(string path, out byte[] contents) - { - if (path == null) - { - throw new ArgumentNullException(nameof(path)); - } - - if (path.Length == 0) - { - throw new ArgumentException("An empty string.", nameof(path)); - } - - if (path.IndexOf("..") > -1) - { - throw new ArgumentException("It contains '..'.", nameof(path)); - } - - return tryReadFile(CreateFilePath(path), out contents); - } - - 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; - } - - private string CreateFilePath(string childPath) - { - childPath = childPath.TrimStart('/', '\\'); - return new StringBuilder(_docRootPath, 32) - .AppendFormat("/{0}", childPath) - .ToString() - .Replace('\\', '/'); - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Server/HttpServer.cs b/EonaCat.Network/System/Sockets/Web/Server/HttpServer.cs deleted file mode 100644 index fefac57..0000000 --- a/EonaCat.Network/System/Sockets/Web/Server/HttpServer.cs +++ /dev/null @@ -1,885 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.IO; -using System.Security.Principal; -using System.Threading; - -namespace EonaCat.Network -{ - public class HttpServer - { - private bool _allowForwardedRequest; - private string _docRootPath; - private string _hostname; - private HttpListener _listener; - private Thread _receiveThread; - private volatile ServerState _state; - private object _locker; - - public HttpServer() - { - init("*", System.Net.IPAddress.Any, 80, false); - } - - public HttpServer(int port) - : this(port, port == 443) - { - } - - public HttpServer(string url, bool forceIpV6 = false) - { - if (url == null) - { - throw new ArgumentNullException(nameof(url)); - } - - if (url.Length == 0) - { - throw new ArgumentException("An empty string.", nameof(url)); - } - - if (!tryCreateUri(url, out Uri uri, out string message)) - { - throw new ArgumentException(message, nameof(url)); - } - - var host = uri.GetDnsSafeHost(true); - - var addr = host.ToIPAddress(forceIpV6); - if (addr == null) - { - message = "The host part could not be converted to an IP address."; - throw new ArgumentException(message, nameof(url)); - } - - if (!addr.IsLocal()) - { - message = "The IP address of the host is not a local IP address."; - throw new ArgumentException(message, nameof(url)); - } - - init(host, addr, uri.Port, uri.Scheme == "https"); - } - - public HttpServer(int port, bool secure) - { - if (!port.IsPortNumber()) - { - var message = "Less than 1 or greater than 65535."; - throw new ArgumentOutOfRangeException(nameof(port), message); - } - - init("*", System.Net.IPAddress.Any, port, secure); - } - - public HttpServer(System.Net.IPAddress address, int port) - : this(address, port, port == 443) - { - } - - public HttpServer(System.Net.IPAddress address, int port, bool secure) - { - if (address == null) - { - throw new ArgumentNullException(nameof(address)); - } - - if (!address.IsLocal()) - { - throw new ArgumentException("Not a local IP address.", nameof(address)); - } - - if (!port.IsPortNumber()) - { - var message = "Less than 1 or greater than 65535."; - throw new ArgumentOutOfRangeException(nameof(port), message); - } - - init(address.ToString(true), address, port, secure); - } - - public event EventHandler OnConnect; - - public event EventHandler OnDelete; - - public event EventHandler OnGet; - - public event EventHandler OnHead; - - public event EventHandler OnOptions; - - public event EventHandler OnPatch; - - public event EventHandler OnPost; - - public event EventHandler OnPut; - - public event EventHandler OnTrace; - - public System.Net.IPAddress Address { get; private set; } - - /// - /// 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 { _allowForwardedRequest = value; } - } - - public AuthenticationSchemes AuthenticationSchemes - { - get - { - return _listener.AuthenticationSchemes; - } - - set - { - if (!canSet(out string message)) - { - Logger.Warning(message); - return; - } - - lock (_locker) - { - if (!canSet(out message)) - { - Logger.Warning(message); - return; - } - - _listener.AuthenticationSchemes = value; - } - } - } - - public string DocumentRootPath - { - get - { - return _docRootPath; - } - - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - - if (value.Length == 0) - { - throw new ArgumentException("An empty string.", nameof(value)); - } - - value = value.TrimSlashOrBackslashFromEnd(); - - string full = null; - try - { - full = Path.GetFullPath(value); - } - catch (Exception ex) - { - throw new ArgumentException("An invalid path string.", nameof(value), ex); - } - - if (value == "/") - { - throw new ArgumentException("An absolute root.", nameof(value)); - } - - if (value == "\\") - { - throw new ArgumentException("An absolute root.", nameof(value)); - } - - if (value.Length == 2 && value[1] == ':') - { - throw new ArgumentException("An absolute root.", nameof(value)); - } - - if (full == "/") - { - throw new ArgumentException("An absolute root.", nameof(value)); - } - - full = full.TrimSlashOrBackslashFromEnd(); - if (full.Length == 2 && full[1] == ':') - { - throw new ArgumentException("An absolute root.", nameof(value)); - } - - if (!canSet(out string message)) - { - Logger.Warning(message); - return; - } - - lock (_locker) - { - if (!canSet(out message)) - { - Logger.Warning(message); - return; - } - - _docRootPath = value; - } - } - } - - public bool IsListening => _state == ServerState.Start; - - public bool IsLoggingEnabled { get; private set; } - public bool IsSecure { get; private set; } - - public bool KeepClean - { - get - { - return WSEndpoints.AutoCleanSessions; - } - - set - { - WSEndpoints.AutoCleanSessions = value; - } - } - - public int Port { get; private set; } - - public string Realm - { - get - { - return _listener.Realm; - } - - set - { - if (!canSet(out string message)) - { - Logger.Warning(message); - return; - } - - lock (_locker) - { - if (!canSet(out message)) - { - Logger.Warning(message); - return; - } - - _listener.Realm = value; - } - } - } - - public bool ReuseAddress - { - get - { - return _listener.ReuseAddress; - } - - set - { - if (!canSet(out string message)) - { - Logger.Warning(message); - return; - } - - lock (_locker) - { - if (!canSet(out message)) - { - Logger.Warning(message); - return; - } - - _listener.ReuseAddress = value; - } - } - } - - public SSLConfigServer SSL - { - get - { - if (!IsSecure) - { - var message = "This instance does not provide secure connections."; - throw new InvalidOperationException(message); - } - - return _listener.SSL; - } - } - - public Func UserCredentialsFinder - { - get - { - return _listener.UserCredentialsFinder; - } - - set - { - if (!canSet(out string message)) - { - Logger.Warning(message); - return; - } - - lock (_locker) - { - if (!canSet(out message)) - { - Logger.Warning(message); - return; - } - - _listener.UserCredentialsFinder = value; - } - } - } - - public TimeSpan ResponseWaitingTime - { - get - { - return WSEndpoints.ResponseWaitingTime; - } - - set - { - WSEndpoints.ResponseWaitingTime = value; - } - } - - public WSEndpointManager WSEndpoints { get; private set; } - - public void AddWebSocketService(string path) - where TEndpoint : WSEndpoint, new() - { - WSEndpoints.AddEndpoint(path, null); - } - - public void AddWebSocketService( - string path, Action initializer - ) - where TEndpoint : WSEndpoint, new() - { - WSEndpoints.AddEndpoint(path, initializer); - } - - public bool RemoveWebSocketService(string path) - { - return WSEndpoints.RemoveEndpoint(path); - } - - public void Start() - { - if (IsSecure) - { - if (!checkCertificate(out string message)) - { - throw new InvalidOperationException(message); - } - } - - start(); - } - - public void Stop() - { - stop((ushort)CloseStatusCode.NoStatus, string.Empty); - } - - public void Stop(ushort code, string reason) - { - if (!code.IsCloseStatusCode()) - { - var message = "Less than 1000 or greater than 4999."; - throw new ArgumentOutOfRangeException(nameof(code), message); - } - - if (code == (ushort)CloseStatusCode.MissingExtension) - { - var message = $"{(ushort)CloseStatusCode.MissingExtension} cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - if (!reason.IsNullOrEmpty()) - { - if (code == (ushort)CloseStatusCode.NoStatus) - { - var message = $"{(ushort)CloseStatusCode.NoStatus} cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - if (!reason.TryGetUTF8EncodedBytes(out byte[] bytes)) - { - var message = "It could not be UTF-8-encoded."; - throw new ArgumentException(message, nameof(reason)); - } - - if (bytes.Length > 123) - { - var message = "Its size is greater than 123 bytes."; - throw new ArgumentOutOfRangeException(nameof(reason), message); - } - } - - stop(code, reason); - } - - public void Stop(CloseStatusCode code, string reason) - { - if (code == CloseStatusCode.MissingExtension) - { - var message = "MandatoryExtension cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - if (!reason.IsNullOrEmpty()) - { - if (code == CloseStatusCode.NoStatus) - { - var message = "NoStatus cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - if (!reason.TryGetUTF8EncodedBytes(out byte[] bytes)) - { - var message = "It could not be UTF-8-encoded."; - throw new ArgumentException(message, nameof(reason)); - } - - if (bytes.Length > 123) - { - var message = "Its size is greater than 123 bytes."; - throw new ArgumentOutOfRangeException(nameof(reason), message); - } - } - - stop((ushort)code, reason); - } - - 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 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; - } - - private void abort() - { - lock (_locker) - { - if (_state != ServerState.Start) - { - return; - } - - _state = ServerState.ShuttingDown; - } - - try - { - try - { - WSEndpoints.Stop((ushort)CloseStatusCode.Abnormal, 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.SSL.Certificate != null; - - var path = _listener.CertificateFolderPath; - var withPort = EndPointListener.CertificateExists(Port, path); - - var both = byUser && withPort; - if (both) - { - Logger.Warning("A server certificate associated with the port is used."); - return true; - } - - var either = byUser || withPort; - if (!either) - { - message = "There is no server certificate for secure connections."; - return false; - } - - return true; - } - - private void init( - string hostname, System.Net.IPAddress address, int port, bool secure - ) - { - _hostname = hostname; - Address = address; - Port = port; - IsSecure = secure; - - _docRootPath = "./Public"; - _listener = createListener(_hostname, Port, IsSecure); - WSEndpoints = new WSEndpointManager(); - _locker = 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 == "OPTIONS" - ? OnOptions - : method == "TRACE" - ? OnTrace - : method == "CONNECT" - ? OnConnect - : method == "PATCH" - ? OnPatch - : null; - - if (evt != null) - { - evt(this, new HttpRequestEventArgs(context, _docRootPath)); - } - else - { - context.Response.StatusCode = 501; // Not Implemented - } - - context.Response.Close(); - } - - private void processRequest(HttpListenerWSContext context) - { - var path = context.RequestUri.AbsolutePath; - - if (!WSEndpoints.InternalTryGetEndpointHost(path, out WSEndpointHost host)) - { - context.Close(HttpStatusCode.NotImplemented); - return; - } - - host.StartSession(context); - } - - private void receiveRequest() - { - while (true) - { - HttpListenerContext context = null; - try - { - context = _listener.GetContext(); - ThreadPool.QueueUserWorkItem( - state => - { - try - { - if (context.Request.IsUpgradeTo("websocket")) - { - processRequest(context.AcceptWebSocket(null)); - return; - } - - processRequest(context); - } - catch (Exception ex) - { - Logger.Error(ex.Message); - Logger.Debug(ex.ToString()); - - context.Connection.Close(true); - } - } - ); - } - catch (HttpListenerException) - { - Logger.Info("The underlying listener is stopped."); - break; - } - catch (InvalidOperationException) - { - Logger.Info("The underlying listener is stopped."); - break; - } - catch (Exception ex) - { - Logger.Error(ex.Message); - Logger.Debug(ex.ToString()); - - context?.Connection.Close(true); - - break; - } - } - - if (_state != ServerState.ShuttingDown) - { - abort(); - } - } - - private void start() - { - if (_state == ServerState.Start) - { - Logger.Info("The server has already started."); - return; - } - - if (_state == ServerState.ShuttingDown) - { - Logger.Warning("The server is shutting down."); - return; - } - - lock (_locker) - { - if (_state == ServerState.Start) - { - Logger.Info("The server has already started."); - return; - } - - if (_state == ServerState.ShuttingDown) - { - Logger.Warning("The server is shutting down."); - return; - } - - WSEndpoints.Start(); - - try - { - startReceiving(); - } - catch - { - WSEndpoints.Stop((ushort)CloseStatusCode.ServerError, string.Empty); - throw; - } - - _state = ServerState.Start; - } - } - - private void startReceiving() - { - try - { - _listener.Start(); - } - catch (Exception exception) - { - var message = "The underlying listener has failed to start."; - throw new InvalidOperationException(message, exception); - } - - _receiveThread = new Thread(new ThreadStart(receiveRequest)); - _receiveThread.IsBackground = true; - _receiveThread.Start(); - } - - private void stop(ushort code, string reason) - { - if (_state == ServerState.Started) - { - Logger.Info("The server is not started."); - return; - } - - if (_state == ServerState.ShuttingDown) - { - Logger.Info("The server is shutting down."); - return; - } - - if (_state == ServerState.Stop) - { - Logger.Info("The server has already stopped."); - return; - } - - lock (_locker) - { - if (_state == ServerState.ShuttingDown) - { - Logger.Info("The server is shutting down."); - return; - } - - if (_state == ServerState.Stop) - { - Logger.Info("The server has already stopped."); - return; - } - - _state = ServerState.ShuttingDown; - } - - try - { - var threw = false; - try - { - WSEndpoints.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); - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Server/IWSSession.cs b/EonaCat.Network/System/Sockets/Web/Server/IWSSession.cs deleted file mode 100644 index 826a9cd..0000000 --- a/EonaCat.Network/System/Sockets/Web/Server/IWSSession.cs +++ /dev/null @@ -1,20 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; - -namespace EonaCat.Network -{ - public interface IWSSession - { - WSContext Context { get; } - - string ID { get; } - - string Protocol { get; } - - DateTime StartTime { get; } - - WSState State { get; } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Server/ServerState.cs b/EonaCat.Network/System/Sockets/Web/Server/ServerState.cs deleted file mode 100644 index b9e8f3d..0000000 --- a/EonaCat.Network/System/Sockets/Web/Server/ServerState.cs +++ /dev/null @@ -1,13 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -namespace EonaCat.Network -{ - internal enum ServerState - { - Started, - Start, - ShuttingDown, - Stop - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Server/WSEndpoint.cs b/EonaCat.Network/System/Sockets/Web/Server/WSEndpoint.cs deleted file mode 100644 index cf0cdaa..0000000 --- a/EonaCat.Network/System/Sockets/Web/Server/WSEndpoint.cs +++ /dev/null @@ -1,209 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.IO; - -namespace EonaCat.Network -{ - public abstract class WSEndpoint : IWSSession - { - private bool _emitOnPing; - private string _protocol; - private WSClient _websocket; - - protected WSEndpoint() - { - StartTime = DateTime.MaxValue; - } - - public WSContext Context { get; private set; } - public Func CookiesValidator { get; set; } - - public bool EmitOnPing - { - get - { - return _websocket != null ? _websocket.CallMessageOnPing : _emitOnPing; - } - - set - { - if (_websocket != null) - { - _websocket.CallMessageOnPing = value; - return; - } - - _emitOnPing = value; - } - } - - public string ID { get; private set; } - public bool IgnoreExtensions { get; set; } - public Func OriginValidator { get; set; } - - public string Protocol - { - get - { - return _websocket != null ? _websocket.Protocol : (_protocol ?? string.Empty); - } - - set - { - if (State != WSState.Connecting) - { - return; - } - - if (value != null && (value.Length == 0 || !value.IsToken())) - { - return; - } - - _protocol = value; - } - } - - public DateTime StartTime { get; private set; } - public WSState State => _websocket != null ? _websocket.ReadyState : WSState.Connecting; - protected WSSessionManager Sessions { get; private set; } - - internal void Start(WSContext context, WSSessionManager sessions) - { - if (_websocket != null) - { - Logger.Error("A session instance cannot be reused."); - context.WebSocket.Close(HttpStatusCode.ServiceUnavailable); - - return; - } - - Context = context; - Sessions = sessions; - - _websocket = context.WebSocket; - _websocket.CustomHandshakeRequestChecker = checkHandshakeRequest; - _websocket.CallMessageOnPing = _emitOnPing; - _websocket.IgnoreExtensions = IgnoreExtensions; - _websocket.Protocol = _protocol; - - var responseWaitingTime = sessions.ResponseWaitingTime; - if (responseWaitingTime != _websocket.ResponseWaitingTime) - { - _websocket.ResponseWaitingTime = responseWaitingTime; - } - - _websocket.OnConnect += onOpen; - _websocket.OnMessageReceived += onMessage; - _websocket.OnError += onError; - _websocket.OnDisconnect += onClose; - - _websocket.InternalAccept(); - } - - protected void Error(string message, Exception exception) - { - if (message != null && message.Length > 0) - { - OnError(new ErrorEventArgs(message, exception)); - } - } - - protected virtual void OnClose(CloseEventArgs e) - { - } - - protected virtual void OnError(ErrorEventArgs e) - { - } - - protected virtual void OnMessage(MessageEventArgs e) - { - } - - protected virtual void OnOpen() - { - } - - protected void Send(byte[] data) - { - _websocket?.Send(data); - } - - protected void Send(FileInfo file) - { - _websocket?.Send(file); - } - - protected void Send(string data) - { - _websocket?.Send(data); - } - - protected void SendAsync(byte[] data, Action completed) - { - _websocket?.SendAsync(data, completed); - } - - protected void SendAsync(FileInfo file, Action completed) - { - _websocket?.SendAsync(file, completed); - } - - protected void SendAsync(string data, Action completed) - { - _websocket?.SendAsync(data, completed); - } - - protected void SendAsync(Stream stream, int length, Action completed) - { - _websocket?.SendAsync(stream, length, completed); - } - - private string checkHandshakeRequest(WSContext context) - { - return OriginValidator != null && !OriginValidator(context.Origin) - ? "Includes no Origin header, or it has an invalid value." - : CookiesValidator != null - && !CookiesValidator(context.CookieCollection, context.WebSocket.CookieCollection) - ? "Includes no cookie, or an invalid cookie exists." - : 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(); - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Server/WSEndpointHost.cs b/EonaCat.Network/System/Sockets/Web/Server/WSEndpointHost.cs deleted file mode 100644 index e6e7011..0000000 --- a/EonaCat.Network/System/Sockets/Web/Server/WSEndpointHost.cs +++ /dev/null @@ -1,66 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; - -namespace EonaCat.Network -{ - public abstract class WSEndpointHost - { - protected WSEndpointHost(string path) - { - Path = path; - Sessions = new WSSessionManager(); - } - - public abstract Type EndpointType { get; } - - public bool AutoCleanSessions - { - get - { - return Sessions.KeepClean; - } - - set - { - Sessions.KeepClean = value; - } - } - - public string Path { get; } - public WSSessionManager Sessions { get; } - - public TimeSpan ResponseWaitingTime - { - get - { - return Sessions.ResponseWaitingTime; - } - - set - { - Sessions.ResponseWaitingTime = value; - } - } - - internal ServerState State => Sessions.State; - - internal void Start() - { - Sessions.Start(); - } - - internal void StartSession(WSContext context) - { - CreateSession().Start(context, Sessions); - } - - internal void Stop(ushort code, string reason) - { - Sessions.Stop(code, reason); - } - - protected abstract WSEndpoint CreateSession(); - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Server/WSEndpointManager.cs b/EonaCat.Network/System/Sockets/Web/Server/WSEndpointManager.cs deleted file mode 100644 index d993dd5..0000000 --- a/EonaCat.Network/System/Sockets/Web/Server/WSEndpointManager.cs +++ /dev/null @@ -1,467 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; - -namespace EonaCat.Network -{ - public class WSEndpointManager - { - private readonly Dictionary _hosts; - private readonly object _locker; - private volatile bool _clean; - private volatile ServerState _state; - private TimeSpan _responseWaitingTime; - - internal WSEndpointManager() - { - _clean = true; - _hosts = new Dictionary(); - _state = ServerState.Started; - _locker = ((ICollection)_hosts).SyncRoot; - _responseWaitingTime = TimeSpan.FromSeconds(1); - } - - public bool AutoCleanSessions - { - get - { - return _clean; - } - - set - { - if (!canSet(out string message)) - { - Logger.Warning(message); - return; - } - - lock (_locker) - { - if (!canSet(out message)) - { - Logger.Warning(message); - return; - } - - foreach (var host in _hosts.Values) - { - host.AutoCleanSessions = value; - } - - _clean = value; - } - } - } - - public int Count - { - get - { - lock (_locker) - { - return _hosts.Count; - } - } - } - - public IEnumerable Hosts - { - get - { - lock (_locker) - { - return _hosts.Values.ToList(); - } - } - } - - public IEnumerable Paths - { - get - { - lock (_locker) - { - return _hosts.Keys.ToList(); - } - } - } - - public TimeSpan ResponseWaitingTime - { - get - { - return _responseWaitingTime; - } - - set - { - if (value <= TimeSpan.Zero) - { - throw new ArgumentOutOfRangeException(nameof(value), "Zero or less."); - } - - if (!canSet(out string message)) - { - Logger.Warning(message); - return; - } - - lock (_locker) - { - if (!canSet(out message)) - { - Logger.Warning(message); - return; - } - - foreach (var host in _hosts.Values) - { - host.ResponseWaitingTime = value; - } - - _responseWaitingTime = value; - } - } - } - - public WSEndpointHost this[string path] - { - get - { - if (path == null) - { - throw new ArgumentNullException(nameof(path)); - } - - if (path.Length == 0) - { - throw new ArgumentException("An empty string.", nameof(path)); - } - - if (path[0] != '/') - { - throw new ArgumentException("Not an absolute path.", nameof(path)); - } - - if (path.IndexOfAny(new[] { '?', '#' }) > -1) - { - var message = "It includes either or both query and fragment components."; - throw new ArgumentException(message, nameof(path)); - } - - InternalTryGetEndpointHost(path, out WSEndpointHost host); - - return host; - } - } - - public void AddEndpoint( - string path, Action initializer - ) - where TEndpoint : WSEndpoint, new() - { - if (path == null) - { - throw new ArgumentNullException(nameof(path)); - } - - if (path.Length == 0) - { - throw new ArgumentException("An empty string.", nameof(path)); - } - - if (path[0] != '/') - { - throw new ArgumentException("Not an absolute path.", nameof(path)); - } - - if (path.IndexOfAny(new[] { '?', '#' }) > -1) - { - var message = "It includes either or both query and fragment components."; - throw new ArgumentException(message, nameof(path)); - } - - path = HttpUtility.UrlDecode(path).TrimSlashFromEnd(); - - lock (_locker) - { - if (_hosts.TryGetValue(path, out WSEndpointHost host)) - { - throw new ArgumentException("Already in use.", nameof(path)); - } - - host = new WebSocketEndpointHost( - path, () => new TEndpoint(), initializer - ); - - if (!_clean) - { - host.AutoCleanSessions = false; - } - - if (_responseWaitingTime != host.ResponseWaitingTime) - { - host.ResponseWaitingTime = _responseWaitingTime; - } - - if (_state == ServerState.Start) - { - host.Start(); - } - - _hosts.Add(path, host); - } - } - - public void Clear() - { - List hosts = null; - - lock (_locker) - { - hosts = _hosts.Values.ToList(); - _hosts.Clear(); - } - - foreach (var host in hosts) - { - if (host.State == ServerState.Start) - { - host.Stop((ushort)CloseStatusCode.Away, string.Empty); - } - } - } - - public bool RemoveEndpoint(string path) - { - if (path == null) - { - throw new ArgumentNullException(nameof(path)); - } - - if (path.Length == 0) - { - throw new ArgumentException("An empty string.", nameof(path)); - } - - if (path[0] != '/') - { - throw new ArgumentException("Not an absolute path.", nameof(path)); - } - - if (path.IndexOfAny(new[] { '?', '#' }) > -1) - { - var message = "It includes either or both query and fragment components."; - throw new ArgumentException(message, nameof(path)); - } - - path = HttpUtility.UrlDecode(path).TrimSlashFromEnd(); - - WSEndpointHost host; - lock (_locker) - { - if (!_hosts.TryGetValue(path, out host)) - { - return false; - } - - _hosts.Remove(path); - } - - if (host.State == ServerState.Start) - { - host.Stop((ushort)CloseStatusCode.Away, string.Empty); - } - - return true; - } - - public bool TryGetEndpointHost(string path, out WSEndpointHost host) - { - if (path == null) - { - throw new ArgumentNullException(nameof(path)); - } - - if (path.Length == 0) - { - throw new ArgumentException("An empty string.", nameof(path)); - } - - if (path[0] != '/') - { - throw new ArgumentException("Not an absolute path.", nameof(path)); - } - - if (path.IndexOfAny(new[] { '?', '#' }) > -1) - { - var message = "It includes either or both query and fragment components."; - throw new ArgumentException(message, nameof(path)); - } - - return InternalTryGetEndpointHost(path, out host); - } - - internal void Add(string path, Func creator) - where TEndpoint : WSEndpoint - { - path = HttpUtility.UrlDecode(path).TrimSlashFromEnd(); - - lock (_locker) - { - if (_hosts.TryGetValue(path, out WSEndpointHost host)) - { - throw new ArgumentException("Already in use.", nameof(path)); - } - - host = new WebSocketEndpointHost( - path, creator, null - ); - - if (!_clean) - { - host.AutoCleanSessions = false; - } - - if (_responseWaitingTime != host.ResponseWaitingTime) - { - host.ResponseWaitingTime = _responseWaitingTime; - } - - if (_state == ServerState.Start) - { - host.Start(); - } - - _hosts.Add(path, host); - } - } - - internal bool InternalTryGetEndpointHost( - string path, out WSEndpointHost host - ) - { - path = HttpUtility.UrlDecode(path).TrimSlashFromEnd(); - - lock (_locker) - { - return _hosts.TryGetValue(path, out host); - } - } - - internal void Start() - { - lock (_locker) - { - foreach (var host in _hosts.Values) - { - host.Start(); - } - - _state = ServerState.Start; - } - } - - internal void Stop(ushort code, string reason) - { - lock (_locker) - { - _state = ServerState.ShuttingDown; - - foreach (var host in _hosts.Values) - { - host.Stop(code, reason); - } - - _state = ServerState.Stop; - } - } - - private void broadcast(OperationCode opcode, byte[] data, Action completed) - { - var cache = new Dictionary(); - - try - { - foreach (var host in Hosts) - { - if (_state != ServerState.Start) - { - Logger.Error("The server is shutting down."); - break; - } - - host.Sessions.Broadcast(opcode, data, cache); - } - - completed?.Invoke(); - } - catch (Exception ex) - { - Logger.Error(ex, "Could not broadcast"); - } - finally - { - cache.Clear(); - } - } - - private void broadcast(OperationCode opcode, Stream stream, Action completed) - { - var cache = new Dictionary(); - - try - { - foreach (var host in Hosts) - { - if (_state != ServerState.Start) - { - Logger.Error("The server is shutting down."); - break; - } - - host.Sessions.Broadcast(opcode, stream, cache); - } - - completed?.Invoke(); - } - catch (Exception ex) - { - Logger.Error(ex, "Could not broadcast"); - } - finally - { - foreach (var cached in cache.Values) - { - cached.Dispose(); - } - - cache.Clear(); - } - } - - 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; - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Server/WSServer.cs b/EonaCat.Network/System/Sockets/Web/Server/WSServer.cs deleted file mode 100644 index d4cd042..0000000 --- a/EonaCat.Network/System/Sockets/Web/Server/WSServer.cs +++ /dev/null @@ -1,786 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.Net.Sockets; -using System.Security.Principal; -using System.Threading; - -namespace EonaCat.Network -{ - public class WSServer - { - private static readonly string _defaultRealm = "EonaCat Network - Area51"; - private bool _allowForwardedRequest; - private AuthenticationSchemes _authSchemes; - private bool _dnsStyle; - private string _hostname; - private TcpListener _listener; - private string _realm; - private string _realmInUse; - private Thread _receiveThread; - private bool _reuseAddress; - private SSLConfigServer _sslConfig; - private SSLConfigServer _sslConfigInUse; - private volatile ServerState _state; - private object _locker; - private Func _userCredentialsFinder; - - public WSServer() - { - var address = System.Net.IPAddress.Any; - init(address.ToString(), address, 80, false); - } - - public WSServer(int port) - : this(port, port == 443) - { - } - - public WSServer(string url, bool forceIpV6 = false) - { - if (url == null) - { - throw new ArgumentNullException(nameof(url)); - } - - if (url.Length == 0) - { - throw new ArgumentException("An empty string.", nameof(url)); - } - - if (!tryCreateUri(url, out Uri uri, out string message)) - { - throw new ArgumentException(message, nameof(url)); - } - - var host = uri.DnsSafeHost; - - var addr = host.ToIPAddress(forceIpV6); - if (addr == null) - { - message = "The host part could not be converted to an IP address."; - throw new ArgumentException(message, nameof(url)); - } - - if (!addr.IsLocal()) - { - message = "The IP address of the host is not a local IP address."; - throw new ArgumentException(message, nameof(url)); - } - - init(host, addr, uri.Port, uri.Scheme == "wss"); - } - - public WSServer(int port, bool secure) - { - if (!port.IsPortNumber()) - { - var message = "Less than 1 or greater than 65535."; - throw new ArgumentOutOfRangeException(nameof(port), message); - } - - var addr = System.Net.IPAddress.Any; - init(addr.ToString(), addr, port, secure); - } - - public WSServer(System.Net.IPAddress address, int port) - : this(address, port, port == 443) - { - } - - public WSServer(System.Net.IPAddress address, int port, bool secure) - { - if (address == null) - { - throw new ArgumentNullException(nameof(address)); - } - - if (!address.IsLocal()) - { - throw new ArgumentException("Not a local IP address.", nameof(address)); - } - - if (!port.IsPortNumber()) - { - var message = "Less than 1 or greater than 65535."; - throw new ArgumentOutOfRangeException(nameof(port), message); - } - - init(address.ToString(), address, port, secure); - } - - public System.Net.IPAddress Address { get; private set; } - - public bool AllowForwardedRequest - { - get - { - return _allowForwardedRequest; - } - - set - { - if (!CanSet(out string message)) - { - Logger.Warning(message); - return; - } - - lock (_locker) - { - if (!CanSet(out message)) - { - Logger.Warning(message); - return; - } - - _allowForwardedRequest = value; - } - } - } - - public AuthenticationSchemes AuthenticationSchemes - { - get - { - return _authSchemes; - } - - set - { - if (!CanSet(out string message)) - { - Logger.Warning(message); - return; - } - - lock (_locker) - { - if (!CanSet(out message)) - { - Logger.Warning(message); - return; - } - - _authSchemes = value; - } - } - } - - /// - /// Determines if sessions need to be removed automatically - /// - public bool AutoCleanSessions - { - get - { - return Endpoints.AutoCleanSessions; - } - - set - { - Endpoints.AutoCleanSessions = value; - } - } - - public WSEndpointManager Endpoints { get; private set; } - - public Func FindCredentials - { - get - { - return _userCredentialsFinder; - } - - set - { - if (!CanSet(out string message)) - { - Logger.Warning(message); - return; - } - - lock (_locker) - { - if (!CanSet(out message)) - { - Logger.Warning(message); - return; - } - - _userCredentialsFinder = value; - } - } - } - - public bool IsConsoleLoggingEnabled { get; set; } - public bool IsListening => _state == ServerState.Start; - public bool IsLoggingEnabled { get; set; } - public bool IsSecure { get; private set; } - public int Port { get; private set; } - - public string Realm - { - get - { - return _realm; - } - - set - { - if (!CanSet(out string message)) - { - Logger.Warning(message); - return; - } - - lock (_locker) - { - if (!CanSet(out message)) - { - Logger.Warning(message); - return; - } - - _realm = value; - } - } - } - - public bool ReuseAddress - { - get - { - return _reuseAddress; - } - - set - { - if (!CanSet(out string message)) - { - Logger.Warning(message); - return; - } - - lock (_locker) - { - if (!CanSet(out message)) - { - Logger.Warning(message); - return; - } - - _reuseAddress = value; - } - } - } - - public SSLConfigServer SSL - { - get - { - if (!IsSecure) - { - var message = "This instance does not provide secure connections."; - throw new InvalidOperationException(message); - } - - return GetSSLConfig(); - } - } - - public TimeSpan ResponseWaitingTime - { - get - { - return Endpoints.ResponseWaitingTime; - } - - set - { - Endpoints.ResponseWaitingTime = value; - } - } - - public void AddEndpoint(string path) where TEndpoint : WSEndpoint, new() - { - Endpoints.AddEndpoint(path, null); - } - - public void AddEndpoint(string path, Action initializer) where TEndpoint : WSEndpoint, new() - { - Endpoints.AddEndpoint(path, initializer); - } - - public bool RemoveEndpoint(string path) - { - return Endpoints.RemoveEndpoint(path); - } - - public void Start() - { - SSLConfigServer sslConfig = null; - - if (IsSecure) - { - sslConfig = new SSLConfigServer(GetSSLConfig()); - - if (!CheckSslConfig(sslConfig, out string message)) - { - throw new InvalidOperationException(message); - } - } - - start(sslConfig); - } - - public void Stop() - { - stop((ushort)CloseStatusCode.NoStatus, string.Empty); - } - - public void Stop(ushort code, string reason) - { - if (!code.IsCloseStatusCode()) - { - var message = "Less than 1000 or greater than 4999."; - throw new ArgumentOutOfRangeException(nameof(code), message); - } - - if (code == (ushort)CloseStatusCode.MissingExtension) - { - var message = $"{(ushort)CloseStatusCode.MissingExtension} cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - if (!reason.IsNullOrEmpty()) - { - if (code == (ushort)CloseStatusCode.NoStatus) - { - var message = $"{(ushort)CloseStatusCode.NoStatus} cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - if (!reason.TryGetUTF8EncodedBytes(out byte[] bytes)) - { - var message = "It could not be UTF-8-encoded."; - throw new ArgumentException(message, nameof(reason)); - } - - if (bytes.Length > 123) - { - var message = "Its size is greater than 123 bytes."; - throw new ArgumentOutOfRangeException(nameof(reason), message); - } - } - - stop(code, reason); - } - - public void Stop(CloseStatusCode code, string reason) - { - if (code == CloseStatusCode.MissingExtension) - { - var message = "MandatoryExtension cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - if (!reason.IsNullOrEmpty()) - { - if (code == CloseStatusCode.NoStatus) - { - var message = "NoStatus cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - if (!reason.TryGetUTF8EncodedBytes(out byte[] bytes)) - { - var message = "It could not be UTF-8-encoded."; - throw new ArgumentException(message, nameof(reason)); - } - - if (bytes.Length > 123) - { - var message = "Its size is greater than 123 bytes."; - throw new ArgumentOutOfRangeException(nameof(reason), message); - } - } - - stop((ushort)code, reason); - } - - private static bool CheckSslConfig(SSLConfigServer sslConfig, out string message - ) - { - message = null; - - if (sslConfig.Certificate == null) - { - message = "There is no server certificate for secure connections."; - return false; - } - - return true; - } - - 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; - } - - private void abort() - { - lock (_locker) - { - if (_state != ServerState.Start) - { - return; - } - - _state = ServerState.ShuttingDown; - } - - try - { - try - { - _listener.Stop(); - } - finally - { - Endpoints.Stop((ushort)CloseStatusCode.Abnormal, string.Empty); - } - } - 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 CheckHostNameForRequest(string name) - { - return !_dnsStyle - || Uri.CheckHostName(name) != UriHostNameType.Dns - || name == _hostname; - } - - private string GetRealm() - { - var realm = _realm; - return realm != null && realm.Length > 0 ? realm : _defaultRealm; - } - - private SSLConfigServer GetSSLConfig() - { - _sslConfig ??= new SSLConfigServer(); - - return _sslConfig; - } - - private void init( - string hostname, System.Net.IPAddress address, int port, bool secure - ) - { - _hostname = hostname; - Address = address; - Port = port; - IsSecure = secure; - - _authSchemes = AuthenticationSchemes.Anonymous; - _dnsStyle = Uri.CheckHostName(hostname) == UriHostNameType.Dns; - _listener = new TcpListener(address, port); - Endpoints = new WSEndpointManager(); - _locker = new object(); - } - - private void processRequest(TcpListenerWSContext context) - { - 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; - } - } - - if (!Endpoints.InternalTryGetEndpointHost(uri.AbsolutePath, out WSEndpointHost 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 context = new TcpListenerWSContext( - cl, null, IsSecure, _sslConfigInUse); - - if (!context.Authenticate(_authSchemes, _realmInUse, _userCredentialsFinder)) - { - return; - } - - processRequest(context); - } - catch (Exception ex) - { - Logger.Error(ex.Message); - Logger.Debug(ex.ToString()); - - cl.Close(); - } - } - ); - } - catch (SocketException ex) - { - if (_state == ServerState.ShuttingDown) - { - Logger.Info("The underlying listener is stopped."); - break; - } - - Logger.Error(ex.Message); - Logger.Debug(ex.ToString()); - - break; - } - catch (Exception ex) - { - Logger.Error(ex.Message); - Logger.Debug(ex.ToString()); - - cl?.Close(); - - break; - } - } - - if (_state != ServerState.ShuttingDown) - { - abort(); - } - } - - private void start(SSLConfigServer sslConfig) - { - Logger.IsLoggingEnabled = IsLoggingEnabled; - Logger.DisableConsole = !IsConsoleLoggingEnabled; - - if (_state == ServerState.Start) - { - Logger.Info("The server has already started."); - return; - } - - if (_state == ServerState.ShuttingDown) - { - Logger.Warning("The server is shutting down."); - return; - } - - lock (_locker) - { - if (_state == ServerState.Start) - { - Logger.Info("The server has already started."); - return; - } - - if (_state == ServerState.ShuttingDown) - { - Logger.Warning("The server is shutting down."); - return; - } - - _sslConfigInUse = sslConfig; - _realmInUse = GetRealm(); - - Endpoints.Start(); - try - { - startReceiving(); - } - catch - { - Endpoints.Stop((ushort)CloseStatusCode.ServerError, 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 message = "The underlying listener has failed to start."; - throw new InvalidOperationException(message, ex); - } - - _receiveThread = new Thread(new ThreadStart(receiveRequest)); - _receiveThread.IsBackground = true; - _receiveThread.Start(); - } - - private void stop(ushort code, string reason) - { - if (_state == ServerState.Started) - { - Logger.Info("The server is not started."); - return; - } - - if (_state == ServerState.ShuttingDown) - { - Logger.Info("The server is shutting down."); - return; - } - - if (_state == ServerState.Stop) - { - Logger.Info("The server has already stopped."); - return; - } - - lock (_locker) - { - if (_state == ServerState.ShuttingDown) - { - Logger.Info("The server is shutting down."); - return; - } - - if (_state == ServerState.Stop) - { - Logger.Info("The server has already stopped."); - return; - } - - _state = ServerState.ShuttingDown; - } - - try - { - var threw = false; - try - { - stopReceiving(5000); - } - catch - { - threw = true; - throw; - } - finally - { - try - { - Endpoints.Stop(code, reason); - } - catch - { - if (!threw) - { - throw; - } - } - } - } - finally - { - _state = ServerState.Stop; - } - } - - private void stopReceiving(int millisecondsTimeout) - { - try - { - _listener.Stop(); - } - catch (Exception ex) - { - var message = "The underlying listener has failed to stop."; - throw new InvalidOperationException(message, ex); - } - - _receiveThread.Join(millisecondsTimeout); - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Server/WSSessionManager.cs b/EonaCat.Network/System/Sockets/Web/Server/WSSessionManager.cs deleted file mode 100644 index 77fd73c..0000000 --- a/EonaCat.Network/System/Sockets/Web/Server/WSSessionManager.cs +++ /dev/null @@ -1,871 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; - -namespace EonaCat.Network -{ - public class WSSessionManager - { - private const int _cleanupIntervalInMiliSeconds = 60000; - private readonly object _cleanLocker; - private readonly Dictionary _sessions; - private readonly object _locker; - private volatile bool _clean; - private volatile ServerState _state; - private volatile bool _sweeping; - private System.Timers.Timer _sweepTimer; - private TimeSpan _responseWaitingTime; - - internal WSSessionManager() - { - _clean = true; - _cleanLocker = new object(); - _sessions = new Dictionary(); - _state = ServerState.Started; - _locker = ((ICollection)_sessions).SyncRoot; - _responseWaitingTime = TimeSpan.FromSeconds(1); - - setCleanupTimer(_cleanupIntervalInMiliSeconds); - } - - public IEnumerable ActiveIDs - { - get - { - foreach (var res in broadping(WSFrame.EmptyPingBytes)) - { - if (res.Value) - { - yield return res.Key; - } - } - } - } - - public int Count - { - get - { - lock (_locker) - { - return _sessions.Count; - } - } - } - - public IEnumerable IDs - { - get - { - if (_state != ServerState.Start) - { - return Enumerable.Empty(); - } - - lock (_locker) - { - if (_state != ServerState.Start) - { - return Enumerable.Empty(); - } - - return _sessions.Keys.ToList(); - } - } - } - - public IEnumerable InactiveIDs - { - get - { - foreach (var res in broadping(WSFrame.EmptyPingBytes)) - { - if (!res.Value) - { - yield return res.Key; - } - } - } - } - - public bool KeepClean - { - get - { - return _clean; - } - - set - { - if (!canSet(out string message)) - { - Logger.Warning(message); - return; - } - - lock (_locker) - { - if (!canSet(out message)) - { - Logger.Warning(message); - return; - } - - _clean = value; - } - } - } - - public IEnumerable Sessions - { - get - { - if (_state != ServerState.Start) - { - return Enumerable.Empty(); - } - - lock (_locker) - { - if (_state != ServerState.Start) - { - return Enumerable.Empty(); - } - - return _sessions.Values.ToList(); - } - } - } - - internal TimeSpan ResponseWaitingTime - { - get - { - return _responseWaitingTime; - } - - set - { - if (value <= TimeSpan.Zero) - { - throw new ArgumentOutOfRangeException(nameof(value), "Zero or less."); - } - - if (!canSet(out string message)) - { - Logger.Warning(message); - return; - } - - lock (_locker) - { - if (!canSet(out message)) - { - Logger.Warning(message); - return; - } - - _responseWaitingTime = value; - } - } - } - - internal ServerState State => _state; - - public IWSSession this[string id] - { - get - { - if (id == null) - { - throw new ArgumentNullException(nameof(id)); - } - - if (id.Length == 0) - { - throw new ArgumentException("An empty string.", nameof(id)); - } - - tryGetSession(id, out IWSSession session); - - return session; - } - } - - public void Broadcast(byte[] data) - { - if (_state != ServerState.Start) - { - var message = "The current state of the manager is not Start."; - throw new InvalidOperationException(message); - } - - if (data == null) - { - throw new ArgumentNullException(nameof(data)); - } - - if (data.LongLength <= WSClient.FragmentLength) - { - broadcast(OperationCode.Binary, data, null); - } - else - { - broadcast(OperationCode.Binary, new MemoryStream(data), null); - } - } - - public void Broadcast(string data) - { - if (_state != ServerState.Start) - { - var message = "The current state of the manager is not Start."; - throw new InvalidOperationException(message); - } - - if (data == null) - { - throw new ArgumentNullException(nameof(data)); - } - - if (!data.TryGetUTF8EncodedBytes(out byte[] bytes)) - { - var message = "It could not be UTF-8-encoded."; - throw new ArgumentException(message, nameof(data)); - } - - if (bytes.LongLength <= WSClient.FragmentLength) - { - broadcast(OperationCode.Text, bytes, null); - } - else - { - broadcast(OperationCode.Text, new MemoryStream(bytes), null); - } - } - - public void Broadcast(Stream stream, int length) - { - if (_state != ServerState.Start) - { - var message = "The current state of the manager is not Start."; - throw new InvalidOperationException(message); - } - - if (stream == null) - { - throw new ArgumentNullException(nameof(stream)); - } - - if (!stream.CanRead) - { - var message = "It cannot be read."; - throw new ArgumentException(message, nameof(stream)); - } - - if (length < 1) - { - var message = "Less than 1."; - throw new ArgumentException(message, nameof(length)); - } - - var bytes = stream.ReadBytes(length); - - var len = bytes.Length; - if (len == 0) - { - var message = "No data could be read from it."; - throw new ArgumentException(message, nameof(stream)); - } - - if (len < length) - { - Logger.Warning( - string.Format( - "Only {0} byte(s) of data could be read from the stream.", - len - ) - ); - } - - if (len <= WSClient.FragmentLength) - { - broadcast(OperationCode.Binary, bytes, null); - } - else - { - broadcast(OperationCode.Binary, new MemoryStream(bytes), null); - } - } - - public void BroadcastAsync(byte[] data, Action completed) - { - if (_state != ServerState.Start) - { - var message = "The current state of the manager is not Start."; - throw new InvalidOperationException(message); - } - - if (data == null) - { - throw new ArgumentNullException(nameof(data)); - } - - if (data.LongLength <= WSClient.FragmentLength) - { - broadcastAsync(OperationCode.Binary, data, completed); - } - else - { - broadcastAsync(OperationCode.Binary, new MemoryStream(data), completed); - } - } - - public void BroadcastAsync(string data, Action completed) - { - if (_state != ServerState.Start) - { - var message = "The current state of the manager is not Start."; - throw new InvalidOperationException(message); - } - - if (data == null) - { - throw new ArgumentNullException(nameof(data)); - } - - if (!data.TryGetUTF8EncodedBytes(out byte[] bytes)) - { - var message = "It could not be UTF-8-encoded."; - throw new ArgumentException(message, nameof(data)); - } - - if (bytes.LongLength <= WSClient.FragmentLength) - { - broadcastAsync(OperationCode.Text, bytes, completed); - } - else - { - broadcastAsync(OperationCode.Text, new MemoryStream(bytes), completed); - } - } - - public void BroadcastAsync(Stream stream, int length, Action completed) - { - if (_state != ServerState.Start) - { - var message = "The current state of the manager is not Start."; - throw new InvalidOperationException(message); - } - - if (stream == null) - { - throw new ArgumentNullException(nameof(stream)); - } - - if (!stream.CanRead) - { - var message = "It cannot be read."; - throw new ArgumentException(message, nameof(stream)); - } - - if (length < 1) - { - var message = "Less than 1."; - throw new ArgumentException(message, nameof(length)); - } - - var bytes = stream.ReadBytes(length); - - var len = bytes.Length; - if (len == 0) - { - var message = "No data could be read from it."; - throw new ArgumentException(message, nameof(stream)); - } - - if (len < length) - { - Logger.Warning( - string.Format( - "Only {0} byte(s) of data could be read from the stream.", - len - ) - ); - } - - if (len <= WSClient.FragmentLength) - { - broadcastAsync(OperationCode.Binary, bytes, completed); - } - else - { - broadcastAsync(OperationCode.Binary, new MemoryStream(bytes), completed); - } - } - - public void CloseSession(string id) - { - if (!TryGetSession(id, out IWSSession session)) - { - var message = "The session could not be found."; - throw new InvalidOperationException(message); - } - - session.Context.WebSocket.Close(); - } - - public void CloseSession(string id, ushort code, string reason) - { - if (!TryGetSession(id, out IWSSession session)) - { - var message = "The session could not be found."; - throw new InvalidOperationException(message); - } - - session.Context.WebSocket.Close(code, reason); - } - - public void CloseSession(string id, CloseStatusCode code, string reason) - { - if (!TryGetSession(id, out IWSSession session)) - { - var message = "The session could not be found."; - throw new InvalidOperationException(message); - } - - session.Context.WebSocket.Close(code, reason); - } - - public bool PingTo(string id) - { - if (!TryGetSession(id, out IWSSession session)) - { - var message = "The session could not be found."; - throw new InvalidOperationException(message); - } - - return session.Context.WebSocket.Ping(); - } - - public bool PingTo(string message, string id) - { - if (!TryGetSession(id, out IWSSession session)) - { - var pingMessage = "The session could not be found."; - throw new InvalidOperationException(pingMessage); - } - - return session.Context.WebSocket.Ping(message); - } - - public void SendTo(byte[] data, string id) - { - if (!TryGetSession(id, out IWSSession session)) - { - var message = "The session could not be found."; - throw new InvalidOperationException(message); - } - - session.Context.WebSocket.Send(data); - } - - public void SendTo(string data, string id) - { - if (!TryGetSession(id, out IWSSession session)) - { - var message = "The session could not be found."; - throw new InvalidOperationException(message); - } - - session.Context.WebSocket.Send(data); - } - - public void SendTo(Stream stream, int length, string id) - { - if (!TryGetSession(id, out IWSSession session)) - { - var message = "The session could not be found."; - throw new InvalidOperationException(message); - } - - session.Context.WebSocket.Send(stream, length); - } - - public void SendToAsync(byte[] data, string id, Action completed) - { - if (!TryGetSession(id, out IWSSession session)) - { - var message = "The session could not be found."; - throw new InvalidOperationException(message); - } - - session.Context.WebSocket.SendAsync(data, completed); - } - - public void SendToAsync(string data, string id, Action completed) - { - if (!TryGetSession(id, out IWSSession session)) - { - var message = "The session could not be found."; - throw new InvalidOperationException(message); - } - - session.Context.WebSocket.SendAsync(data, completed); - } - - public void SendToAsync( - Stream stream, int length, string id, Action completed - ) - { - if (!TryGetSession(id, out IWSSession session)) - { - var message = "The session could not be found."; - throw new InvalidOperationException(message); - } - - session.Context.WebSocket.SendAsync(stream, length, completed); - } - - public void Sweep() - { - if (_sweeping) - { - Logger.Info("The sweeping is already in progress."); - return; - } - - lock (_cleanLocker) - { - if (_sweeping) - { - Logger.Info("The sweeping is already in progress."); - return; - } - - _sweeping = true; - } - - foreach (var id in InactiveIDs) - { - if (_state != ServerState.Start) - { - break; - } - - lock (_locker) - { - if (_state != ServerState.Start) - { - break; - } - - if (_sessions.TryGetValue(id, out IWSSession session)) - { - var state = session.State; - if (state == WSState.Open) - { - session.Context.WebSocket.Close(CloseStatusCode.Abnormal); - } - else if (state == WSState.Closing) - { - continue; - } - else - { - _sessions.Remove(id); - } - } - } - } - - _sweeping = false; - } - - public bool TryGetSession(string id, out IWSSession session) - { - if (id == null) - { - throw new ArgumentNullException(nameof(id)); - } - - if (id.Length == 0) - { - throw new ArgumentException("An empty string.", nameof(id)); - } - - return tryGetSession(id, out session); - } - - internal string Add(IWSSession session) - { - lock (_locker) - { - if (_state != ServerState.Start) - { - return null; - } - - var id = createID(); - _sessions.Add(id, session); - - return id; - } - } - - internal void Broadcast( - OperationCode opcode, byte[] data, Dictionary cache - ) - { - foreach (var session in Sessions) - { - if (_state != ServerState.Start) - { - Logger.Error("The endpoint is shutting down."); - break; - } - - session.Context.WebSocket.Send(opcode, data, cache); - } - } - - internal void Broadcast( - OperationCode opcode, Stream stream, Dictionary cache - ) - { - foreach (var session in Sessions) - { - if (_state != ServerState.Start) - { - Logger.Error("The endpoint 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) - { - Logger.Error("The endpoint 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 (_locker) - { - return _sessions.Remove(id); - } - } - - internal void Start() - { - lock (_locker) - { - _sweepTimer.Enabled = _clean; - _state = ServerState.Start; - } - } - - internal void Stop(ushort code, string reason) - { - if (code == (ushort)CloseStatusCode.NoStatus) - { // == no status - stop(Payload.Empty, true); - return; - } - - stop(new Payload(code, reason), !code.IsReserved()); - } - - private static string createID() - { - return Guid.NewGuid().ToString("N"); - } - - private void broadcast(OperationCode opcode, byte[] data, Action completed) - { - var cache = new Dictionary(); - - try - { - foreach (var session in Sessions) - { - if (_state != ServerState.Start) - { - Logger.Error("The endpoint is shutting down."); - break; - } - - session.Context.WebSocket.Send(opcode, data, cache); - } - - completed?.Invoke(); - } - catch (Exception ex) - { - Logger.Error(ex.Message); - Logger.Debug(ex.ToString()); - } - finally - { - cache.Clear(); - } - } - - private void broadcast(OperationCode opcode, Stream stream, Action completed) - { - var cache = new Dictionary(); - - try - { - foreach (var session in Sessions) - { - if (_state != ServerState.Start) - { - Logger.Error("The endpoint is shutting down."); - break; - } - - session.Context.WebSocket.Send(opcode, stream, cache); - } - - completed?.Invoke(); - } - catch (Exception ex) - { - Logger.Error(ex.Message); - Logger.Debug(ex.ToString()); - } - finally - { - foreach (var cached in cache.Values) - { - cached.Dispose(); - } - - cache.Clear(); - } - } - - private void broadcastAsync(OperationCode opcode, byte[] data, Action completed) - { - ThreadPool.QueueUserWorkItem( - state => broadcast(opcode, data, completed) - ); - } - - private void broadcastAsync(OperationCode opcode, Stream stream, Action completed) - { - ThreadPool.QueueUserWorkItem( - state => broadcast(opcode, stream, completed) - ); - } - - private Dictionary broadping(byte[] frameAsBytes) - { - var result = new Dictionary(); - - foreach (var session in Sessions) - { - if (_state != ServerState.Start) - { - Logger.Error("The endpoint is shutting down."); - break; - } - - var pingResult = session.Context.WebSocket.Ping(frameAsBytes, _responseWaitingTime); - result.Add(session.ID, pingResult); - } - - return result; - } - - private bool canSet(out string message) - { - message = null; - - if (_state == ServerState.Start) - { - message = "The endpoint has already started."; - return false; - } - - if (_state == ServerState.ShuttingDown) - { - message = "The endpoint is shutting down."; - return false; - } - - return true; - } - - private void setCleanupTimer(double interval) - { - _sweepTimer = new System.Timers.Timer(interval); - _sweepTimer.Elapsed += (sender, e) => Sweep(); - } - - private void stop(Payload payload, bool send) - { - var bytes = send - ? WSFrame.CreateCloseFrame(payload, false).ToArray() - : null; - - lock (_locker) - { - _state = ServerState.ShuttingDown; - - _sweepTimer.Enabled = false; - foreach (var session in _sessions.Values.ToList()) - { - session.Context.WebSocket.Close(payload, bytes); - } - - _state = ServerState.Stop; - } - } - - private bool tryGetSession(string id, out IWSSession session) - { - session = null; - - if (_state != ServerState.Start) - { - return false; - } - - lock (_locker) - { - if (_state != ServerState.Start) - { - return false; - } - - return _sessions.TryGetValue(id, out session); - } - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Server/WebSocketEndpointHost.cs b/EonaCat.Network/System/Sockets/Web/Server/WebSocketEndpointHost.cs deleted file mode 100644 index fe211f8..0000000 --- a/EonaCat.Network/System/Sockets/Web/Server/WebSocketEndpointHost.cs +++ /dev/null @@ -1,55 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; - -namespace EonaCat.Network -{ - internal class WebSocketEndpointHost : WSEndpointHost - where TEndpoint : WSEndpoint - { - private readonly Func _creator; - - internal WebSocketEndpointHost( - string path, Func creator - ) - : this(path, creator, null) - { - } - - internal WebSocketEndpointHost( - string path, - Func creator, - Action initializer - ) - : base(path) - { - _creator = createCreator(creator, initializer); - } - - public override Type EndpointType => typeof(TEndpoint); - - protected override WSEndpoint CreateSession() - { - return _creator(); - } - - private Func createCreator( - Func creator, Action initializer - ) - { - if (initializer == null) - { - return creator; - } - - return () => - { - var ret = creator(); - initializer(ret); - - return ret; - }; - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/WSClient.cs b/EonaCat.Network/System/Sockets/Web/WSClient.cs deleted file mode 100644 index 697feec..0000000 --- a/EonaCat.Network/System/Sockets/Web/WSClient.cs +++ /dev/null @@ -1,2980 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Net.Security; -using System.Net.Sockets; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace EonaCat.Network -{ - public class WSClient : IDisposable - { - internal static readonly byte[] EmptyBytes; - internal static readonly RandomNumberGenerator RandomNumber; - private const string _webSocketVersion = "13"; - private const string _webSocketGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - private const int FRAGMENT_LENGTH = 1016; - private const int MAX_PING_SIZE = 125; - private const string NOT_CONNECTED_MESSAGE = "Connection state is not 'Open', please connect to the server first"; - private const string SERVER_CANNOT_USE_SET_MESSAGE = "The set operation cannot be used by servers."; - private const string CLOSING_MESSAGE = "The connection is closing."; - private static readonly int _maxRetryCountForConnect; - private readonly bool _client; - private readonly Action _message; - private readonly string[] _protocols; - private AuthenticationChallenge _authChallenge; - private string _base64Key; - private Action _closeContext; - private CompressionMethod _compression; - private WSContext _context; - private string _extensions; - private bool _extensionsRequested; - private object _messageEventQueueLocker; - private object _pingLocker; - private object _sendLocker; - private object _stateLocker; - private MemoryStream _fragmentsBuffer; - private bool _fragmentsCompressed; - private OperationCode _fragmentsOpcode; - private bool _inContinuation; - private volatile bool _inMessage; - private bool _isRedirectionEnabled; - private Queue _messageEventQueue; - private uint _nonceCount; - private string _origin; - private ManualResetEvent _pongReceived; - private bool _preAuth; - private string _protocol; - private bool _protocolsRequested; - private NetworkCredential _proxyCredentials; - private Uri _proxyUri; - private volatile WSState _readyState; - private ManualResetEvent _receivingExited; - private int _retryCountForConnect; - private SSLConfigClient _sslConfig; - private Stream _stream; - private TcpClient _tcpClient; - private Uri _uri; - private TimeSpan _responseWaitingTime; - - static WSClient() - { - _maxRetryCountForConnect = 10; - EmptyBytes = new byte[0]; - FragmentLength = FRAGMENT_LENGTH; - RandomNumber = new RNGCryptoServiceProvider(); - } - - public WSClient(string url, X509Certificate clientCertificate = null, params string[] protocols) - { - if (url == null) - { - throw new ArgumentNullException(nameof(url)); - } - - if (url.Length == 0) - { - throw new ArgumentException("Empty string.", nameof(url)); - } - - if (!url.TryCreateWebSocketUri(out _uri, out var message)) - { - throw new ArgumentException(message, nameof(url)); - } - - if (protocols != null && protocols.Length > 0 && !checkProtocols(protocols, out message)) - { - throw new ArgumentException(message, nameof(protocols)); - } - - _protocols = protocols; - _base64Key = CreateBase64Key(); - _client = true; - _message = messagec; - IsSSL = _uri.Scheme == "wss"; - _responseWaitingTime = TimeSpan.FromSeconds(5); - - if (IsSSL && clientCertificate != null) - { - SSL.Certificates.Add(clientCertificate); - } - - Setup(); - } - - internal WSClient(HttpListenerWSContext context, string protocol) - { - _context = context; - _protocol = protocol; - - _closeContext = context.Close; - _message = messages; - IsSSL = context.IsSecureConnection; - _stream = context.Stream; - _responseWaitingTime = TimeSpan.FromSeconds(1); - - Setup(); - } - - internal WSClient(TcpListenerWSContext context, string protocol) - { - _context = context; - _protocol = protocol; - - _closeContext = context.Close; - _message = messages; - IsSSL = context.IsSecureConnection; - _stream = context.Stream; - _responseWaitingTime = TimeSpan.FromSeconds(1); - - Setup(); - } - - public event EventHandler OnConnect; - - public event EventHandler OnDisconnect; - - public event EventHandler OnError; - - public event EventHandler OnMessageReceived; - - /// - /// Fragment length - /// - public static int FragmentLength { get; set; } - - public bool CallMessageOnPing { get; set; } - - public CompressionMethod Compression - { - get - { - return _compression; - } - - set - { - if (!_client) - { - throw new InvalidOperationException(SERVER_CANNOT_USE_SET_MESSAGE); - } - - if (!canSet(out string message)) - { - Logger.Warning(message); - return; - } - - lock (_stateLocker) - { - if (!canSet(out message)) - { - Logger.Warning(message); - return; - } - - _compression = value; - } - } - } - - public IEnumerable Cookies - { - get - { - lock (CookieCollection.SyncRoot) - { - foreach (Cookie cookie in CookieCollection) - { - yield return cookie; - } - } - } - } - - public NetworkCredential Credentials { get; private set; } - - /// - /// Gets or sets the custom headers - /// - public IEnumerable> CustomHeaders { get; set; } - - public string Extensions => _extensions ?? string.Empty; - public bool IsAlive => ping(EmptyBytes); - - public bool IsRedirectionEnabled - { - get - { - return _isRedirectionEnabled; - } - - set - { - - if (!_client) - { - throw new InvalidOperationException(SERVER_CANNOT_USE_SET_MESSAGE); - } - - if (!canSet(out string message)) - { - Logger.Warning(message); - return; - } - - lock (_stateLocker) - { - if (!canSet(out message)) - { - Logger.Warning(message); - return; - } - - _isRedirectionEnabled = value; - } - } - } - - public bool IsSSL { get; private set; } - - public string Origin - { - get - { - return _origin; - } - - set - { - string message = null; - - if (!_client) - { - message = "This instance is not a client."; - throw new InvalidOperationException(message); - } - - if (!value.IsNullOrEmpty()) - { - if (!Uri.TryCreate(value, UriKind.Absolute, out Uri uri)) - { - message = "Not an absolute URI string."; - throw new ArgumentException(message, value); - } - - if (uri.Segments.Length > 1) - { - message = "It includes the path segments."; - throw new ArgumentException(message, value); - } - } - - if (!canSet(out message)) - { - Logger.Warning(message); - return; - } - - lock (_stateLocker) - { - if (!canSet(out message)) - { - Logger.Warning(message); - return; - } - - _origin = !value.IsNullOrEmpty() ? value.TrimEnd('/') : value; - } - } - } - - public string Protocol - { - get - { - return _protocol ?? string.Empty; - } - - internal set - { - _protocol = value; - } - } - - public WSState ReadyState => _readyState; - - public SSLConfigClient SSL - { - get - { - if (!_client) - { - var message = "This instance is not a client."; - throw new InvalidOperationException(message); - } - - if (!IsSSL) - { - var message = "This instance does not use a secure connection."; - throw new InvalidOperationException(message); - } - - _sslConfig ??= new SSLConfigClient(_uri.DnsSafeHost); - - return _sslConfig; - } - } - - /// - /// Gets the TcpClient used for the WebSocket. - /// - public TcpClient TcpClient - { - get - { - return _tcpClient; - } - } - - public Uri Url => _client ? _uri : _context.RequestUri; - - internal TimeSpan ResponseWaitingTime - { - get - { - return _responseWaitingTime; - } - - set - { - if (value <= TimeSpan.Zero) - { - throw new ArgumentOutOfRangeException(nameof(value), "Zero or less."); - } - - if (!canSet(out string message)) - { - Logger.Warning(message); - return; - } - - lock (_stateLocker) - { - if (!canSet(out message)) - { - Logger.Warning(message); - return; - } - - _responseWaitingTime = value; - } - } - } - - internal CookieCollection CookieCollection { get; private set; } - internal Func CustomHandshakeRequestChecker { get; set; } - - internal bool HasMessage - { - get - { - lock (_messageEventQueueLocker) - { - return _messageEventQueue.Count > 0; - } - } - } - - internal bool IgnoreExtensions { get; set; } - - internal bool IsConnected => _readyState == WSState.Open || _readyState == WSState.Closing; - - public void Accept() - { - if (!checkIfAvailable(false, true, true, false, false, false, out string message)) - { - Logger.Error(message); - Error("An error has occurred in accepting.", null); - - return; - } - - if (accept()) - { - open(); - } - } - - public void AcceptAsync() - { - if (!checkIfAvailable(false, true, true, false, false, false, out string message)) - { - Logger.Error(message); - Error("An error has occurred in accepting.", null); - return; - } - - Func acceptor = accept; - Task.Run(() => - { - if (acceptor()) - { - open(); - } - }); - } - - public void Close() - { - close((ushort)CloseStatusCode.NoStatus, string.Empty); - } - - public void Close(ushort code) - { - if (!code.IsCloseStatusCode()) - { - var message = "Less than 1000 or greater than 4999."; - throw new ArgumentOutOfRangeException(nameof(code), message); - } - - if (_client && code == (ushort)CloseStatusCode.ServerError) - { - var message = $"{(ushort)CloseStatusCode.ServerError} cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - if (!_client && code == (ushort)CloseStatusCode.MissingExtension) - { - var message = $"{(ushort)CloseStatusCode.MissingExtension} cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - close(code, string.Empty); - } - - public void Close(CloseStatusCode code) - { - if (_client && code == CloseStatusCode.ServerError) - { - var message = "ServerError cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - if (!_client && code == CloseStatusCode.MissingExtension) - { - var message = "MandatoryExtension cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - close((ushort)code, string.Empty); - } - - public void Close(ushort code, string reason) - { - if (!code.IsCloseStatusCode()) - { - var message = "Less than 1000 or greater than 4999."; - throw new ArgumentOutOfRangeException(nameof(code), message); - } - - if (_client && code == (ushort)CloseStatusCode.ServerError) - { - var message = $"{(ushort)CloseStatusCode.ServerError} cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - if (!_client && code == (ushort)CloseStatusCode.MissingExtension) - { - var message = $"{(ushort)CloseStatusCode.MissingExtension} cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - if (reason.IsNullOrEmpty()) - { - close(code, string.Empty); - return; - } - - if (code == (ushort)CloseStatusCode.NoStatus) - { - var message = $"{(ushort)CloseStatusCode.NoStatus} cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - if (!reason.TryGetUTF8EncodedBytes(out byte[] bytes)) - { - var message = "It could not be UTF-8-encoded."; - throw new ArgumentException(message, nameof(reason)); - } - - if (bytes.Length > 123) - { - var message = "Its size is greater than 123 bytes."; - throw new ArgumentOutOfRangeException(nameof(reason), message); - } - - close(code, reason); - } - - public void Close(CloseStatusCode code, string reason) - { - if (_client && code == CloseStatusCode.ServerError) - { - var message = "ServerError cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - if (!_client && code == CloseStatusCode.MissingExtension) - { - var message = "MandatoryExtension cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - if (reason.IsNullOrEmpty()) - { - close((ushort)code, string.Empty); - return; - } - - if (code == CloseStatusCode.NoStatus) - { - var message = "NoStatus cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - if (!reason.TryGetUTF8EncodedBytes(out byte[] bytes)) - { - var message = "It could not be UTF-8-encoded."; - throw new ArgumentException(message, nameof(reason)); - } - - if (bytes.Length > 123) - { - var message = "Its size is greater than 123 bytes."; - throw new ArgumentOutOfRangeException(nameof(reason), message); - } - - close((ushort)code, reason); - } - - public void CloseAsync() - { - closeAsync((ushort)CloseStatusCode.NoStatus, string.Empty); - } - - public void CloseAsync(ushort code) - { - if (!code.IsCloseStatusCode()) - { - var message = "Less than 1000 or greater than 4999."; - throw new ArgumentOutOfRangeException(nameof(code), message); - } - - if (_client && code == (ushort)CloseStatusCode.ServerError) - { - var message = $"{(ushort)CloseStatusCode.ServerError} cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - if (!_client && code == (ushort)CloseStatusCode.MissingExtension) - { - var message = $"{(ushort)CloseStatusCode.MissingExtension} cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - closeAsync(code, string.Empty); - } - - public void CloseAsync(CloseStatusCode code) - { - if (_client && code == CloseStatusCode.ServerError) - { - var message = "ServerError cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - if (!_client && code == CloseStatusCode.MissingExtension) - { - var message = "MandatoryExtension cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - closeAsync((ushort)code, string.Empty); - } - - public void CloseAsync(ushort code, string reason) - { - if (!code.IsCloseStatusCode()) - { - var message = "Less than 1000 or greater than 4999."; - throw new ArgumentOutOfRangeException(nameof(code), message); - } - - if (_client && code == (ushort)CloseStatusCode.ServerError) - { - var message = $"{(ushort)CloseStatusCode.ServerError} cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - if (!_client && code == (ushort)CloseStatusCode.MissingExtension) - { - var message = $"{(ushort)CloseStatusCode.MissingExtension} cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - if (reason.IsNullOrEmpty()) - { - closeAsync(code, string.Empty); - return; - } - - if (code == (ushort)CloseStatusCode.NoStatus) - { - var message = $"{(ushort)CloseStatusCode.NoStatus} cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - if (!reason.TryGetUTF8EncodedBytes(out byte[] bytes)) - { - var message = "It could not be UTF-8-encoded."; - throw new ArgumentException(message, nameof(reason)); - } - - if (bytes.Length > 123) - { - var message = "Its size is greater than 123 bytes."; - throw new ArgumentOutOfRangeException(nameof(reason), message); - } - - closeAsync(code, reason); - } - - public void CloseAsync(CloseStatusCode code, string reason) - { - if (_client && code == CloseStatusCode.ServerError) - { - var message = "ServerError cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - if (!_client && code == CloseStatusCode.MissingExtension) - { - var message = "MandatoryExtension cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - if (reason.IsNullOrEmpty()) - { - closeAsync((ushort)code, string.Empty); - return; - } - - if (code == CloseStatusCode.NoStatus) - { - var message = "NoStatus cannot be used."; - throw new ArgumentException(message, nameof(code)); - } - - if (!reason.TryGetUTF8EncodedBytes(out byte[] bytes)) - { - var message = "It could not be UTF-8-encoded."; - throw new ArgumentException(message, nameof(reason)); - } - - if (bytes.Length > 123) - { - var message = "Its size is greater than 123 bytes."; - throw new ArgumentOutOfRangeException(nameof(reason), message); - } - - closeAsync((ushort)code, reason); - } - - /// - /// Connect to the server - /// - public void Connect() - { - if (TryConnect(out string errorMessage)) - { - Logger.Error(errorMessage); - Error("An error has occurred in connecting.", null); - return; - } - - open(); - } - - /// - /// COnnect to the server - /// - /// - public async Task ConnectAsync() - { - if (TryConnect(out string errorMessage)) - { - Logger.Error(errorMessage); - Error("An error has occurred in connecting.", null); - return; - } - - await Task.Run(() => open()).ConfigureAwait(false); - } - - private bool TryConnect(out string errorMessage) - { - if (!checkIfAvailable(true, false, true, false, false, true, out errorMessage)) - { - return true; - } - - return !connect(); - } - - void IDisposable.Dispose() - { - close((ushort)CloseStatusCode.Away, string.Empty); - } - - public bool Ping() - { - return ping(EmptyBytes); - } - - public bool Ping(string message) - { - if (message.IsNullOrEmpty()) - { - return ping(EmptyBytes); - } - - if (!message.TryGetUTF8EncodedBytes(out byte[] bytes)) - { - var pingMessage = "It could not be UTF-8-encoded."; - throw new ArgumentException(pingMessage, nameof(message)); - } - - if (bytes.Length > MAX_PING_SIZE) - { - var pingMessage = $"Size is greater than {MAX_PING_SIZE} bytes."; - throw new ArgumentOutOfRangeException(nameof(message), pingMessage); - } - - return ping(bytes); - } - - /// - /// 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 void PingAsync(Action completed) - { - pingAsync(EmptyBytes, completed); - } - - public void Send(byte[] data) - { - if (_readyState != WSState.Open) - { - throw new InvalidOperationException(NOT_CONNECTED_MESSAGE); - } - - if (data == null) - { - throw new ArgumentNullException(nameof(data)); - } - - send(OperationCode.Binary, new MemoryStream(data)); - } - - public void Send(FileInfo fileInfo) - { - if (_readyState != WSState.Open) - { - throw new InvalidOperationException(NOT_CONNECTED_MESSAGE); - } - - if (fileInfo == null) - { - throw new ArgumentNullException(nameof(fileInfo)); - } - - if (!fileInfo.Exists) - { - var message = "The file does not exist."; - throw new ArgumentException(message, nameof(fileInfo)); - } - - if (!fileInfo.TryOpenRead(out FileStream stream)) - { - var message = "The file could not be opened."; - throw new ArgumentException(message, nameof(fileInfo)); - } - - send(OperationCode.Binary, stream); - } - - public void Send(string data) - { - if (_readyState != WSState.Open) - { - throw new InvalidOperationException(NOT_CONNECTED_MESSAGE); - } - - if (data == null) - { - throw new ArgumentNullException(nameof(data)); - } - - if (!data.TryGetUTF8EncodedBytes(out byte[] bytes)) - { - var message = "It could not be UTF-8-encoded."; - throw new ArgumentException(message, nameof(data)); - } - - send(OperationCode.Text, new MemoryStream(bytes)); - } - - public void Send(Stream stream, int length) - { - if (_readyState != WSState.Open) - { - throw new InvalidOperationException(NOT_CONNECTED_MESSAGE); - } - - if (stream == null) - { - throw new ArgumentNullException(nameof(stream)); - } - - if (!stream.CanRead) - { - var message = "It cannot be read."; - throw new ArgumentException(message, nameof(stream)); - } - - if (length < 1) - { - var message = "Less than 1."; - throw new ArgumentException(message, nameof(length)); - } - - var bytes = stream.ReadBytes(length); - - var len = bytes.Length; - if (len == 0) - { - var message = "No data could be read from it."; - throw new ArgumentException(message, nameof(stream)); - } - - if (len < length) - { - Logger.Warning( - string.Format( - "Only {0} byte(s) of data could be read from the stream.", - len - ) - ); - } - - send(OperationCode.Binary, new MemoryStream(bytes)); - } - - public void SendAsync(byte[] data, Action completed = null) - { - if (_readyState != WSState.Open) - { - throw new InvalidOperationException(NOT_CONNECTED_MESSAGE); - } - - if (data == null) - { - throw new ArgumentNullException(nameof(data)); - } - - sendAsync(OperationCode.Binary, new MemoryStream(data), completed); - } - - public void SendAsync(FileInfo fileInfo, Action completed) - { - if (_readyState != WSState.Open) - { - throw new InvalidOperationException(NOT_CONNECTED_MESSAGE); - } - - if (fileInfo == null) - { - throw new ArgumentNullException(nameof(fileInfo)); - } - - if (!fileInfo.Exists) - { - var message = "The file does not exist."; - throw new ArgumentException(message, nameof(fileInfo)); - } - - if (!fileInfo.TryOpenRead(out FileStream stream)) - { - var message = "The file could not be opened."; - throw new ArgumentException(message, nameof(fileInfo)); - } - - sendAsync(OperationCode.Binary, stream, completed); - } - - public void SendAsync(string data, Action completed) - { - if (_readyState != WSState.Open) - { - throw new InvalidOperationException(NOT_CONNECTED_MESSAGE); - } - - if (data == null) - { - throw new ArgumentNullException(nameof(data)); - } - - if (!data.TryGetUTF8EncodedBytes(out byte[] bytes)) - { - var message = "It could not be UTF-8-encoded."; - throw new ArgumentException(message, nameof(data)); - } - - sendAsync(OperationCode.Text, new MemoryStream(bytes), completed); - } - - public void SendAsync(Stream stream, int length, Action completed) - { - if (_readyState != WSState.Open) - { - throw new InvalidOperationException(NOT_CONNECTED_MESSAGE); - } - - if (stream == null) - { - throw new ArgumentNullException(nameof(stream)); - } - - if (!stream.CanRead) - { - var message = "It cannot be read."; - throw new ArgumentException(message, nameof(stream)); - } - - if (length < 1) - { - var message = "Less than 1."; - throw new ArgumentException(message, nameof(length)); - } - - var bytes = stream.ReadBytes(length); - - var len = bytes.Length; - if (len == 0) - { - var message = "No data could be read from it."; - throw new ArgumentException(message, nameof(stream)); - } - - if (len < length) - { - Logger.Warning( - string.Format( - "Only {0} byte(s) of data could be read from the stream.", - len - ) - ); - } - - sendAsync(OperationCode.Binary, new MemoryStream(bytes), completed); - } - - public void SetCookie(Cookie cookie) - { - if (!checkIfAvailable(true, false, true, false, false, true, out string message)) - { - Logger.Error(message); - Error("An error has occurred in setting a cookie.", null); - - return; - } - - if (cookie == null) - { - Logger.Error("'cookie' is null."); - Error("An error has occurred in setting a cookie.", null); - - return; - } - - lock (_stateLocker) - { - if (!checkIfAvailable(true, false, false, true, out message)) - { - Logger.Error(message); - Error("An error has occurred in setting a cookie.", null); - - return; - } - - lock (CookieCollection.SyncRoot) - { - CookieCollection.SetOrRemove(cookie); - } - } - } - - public void SetCredentials(string username, string password, bool preAuth) - { - if (!checkIfAvailable(true, false, true, false, false, true, out string message)) - { - Logger.Error(message); - Error("An error has occurred in setting the credentials.", null); - - return; - } - - if (!checkParametersForSetCredentials(username, password, out message)) - { - Logger.Error(message); - Error("An error has occurred in setting the credentials.", null); - - return; - } - - lock (_stateLocker) - { - if (!checkIfAvailable(true, false, false, true, out message)) - { - Logger.Error(message); - Error("An error has occurred in setting the credentials.", null); - - return; - } - - if (username.IsNullOrEmpty()) - { - Logger.Warning("The credentials are initialized."); - Credentials = null; - _preAuth = false; - - return; - } - - Credentials = new NetworkCredential(username, password, _uri.PathAndQuery); - _preAuth = preAuth; - } - } - - public void SetProxy(string url, string username, string password) - { - if (!checkIfAvailable(true, false, true, false, false, true, out string message)) - { - Logger.Error(message); - Error("An error has occurred in setting the proxy.", null); - - return; - } - - if (!checkParametersForSetProxy(url, username, password, out message)) - { - Logger.Error(message); - Error("An error has occurred in setting the proxy.", null); - - return; - } - - lock (_stateLocker) - { - if (!checkIfAvailable(true, false, false, true, out message)) - { - Logger.Error(message); - Error("An error has occurred in setting the proxy.", null); - - return; - } - - if (url.IsNullOrEmpty()) - { - Logger.Warning("The url and credentials for the proxy are initialized."); - _proxyUri = null; - _proxyCredentials = null; - - return; - } - - _proxyUri = new Uri(url); - - if (username.IsNullOrEmpty()) - { - Logger.Warning("The credentials for the proxy are initialized."); - _proxyCredentials = null; - - return; - } - - _proxyCredentials = - new NetworkCredential( - username, password, string.Format("{0}:{1}", _uri.DnsSafeHost, _uri.Port) - ); - } - } - - 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(_webSocketGuid); - SHA1 sha1 = new SHA1CryptoServiceProvider(); - var src = sha1.ComputeHash(buff.ToString().UTF8Encode()); - - return Convert.ToBase64String(src); - } - - internal void Close(WebResponse response) - { - _readyState = WSState.Closing; - - sendHttpResponse(response); - releaseServerResources(); - - _readyState = WSState.Closed; - } - - internal void Close(HttpStatusCode code) - { - Close(createHandshakeFailureResponse(code)); - } - - internal void Close(Payload payload, byte[] frameAsBytes) - { - lock (_stateLocker) - { - if (_readyState == WSState.Closing) - { - Logger.Info("The closing is already in progress."); - return; - } - - if (_readyState == WSState.Closed) - { - Logger.Info("The connection has already been closed."); - return; - } - - _readyState = WSState.Closing; - } - - Logger.Trace("Begin closing the connection."); - - var sent = frameAsBytes != null && sendBytes(frameAsBytes); - var received = sent && _receivingExited != null && _receivingExited.WaitOne(_responseWaitingTime); - - var result = sent && received; - - Logger.Debug($"Was clean?: {result}\n sent: {sent}\n received: {received}"); - - releaseServerResources(); - releaseCommonResources(); - - Logger.Trace("End closing the connection."); - - _readyState = WSState.Closed; - - var e = new CloseEventArgs(payload); - e.WasClean = result; - - try - { - OnDisconnect.Emit(this, e); - } - catch (Exception ex) - { - Logger.Error(ex.ToString()); - } - } - - internal void InternalAccept() - { - try - { - if (!acceptHandshake()) - { - return; - } - - _readyState = WSState.Open; - } - catch (Exception ex) - { - Logger.Error(ex.ToString()); - fatal("An exception has occurred while accepting.", ex); - - return; - } - - open(); - } - - internal bool Ping(byte[] frameAsBytes, TimeSpan timeout) - { - if (_readyState != WSState.Open) - { - return false; - } - - var pongReceived = _pongReceived; - if (pongReceived == null) - { - return false; - } - - lock (_pingLocker) - { - try - { - pongReceived.Reset(); - - lock (_stateLocker) - { - if (_readyState != WSState.Open) - { - return false; - } - - if (!sendBytes(frameAsBytes)) - { - return false; - } - } - - return pongReceived.WaitOne(timeout); - } - catch (ObjectDisposedException) - { - return false; - } - } - } - - internal void Send( - OperationCode opcode, byte[] data, Dictionary cache - ) - { - lock (_sendLocker) - { - lock (_stateLocker) - { - if (_readyState != WSState.Open) - { - Logger.Error(CLOSING_MESSAGE); - return; - } - - if (!cache.TryGetValue(_compression, out byte[] found)) - { - found = new WSFrame( - FinalFrame.Final, - opcode, - data.Compress(_compression), - _compression != CompressionMethod.None, - false - ) - .ToArray(); - - cache.Add(_compression, found); - } - - sendBytes(found); - } - } - } - - internal void Send( - OperationCode opcode, Stream stream, Dictionary cache - ) - { - lock (_sendLocker) - { - if (!cache.TryGetValue(_compression, out Stream found)) - { - found = stream.Compress(_compression); - cache.Add(_compression, found); - } - else - { - found.Position = 0; - } - - send(opcode, found, _compression != CompressionMethod.None); - } - } - - private static bool checkParametersForSetCredentials( - string username, string password, out string message - ) - { - message = null; - - if (username.IsNullOrEmpty()) - { - return true; - } - - if (username.Contains(':') || !username.IsText()) - { - message = "'username' contains an invalid character."; - return false; - } - - if (password.IsNullOrEmpty()) - { - return true; - } - - if (!password.IsText()) - { - message = "'password' contains an invalid character."; - return false; - } - - return true; - } - - private static bool checkParametersForSetProxy( - string url, string username, string password, out string message - ) - { - message = null; - - if (url.IsNullOrEmpty()) - { - return true; - } - - if (!Uri.TryCreate(url, UriKind.Absolute, out Uri uri) - || uri.Scheme != "http" - || uri.Segments.Length > 1 - ) - { - message = "'url' is an invalid URL."; - return false; - } - - if (username.IsNullOrEmpty()) - { - return true; - } - - if (username.Contains(':') || !username.IsText()) - { - message = "'username' contains an invalid character."; - return false; - } - - if (password.IsNullOrEmpty()) - { - return true; - } - - if (!password.IsText()) - { - message = "'password' contains an invalid character."; - 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 accept() - { - lock (_stateLocker) - { - if (!checkIfAvailable(true, false, false, false, out string message)) - { - Logger.Error(message); - Error("An error has occurred in accepting.", null); - - return false; - } - - try - { - if (!acceptHandshake()) - { - return false; - } - - _readyState = WSState.Open; - } - catch (Exception ex) - { - Logger.Error(ex.ToString()); - fatal("An exception has occurred while accepting.", ex); - - return false; - } - - return true; - } - } - - private bool acceptHandshake() - { - Logger.Debug($"A request from {_context.UserEndPoint}:\n{_context}"); - - if (!checkHandshakeRequest(_context, out string message)) - { - sendHttpResponse(createHandshakeFailureResponse(HttpStatusCode.BadRequest)); - - Logger.Error(message); - fatal("An error has occurred while accepting.", CloseStatusCode.ProtocolError); - - return false; - } - - if (!customCheckHandshakeRequest(_context, out message)) - { - sendHttpResponse(createHandshakeFailureResponse(HttpStatusCode.BadRequest)); - - Logger.Error(message); - fatal("An error has occurred while accepting.", CloseStatusCode.PolicyViolation); - - return false; - } - - _base64Key = _context.Headers["Sec-WebSocket-Key"]; - - if (_protocol != null) - { - processSecWebSocketProtocolHeader(_context.SecWebSocketProtocols); - } - - if (!IgnoreExtensions) - { - processSecWebSocketExtensionsClientHeader(_context.Headers["Sec-WebSocket-Extensions"]); - } - - return sendHttpResponse(createHandshakeResponse()); - } - - private bool canSet(out string message) - { - message = null; - - if (_readyState == WSState.Open) - { - message = "The connection has already been established."; - return false; - } - - if (_readyState == WSState.Closing) - { - message = CLOSING_MESSAGE; - return false; - } - - return true; - } - - private bool checkHandshakeRequest(WSContext context, out string message) - { - message = null; - - if (context.RequestUri == null) - { - message = "Specifies an invalid Request-URI."; - return false; - } - - if (!context.IsWebSocketRequest) - { - message = "Not a WebSocket handshake request."; - return false; - } - - var headers = context.Headers; - if (!validateSecWebSocketKeyHeader(headers["Sec-WebSocket-Key"])) - { - message = "Includes no Sec-WebSocket-Key header, or it has an invalid value."; - return false; - } - - if (!validateSecWebSocketVersionClientHeader(headers["Sec-WebSocket-Version"])) - { - message = "Includes no Sec-WebSocket-Version header, or it has an invalid value."; - return false; - } - - if (!validateSecWebSocketProtocolClientHeader(headers["Sec-WebSocket-Protocol"])) - { - message = "Includes an invalid Sec-WebSocket-Protocol header."; - return false; - } - - if (!IgnoreExtensions - && !validateSecWebSocketExtensionsClientHeader(headers["Sec-WebSocket-Extensions"]) - ) - { - message = "Includes an invalid Sec-WebSocket-Extensions header."; - return false; - } - - return true; - } - - private bool checkHandshakeResponse(WebResponse 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 bool checkIfAvailable( - bool connecting, bool open, bool closing, bool closed, out string message - ) - { - message = null; - - if (!connecting && _readyState == WSState.Connecting) - { - message = "This operation is not available in: connecting"; - return false; - } - - if (!open && _readyState == WSState.Open) - { - message = "This operation is not available in: open"; - return false; - } - - if (!closing && _readyState == WSState.Closing) - { - message = "This operation is not available in: closing"; - return false; - } - - if (!closed && _readyState == WSState.Closed) - { - message = "This operation is not available in: closed"; - return false; - } - - return true; - } - - private bool checkIfAvailable( - bool client, - bool server, - bool connecting, - bool open, - bool closing, - bool closed, - out string message - ) - { - message = null; - - if (!client && _client) - { - message = "This operation is not available in: client"; - return false; - } - - if (!server && !_client) - { - message = "This operation is not available in: server"; - return false; - } - - return checkIfAvailable(connecting, open, closing, closed, out message); - } - - private bool checkReceivedFrame(WSFrame 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.Opcode == OperationCode.Continue && !_inContinuation) - { - message = "A continuation frame has been received but there is nothing to continue."; - return false; - } - - if (frame.Rsv2 == ReservedBits.On) - { - message = "The RSV2 of a frame is non-zero without any negotiation for it."; - return false; - } - - if (frame.Rsv3 == ReservedBits.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 == WSState.Closing) - { - Logger.Info("The closing is already in progress."); - return; - } - - if (_readyState == WSState.Closed) - { - Logger.Info("The connection has already been closed."); - return; - } - - if (code == (ushort)CloseStatusCode.NoStatus) - { // == no status - close(Payload.Empty, true, true, false); - return; - } - - var send = !code.IsReserved(); - close(new Payload(code, reason), send, send, false); - } - - private void close(Payload payload, bool send, bool receive, bool received) - { - lock (_stateLocker) - { - if (_readyState == WSState.Closing) - { - Logger.Info("The closing is already in progress."); - return; - } - - if (_readyState == WSState.Closed) - { - Logger.Info("The connection has already been closed."); - return; - } - - send = send && _readyState == WSState.Open; - receive = send && receive; - - _readyState = WSState.Closing; - } - - Logger.Trace("Begin closing the connection."); - - var res = closeHandshake(payload, send, receive, received); - releaseResources(); - - Logger.Trace("End closing the connection."); - - _readyState = WSState.Closed; - - var e = new CloseEventArgs(payload); - e.WasClean = res; - - try - { - OnDisconnect.Emit(this, e); - } - catch (Exception ex) - { - Logger.Error(ex.ToString()); - Error("An error has occurred during the OnClose event.", ex); - } - } - - private void closeAsync(ushort code, string reason) - { - if (_readyState == WSState.Closing) - { - Logger.Info("The closing is already in progress."); - return; - } - - if (_readyState == WSState.Closed) - { - Logger.Info("The connection has already been closed."); - return; - } - - if (code == (ushort)CloseStatusCode.NoStatus) - { // == no status - closeAsync(Payload.Empty, true, true, false); - return; - } - - var send = !code.IsReserved(); - closeAsync(new Payload(code, reason), send, send, false); - } - - private async Task closeAsync(Payload payload, bool send, bool receive, bool received) - { - await Task.Run(() => close(payload, send, receive, received)); - } - - private bool closeHandshake( - Payload payload, bool send, bool receive, bool received - ) - { - var sent = false; - if (send) - { - var frame = WSFrame.CreateCloseFrame(payload, _client); - sent = sendBytes(frame.ToArray()); - - if (_client) - { - frame.Unmask(); - } - } - - var wait = !received && sent && receive && _receivingExited != null; - if (wait) - { - received = _receivingExited.WaitOne(_responseWaitingTime); - } - - var ret = sent && received; - - Logger.Debug( - string.Format( - "Was clean?: {0}\n sent: {1}\n received: {2}", ret, sent, received - ) - ); - - return ret; - } - - private bool connect() - { - lock (_stateLocker) - { - if (!checkIfAvailable(true, false, false, true, out string message)) - { - Logger.Error(message); - Error("An error has occurred in connecting.", null); - - return false; - } - - if (_retryCountForConnect > _maxRetryCountForConnect) - { - _retryCountForConnect = 0; - Logger.Error("A series of reconnecting has failed."); - - return false; - } - - _readyState = WSState.Connecting; - - try - { - Handshake(); - } - catch (Exception ex) - { - _retryCountForConnect++; - Logger.Error(ex.ToString()); - fatal("An exception has occurred while connecting.", ex); - - return false; - } - - _retryCountForConnect = 1; - _readyState = WSState.Open; - - return true; - } - } - - 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; - } - - private WebResponse createHandshakeFailureResponse(HttpStatusCode code) - { - var result = WebResponse.CreateCloseResponse(code); - result.Headers["Sec-WebSocket-Version"] = _webSocketVersion; - return result; - } - - private WebRequest createHandshakeRequest() - { - var ret = WebRequest.CreateWebSocketRequest(_uri); - - var headers = ret.Headers; - if (!_origin.IsNullOrEmpty()) - { - headers["Origin"] = _origin; - } - - headers["Sec-WebSocket-Key"] = _base64Key; - - _protocolsRequested = _protocols != null && _protocols.Length > 0; - if (_protocolsRequested) - { - headers["Sec-WebSocket-Protocol"] = _protocols.ToString(", "); - } - - _extensionsRequested = _compression != CompressionMethod.None; - if (_extensionsRequested) - { - headers["Sec-WebSocket-Extensions"] = createExtensions(); - } - - headers["Sec-WebSocket-Version"] = _webSocketVersion; - - 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 (CookieCollection.Count > 0) - { - ret.SetCookies(CookieCollection); - } - - if (CustomHeaders != null) - foreach (var header in CustomHeaders) - if (!headers.Contains(header.Key)) - ret.Headers[header.Key] = header.Value; - - return ret; - } - - private WebResponse createHandshakeResponse() - { - var ret = WebResponse.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 (CookieCollection.Count > 0) - { - ret.SetCookies(CookieCollection); - } - - return ret; - } - - private bool customCheckHandshakeRequest(WSContext context, out string message) - { - message = null; - return CustomHandshakeRequestChecker == null - || (message = CustomHandshakeRequestChecker(context)) == null; - } - - private void EnqueueToMessageEventQueue(MessageEventArgs e) - { - lock (_messageEventQueueLocker) - { - _messageEventQueue.Enqueue(e); - } - } - - private void Error(string message, Exception exception) - { - try - { - OnError.Emit(this, new ErrorEventArgs(message, exception)); - } - catch (Exception ex) - { - Logger.Error(ex.ToString()); - } - } - - private void fatal(string message, Exception exception) - { - var code = exception is WSException - ? ((WSException)exception).Code - : CloseStatusCode.Abnormal; - - fatal(message, (ushort)code); - } - - private void fatal(string message, ushort code) - { - var payload = new Payload(code, message); - close(payload, !code.IsReserved(), false, false); - } - - private void fatal(string message, CloseStatusCode code) - { - fatal(message, (ushort)code); - } - - private void Handshake() - { - SetClientStream(); - - var response = sendHandshakeRequest(); - if (!checkHandshakeResponse(response, out string message)) - { - throw new WSException(CloseStatusCode.ProtocolError, message); - } - - if (_protocolsRequested) - { - _protocol = response.Headers["Sec-WebSocket-Protocol"]; - } - - if (_extensionsRequested) - { - processSecWebSocketExtensionsServerHeader(response.Headers["Sec-WebSocket-Extensions"]); - } - - processCookies(response.Cookies); - } - - private void message() - { - MessageEventArgs e = null; - lock (_messageEventQueueLocker) - { - if (_inMessage || _messageEventQueue.Count == 0 || _readyState != WSState.Open) - { - return; - } - - _inMessage = true; - e = _messageEventQueue.Dequeue(); - } - - _message(e); - } - - private void messagec(MessageEventArgs e) - { - do - { - try - { - OnMessageReceived.Emit(this, e); - } - catch (Exception ex) - { - Logger.Error(ex.ToString()); - Error("An error has occurred during an OnMessage event.", ex); - } - - lock (_messageEventQueueLocker) - { - if (_messageEventQueue.Count == 0 || _readyState != WSState.Open) - { - _inMessage = false; - break; - } - - e = _messageEventQueue.Dequeue(); - } - } - while (true); - } - - private void messages(MessageEventArgs e) - { - try - { - OnMessageReceived.Emit(this, e); - } - catch (Exception ex) - { - Logger.Error(ex.ToString()); - Error("An error has occurred during an OnMessage event.", ex); - } - - lock (_messageEventQueueLocker) - { - if (_messageEventQueue.Count == 0 || _readyState != WSState.Open) - { - _inMessage = false; - return; - } - - e = _messageEventQueue.Dequeue(); - } - - ThreadPool.QueueUserWorkItem(state => messages(e)); - } - - private void open() - { - _inMessage = true; - startReceiving(); - try - { - OnConnect.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 (_messageEventQueueLocker) - { - if (_messageEventQueue.Count == 0 || _readyState != WSState.Open) - { - _inMessage = false; - return; - } - - e = _messageEventQueue.Dequeue(); - } - - Task.Run(() => _message(e)); - } - - private bool ping(byte[] data) - { - if (_readyState != WSState.Open) - { - return false; - } - - var pongReceived = _pongReceived; - if (pongReceived == null) - { - return false; - } - - lock (_pingLocker) - { - try - { - pongReceived.Reset(); - if (!send(FinalFrame.Final, OperationCode.Ping, data, false)) - { - return false; - } - - return pongReceived.WaitOne(_responseWaitingTime); - } - catch (ObjectDisposedException) - { - return false; - } - } - } - - private void pingAsync(byte[] data, Action completed) - { - if (_readyState != WSState.Open) - { - throw new InvalidOperationException(NOT_CONNECTED_MESSAGE); - } - - sendAsync(OperationCode.Ping, new MemoryStream(data), completed); - } - - private bool processCloseFrame(WSFrame frame) - { - var payload = frame.Payload; - close(payload, !payload.HasReservedCode, false, true); - - return ping(EmptyBytes); - } - - private void processCookies(CookieCollection cookies) - { - if (cookies.Count == 0) - { - return; - } - - CookieCollection.SetOrRemove(cookies); - } - - private bool processDataFrame(WSFrame frame) - { - EnqueueToMessageEventQueue( - frame.IsCompressed - ? new MessageEventArgs( - frame.Opcode, frame.Payload.ApplicationData.Decompress(_compression)) - : new MessageEventArgs(frame)); - - return true; - } - - private bool processFragmentFrame(WSFrame 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.Payload.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(WSFrame frame) - { - Logger.Trace("A ping was received."); - - var pong = WSFrame.CreatePongFrame(frame.Payload, _client); - - lock (_stateLocker) - { - if (_readyState != WSState.Open) - { - Logger.Error(CLOSING_MESSAGE); - return true; - } - - if (!sendBytes(pong.ToArray())) - { - return false; - } - } - - Logger.Trace("A pong to this ping has been sent."); - - if (CallMessageOnPing) - { - if (_client) - { - pong.Unmask(); - } - - EnqueueToMessageEventQueue(new MessageEventArgs(frame)); - } - - return true; - } - - private bool processPongFrame(WSFrame 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(WSFrame frame) - { - if (!checkReceivedFrame(frame, out string message)) - { - throw new WSException(CloseStatusCode.ProtocolError, message); - } - - 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); - } - - private void processSecWebSocketExtensionsClientHeader(string value) - { - if (value == null) - { - return; - } - - var buff = new StringBuilder(80); - - var comp = false; - foreach (var extensions in value.SplitHeaderValue(',')) - { - var extension = extensions.Trim(); - if (!comp && extension.IsCompressionExtension(CompressionMethod.Deflate)) - { - _compression = CompressionMethod.Deflate; - buff.AppendFormat($"{_compression.ToExtensionString("client_no_context_takeover", "server_no_context_takeover")}, "); - comp = true; - } - } - - var len = buff.Length; - if (len > 2) - { - buff.Length = len - 2; - _extensions = buff.ToString(); - } - } - - private void processSecWebSocketExtensionsServerHeader(string value) - { - if (value == null) - { - _compression = CompressionMethod.None; - return; - } - - _extensions = value; - } - - private void processSecWebSocketProtocolHeader(IEnumerable values) - { - if (values.Contains(p => p == _protocol)) - { - return; - } - - _protocol = null; - } - - private bool processUnsupportedFrame(WSFrame frame) - { - Logger.Error("An unsupported frame:" + frame.PrintToString(false)); - fatal("There is no way to handle it.", CloseStatusCode.PolicyViolation); - return false; - } - - 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(); - } - - private void releaseServerResources() - { - if (_closeContext == null) - { - return; - } - - _closeContext(); - _closeContext = null; - _stream = null; - _context = null; - } - - private bool send(OperationCode opcode, Stream stream) - { - lock (_sendLocker) - { - 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(OperationCode opcode, Stream stream, bool compressed) - { - var len = stream.Length; - if (len == 0) - { - return send(FinalFrame.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(FinalFrame.Final, opcode, buff, compressed); - } - - if (quo == 1 && rem == 0) - { - buff = new byte[FragmentLength]; - return stream.Read(buff, 0, FragmentLength) == FragmentLength - && send(FinalFrame.Final, opcode, buff, compressed); - } - - /* Send fragments */ - - // Begin - buff = new byte[FragmentLength]; - var sent = stream.Read(buff, 0, FragmentLength) == FragmentLength - && send(FinalFrame.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(FinalFrame.More, OperationCode.Continue, 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(FinalFrame.Final, OperationCode.Continue, buff, false); - } - - private bool send(FinalFrame fin, OperationCode opcode, byte[] data, bool compressed) - { - lock (_stateLocker) - { - if (_readyState != WSState.Open) - { - Logger.Error(CLOSING_MESSAGE); - return false; - } - - var frame = new WSFrame(fin, opcode, data, compressed, _client); - return sendBytes(frame.ToArray()); - } - } - - private void sendAsync(OperationCode opcode, Stream stream, Action completed) - { - Func sender = send; - Task.Run(() => - { - try - { - var sent = sender(opcode, stream); - completed?.Invoke(sent); - } - catch (Exception ex) - { - Logger.Error(ex.ToString()); - Error("An error has occurred during the callback for an async send.", ex); - } - }); - } - - 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; - } - - private WebResponse sendHandshakeRequest() - { - var req = createHandshakeRequest(); - var result = sendHttpRequest(req, 90000); - if (result.IsUnauthorized) - { - var challenge = result.Headers["WWW-Authenticate"]; - Logger.Warning($"Received an authentication requirement for '{challenge}'."); - if (challenge.IsNullOrEmpty()) - { - Logger.Error("No authentication challenge is specified."); - return result; - } - - _authChallenge = AuthenticationChallenge.Parse(challenge); - if (_authChallenge == null) - { - Logger.Error("An invalid authentication challenge is specified."); - return result; - } - - if (Credentials != null && - (!_preAuth || _authChallenge.Scheme == AuthenticationSchemes.Digest)) - { - if (result.HasConnectionClose) - { - releaseClientResources(); - SetClientStream(); - } - - var authRes = new AuthenticationResponse(_authChallenge, Credentials, _nonceCount); - _nonceCount = authRes.NonceCount; - req.Headers["Authorization"] = authRes.ToString(); - result = sendHttpRequest(req, 15000); - } - } - - if (result.IsRedirect) - { - var url = result.Headers["Location"]; - Logger.Warning($"Received a redirection to '{url}'."); - if (_isRedirectionEnabled) - { - if (url.IsNullOrEmpty()) - { - Logger.Error("No url to redirect is located."); - return result; - } - - if (!url.TryCreateWebSocketUri(out Uri uri, out string message)) - { - Logger.Error("An invalid url to redirect is located: " + message); - return result; - } - - releaseClientResources(); - - _uri = uri; - IsSSL = uri.Scheme == "wss"; - - SetClientStream(); - return sendHandshakeRequest(); - } - } - - return result; - } - - private WebResponse sendHttpRequest(WebRequest request, int millisecondsTimeout) - { - Logger.Debug("Request to server:\n" + request.ToString()); - var res = request.GetResponse(_stream, millisecondsTimeout); - Logger.Debug("Response to request:\n" + res.ToString()); - - return res; - } - - private bool sendHttpResponse(WebResponse response) - { - Logger.Debug("Response to request:\n" + response.ToString()); - return sendBytes(response.ToByteArray()); - } - - private void sendProxyConnectRequest() - { - var req = WebRequest.CreateConnectRequest(_uri); - var result = sendHttpRequest(req, 90000); - if (result.IsProxyAuthenticationRequired) - { - var challenge = result.Headers["Proxy-Authenticate"]; - Logger.Warning($"Received a proxy authentication requirement for '{challenge}'."); - - if (challenge.IsNullOrEmpty()) - { - throw new WSException("No proxy authentication challenge is specified."); - } - - var authChallenge = AuthenticationChallenge.Parse(challenge) ?? throw new WSException("An invalid proxy authentication challenge is specified."); - if (_proxyCredentials != null) - { - if (result.HasConnectionClose) - { - releaseClientResources(); - _tcpClient = new TcpClient(_proxyUri.DnsSafeHost, _proxyUri.Port); - _stream = _tcpClient.GetStream(); - } - - var authRes = new AuthenticationResponse(authChallenge, _proxyCredentials, 0); - req.Headers["Proxy-Authorization"] = authRes.ToString(); - result = sendHttpRequest(req, 15000); - } - - if (result.IsProxyAuthenticationRequired) - { - throw new WSException("A proxy authentication is required."); - } - } - - if (result.StatusCode[0] != '2') - { - throw new WSException("The proxy has failed a connection to the requested host and port."); - } - } - - 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 (IsSSL) - { - var conf = SSL; - var host = conf.TargetHost; - if (host != _uri.DnsSafeHost) - { - throw new WSException( - CloseStatusCode.TlsHandshakeFailure, "An invalid host name is specified."); - } - - try - { - var sslStream = new SslStream( - _stream, - false, - conf.ServerCertificateValidationCallback, - conf.ClientCertificateSelectionCallback); - - sslStream.ReadTimeout = (int)ResponseWaitingTime.TotalMilliseconds; - sslStream.WriteTimeout = (int)ResponseWaitingTime.TotalMilliseconds; - - sslStream.AuthenticateAsClient( - host, - conf.Certificates, - conf.SslProtocols, - conf.CheckForCertificateRevocation); - - _stream = sslStream; - } - catch (Exception ex) - { - throw new WSException(CloseStatusCode.TlsHandshakeFailure, ex); - } - } - } - - private void Setup() - { - _compression = CompressionMethod.None; - CookieCollection = new CookieCollection(); - _pingLocker = new object(); - _sendLocker = new object(); - _stateLocker = new object(); - _messageEventQueue = new Queue(); - _messageEventQueueLocker = ((ICollection)_messageEventQueue).SyncRoot; - _readyState = WSState.Connecting; - } - - private void startReceiving() - { - if (_messageEventQueue.Count > 0) - { - _messageEventQueue.Clear(); - } - - _pongReceived = new ManualResetEvent(false); - _receivingExited = new ManualResetEvent(false); - - Action receive = null; - receive = - () => - WSFrame.ReadFrameAsync( - _stream, - false, - frame => - { - if (!processReceivedFrame(frame) || _readyState == WSState.Closed) - { - var exited = _receivingExited; - exited?.Set(); - - return; - } - - // Receive next because the Ping or Close needs a response to it. - receive(); - - if (_inMessage || !HasMessage || _readyState != WSState.Open) - { - return; - } - - message(); - }, - ex => - { - Logger.Error(ex.ToString()); - fatal("An exception has occurred while receiving.", ex); - } - ); - - receive(); - } - - private bool validateSecWebSocketAcceptHeader(string value) - { - return value != null && value == CreateResponseKey(_base64Key); - } - - private bool validateSecWebSocketExtensionsClientHeader(string value) - { - return value == null || value.Length > 0; - } - - 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 currentExtension in value.SplitHeaderValue(',')) - { - var extension = currentExtension.Trim(); - if (comp && extension.IsCompressionExtension(_compression)) - { - if (!extension.Contains("server_no_context_takeover")) - { - Logger.Error("The server hasn't sent back 'server_no_context_takeover'."); - return false; - } - - if (!extension.Contains("client_no_context_takeover")) - { - Logger.Warning("The server hasn't sent back 'client_no_context_takeover'."); - } - - var method = _compression.ToExtensionString(); - var invalid = - extension.SplitHeaderValue(';').Contains( - x => - { - x = x.Trim(); - return x != method - && x != "server_no_context_takeover" - && x != "client_no_context_takeover"; - } - ); - - if (invalid) - { - return false; - } - } - else - { - return false; - } - } - - return true; - } - - private bool validateSecWebSocketKeyHeader(string value) - { - return value != null && value.Length > 0; - } - - private bool validateSecWebSocketProtocolClientHeader(string value) - { - return value == null || value.Length > 0; - } - - private bool validateSecWebSocketProtocolServerHeader(string value) - { - if (value == null) - { - return !_protocolsRequested; - } - - if (value.Length == 0) - { - return false; - } - - return _protocolsRequested && _protocols.Contains(p => p == value); - } - - private bool validateSecWebSocketVersionClientHeader(string value) - { - return value != null && value == _webSocketVersion; - } - - private bool validateSecWebSocketVersionServerHeader(string value) - { - return value == null || value == _webSocketVersion; - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/WSException.cs b/EonaCat.Network/System/Sockets/Web/WSException.cs deleted file mode 100644 index 487f7a1..0000000 --- a/EonaCat.Network/System/Sockets/Web/WSException.cs +++ /dev/null @@ -1,55 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; - -namespace EonaCat.Network -{ - public class WSException : Exception - { - internal WSException() - : this(CloseStatusCode.Abnormal, null, null) - { - } - - internal WSException(Exception innerException) - : this(CloseStatusCode.Abnormal, null, innerException) - { - } - - internal WSException(string message) - : this(CloseStatusCode.Abnormal, message, null) - { - } - - internal WSException(CloseStatusCode code) - : this(code, null, null) - { - } - - internal WSException(string message, Exception innerException) - : this(CloseStatusCode.Abnormal, message, innerException) - { - } - - internal WSException(CloseStatusCode code, Exception innerException) - : this(code, null, innerException) - { - } - - internal WSException(CloseStatusCode code, string message) - : this(code, message, null) - { - } - - internal WSException( - CloseStatusCode code, string message, Exception innerException - ) - : base("EonaCat Network: " + (message ?? code.GetMessage()), innerException) - { - Code = code; - } - - public CloseStatusCode Code { get; } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/WSFrame.cs b/EonaCat.Network/System/Sockets/Web/WSFrame.cs deleted file mode 100644 index 3b573b5..0000000 --- a/EonaCat.Network/System/Sockets/Web/WSFrame.cs +++ /dev/null @@ -1,641 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace EonaCat.Network -{ - internal class WSFrame : IEnumerable - { - internal static readonly byte[] EmptyPingBytes; - private const int BUFFER_SIZE = 1024; - - static WSFrame() - { - EmptyPingBytes = CreatePingFrame(false).ToArray(); - } - - internal WSFrame(OperationCode opcode, Payload payload, bool mask) - : this(FinalFrame.Final, opcode, payload, false, mask) - { - } - - internal WSFrame(FinalFrame finalFrame, OperationCode opcode, byte[] data, bool compressed, bool mask) - : this(finalFrame, opcode, new Payload(data), compressed, mask) - { - } - - internal WSFrame( - FinalFrame fin, OperationCode opcode, Payload payload, bool compressed, bool mask) - { - Fin = fin; - Rsv1 = opcode.IsData() && compressed ? ReservedBits.On : ReservedBits.Off; - Rsv2 = ReservedBits.Off; - Rsv3 = ReservedBits.Off; - Opcode = opcode; - Payload = new Payload(payload); - - var len = Payload.Length; - if (len < 126) - { - PayloadLength = (byte)len; - ExtendedPayloadLength = WSClient.EmptyBytes; - } - else if (len < 0x010000) - { - PayloadLength = 126; - ExtendedPayloadLength = ((ushort)len).InternalToByteArray(ByteOrder.Big); - } - else - { - PayloadLength = 127; - ExtendedPayloadLength = len.InternalToByteArray(ByteOrder.Big); - } - - if (mask) - { - Mask = Mask.On; - MaskingKey = CreateMaskingKey(); - Payload.Mask(MaskingKey); - } - else - { - Mask = Mask.Off; - MaskingKey = WSClient.EmptyBytes; - } - } - - private WSFrame() - { - } - - public byte[] ExtendedPayloadLength { get; private set; } - public FinalFrame Fin { get; private set; } - public bool IsBinary => Opcode == OperationCode.Binary; - public bool IsClose => Opcode == OperationCode.Close; - public bool IsCompressed => Rsv1 == ReservedBits.On; - public bool IsContinuation => Opcode == OperationCode.Continue; - public bool IsControl => Opcode >= OperationCode.Close; - public bool IsData => Opcode == OperationCode.Text || Opcode == OperationCode.Binary; - public bool IsFinal => Fin == FinalFrame.Final; - public bool IsFragment => Fin == FinalFrame.More || Opcode == OperationCode.Continue; - public bool IsMasked => Mask == Mask.On; - public bool IsPing => Opcode == OperationCode.Ping; - public bool IsPong => Opcode == OperationCode.Pong; - public bool IsText => Opcode == OperationCode.Text; - public ulong Length => 2 + (ulong)(ExtendedPayloadLength.Length + MaskingKey.Length) + Payload.Length; - public Mask Mask { get; private set; } - public byte[] MaskingKey { get; private set; } - public OperationCode Opcode { get; private set; } - public Payload Payload { get; private set; } - public byte PayloadLength { get; private set; } - public ReservedBits Rsv1 { get; private set; } - public ReservedBits Rsv2 { get; private set; } - public ReservedBits Rsv3 { get; private set; } - internal int ExtendedPayloadLengthCount => PayloadLength < 126 ? 0 : (PayloadLength == 126 ? 2 : 8); - - internal ulong FullPayloadLength => PayloadLength < 126 - ? PayloadLength - : PayloadLength == 126 - ? ExtendedPayloadLength.ToUInt16(ByteOrder.Big) - : ExtendedPayloadLength.ToUInt64(ByteOrder.Big); - - public IEnumerator GetEnumerator() - { - foreach (var b in ToArray()) - { - yield return b; - } - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - 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) + PayloadLength; - buff.Write(((ushort)header).InternalToByteArray(ByteOrder.Big), 0, 2); - - if (PayloadLength > 125) - { - buff.Write(ExtendedPayloadLength, 0, PayloadLength == 126 ? 2 : 8); - } - - if (Mask == Mask.On) - { - buff.Write(MaskingKey, 0, 4); - } - - if (PayloadLength > 0) - { - var bytes = Payload.ToArray(); - if (PayloadLength < 127) - { - buff.Write(bytes, 0, bytes.Length); - } - else - { - buff.WriteBytes(bytes, BUFFER_SIZE); - } - } - - buff.Close(); - return buff.ToArray(); - } - } - - public override string ToString() - { - return BitConverter.ToString(ToArray()); - } - - internal static WSFrame CreateCloseFrame( - Payload payload, bool mask - ) - { - return new WSFrame( - FinalFrame.Final, OperationCode.Close, payload, false, mask - ); - } - - internal static WSFrame CreatePingFrame(bool mask) - { - return new WSFrame( - FinalFrame.Final, OperationCode.Ping, Payload.Empty, false, mask - ); - } - - internal static WSFrame CreatePingFrame(byte[] data, bool mask) - { - return new WSFrame( - FinalFrame.Final, OperationCode.Ping, new Payload(data), false, mask - ); - } - - internal static WSFrame CreatePongFrame( - Payload payload, bool mask - ) - { - return new WSFrame( - FinalFrame.Final, OperationCode.Pong, payload, false, mask - ); - } - - internal static WSFrame ReadFrame(Stream stream, bool unmask) - { - var frame = readHeader(stream); - readExtendedPayloadLength(stream, frame); - readMaskingKey(stream, frame); - readPayload(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 => - readPayloadAsync( - stream, - frame2, - frame3 => - { - if (unmask) - { - frame3.Unmask(); - } - - completed(frame3); - }, - error - ), - error - ), - error - ), - error - ); - } - - internal void Unmask() - { - if (Mask == Mask.Off) - { - return; - } - - Mask = Mask.Off; - Payload.Mask(MaskingKey); - MaskingKey = WSClient.EmptyBytes; - } - - private static byte[] CreateMaskingKey() - { - var key = new byte[4]; - WSClient.RandomNumber.GetBytes(key); - - return key; - } - - private static string Dump(WSFrame frame) - { - var len = frame.Length; - var amount = (long)(len / 4); - var remainder = (int)(len % 4); - - int digitAmount; - string frameCount; - if (amount < 10000) - { - digitAmount = 4; - frameCount = "{0,4}"; - } - else if (amount < 0x010000) - { - digitAmount = 4; - frameCount = "{0,4:X}"; - } - else if (amount < 0x0100000000) - { - digitAmount = 8; - frameCount = "{0,8:X}"; - } - else - { - digitAmount = 16; - frameCount = "{0,16:X}"; - } - - var spFmt = string.Format("{{0,{0}}}", digitAmount); - var headerFormat = string.Format(@"{0} 01234567 89ABCDEF 01234567 89ABCDEF{0}+--------+--------+--------+--------+\n", spFmt); - var lineFmt = string.Format("{0}|{{1,8}} {{2,8}} {{3,8}} {{4,8}}|\n", frameCount); - var footerFmt = string.Format("{0}+--------+--------+--------+--------+", spFmt); - - var output = new StringBuilder(64); - Func> linePrinter = () => - { - long lineCnt = 0; - return (arg1, arg2, arg3, arg4) => - output.AppendFormat(lineFmt, ++lineCnt, arg1, arg2, arg3, arg4); - }; - var printLine = linePrinter(); - - output.AppendFormat(headerFormat, string.Empty); - - var bytes = frame.ToArray(); - for (long i = 0; i <= amount; i++) - { - var j = i * 4; - if (i < amount) - { - 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 (remainder > 0) - { - printLine( - Convert.ToString(bytes[j], 2).PadLeft(8, '0'), - remainder >= 2 ? Convert.ToString(bytes[j + 1], 2).PadLeft(8, '0') : string.Empty, - remainder == 3 ? Convert.ToString(bytes[j + 2], 2).PadLeft(8, '0') : string.Empty, - string.Empty); - } - } - - output.AppendFormat(footerFmt, string.Empty); - return output.ToString(); - } - - private static string Print(WSFrame frame) - { - // Payload Length - var payloadLen = frame.PayloadLength; - - // Extended Payload Length - var extPayloadLen = payloadLen > 125 ? frame.FullPayloadLength.ToString() : string.Empty; - - // Masking Key - var maskingKey = BitConverter.ToString(frame.MaskingKey); - - // Payload Data - var payload = payloadLen == 0 - ? string.Empty - : payloadLen > 125 - ? "---" - : frame.IsText && !(frame.IsFragment || frame.IsMasked || frame.IsCompressed) - ? frame.Payload.ApplicationData.UTF8Decode() - : frame.Payload.ToString(); - - var format = @" - 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( - format, - frame.Fin, - frame.Rsv1, - frame.Rsv2, - frame.Rsv3, - frame.Opcode, - frame.Mask, - payloadLen, - extPayloadLen, - maskingKey, - payload); - } - - private static WSFrame ProcessHeader(byte[] header) - { - if (header.Length != 2) - { - throw new WSException("The header of a frame cannot be read from the stream."); - } - - // FIN - var fin = (header[0] & 0x80) == 0x80 ? FinalFrame.Final : FinalFrame.More; - - // RSV1 - var rsv1 = (header[0] & 0x40) == 0x40 ? ReservedBits.On : ReservedBits.Off; - - // RSV2 - var rsv2 = (header[0] & 0x20) == 0x20 ? ReservedBits.On : ReservedBits.Off; - - // RSV3 - var rsv3 = (header[0] & 0x10) == 0x10 ? ReservedBits.On : ReservedBits.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); - - var err = !opcode.IsSupported() - ? "An unsupported opcode." - : !opcode.IsData() && rsv1 == ReservedBits.On - ? "A non data frame is compressed." - : opcode.IsControl() && fin == FinalFrame.More - ? "A control frame is fragmented." - : opcode.IsControl() && payloadLen > 125 - ? "A control frame has a long payload length." - : null; - - if (err != null) - { - throw new WSException(CloseStatusCode.ProtocolError, err); - } - - var frame = new WSFrame(); - frame.Fin = fin; - frame.Rsv1 = rsv1; - frame.Rsv2 = rsv2; - frame.Rsv3 = rsv3; - frame.Opcode = (OperationCode)opcode; - frame.Mask = mask; - frame.PayloadLength = payloadLen; - - return frame; - } - - private static WSFrame readExtendedPayloadLength(Stream stream, WSFrame frame) - { - var len = frame.ExtendedPayloadLengthCount; - if (len == 0) - { - frame.ExtendedPayloadLength = WSClient.EmptyBytes; - return frame; - } - - var bytes = stream.ReadBytes(len); - if (bytes.Length != len) - { - throw new WSException( - "The extended payload length of a frame cannot be read from the stream."); - } - - frame.ExtendedPayloadLength = bytes; - return frame; - } - - private static void readExtendedPayloadLengthAsync( - Stream stream, - WSFrame frame, - Action completed, - Action error) - { - var len = frame.ExtendedPayloadLengthCount; - if (len == 0) - { - frame.ExtendedPayloadLength = WSClient.EmptyBytes; - completed(frame); - - return; - } - - stream.ReadBytesAsync( - len, - bytes => - { - if (bytes.Length != len) - { - throw new WSException( - "The extended payload length of a frame cannot be read from the stream."); - } - - frame.ExtendedPayloadLength = bytes; - completed(frame); - }, - error); - } - - private static WSFrame 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 WSFrame readMaskingKey(Stream stream, WSFrame frame) - { - var len = frame.IsMasked ? 4 : 0; - if (len == 0) - { - frame.MaskingKey = WSClient.EmptyBytes; - return frame; - } - - var bytes = stream.ReadBytes(len); - if (bytes.Length != len) - { - throw new WSException("The masking key of a frame cannot be read from the stream."); - } - - frame.MaskingKey = bytes; - return frame; - } - - private static void readMaskingKeyAsync( - Stream stream, - WSFrame frame, - Action completed, - Action error) - { - var len = frame.IsMasked ? 4 : 0; - if (len == 0) - { - frame.MaskingKey = WSClient.EmptyBytes; - completed(frame); - - return; - } - - stream.ReadBytesAsync( - len, - bytes => - { - if (bytes.Length != len) - { - throw new WSException( - "The masking key of a frame cannot be read from the stream."); - } - - frame.MaskingKey = bytes; - completed(frame); - }, - error); - } - - private static WSFrame readPayload(Stream stream, WSFrame frame) - { - var len = frame.FullPayloadLength; - if (len == 0) - { - frame.Payload = Payload.Empty; - return frame; - } - - if (len > Payload.MaxLength) - { - throw new WSException(CloseStatusCode.TooBig, "A frame has a long payload length."); - } - - var llen = (long)len; - var bytes = frame.PayloadLength < 127 - ? stream.ReadBytes((int)len) - : stream.ReadBytes(llen, BUFFER_SIZE); - - if (bytes.LongLength != llen) - { - throw new WSException( - "The payload data of a frame cannot be read from the stream."); - } - - frame.Payload = new Payload(bytes, llen); - return frame; - } - - private static void readPayloadAsync( - Stream stream, - WSFrame frame, - Action completed, - Action error) - { - var len = frame.FullPayloadLength; - if (len == 0) - { - frame.Payload = Payload.Empty; - completed(frame); - - return; - } - - if (len > Payload.MaxLength) - { - throw new WSException(CloseStatusCode.TooBig, "A frame has a long payload length."); - } - - var llen = (long)len; - Action compl = bytes => - { - if (bytes.LongLength != llen) - { - throw new WSException( - "The payload data of a frame cannot be read from the stream."); - } - - frame.Payload = new Payload(bytes, llen); - completed(frame); - }; - - if (frame.PayloadLength < 127) - { - stream.ReadBytesAsync((int)len, compl, error); - return; - } - - stream.ReadBytesAsync(llen, BUFFER_SIZE, compl, error); - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/WSState.cs b/EonaCat.Network/System/Sockets/Web/WSState.cs deleted file mode 100644 index d95b730..0000000 --- a/EonaCat.Network/System/Sockets/Web/WSState.cs +++ /dev/null @@ -1,16 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -namespace EonaCat.Network -{ - public enum WSState : ushort - { - Connecting = 0, - - Open = 1, - - Closing = 2, - - Closed = 3 - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/WebBase.cs b/EonaCat.Network/System/Sockets/Web/WebBase.cs deleted file mode 100644 index 9018a40..0000000 --- a/EonaCat.Network/System/Sockets/Web/WebBase.cs +++ /dev/null @@ -1,162 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.IO; -using System.Text; -using System.Threading; - -namespace EonaCat.Network -{ - internal abstract class WebBase - { - internal byte[] EntityBodyData; - protected const string CrLf = "\r\n"; - private const int _headersMaxLength = 8192; - - protected WebBase(Version version, NameValueCollection headers) - { - ProtocolVersion = version; - Headers = headers; - } - - 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; } - - public Version ProtocolVersion { get; } - - public byte[] ToByteArray() - { - return Encoding.UTF8.GetBytes(ToString()); - } - - protected static T Read(Stream stream, Func parser, int millisecondsTimeout) - where T : WebBase - { - 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 message = 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 (message != null) - { - throw new WSException(message, exception); - } - - return http; - } - - private static byte[] readEntityBody(Stream stream, string length) - { - if (!long.TryParse(length, out long len)) - { - throw new ArgumentException("Cannot be parsed.", nameof(length)); - } - - if (len < 0) - { - throw new ArgumentOutOfRangeException(nameof(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 WSException("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); - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/WebRequest.cs b/EonaCat.Network/System/Sockets/Web/WebRequest.cs deleted file mode 100644 index d7e9e59..0000000 --- a/EonaCat.Network/System/Sockets/Web/WebRequest.cs +++ /dev/null @@ -1,172 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.Collections.Specialized; -using System.IO; -using System.Text; - -namespace EonaCat.Network -{ - internal class WebRequest : WebBase - { - private bool _websocketRequest; - private bool _websocketRequestSet; - - internal WebRequest(string method, string uri) - : this(method, uri, HttpVersion.Version11, new NameValueCollection()) - { - Headers["User-Agent"] = $"EonaCat.Network/{Constants.Version}"; - } - - private WebRequest(string method, string uri, Version version, NameValueCollection headers) - : base(version, headers) - { - HttpMethod = method; - RequestUri = uri; - } - - public AuthenticationResponse AuthenticationResponse - { - get - { - var res = Headers["Authorization"]; - return res != null && res.Length > 0 - ? AuthenticationResponse.Parse(res) - : null; - } - } - - public CookieCollection Cookies => Headers.GetCookies(false); - - public string HttpMethod { get; } - - public bool IsWebSocketRequest - { - get - { - if (!_websocketRequestSet) - { - var headers = Headers; - _websocketRequest = HttpMethod == "GET" && - ProtocolVersion > HttpVersion.Version10 && - headers.Contains("Upgrade", "websocket") && - headers.Contains("Connection", "Upgrade"); - - _websocketRequestSet = true; - } - - return _websocketRequest; - } - } - - public string RequestUri { get; } - - 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}", HttpMethod, RequestUri, 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(); - } - - internal static WebRequest CreateConnectRequest(Uri uri) - { - var host = uri.DnsSafeHost; - var port = uri.Port; - var authority = string.Format("{0}:{1}", host, port); - var req = new WebRequest("CONNECT", authority); - req.Headers["Host"] = port == 80 ? host : authority; - - return req; - } - - internal static WebRequest CreateWebSocketRequest(Uri uri) - { - var req = new WebRequest("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 static WebRequest 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 WebRequest( - requestLine[0], requestLine[1], new Version(requestLine[2].Substring(5)), headers); - } - - internal static WebRequest Read(Stream stream, int millisecondsTimeout) - { - return Read(stream, Parse, millisecondsTimeout); - } - - internal WebResponse GetResponse(Stream stream, int millisecondsTimeout) - { - var buff = ToByteArray(); - stream.Write(buff, 0, buff.Length); - - return Read(stream, WebResponse.Parse, millisecondsTimeout); - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/WebResponse.cs b/EonaCat.Network/System/Sockets/Web/WebResponse.cs deleted file mode 100644 index 303c3e2..0000000 --- a/EonaCat.Network/System/Sockets/Web/WebResponse.cs +++ /dev/null @@ -1,143 +0,0 @@ -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -using System; -using System.Collections.Specialized; -using System.IO; -using System.Text; - -namespace EonaCat.Network -{ - internal class WebResponse : WebBase - { - internal WebResponse(HttpStatusCode code) - : this(code, code.GetDescription()) - { - } - - internal WebResponse(HttpStatusCode code, string reason) - : this(((int)code).ToString(), reason, HttpVersion.Version11, new NameValueCollection()) - { - Headers["Server"] = $"EonaCat.Network/{Constants.Version}"; - } - - private WebResponse(string code, string reason, Version version, NameValueCollection headers) - : base(version, headers) - { - StatusCode = code; - Reason = reason; - } - - public CookieCollection Cookies => Headers.GetCookies(true); - - public bool HasConnectionClose => Headers.Contains("Connection", "close"); - - public bool IsProxyAuthenticationRequired => StatusCode == "407"; - - public bool IsRedirect => StatusCode == "301" || StatusCode == "302"; - - public bool IsUnauthorized => StatusCode == "401"; - - public bool IsWebSocketResponse - { - get - { - var headers = Headers; - return ProtocolVersion > HttpVersion.Version10 && - StatusCode == "101" && - headers.Contains("Upgrade", "websocket") && - headers.Contains("Connection", "Upgrade"); - } - } - - public string Reason { get; } - - public string StatusCode { get; } - - 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, StatusCode, 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(); - } - - internal static WebResponse CreateCloseResponse(HttpStatusCode code) - { - var res = new WebResponse(code); - res.Headers["Connection"] = "close"; - - return res; - } - - internal static WebResponse CreateUnauthorizedResponse(string challenge) - { - var res = new WebResponse(HttpStatusCode.Unauthorized); - res.Headers["WWW-Authenticate"] = challenge; - - return res; - } - - internal static WebResponse CreateWebSocketResponse() - { - var res = new WebResponse(HttpStatusCode.SwitchingProtocols); - - var headers = res.Headers; - headers["Upgrade"] = "websocket"; - headers["Connection"] = "Upgrade"; - - return res; - } - - internal static WebResponse 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 WebResponse( - statusLine[1], statusLine[2], new Version(statusLine[0].Substring(5)), headers); - } - - internal static WebResponse Read(Stream stream, int millisecondsTimeout) - { - return Read(stream, Parse, millisecondsTimeout); - } - } -} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Buffer/BufferValidator.cs b/EonaCat.Network/System/Sockets/WebSockets/Buffer/BufferValidator.cs new file mode 100644 index 0000000..9cf5723 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Buffer/BufferValidator.cs @@ -0,0 +1,49 @@ +using System; + +namespace EonaCat.WebSockets.Buffer +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class BufferValidator + { + public static void ValidateBuffer(byte[] buffer, int offset, int count, + string bufferParameterName = null, + string offsetParameterName = null, + string countParameterName = null) + { + if (buffer == null) + { + throw new ArgumentNullException(!string.IsNullOrEmpty(bufferParameterName) ? bufferParameterName : "buffer"); + } + + if (offset < 0 || offset > buffer.Length) + { + throw new ArgumentOutOfRangeException(!string.IsNullOrEmpty(offsetParameterName) ? offsetParameterName : "offset"); + } + + if (count < 0 || count > (buffer.Length - offset)) + { + throw new ArgumentOutOfRangeException(!string.IsNullOrEmpty(countParameterName) ? countParameterName : "count"); + } + } + + public static void ValidateArraySegment(ArraySegment arraySegment, string arraySegmentParameterName = null) + { + if (arraySegment.Array == null) + { + throw new ArgumentNullException((!string.IsNullOrEmpty(arraySegmentParameterName) ? arraySegmentParameterName : "arraySegment") + ".Array"); + } + + if (arraySegment.Offset < 0 || arraySegment.Offset > arraySegment.Array.Length) + { + throw new ArgumentOutOfRangeException((!string.IsNullOrEmpty(arraySegmentParameterName) ? arraySegmentParameterName : "arraySegment") + ".Offset"); + } + + if (arraySegment.Count < 0 || arraySegment.Count > (arraySegment.Array.Length - arraySegment.Offset)) + { + throw new ArgumentOutOfRangeException((!string.IsNullOrEmpty(arraySegmentParameterName) ? arraySegmentParameterName : "arraySegment") + ".Count"); + } + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Buffer/ISegmentBufferManager.cs b/EonaCat.Network/System/Sockets/WebSockets/Buffer/ISegmentBufferManager.cs new file mode 100644 index 0000000..a5db27d --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Buffer/ISegmentBufferManager.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; + +namespace EonaCat.WebSockets.Buffer +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public interface ISegmentBufferManager + { + ArraySegment BorrowBuffer(); + IEnumerable> BorrowBuffers(int count); + void ReturnBuffer(ArraySegment buffer); + void ReturnBuffers(IEnumerable> buffers); + void ReturnBuffers(params ArraySegment[] buffers); + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Buffer/SegmentBufferDeflector.cs b/EonaCat.Network/System/Sockets/WebSockets/Buffer/SegmentBufferDeflector.cs new file mode 100644 index 0000000..43be208 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Buffer/SegmentBufferDeflector.cs @@ -0,0 +1,93 @@ +using System; + +namespace EonaCat.WebSockets.Buffer +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class SegmentBufferDeflector + { + public static void AppendBuffer( + ISegmentBufferManager bufferManager, + ref ArraySegment receiveBuffer, + int receiveCount, + ref ArraySegment sessionBuffer, + ref int sessionBufferCount) + { + if (sessionBuffer.Count < (sessionBufferCount + receiveCount)) + { + ArraySegment autoExpandedBuffer = bufferManager.BorrowBuffer(); + if (autoExpandedBuffer.Count < (sessionBufferCount + receiveCount) * 2) + { + bufferManager.ReturnBuffer(autoExpandedBuffer); + autoExpandedBuffer = new ArraySegment(new byte[(sessionBufferCount + receiveCount) * 2]); + } + + Array.Copy(sessionBuffer.Array, sessionBuffer.Offset, autoExpandedBuffer.Array, autoExpandedBuffer.Offset, sessionBufferCount); + + var discardBuffer = sessionBuffer; + sessionBuffer = autoExpandedBuffer; + bufferManager.ReturnBuffer(discardBuffer); + } + + Array.Copy(receiveBuffer.Array, receiveBuffer.Offset, sessionBuffer.Array, sessionBuffer.Offset + sessionBufferCount, receiveCount); + sessionBufferCount = sessionBufferCount + receiveCount; + } + + public static void ShiftBuffer( + ISegmentBufferManager bufferManager, + int shiftStart, + ref ArraySegment sessionBuffer, + ref int sessionBufferCount) + { + if ((sessionBufferCount - shiftStart) < shiftStart) + { + Array.Copy(sessionBuffer.Array, sessionBuffer.Offset + shiftStart, sessionBuffer.Array, sessionBuffer.Offset, sessionBufferCount - shiftStart); + sessionBufferCount = sessionBufferCount - shiftStart; + } + else + { + ArraySegment copyBuffer = bufferManager.BorrowBuffer(); + if (copyBuffer.Count < (sessionBufferCount - shiftStart)) + { + bufferManager.ReturnBuffer(copyBuffer); + copyBuffer = new ArraySegment(new byte[sessionBufferCount - shiftStart]); + } + + Array.Copy(sessionBuffer.Array, sessionBuffer.Offset + shiftStart, copyBuffer.Array, copyBuffer.Offset, sessionBufferCount - shiftStart); + Array.Copy(copyBuffer.Array, copyBuffer.Offset, sessionBuffer.Array, sessionBuffer.Offset, sessionBufferCount - shiftStart); + sessionBufferCount = sessionBufferCount - shiftStart; + + bufferManager.ReturnBuffer(copyBuffer); + } + } + + public static void ReplaceBuffer( + ISegmentBufferManager bufferManager, + ref ArraySegment receiveBuffer, + ref int receiveBufferOffset, + int receiveCount) + { + if ((receiveBufferOffset + receiveCount) < receiveBuffer.Count) + { + receiveBufferOffset = receiveBufferOffset + receiveCount; + } + else + { + ArraySegment autoExpandedBuffer = bufferManager.BorrowBuffer(); + if (autoExpandedBuffer.Count < (receiveBufferOffset + receiveCount) * 2) + { + bufferManager.ReturnBuffer(autoExpandedBuffer); + autoExpandedBuffer = new ArraySegment(new byte[(receiveBufferOffset + receiveCount) * 2]); + } + + Array.Copy(receiveBuffer.Array, receiveBuffer.Offset, autoExpandedBuffer.Array, autoExpandedBuffer.Offset, receiveBufferOffset + receiveCount); + receiveBufferOffset = receiveBufferOffset + receiveCount; + + var discardBuffer = receiveBuffer; + receiveBuffer = autoExpandedBuffer; + bufferManager.ReturnBuffer(discardBuffer); + } + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Buffer/SegmentBufferManager.cs b/EonaCat.Network/System/Sockets/WebSockets/Buffer/SegmentBufferManager.cs new file mode 100644 index 0000000..cab78bb --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Buffer/SegmentBufferManager.cs @@ -0,0 +1,273 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +namespace EonaCat.WebSockets.Buffer +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// A manager to handle buffers for the socket connections. + /// + /// + /// When used in an async call a buffer is pinned. Large numbers of pinned buffers + /// cause problem with the GC (in particular it causes heap fragmentation). + /// This class maintains a set of large segments and gives clients pieces of these + /// segments that they can use for their buffers. The alternative to this would be to + /// create many small arrays which it then maintained. This methodology should be slightly + /// better than the many small array methodology because in creating only a few very + /// large objects it will force these objects to be placed on the LOH. Since the + /// objects are on the LOH they are at this time not subject to compacting which would + /// require an update of all GC roots as would be the case with lots of smaller arrays + /// that were in the normal heap. + /// + public class SegmentBufferManager : ISegmentBufferManager + { + private const int TrialsCount = 100; + + private static SegmentBufferManager _defaultBufferManager; + + private readonly int _segmentChunks; + private readonly int _chunkSize; + private readonly int _segmentSize; + private readonly bool _allowedToCreateMemory; + + private readonly ConcurrentStack> _buffers = new ConcurrentStack>(); + + private readonly List _segments; + private readonly object _creatingNewSegmentLock = new object(); + + public static SegmentBufferManager Default + { + get + { + // default to 1024 1kb buffers if people don't want to manage it on their own; + if (_defaultBufferManager == null) + { + _defaultBufferManager = new SegmentBufferManager(1024, 1024, 1); + } + + return _defaultBufferManager; + } + } + + public static void SetDefaultBufferManager(SegmentBufferManager manager) + { + if (manager == null) + { + throw new ArgumentNullException("manager"); + } + + _defaultBufferManager = manager; + } + + public int ChunkSize + { + get { return _chunkSize; } + } + + public int SegmentsCount + { + get { return _segments.Count; } + } + + public int SegmentChunksCount + { + get { return _segmentChunks; } + } + + public int AvailableBuffers + { + get { return _buffers.Count; } + } + + public int TotalBufferSize + { + get { return _segments.Count * _segmentSize; } + } + + public SegmentBufferManager(int segmentChunks, int chunkSize) + : this(segmentChunks, chunkSize, 1) { } + + public SegmentBufferManager(int segmentChunks, int chunkSize, int initialSegments) + : this(segmentChunks, chunkSize, initialSegments, true) { } + + /// + /// Constructs a new object + /// + /// The number of chunks to create per segment + /// The size of a chunk in bytes + /// The initial number of segments to create + /// If false when empty and checkout is called an exception will be thrown + public SegmentBufferManager(int segmentChunks, int chunkSize, int initialSegments, bool allowedToCreateMemory) + { + if (segmentChunks <= 0) + { + throw new ArgumentException("segmentChunks"); + } + + if (chunkSize <= 0) + { + throw new ArgumentException("chunkSize"); + } + + if (initialSegments < 0) + { + throw new ArgumentException("initialSegments"); + } + + _segmentChunks = segmentChunks; + _chunkSize = chunkSize; + _segmentSize = _segmentChunks * _chunkSize; + + _segments = new List(); + + _allowedToCreateMemory = true; + for (int i = 0; i < initialSegments; i++) + { + CreateNewSegment(true); + } + _allowedToCreateMemory = allowedToCreateMemory; + } + + private void CreateNewSegment(bool forceCreation) + { + if (!_allowedToCreateMemory) + { + throw new UnableToCreateMemoryException(); + } + + lock (_creatingNewSegmentLock) + { + if (!forceCreation && _buffers.Count > _segmentChunks / 2) + { + return; + } + + var bytes = new byte[_segmentSize]; + _segments.Add(bytes); + for (int i = 0; i < _segmentChunks; i++) + { + var chunk = new ArraySegment(bytes, i * _chunkSize, _chunkSize); + _buffers.Push(chunk); + } + } + } + + public ArraySegment BorrowBuffer() + { + int trial = 0; + while (trial < TrialsCount) + { + ArraySegment result; + if (_buffers.TryPop(out result)) + { + return result; + } + + CreateNewSegment(false); + trial++; + } + throw new UnableToAllocateBufferException(); + } + + public IEnumerable> BorrowBuffers(int count) + { + var result = new ArraySegment[count]; + var trial = 0; + var totalReceived = 0; + + try + { + while (trial < TrialsCount) + { + ArraySegment piece; + while (totalReceived < count) + { + if (!_buffers.TryPop(out piece)) + { + break; + } + + result[totalReceived] = piece; + ++totalReceived; + } + if (totalReceived == count) + { + return result; + } + + CreateNewSegment(false); + trial++; + } + throw new UnableToAllocateBufferException(); + } + catch + { + if (totalReceived > 0) + { + ReturnBuffers(result.Take(totalReceived)); + } + + throw; + } + } + + public void ReturnBuffer(ArraySegment buffer) + { + if (ValidateBuffer(buffer)) + { + _buffers.Push(buffer); + } + } + + public void ReturnBuffers(IEnumerable> buffers) + { + if (buffers == null) + { + throw new ArgumentNullException("buffers"); + } + + foreach (var buf in buffers) + { + if (ValidateBuffer(buf)) + { + _buffers.Push(buf); + } + } + } + + public void ReturnBuffers(params ArraySegment[] buffers) + { + if (buffers == null) + { + throw new ArgumentNullException("buffers"); + } + + foreach (var buf in buffers) + { + if (ValidateBuffer(buf)) + { + _buffers.Push(buf); + } + } + } + + private bool ValidateBuffer(ArraySegment buffer) + { + if (buffer.Array == null || buffer.Count == 0 || buffer.Array.Length < buffer.Offset + buffer.Count) + { + return false; + } + + if (buffer.Count != _chunkSize) + { + return false; + } + + return true; + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Buffer/UnableToAllocateBufferException.cs b/EonaCat.Network/System/Sockets/WebSockets/Buffer/UnableToAllocateBufferException.cs new file mode 100644 index 0000000..5f32b5e --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Buffer/UnableToAllocateBufferException.cs @@ -0,0 +1,16 @@ +using System; + +namespace EonaCat.WebSockets.Buffer +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + [Serializable] + public class UnableToAllocateBufferException : Exception + { + public UnableToAllocateBufferException() + : base("Cannot allocate buffer after few trials.") + { + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Buffer/UnableToCreateMemoryException .cs b/EonaCat.Network/System/Sockets/WebSockets/Buffer/UnableToCreateMemoryException .cs new file mode 100644 index 0000000..7f6d0f2 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Buffer/UnableToCreateMemoryException .cs @@ -0,0 +1,16 @@ +using System; + +namespace EonaCat.WebSockets.Buffer +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + [Serializable] + public class UnableToCreateMemoryException : Exception + { + public UnableToCreateMemoryException() + : base("All buffers were in use and acquiring more memory has been disabled.") + { + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Client/AsyncWebSocketClient.cs b/EonaCat.Network/System/Sockets/WebSockets/Client/AsyncWebSocketClient.cs new file mode 100644 index 0000000..4c5b8cf --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Client/AsyncWebSocketClient.cs @@ -0,0 +1,1332 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using EonaCat.WebSockets.Buffer; +using EonaCat.Logger.Extensions; +using EonaCat.Network; +using EonaCat.WebSockets.Extensions; +using EonaCat.WebSockets.SubProtocols; + +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class AsyncWebSocketClient : IDisposable + { + #region Fields + + private TcpClient _tcpClient; + private readonly IAsyncWebSocketClientMessageDispatcher _dispatcher; + private readonly AsyncWebSocketClientConfiguration _configuration; + private readonly IFrameBuilder _frameBuilder = new WebSocketFrameBuilder(); + private IPEndPoint _remoteEndPoint; + private Stream _stream; + private ArraySegment _receiveBuffer = default(ArraySegment); + private int _receiveBufferOffset = 0; + + private readonly Uri _uri; + private bool _sslEnabled = false; + private string _secWebSocketKey; + + private int _state; + private const int _none = 0; + private const int _connecting = 1; + private const int _connected = 2; + private const int _closing = 3; + private const int _closed = 5; + + private readonly SemaphoreSlim _keepAliveLocker = new SemaphoreSlim(1, 1); + private KeepAliveTracker _keepAliveTracker; + private Timer _keepAliveTimeoutTimer; + private Timer _closingTimeoutTimer; + + #endregion + + #region Constructors + + public AsyncWebSocketClient(Uri uri, IAsyncWebSocketClientMessageDispatcher dispatcher, AsyncWebSocketClientConfiguration configuration = null) + { + if (uri == null) + { + throw new ArgumentNullException("uri"); + } + + if (dispatcher == null) + { + throw new ArgumentNullException("dispatcher"); + } + + if (!Consts.WebSocketSchemes.Contains(uri.Scheme.ToLowerInvariant())) + { + throw new NotSupportedException( + string.Format("Not support the specified scheme [{0}].", uri.Scheme)); + } + + _uri = uri; + _remoteEndPoint = ResolveRemoteEndPoint(_uri); + _dispatcher = dispatcher; + _configuration = configuration ?? new AsyncWebSocketClientConfiguration(); + _sslEnabled = uri.Scheme.ToLowerInvariant() == "wss"; + + if (_configuration.BufferManager == null) + { + throw new InvalidProgramException("The buffer manager in configuration cannot be null."); + } + } + + public AsyncWebSocketClient(Uri uri, + Func onServerTextReceived = null, + Func onServerBinaryReceived = null, + Func onServerConnected = null, + Func onServerDisconnected = null, + AsyncWebSocketClientConfiguration configuration = null) + : this(uri, + new InternalAsyncWebSocketClientMessageDispatcherImplementation( + onServerTextReceived, onServerBinaryReceived, onServerConnected, onServerDisconnected), + configuration) + { + } + + public AsyncWebSocketClient(Uri uri, + Func onServerTextReceived = null, + Func onServerBinaryReceived = null, + Func onServerConnected = null, + Func onServerDisconnected = null, + Func onServerFragmentationStreamOpened = null, + Func onServerFragmentationStreamContinued = null, + Func onServerFragmentationStreamClosed = null, + AsyncWebSocketClientConfiguration configuration = null) + : this(uri, + new InternalAsyncWebSocketClientMessageDispatcherImplementation( + onServerTextReceived, onServerBinaryReceived, onServerConnected, onServerDisconnected, + onServerFragmentationStreamOpened, onServerFragmentationStreamContinued, onServerFragmentationStreamClosed), + configuration) + { + } + + private IPEndPoint ResolveRemoteEndPoint(Uri uri) + { + var host = uri.Host; + var port = uri.Port > 0 ? uri.Port : uri.Scheme.ToLowerInvariant() == "wss" ? 443 : 80; + + IPAddress ipAddress; + if (IPAddress.TryParse(host, out ipAddress)) + { + return new IPEndPoint(ipAddress, port); + } + else + { + if (host.ToLowerInvariant() == "localhost") + { + return new IPEndPoint(IPAddress.Parse(@"127.0.0.1"), port); + } + else + { + IPAddress[] addresses = Dns.GetHostAddresses(host); + if (addresses.Length > 0) + { + return new IPEndPoint(addresses[0], port); + } + else + { + throw new InvalidOperationException( + string.Format("Cannot resolve host [{0}] by DNS.", host)); + } + } + } + } + + #endregion + + #region Properties + + private bool Connected { get { return _tcpClient != null && _tcpClient.Client.Connected; } } + public IPEndPoint RemoteEndPoint { get { return Connected ? (IPEndPoint)_tcpClient.Client.RemoteEndPoint : _remoteEndPoint; } } + public IPEndPoint LocalEndPoint { get { return Connected ? (IPEndPoint)_tcpClient.Client.LocalEndPoint : null; } } + + public Uri Uri { get { return _uri; } } + + public TimeSpan ConnectTimeout { get { return _configuration.ConnectTimeout; } } + public TimeSpan CloseTimeout { get { return _configuration.CloseTimeout; } } + public TimeSpan KeepAliveInterval { get { return _configuration.KeepAliveInterval; } } + public TimeSpan KeepAliveTimeout { get { return _configuration.KeepAliveTimeout; } } + + public IDictionary EnabledExtensions { get { return _configuration.EnabledExtensions; } } + public IDictionary EnabledSubProtocols { get { return _configuration.EnabledSubProtocols; } } + public IEnumerable OfferedExtensions { get { return _configuration.OfferedExtensions; } } + public IEnumerable RequestedSubProtocols { get { return _configuration.RequestedSubProtocols; } } + + public WebSocketState State + { + get + { + switch (_state) + { + case _none: + return WebSocketState.None; + case _connecting: + return WebSocketState.Connecting; + case _connected: + return WebSocketState.Open; + case _closing: + return WebSocketState.Closing; + case _closed: + return WebSocketState.Closed; + default: + return WebSocketState.Closed; + } + } + } + + public override string ToString() + { + return string.Format("RemoteEndPoint[{0}], LocalEndPoint[{1}]", + this.RemoteEndPoint, this.LocalEndPoint); + } + + #endregion + + #region Connect + + public async Task Connect() + { + int origin = Interlocked.Exchange(ref _state, _connecting); + if (!(origin == _none || origin == _closed)) + { + await InternalClose(false); + throw new InvalidOperationException("This websocket client is in invalid state when connecting."); + } + + try + { + Clean(); // forcefully clean all things + ResetKeepAlive(); + + _tcpClient = new TcpClient(_remoteEndPoint.Address.AddressFamily); + + var awaiter = _tcpClient.ConnectAsync(_remoteEndPoint.Address, _remoteEndPoint.Port); + if (!awaiter.Wait(ConnectTimeout)) + { + await InternalClose(false); + throw new TimeoutException(string.Format( + "Connect to [{0}] timeout [{1}].", _remoteEndPoint, ConnectTimeout)); + } + + ConfigureClient(); + var negotiator = NegotiateStream(_tcpClient.GetStream()); + if (!negotiator.Wait(ConnectTimeout)) + { + await InternalClose(false); + throw new TimeoutException(string.Format( + "Negotiate SSL/TSL with remote [{0}] timeout [{1}].", RemoteEndPoint, ConnectTimeout)); + } + _stream = negotiator.Result; + + _receiveBuffer = _configuration.BufferManager.BorrowBuffer(); + _receiveBufferOffset = 0; + + var handshaker = OpenHandshake(); + if (!handshaker.Wait(ConnectTimeout)) + { + await Close(WebSocketCloseCode.ProtocolError, "Opening handshake timeout."); + throw new TimeoutException(string.Format( + "Handshake with remote [{0}] timeout [{1}].", RemoteEndPoint, ConnectTimeout)); + } + if (!handshaker.Result) + { + await Close(WebSocketCloseCode.ProtocolError, "Opening handshake failed."); + throw new WebSocketException(string.Format( + "Handshake with remote [{0}] failed.", RemoteEndPoint)); + } + + if (Interlocked.CompareExchange(ref _state, _connected, _connecting) != _connecting) + { + await InternalClose(false); + throw new InvalidOperationException("This websocket client is in invalid state when connected."); + } + + NetworkHelper.Logger.Debug($"Connected to server [{this.RemoteEndPoint}] with dispatcher [{_dispatcher.GetType().Name}] on [{DateTime.UtcNow.ToString(@"yyyy-MM-dd HH:mm:ss.fffffff")}]."); + + bool isErrorOccurredInUserSide = false; + try + { + await _dispatcher.OnServerConnected(this); + } + catch (Exception ex) + { + isErrorOccurredInUserSide = true; + await HandleUserSideError(ex); + } + + if (!isErrorOccurredInUserSide) + { + Task.Factory.StartNew(async () => + { + _keepAliveTracker.StartTimer(); + await Process(); + }, + TaskCreationOptions.LongRunning) + .Forget(); + } + else + { + await InternalClose(true); // user side handle tcp connection error occurred + } + } + catch (Exception ex) + { + NetworkHelper.Logger.Error(ex.FormatExceptionToMessage()); + throw; + } + } + + private void ConfigureClient() + { + _tcpClient.ReceiveBufferSize = _configuration.ReceiveBufferSize; + _tcpClient.SendBufferSize = _configuration.SendBufferSize; + _tcpClient.ReceiveTimeout = (int)_configuration.ReceiveTimeout.TotalMilliseconds; + _tcpClient.SendTimeout = (int)_configuration.SendTimeout.TotalMilliseconds; + _tcpClient.NoDelay = _configuration.NoDelay; + _tcpClient.LingerState = _configuration.LingerState; + } + + private async Task NegotiateStream(Stream stream) + { + if (!_sslEnabled) + { + return stream; + } + + var validateRemoteCertificate = new RemoteCertificateValidationCallback( + (object sender, + X509Certificate certificate, + X509Chain chain, + SslPolicyErrors sslPolicyErrors) + => + { + if (sslPolicyErrors == SslPolicyErrors.None) + { + return true; + } + + if (_configuration.SslPolicyErrorsBypassed) + { + return true; + } + else + { + NetworkHelper.Logger.Error($"Error occurred when validating remote certificate: [{this.RemoteEndPoint}], [{sslPolicyErrors}]."); + } + + return false; + }); + + var sslStream = new SslStream( + stream, + false, + validateRemoteCertificate, + null, + _configuration.SslEncryptionPolicy); + + if (_configuration.SslClientCertificates == null || _configuration.SslClientCertificates.Count == 0) + { + await sslStream.AuthenticateAsClientAsync( // No client certificates are used in the authentication. The certificate revocation list is not checked during authentication. + _configuration.SslTargetHost); // The name of the server that will share this SslStream. The value specified for targetHost must match the name on the server's certificate. + } + else + { + await sslStream.AuthenticateAsClientAsync( + _configuration.SslTargetHost, // The name of the server that will share this SslStream. The value specified for targetHost must match the name on the server's certificate. + _configuration.SslClientCertificates, // The X509CertificateCollection that contains client certificates. + _configuration.SslEnabledProtocols, // The SslProtocols value that represents the protocol used for authentication. + _configuration.SslCheckCertificateRevocation); // A Boolean value that specifies whether the certificate revocation list is checked during authentication. + } + + // When authentication succeeds, you must check the IsEncrypted and IsSigned properties + // to determine what security services are used by the SslStream. + // Check the IsMutuallyAuthenticated property to determine whether mutual authentication occurred. + NetworkHelper.Logger.Debug(string.Format( + "Ssl Stream: SslProtocol[{0}], IsServer[{1}], IsAuthenticated[{2}], IsEncrypted[{3}], IsSigned[{4}], IsMutuallyAuthenticated[{5}], " + + "HashAlgorithm[{6}], HashStrength[{7}], KeyExchangeAlgorithm[{8}], KeyExchangeStrength[{9}], CipherAlgorithm[{10}], CipherStrength[{11}].", + sslStream.SslProtocol, + sslStream.IsServer, + sslStream.IsAuthenticated, + sslStream.IsEncrypted, + sslStream.IsSigned, + sslStream.IsMutuallyAuthenticated, + sslStream.HashAlgorithm, + sslStream.HashStrength, + sslStream.KeyExchangeAlgorithm, + sslStream.KeyExchangeStrength, + sslStream.CipherAlgorithm, + sslStream.CipherStrength)); + + return sslStream; + } + + private async Task OpenHandshake() + { + bool handshakeResult = false; + + try + { + var requestBuffer = WebSocketClientHandshaker.CreateOpenningHandshakeRequest(this, out _secWebSocketKey); + await _stream.WriteAsync(requestBuffer, 0, requestBuffer.Length); + + int terminatorIndex = -1; + while (!WebSocketHelpers.FindHttpMessageTerminator(_receiveBuffer.Array, _receiveBuffer.Offset, _receiveBufferOffset, out terminatorIndex)) + { + int receiveCount = await _stream.ReadAsync( + _receiveBuffer.Array, + _receiveBuffer.Offset + _receiveBufferOffset, + _receiveBuffer.Count - _receiveBufferOffset); + if (receiveCount == 0) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to receive zero bytes.", RemoteEndPoint)); + } + + SegmentBufferDeflector.ReplaceBuffer(_configuration.BufferManager, ref _receiveBuffer, ref _receiveBufferOffset, receiveCount); + + if (_receiveBufferOffset > 2048) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to receive weird stream.", RemoteEndPoint)); + } + } + + handshakeResult = WebSocketClientHandshaker.VerifyOpenningHandshakeResponse( + this, + _receiveBuffer.Array, + _receiveBuffer.Offset, + terminatorIndex + Consts.HttpMessageTerminator.Length, + _secWebSocketKey); + + SegmentBufferDeflector.ShiftBuffer( + _configuration.BufferManager, + terminatorIndex + Consts.HttpMessageTerminator.Length, + ref _receiveBuffer, + ref _receiveBufferOffset); + } + catch (WebSocketHandshakeException ex) + { + NetworkHelper.Logger.Error(ex.FormatExceptionToMessage()); + handshakeResult = false; + } + catch (Exception) + { + handshakeResult = false; + throw; + } + + return handshakeResult; + } + + private void ResetKeepAlive() + { + _keepAliveTracker = KeepAliveTracker.Create(KeepAliveInterval, new TimerCallback((s) => OnKeepAlive())); + _keepAliveTimeoutTimer = new Timer(new TimerCallback((s) => OnKeepAliveTimeout()), null, Timeout.Infinite, Timeout.Infinite); + _closingTimeoutTimer = new Timer(new TimerCallback((s) => OnCloseTimeout()), null, Timeout.Infinite, Timeout.Infinite); + } + + #endregion + + #region Process + + private async Task Process() + { + try + { + Header frameHeader; + byte[] payload; + int payloadOffset; + int payloadCount; + int consumedLength = 0; + + while (State == WebSocketState.Open || State == WebSocketState.Closing) + { + int receiveCount = await _stream.ReadAsync( + _receiveBuffer.Array, + _receiveBuffer.Offset + _receiveBufferOffset, + _receiveBuffer.Count - _receiveBufferOffset); + if (receiveCount == 0) + { + break; + } + + _keepAliveTracker.OnDataReceived(); + SegmentBufferDeflector.ReplaceBuffer(_configuration.BufferManager, ref _receiveBuffer, ref _receiveBufferOffset, receiveCount); + consumedLength = 0; + + while (true) + { + frameHeader = null; + payload = null; + payloadOffset = 0; + payloadCount = 0; + + if (_frameBuilder.TryDecodeFrameHeader( + _receiveBuffer.Array, + _receiveBuffer.Offset + consumedLength, + _receiveBufferOffset - consumedLength, + out frameHeader) + && frameHeader.Length + frameHeader.PayloadLength <= _receiveBufferOffset - consumedLength) + { + try + { + if (frameHeader.IsMasked) + { + await Close(WebSocketCloseCode.ProtocolError, "A client MUST close a connection if it detects a masked frame."); + throw new WebSocketException(string.Format( + "Client received masked frame [{0}] from remote [{1}].", frameHeader.OpCode, RemoteEndPoint)); + } + + _frameBuilder.DecodePayload( + _receiveBuffer.Array, + _receiveBuffer.Offset + consumedLength, + frameHeader, + out payload, out payloadOffset, out payloadCount); + + switch (frameHeader.OpCode) + { + case OpCode.Continuation: + await HandleContinuationFrame(frameHeader, payload, payloadOffset, payloadCount); + break; + case OpCode.Text: + await HandleTextFrame(frameHeader, payload, payloadOffset, payloadCount); + break; + case OpCode.Binary: + await HandleBinaryFrame(frameHeader, payload, payloadOffset, payloadCount); + break; + case OpCode.Close: + await HandleCloseFrame(frameHeader, payload, payloadOffset, payloadCount); + break; + case OpCode.Ping: + await HandlePingFrame(frameHeader, payload, payloadOffset, payloadCount); + break; + case OpCode.Pong: + await HandlePongFrame(frameHeader, payload, payloadOffset, payloadCount); + break; + default: + { + // Incoming data MUST always be validated by both clients and servers. + // If, at any time, an endpoint is faced with data that it does not + // understand or that violates some criteria by which the endpoint + // determines safety of input, or when the endpoint sees an opening + // handshake that does not correspond to the values it is expecting + // (e.g., incorrect path or origin in the client request), the endpoint + // MAY drop the TCP connection. If the invalid data was received after + // a successful WebSocket handshake, the endpoint SHOULD send a Close + // frame with an appropriate status code (Section 7.4) before proceeding + // to _Close the WebSocket Connection_. Use of a Close frame with an + // appropriate status code can help in diagnosing the problem. If the + // invalid data is sent during the WebSocket handshake, the server + // SHOULD return an appropriate HTTP [RFC2616] status code. + await Close(WebSocketCloseCode.InvalidMessageType); + throw new NotSupportedException( + string.Format("Not support received opcode [{0}].", (byte)frameHeader.OpCode)); + } + } + } + catch (Exception ex) + { + NetworkHelper.Logger.Error(ex.FormatExceptionToMessage()); + throw; + } + finally + { + consumedLength += frameHeader.Length + frameHeader.PayloadLength; + } + } + else + { + break; + } + } + + if (_receiveBuffer != null && _receiveBuffer.Array != null) + { + SegmentBufferDeflector.ShiftBuffer(_configuration.BufferManager, consumedLength, ref _receiveBuffer, ref _receiveBufferOffset); + } + } + } + catch (ObjectDisposedException) + { + // looking forward to a graceful quit from the ReadAsync but the inside EndRead will raise the ObjectDisposedException, + // so a gracefully close for the socket should be a Shutdown, but we cannot avoid the Close triggers this happen. + } + catch (Exception ex) + { + await HandleReceiveOperationException(ex); + } + finally + { + await InternalClose(true); // read async buffer returned, remote notifies closed + } + } + + private async Task HandleContinuationFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) + { + if (!frameHeader.IsFIN) + { + try + { + await _dispatcher.OnServerFragmentationStreamContinued(this, payload, payloadOffset, payloadCount); + } + catch (Exception ex) + { + await HandleUserSideError(ex); + } + } + else + { + try + { + await _dispatcher.OnServerFragmentationStreamClosed(this, payload, payloadOffset, payloadCount); + } + catch (Exception ex) + { + await HandleUserSideError(ex); + } + } + } + + private async Task HandleTextFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) + { + if (frameHeader.IsFIN) + { + try + { + var text = Encoding.UTF8.GetString(payload, payloadOffset, payloadCount); + await _dispatcher.OnServerTextReceived(this, text); + } + catch (Exception ex) + { + await HandleUserSideError(ex); + } + } + else + { + try + { + await _dispatcher.OnServerFragmentationStreamOpened(this, payload, payloadOffset, payloadCount); + } + catch (Exception ex) + { + await HandleUserSideError(ex); + } + } + } + + private async Task HandleBinaryFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) + { + if (frameHeader.IsFIN) + { + try + { + await _dispatcher.OnServerBinaryReceived(this, payload, payloadOffset, payloadCount); + } + catch (Exception ex) + { + await HandleUserSideError(ex); + } + } + else + { + try + { + await _dispatcher.OnServerFragmentationStreamOpened(this, payload, payloadOffset, payloadCount); + } + catch (Exception ex) + { + await HandleUserSideError(ex); + } + } + } + + private async Task HandleCloseFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) + { + if (!frameHeader.IsFIN) + { + throw new WebSocketException(string.Format( + "Client received unfinished frame [{0}] from remote [{1}].", frameHeader.OpCode, RemoteEndPoint)); + } + + if (payloadCount > 1) + { + var statusCode = payload[payloadOffset + 0] * 256 + payload[payloadOffset + 1]; + var closeCode = (WebSocketCloseCode)statusCode; + var closeReason = string.Empty; + + if (payloadCount > 2) + { + closeReason = Encoding.UTF8.GetString(payload, payloadOffset + 2, payloadCount - 2); + } +#if DEBUG + NetworkHelper.Logger.Debug($"Receive server side close frame [{closeCode}] [{closeReason}]."); +#endif + // If an endpoint receives a Close frame and did not previously send a + // Close frame, the endpoint MUST send a Close frame in response. (When + // sending a Close frame in response, the endpoint typically echos the + // status code it received.) It SHOULD do so as soon as practical. + await Close(closeCode, closeReason); + } + else + { +#if DEBUG + NetworkHelper.Logger.Debug($"Receive server side close frame but no status code."); +#endif + await Close(WebSocketCloseCode.InvalidPayloadData); + } + } + + private async Task HandlePingFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) + { + if (!frameHeader.IsFIN) + { + throw new WebSocketException(string.Format( + "Client received unfinished frame [{0}] from remote [{1}].", frameHeader.OpCode, RemoteEndPoint)); + } + + // Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in + // response, unless it already received a Close frame. It SHOULD + // respond with Pong frame as soon as is practical. Pong frames are + // discussed in Section 5.5.3. + // + // An endpoint MAY send a Ping frame any time after the connection is + // established and before the connection is closed. + // + // A Ping frame may serve either as a keep-alive or as a means to + // verify that the remote endpoint is still responsive. + var ping = Encoding.UTF8.GetString(payload, payloadOffset, payloadCount); +#if DEBUG + NetworkHelper.Logger.Debug($"Receive server side ping frame [{ping}]."); +#endif + if (State == WebSocketState.Open) + { + // A Pong frame sent in response to a Ping frame must have identical + // "Application data" as found in the message body of the Ping frame being replied to. + var pong = new PongFrame(ping).ToArray(_frameBuilder); + await SendFrame(pong); +#if DEBUG + NetworkHelper.Logger.Debug($"Send client side pong frame [{ping}]."); +#endif + } + } + + private async Task HandlePongFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) + { + if (!frameHeader.IsFIN) + { + throw new WebSocketException(string.Format( + "Client received unfinished frame [{0}] from remote [{1}].", frameHeader.OpCode, RemoteEndPoint)); + } + + // If an endpoint receives a Ping frame and has not yet sent Pong + // frame(s) in response to previous Ping frame(s), the endpoint MAY + // elect to send a Pong frame for only the most recently processed Ping frame. + // + // A Pong frame MAY be sent unsolicited. This serves as a + // unidirectional heartbeat. A response to an unsolicited Pong frame is not expected. + var pong = Encoding.UTF8.GetString(payload, payloadOffset, payloadCount); + StopKeepAliveTimeoutTimer(); +#if DEBUG + NetworkHelper.Logger.Debug($"Receive server side pong frame [{pong}]."); +#endif + await Task.CompletedTask; + } + + #endregion + + #region Close + + public async Task Close(WebSocketCloseCode closeCode) + { + await Close(closeCode, null); + } + + public async Task Close(WebSocketCloseCode closeCode, string closeReason) + { + if (State == WebSocketState.Closed || State == WebSocketState.None) + { + return; + } + + var priorState = Interlocked.Exchange(ref _state, _closing); + switch (priorState) + { + case _connected: + { + var closingHandshake = new CloseFrame(closeCode, closeReason).ToArray(_frameBuilder); + try + { + StartClosingTimer(); +#if DEBUG + NetworkHelper.Logger.Debug($"Send client side close frame [{closeCode}] [{closeReason}]."); +#endif + var awaiter = _stream.WriteAsync(closingHandshake, 0, closingHandshake.Length); + if (!awaiter.Wait(ConnectTimeout)) + { + await InternalClose(true); + throw new TimeoutException(string.Format( + "Closing handshake with [{0}] timeout [{1}].", _remoteEndPoint, ConnectTimeout)); + } + } + catch (Exception ex) + { + await HandleSendOperationException(ex); + } + return; + } + case _connecting: + case _closing: + { + await InternalClose(true); + return; + } + case _closed: + case _none: + default: + return; + } + } + + private async Task InternalClose(bool shallNotifyUserSide) + { + if (Interlocked.Exchange(ref _state, _closed) == _closed) + { + return; + } + + Shutdown(); + + if (shallNotifyUserSide) + { + NetworkHelper.Logger.Debug(string.Format("Disconnected from server [{0}] with dispatcher [{1}] on [{2}].", + this.RemoteEndPoint, + _dispatcher.GetType().Name, + DateTime.UtcNow.ToString(@"yyyy-MM-dd HH:mm:ss.fffffff"))); + + try + { + await _dispatcher.OnServerDisconnected(this); + } + catch (Exception ex) + { + await HandleUserSideError(ex); + } + } + + Clean(); + } + + public void Shutdown() + { + // The correct way to shut down the connection (especially if you are in a full-duplex conversation) + // is to call socket.Shutdown(SocketShutdown.Send) and give the remote party some time to close + // their send channel. This ensures that you receive any pending data instead of slamming the + // connection shut. ObjectDisposedException should never be part of the normal application flow. + if (_tcpClient != null && _tcpClient.Connected) + { + _tcpClient.Client.Shutdown(SocketShutdown.Send); + } + } + + private void Clean() + { + try + { + try + { + if (_keepAliveTracker != null) + { + _keepAliveTracker.StopTimer(); + _keepAliveTracker.Dispose(); + } + } + catch { } + try + { + if (_keepAliveTimeoutTimer != null) + { + _keepAliveTimeoutTimer.Change(Timeout.Infinite, Timeout.Infinite); + _keepAliveTimeoutTimer.Dispose(); + } + } + catch { } + try + { + if (_closingTimeoutTimer != null) + { + _closingTimeoutTimer.Change(Timeout.Infinite, Timeout.Infinite); + _closingTimeoutTimer.Dispose(); + } + } + catch { } + try + { + if (_stream != null) + { + _stream.Dispose(); + } + } + catch { } + try + { + if (_tcpClient != null) + { + _tcpClient.Dispose(); + } + } + catch { } + } + catch { } + finally + { + _keepAliveTracker = null; + _keepAliveTimeoutTimer = null; + _closingTimeoutTimer = null; + _stream = null; + _tcpClient = null; + } + + if (_receiveBuffer != default(ArraySegment)) + { + _configuration.BufferManager.ReturnBuffer(_receiveBuffer); + } + + _receiveBuffer = default(ArraySegment); + _receiveBufferOffset = 0; + } + + public async Task Abort() + { + await InternalClose(true); + } + + private void StartClosingTimer() + { + // In abnormal cases (such as not having received a TCP Close + // from the server after a reasonable amount of time) a client MAY initiate the TCP Close. + _closingTimeoutTimer.Change((int)CloseTimeout.TotalMilliseconds, Timeout.Infinite); + } + + private async void OnCloseTimeout() + { + // After both sending and receiving a Close message, an endpoint + // considers the WebSocket connection closed and MUST close the + // underlying TCP connection. The server MUST close the underlying TCP + // connection immediately; the client SHOULD wait for the server to + // close the connection but MAY close the connection at any time after + // sending and receiving a Close message, e.g., if it has not received a + // TCP Close from the server in a reasonable time period. + NetworkHelper.Logger.Warn($"Closing timer timeout [{CloseTimeout}] then close automatically."); + await InternalClose(true); + } + + #endregion + + #region Exception Handler + + private async Task HandleSendOperationException(Exception ex) + { + if (IsSocketTimeOut(ex)) + { + await CloseIfShould(ex); + throw new WebSocketException(ex.Message, new TimeoutException(ex.Message, ex)); + } + + await CloseIfShould(ex); + throw new WebSocketException(ex.Message, ex); + } + + private async Task HandleReceiveOperationException(Exception ex) + { + if (IsSocketTimeOut(ex)) + { + await CloseIfShould(ex); + throw new WebSocketException(ex.Message, new TimeoutException(ex.Message, ex)); + } + + await CloseIfShould(ex); + throw new WebSocketException(ex.Message, ex); + } + + private bool IsSocketTimeOut(Exception ex) + { + return ex is IOException + && ex.InnerException != null + && ex.InnerException is SocketException + && (ex.InnerException as SocketException).SocketErrorCode == SocketError.TimedOut; + } + + private async Task CloseIfShould(Exception ex) + { + if (ex is ObjectDisposedException + || ex is InvalidOperationException + || ex is SocketException + || ex is IOException + || ex is NullReferenceException // buffer array operation + || ex is ArgumentException // buffer array operation + ) + { + NetworkHelper.Logger.Error(ex.FormatExceptionToMessage()); + + await InternalClose(false); // intend to close the session + + return true; + } + + return false; + } + + private async Task HandleUserSideError(Exception ex) + { + NetworkHelper.Logger.Error($"Client [{this}] error occurred in user side [{ex.Message}].{Environment.NewLine}{ex.FormatExceptionToMessage()}"); + await Task.CompletedTask; + } + + #endregion + + #region Send + + public async Task SendTextAsync(string text) + { + await SendFrame(new TextFrame(text).ToArray(_frameBuilder)); + } + + public async Task SendBinaryAsync(byte[] data) + { + await SendBinaryAsync(data, 0, data.Length); + } + + public async Task SendBinaryAsync(byte[] data, int offset, int count) + { + await SendFrame(new BinaryFrame(data, offset, count).ToArray(_frameBuilder)); + } + + public async Task SendBinaryAsync(ArraySegment segment) + { + await SendFrame(new BinaryFrame(segment).ToArray(_frameBuilder)); + } + + public async Task SendStreamAsync(Stream stream) + { + if (stream == null) + { + throw new ArgumentNullException("stream"); + } + + int fragmentLength = _configuration.ReasonableFragmentSize; + var buffer = new byte[fragmentLength]; + int readCount = 0; + + readCount = await stream.ReadAsync(buffer, 0, fragmentLength); + if (readCount == 0) + { + return; + } + + await SendFrame(new BinaryFragmentationFrame(OpCode.Binary, buffer, 0, readCount, isFin: false).ToArray(_frameBuilder)); + + while (true) + { + readCount = await stream.ReadAsync(buffer, 0, fragmentLength); + if (readCount != 0) + { + await SendFrame(new BinaryFragmentationFrame(OpCode.Continuation, buffer, 0, readCount, isFin: false).ToArray(_frameBuilder)); + } + else + { + await SendFrame(new BinaryFragmentationFrame(OpCode.Continuation, buffer, 0, 0, isFin: true).ToArray(_frameBuilder)); + break; + } + } + } + + private async Task SendFrame(byte[] frame) + { + if (frame == null) + { + throw new ArgumentNullException("frame"); + } + if (State != WebSocketState.Open) + { + throw new InvalidOperationException("This websocket client has not connected to server."); + } + + try + { + await _stream.WriteAsync(frame, 0, frame.Length); + _keepAliveTracker.OnDataSent(); + } + catch (Exception ex) + { + await HandleSendOperationException(ex); + } + } + + #endregion + + #region Keep Alive + + private void StartKeepAliveTimeoutTimer() + { + _keepAliveTimeoutTimer.Change((int)KeepAliveTimeout.TotalMilliseconds, Timeout.Infinite); + } + + private void StopKeepAliveTimeoutTimer() + { + _keepAliveTimeoutTimer.Change(Timeout.Infinite, Timeout.Infinite); + } + + private async void OnKeepAliveTimeout() + { + NetworkHelper.Logger.Warn($"Keep-alive timer timeout [{KeepAliveTimeout}]."); + await Close(WebSocketCloseCode.AbnormalClosure, "Keep-Alive Timeout"); + } + + private async void OnKeepAlive() + { + if (await _keepAliveLocker.WaitAsync(0)) + { + try + { + if (State != WebSocketState.Open) + { + return; + } + + if (_keepAliveTracker.ShouldSendKeepAlive()) + { + var keepAliveFrame = new PingFrame().ToArray(_frameBuilder); + await SendFrame(keepAliveFrame); + StartKeepAliveTimeoutTimer(); +#if DEBUG + NetworkHelper.Logger.Debug($"Send client side ping frame [{string.Empty}]."); +#endif + _keepAliveTracker.ResetTimer(); + } + } + catch (Exception ex) + { + NetworkHelper.Logger.Error(ex.FormatExceptionToMessage()); + await Close(WebSocketCloseCode.EndpointUnavailable); + } + finally + { + _keepAliveLocker.Release(); + } + } + } + + #endregion + + #region Extensions + + internal void AgreeExtensions(IEnumerable extensions) + { + if (extensions == null) + { + throw new ArgumentNullException("extensions"); + } + + // If a server gives an invalid response, such as accepting a PMCE that + // the client did not offer, the client MUST _Fail the WebSocket Connection_. + if (this.OfferedExtensions == null + || !this.OfferedExtensions.Any() + || this.EnabledExtensions == null + || !this.EnabledExtensions.Any()) + { + throw new WebSocketHandshakeException(string.Format( + "Negotiate extension with remote [{0}] failed due to no extension enabled.", this.RemoteEndPoint)); + } + + // Note that the order of extensions is significant. Any interactions + // between multiple extensions MAY be defined in the documents defining + // the extensions. In the absence of such definitions, the + // interpretation is that the header fields listed by the client in its + // request represent a preference of the header fields it wishes to use, + // with the first options listed being most preferable. The extensions + // listed by the server in response represent the extensions actually in + // use for the connection. Should the extensions modify the data and/or + // framing, the order of operations on the data should be assumed to be + // the same as the order in which the extensions are listed in the + // server's response in the opening handshake. + // For example, if there are two extensions "foo" and "bar" and if the + // header field |Sec-WebSocket-Extensions| sent by the server has the + // value "foo, bar", then operations on the data will be made as + // bar(foo(data)), be those changes to the data itself (such as + // compression) or changes to the framing that may "stack". + var agreedExtensions = new SortedList(); + var suggestedExtensions = string.Join(",", extensions).Split(',') + .Select(p => p.TrimStart().TrimEnd()).Where(p => !string.IsNullOrWhiteSpace(p)); + + int order = 0; + foreach (var extension in suggestedExtensions) + { + order++; + + var offeredExtensionName = extension.Split(';').First(); + + // Extensions not listed by the client MUST NOT be listed. + if (!this.EnabledExtensions.ContainsKey(offeredExtensionName)) + { + throw new WebSocketHandshakeException(string.Format( + "Negotiate extension with remote [{0}] failed due to un-enabled extensions [{1}].", + this.RemoteEndPoint, offeredExtensionName)); + } + + var extensionNegotiator = this.EnabledExtensions[offeredExtensionName]; + + string invalidParameter; + IWebSocketExtension negotiatedExtension; + if (!extensionNegotiator.NegotiateAsClient(extension, out invalidParameter, out negotiatedExtension) + || !string.IsNullOrEmpty(invalidParameter) + || negotiatedExtension == null) + { + throw new WebSocketHandshakeException(string.Format( + "Negotiate extension with remote [{0}] failed due to extension [{1}] has invalid parameter [{2}].", + this.RemoteEndPoint, extension, invalidParameter)); + } + + agreedExtensions.Add(order, negotiatedExtension); + } + + // If a server gives an invalid response, such as accepting a PMCE that + // the client did not offer, the client MUST _Fail the WebSocket Connection_. + foreach (var extension in agreedExtensions.Values) + { + if (!this.OfferedExtensions.Any(x => x.ExtensionNegotiationOffer.StartsWith(extension.Name))) + { + throw new WebSocketHandshakeException(string.Format( + "Negotiate extension with remote [{0}] failed due to extension [{1}] not be offered.", + this.RemoteEndPoint, extension.Name)); + } + } + + // A server MUST NOT accept a PMCE extension negotiation offer together + // with another extension if the PMCE will conflict with the extension + // on their use of the RSV1 bit. A client that received a response + // accepting a PMCE extension negotiation offer together with such an + // extension MUST _Fail the WebSocket Connection_. + bool isRsv1BitOccupied = false; + bool isRsv2BitOccupied = false; + bool isRsv3BitOccupied = false; + foreach (var extension in agreedExtensions.Values) + { + if ((isRsv1BitOccupied && extension.Rsv1BitOccupied) + || (isRsv2BitOccupied && extension.Rsv2BitOccupied) + || (isRsv3BitOccupied && extension.Rsv3BitOccupied)) + { + throw new WebSocketHandshakeException(string.Format( + "Negotiate extension with remote [{0}] failed due to conflict bit occupied.", this.RemoteEndPoint)); + } + + isRsv1BitOccupied = isRsv1BitOccupied | extension.Rsv1BitOccupied; + isRsv2BitOccupied = isRsv2BitOccupied | extension.Rsv2BitOccupied; + isRsv3BitOccupied = isRsv3BitOccupied | extension.Rsv3BitOccupied; + } + + _frameBuilder.NegotiatedExtensions = agreedExtensions; + } + + #endregion + + #region Sub-Protocols + + internal void UseSubProtocol(string protocol) + { + if (string.IsNullOrWhiteSpace(protocol)) + { + throw new ArgumentNullException("protocol"); + } + + if (this.RequestedSubProtocols == null + || !this.RequestedSubProtocols.Any() + || this.EnabledSubProtocols == null + || !this.EnabledSubProtocols.Any()) + { + throw new WebSocketHandshakeException(string.Format( + "Negotiate sub-protocol with remote [{0}] failed due to sub-protocol [{1}] is not enabled.", + this.RemoteEndPoint, protocol)); + } + + var requestedSubProtocols = string.Join(",", this.RequestedSubProtocols.Select(s => s.RequestedSubProtocol)) + .Split(',').Select(p => p.TrimStart().TrimEnd()).Where(p => !string.IsNullOrWhiteSpace(p)); + + if (!requestedSubProtocols.Contains(protocol)) + { + throw new WebSocketHandshakeException(string.Format( + "Negotiate sub-protocol with remote [{0}] failed due to sub-protocol [{1}] has not been requested.", + this.RemoteEndPoint, protocol)); + } + + // format : name.version.parameter + var segements = protocol.Split('.') + .Select(p => p.TrimStart().TrimEnd()).Where(p => !string.IsNullOrWhiteSpace(p)) + .ToArray(); + string protocolName = segements[0]; + string protocolVersion = segements.Length > 1 ? segements[1] : null; + string protocolParameter = segements.Length > 2 ? segements[2] : null; + + if (!this.EnabledSubProtocols.ContainsKey(protocolName)) + { + throw new WebSocketHandshakeException(string.Format( + "Negotiate sub-protocol with remote [{0}] failed due to sub-protocol [{1}] is not enabled.", + this.RemoteEndPoint, protocolName)); + } + + var subProtocolNegotiator = this.EnabledSubProtocols[protocolName]; + + string invalidParameter; + IWebSocketSubProtocol negotiatedSubProtocol; + if (!subProtocolNegotiator.NegotiateAsClient(protocolName, protocolVersion, protocolParameter, out invalidParameter, out negotiatedSubProtocol) + || !string.IsNullOrEmpty(invalidParameter) + || negotiatedSubProtocol == null) + { + throw new WebSocketHandshakeException(string.Format( + "Negotiate sub-protocol with remote [{0}] failed due to sub-protocol [{1}] has invalid parameter [{2}].", + this.RemoteEndPoint, protocol, invalidParameter)); + } + } + + #endregion + + #region IDisposable Members + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (disposing) + { + try + { + InternalClose(false).Wait(); // disposing + } + catch (Exception ex) + { + NetworkHelper.Logger.Error(ex.FormatExceptionToMessage()); + } + } + } + + #endregion + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Client/AsyncWebSocketClientConfiguration.cs b/EonaCat.Network/System/Sockets/WebSockets/Client/AsyncWebSocketClientConfiguration.cs new file mode 100644 index 0000000..6fcb09c --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Client/AsyncWebSocketClientConfiguration.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using EonaCat.WebSockets.Buffer; +using EonaCat.WebSockets.Extensions; +using EonaCat.WebSockets.SubProtocols; + +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class AsyncWebSocketClientConfiguration + { + public AsyncWebSocketClientConfiguration() + { + BufferManager = new SegmentBufferManager(100, 8192, 1, true); + ReceiveBufferSize = 8192; + SendBufferSize = 8192; + ReceiveTimeout = TimeSpan.Zero; + SendTimeout = TimeSpan.Zero; + NoDelay = true; + LingerState = new LingerOption(false, 0); // The socket will linger for x seconds after Socket.Close is called. + + SslTargetHost = null; + SslClientCertificates = new X509CertificateCollection(); + SslEncryptionPolicy = EncryptionPolicy.RequireEncryption; + SslEnabledProtocols = SslProtocols.Ssl3 | SslProtocols.Tls; + SslCheckCertificateRevocation = false; + SslPolicyErrorsBypassed = false; + + ConnectTimeout = TimeSpan.FromSeconds(10); + CloseTimeout = TimeSpan.FromSeconds(5); + KeepAliveInterval = TimeSpan.FromSeconds(30); + KeepAliveTimeout = TimeSpan.FromSeconds(5); + ReasonableFragmentSize = 4096; + + EnabledExtensions = new Dictionary() + { + { PerMessageCompressionExtension.RegisteredToken, new PerMessageCompressionExtensionNegotiator() }, + }; + EnabledSubProtocols = new Dictionary(); + + OfferedExtensions = new List() + { + new WebSocketExtensionOfferDescription(PerMessageCompressionExtension.RegisteredToken), + }; + RequestedSubProtocols = new List(); + } + + public ISegmentBufferManager BufferManager { get; set; } + public int ReceiveBufferSize { get; set; } + public int SendBufferSize { get; set; } + public TimeSpan ReceiveTimeout { get; set; } + public TimeSpan SendTimeout { get; set; } + public bool NoDelay { get; set; } + public LingerOption LingerState { get; set; } + + public string SslTargetHost { get; set; } + public X509CertificateCollection SslClientCertificates { get; set; } + public EncryptionPolicy SslEncryptionPolicy { get; set; } + public SslProtocols SslEnabledProtocols { get; set; } + public bool SslCheckCertificateRevocation { get; set; } + public bool SslPolicyErrorsBypassed { get; set; } + + public TimeSpan ConnectTimeout { get; set; } + public TimeSpan CloseTimeout { get; set; } + public TimeSpan KeepAliveInterval { get; set; } + public TimeSpan KeepAliveTimeout { get; set; } + public int ReasonableFragmentSize { get; set; } + + public Dictionary EnabledExtensions { get; set; } + public Dictionary EnabledSubProtocols { get; set; } + + public List OfferedExtensions { get; set; } + public List RequestedSubProtocols { get; set; } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Client/IAsyncWebSocketClientMessageDispatcher.cs b/EonaCat.Network/System/Sockets/WebSockets/Client/IAsyncWebSocketClientMessageDispatcher.cs new file mode 100644 index 0000000..32c380a --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Client/IAsyncWebSocketClientMessageDispatcher.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; + +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public interface IAsyncWebSocketClientMessageDispatcher + { + Task OnServerConnected(AsyncWebSocketClient client); + Task OnServerTextReceived(AsyncWebSocketClient client, string text); + Task OnServerBinaryReceived(AsyncWebSocketClient client, byte[] data, int offset, int count); + Task OnServerDisconnected(AsyncWebSocketClient client); + + Task OnServerFragmentationStreamOpened(AsyncWebSocketClient client, byte[] data, int offset, int count); + Task OnServerFragmentationStreamContinued(AsyncWebSocketClient client, byte[] data, int offset, int count); + Task OnServerFragmentationStreamClosed(AsyncWebSocketClient client, byte[] data, int offset, int count); + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Client/InternalAsyncWebSocketClientMessageDispatcherImplementation.cs b/EonaCat.Network/System/Sockets/WebSockets/Client/InternalAsyncWebSocketClientMessageDispatcherImplementation.cs new file mode 100644 index 0000000..9f57d80 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Client/InternalAsyncWebSocketClientMessageDispatcherImplementation.cs @@ -0,0 +1,113 @@ +using System; +using System.Threading.Tasks; + +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + internal class InternalAsyncWebSocketClientMessageDispatcherImplementation : IAsyncWebSocketClientMessageDispatcher + { + private Func _onServerTextReceived; + private Func _onServerBinaryReceived; + private Func _onServerConnected; + private Func _onServerDisconnected; + + private Func _onServerFragmentationStreamOpened; + private Func _onServerFragmentationStreamContinued; + private Func _onServerFragmentationStreamClosed; + + public InternalAsyncWebSocketClientMessageDispatcherImplementation() + { + } + + public InternalAsyncWebSocketClientMessageDispatcherImplementation( + Func onServerTextReceived, + Func onServerDataReceived, + Func onServerConnected, + Func onServerDisconnected) + : this() + { + _onServerTextReceived = onServerTextReceived; + _onServerBinaryReceived = onServerDataReceived; + _onServerConnected = onServerConnected; + _onServerDisconnected = onServerDisconnected; + } + + public InternalAsyncWebSocketClientMessageDispatcherImplementation( + Func onServerTextReceived, + Func onServerDataReceived, + Func onServerConnected, + Func onServerDisconnected, + Func onServerFragmentationStreamOpened, + Func onServerFragmentationStreamContinued, + Func onServerFragmentationStreamClosed) + : this() + { + _onServerTextReceived = onServerTextReceived; + _onServerBinaryReceived = onServerDataReceived; + _onServerConnected = onServerConnected; + _onServerDisconnected = onServerDisconnected; + + _onServerFragmentationStreamOpened = onServerFragmentationStreamOpened; + _onServerFragmentationStreamContinued = onServerFragmentationStreamContinued; + _onServerFragmentationStreamClosed = onServerFragmentationStreamClosed; + } + + public async Task OnServerConnected(AsyncWebSocketClient client) + { + if (_onServerConnected != null) + { + await _onServerConnected(client); + } + } + + public async Task OnServerTextReceived(AsyncWebSocketClient client, string text) + { + if (_onServerTextReceived != null) + { + await _onServerTextReceived(client, text); + } + } + + public async Task OnServerBinaryReceived(AsyncWebSocketClient client, byte[] data, int offset, int count) + { + if (_onServerBinaryReceived != null) + { + await _onServerBinaryReceived(client, data, offset, count); + } + } + + public async Task OnServerDisconnected(AsyncWebSocketClient client) + { + if (_onServerDisconnected != null) + { + await _onServerDisconnected(client); + } + } + + public async Task OnServerFragmentationStreamOpened(AsyncWebSocketClient client, byte[] data, int offset, int count) + { + if (_onServerFragmentationStreamOpened != null) + { + await _onServerFragmentationStreamOpened(client, data, offset, count); + } + } + + public async Task OnServerFragmentationStreamContinued(AsyncWebSocketClient client, byte[] data, int offset, int count) + { + if (_onServerFragmentationStreamContinued != null) + { + await _onServerFragmentationStreamContinued(client, data, offset, count); + } + } + + public async Task OnServerFragmentationStreamClosed(AsyncWebSocketClient client, byte[] data, int offset, int count) + { + if (_onServerFragmentationStreamClosed != null) + { + await _onServerFragmentationStreamClosed(client, data, offset, count); + } + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Client/WebSocketClientHandshaker.cs b/EonaCat.Network/System/Sockets/WebSockets/Client/WebSocketClientHandshaker.cs new file mode 100644 index 0000000..0a41d9b --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Client/WebSocketClientHandshaker.cs @@ -0,0 +1,414 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Security.Cryptography; +using System.Text; +using EonaCat.WebSockets.Buffer; +using EonaCat.Logger.Extensions; +using EonaCat.Logger.Managers; +using EonaCat.Network; + +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + internal sealed class WebSocketClientHandshaker + { + private static readonly char[] _headerLineSplitter = new char[] { '\r', '\n' }; + + internal static byte[] CreateOpenningHandshakeRequest(AsyncWebSocketClient client, out string secWebSocketKey) + { + var sb = new StringBuilder(); + + // The handshake MUST be a valid HTTP request as specified by [RFC2616]. + // The method of the request MUST be GET, and the HTTP version MUST be at least 1.1. + // For example, if the WebSocket URI is "ws://example.com/chat", + // the first line sent should be "GET /chat HTTP/1.1". + sb.AppendFormatWithCrCf("GET {0} HTTP/{1}", + !string.IsNullOrEmpty(client.Uri.PathAndQuery) ? client.Uri.PathAndQuery : "/", + Consts.HttpVersion); + + // The request MUST contain a |Host| header field whose value + // contains /host/ plus optionally ":" followed by /port/ (when not + // using the default port). + sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.Host, client.Uri.Host); + + // The request MUST contain an |Upgrade| header field whose value + // MUST include the "websocket" keyword. + sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.Upgrade, Consts.WebSocketUpgradeToken); + + // The request MUST contain a |Connection| header field whose value + // MUST include the "Upgrade" token. + sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.Connection, Consts.WebSocketConnectionToken); + + // The request MUST include a header field with the name + // |Sec-WebSocket-Key|. The value of this header field MUST be a + // nonce consisting of a randomly selected 16-byte value that has + // been base64-encoded (see Section 4 of [RFC4648]). The nonce + // MUST be selected randomly for each connection. + secWebSocketKey = Convert.ToBase64String(Encoding.ASCII.GetBytes(Guid.NewGuid().ToString().Substring(0, 16))); + sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.SecWebSocketKey, secWebSocketKey); + + // The request MUST include a header field with the name + // |Sec-WebSocket-Version|. The value of this header field MUST be 13. + sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.SecWebSocketVersion, Consts.WebSocketVersion); + + // The request MAY include a header field with the name + // |Sec-WebSocket-Extensions|. If present, this value indicates + // the protocol-level extension(s) the client wishes to speak. The + // interpretation and format of this header field is described in Section 9.1. + if (client.OfferedExtensions != null && client.OfferedExtensions.Any()) + { + foreach (var extension in client.OfferedExtensions) + { + sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.SecWebSocketExtensions, extension.ExtensionNegotiationOffer); + } + } + + // The request MAY include a header field with the name + // |Sec-WebSocket-Protocol|. If present, this value indicates one + // or more comma-separated subprotocol the client wishes to speak, + // ordered by preference. The elements that comprise this value + // MUST be non-empty strings with characters in the range U+0021 to + // U+007E not including separator characters as defined in + // [RFC2616] and MUST all be unique strings. The ABNF for the + // value of this header field is 1#token, where the definitions of + // constructs and rules are as given in [RFC2616]. + if (client.RequestedSubProtocols != null && client.RequestedSubProtocols.Any()) + { + foreach (var description in client.RequestedSubProtocols) + { + sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.SecWebSocketProtocol, description.RequestedSubProtocol); + } + } + + // The request MUST include a header field with the name |Origin| + // [RFC6454] if the request is coming from a browser client. If + // the connection is from a non-browser client, the request MAY + // include this header field if the semantics of that client match + // the use-case described here for browser clients. The value of + // this header field is the ASCII serialization of origin of the + // context in which the code establishing the connection is + // running. See [RFC6454] for the details of how this header field + // value is constructed. + + // The request MAY include any other header fields, for example, + // cookies [RFC6265] and/or authentication-related header fields + // such as the |Authorization| header field [RFC2616], which are + // processed according to documents that define them. + + sb.AppendWithCrCf(); + + // GET /chat HTTP/1.1 + // Host: server.example.com + // Upgrade: websocket + // Connection: Upgrade + // Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== + // Sec-WebSocket-Protocol: chat, superchat + // Sec-WebSocket-Version: 13 + // Origin: http://example.com + var request = sb.ToString(); +#if DEBUG + NetworkHelper.Logger.Debug($"{client.RemoteEndPoint}{Environment.NewLine}{request}"); +#endif + return Encoding.UTF8.GetBytes(request); + } + + internal static bool VerifyOpenningHandshakeResponse(AsyncWebSocketClient client, byte[] buffer, int offset, int count, string secWebSocketKey) + { + BufferValidator.ValidateBuffer(buffer, offset, count, "buffer"); + if (string.IsNullOrEmpty(secWebSocketKey)) + { + throw new ArgumentNullException("secWebSocketKey"); + } + + var response = Encoding.UTF8.GetString(buffer, offset, count); +#if DEBUG + NetworkHelper.Logger.Debug($"{client.RemoteEndPoint}{Environment.NewLine}{response}"); +#endif + try + { + // HTTP/1.1 101 Switching Protocols + // Upgrade: websocket + // Connection: Upgrade + // Sec-WebSocket-Accept: 1tGBmA9p0DQDgmFll6P0/UcVS/E= + // Sec-WebSocket-Protocol: chat + Dictionary headers; + List extensions; + List protocols; + ParseOpenningHandshakeResponseHeaders(response, out headers, out extensions, out protocols); + if (headers == null) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to invalid headers.", client.RemoteEndPoint)); + } + + // If the status code received from the server is not 101, the + // client handles the response per HTTP [RFC2616] procedures. In + // particular, the client might perform authentication if it + // receives a 401 status code; the server might redirect the client + // using a 3xx status code (but clients are not required to follow them), etc. + if (!headers.ContainsKey(Consts.HttpStatusCodeName)) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to lack of status code.", client.RemoteEndPoint)); + } + + if (!headers.ContainsKey(Consts.HttpStatusCodeDescription)) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to lack of status description.", client.RemoteEndPoint)); + } + + if (headers[Consts.HttpStatusCodeName] == ((int)HttpStatusCode.BadRequest).ToString()) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to bad request [{1}].", + client.RemoteEndPoint, headers[Consts.HttpStatusCodeName])); + } + + if (headers[Consts.HttpStatusCodeName] != ((int)HttpStatusCode.SwitchingProtocols).ToString()) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to expected 101 Switching Protocols but received [{1}].", + client.RemoteEndPoint, headers[Consts.HttpStatusCodeName])); + } + + // If the response lacks an |Upgrade| header field or the |Upgrade| + // header field contains a value that is not an ASCII case- + // insensitive match for the value "websocket", the client MUST + // _Fail the WebSocket Connection_. + if (!headers.ContainsKey(HttpKnownHeaderNames.Connection)) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to lack of connection header item.", client.RemoteEndPoint)); + } + + if (headers[HttpKnownHeaderNames.Connection].ToLowerInvariant() != Consts.WebSocketConnectionToken.ToLowerInvariant()) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to invalid connection header item value [{1}].", + client.RemoteEndPoint, headers[HttpKnownHeaderNames.Connection])); + } + + // If the response lacks a |Connection| header field or the + // |Connection| header field doesn't contain a token that is an + // ASCII case-insensitive match for the value "Upgrade", the client + // MUST _Fail the WebSocket Connection_. + if (!headers.ContainsKey(HttpKnownHeaderNames.Upgrade)) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to lack of upgrade header item.", client.RemoteEndPoint)); + } + + if (headers[HttpKnownHeaderNames.Upgrade].ToLowerInvariant() != Consts.WebSocketUpgradeToken.ToLowerInvariant()) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to invalid upgrade header item value [{1}].", + client.RemoteEndPoint, headers[HttpKnownHeaderNames.Upgrade])); + } + + // If the response lacks a |Sec-WebSocket-Accept| header field or + // the |Sec-WebSocket-Accept| contains a value other than the + // base64-encoded SHA-1 of the concatenation of the |Sec-WebSocket- + // Key| (as a string, not base64-decoded) with the string "258EAFA5- + // E914-47DA-95CA-C5AB0DC85B11" but ignoring any leading and + // trailing whitespace, the client MUST _Fail the WebSocket Connection_. + if (!headers.ContainsKey(HttpKnownHeaderNames.SecWebSocketAccept)) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to lack of Sec-WebSocket-Accept header item.", client.RemoteEndPoint)); + } + + string challenge = GetSecWebSocketAcceptString(secWebSocketKey); + if (!headers[HttpKnownHeaderNames.SecWebSocketAccept].Equals(challenge, StringComparison.OrdinalIgnoreCase)) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to invalid Sec-WebSocket-Accept header item value [{1}].", + client.RemoteEndPoint, headers[HttpKnownHeaderNames.SecWebSocketAccept])); + } + + // If the response includes a |Sec-WebSocket-Extensions| header + // field and this header field indicates the use of an extension + // that was not present in the client's handshake (the server has + // indicated an extension not requested by the client), the client + // MUST _Fail the WebSocket Connection_. + if (extensions != null) + { + foreach (var extension in extensions) + { + // The empty string is not the same as the null value for these + // purposes and is not a legal value for this field. + if (string.IsNullOrWhiteSpace(extension)) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to empty extension.", client.RemoteEndPoint)); + } + } + + client.AgreeExtensions(extensions); + } + + // If the response includes a |Sec-WebSocket-Protocol| header field + // and this header field indicates the use of a subprotocol that was + // not present in the client's handshake (the server has indicated a + // subprotocol not requested by the client), the client MUST _Fail + // the WebSocket Connection_. + if (protocols != null) + { + if (!protocols.Any()) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to empty sub-protocol.", client.RemoteEndPoint)); + } + + if (protocols.Count > 1) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to suggest to use multiple sub-protocols.", client.RemoteEndPoint)); + } + + foreach (var protocol in protocols) + { + // The empty string is not the same as the null value for these + // purposes and is not a legal value for this field. + if (string.IsNullOrWhiteSpace(protocol)) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to empty sub-protocol.", client.RemoteEndPoint)); + } + } + + var suggestedProtocols = protocols.First().Split(',') + .Select(p => p.TrimStart().TrimEnd()).Where(p => !string.IsNullOrWhiteSpace(p)); + + if (!suggestedProtocols.Any()) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to invalid sub-protocol.", client.RemoteEndPoint)); + } + + if (suggestedProtocols.Count() > 1) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to suggest to use multiple sub-protocols.", client.RemoteEndPoint)); + } + + // The value chosen MUST be derived + // from the client's handshake, specifically by selecting one of + // the values from the |Sec-WebSocket-Protocol| field that the + // server is willing to use for this connection (if any). + client.UseSubProtocol(suggestedProtocols.First()); + } + } + catch (Exception ex) + { + NetworkHelper.Logger.Error($"{client.RemoteEndPoint}{Environment.NewLine}{ex.FormatExceptionToMessage()}"); + throw; + } + + return true; + } + + private static void ParseOpenningHandshakeResponseHeaders(string response, + out Dictionary headers, + out List extensions, + out List protocols) + { + headers = new Dictionary(); + + // The |Sec-WebSocket-Extensions| header field MAY appear multiple times + // in an HTTP request (which is logically the same as a single + // |Sec-WebSocket-Extensions| header field that contains all values. + // However, the |Sec-WebSocket-Extensions| header field MUST NOT appear + // more than once in an HTTP response. + extensions = null; + // The |Sec-WebSocket-Protocol| header field MAY appear multiple times + // in an HTTP request (which is logically the same as a single + // |Sec-WebSocket-Protocol| header field that contains all values). + // However, the |Sec-WebSocket-Protocol| header field MUST NOT appear + // more than once in an HTTP response. + protocols = null; + + var lines = response.Split(_headerLineSplitter).Where(l => l.Length > 0); + foreach (var line in lines) + { + // HTTP/1.1 101 Switching Protocols + // HTTP/1.1 400 Bad Request + if (line.StartsWith(@"HTTP/")) + { + var segements = line.Split(' '); + if (segements.Length > 1) + { + headers.Add(Consts.HttpStatusCodeName, segements[1]); + + if (segements.Length > 2) + { + headers.Add(Consts.HttpStatusCodeDescription, segements[2]); + } + } + } + else + { + foreach (var key in HttpKnownHeaderNames.All) + { + if (line.StartsWith(key + ":")) + { + var index = line.IndexOf(':'); + if (index != -1) + { + var value = line.Substring(index + 1); + + if (key == HttpKnownHeaderNames.SecWebSocketExtensions) + { + if (extensions == null) + { + extensions = new List(); + } + + extensions.Add(value.Trim()); + } + else if (key == HttpKnownHeaderNames.SecWebSocketProtocol) + { + if (protocols == null) + { + protocols = new List(); + } + + protocols.Add(value.Trim()); + } + else + { + if (headers.ContainsKey(key)) + { + headers[key] = string.Join(",", headers[key], value.Trim()); + } + else + { + headers.Add(key, value.Trim()); + } + } + } + } + } + } + } + } + + private static string GetSecWebSocketAcceptString(string secWebSocketKey) + { + string retVal; + + using (SHA1 sha1 = SHA1.Create()) + { + string acceptString = string.Concat(secWebSocketKey, Consts.SecWebSocketKeyGuid); + byte[] toHash = Encoding.UTF8.GetBytes(acceptString); + retVal = Convert.ToBase64String(sha1.ComputeHash(toHash)); + } + + return retVal; + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Exceptions/WebSocketException.cs b/EonaCat.Network/System/Sockets/WebSockets/Exceptions/WebSocketException.cs new file mode 100644 index 0000000..b70e425 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Exceptions/WebSocketException.cs @@ -0,0 +1,21 @@ +using System; + +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + [Serializable] + public class WebSocketException : Exception + { + public WebSocketException(string message) + : base(message) + { + } + + public WebSocketException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Exceptions/WebSocketHandshakeException.cs b/EonaCat.Network/System/Sockets/WebSockets/Exceptions/WebSocketHandshakeException.cs new file mode 100644 index 0000000..05cd39c --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Exceptions/WebSocketHandshakeException.cs @@ -0,0 +1,21 @@ +using System; + +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + [Serializable] + public sealed class WebSocketHandshakeException : WebSocketException + { + public WebSocketHandshakeException(string message) + : base(message) + { + } + + public WebSocketHandshakeException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Extensions/IWebSocketExtension.cs b/EonaCat.Network/System/Sockets/WebSockets/Extensions/IWebSocketExtension.cs new file mode 100644 index 0000000..d62ca75 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Extensions/IWebSocketExtension.cs @@ -0,0 +1,21 @@ +namespace EonaCat.WebSockets.Extensions +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public interface IWebSocketExtension + { + string Name { get; } + + bool Rsv1BitOccupied { get; } + bool Rsv2BitOccupied { get; } + bool Rsv3BitOccupied { get; } + + string GetAgreedOffer(); + + byte[] BuildExtensionData(byte[] payload, int offset, int count); + + byte[] ProcessIncomingMessagePayload(byte[] payload, int offset, int count); + byte[] ProcessOutgoingMessagePayload(byte[] payload, int offset, int count); + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Extensions/IWebSocketExtensionNegotiator.cs b/EonaCat.Network/System/Sockets/WebSockets/Extensions/IWebSocketExtensionNegotiator.cs new file mode 100644 index 0000000..9a079ee --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Extensions/IWebSocketExtensionNegotiator.cs @@ -0,0 +1,11 @@ +namespace EonaCat.WebSockets.Extensions +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public interface IWebSocketExtensionNegotiator + { + bool NegotiateAsServer(string offer, out string invalidParameter, out IWebSocketExtension negotiatedExtension); + bool NegotiateAsClient(string offer, out string invalidParameter, out IWebSocketExtension negotiatedExtension); + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/AbsentableValueParameter.cs b/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/AbsentableValueParameter.cs new file mode 100644 index 0000000..f96d2c5 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/AbsentableValueParameter.cs @@ -0,0 +1,34 @@ +using System; + +namespace EonaCat.WebSockets.Extensions +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class AbsentableValueParameter : ExtensionParameter + { + public AbsentableValueParameter(string name, Func valueValidator, T defaultValue) + : base(name) + { + if (valueValidator == null) + { + throw new ArgumentNullException("valueValidator"); + } + + this.ValueValidator = valueValidator; + this.DefaultValue = defaultValue; + } + + public override ExtensionParameterType ParameterType + { + get + { + return ExtensionParameterType.Single | ExtensionParameterType.Valuable; + } + } + + public Func ValueValidator { get; private set; } + + public T DefaultValue { get; private set; } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/AgreedExtensionParameter.cs b/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/AgreedExtensionParameter.cs new file mode 100644 index 0000000..2f3effd --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/AgreedExtensionParameter.cs @@ -0,0 +1,28 @@ +using System; + +namespace EonaCat.WebSockets.Extensions +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public abstract class AgreedExtensionParameter + { + public AgreedExtensionParameter(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException("name"); + } + + this.Name = name; + } + + public string Name { get; private set; } + public abstract ExtensionParameterType ParameterType { get; } + + public override string ToString() + { + return string.Format("{0}", this.Name); + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/AgreedSingleParameter.cs b/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/AgreedSingleParameter.cs new file mode 100644 index 0000000..87a3b9e --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/AgreedSingleParameter.cs @@ -0,0 +1,21 @@ +namespace EonaCat.WebSockets.Extensions +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class AgreedSingleParameter : AgreedExtensionParameter + { + public AgreedSingleParameter(string name) + : base(name) + { + } + + public override ExtensionParameterType ParameterType + { + get + { + return ExtensionParameterType.Single; + } + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/AgreedValuableParameter.cs b/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/AgreedValuableParameter.cs new file mode 100644 index 0000000..313733b --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/AgreedValuableParameter.cs @@ -0,0 +1,29 @@ +namespace EonaCat.WebSockets.Extensions +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class AgreedValuableParameter : AgreedExtensionParameter + { + public AgreedValuableParameter(string name, T @value) + : base(name) + { + this.Value = @value; + } + + public override ExtensionParameterType ParameterType + { + get + { + return ExtensionParameterType.Valuable; + } + } + + public T Value { get; private set; } + + public override string ToString() + { + return string.Format("{0}={1}", this.Name, this.Value); + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/ExtensionParameter.cs b/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/ExtensionParameter.cs new file mode 100644 index 0000000..6b24153 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/ExtensionParameter.cs @@ -0,0 +1,28 @@ +using System; + +namespace EonaCat.WebSockets.Extensions +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public abstract class ExtensionParameter + { + public ExtensionParameter(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException("name"); + } + + this.Name = name; + } + + public string Name { get; private set; } + public abstract ExtensionParameterType ParameterType { get; } + + public override string ToString() + { + return string.Format("{0}", this.Name); + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/ExtensionParameterType.cs b/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/ExtensionParameterType.cs new file mode 100644 index 0000000..6c8d0ed --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/ExtensionParameterType.cs @@ -0,0 +1,14 @@ +using System; + +namespace EonaCat.WebSockets.Extensions +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + [Flags] + public enum ExtensionParameterType : byte + { + Single = 0x1, + Valuable = 0x2, + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/SingleParameter.cs b/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/SingleParameter.cs new file mode 100644 index 0000000..24087b3 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/SingleParameter.cs @@ -0,0 +1,21 @@ +namespace EonaCat.WebSockets.Extensions +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class SingleParameter : ExtensionParameter + { + public SingleParameter(string name) + : base(name) + { + } + + public override ExtensionParameterType ParameterType + { + get + { + return ExtensionParameterType.Single; + } + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/ValuableParameter.cs b/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/ValuableParameter.cs new file mode 100644 index 0000000..644fb5a --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/ValuableParameter.cs @@ -0,0 +1,31 @@ +using System; + +namespace EonaCat.WebSockets.Extensions +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class ValuableParameter : ExtensionParameter + { + public ValuableParameter(string name, Func valueValidator) + : base(name) + { + if (valueValidator == null) + { + throw new ArgumentNullException("valueValidator"); + } + + this.ValueValidator = valueValidator; + } + + public override ExtensionParameterType ParameterType + { + get + { + return ExtensionParameterType.Valuable; + } + } + + public Func ValueValidator { get; private set; } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Extensions/PerMessageExtensions/PMCE/DeflateCompression.cs b/EonaCat.Network/System/Sockets/WebSockets/Extensions/PerMessageExtensions/PMCE/DeflateCompression.cs new file mode 100644 index 0000000..fda5ba9 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Extensions/PerMessageExtensions/PMCE/DeflateCompression.cs @@ -0,0 +1,81 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.IO.Compression; +using EonaCat.WebSockets.Buffer; + +namespace EonaCat.WebSockets.Extensions +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class DeflateCompression + { + private readonly ISegmentBufferManager _bufferAllocator; + + public DeflateCompression(ISegmentBufferManager bufferAllocator) + { + if (bufferAllocator == null) + { + throw new ArgumentNullException("bufferAllocator"); + } + + _bufferAllocator = bufferAllocator; + } + + public byte[] Compress(byte[] raw) + { + return Compress(raw, 0, raw.Length); + } + + [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")] + public byte[] Compress(byte[] raw, int offset, int count) + { + using (var memory = new MemoryStream()) + { + using (var deflate = new DeflateStream(memory, CompressionMode.Compress, leaveOpen: true)) + { + deflate.Write(raw, offset, count); + } + + return memory.ToArray(); + } + } + + public byte[] Decompress(byte[] raw) + { + return Decompress(raw, 0, raw.Length); + } + + [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")] + public byte[] Decompress(byte[] raw, int offset, int count) + { + var buffer = _bufferAllocator.BorrowBuffer(); + + try + { + using (var input = new MemoryStream(raw, offset, count)) + using (var deflate = new DeflateStream(input, CompressionMode.Decompress, leaveOpen: true)) + using (var memory = new MemoryStream()) + { + int readCount = 0; + do + { + readCount = deflate.Read(buffer.Array, buffer.Offset, buffer.Count); + if (readCount > 0) + { + memory.Write(buffer.Array, buffer.Offset, readCount); + } + } + while (readCount > 0); + + return memory.ToArray(); + } + } + finally + { + _bufferAllocator.ReturnBuffer(buffer); + } + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Extensions/PerMessageExtensions/PMCE/PerMessageCompressionExtension.cs b/EonaCat.Network/System/Sockets/WebSockets/Extensions/PerMessageExtensions/PMCE/PerMessageCompressionExtension.cs new file mode 100644 index 0000000..201dbbe --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Extensions/PerMessageExtensions/PMCE/PerMessageCompressionExtension.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using EonaCat.WebSockets.Buffer; + +namespace EonaCat.WebSockets.Extensions +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class PerMessageCompressionExtension : IWebSocketExtension + { + // Any extension-token used MUST be a registered token (see + // Section 11.4). The parameters supplied with any given extension MUST + // be defined for that extension. Note that the client is only offering + // to use any advertised extensions and MUST NOT use them unless the + // server indicates that it wishes to use the extension. + public static readonly string RegisteredToken = @"permessage-deflate"; + + private readonly DeflateCompression _deflater; + private SortedList _agreedParameters; + + public PerMessageCompressionExtension() + { + var bufferAllocator = new SegmentBufferManager(100, 8192, 1, true); + _deflater = new DeflateCompression(bufferAllocator); + } + + public PerMessageCompressionExtension(SortedList agreedParameters) + : this() + { + _agreedParameters = agreedParameters; + } + + public string Name { get { return RegisteredToken; } } + + // PMCEs use the RSV1 bit of the WebSocket frame header to indicate whether a + // message is compressed or not so that an endpoint can choose not to + // compress messages with incompressible contents. + public bool Rsv1BitOccupied { get { return true; } } + public bool Rsv2BitOccupied { get { return false; } } + public bool Rsv3BitOccupied { get { return false; } } + + public string GetAgreedOffer() + { + var sb = new StringBuilder(); + + sb.Append(this.Name); + + if (_agreedParameters != null && _agreedParameters.Any()) + { + foreach (var parameter in _agreedParameters.Values) + { + sb.Append("; "); + sb.Append(parameter.ToString()); + } + } + + return sb.ToString(); + } + + public byte[] BuildExtensionData(byte[] payload, int offset, int count) + { + // Payload data: (x+y) bytes + // + // The "Payload data" is defined as "Extension data" concatenated + // with "Application data". + // + // Extension data: x bytes + // + // The "Extension data" is 0 bytes unless an extension has been + // negotiated. Any extension MUST specify the length of the + // "Extension data", or how that length may be calculated, and how + // the extension use MUST be negotiated during the opening handshake. + // If present, the "Extension data" is included in the total payload + // length. + // + // Application data: y bytes + // + // Arbitrary "Application data", taking up the remainder of the frame + // after any "Extension data". The length of the "Application data" + // is equal to the payload length minus the length of the "Extension + // data". + return null; // PMCE doesn't have an extension data definition. + } + + public byte[] ProcessIncomingMessagePayload(byte[] payload, int offset, int count) + { + return _deflater.Decompress(payload, offset, count); + } + + public byte[] ProcessOutgoingMessagePayload(byte[] payload, int offset, int count) + { + return _deflater.Compress(payload, offset, count); + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Extensions/PerMessageExtensions/PMCE/PerMessageCompressionExtensionNegotiator.cs b/EonaCat.Network/System/Sockets/WebSockets/Extensions/PerMessageExtensions/PMCE/PerMessageCompressionExtensionNegotiator.cs new file mode 100644 index 0000000..3721a30 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Extensions/PerMessageExtensions/PMCE/PerMessageCompressionExtensionNegotiator.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; + +namespace EonaCat.WebSockets.Extensions +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class PerMessageCompressionExtensionNegotiator : IWebSocketExtensionNegotiator + { + private static readonly char[] TrimableChars = new char[] { ' ', ';', '\r', '\n' }; + + public bool NegotiateAsServer(string offer, out string invalidParameter, out IWebSocketExtension negotiatedExtension) + { + return Negotiate(offer, AgreeAsServer, out invalidParameter, out negotiatedExtension); + } + + public bool NegotiateAsClient(string offer, out string invalidParameter, out IWebSocketExtension negotiatedExtension) + { + return Negotiate(offer, AgreeAsClient, out invalidParameter, out negotiatedExtension); + } + + private bool Negotiate(string offer, Func agree, out string invalidParameter, out IWebSocketExtension negotiatedExtension) + { + invalidParameter = null; + negotiatedExtension = null; + + if (string.IsNullOrWhiteSpace(offer)) + { + invalidParameter = offer; + return false; + } + + var segements = offer.Replace('\r', ' ').Replace('\n', ' ').TrimStart(TrimableChars).TrimEnd(TrimableChars).Split(';'); + + var offeredExtensionName = segements[0].TrimStart(TrimableChars).TrimEnd(TrimableChars); + if (string.IsNullOrEmpty(offeredExtensionName)) + { + invalidParameter = offer; + return false; + } + + if (string.Compare(offeredExtensionName, PerMessageCompressionExtension.RegisteredToken, StringComparison.OrdinalIgnoreCase) != 0) + { + invalidParameter = offeredExtensionName; + return false; + } + + if (segements.Length == 1) + { + negotiatedExtension = new PerMessageCompressionExtension(); + return true; + } + + // This set of elements MAY include multiple PMCEs with the same extension + // name to offer the possibility to use the same algorithm with + // different configuration parameters. + for (int i = 1; i < segements.Length; i++) + { + var offeredParameter = segements[i]; + if (!PerMessageCompressionExtensionParameters.ValidateParameter(offeredParameter)) + { + invalidParameter = offeredParameter; + return false; + } + } + + // The order of elements is important as it specifies the client's preference. + // An element preceding another element has higher preference. It is recommended + // that a server accepts PMCEs with higher preference if the server supports them. + var agreedSet = new SortedList(); + + for (int i = 1; i < segements.Length; i++) + { + var offeredParameter = segements[i]; + var agreeingParameter = PerMessageCompressionExtensionParameters.ResolveParameter(offeredParameter); + if (agree(agreeingParameter)) + { + agreedSet.Add(i, agreeingParameter); + } + } + + negotiatedExtension = new PerMessageCompressionExtension(agreedSet); + return true; + } + + private bool AgreeAsServer(AgreedExtensionParameter parameter) + { + if (parameter == null) + { + return false; + } + + switch (parameter.Name) + { + case PerMessageCompressionExtensionParameters.ServerNoContextTakeOverParameterName: + case PerMessageCompressionExtensionParameters.ClientNoContextTakeOverParameterName: + { + return false; + } + case PerMessageCompressionExtensionParameters.ServerMaxWindowBitsParameterName: + case PerMessageCompressionExtensionParameters.ClientMaxWindowBitsParameterName: + { + return false; + } + default: + throw new NotSupportedException("Invalid parameter name."); + } + } + + private bool AgreeAsClient(AgreedExtensionParameter parameter) + { + if (parameter == null) + { + return false; + } + + switch (parameter.Name) + { + case PerMessageCompressionExtensionParameters.ServerNoContextTakeOverParameterName: + case PerMessageCompressionExtensionParameters.ClientNoContextTakeOverParameterName: + { + return false; + } + case PerMessageCompressionExtensionParameters.ServerMaxWindowBitsParameterName: + case PerMessageCompressionExtensionParameters.ClientMaxWindowBitsParameterName: + { + return false; + } + default: + throw new NotSupportedException("Invalid parameter name."); + } + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Extensions/PerMessageExtensions/PMCE/PerMessageCompressionExtensionParameters.cs b/EonaCat.Network/System/Sockets/WebSockets/Extensions/PerMessageExtensions/PMCE/PerMessageCompressionExtensionParameters.cs new file mode 100644 index 0000000..2eb1da0 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Extensions/PerMessageExtensions/PMCE/PerMessageCompressionExtensionParameters.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace EonaCat.WebSockets.Extensions +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class PerMessageCompressionExtensionParameters + { + public const string ServerNoContextTakeOverParameterName = @"server_no_context_takeover"; + public const string ClientNoContextTakeOverParameterName = @"client_no_context_takeover"; + public const string ServerMaxWindowBitsParameterName = @"server_max_window_bits"; + public const string ClientMaxWindowBitsParameterName = @"client_max_window_bits"; + + public static readonly SingleParameter ServerNoContextTakeOver = new SingleParameter(ServerNoContextTakeOverParameterName); + public static readonly SingleParameter ClientNoContextTakeOver = new SingleParameter(ClientNoContextTakeOverParameterName); + public static readonly AbsentableValueParameter ServerMaxWindowBits = new AbsentableValueParameter(ServerMaxWindowBitsParameterName, ValidateServerMaxWindowBitsParameterValue, 15); + public static readonly AbsentableValueParameter ClientMaxWindowBits = new AbsentableValueParameter(ClientMaxWindowBitsParameterName, ValidateClientMaxWindowBitsParameterValue, 15); + + public static readonly IEnumerable AllAvailableParameters = new List() + { + ServerNoContextTakeOver, + ClientNoContextTakeOver, + ServerMaxWindowBits, + ClientMaxWindowBits, + }; + + public static readonly IEnumerable AllAvailableParameterNames = AllAvailableParameters.Select(p => p.Name); + + private static bool ValidateServerMaxWindowBitsParameterValue(string @value) + { + // A client MAY include the "server_max_window_bits" extension parameter + // in an extension negotiation offer. This parameter has a decimal + // integer value without leading zeroes between 8 to 15, inclusive, + // indicating the base-2 logarithm of the LZ77 sliding window size, and + // MUST conform to the ABNF below. + // server-max-window-bits = 1*DIGIT + + if (string.IsNullOrWhiteSpace(@value)) + { + return false; + } + + int paramValue = -1; + if (int.TryParse(@value, out paramValue)) + { + if (8 <= paramValue && paramValue <= 15) + { + return true; + } + } + + return false; + } + + private static bool ValidateClientMaxWindowBitsParameterValue(string @value) + { + // A client MAY include the "client_max_window_bits" extension parameter + // in an extension negotiation offer. This parameter has no value or a + // decimal integer value without leading zeroes between 8 to 15 + // inclusive indicating the base-2 logarithm of the LZ77 sliding window + // size. If a value is specified for this parameter, the value MUST + // conform to the ABNF below. + // client-max-window-bits = 1*DIGIT + + if (string.IsNullOrWhiteSpace(@value)) + { + return false; + } + + int paramValue = -1; + if (int.TryParse(@value, out paramValue)) + { + if (8 <= paramValue && paramValue <= 15) + { + return true; + } + } + + return false; + } + + public static bool ValidateParameter(string parameter) + { + if (string.IsNullOrWhiteSpace(parameter)) + { + return false; + } + + var keyValuePair = parameter.TrimStart().TrimEnd().Split('='); + var inputParameterName = keyValuePair[0].TrimStart().TrimEnd(); + ExtensionParameter matchedParameter = null; + + foreach (var @param in AllAvailableParameters) + { + if (string.Compare(inputParameterName, @param.Name, StringComparison.OrdinalIgnoreCase) == 0) + { + matchedParameter = @param; + break; + } + } + + if (matchedParameter == null) + { + return false; + } + + switch (matchedParameter.ParameterType) + { + case ExtensionParameterType.Single: + { + if (keyValuePair.Length == 1) + { + return true; + } + } + break; + case ExtensionParameterType.Valuable: + { + if (keyValuePair.Length != 2) + { + return false; + } + + var inputParameterValue = keyValuePair[1].TrimStart().TrimEnd(); + if (((ValuableParameter)matchedParameter).ValueValidator.Invoke(inputParameterValue)) + { + return true; + } + } + break; + case ExtensionParameterType.Single | ExtensionParameterType.Valuable: + { + if (keyValuePair.Length == 1) + { + return true; + } + + if (keyValuePair.Length > 2) + { + return false; + } + + var inputParameterValue = keyValuePair[1].TrimStart().TrimEnd(); + if (((AbsentableValueParameter)matchedParameter).ValueValidator.Invoke(inputParameterValue)) + { + return true; + } + } + break; + default: + throw new NotSupportedException("Invalid parameter type."); + } + + return false; + } + + public static AgreedExtensionParameter ResolveParameter(string parameter) + { + if (!ValidateParameter(parameter)) + { + return null; + } + + var keyValuePair = parameter.TrimStart().TrimEnd().Split('='); + var inputParameterName = keyValuePair[0].TrimStart().TrimEnd(); + ExtensionParameter matchedParameter = null; + + foreach (var @param in AllAvailableParameters) + { + if (string.Compare(inputParameterName, @param.Name, StringComparison.OrdinalIgnoreCase) == 0) + { + matchedParameter = @param; + break; + } + } + + switch (matchedParameter.Name) + { + case ServerNoContextTakeOverParameterName: + case ClientNoContextTakeOverParameterName: + { + return new AgreedSingleParameter(matchedParameter.Name); + } + case ServerMaxWindowBitsParameterName: + case ClientMaxWindowBitsParameterName: + { + if (keyValuePair.Length == 1) + { + return new AgreedValuableParameter(matchedParameter.Name, ((AbsentableValueParameter)matchedParameter).DefaultValue); + } + + var inputParameterValue = keyValuePair[1].TrimStart().TrimEnd(); + return new AgreedValuableParameter(matchedParameter.Name, byte.Parse(inputParameterValue)); + } + default: + throw new NotSupportedException("Invalid parameter type."); + } + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Extensions/WebSocketExtensionOfferDescription.cs b/EonaCat.Network/System/Sockets/WebSockets/Extensions/WebSocketExtensionOfferDescription.cs new file mode 100644 index 0000000..96f3f99 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Extensions/WebSocketExtensionOfferDescription.cs @@ -0,0 +1,22 @@ +using System; + +namespace EonaCat.WebSockets.Extensions +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class WebSocketExtensionOfferDescription + { + public WebSocketExtensionOfferDescription(string offer) + { + if (string.IsNullOrWhiteSpace(offer)) + { + throw new ArgumentNullException("offer"); + } + + this.ExtensionNegotiationOffer = offer; + } + + public string ExtensionNegotiationOffer { get; private set; } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Framing/BinaryFragmentationFrame.cs b/EonaCat.Network/System/Sockets/WebSockets/Framing/BinaryFragmentationFrame.cs new file mode 100644 index 0000000..cf3b473 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Framing/BinaryFragmentationFrame.cs @@ -0,0 +1,46 @@ +using System; +using EonaCat.WebSockets.Buffer; + +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class BinaryFragmentationFrame : Frame + { + private OpCode _opCode; + + public BinaryFragmentationFrame(OpCode opCode, byte[] data, int offset, int count, bool isFin = false, bool isMasked = true) + { + BufferValidator.ValidateBuffer(data, offset, count, "data"); + + _opCode = opCode; + this.Data = data; + this.Offset = offset; + this.Count = count; + this.IsFin = isFin; + this.IsMasked = isMasked; + } + + public byte[] Data { get; private set; } + public int Offset { get; private set; } + public int Count { get; private set; } + public bool IsFin { get; private set; } + public bool IsMasked { get; private set; } + + public override OpCode OpCode + { + get { return _opCode; } + } + + public byte[] ToArray(IFrameBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException("builder"); + } + + return builder.EncodeFrame(this); + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Framing/BinaryFrame.cs b/EonaCat.Network/System/Sockets/WebSockets/Framing/BinaryFrame.cs new file mode 100644 index 0000000..97badef --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Framing/BinaryFrame.cs @@ -0,0 +1,51 @@ +using System; +using EonaCat.WebSockets.Buffer; + +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class BinaryFrame : DataFrame + { + public BinaryFrame(ArraySegment segment, bool isMasked = true) + { + BufferValidator.ValidateArraySegment(segment, "segment"); + + this.Data = segment.Array; + this.Offset = segment.Offset; + this.Count = segment.Count; + this.IsMasked = isMasked; + } + + public BinaryFrame(byte[] data, int offset, int count, bool isMasked = true) + { + BufferValidator.ValidateBuffer(data, offset, count, "data"); + + this.Data = data; + this.Offset = offset; + this.Count = count; + this.IsMasked = isMasked; + } + + public byte[] Data { get; private set; } + public int Offset { get; private set; } + public int Count { get; private set; } + public bool IsMasked { get; private set; } + + public override OpCode OpCode + { + get { return OpCode.Binary; } + } + + public byte[] ToArray(IFrameBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException("builder"); + } + + return builder.EncodeFrame(this); + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Framing/Builder/IFrameBuilder.cs b/EonaCat.Network/System/Sockets/WebSockets/Framing/Builder/IFrameBuilder.cs new file mode 100644 index 0000000..b12e326 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Framing/Builder/IFrameBuilder.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using EonaCat.WebSockets.Extensions; + +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public interface IFrameBuilder + { + SortedList NegotiatedExtensions { get; set; } + + byte[] EncodeFrame(PingFrame frame); + byte[] EncodeFrame(PongFrame frame); + byte[] EncodeFrame(CloseFrame frame); + byte[] EncodeFrame(TextFrame frame); + byte[] EncodeFrame(BinaryFrame frame); + byte[] EncodeFrame(BinaryFragmentationFrame frame); + + bool TryDecodeFrameHeader(byte[] buffer, int offset, int count, out Header frameHeader); + void DecodePayload(byte[] buffer, int offset, Header frameHeader, out byte[] payload, out int payloadOffset, out int payloadCount); + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Framing/Builder/WebSocketFrameBuilder.cs b/EonaCat.Network/System/Sockets/WebSockets/Framing/Builder/WebSocketFrameBuilder.cs new file mode 100644 index 0000000..3e91277 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Framing/Builder/WebSocketFrameBuilder.cs @@ -0,0 +1,437 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using EonaCat.WebSockets.Extensions; + +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + // http://tools.ietf.org/html/rfc6455 + // This wire format for the data transfer part is described by the ABNF + // [RFC5234] given in detail in this section. (Note that, unlike in + // other sections of this document, the ABNF in this section is + // operating on groups of bits. The length of each group of bits is + // indicated in a comment. When encoded on the wire, the most + // significant bit is the leftmost in the ABNF). A high-level overview + // of the framing is given in the following figure. In a case of + // conflict between the figure below and the ABNF specified later in + // this section, the figure is authoritative. + // 0 1 2 3 + // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + // +-+-+-+-+-------+-+-------------+-------------------------------+ + // |F|R|R|R| opcode|M| Payload len | Extended payload length | + // |I|S|S|S| (4) |A| (7) | (16/64) | + // |N|V|V|V| |S| | (if payload len==126/127) | + // | |1|2|3| |K| | | + // +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + + // | Extended payload length continued, if payload len == 127 | + // + - - - - - - - - - - - - - - - +-------------------------------+ + // | |Masking-key, if MASK set to 1 | + // +-------------------------------+-------------------------------+ + // | Masking-key (continued) | Payload Data | + // +-------------------------------- - - - - - - - - - - - - - - - + + // : Payload Data continued ... : + // + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + // | Payload Data continued ... | + // +---------------------------------------------------------------+ + public class WebSocketFrameBuilder : IFrameBuilder + { + private static readonly byte[] EmptyArray = new byte[0]; + private static readonly Random _rng = new Random(DateTime.UtcNow.Millisecond); + private static readonly int MaskingKeyLength = 4; + + public WebSocketFrameBuilder() + { + } + + public SortedList NegotiatedExtensions { get; set; } + + public byte[] EncodeFrame(PingFrame frame) + { + if (!string.IsNullOrEmpty(frame.Data)) + { + var data = Encoding.UTF8.GetBytes(frame.Data); + if (data.Length > 125) + { + throw new WebSocketException("All control frames must have a payload length of 125 bytes or less."); + } + + return Encode(frame.OpCode, data, 0, data.Length, isMasked: frame.IsMasked); + } + else + { + return Encode(frame.OpCode, EmptyArray, 0, 0, isMasked: frame.IsMasked); + } + } + + public byte[] EncodeFrame(PongFrame frame) + { + if (!string.IsNullOrEmpty(frame.Data)) + { + var data = Encoding.UTF8.GetBytes(frame.Data); + if (data.Length > 125) + { + throw new WebSocketException("All control frames must have a payload length of 125 bytes or less."); + } + + return Encode(frame.OpCode, data, 0, data.Length, isMasked: frame.IsMasked); + } + else + { + return Encode(frame.OpCode, EmptyArray, 0, 0, isMasked: frame.IsMasked); + } + } + + public byte[] EncodeFrame(CloseFrame frame) + { + // The Close frame MAY contain a body (the "Application data" portion of + // the frame) that indicates a reason for closing, such as an endpoint + // shutting down, an endpoint having received a frame too large, or an + // endpoint having received a frame that does not conform to the format + // expected by the endpoint. If there is a body, the first two bytes of + // the body MUST be a 2-byte unsigned integer (in network byte order) + // representing a status code with value /code/ defined in Section 7.4. + // Following the 2-byte integer, the body MAY contain UTF-8-encoded data + // with value /reason/, the interpretation of which is not defined by + // this specification. This data is not necessarily human readable but + // may be useful for debugging or passing information relevant to the + // script that opened the connection. As the data is not guaranteed to + // be human readable, clients MUST NOT show it to end users. + int payloadLength = (string.IsNullOrEmpty(frame.CloseReason) ? 0 : Encoding.UTF8.GetMaxByteCount(frame.CloseReason.Length)) + 2; + if (payloadLength > 125) + { + throw new WebSocketException("All control frames must have a payload length of 125 bytes or less."); + } + + byte[] payload = new byte[payloadLength]; + + int higherByte = (int)frame.CloseCode / 256; + int lowerByte = (int)frame.CloseCode % 256; + + payload[0] = (byte)higherByte; + payload[1] = (byte)lowerByte; + + if (!string.IsNullOrEmpty(frame.CloseReason)) + { + int count = Encoding.UTF8.GetBytes(frame.CloseReason, 0, frame.CloseReason.Length, payload, 2); + return Encode(frame.OpCode, payload, 0, 2 + count, isMasked: frame.IsMasked); + } + else + { + return Encode(frame.OpCode, payload, 0, payload.Length, isMasked: frame.IsMasked); + } + } + + public byte[] EncodeFrame(TextFrame frame) + { + if (!string.IsNullOrEmpty(frame.Text)) + { + var data = Encoding.UTF8.GetBytes(frame.Text); + return Encode(frame.OpCode, data, 0, data.Length, isMasked: frame.IsMasked); + } + else + { + return Encode(frame.OpCode, EmptyArray, 0, 0, isMasked: frame.IsMasked); + } + } + + public byte[] EncodeFrame(BinaryFrame frame) + { + return Encode(frame.OpCode, frame.Data, frame.Offset, frame.Count, isMasked: frame.IsMasked); + } + + public byte[] EncodeFrame(BinaryFragmentationFrame frame) + { + return Encode(frame.OpCode, frame.Data, frame.Offset, frame.Count, isMasked: frame.IsMasked, isFin: frame.IsFin); + } + + private byte[] Encode(OpCode opCode, byte[] payload, int offset, int count, bool isMasked = true, bool isFin = true) + { + // Payload data: (x+y) bytes + // Extension data: x bytes + // Application data: y bytes + // The "Extension data" is 0 bytes unless an extension has been + // negotiated. Any extension MUST specify the length of the + // "Extension data", or how that length may be calculated, and how + // the extension use MUST be negotiated during the opening handshake. + // If present, the "Extension data" is included in the total payload length. + if (this.NegotiatedExtensions != null) + { + byte[] bakedBuffer = null; + foreach (var extension in this.NegotiatedExtensions.Values) + { + if (bakedBuffer == null) + { + bakedBuffer = extension.ProcessOutgoingMessagePayload(payload, offset, count); + } + else + { + bakedBuffer = extension.ProcessOutgoingMessagePayload(bakedBuffer, 0, bakedBuffer.Length); + } + } + + payload = bakedBuffer; + offset = 0; + count = payload.Length; + } + + byte[] fragment; + + // Payload length: 7 bits, 7+16 bits, or 7+64 bits. + // The length of the "Payload data", in bytes: if 0-125, that is the + // payload length. If 126, the following 2 bytes interpreted as a + // 16-bit unsigned integer are the payload length. If 127, the + // following 8 bytes interpreted as a 64-bit unsigned integer (the + // most significant bit MUST be 0) are the payload length. + if (count < 126) + { + fragment = new byte[2 + (isMasked ? MaskingKeyLength : 0) + count]; + fragment[1] = (byte)count; + } + else if (count < 65536) + { + fragment = new byte[2 + 2 + (isMasked ? MaskingKeyLength : 0) + count]; + fragment[1] = (byte)126; + fragment[2] = (byte)(count / 256); + fragment[3] = (byte)(count % 256); + } + else + { + fragment = new byte[2 + 8 + (isMasked ? MaskingKeyLength : 0) + count]; + fragment[1] = (byte)127; + + int left = count; + for (int i = 9; i > 1; i--) + { + fragment[i] = (byte)(left % 256); + left = left / 256; + + if (left == 0) + { + break; + } + } + } + + // FIN: 1 bit + // Indicates that this is the final fragment in a message. The first + // fragment MAY also be the final fragment. + if (isFin) + { + fragment[0] = 0x80; + } + + // RSV1, RSV2, RSV3: 1 bit each + // MUST be 0 unless an extension is negotiated that defines meanings + // for non-zero values. If a nonzero value is received and none of + // the negotiated extensions defines the meaning of such a nonzero + // value, the receiving endpoint MUST _Fail the WebSocket + // Connection_. + if (this.NegotiatedExtensions != null) + { + foreach (var extension in this.NegotiatedExtensions.Values) + { + if (extension.Rsv1BitOccupied) + { + fragment[0] = (byte)(fragment[0] | 0x40); + } + + if (extension.Rsv2BitOccupied) + { + fragment[0] = (byte)(fragment[0] | 0x20); + } + + if (extension.Rsv3BitOccupied) + { + fragment[0] = (byte)(fragment[0] | 0x10); + } + } + } + + // Opcode: 4 bits + // Defines the interpretation of the "Payload data". If an unknown + // opcode is received, the receiving endpoint MUST _Fail the + // WebSocket Connection_. The following values are defined. + fragment[0] = (byte)(fragment[0] | (byte)opCode); + + // Mask: 1 bit + // Defines whether the "Payload data" is masked. If set to 1, a + // masking key is present in masking-key, and this is used to unmask + // the "Payload data" as per Section 5.3. All frames sent from + // client to server have this bit set to 1. + if (isMasked) + { + fragment[1] = (byte)(fragment[1] | 0x80); + } + + // Masking-key: 0 or 4 bytes + // All frames sent from the client to the server are masked by a + // 32-bit value that is contained within the frame. + // The masking key is a 32-bit value chosen at random by the client. + // When preparing a masked frame, the client MUST pick a fresh masking + // key from the set of allowed 32-bit values. The masking key needs to + // be unpredictable; thus, the masking key MUST be derived from a strong + // source of entropy, and the masking key for a given frame MUST NOT + // make it simple for a server/proxy to predict the masking key for a + // subsequent frame. The unpredictability of the masking key is + // essential to prevent authors of malicious applications from selecting + // the bytes that appear on the wire. RFC 4086 [RFC4086] discusses what + // entails a suitable source of entropy for security-sensitive applications. + if (isMasked) + { + int maskingKeyIndex = fragment.Length - (MaskingKeyLength + count); + for (var i = maskingKeyIndex; i < maskingKeyIndex + MaskingKeyLength; i++) + { + fragment[i] = (byte)_rng.Next(0, 255); + } + + if (count > 0) + { + int payloadIndex = fragment.Length - count; + for (var i = 0; i < count; i++) + { + fragment[payloadIndex + i] = (byte)(payload[offset + i] ^ fragment[maskingKeyIndex + i % MaskingKeyLength]); + } + } + } + else + { + if (count > 0) + { + int payloadIndex = fragment.Length - count; + Array.Copy(payload, offset, fragment, payloadIndex, count); + } + } + + return fragment; + } + + public bool TryDecodeFrameHeader(byte[] buffer, int offset, int count, out Header frameHeader) + { + frameHeader = DecodeFrameHeader(buffer, offset, count); + return frameHeader != null; + } + + private Header DecodeFrameHeader(byte[] buffer, int offset, int count) + { + if (count < 2) + { + return null; + } + + // parse fixed header + var header = new Header() + { + IsFIN = ((buffer[offset + 0] & 0x80) == 0x80), + IsRSV1 = ((buffer[offset + 0] & 0x40) == 0x40), + IsRSV2 = ((buffer[offset + 0] & 0x20) == 0x20), + IsRSV3 = ((buffer[offset + 0] & 0x10) == 0x10), + OpCode = (OpCode)(buffer[offset + 0] & 0x0f), + IsMasked = ((buffer[offset + 1] & 0x80) == 0x80), + PayloadLength = (buffer[offset + 1] & 0x7f), + Length = 2, + }; + + // parse extended payload length + if (header.PayloadLength >= 126) + { + if (header.PayloadLength == 126) + { + header.Length += 2; + } + else + { + header.Length += 8; + } + + if (count < header.Length) + { + return null; + } + + if (header.PayloadLength == 126) + { + header.PayloadLength = buffer[offset + 2] * 256 + buffer[offset + 3]; + } + else + { + int totalLength = 0; + int level = 1; + + for (int i = 7; i >= 0; i--) + { + totalLength += buffer[offset + i + 2] * level; + level *= 256; + } + + header.PayloadLength = totalLength; + } + } + + // parse masking key + if (header.IsMasked) + { + if (count < header.Length + MaskingKeyLength) + { + return null; + } + + header.MaskingKeyOffset = header.Length; + header.Length += MaskingKeyLength; + } + + return header; + } + + public void DecodePayload(byte[] buffer, int offset, Header frameHeader, out byte[] payload, out int payloadOffset, out int payloadCount) + { + payload = buffer; + payloadOffset = offset + frameHeader.Length; + payloadCount = frameHeader.PayloadLength; + + if (frameHeader.IsMasked) + { + payload = new byte[payloadCount]; + + for (var i = 0; i < payloadCount; i++) + { + payload[i] = (byte)(buffer[payloadOffset + i] ^ buffer[offset + frameHeader.MaskingKeyOffset + i % MaskingKeyLength]); + } + + payloadOffset = 0; + payloadCount = payload.Length; + } + + // Payload data: (x+y) bytes + // Extension data: x bytes + // Application data: y bytes + // The "Extension data" is 0 bytes unless an extension has been + // negotiated. Any extension MUST specify the length of the + // "Extension data", or how that length may be calculated, and how + // the extension use MUST be negotiated during the opening handshake. + // If present, the "Extension data" is included in the total payload length. + if (this.NegotiatedExtensions != null) + { + byte[] bakedBuffer = null; + foreach (var extension in this.NegotiatedExtensions.Reverse().Select(e => e.Value)) + { + if (bakedBuffer == null) + { + bakedBuffer = extension.ProcessIncomingMessagePayload(payload, payloadOffset, payloadCount); + } + else + { + bakedBuffer = extension.ProcessIncomingMessagePayload(bakedBuffer, 0, bakedBuffer.Length); + } + } + + payload = bakedBuffer; + payloadOffset = 0; + payloadCount = payload.Length; + } + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Framing/CloseFrame.cs b/EonaCat.Network/System/Sockets/WebSockets/Framing/CloseFrame.cs new file mode 100644 index 0000000..8fa9172 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Framing/CloseFrame.cs @@ -0,0 +1,41 @@ +using System; + +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class CloseFrame : ControlFrame + { + public CloseFrame(bool isMasked = true) + { + this.IsMasked = isMasked; + } + + public CloseFrame(WebSocketCloseCode closeCode, string closeReason, bool isMasked = true) + : this(isMasked) + { + this.CloseCode = closeCode; + this.CloseReason = closeReason; + } + + public WebSocketCloseCode CloseCode { get; private set; } + public string CloseReason { get; private set; } + public bool IsMasked { get; private set; } + + public override OpCode OpCode + { + get { return OpCode.Close; } + } + + public byte[] ToArray(IFrameBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException("builder"); + } + + return builder.EncodeFrame(this); + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Framing/ControlFrame.cs b/EonaCat.Network/System/Sockets/WebSockets/Framing/ControlFrame.cs new file mode 100644 index 0000000..82a40e3 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Framing/ControlFrame.cs @@ -0,0 +1,9 @@ +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public abstract class ControlFrame : Frame + { + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Framing/DataFrame.cs b/EonaCat.Network/System/Sockets/WebSockets/Framing/DataFrame.cs new file mode 100644 index 0000000..9824b89 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Framing/DataFrame.cs @@ -0,0 +1,9 @@ +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public abstract class DataFrame : Frame + { + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Framing/Frame.cs b/EonaCat.Network/System/Sockets/WebSockets/Framing/Frame.cs new file mode 100644 index 0000000..ee10fc9 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Framing/Frame.cs @@ -0,0 +1,15 @@ +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public abstract class Frame + { + public abstract OpCode OpCode { get; } + + public override string ToString() + { + return string.Format("OpName[{0}], OpCode[{1}]", OpCode, (byte)OpCode); + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Framing/Header/Header.cs b/EonaCat.Network/System/Sockets/WebSockets/Framing/Header/Header.cs new file mode 100644 index 0000000..58eb08b --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Framing/Header/Header.cs @@ -0,0 +1,32 @@ +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class FixedHeader + { + public bool IsFIN { get; set; } + public bool IsRSV1 { get; set; } + public bool IsRSV2 { get; set; } + public bool IsRSV3 { get; set; } + public OpCode OpCode { get; set; } + public bool IsMasked { get; set; } + + public override string ToString() + { + return $"IsFIN[{IsFIN}], IsRSV1[{IsRSV1}], IsRSV2[{IsRSV2}], IsRSV3[{IsRSV3}], OpCode[{OpCode}], IsMasked[{IsMasked}]"; + } + } + + public class Header : FixedHeader + { + public int PayloadLength { get; set; } + public int MaskingKeyOffset { get; set; } + public int Length { get; set; } + + public override string ToString() + { + return $"IsFIN[{IsFIN}], IsRSV1[{IsRSV1}], IsRSV2[{IsRSV2}], IsRSV3[{IsRSV3}], OpCode[{OpCode}], IsMasked[{IsMasked}], PayloadLength[{PayloadLength}], MaskingKeyOffset[{MaskingKeyOffset}], Length[{Length}]"; + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Framing/OpCode.cs b/EonaCat.Network/System/Sockets/WebSockets/Framing/OpCode.cs new file mode 100644 index 0000000..dacdc48 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Framing/OpCode.cs @@ -0,0 +1,27 @@ +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + // https://www.iana.org/assignments/websocket/websocket.xhtml + // The opcode denotes the frame type of the WebSocket frame. + // The opcode is an integer number between 0 and 15, inclusive. + // Opcode Meaning Reference + // 0 Continuation Frame [RFC6455] + // 1 Text Frame [RFC6455] + // 2 Binary Frame [RFC6455] + // 3-7 Unassigned + // 8 Connection Close Frame [RFC6455] + // 9 Ping Frame [RFC6455] + // 10 Pong Frame [RFC6455] + // 11-15 Unassigned + public enum OpCode : byte + { + Continuation = 0, + Text = 1, + Binary = 2, + Close = 8, + Ping = 9, + Pong = 10, + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Framing/PingFrame.cs b/EonaCat.Network/System/Sockets/WebSockets/Framing/PingFrame.cs new file mode 100644 index 0000000..47e7a04 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Framing/PingFrame.cs @@ -0,0 +1,39 @@ +using System; + +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class PingFrame : ControlFrame + { + public PingFrame(bool isMasked = true) + { + this.IsMasked = isMasked; + } + + public PingFrame(string data, bool isMasked = true) + : this(isMasked) + { + this.Data = data; + } + + public string Data { get; private set; } + public bool IsMasked { get; private set; } + + public override OpCode OpCode + { + get { return OpCode.Ping; } + } + + public byte[] ToArray(IFrameBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException("builder"); + } + + return builder.EncodeFrame(this); + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Framing/PongFrame.cs b/EonaCat.Network/System/Sockets/WebSockets/Framing/PongFrame.cs new file mode 100644 index 0000000..60c071b --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Framing/PongFrame.cs @@ -0,0 +1,39 @@ +using System; + +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class PongFrame : ControlFrame + { + public PongFrame(bool isMasked = true) + { + this.IsMasked = isMasked; + } + + public PongFrame(string data, bool isMasked = true) + : this(isMasked) + { + this.Data = data; + } + + public string Data { get; private set; } + public bool IsMasked { get; private set; } + + public override OpCode OpCode + { + get { return OpCode.Pong; } + } + + public byte[] ToArray(IFrameBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException("builder"); + } + + return builder.EncodeFrame(this); + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Framing/TextFrame.cs b/EonaCat.Network/System/Sockets/WebSockets/Framing/TextFrame.cs new file mode 100644 index 0000000..c7e07b7 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Framing/TextFrame.cs @@ -0,0 +1,39 @@ +using System; + +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class TextFrame : DataFrame + { + public TextFrame(string text, bool isMasked = true) + { + if (string.IsNullOrEmpty(text)) + { + throw new ArgumentNullException("text"); + } + + this.Text = text; + this.IsMasked = isMasked; + } + + public string Text { get; private set; } + public bool IsMasked { get; private set; } + + public override OpCode OpCode + { + get { return OpCode.Text; } + } + + public byte[] ToArray(IFrameBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException("builder"); + } + + return builder.EncodeFrame(this); + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Helpers/Consts.cs b/EonaCat.Network/System/Sockets/WebSockets/Helpers/Consts.cs new file mode 100644 index 0000000..7927def --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Helpers/Consts.cs @@ -0,0 +1,45 @@ +using System.Text; + +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + internal sealed class Consts + { + internal static readonly byte[] HttpMessageTerminator = Encoding.UTF8.GetBytes("\r\n\r\n"); + + internal static readonly string[] WebSocketSchemes = new string[] { "ws", "wss" }; + + internal const string HttpHeaderLineFormat = "{0}: {1}"; + + internal const string HttpStatusCodeName = "HttpStatusCode"; + internal const string HttpStatusCodeDescription = "HttpStatusCodeDescription"; + internal const string HttpGetMethodName = "GET"; + internal const string HttpVersionName = "HTTP"; + internal const string HttpVersion = "1.1"; + + internal const string SecWebSocketKeyGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + + internal const string WebSocketUpgradeToken = "websocket"; + internal const string WebSocketConnectionToken = "Upgrade"; + + // https://www.iana.org/assignments/websocket/websocket.xhtml#version-number + // Version Number Reference Status + // 0 [draft-ietf-hybi-thewebsocketprotocol-00] Interim + // 1 [draft-ietf-hybi-thewebsocketprotocol-01] Interim + // 2 [draft-ietf-hybi-thewebsocketprotocol-02] Interim + // 3 [draft-ietf-hybi-thewebsocketprotocol-03] Interim + // 4 [draft-ietf-hybi-thewebsocketprotocol-04] Interim + // 5 [draft-ietf-hybi-thewebsocketprotocol-05] Interim + // 6 [draft-ietf-hybi-thewebsocketprotocol-06] Interim + // 7 [draft-ietf-hybi-thewebsocketprotocol-07] Interim + // 8 [draft-ietf-hybi-thewebsocketprotocol-08] Interim + // 9 [Reserved] + // 10 [Reserved] + // 11 [Reserved] + // 12 [Reserved] + // 13 [RFC6455] Standard + internal const string WebSocketVersion = "13"; + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Helpers/HttpKnownHeaderNames.cs b/EonaCat.Network/System/Sockets/WebSockets/Helpers/HttpKnownHeaderNames.cs new file mode 100644 index 0000000..c354841 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Helpers/HttpKnownHeaderNames.cs @@ -0,0 +1,144 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + internal static class HttpKnownHeaderNames + { + public const string CacheControl = "Cache-Control"; + public const string Connection = "Connection"; + public const string Date = "Date"; + public const string KeepAlive = "Keep-Alive"; + public const string Pragma = "Pragma"; + public const string ProxyConnection = "Proxy-Connection"; + public const string Trailer = "Trailer"; + public const string TransferEncoding = "Transfer-Encoding"; + public const string Upgrade = "Upgrade"; + public const string Via = "Via"; + public const string Warning = "Warning"; + public const string ContentLength = "Content-Length"; + public const string ContentType = "Content-Type"; + public const string ContentDisposition = "Content-Disposition"; + public const string ContentEncoding = "Content-Encoding"; + public const string ContentLanguage = "Content-Language"; + public const string ContentLocation = "Content-Location"; + public const string ContentRange = "Content-Range"; + public const string Expires = "Expires"; + public const string LastModified = "Last-Modified"; + public const string Age = "Age"; + public const string Location = "Location"; + public const string ProxyAuthenticate = "Proxy-Authenticate"; + public const string RetryAfter = "Retry-After"; + public const string Server = "Server"; + public const string SetCookie = "Set-Cookie"; + public const string SetCookie2 = "Set-Cookie2"; + public const string Vary = "Vary"; + public const string WWWAuthenticate = "WWW-Authenticate"; + public const string Accept = "Accept"; + public const string AcceptCharset = "Accept-Charset"; + public const string AcceptEncoding = "Accept-Encoding"; + public const string AcceptLanguage = "Accept-Language"; + public const string Authorization = "Authorization"; + public const string Cookie = "Cookie"; + public const string Cookie2 = "Cookie2"; + public const string Expect = "Expect"; + public const string From = "From"; + public const string Host = "Host"; + public const string IfMatch = "If-Match"; + public const string IfModifiedSince = "If-Modified-Since"; + public const string IfNoneMatch = "If-None-Match"; + public const string IfRange = "If-Range"; + public const string IfUnmodifiedSince = "If-Unmodified-Since"; + public const string MaxForwards = "Max-Forwards"; + public const string ProxyAuthorization = "Proxy-Authorization"; + public const string Referer = "Referer"; + public const string Range = "Range"; + public const string UserAgent = "User-Agent"; + public const string ContentMD5 = "Content-MD5"; + public const string ETag = "ETag"; + public const string TE = "TE"; + public const string Allow = "Allow"; + public const string AcceptRanges = "Accept-Ranges"; + public const string P3P = "P3P"; + public const string XPoweredBy = "X-Powered-By"; + public const string XAspNetVersion = "X-AspNet-Version"; + public const string SecWebSocketKey = "Sec-WebSocket-Key"; + public const string SecWebSocketExtensions = "Sec-WebSocket-Extensions"; + public const string SecWebSocketAccept = "Sec-WebSocket-Accept"; + public const string Origin = "Origin"; + public const string SecWebSocketProtocol = "Sec-WebSocket-Protocol"; + public const string SecWebSocketVersion = "Sec-WebSocket-Version"; + + public static readonly IReadOnlyCollection All = + new ReadOnlyCollection( + new List() + { + CacheControl , + Connection , + Date, + KeepAlive , + Pragma, + ProxyConnection , + Trailer , + TransferEncoding, + Upgrade , + Via , + Warning , + ContentLength , + ContentType , + ContentDisposition, + ContentEncoding , + ContentLanguage , + ContentLocation, + ContentRange , + Expires , + LastModified , + Age , + Location , + ProxyAuthenticate , + RetryAfter , + Server , + SetCookie , + SetCookie2, + Vary, + WWWAuthenticate, + Accept, + AcceptCharset, + AcceptEncoding, + AcceptLanguage, + Authorization, + Cookie, + Cookie2, + Expect, + From, + Host, + IfMatch, + IfModifiedSince, + IfNoneMatch, + IfRange, + IfUnmodifiedSince, + MaxForwards, + ProxyAuthorization, + Referer, + Range, + UserAgent, + ContentMD5, + ETag, + TE, + Allow, + AcceptRanges, + P3P, + XPoweredBy, + XAspNetVersion, + SecWebSocketKey, + SecWebSocketExtensions, + SecWebSocketAccept, + Origin, + SecWebSocketProtocol, + SecWebSocketVersion, + }); + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Helpers/KeepAliveTracker.cs b/EonaCat.Network/System/Sockets/WebSockets/Helpers/KeepAliveTracker.cs new file mode 100644 index 0000000..2b6799c --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Helpers/KeepAliveTracker.cs @@ -0,0 +1,172 @@ +using System; +using System.Diagnostics; +using System.Threading; + +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + internal abstract class KeepAliveTracker : IDisposable + { + public abstract void OnDataReceived(); + public abstract void OnDataSent(); + public abstract void StartTimer(); + public abstract void StopTimer(); + public abstract void ResetTimer(); + public abstract bool ShouldSendKeepAlive(); + public abstract void Dispose(); + + public static KeepAliveTracker Create(TimeSpan keepAliveInterval, TimerCallback keepAliveCallback) + { + if ((int)keepAliveInterval.TotalMilliseconds > 0) + { + return new DefaultKeepAliveTracker(keepAliveInterval, keepAliveCallback); + } + + return new DisabledKeepAliveTracker(); + } + + private class DisabledKeepAliveTracker : KeepAliveTracker + { + public override void OnDataReceived() + { + } + + public override void OnDataSent() + { + } + + public override void StartTimer() + { + } + + public override void StopTimer() + { + } + + public override void ResetTimer() + { + } + + public override bool ShouldSendKeepAlive() + { + return false; + } + + public override void Dispose() + { + } + } + + private class DefaultKeepAliveTracker : KeepAliveTracker + { + private readonly TimerCallback _keepAliveTimerElapsedCallback; + private readonly TimeSpan _keepAliveInterval; + private readonly Stopwatch _lastSendActivity; + private readonly Stopwatch _lastReceiveActivity; + private Timer _keepAliveTimer; + + public DefaultKeepAliveTracker(TimeSpan keepAliveInterval, TimerCallback keepAliveCallback) + { + _keepAliveInterval = keepAliveInterval; + _keepAliveTimerElapsedCallback = keepAliveCallback; + _lastSendActivity = new Stopwatch(); + _lastReceiveActivity = new Stopwatch(); + } + + public override void OnDataReceived() + { + _lastReceiveActivity.Restart(); + } + + public override void OnDataSent() + { + _lastSendActivity.Restart(); + } + + public override void StartTimer() + { + int keepAliveIntervalMilliseconds = (int)_keepAliveInterval.TotalMilliseconds; + + if (ExecutionContext.IsFlowSuppressed()) + { + _keepAliveTimer = new Timer(_keepAliveTimerElapsedCallback, null, Timeout.Infinite, Timeout.Infinite); + _keepAliveTimer.Change(keepAliveIntervalMilliseconds, Timeout.Infinite); + } + else + { + using (ExecutionContext.SuppressFlow()) + { + _keepAliveTimer = new Timer(_keepAliveTimerElapsedCallback, null, Timeout.Infinite, Timeout.Infinite); + _keepAliveTimer.Change(keepAliveIntervalMilliseconds, Timeout.Infinite); + } + } + } + + public override void StopTimer() + { + if (_keepAliveTimer != null) + { + _keepAliveTimer.Change(Timeout.Infinite, Timeout.Infinite); + } + } + + public override void ResetTimer() + { + ResetTimer((int)_keepAliveInterval.TotalMilliseconds); + } + + public override bool ShouldSendKeepAlive() + { + TimeSpan idleTime = GetIdleTime(); + if (idleTime >= _keepAliveInterval) + { + return true; + } + + ResetTimer((int)(_keepAliveInterval - idleTime).TotalMilliseconds); + return false; + } + + public override void Dispose() + { + if (_keepAliveTimer != null) + { + _keepAliveTimer.Dispose(); + } + } + + private void ResetTimer(int dueInMilliseconds) + { + if (_keepAliveTimer != null) + { + _keepAliveTimer.Change(dueInMilliseconds, Timeout.Infinite); + } + } + + private TimeSpan GetIdleTime() + { + TimeSpan sinceLastSendActivity = GetTimeElapsed(_lastSendActivity); + TimeSpan sinceLastReceiveActivity = GetTimeElapsed(_lastReceiveActivity); + + if (sinceLastReceiveActivity < sinceLastSendActivity) + { + return sinceLastReceiveActivity; + } + + return sinceLastSendActivity; + } + + private TimeSpan GetTimeElapsed(Stopwatch watch) + { + if (watch.IsRunning) + { + return watch.Elapsed; + } + + return _keepAliveInterval; + } + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Helpers/StringBuilderExtensions.cs b/EonaCat.Network/System/Sockets/WebSockets/Helpers/StringBuilderExtensions.cs new file mode 100644 index 0000000..35d9085 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Helpers/StringBuilderExtensions.cs @@ -0,0 +1,35 @@ +using System.Text; + +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + internal static class StringBuilderExtensions + { + private static readonly char[] _crcf = new char[] { '\r', '\n' }; + + public static void AppendFormatWithCrCf(this StringBuilder builder, string format, object arg) + { + builder.AppendFormat(format, arg); + builder.Append(_crcf); + } + + public static void AppendFormatWithCrCf(this StringBuilder builder, string format, params object[] args) + { + builder.AppendFormat(format, args); + builder.Append(_crcf); + } + + public static void AppendWithCrCf(this StringBuilder builder, string text) + { + builder.Append(text); + builder.Append(_crcf); + } + + public static void AppendWithCrCf(this StringBuilder builder) + { + builder.Append(_crcf); + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Helpers/TplExtensions.cs b/EonaCat.Network/System/Sockets/WebSockets/Helpers/TplExtensions.cs new file mode 100644 index 0000000..91c5327 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Helpers/TplExtensions.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; + +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + internal static class TplExtensions + { + public static void Forget(this Task task) + { + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Helpers/WebSocketHelpers.cs b/EonaCat.Network/System/Sockets/WebSockets/Helpers/WebSocketHelpers.cs new file mode 100644 index 0000000..200e4e9 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Helpers/WebSocketHelpers.cs @@ -0,0 +1,77 @@ +using System; +using System.Globalization; + +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + internal sealed class WebSocketHelpers + { + internal static bool FindHttpMessageTerminator(byte[] buffer, int offset, int count, out int index) + { + index = -1; + + for (int i = 0; i < count; i++) + { + if (i + Consts.HttpMessageTerminator.Length <= count) + { + bool matched = true; + for (int j = 0; j < Consts.HttpMessageTerminator.Length; j++) + { + if (buffer[offset + i + j] != Consts.HttpMessageTerminator[j]) + { + matched = false; + break; + } + } + + if (matched) + { + index = i; + return true; + } + } + else + { + break; + } + } + + return false; + } + + internal static bool ValidateSubprotocol(string subProtocol) + { + if (string.IsNullOrWhiteSpace(subProtocol)) + { + throw new ArgumentNullException("subProtocol"); + } + + string separators = "()<>@,;:\\\"/[]?={} "; + + char[] chars = subProtocol.ToCharArray(); + string invalidChar = null; + int i = 0; + while (i < chars.Length) + { + char ch = chars[i]; + if (ch < 0x21 || ch > 0x7e) + { + invalidChar = string.Format(CultureInfo.InvariantCulture, "[{0}]", (int)ch); + break; + } + + if (!char.IsLetterOrDigit(ch) && separators.IndexOf(ch) >= 0) + { + invalidChar = ch.ToString(); + break; + } + + i++; + } + + return invalidChar == null; + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Server/AsyncWebSocketServer.cs b/EonaCat.Network/System/Sockets/WebSockets/Server/AsyncWebSocketServer.cs new file mode 100644 index 0000000..eddbd84 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Server/AsyncWebSocketServer.cs @@ -0,0 +1,346 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using EonaCat.WebSockets.Buffer; +using EonaCat.Logger.Extensions; +using EonaCat.Network; + +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class AsyncWebSocketServer + { + #region Fields + + private TcpListener _listener; + private readonly ConcurrentDictionary _sessions = new ConcurrentDictionary(); + private readonly AsyncWebSocketServerModuleCatalog _catalog; + private readonly AsyncWebSocketServerConfiguration _configuration; + private AsyncWebSocketRouteResolver _routeResolver; + + private int _state; + private const int _none = 0; + private const int _listening = 1; + private const int _disposed = 5; + + #endregion + + #region Constructors + + public AsyncWebSocketServer(int listenedPort, AsyncWebSocketServerModuleCatalog catalog, AsyncWebSocketServerConfiguration configuration = null) + : this(IPAddress.Any, listenedPort, catalog, configuration) + { + } + + public AsyncWebSocketServer(IPAddress listenedAddress, int listenedPort, AsyncWebSocketServerModuleCatalog catalog, AsyncWebSocketServerConfiguration configuration = null) + : this(new IPEndPoint(listenedAddress, listenedPort), catalog, configuration) + { + } + + public AsyncWebSocketServer(IPEndPoint listenedEndPoint, AsyncWebSocketServerModuleCatalog catalog, AsyncWebSocketServerConfiguration configuration = null) + { + if (listenedEndPoint == null) + { + throw new ArgumentNullException("listenedEndPoint"); + } + + if (catalog == null) + { + throw new ArgumentNullException("catalog"); + } + + this.ListenedEndPoint = listenedEndPoint; + _catalog = catalog; + _configuration = configuration ?? new AsyncWebSocketServerConfiguration(); + + if (_configuration.BufferManager == null) + { + throw new InvalidProgramException("The buffer manager in configuration cannot be null."); + } + + Initialize(); + } + + private void Initialize() + { + _routeResolver = new AsyncWebSocketRouteResolver(_catalog); + } + + #endregion + + #region Properties + + public IPEndPoint ListenedEndPoint { get; private set; } + public bool IsListening { get { return _state == _listening; } } + public int SessionCount { get { return _sessions.Count; } } + + public IEnumerable EnabledExtensions + { + get { return _configuration.EnabledExtensions != null ? _configuration.EnabledExtensions.Keys : null; } + } + public IEnumerable EnabledSubProtocols + { + get { return _configuration.EnabledSubProtocols != null ? _configuration.EnabledSubProtocols.Keys : null; } + } + + #endregion + + #region Server + + public void Listen() + { + int origin = Interlocked.CompareExchange(ref _state, _listening, _none); + if (origin == _disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + else if (origin != _none) + { + throw new InvalidOperationException("This websocket server has already started."); + } + + try + { + _listener = new TcpListener(this.ListenedEndPoint); + ConfigureListener(); + + _listener.Start(_configuration.PendingConnectionBacklog); + + Task.Factory.StartNew(async () => + { + await Accept(); + }, + TaskCreationOptions.LongRunning) + .Forget(); + } + catch (Exception ex) when (!ShouldThrow(ex)) { } + } + + public void Shutdown() + { + if (Interlocked.Exchange(ref _state, _disposed) == _disposed) + { + return; + } + + try + { + _listener.Stop(); + _listener = null; + + Task.Factory.StartNew(async () => + { + try + { + foreach (var session in _sessions.Values) + { + await session.Close(WebSocketCloseCode.NormalClosure); + } + } + catch (Exception ex) when (!ShouldThrow(ex)) { } + }, + TaskCreationOptions.PreferFairness) + .Wait(); + } + catch (Exception ex) when (!ShouldThrow(ex)) { } + } + + private void ConfigureListener() + { + _listener.AllowNatTraversal(_configuration.AllowNatTraversal); + } + + public bool Pending() + { + if (!IsListening) + { + throw new InvalidOperationException("The websocket server is not active."); + } + + // determine if there are pending connection requests. + return _listener.Pending(); + } + + private async Task Accept() + { + try + { + while (IsListening) + { + var tcpClient = await _listener.AcceptTcpClientAsync(); + Task.Factory.StartNew(async () => + { + await Process(tcpClient); + }, + TaskCreationOptions.PreferFairness) + .Forget(); + } + } + catch (Exception ex) when (!ShouldThrow(ex)) { } + catch (Exception ex) + { + NetworkHelper.Logger.Error(ex.FormatExceptionToMessage()); + } + } + + private async Task Process(TcpClient acceptedTcpClient) + { + var session = new AsyncWebSocketSession(acceptedTcpClient, _configuration, _configuration.BufferManager, _routeResolver, this); + + if (_sessions.TryAdd(session.SessionKey, session)) + { + NetworkHelper.Logger.Debug($"New session [{session}]."); + try + { + await session.Start(); + } + catch (Exception ex) + when (ex is TimeoutException || ex is WebSocketException) + { + NetworkHelper.Logger.Error(ex.FormatExceptionToMessage()); + } + finally + { + AsyncWebSocketSession throwAway; + if (_sessions.TryRemove(session.SessionKey, out throwAway)) + { + NetworkHelper.Logger.Debug($"Close session [{throwAway}]."); + } + } + } + } + + private bool ShouldThrow(Exception ex) + { + if (ex is ObjectDisposedException + || ex is InvalidOperationException + || ex is SocketException + || ex is IOException) + { + return false; + } + return true; + } + + #endregion + + #region Send + + public async Task SendTextToAsync(string sessionKey, string text) + { + AsyncWebSocketSession sessionFound; + if (_sessions.TryGetValue(sessionKey, out sessionFound)) + { + await sessionFound.SendTextAsync(text); + } + else + { + NetworkHelper.Logger.Warn(string.Format("Cannot find session [{0}].", sessionKey)); + } + } + + public async Task SendTextToAsync(AsyncWebSocketSession session, string text) + { + AsyncWebSocketSession sessionFound; + if (_sessions.TryGetValue(session.SessionKey, out sessionFound)) + { + await sessionFound.SendTextAsync(text); + } + else + { + NetworkHelper.Logger.Warn($"Send text data but cannot find session [{session}]."); + } + } + + public async Task SendBinaryToAsync(string sessionKey, byte[] data) + { + await SendBinaryToAsync(sessionKey, data, 0, data.Length); + } + + public async Task SendBinaryToAsync(string sessionKey, byte[] data, int offset, int count) + { + AsyncWebSocketSession sessionFound; + if (_sessions.TryGetValue(sessionKey, out sessionFound)) + { + await sessionFound.SendBinaryAsync(data, offset, count); + } + else + { + NetworkHelper.Logger.Warn($"Cannot find session [{sessionKey}]."); + } + } + + public async Task SendBinaryToAsync(AsyncWebSocketSession session, byte[] data) + { + await SendBinaryToAsync(session, data, 0, data.Length); + } + + public async Task SendBinaryToAsync(AsyncWebSocketSession session, byte[] data, int offset, int count) + { + AsyncWebSocketSession sessionFound; + if (_sessions.TryGetValue(session.SessionKey, out sessionFound)) + { + await sessionFound.SendBinaryAsync(data, offset, count); + } + else + { + NetworkHelper.Logger.Warn($"Send binary data but cannot find session [{session}]."); + } + } + + public async Task BroadcastTextAsync(string text) + { + foreach (var session in _sessions.Values) + { + await session.SendTextAsync(text); + } + } + + public async Task BroadcastBinaryAsync(byte[] data) + { + await BroadcastBinaryAsync(data, 0, data.Length); + } + + public async Task BroadcastBinaryAsync(byte[] data, int offset, int count) + { + foreach (var session in _sessions.Values) + { + await session.SendBinaryAsync(data, offset, count); + } + } + + #endregion + + #region Session + + public bool HasSession(string sessionKey) + { + return _sessions.ContainsKey(sessionKey); + } + + public AsyncWebSocketSession GetSession(string sessionKey) + { + AsyncWebSocketSession session = null; + _sessions.TryGetValue(sessionKey, out session); + return session; + } + + public async Task CloseSession(string sessionKey) + { + AsyncWebSocketSession session = null; + if (_sessions.TryGetValue(sessionKey, out session)) + { + await session.Close(WebSocketCloseCode.NormalClosure); + } + } + + #endregion + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Server/AsyncWebSocketServerConfiguration.cs b/EonaCat.Network/System/Sockets/WebSockets/Server/AsyncWebSocketServerConfiguration.cs new file mode 100644 index 0000000..19c1f15 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Server/AsyncWebSocketServerConfiguration.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using EonaCat.WebSockets.Buffer; +using EonaCat.WebSockets.Extensions; +using EonaCat.WebSockets.SubProtocols; + +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class AsyncWebSocketServerConfiguration + { + public AsyncWebSocketServerConfiguration() + { + BufferManager = new SegmentBufferManager(1024, 8192, 1, true); + ReceiveBufferSize = 8192; + SendBufferSize = 8192; + ReceiveTimeout = TimeSpan.Zero; + SendTimeout = TimeSpan.Zero; + NoDelay = true; + LingerState = new LingerOption(false, 0); // The socket will linger for x seconds after Socket.Close is called. + + PendingConnectionBacklog = 200; + AllowNatTraversal = true; + + SslEnabled = false; + SslServerCertificate = null; + SslEncryptionPolicy = EncryptionPolicy.RequireEncryption; + SslEnabledProtocols = SslProtocols.Ssl3 | SslProtocols.Tls; + SslClientCertificateRequired = true; + SslCheckCertificateRevocation = false; + SslPolicyErrorsBypassed = false; + + ConnectTimeout = TimeSpan.FromSeconds(10); + CloseTimeout = TimeSpan.FromSeconds(5); + KeepAliveInterval = TimeSpan.FromSeconds(60); + KeepAliveTimeout = TimeSpan.FromSeconds(15); + ReasonableFragmentSize = 4096; + + EnabledExtensions = new Dictionary() + { + { PerMessageCompressionExtension.RegisteredToken, new PerMessageCompressionExtensionNegotiator() }, + }; + EnabledSubProtocols = new Dictionary(); + } + + public ISegmentBufferManager BufferManager { get; set; } + public int ReceiveBufferSize { get; set; } + public int SendBufferSize { get; set; } + public TimeSpan ReceiveTimeout { get; set; } + public TimeSpan SendTimeout { get; set; } + public bool NoDelay { get; set; } + public LingerOption LingerState { get; set; } + + public int PendingConnectionBacklog { get; set; } + public bool AllowNatTraversal { get; set; } + + public bool SslEnabled { get; set; } + public X509Certificate2 SslServerCertificate { get; set; } + public EncryptionPolicy SslEncryptionPolicy { get; set; } + public SslProtocols SslEnabledProtocols { get; set; } + public bool SslClientCertificateRequired { get; set; } + public bool SslCheckCertificateRevocation { get; set; } + public bool SslPolicyErrorsBypassed { get; set; } + + public TimeSpan ConnectTimeout { get; set; } + public TimeSpan CloseTimeout { get; set; } + public TimeSpan KeepAliveInterval { get; set; } + public TimeSpan KeepAliveTimeout { get; set; } + public int ReasonableFragmentSize { get; set; } + + public Dictionary EnabledExtensions { get; set; } + public Dictionary EnabledSubProtocols { get; set; } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Server/AsyncWebSocketSession.cs b/EonaCat.Network/System/Sockets/WebSockets/Server/AsyncWebSocketSession.cs new file mode 100644 index 0000000..477672f --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Server/AsyncWebSocketSession.cs @@ -0,0 +1,1218 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using EonaCat.WebSockets.Buffer; +using EonaCat.Logger.Extensions; +using EonaCat.Network; +using EonaCat.WebSockets.Extensions; +using EonaCat.WebSockets.SubProtocols; + +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class AsyncWebSocketSession : IDisposable + { + #region Fields + + private TcpClient _tcpClient; + private readonly AsyncWebSocketServerConfiguration _configuration; + private readonly ISegmentBufferManager _bufferManager; + private readonly AsyncWebSocketRouteResolver _routeResolver; + private AsyncWebSocketServerModule _module; + private readonly AsyncWebSocketServer _server; + private readonly IFrameBuilder _frameBuilder = new WebSocketFrameBuilder(); + private readonly string _sessionKey; + private Stream _stream; + private ArraySegment _receiveBuffer = default(ArraySegment); + private int _receiveBufferOffset = 0; + private IPEndPoint _remoteEndPoint; + private IPEndPoint _localEndPoint; + + private int _state; + private const int _none = 0; + private const int _connecting = 1; + private const int _connected = 2; + private const int _closing = 3; + private const int _disposed = 5; + + private readonly SemaphoreSlim _keepAliveLocker = new SemaphoreSlim(1, 1); + private KeepAliveTracker _keepAliveTracker; + private Timer _keepAliveTimeoutTimer; + private Timer _closingTimeoutTimer; + + #endregion + + #region Constructors + + public AsyncWebSocketSession( + TcpClient tcpClient, + AsyncWebSocketServerConfiguration configuration, + ISegmentBufferManager bufferManager, + AsyncWebSocketRouteResolver routeResolver, + AsyncWebSocketServer server) + { + if (tcpClient == null) + { + throw new ArgumentNullException("tcpClient"); + } + + if (configuration == null) + { + throw new ArgumentNullException("configuration"); + } + + if (bufferManager == null) + { + throw new ArgumentNullException("bufferManager"); + } + + if (routeResolver == null) + { + throw new ArgumentNullException("routeResolver"); + } + + if (server == null) + { + throw new ArgumentNullException("server"); + } + + _tcpClient = tcpClient; + _configuration = configuration; + _bufferManager = bufferManager; + _routeResolver = routeResolver; + _server = server; + + _sessionKey = Guid.NewGuid().ToString(); + this.StartTime = DateTime.UtcNow; + + _remoteEndPoint = (_tcpClient != null && _tcpClient.Client.Connected) ? + (IPEndPoint)_tcpClient.Client.RemoteEndPoint : null; + _localEndPoint = (_tcpClient != null && _tcpClient.Client.Connected) ? + (IPEndPoint)_tcpClient.Client.LocalEndPoint : null; + } + + #endregion + + #region Properties + + public string SessionKey { get { return _sessionKey; } } + public DateTime StartTime { get; private set; } + + private bool Connected { get { return _tcpClient != null && _tcpClient.Client.Connected; } } + public IPEndPoint RemoteEndPoint { get { return Connected ? (IPEndPoint)_tcpClient.Client.RemoteEndPoint : _remoteEndPoint; } } + public IPEndPoint LocalEndPoint { get { return Connected ? (IPEndPoint)_tcpClient.Client.LocalEndPoint : _localEndPoint; } } + + public AsyncWebSocketServer Server { get { return _server; } } + + public TimeSpan ConnectTimeout { get { return _configuration.ConnectTimeout; } } + public TimeSpan CloseTimeout { get { return _configuration.CloseTimeout; } } + public TimeSpan KeepAliveInterval { get { return _configuration.KeepAliveInterval; } } + public TimeSpan KeepAliveTimeout { get { return _configuration.KeepAliveTimeout; } } + + public IDictionary EnabledExtensions { get { return _configuration.EnabledExtensions; } } + public IDictionary EnabledSubProtocols { get { return _configuration.EnabledSubProtocols; } } + public SortedList NegotiatedExtensions { get { return _frameBuilder.NegotiatedExtensions; } } + public IWebSocketSubProtocol NegotiatedSubProtocol { get; private set; } + + public WebSocketState State + { + get + { + switch (_state) + { + case _none: + return WebSocketState.None; + case _connecting: + return WebSocketState.Connecting; + case _connected: + return WebSocketState.Open; + case _closing: + return WebSocketState.Closing; + case _disposed: + return WebSocketState.Closed; + default: + return WebSocketState.Closed; + } + } + } + + public override string ToString() + { + return string.Format("SessionKey[{0}], RemoteEndPoint[{1}], LocalEndPoint[{2}]", + this.SessionKey, this.RemoteEndPoint, this.LocalEndPoint); + } + + #endregion + + #region Start + + internal async Task Start() + { + int origin = Interlocked.CompareExchange(ref _state, _connecting, _none); + if (origin == _disposed) + { + throw new ObjectDisposedException("This websocket session has been disposed when connecting."); + } + else if (origin != _none) + { + throw new InvalidOperationException("This websocket session is in invalid state when connecting."); + } + + try + { + ResetKeepAlive(); + ConfigureClient(); + + var negotiator = NegotiateStream(_tcpClient.GetStream()); + if (!negotiator.Wait(ConnectTimeout)) + { + await Close(WebSocketCloseCode.TlsHandshakeFailed, "SSL/TLS handshake timeout."); + throw new TimeoutException(string.Format( + "Negotiate SSL/TSL with remote [{0}] timeout [{1}].", this.RemoteEndPoint, ConnectTimeout)); + } + _stream = negotiator.Result; + + _receiveBuffer = _bufferManager.BorrowBuffer(); + _receiveBufferOffset = 0; + + var handshaker = OpenHandshake(); + if (!handshaker.Wait(ConnectTimeout)) + { + throw new TimeoutException(string.Format( + "Handshake with remote [{0}] timeout [{1}].", this.RemoteEndPoint, ConnectTimeout)); + } + if (!handshaker.Result) + { + var responseBuffer = WebSocketServerHandshaker.CreateOpenningHandshakeBadRequestResponse(this); + await _stream.WriteAsync(responseBuffer, 0, responseBuffer.Length); + + throw new WebSocketException(string.Format( + "Handshake with remote [{0}] failed.", this.RemoteEndPoint)); + } + + if (Interlocked.CompareExchange(ref _state, _connected, _connecting) != _connecting) + { + await InternalClose(false); // connected with wrong state + throw new ObjectDisposedException("This websocket session has been disposed after connected."); + } + + NetworkHelper.Logger.Debug($"Session started for [{this.RemoteEndPoint}] on [{this.StartTime.ToString(@"yyyy-MM-dd HH:mm:ss.fffffff")}] in module [{_module.GetType().Name}] with session count [{this.Server.SessionCount}]."); + + bool isErrorOccurredInUserSide = false; + try + { + await _module.OnSessionStarted(this); + } + catch (Exception ex) + { + isErrorOccurredInUserSide = true; + await HandleUserSideError(ex); + } + + if (!isErrorOccurredInUserSide) + { + _keepAliveTracker.StartTimer(); + await Process(); + } + else + { + await InternalClose(true); // user side handle tcp connection error occurred + } + } + catch (Exception ex) when (ex is TimeoutException || ex is WebSocketException) + { + NetworkHelper.Logger.Error($"Session [{this}] exception occurred, [{ex.Message}].{Environment.NewLine}{ex.FormatExceptionToMessage()}"); + await InternalClose(true); // handle tcp connection error occurred + throw; + } + } + + private void ConfigureClient() + { + _tcpClient.ReceiveBufferSize = _configuration.ReceiveBufferSize; + _tcpClient.SendBufferSize = _configuration.SendBufferSize; + _tcpClient.ReceiveTimeout = (int)_configuration.ReceiveTimeout.TotalMilliseconds; + _tcpClient.SendTimeout = (int)_configuration.SendTimeout.TotalMilliseconds; + _tcpClient.NoDelay = _configuration.NoDelay; + _tcpClient.LingerState = _configuration.LingerState; + } + + private async Task NegotiateStream(Stream stream) + { + if (!_configuration.SslEnabled) + { + return stream; + } + + var validateRemoteCertificate = new RemoteCertificateValidationCallback( + (object sender, + X509Certificate certificate, + X509Chain chain, + SslPolicyErrors sslPolicyErrors) + => + { + if (sslPolicyErrors == SslPolicyErrors.None) + { + return true; + } + + if (_configuration.SslPolicyErrorsBypassed) + { + return true; + } + else + { + NetworkHelper.Logger.Error($"Session [{this}] error occurred when validating remote certificate: [{this.RemoteEndPoint}], [{sslPolicyErrors}]."); + } + + return false; + }); + + var sslStream = new SslStream( + stream, + false, + validateRemoteCertificate, + null, + _configuration.SslEncryptionPolicy); + + if (!_configuration.SslClientCertificateRequired) + { + await sslStream.AuthenticateAsServerAsync( + _configuration.SslServerCertificate); // The X509Certificate used to authenticate the server. + } + else + { + await sslStream.AuthenticateAsServerAsync( + _configuration.SslServerCertificate, // The X509Certificate used to authenticate the server. + _configuration.SslClientCertificateRequired, // A Boolean value that specifies whether the client must supply a certificate for authentication. + _configuration.SslEnabledProtocols, // The SslProtocols value that represents the protocol used for authentication. + _configuration.SslCheckCertificateRevocation); // A Boolean value that specifies whether the certificate revocation list is checked during authentication. + } + + // When authentication succeeds, you must check the IsEncrypted and IsSigned properties + // to determine what security services are used by the SslStream. + // Check the IsMutuallyAuthenticated property to determine whether mutual authentication occurred. + NetworkHelper.Logger.Debug(string.Format( + "Ssl Stream: SslProtocol[{0}], IsServer[{1}], IsAuthenticated[{2}], IsEncrypted[{3}], IsSigned[{4}], IsMutuallyAuthenticated[{5}], " + + "HashAlgorithm[{6}], HashStrength[{7}], KeyExchangeAlgorithm[{8}], KeyExchangeStrength[{9}], CipherAlgorithm[{10}], CipherStrength[{11}].", + sslStream.SslProtocol, + sslStream.IsServer, + sslStream.IsAuthenticated, + sslStream.IsEncrypted, + sslStream.IsSigned, + sslStream.IsMutuallyAuthenticated, + sslStream.HashAlgorithm, + sslStream.HashStrength, + sslStream.KeyExchangeAlgorithm, + sslStream.KeyExchangeStrength, + sslStream.CipherAlgorithm, + sslStream.CipherStrength)); + + return sslStream; + } + + private async Task OpenHandshake() + { + bool handshakeResult = false; + + try + { + int terminatorIndex = -1; + while (!WebSocketHelpers.FindHttpMessageTerminator(_receiveBuffer.Array, _receiveBuffer.Offset, _receiveBufferOffset, out terminatorIndex)) + { + int receiveCount = await _stream.ReadAsync( + _receiveBuffer.Array, + _receiveBuffer.Offset + _receiveBufferOffset, + _receiveBuffer.Count - _receiveBufferOffset); + if (receiveCount == 0) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to receive zero bytes.", RemoteEndPoint)); + } + + SegmentBufferDeflector.ReplaceBuffer(_bufferManager, ref _receiveBuffer, ref _receiveBufferOffset, receiveCount); + + if (_receiveBufferOffset > 2048) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to receive weird stream.", RemoteEndPoint)); + } + } + + string secWebSocketKey = string.Empty; + string path = string.Empty; + string query = string.Empty; + handshakeResult = WebSocketServerHandshaker.HandleOpenningHandshakeRequest( + this, + _receiveBuffer.Array, + _receiveBuffer.Offset, + terminatorIndex + Consts.HttpMessageTerminator.Length, + out secWebSocketKey, out path, out query); + + _module = _routeResolver.Resolve(path, query); + if (_module == null) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to cannot identify the resource name [{1}{2}].", RemoteEndPoint, path, query)); + } + + if (handshakeResult) + { + var responseBuffer = WebSocketServerHandshaker.CreateOpenningHandshakeResponse(this, secWebSocketKey); + await _stream.WriteAsync(responseBuffer, 0, responseBuffer.Length); + } + + SegmentBufferDeflector.ShiftBuffer( + _bufferManager, + terminatorIndex + Consts.HttpMessageTerminator.Length, + ref _receiveBuffer, + ref _receiveBufferOffset); + } + catch (WebSocketHandshakeException ex) + { + NetworkHelper.Logger.Error($"Session [{this}] exception occurred, [{ex.Message}].{Environment.NewLine}{ex.FormatExceptionToMessage()}"); + handshakeResult = false; + } + catch (Exception) + { + handshakeResult = false; + throw; + } + + return handshakeResult; + } + + private void ResetKeepAlive() + { + _keepAliveTracker = KeepAliveTracker.Create(KeepAliveInterval, new TimerCallback((s) => OnKeepAlive())); + _keepAliveTimeoutTimer = new Timer(new TimerCallback((s) => OnKeepAliveTimeout()), null, Timeout.Infinite, Timeout.Infinite); + _closingTimeoutTimer = new Timer(new TimerCallback((s) => OnCloseTimeout()), null, Timeout.Infinite, Timeout.Infinite); + } + + #endregion + + #region Process + + private async Task Process() + { + try + { + Header frameHeader; + byte[] payload; + int payloadOffset; + int payloadCount; + int consumedLength = 0; + + while (State == WebSocketState.Open || State == WebSocketState.Closing) + { + int receiveCount = await _stream.ReadAsync( + _receiveBuffer.Array, + _receiveBuffer.Offset + _receiveBufferOffset, + _receiveBuffer.Count - _receiveBufferOffset); + if (receiveCount == 0) + { + break; + } + + _keepAliveTracker.OnDataReceived(); + SegmentBufferDeflector.ReplaceBuffer(_bufferManager, ref _receiveBuffer, ref _receiveBufferOffset, receiveCount); + consumedLength = 0; + + while (true) + { + frameHeader = null; + payload = null; + payloadOffset = 0; + payloadCount = 0; + + if (_frameBuilder.TryDecodeFrameHeader( + _receiveBuffer.Array, + _receiveBuffer.Offset + consumedLength, + _receiveBufferOffset - consumedLength, + out frameHeader) + && frameHeader.Length + frameHeader.PayloadLength <= _receiveBufferOffset - consumedLength) + { + try + { + if (!frameHeader.IsMasked) + { + await Close(WebSocketCloseCode.ProtocolError, "A server MUST close the connection upon receiving a frame that is not masked."); + throw new WebSocketException(string.Format( + "Server received unmasked frame [{0}] from remote [{1}].", frameHeader.OpCode, RemoteEndPoint)); + } + + _frameBuilder.DecodePayload( + _receiveBuffer.Array, + _receiveBuffer.Offset + consumedLength, + frameHeader, + out payload, out payloadOffset, out payloadCount); + + switch (frameHeader.OpCode) + { + case OpCode.Continuation: + await HandleContinuationFrame(frameHeader, payload, payloadOffset, payloadCount); + break; + case OpCode.Text: + await HandleTextFrame(frameHeader, payload, payloadOffset, payloadCount); + break; + case OpCode.Binary: + await HandleBinaryFrame(frameHeader, payload, payloadOffset, payloadCount); + break; + case OpCode.Close: + await HandleCloseFrame(frameHeader, payload, payloadOffset, payloadCount); + break; + case OpCode.Ping: + await HandlePingFrame(frameHeader, payload, payloadOffset, payloadCount); + break; + case OpCode.Pong: + await HandlePongFrame(frameHeader, payload, payloadOffset, payloadCount); + break; + default: + { + // Incoming data MUST always be validated by both clients and servers. + // If, at any time, an endpoint is faced with data that it does not + // understand or that violates some criteria by which the endpoint + // determines safety of input, or when the endpoint sees an opening + // handshake that does not correspond to the values it is expecting + // (e.g., incorrect path or origin in the client request), the endpoint + // MAY drop the TCP connection. If the invalid data was received after + // a successful WebSocket handshake, the endpoint SHOULD send a Close + // frame with an appropriate status code (Section 7.4) before proceeding + // to _Close the WebSocket Connection_. Use of a Close frame with an + // appropriate status code can help in diagnosing the problem. If the + // invalid data is sent during the WebSocket handshake, the server + // SHOULD return an appropriate HTTP [RFC2616] status code. + await Close(WebSocketCloseCode.InvalidMessageType); + throw new NotSupportedException( + string.Format("Not support received opcode [{0}].", (byte)frameHeader.OpCode)); + } + } + } + catch (Exception ex) + { + NetworkHelper.Logger.Error($"Session [{this}] exception occurred, [{ex.Message}].{Environment.NewLine}{ex.FormatExceptionToMessage()}"); + throw; + } + finally + { + consumedLength += frameHeader.Length + frameHeader.PayloadLength; + } + } + else + { + break; + } + } + + if (_receiveBuffer != null && _receiveBuffer.Array != null) + { + SegmentBufferDeflector.ShiftBuffer(_bufferManager, consumedLength, ref _receiveBuffer, ref _receiveBufferOffset); + } + } + } + catch (ObjectDisposedException) + { + // looking forward to a graceful quit from the ReadAsync but the inside EndRead will raise the ObjectDisposedException, + // so a gracefully close for the socket should be a Shutdown, but we cannot avoid the Close triggers this happen. + } + catch (Exception ex) + { + await HandleReceiveOperationException(ex); + } + finally + { + await InternalClose(true); // read async buffer returned, remote notifies closed + } + } + + private async Task HandleContinuationFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) + { + if (!frameHeader.IsFIN) + { + try + { + await _module.OnSessionFragmentationStreamContinued(this, payload, payloadOffset, payloadCount); + } + catch (Exception ex) + { + await HandleUserSideError(ex); + } + } + else + { + try + { + await _module.OnSessionFragmentationStreamClosed(this, payload, payloadOffset, payloadCount); + } + catch (Exception ex) + { + await HandleUserSideError(ex); + } + } + } + + private async Task HandleTextFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) + { + if (frameHeader.IsFIN) + { + try + { + var text = Encoding.UTF8.GetString(payload, payloadOffset, payloadCount); + await _module.OnSessionTextReceived(this, text); + } + catch (Exception ex) + { + await HandleUserSideError(ex); + } + } + else + { + try + { + await _module.OnSessionFragmentationStreamOpened(this, payload, payloadOffset, payloadCount); + } + catch (Exception ex) + { + await HandleUserSideError(ex); + } + } + } + + private async Task HandleBinaryFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) + { + if (frameHeader.IsFIN) + { + try + { + await _module.OnSessionBinaryReceived(this, payload, payloadOffset, payloadCount); + } + catch (Exception ex) + { + await HandleUserSideError(ex); + } + } + else + { + try + { + await _module.OnSessionFragmentationStreamOpened(this, payload, payloadOffset, payloadCount); + } + catch (Exception ex) + { + await HandleUserSideError(ex); + } + } + } + + private async Task HandleCloseFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) + { + if (!frameHeader.IsFIN) + { + throw new WebSocketException(string.Format( + "Server received unfinished frame [{0}] from remote [{1}].", frameHeader.OpCode, RemoteEndPoint)); + } + + if (payloadCount > 1) + { + var statusCode = payload[payloadOffset + 0] * 256 + payload[payloadOffset + 1]; + var closeCode = (WebSocketCloseCode)statusCode; + var closeReason = string.Empty; + + if (payloadCount > 2) + { + closeReason = Encoding.UTF8.GetString(payload, payloadOffset + 2, payloadCount - 2); + } +#if DEBUG + NetworkHelper.Logger.Debug($"Session [{this}] received client side close frame [{closeCode}] [{closeReason}]."); +#endif + // If an endpoint receives a Close frame and did not previously send a + // Close frame, the endpoint MUST send a Close frame in response. (When + // sending a Close frame in response, the endpoint typically echos the + // status code it received.) It SHOULD do so as soon as practical. + await Close(closeCode, closeReason); + } + else + { +#if DEBUG + NetworkHelper.Logger.Debug($"Session [{this}] received client side close frame but no status code."); +#endif + await Close(WebSocketCloseCode.InvalidPayloadData); + } + } + + private async Task HandlePingFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) + { + if (!frameHeader.IsFIN) + { + throw new WebSocketException(string.Format( + "Server received unfinished frame [{0}] from remote [{1}].", frameHeader.OpCode, RemoteEndPoint)); + } + + // Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in + // response, unless it already received a Close frame. It SHOULD + // respond with Pong frame as soon as is practical. Pong frames are + // discussed in Section 5.5.3. + // + // An endpoint MAY send a Ping frame any time after the connection is + // established and before the connection is closed. + // + // A Ping frame may serve either as a keep-alive or as a means to + // verify that the remote endpoint is still responsive. + var ping = Encoding.UTF8.GetString(payload, payloadOffset, payloadCount); +#if DEBUG + NetworkHelper.Logger.Debug($"Session [{this}] received client side ping frame [{ping}]."); +#endif + if (State == WebSocketState.Open) + { + // A Pong frame sent in response to a Ping frame must have identical + // "Application data" as found in the message body of the Ping frame being replied to. + var pong = new PongFrame(ping, false).ToArray(_frameBuilder); + await SendFrame(pong); +#if DEBUG + NetworkHelper.Logger.Debug($"Session [{this}] sends server side pong frame [{ping}]."); +#endif + } + } + + private async Task HandlePongFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) + { + if (!frameHeader.IsFIN) + { + throw new WebSocketException(string.Format( + "Server received unfinished frame [{0}] from remote [{1}].", frameHeader.OpCode, RemoteEndPoint)); + } + + // If an endpoint receives a Ping frame and has not yet sent Pong + // frame(s) in response to previous Ping frame(s), the endpoint MAY + // elect to send a Pong frame for only the most recently processed Ping frame. + // + // A Pong frame MAY be sent unsolicited. This serves as a + // unidirectional heartbeat. A response to an unsolicited Pong frame is not expected. + var pong = Encoding.UTF8.GetString(payload, payloadOffset, payloadCount); + StopKeepAliveTimeoutTimer(); +#if DEBUG + NetworkHelper.Logger.Debug($"Session [{this}] received client side pong frame [{pong}]."); +#endif + await Task.CompletedTask; + } + + #endregion + + #region Close + + public async Task Close(WebSocketCloseCode closeCode) + { + await Close(closeCode, null); + } + + public async Task Close(WebSocketCloseCode closeCode, string closeReason) + { + if (State == WebSocketState.Closed || State == WebSocketState.None) + { + return; + } + + var priorState = Interlocked.Exchange(ref _state, _closing); + switch (priorState) + { + case _connected: + { + var closingHandshake = new CloseFrame(closeCode, closeReason, false).ToArray(_frameBuilder); + try + { + await _stream.WriteAsync(closingHandshake, 0, closingHandshake.Length); + StartClosingTimer(); +#if DEBUG + NetworkHelper.Logger.Debug($"Session [{this}] sends server side close frame [{closeCode}] [{closeReason}]."); +#endif + } + catch (Exception ex) + { + await HandleSendOperationException(ex); + } + return; + } + case _connecting: + case _closing: + { + await InternalClose(true); // closing + return; + } + case _disposed: + case _none: + default: + return; + } + } + + private async Task InternalClose(bool shallNotifyUserSide) + { + if (Interlocked.Exchange(ref _state, _disposed) == _disposed) + { + return; + } + + Shutdown(); + + if (shallNotifyUserSide) + { + NetworkHelper.Logger.Debug($"Session closed for [{this.RemoteEndPoint}] on [{DateTime.UtcNow.ToString(@"yyyy-MM-dd HH:mm:ss.fffffff")}] in dispatcher [{_module.GetType().Name}] with session count [{this.Server.SessionCount - 1}]."); + + try + { + await _module.OnSessionClosed(this); + } + catch (Exception ex) + { + await HandleUserSideError(ex); + } + } + + Clean(); + } + + public void Shutdown() + { + // The correct way to shut down the connection (especially if you are in a full-duplex conversation) + // is to call socket.Shutdown(SocketShutdown.Send) and give the remote party some time to close + // their send channel. This ensures that you receive any pending data instead of slamming the + // connection shut. ObjectDisposedException should never be part of the normal application flow. + if (_tcpClient != null && _tcpClient.Connected) + { + _tcpClient.Client.Shutdown(SocketShutdown.Send); + } + } + + private void Clean() + { + try + { + try + { + if (_keepAliveTracker != null) + { + _keepAliveTracker.StopTimer(); + _keepAliveTracker.Dispose(); + } + } + catch { } + try + { + if (_keepAliveTimeoutTimer != null) + { + _keepAliveTimeoutTimer.Change(Timeout.Infinite, Timeout.Infinite); + _keepAliveTimeoutTimer.Dispose(); + } + } + catch { } + try + { + if (_closingTimeoutTimer != null) + { + _closingTimeoutTimer.Change(Timeout.Infinite, Timeout.Infinite); + _closingTimeoutTimer.Dispose(); + } + } + catch { } + try + { + if (_stream != null) + { + _stream.Dispose(); + } + } + catch { } + try + { + if (_tcpClient != null) + { + _tcpClient.Dispose(); + } + } + catch { } + } + catch { } + finally + { + _keepAliveTracker = null; + _keepAliveTimeoutTimer = null; + _closingTimeoutTimer = null; + _stream = null; + _tcpClient = null; + } + + if (_receiveBuffer != default(ArraySegment)) + { + _bufferManager.ReturnBuffer(_receiveBuffer); + } + + _receiveBuffer = default(ArraySegment); + _receiveBufferOffset = 0; + } + + public async Task Abort() + { + await InternalClose(true); // abort + } + + private void StartClosingTimer() + { + // In abnormal cases (such as not having received a TCP Close + // from the server after a reasonable amount of time) a client MAY initiate the TCP Close. + _closingTimeoutTimer.Change((int)CloseTimeout.TotalMilliseconds, Timeout.Infinite); + } + + private async void OnCloseTimeout() + { + // After both sending and receiving a Close message, an endpoint + // considers the WebSocket connection closed and MUST close the + // underlying TCP connection. The server MUST close the underlying TCP + // connection immediately; the client SHOULD wait for the server to + // close the connection but MAY close the connection at any time after + // sending and receiving a Close message, e.g., if it has not received a + // TCP Close from the server in a reasonable time period. + NetworkHelper.Logger.Warn($"Session [{this}] closing timer timeout [{CloseTimeout}] then close automatically."); + await InternalClose(true); // close timeout + } + + #endregion + + #region Exception Handler + + private async Task HandleSendOperationException(Exception ex) + { + if (IsSocketTimeOut(ex)) + { + await CloseIfShould(ex); + throw new WebSocketException(ex.Message, new TimeoutException(ex.Message, ex)); + } + + await CloseIfShould(ex); + throw new WebSocketException(ex.Message, ex); + } + + private async Task HandleReceiveOperationException(Exception ex) + { + if (IsSocketTimeOut(ex)) + { + await CloseIfShould(ex); + throw new WebSocketException(ex.Message, new TimeoutException(ex.Message, ex)); + } + + await CloseIfShould(ex); + throw new WebSocketException(ex.Message, ex); + } + + private bool IsSocketTimeOut(Exception ex) + { + return ex is IOException + && ex.InnerException != null + && ex.InnerException is SocketException + && (ex.InnerException as SocketException).SocketErrorCode == SocketError.TimedOut; + } + + private async Task CloseIfShould(Exception ex) + { + if (ex is ObjectDisposedException + || ex is InvalidOperationException + || ex is SocketException + || ex is IOException + || ex is NullReferenceException // buffer array operation + || ex is ArgumentException // buffer array operation + ) + { + NetworkHelper.Logger.Error(ex.FormatExceptionToMessage()); + + await InternalClose(true); // catch specified exception then intend to close the session + + return true; + } + + return false; + } + + private async Task HandleUserSideError(Exception ex) + { + NetworkHelper.Logger.Error($"Session [{this}] error occurred in user side [{ex.Message}].{Environment.NewLine}{ex.FormatExceptionToMessage()}"); + await Task.CompletedTask; + } + + #endregion + + #region Send + + public async Task SendTextAsync(string text) + { + await SendFrame(new TextFrame(text, false).ToArray(_frameBuilder)); + } + + public async Task SendBinaryAsync(byte[] data) + { + await SendBinaryAsync(data, 0, data.Length); + } + + public async Task SendBinaryAsync(byte[] data, int offset, int count) + { + await SendFrame(new BinaryFrame(data, offset, count, false).ToArray(_frameBuilder)); + } + + public async Task SendBinaryAsync(ArraySegment segment) + { + await SendFrame(new BinaryFrame(segment, false).ToArray(_frameBuilder)); + } + + public async Task SendStreamAsync(Stream stream) + { + if (stream == null) + { + throw new ArgumentNullException("stream"); + } + + int fragmentLength = _configuration.ReasonableFragmentSize; + var buffer = new byte[fragmentLength]; + int readCount = 0; + + readCount = await stream.ReadAsync(buffer, 0, fragmentLength); + if (readCount == 0) + { + return; + } + + await SendFrame(new BinaryFragmentationFrame(OpCode.Binary, buffer, 0, readCount, isFin: false, isMasked: false).ToArray(_frameBuilder)); + + while (true) + { + readCount = await stream.ReadAsync(buffer, 0, fragmentLength); + if (readCount != 0) + { + await SendFrame(new BinaryFragmentationFrame(OpCode.Continuation, buffer, 0, readCount, isFin: false, isMasked: false).ToArray(_frameBuilder)); + } + else + { + await SendFrame(new BinaryFragmentationFrame(OpCode.Continuation, buffer, 0, 0, isFin: true, isMasked: false).ToArray(_frameBuilder)); + break; + } + } + } + + private async Task SendFrame(byte[] frame) + { + if (frame == null) + { + throw new ArgumentNullException("frame"); + } + if (State != WebSocketState.Open) + { + throw new InvalidOperationException("This websocket session has not connected."); + } + + try + { + await _stream.WriteAsync(frame, 0, frame.Length); + _keepAliveTracker.OnDataSent(); + } + catch (Exception ex) + { + await HandleSendOperationException(ex); + } + } + + #endregion + + #region Keep Alive + + private void StartKeepAliveTimeoutTimer() + { + _keepAliveTimeoutTimer.Change((int)KeepAliveTimeout.TotalMilliseconds, Timeout.Infinite); + } + + private void StopKeepAliveTimeoutTimer() + { + _keepAliveTimeoutTimer.Change(Timeout.Infinite, Timeout.Infinite); + } + + private async void OnKeepAliveTimeout() + { + NetworkHelper.Logger.Warn($"Session [{this}] keep-alive timer timeout [{KeepAliveTimeout}]."); + await Close(WebSocketCloseCode.AbnormalClosure, "Keep-Alive Timeout"); + } + + private async void OnKeepAlive() + { + if (await _keepAliveLocker.WaitAsync(0)) + { + try + { + if (State != WebSocketState.Open) + { + return; + } + + if (_keepAliveTracker.ShouldSendKeepAlive()) + { + var keepAliveFrame = new PingFrame(false).ToArray(_frameBuilder); + await SendFrame(keepAliveFrame); + StartKeepAliveTimeoutTimer(); +#if DEBUG + NetworkHelper.Logger.Debug($"Session [{this}] sends server side ping frame [{string.Empty}]."); +#endif + _keepAliveTracker.ResetTimer(); + } + } + catch (Exception ex) + { + NetworkHelper.Logger.Error($"Session [{this}] error occurred in user side [{ex.Message}].{Environment.NewLine}{ex.FormatExceptionToMessage()}"); + await Close(WebSocketCloseCode.EndpointUnavailable); + } + finally + { + _keepAliveLocker.Release(); + } + } + } + + #endregion + + #region Extensions + + internal void AgreeExtensions(IEnumerable extensions) + { + if (extensions == null) + { + throw new ArgumentNullException("extensions"); + } + + // no extension configured, but client offered, so just ignore them. + if (this.EnabledExtensions == null || !this.EnabledExtensions.Any()) + { + return; + } + + // Note that the order of extensions is significant. Any interactions + // between multiple extensions MAY be defined in the documents defining + // the extensions. In the absence of such definitions, the + // interpretation is that the header fields listed by the client in its + // request represent a preference of the header fields it wishes to use, + // with the first options listed being most preferable. The extensions + // listed by the server in response represent the extensions actually in + // use for the connection. Should the extensions modify the data and/or + // framing, the order of operations on the data should be assumed to be + // the same as the order in which the extensions are listed in the + // server's response in the opening handshake. + // For example, if there are two extensions "foo" and "bar" and if the + // header field |Sec-WebSocket-Extensions| sent by the server has the + // value "foo, bar", then operations on the data will be made as + // bar(foo(data)), be those changes to the data itself (such as + // compression) or changes to the framing that may "stack". + var agreedExtensions = new SortedList(); + var offeredExtensions = string.Join(",", extensions).Split(',') + .Select(p => p.TrimStart().TrimEnd()).Where(p => !string.IsNullOrWhiteSpace(p)); + + int order = 0; + foreach (var extension in offeredExtensions) + { + order++; + + var offeredExtensionName = extension.Split(';').First(); + if (!this.EnabledExtensions.ContainsKey(offeredExtensionName)) + { + continue; + } + + var extensionNegotiator = this.EnabledExtensions[offeredExtensionName]; + + string invalidParameter; + IWebSocketExtension negotiatedExtension; + if (!extensionNegotiator.NegotiateAsServer(extension, out invalidParameter, out negotiatedExtension) + || !string.IsNullOrEmpty(invalidParameter) + || negotiatedExtension == null) + { + throw new WebSocketHandshakeException(string.Format( + "Negotiate extension with remote [{0}] failed due to extension [{1}] has invalid parameter [{2}].", + this.RemoteEndPoint, extension, invalidParameter)); + } + + agreedExtensions.Add(order, negotiatedExtension); + } + + // A server MUST NOT accept a PMCE extension negotiation offer together + // with another extension if the PMCE will conflict with the extension + // on their use of the RSV1 bit. A client that received a response + // accepting a PMCE extension negotiation offer together with such an + // extension MUST _Fail the WebSocket Connection_. + bool isRsv1BitOccupied = false; + bool isRsv2BitOccupied = false; + bool isRsv3BitOccupied = false; + foreach (var extension in agreedExtensions.Values) + { + if ((isRsv1BitOccupied && extension.Rsv1BitOccupied) + || (isRsv2BitOccupied && extension.Rsv2BitOccupied) + || (isRsv3BitOccupied && extension.Rsv3BitOccupied)) + { + throw new WebSocketHandshakeException(string.Format( + "Negotiate extension with remote [{0}] failed due to conflict bit occupied.", this.RemoteEndPoint)); + } + + isRsv1BitOccupied = isRsv1BitOccupied | extension.Rsv1BitOccupied; + isRsv2BitOccupied = isRsv2BitOccupied | extension.Rsv2BitOccupied; + isRsv3BitOccupied = isRsv3BitOccupied | extension.Rsv3BitOccupied; + } + + _frameBuilder.NegotiatedExtensions = agreedExtensions; + } + + #endregion + + #region Sub-Protocols + + internal void AgreeSubProtocols(string protocols) + { + if (string.IsNullOrWhiteSpace(protocols)) + { + throw new ArgumentNullException("protocols"); + } + } + + #endregion + + #region IDisposable Members + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + [SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_keepAliveTimeoutTimer")] + [SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_keepAliveLocker")] + [SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_closingTimeoutTimer")] + private void Dispose(bool disposing) + { + if (disposing) + { + try + { + InternalClose(false).Wait(); // disposing + } + catch (Exception ex) + { + NetworkHelper.Logger.Error(ex.FormatExceptionToMessage()); + } + } + } + + #endregion + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Server/Module/AsyncWebSocketRouteResolver.cs b/EonaCat.Network/System/Sockets/WebSockets/Server/Module/AsyncWebSocketRouteResolver.cs new file mode 100644 index 0000000..169e70e --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Server/Module/AsyncWebSocketRouteResolver.cs @@ -0,0 +1,34 @@ +using System; +using System.Linq; + +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class AsyncWebSocketRouteResolver + { + private AsyncWebSocketServerModuleCatalog _moduleCatalog; + + public AsyncWebSocketRouteResolver(AsyncWebSocketServerModuleCatalog moduleCatalog) + { + if (moduleCatalog == null) + { + throw new ArgumentNullException("moduleCatalog"); + } + + _moduleCatalog = moduleCatalog; + } + + public AsyncWebSocketServerModule Resolve(string path, string query) + { + var modules = _moduleCatalog.GetAllModules(); + return modules.FirstOrDefault(m => + string.Compare( + m.ModulePath.Trim().TrimStart('/').TrimEnd('/').ToLowerInvariant(), + path.Trim().TrimStart('/').TrimEnd('/').ToLowerInvariant(), + StringComparison.OrdinalIgnoreCase + ) == 0); + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Server/Module/AsyncWebSocketServerModule.cs b/EonaCat.Network/System/Sockets/WebSockets/Server/Module/AsyncWebSocketServerModule.cs new file mode 100644 index 0000000..ed145b4 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Server/Module/AsyncWebSocketServerModule.cs @@ -0,0 +1,119 @@ +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public abstract class AsyncWebSocketServerModule : IAsyncWebSocketServerMessageDispatcher + { + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private static readonly Regex ModuleNameExpression = new Regex(@"(?[\w]+)Module$", RegexOptions.Compiled); + + private ConcurrentDictionary _sessions = new ConcurrentDictionary(); + + protected AsyncWebSocketServerModule() + : this(string.Empty) + { + } + + protected AsyncWebSocketServerModule(string modulePath) + { + this.ModulePath = modulePath; + this.ModuleName = GetModuleName(); + } + + private string GetModuleName() + { + var typeName = this.GetType().Name; + var nameMatch = ModuleNameExpression.Match(typeName); + + if (nameMatch.Success) + { + return nameMatch.Groups["name"].Value; + } + + return typeName; + } + + public string ModuleName { get; protected set; } + + public string ModulePath { get; protected set; } + + public int SessionCount { get { return _sessions.Count; } } + + #region Dispatcher + + public virtual async Task OnSessionStarted(AsyncWebSocketSession session) + { + _sessions.TryAdd(session.SessionKey, session); + await Task.CompletedTask; + } + + public virtual async Task OnSessionTextReceived(AsyncWebSocketSession session, string text) + { + await Task.CompletedTask; + } + + public virtual async Task OnSessionBinaryReceived(AsyncWebSocketSession session, byte[] data, int offset, int count) + { + await Task.CompletedTask; + } + + public virtual async Task OnSessionClosed(AsyncWebSocketSession session) + { + AsyncWebSocketSession throwAway; + _sessions.TryRemove(session.SessionKey, out throwAway); + await Task.CompletedTask; + } + + #endregion + + #region Fragmentation + + public virtual async Task OnSessionFragmentationStreamOpened(AsyncWebSocketSession session, byte[] data, int offset, int count) + { + await Task.CompletedTask; + } + + public virtual async Task OnSessionFragmentationStreamContinued(AsyncWebSocketSession session, byte[] data, int offset, int count) + { + await Task.CompletedTask; + } + + public virtual async Task OnSessionFragmentationStreamClosed(AsyncWebSocketSession session, byte[] data, int offset, int count) + { + await Task.CompletedTask; + } + + #endregion + + #region Send + + public async Task Broadcast(string text) + { + foreach (var session in _sessions.Values) + { + await session.SendTextAsync(text); + } + } + + public async Task Broadcast(byte[] binary) + { + await Broadcast(binary, 0, binary.Length); + } + + public async Task Broadcast(byte[] binary, int offset, int count) + { + foreach (var session in _sessions.Values) + { + await session.SendBinaryAsync(binary, offset, count); + } + } + + #endregion + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Server/Module/AsyncWebSocketServerModuleCatalog.cs b/EonaCat.Network/System/Sockets/WebSockets/Server/Module/AsyncWebSocketServerModuleCatalog.cs new file mode 100644 index 0000000..10ff3a3 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Server/Module/AsyncWebSocketServerModuleCatalog.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; + +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class AsyncWebSocketServerModuleCatalog + { + private Dictionary _modules = new Dictionary(); + + public IEnumerable GetAllModules() + { + return _modules.Values; + } + + public AsyncWebSocketServerModule GetModule(Type moduleType) + { + return _modules[moduleType.FullName]; + } + + public void RegisterModule(AsyncWebSocketServerModule module) + { + _modules.Add(module.GetType().FullName, module); + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Server/Module/IAsyncWebSocketServerMessageDispatcher.cs b/EonaCat.Network/System/Sockets/WebSockets/Server/Module/IAsyncWebSocketServerMessageDispatcher.cs new file mode 100644 index 0000000..70b9849 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Server/Module/IAsyncWebSocketServerMessageDispatcher.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; + +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public interface IAsyncWebSocketServerMessageDispatcher + { + Task OnSessionStarted(AsyncWebSocketSession session); + Task OnSessionTextReceived(AsyncWebSocketSession session, string text); + Task OnSessionBinaryReceived(AsyncWebSocketSession session, byte[] data, int offset, int count); + Task OnSessionClosed(AsyncWebSocketSession session); + + Task OnSessionFragmentationStreamOpened(AsyncWebSocketSession session, byte[] data, int offset, int count); + Task OnSessionFragmentationStreamContinued(AsyncWebSocketSession session, byte[] data, int offset, int count); + Task OnSessionFragmentationStreamClosed(AsyncWebSocketSession session, byte[] data, int offset, int count); + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/Server/WebSocketServerHandshaker.cs b/EonaCat.Network/System/Sockets/WebSockets/Server/WebSocketServerHandshaker.cs new file mode 100644 index 0000000..ede6e14 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/Server/WebSocketServerHandshaker.cs @@ -0,0 +1,459 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Security.Cryptography; +using System.Text; +using EonaCat.WebSockets.Buffer; +using EonaCat.Logger.Extensions; +using EonaCat.Network; + +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + internal sealed class WebSocketServerHandshaker + { + private static readonly char[] _headerLineSplitter = new char[] { '\r', '\n' }; + + internal static bool HandleOpenningHandshakeRequest(AsyncWebSocketSession session, byte[] buffer, int offset, int count, + out string secWebSocketKey, + out string path, + out string query) + { + BufferValidator.ValidateBuffer(buffer, offset, count, "buffer"); + + var request = Encoding.UTF8.GetString(buffer, offset, count); +#if DEBUG + NetworkHelper.Logger.Debug($"[{session.RemoteEndPoint}]{Environment.NewLine}{request}"); +#endif + try + { + // GET /chat HTTP/1.1 + // Host: server.example.com + // Upgrade: websocket + // Connection: Upgrade + // Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== + // Origin: http://example.com + // Sec-WebSocket-Protocol: chat, superchat + // Sec-WebSocket-Version: 13 + Dictionary headers; + List extensions; + List protocols; + ParseOpenningHandshakeRequestHeaders(request, out headers, out extensions, out protocols); + if (headers == null) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to invalid headers.", session.RemoteEndPoint)); + } + + // An HTTP/1.1 or higher GET request, including a "Request-URI" + // [RFC2616] that should be interpreted as a /resource name/ + // defined in Section 3 (or an absolute HTTP/HTTPS URI containing the /resource name/). + // A |Host| header field containing the server's authority. + if (!headers.ContainsKey(Consts.HttpGetMethodName)) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to lack of get method.", session.RemoteEndPoint)); + } + + if (!headers.ContainsKey(HttpKnownHeaderNames.Host)) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to lack of host authority.", session.RemoteEndPoint)); + } + + string uriString = string.Empty; + var host = headers[HttpKnownHeaderNames.Host]; + IPAddress hostIpAddress; + if (IPAddress.TryParse(host, out hostIpAddress)) + { + if (hostIpAddress.AddressFamily == AddressFamily.InterNetworkV6) + { + uriString = string.Format("ws://{0}{1}", string.Format("[{0}]", host), headers[Consts.HttpGetMethodName]); + } + else + { + uriString = string.Format("ws://{0}{1}", host, headers[Consts.HttpGetMethodName]); + } + } + else + { + uriString = string.Format("ws://{0}{1}", host, headers[Consts.HttpGetMethodName]); + } + Uri requestUri = null; + if (!Uri.TryCreate(uriString, UriKind.RelativeOrAbsolute, out requestUri)) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to invalid requested resource name.", session.RemoteEndPoint)); + } + path = requestUri.AbsolutePath; + query = requestUri.Query; + + // A |Connection| header field that includes the token "Upgrade", + // treated as an ASCII case-insensitive value. + if (!headers.ContainsKey(HttpKnownHeaderNames.Connection)) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to lack of connection header item.", session.RemoteEndPoint)); + } + + if (headers[HttpKnownHeaderNames.Connection].ToLowerInvariant() != Consts.WebSocketConnectionToken.ToLowerInvariant()) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to invalid connection header item value [{1}].", + session.RemoteEndPoint, headers[HttpKnownHeaderNames.Connection])); + } + + // An |Upgrade| header field containing the value "websocket", + // treated as an ASCII case-insensitive value. + if (!headers.ContainsKey(HttpKnownHeaderNames.Upgrade)) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to lack of upgrade header item.", session.RemoteEndPoint)); + } + + if (headers[HttpKnownHeaderNames.Upgrade].ToLowerInvariant() != Consts.WebSocketUpgradeToken.ToLowerInvariant()) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to invalid upgrade header item value [{1}].", + session.RemoteEndPoint, headers[HttpKnownHeaderNames.Upgrade])); + } + + // A |Sec-WebSocket-Key| header field with a base64-encoded (see + // Section 4 of [RFC4648]) value that, when decoded, is 16 bytes in length. + if (!headers.ContainsKey(HttpKnownHeaderNames.SecWebSocketKey)) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to lack of Sec-WebSocket-Key header item.", session.RemoteEndPoint)); + } + + if (string.IsNullOrWhiteSpace(headers[HttpKnownHeaderNames.SecWebSocketKey])) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to invalid Sec-WebSocket-Key header item value [{1}].", + session.RemoteEndPoint, headers[HttpKnownHeaderNames.SecWebSocketKey])); + } + + secWebSocketKey = headers[HttpKnownHeaderNames.SecWebSocketKey]; + + // A |Sec-WebSocket-Version| header field, with a value of 13. + if (!headers.ContainsKey(HttpKnownHeaderNames.SecWebSocketVersion)) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to lack of Sec-WebSocket-Version header item.", session.RemoteEndPoint)); + } + + if (headers[HttpKnownHeaderNames.SecWebSocketVersion].ToLowerInvariant() != Consts.WebSocketVersion.ToLowerInvariant()) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to invalid Sec-WebSocket-Version header item value [{1}].", + session.RemoteEndPoint, headers[HttpKnownHeaderNames.SecWebSocketVersion])); + } + + // Optionally, a |Sec-WebSocket-Extensions| header field, with a + // list of values indicating which extensions the client would like + // to speak. The interpretation of this header field is discussed in Section 9.1. + if (extensions != null) + { + if (!extensions.Any()) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to empty extension.", session.RemoteEndPoint)); + } + + foreach (var extension in extensions) + { + // The empty string is not the same as the null value for these + // purposes and is not a legal value for this field. + if (string.IsNullOrWhiteSpace(extension)) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to empty extension.", session.RemoteEndPoint)); + } + } + + session.AgreeExtensions(extensions); + } + + // Optionally, a |Sec-WebSocket-Protocol| header field, with a list + // of values indicating which protocols the client would like to + // speak, ordered by preference. + if (protocols != null) + { + if (!protocols.Any()) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to empty sub-protocol.", session.RemoteEndPoint)); + } + + foreach (var protocol in protocols) + { + // The empty string is not the same as the null value for these + // purposes and is not a legal value for this field. + if (string.IsNullOrWhiteSpace(protocol)) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to empty sub-protocol.", session.RemoteEndPoint)); + } + } + + session.AgreeSubProtocols(string.Join(",", protocols)); + } + + // Optionally, an |Origin| header field. This header field is sent + // by all browser clients. A connection attempt lacking this + // header field SHOULD NOT be interpreted as coming from a browser client. + // + // Servers that are not intended to process input from any web page but + // only for certain sites SHOULD verify the |Origin| field is an origin + // they expect. If the origin indicated is unacceptable to the server, + // then it SHOULD respond to the WebSocket handshake with a reply + // containing HTTP 403 Forbidden status code. + // + // The |Origin| header field protects from the attack cases when the + // untrusted party is typically the author of a JavaScript application + // that is executing in the context of the trusted client. The client + // itself can contact the server and, via the mechanism of the |Origin| + // header field, determine whether to extend those communication + // privileges to the JavaScript application. The intent is not to + // prevent non-browsers from establishing connections but rather to + // ensure that trusted browsers under the control of potentially + // malicious JavaScript cannot fake a WebSocket handshake. + + // Optionally, other header fields, such as those used to send + // cookies or request authentication to a server. Unknown header + // fields are ignored, as per [RFC2616]. + } + catch (Exception ex) + { + NetworkHelper.Logger.Error($"{session}{Environment.NewLine}{request}{Environment.NewLine}{ex.FormatExceptionToMessage()}"); + throw; + } + + return true; + } + + internal static byte[] CreateOpenningHandshakeResponse(AsyncWebSocketSession session, string secWebSocketKey) + { + var sb = new StringBuilder(); + + // A Status-Line with a 101 response code as per RFC 2616 + // [RFC2616]. Such a response could look like "HTTP/1.1 101 Switching Protocols". + sb.AppendFormatWithCrCf("HTTP/{0} {1} {2}", + Consts.HttpVersion, + (int)HttpStatusCode.SwitchingProtocols, + @"Switching Protocols"); + + // An |Upgrade| header field with value "websocket" as per RFC2616 [RFC2616]. + sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.Upgrade, Consts.WebSocketUpgradeToken); + + // A |Connection| header field with value "Upgrade". + sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.Connection, Consts.WebSocketConnectionToken); + + // A |Sec-WebSocket-Accept| header field. The value of this + // header field is constructed by concatenating /key/, defined + // above in step 4 in Section 4.2.2, with the string "258EAFA5- + // E914-47DA-95CA-C5AB0DC85B11", taking the SHA-1 hash of this + // concatenated value to obtain a 20-byte value and base64- + // encoding (see Section 4 of [RFC4648]) this 20-byte hash. + var secWebSocketAccept = GetSecWebSocketAcceptString(secWebSocketKey); + sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.SecWebSocketAccept, secWebSocketAccept); + + // Optionally, a |Sec-WebSocket-Extensions| header field, with a + // value /extensions/ as defined in step 4 in Section 4.2.2. If + // multiple extensions are to be used, they can all be listed in + // a single |Sec-WebSocket-Extensions| header field or split + // between multiple instances of the |Sec-WebSocket-Extensions| header field. + // A server accepts one or more extensions by including a + // |Sec-WebSocket-Extensions| header field containing one or more + // extensions that were requested by the client. The interpretation of + // any extension parameters, and what constitutes a valid response by a + // server to a requested set of parameters by a client, will be defined + // by each such extension. + if (session.NegotiatedExtensions != null && session.NegotiatedExtensions.Any()) + { + foreach (var extension in session.NegotiatedExtensions.Values) + { + var offer = extension.GetAgreedOffer(); + sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.SecWebSocketExtensions, offer); + } + } + + /** + // Optionally, a |Sec-WebSocket-Protocol| header field, with a + // value /subprotocol/ as defined in step 4 in Section 4.2.2. + // + // The client can request that the server use a specific subprotocol by + // including the |Sec-WebSocket-Protocol| field in its handshake. If it + // is specified, the server needs to include the same field and one of + // the selected subprotocol values in its response for the connection to + // be established. + // + // These subprotocol names should be registered as per Section 11.5. To + // avoid potential collisions, it is recommended to use names that + // contain the ASCII version of the domain name of the subprotocol's + // originator. For example, if Example Corporation were to create a + // Chat subprotocol to be implemented by many servers around the Web, + // they could name it "chat.example.com". If the Example Organization + // called their competing subprotocol "chat.example.org", then the two + // subprotocols could be implemented by servers simultaneously, with the + // server dynamically selecting which subprotocol to use based on the + // value sent by the client. + // + // Subprotocols can be versioned in backward-incompatible ways by + // changing the subprotocol name, e.g., going from + // "bookings.example.net" to "v2.bookings.example.net". These + // subprotocols would be considered completely separate by WebSocket + // clients. Backward-compatible versioning can be implemented by + // reusing the same subprotocol string but carefully designing the + // actual subprotocol to support this kind of extensibility. + */ + + sb.AppendWithCrCf(); + + // HTTP/1.1 101 Switching Protocols + // Upgrade: websocket + // Connection: Upgrade + // Sec-WebSocket-Accept: 1tGBmA9p0DQDgmFll6P0/UcVS/E= + // Sec-WebSocket-Protocol: chat + var response = sb.ToString(); +#if DEBUG + NetworkHelper.Logger.Debug($"[{session.RemoteEndPoint}]{Environment.NewLine}{response}"); +#endif + return Encoding.UTF8.GetBytes(response); + } + + internal static byte[] CreateOpenningHandshakeBadRequestResponse(AsyncWebSocketSession session) + { + var sb = new StringBuilder(); + + // HTTP/1.1 400 Bad Request + sb.AppendFormatWithCrCf("HTTP/{0} {1} {2}", + Consts.HttpVersion, + (int)HttpStatusCode.BadRequest, + @"Bad Request"); + + // Upgrade: websocket + sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.Upgrade, Consts.WebSocketUpgradeToken); + + // Connection: Upgrade + sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.Connection, Consts.WebSocketConnectionToken); + + // Sec-WebSocket-Version: 13 + sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.SecWebSocketVersion, Consts.WebSocketVersion); + + sb.AppendWithCrCf(); + + var response = sb.ToString(); +#if DEBUG + NetworkHelper.Logger.Debug($"[{session.RemoteEndPoint}]{Environment.NewLine}{response}"); +#endif + return Encoding.UTF8.GetBytes(response); + } + + private static void ParseOpenningHandshakeRequestHeaders(string request, + out Dictionary headers, + out List extensions, + out List protocols) + { + headers = new Dictionary(); + + // The |Sec-WebSocket-Extensions| header field MAY appear multiple times + // in an HTTP request (which is logically the same as a single + // |Sec-WebSocket-Extensions| header field that contains all values. + // However, the |Sec-WebSocket-Extensions| header field MUST NOT appear + // more than once in an HTTP response. + extensions = null; + // The |Sec-WebSocket-Protocol| header field MAY appear multiple times + // in an HTTP request (which is logically the same as a single + // |Sec-WebSocket-Protocol| header field that contains all values). + // However, the |Sec-WebSocket-Protocol| header field MUST NOT appear + // more than once in an HTTP response. + protocols = null; + + var lines = request.Split(_headerLineSplitter).Where(l => l.Length > 0); + foreach (var line in lines) + { + // GET /chat HTTP/1.1 + if (line.StartsWith(Consts.HttpGetMethodName)) + { + var segements = line.Split(' '); + if (segements.Length > 1) + { + headers.Add(Consts.HttpGetMethodName, segements[1]); + + if (segements.Length > 2) + { + var versions = segements[2].Split('/'); + if (versions.Length > 1) + { + headers.Add(Consts.HttpVersionName, versions[1]); + } + } + } + } + else + { + foreach (var key in HttpKnownHeaderNames.All) + { + if (line.StartsWith(key + ":")) + { + var index = line.IndexOf(':'); + if (index != -1) + { + var value = line.Substring(index + 1); + + if (key == HttpKnownHeaderNames.SecWebSocketExtensions) + { + if (extensions == null) + { + extensions = new List(); + } + + extensions.Add(value.Trim()); + } + else if (key == HttpKnownHeaderNames.SecWebSocketProtocol) + { + if (protocols == null) + { + protocols = new List(); + } + + protocols.Add(value.Trim()); + } + else + { + if (headers.ContainsKey(key)) + { + headers[key] = string.Join(",", headers[key], value.Trim()); + } + else + { + headers.Add(key, value.Trim()); + } + } + } + } + } + } + } + } + + private static string GetSecWebSocketAcceptString(string secWebSocketKey) + { + string retVal; + + using (SHA1 sha1 = SHA1.Create()) + { + string acceptString = string.Concat(secWebSocketKey, Consts.SecWebSocketKeyGuid); + byte[] toHash = Encoding.UTF8.GetBytes(acceptString); + retVal = Convert.ToBase64String(sha1.ComputeHash(toHash)); + } + + return retVal; + } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/SubProtocols/IWebSocketSubProtocol.cs b/EonaCat.Network/System/Sockets/WebSockets/SubProtocols/IWebSocketSubProtocol.cs new file mode 100644 index 0000000..cb55be8 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/SubProtocols/IWebSocketSubProtocol.cs @@ -0,0 +1,9 @@ +namespace EonaCat.WebSockets.SubProtocols +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public interface IWebSocketSubProtocol + { + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/SubProtocols/IWebSocketSubProtocolNegotiator.cs b/EonaCat.Network/System/Sockets/WebSockets/SubProtocols/IWebSocketSubProtocolNegotiator.cs new file mode 100644 index 0000000..454e467 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/SubProtocols/IWebSocketSubProtocolNegotiator.cs @@ -0,0 +1,11 @@ +namespace EonaCat.WebSockets.SubProtocols +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public interface IWebSocketSubProtocolNegotiator + { + bool NegotiateAsClient(string protocolName, string protocolVersion, string protocolParameter, out string invalidParameter, out IWebSocketSubProtocol negotiatedSubProtocol); + bool NegotiateAsServer(string protocolName, string protocolVersion, string protocolParameter, out string invalidParameter, out IWebSocketSubProtocol negotiatedSubProtocol); + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/SubProtocols/WebSocketSubProtocolRequestDescription.cs b/EonaCat.Network/System/Sockets/WebSockets/SubProtocols/WebSocketSubProtocolRequestDescription.cs new file mode 100644 index 0000000..9ca0279 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/SubProtocols/WebSocketSubProtocolRequestDescription.cs @@ -0,0 +1,22 @@ +using System; + +namespace EonaCat.WebSockets.SubProtocols +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public sealed class WebSocketSubProtocolRequestDescription + { + public WebSocketSubProtocolRequestDescription(string requestedSubProtocol) + { + if (string.IsNullOrWhiteSpace(requestedSubProtocol)) + { + throw new ArgumentNullException("requestedSubProtocol"); + } + + this.RequestedSubProtocol = requestedSubProtocol; + } + + public string RequestedSubProtocol { get; private set; } + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/WebSocketCloseCode.cs b/EonaCat.Network/System/Sockets/WebSockets/WebSocketCloseCode.cs new file mode 100644 index 0000000..f886386 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/WebSocketCloseCode.cs @@ -0,0 +1,29 @@ +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + // 0 - 999 Status codes in the range 0-999 are not used. + // 1000 - 1999 Status codes in the range 1000-1999 are reserved for definition by this protocol. + // 2000 - 2999 Status codes in the range 2000-2999 are reserved for use by extensions. + // 3000 - 3999 Status codes in the range 3000-3999 MAY be used by libraries and frameworks. The + // interpretation of these codes is undefined by this protocol. End applications MUST + // NOT use status codes in this range. + // 4000 - 4999 Status codes in the range 4000-4999 MAY be used by application code. The interpretation + // of these codes is undefined by this protocol. + public enum WebSocketCloseCode + { + NormalClosure = 1000, + EndpointUnavailable = 1001, + ProtocolError = 1002, + InvalidMessageType = 1003, + Empty = 1005, + AbnormalClosure = 1006, // 1006 is reserved and should never be used by user + InvalidPayloadData = 1007, + PolicyViolation = 1008, + MessageTooBig = 1009, + MandatoryExtension = 1010, + InternalServerError = 1011, + TlsHandshakeFailed = 1015, // 1015 is reserved and should never be used by user + } +} diff --git a/EonaCat.Network/System/Sockets/WebSockets/WebSocketState.cs b/EonaCat.Network/System/Sockets/WebSockets/WebSocketState.cs new file mode 100644 index 0000000..e8591b0 --- /dev/null +++ b/EonaCat.Network/System/Sockets/WebSockets/WebSocketState.cs @@ -0,0 +1,14 @@ +namespace EonaCat.WebSockets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public enum WebSocketState + { + None = 0, + Connecting = 1, + Open = 2, + Closing = 3, + Closed = 5, + } +} diff --git a/EonaCat.Network/System/Tools/CertificateInfoHelper.cs b/EonaCat.Network/System/Tools/CertificateInfoHelper.cs index a1d5d48..a432a38 100644 --- a/EonaCat.Network/System/Tools/CertificateInfoHelper.cs +++ b/EonaCat.Network/System/Tools/CertificateInfoHelper.cs @@ -4,6 +4,9 @@ using System.Text; namespace EonaCat.Network { + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + public class CertificateInfoHelper { public static string GetCertificatesInformation(IEnumerable certificates) diff --git a/EonaCat.Network/System/Tools/FileReader.cs b/EonaCat.Network/System/Tools/FileReader.cs index 0ddb13a..a9cdfb6 100644 --- a/EonaCat.Network/System/Tools/FileReader.cs +++ b/EonaCat.Network/System/Tools/FileReader.cs @@ -2,11 +2,11 @@ using System.IO; using System.Text; -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - namespace EonaCat.Network { + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + public class FileReader { /// diff --git a/EonaCat.Network/System/Web/EonaCatWebserver.cs b/EonaCat.Network/System/Web/EonaCatWebserver.cs index 523e5a6..3bd519d 100644 --- a/EonaCat.Network/System/Web/EonaCatWebserver.cs +++ b/EonaCat.Network/System/Web/EonaCatWebserver.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Reflection; using System.Threading; using System.Threading.Tasks; diff --git a/LICENSE b/LICENSE index bc121e6..ab37379 100644 --- a/LICENSE +++ b/LICENSE @@ -1,206 +1,204 @@ -Apache License + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + https://EonaCat.com/license/ -Version 2.0, January 2004 - -http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, -AND DISTRIBUTION + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + OF SOFTWARE BY EONACAT (JEROEN SAEY) 1. Definitions. - + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -"License" shall mean the terms and conditions for use, reproduction, and distribution -as defined by Sections 1 through 9 of this document. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. - + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. -"Licensor" shall mean the copyright owner or entity authorized by the copyright -owner that is granting the License. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. - + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. -"Legal Entity" shall mean the union of the acting entity and all other entities -that control, are controlled by, or are under common control with that entity. -For the purposes of this definition, "control" means (i) the power, direct -or indirect, to cause the direction or management of such entity, whether -by contract or otherwise, or (ii) ownership of fifty percent (50%) or more -of the outstanding shares, or (iii) beneficial ownership of such entity. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. - + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions -granted by this License. + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. - + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." -"Source" form shall mean the preferred form for making modifications, including -but not limited to software source code, documentation source, and configuration -files. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. - + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. -"Object" form shall mean any form resulting from mechanical transformation -or translation of a Source form, including but not limited to compiled object -code, generated documentation, and conversions to other media types. + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. - + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: -"Work" shall mean the work of authorship, whether in Source or Object form, -made available under the License, as indicated by a copyright notice that -is included in or attached to the work (an example is provided in the Appendix -below). + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and -"Derivative Works" shall mean any work, whether in Source or Object form, -that is based on (or derived from) the Work and for which the editorial revisions, -annotations, elaborations, or other modifications represent, as a whole, an -original work of authorship. For the purposes of this License, Derivative -Works shall not include works that remain separable from, or merely link (or -bind by name) to the interfaces of, the Work and Derivative Works thereof. + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and - + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. -"Contribution" shall mean any work of authorship, including the original version -of the Work and any modifications or additions to that Work or Derivative -Works thereof, that is intentionally submitted to Licensor for inclusion in -the Work by the copyright owner or by an individual or Legal Entity authorized -to submit on behalf of the copyright owner. For the purposes of this definition, -"submitted" means any form of electronic, verbal, or written communication -sent to the Licensor or its representatives, including but not limited to -communication on electronic mailing lists, source code control systems, and -issue tracking systems that are managed by, or on behalf of, the Licensor -for the purpose of discussing and improving the Work, but excluding communication -that is conspicuously marked or otherwise designated in writing by the copyright -owner as "Not a Contribution." + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. - + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf -of whom a Contribution has been received by Licensor and subsequently incorporated -within the Work. + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. -2. Grant of Copyright License. Subject to the terms and conditions of this -License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, -no-charge, royalty-free, irrevocable copyright license to reproduce, prepare -Derivative Works of, publicly display, publicly perform, sublicense, and distribute -the Work and such Derivative Works in Source or Object form. + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. -3. Grant of Patent License. Subject to the terms and conditions of this License, -each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, -no-charge, royalty-free, irrevocable (except as stated in this section) patent -license to make, have made, use, offer to sell, sell, import, and otherwise -transfer the Work, where such license applies only to those patent claims -licensable by such Contributor that are necessarily infringed by their Contribution(s) -alone or by combination of their Contribution(s) with the Work to which such -Contribution(s) was submitted. If You institute patent litigation against -any entity (including a cross-claim or counterclaim in a lawsuit) alleging -that the Work or a Contribution incorporated within the Work constitutes direct -or contributory patent infringement, then any patent licenses granted to You -under this License for that Work shall terminate as of the date such litigation -is filed. + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. -4. Redistribution. You may reproduce and distribute copies of the Work or -Derivative Works thereof in any medium, with or without modifications, and -in Source or Object form, provided that You meet the following conditions: + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. -(a) You must give any other recipients of the Work or Derivative Works a copy -of this License; and + END OF TERMS AND CONDITIONS -(b) You must cause any modified files to carry prominent notices stating that -You changed the files; and + APPENDIX: How to apply the Apache License to your work. -(c) You must retain, in the Source form of any Derivative Works that You distribute, -all copyright, patent, trademark, and attribution notices from the Source -form of the Work, excluding those notices that do not pertain to any part -of the Derivative Works; and + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. -(d) If the Work includes a "NOTICE" text file as part of its distribution, -then any Derivative Works that You distribute must include a readable copy -of the attribution notices contained within such NOTICE file, excluding those -notices that do not pertain to any part of the Derivative Works, in at least -one of the following places: within a NOTICE text file distributed as part -of the Derivative Works; within the Source form or documentation, if provided -along with the Derivative Works; or, within a display generated by the Derivative -Works, if and wherever such third-party notices normally appear. The contents -of the NOTICE file are for informational purposes only and do not modify the -License. You may add Your own attribution notices within Derivative Works -that You distribute, alongside or as an addendum to the NOTICE text from the -Work, provided that such additional attribution notices cannot be construed -as modifying the License. + Copyright [yyyy] [name of copyright owner] -You may add Your own copyright statement to Your modifications and may provide -additional or different license terms and conditions for use, reproduction, -or distribution of Your modifications, or for any such Derivative Works as -a whole, provided Your use, reproduction, and distribution of the Work otherwise -complies with the conditions stated in this License. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at -5. Submission of Contributions. Unless You explicitly state otherwise, any -Contribution intentionally submitted for inclusion in the Work by You to the -Licensor shall be under the terms and conditions of this License, without -any additional terms or conditions. Notwithstanding the above, nothing herein -shall supersede or modify the terms of any separate license agreement you -may have executed with Licensor regarding such Contributions. + http://www.apache.org/licenses/LICENSE-2.0 -6. Trademarks. This License does not grant permission to use the trade names, -trademarks, service marks, or product names of the Licensor, except as required -for reasonable and customary use in describing the origin of the Work and -reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to -in writing, Licensor provides the Work (and each Contributor provides its -Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -KIND, either express or implied, including, without limitation, any warranties -or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR -A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness -of using or redistributing the Work and assume any risks associated with Your -exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether -in tort (including negligence), contract, or otherwise, unless required by -applicable law (such as deliberate and grossly negligent acts) or agreed to -in writing, shall any Contributor be liable to You for damages, including -any direct, indirect, special, incidental, or consequential damages of any -character arising as a result of this License or out of the use or inability -to use the Work (including but not limited to damages for loss of goodwill, -work stoppage, computer failure or malfunction, or any and all other commercial -damages or losses), even if such Contributor has been advised of the possibility -of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work -or Derivative Works thereof, You may choose to offer, and charge a fee for, -acceptance of support, warranty, indemnity, or other liability obligations -and/or rights consistent with this License. However, in accepting such obligations, -You may act only on Your own behalf and on Your sole responsibility, not on -behalf of any other Contributor, and only if You agree to indemnify, defend, -and hold each Contributor harmless for any liability incurred by, or claims -asserted against, such Contributor by reason of your accepting any such warranty -or additional liability. END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate -notice, with the fields enclosed by brackets "[]" replaced with your own identifying -information. (Don't include the brackets!) The text should be enclosed in -the appropriate comment syntax for the file format. We also recommend that -a file or class name and description of purpose be included on the same "printed -page" as the copyright notice for easier identification within third-party -archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); - -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0unless required by applicable law or agreed to in writing, software - -distributed under the License is distributed on an "AS IS" BASIS, - -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and - -limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License.