From cbfb1a94077514d83bfb4f1022328a9c6e882d83 Mon Sep 17 00:00:00 2001 From: Jeroen Date: Fri, 17 Nov 2023 00:54:48 +0100 Subject: [PATCH] Updated --- .../EonaCat.Network.Tester.csproj | 14 + EonaCat.Network.Tester/Program.cs | 130 + EonaCat.Network.sln | 10 +- EonaCat.Network/Constants.cs | 11 + EonaCat.Network/EonaCat.Network.csproj | 7 +- .../{ => Helpers}/NetworkHelper.cs | 8 +- 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 + .../System/Quic/Connections/ConnectionPool.cs | 26 +- .../System/Quic/Connections/QuicConnection.cs | 66 +- .../System/Quic/Context/QuicStreamContext.cs | 10 +- .../Quic/Exceptions/ConnectionException.cs | 2 +- .../Exceptions/ServerNotStartedException.cs | 2 +- .../System/Quic/Exceptions/StreamException.cs | 2 +- .../System/Quic/Helpers/ByteArray.cs | 8 +- .../System/Quic/Helpers/ByteHelpers.cs | 30 +- .../System/Quic/Helpers/IntegerParts.cs | 48 +- .../System/Quic/Helpers/StreamId.cs | 20 +- .../System/Quic/Helpers/VariableInteger.cs | 43 +- .../System/Quic/Infrastructure/ErrorCodes.cs | 2 +- .../Exceptions/ProtocolException.cs | 2 +- .../System/Quic/Infrastructure/FrameParser.cs | 4 +- .../Quic/Infrastructure/Frames/AckFrame.cs | 4 +- .../Frames/ConnectionCloseFrame.cs | 12 +- .../Quic/Infrastructure/Frames/CryptoFrame.cs | 4 +- .../Infrastructure/Frames/DataBlockedFrame.cs | 6 +- .../Infrastructure/Frames/MaxDataFrame.cs | 6 +- .../Frames/MaxStreamDataFrame.cs | 6 +- .../Infrastructure/Frames/MaxStreamsFrame.cs | 6 +- .../Frames/NewConnectionIdFrame.cs | 4 +- .../Infrastructure/Frames/NewTokenFrame.cs | 4 +- .../Infrastructure/Frames/PaddingFrame.cs | 4 +- .../Frames/PathChallengeFrame.cs | 4 +- .../Frames/PathResponseFrame.cs | 4 +- .../Quic/Infrastructure/Frames/PingFrame.cs | 4 +- .../Infrastructure/Frames/ResetStreamFrame.cs | 4 +- .../Frames/RetireConnectionIdFrame.cs | 4 +- .../Infrastructure/Frames/StopSendingFrame.cs | 4 +- .../Frames/StreamDataBlockedFrame.cs | 6 +- .../Quic/Infrastructure/Frames/StreamFrame.cs | 28 +- .../Frames/StreamsBlockedFrame.cs | 4 +- .../System/Quic/Infrastructure/NumberSpace.cs | 10 +- .../PacketProcessing/InitialPacketCreator.cs | 2 + .../PacketProcessing/PacketCreator.cs | 6 +- .../System/Quic/Infrastructure/PacketType.cs | 2 +- .../Infrastructure/Packets/InitialPacket.cs | 17 +- .../Packets/LongHeaderPacket.cs | 12 +- .../Quic/Infrastructure/Packets/Packet.cs | 12 +- .../Packets/ShortHeaderPacket.cs | 4 +- .../Quic/Infrastructure/Packets/Unpacker.cs | 15 + .../Infrastructure/Settings/QuicVersion.cs | 2 +- .../PacketWireTransfer.cs | 12 +- EonaCat.Network/System/Quic/QuicClient.cs | 20 +- EonaCat.Network/System/Quic/QuicServer.cs | 18 +- EonaCat.Network/System/Quic/QuicTransport.cs | 2 + .../System/Quic/Streams/QuicStream.cs | 62 +- EonaCat.Network/System/Sockets/RemoteInfo.cs | 7 +- .../System/Sockets/Tcp/SocketTcpClient.cs | 6 +- .../System/Sockets/Tcp/SocketTcpServer.cs | 6 +- .../System/Sockets/Udp/SocketUdpClient.cs | 8 +- .../System/Sockets/Udp/SocketUdpServer.cs | 13 +- .../System/Sockets/Web/ByteOrder.cs | 12 + .../System/Sockets/Web/CloseStatusCode.cs | 34 + .../System/Sockets/Web/CompressionMethod.cs | 12 + .../Core/Authentication/AuthenticationBase.cs | 129 + .../Authentication/AuthenticationChallenge.cs | 153 + .../Authentication/AuthenticationResponse.cs | 357 ++ .../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 | 603 ++++ .../Context/HttpListenerWebSocketContext.cs | 111 + .../Context/TcpListenerWebSocketContext.cs | 231 ++ .../Web/Core/Context/WebSocketContext.cs | 112 + .../System/Sockets/Web/Core/Cookies/Cookie.cs | 593 ++++ .../Web/Core/Cookies/CookieCollection.cs | 547 +++ .../Web/Core/Cookies/CookieException.cs | 81 + .../Web/Core/Endpoints/EndPointListener.cs | 533 +++ .../Web/Core/Endpoints/EndPointManager.cs | 237 ++ .../Web/Core/Http/HttpBasicIdentity.cs | 31 + .../Sockets/Web/Core/Http/HttpConnection.cs | 633 ++++ .../Web/Core/Http/HttpDigestIdentity.cs | 93 + .../Sockets/Web/Core/Http/HttpHeaderInfo.cs | 75 + .../Sockets/Web/Core/Http/HttpHeaderType.cs | 49 + .../Sockets/Web/Core/Http/HttpListener.cs | 619 ++++ .../Web/Core/Http/HttpListenerAsyncResult.cs | 165 + .../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 | 417 +++ .../Web/Core/Http/HttpListenerResponse.cs | 655 ++++ .../Web/Core/Http/HttpRequestHeader.cs | 101 + .../Web/Core/Http/HttpResponseHeader.cs | 79 + .../Sockets/Web/Core/Http/HttpStatusCode.cs | 103 + .../Web/Core/Http/HttpStreamAsyncResult.cs | 87 + .../Sockets/Web/Core/Http/HttpUtility.cs | 1366 ++++++++ .../Sockets/Web/Core/Http/HttpVersion.cs | 18 + .../Sockets/Web/Core/InputChunkState.cs | 14 + .../System/Sockets/Web/Core/InputState.cs | 11 + .../System/Sockets/Web/Core/LineState.cs | 12 + .../System/Sockets/Web/Core/Logger.cs | 205 ++ .../Sockets/Web/Core/ReadBufferState.cs | 28 + .../Web/Core/SSL/SSLConfigurationClient.cs | 114 + .../Web/Core/SSL/SSLConfigurationServer.cs | 73 + .../Sockets/Web/Core/Stream/RequestStream.cs | 238 ++ .../Sockets/Web/Core/Stream/ResponseStream.cs | 286 ++ .../Sockets/Web/Endpoints/WelcomeEndpoint.cs | 17 + .../Sockets/Web/EventArgs/CloseEventArgs.cs | 48 + .../Sockets/Web/EventArgs/ErrorEventArgs.cs | 25 + .../Sockets/Web/EventArgs/MessageEventArgs.cs | 74 + .../System/Sockets/Web/Extensions.cs | 1644 +++++++++ .../System/Sockets/Web/FinalFrame.cs | 12 + EonaCat.Network/System/Sockets/Web/Mask.cs | 12 + EonaCat.Network/System/Sockets/Web/Opcode.cs | 20 + .../System/Sockets/Web/PayloadData.cs | 142 + EonaCat.Network/System/Sockets/Web/Rsv.cs | 12 + .../Web/Server/HttpRequestEventArgs.cs | 103 + .../System/Sockets/Web/Server/HttpServer.cs | 886 +++++ .../System/Sockets/Web/Server/IWSSession.cs | 20 + .../System/Sockets/Web/Server/ServerState.cs | 13 + .../System/Sockets/Web/Server/WSEndpoint.cs | 235 ++ .../Sockets/Web/Server/WSEndpointHost.cs | 67 + .../Sockets/Web/Server/WSEndpointManager.cs | 514 +++ .../System/Sockets/Web/Server/WSServer.cs | 808 +++++ .../Sockets/Web/Server/WSSessionManager.cs | 893 +++++ .../Web/Server/WebSocketEndpointHost.cs | 55 + EonaCat.Network/System/Sockets/Web/WebBase.cs | 164 + .../System/Sockets/Web/WebRequest.cs | 173 + .../System/Sockets/Web/WebResponse.cs | 144 + .../System/Sockets/Web/WebSocket.cs | 3056 +++++++++++++++++ .../System/Sockets/Web/WebSocketException.cs | 55 + .../System/Sockets/Web/WebSocketFrame.cs | 668 ++++ .../Sockets/Web/WebSocketSecureClient.cs | 147 - .../Sockets/Web/WebSocketSecureServer.cs | 280 -- .../System/Sockets/Web/WebSocketState.cs | 16 + .../Sockets/Web/WebSocketTransportContext.cs | 9 - EonaCat.Network/System/Tools/FileReader.cs | 4 +- EonaCat.Network/System/Tools/Helpers.cs | 5 +- .../System/Web/AccessControlManager.cs | 11 +- .../System/Web/AccessControlMode.cs | 6 +- .../{ => Attributes}/DynamicRouteAttribute.cs | 7 +- .../ParameterRouteAttribute.cs | 7 +- .../{ => Attributes}/StaticRouteAttribute.cs | 7 +- EonaCat.Network/System/Web/ContentRoute.cs | 16 +- .../System/Web/ContentRouteManager.cs | 60 +- .../System/Web/ContentRouteProcessor.cs | 22 +- EonaCat.Network/System/Web/DynamicRoute.cs | 16 +- .../System/Web/DynamicRouteManager.cs | 36 +- .../System/Web/EonaCatWebserver.cs | 57 +- .../System/Web/EonaCatWebserverEvents.cs | 2 + .../System/Web/EonaCatWebserverPages.cs | 15 +- .../System/Web/EonaCatWebserverRoutes.cs | 62 +- .../System/Web/EonaCatWebserverSettings.cs | 50 +- .../System/Web/EonaCatWebserverStatistics.cs | 25 +- .../ConnectionReceivedEventArgs.cs | 8 +- .../Web/{ => EventArgs}/ExceptionEventArgs.cs | 3 + .../Web/{ => EventArgs}/RequestEventArgs.cs | 0 .../Web/{ => EventArgs}/ResponseEventArgs.cs | 0 .../Web/{ => Extensions}/ObjectExtensions.cs | 0 .../Web/{ => Helpers}/SerializationHelper.cs | 12 +- EonaCat.Network/System/Web/HttpContext.cs | 15 +- EonaCat.Network/System/Web/HttpMethod.cs | 6 +- EonaCat.Network/System/Web/HttpRequest.cs | 180 +- EonaCat.Network/System/Web/HttpResponse.cs | 181 +- EonaCat.Network/System/Web/MimeTypes.cs | 4 +- EonaCat.Network/System/Web/ParameterRoute.cs | 18 +- .../System/Web/ParameterRouteManager.cs | 40 +- EonaCat.Network/System/Web/RouteTypeEnum.cs | 6 +- EonaCat.Network/System/Web/StaticRoute.cs | 23 +- .../System/Web/StaticRouteManager.cs | 49 +- 178 files changed, 22127 insertions(+), 915 deletions(-) create mode 100644 EonaCat.Network.Tester/EonaCat.Network.Tester.csproj create mode 100644 EonaCat.Network.Tester/Program.cs create mode 100644 EonaCat.Network/Constants.cs rename EonaCat.Network/{ => Helpers}/NetworkHelper.cs (99%) create mode 100644 EonaCat.Network/System/BlockChain/Block.cs create mode 100644 EonaCat.Network/System/BlockChain/BlockChain.cs create mode 100644 EonaCat.Network/System/BlockChain/ClientServerExample.cs create mode 100644 EonaCat.Network/System/BlockChain/P2PClient.cs create mode 100644 EonaCat.Network/System/BlockChain/P2PServer.cs create mode 100644 EonaCat.Network/System/BlockChain/Transaction.cs create mode 100644 EonaCat.Network/System/Sockets/Web/ByteOrder.cs create mode 100644 EonaCat.Network/System/Sockets/Web/CloseStatusCode.cs create mode 100644 EonaCat.Network/System/Sockets/Web/CompressionMethod.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationBase.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationChallenge.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationResponse.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationSchemes.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Authentication/NetworkCredential.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Chunks/Chunk.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Chunks/ChunkStream.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Chunks/ChunkedRequestStream.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Collections/QueryStringCollection.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Collections/WebHeaderCollection.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Context/HttpListenerWebSocketContext.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Context/TcpListenerWebSocketContext.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Context/WebSocketContext.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Cookies/Cookie.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Cookies/CookieCollection.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Cookies/CookieException.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Endpoints/EndPointListener.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Endpoints/EndPointManager.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpBasicIdentity.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpConnection.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpDigestIdentity.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpHeaderInfo.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpHeaderType.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpListener.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerAsyncResult.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerContext.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerException.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerPrefix.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerPrefixCollection.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerRequest.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerResponse.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpRequestHeader.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpResponseHeader.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpStatusCode.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpStreamAsyncResult.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpUtility.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Http/HttpVersion.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/InputChunkState.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/InputState.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/LineState.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Logger.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/ReadBufferState.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/SSL/SSLConfigurationClient.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/SSL/SSLConfigurationServer.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Stream/RequestStream.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Core/Stream/ResponseStream.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Endpoints/WelcomeEndpoint.cs create mode 100644 EonaCat.Network/System/Sockets/Web/EventArgs/CloseEventArgs.cs create mode 100644 EonaCat.Network/System/Sockets/Web/EventArgs/ErrorEventArgs.cs create mode 100644 EonaCat.Network/System/Sockets/Web/EventArgs/MessageEventArgs.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Extensions.cs create mode 100644 EonaCat.Network/System/Sockets/Web/FinalFrame.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Mask.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Opcode.cs create mode 100644 EonaCat.Network/System/Sockets/Web/PayloadData.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Rsv.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Server/HttpRequestEventArgs.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Server/HttpServer.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Server/IWSSession.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Server/ServerState.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Server/WSEndpoint.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Server/WSEndpointHost.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Server/WSEndpointManager.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Server/WSServer.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Server/WSSessionManager.cs create mode 100644 EonaCat.Network/System/Sockets/Web/Server/WebSocketEndpointHost.cs create mode 100644 EonaCat.Network/System/Sockets/Web/WebBase.cs create mode 100644 EonaCat.Network/System/Sockets/Web/WebRequest.cs create mode 100644 EonaCat.Network/System/Sockets/Web/WebResponse.cs create mode 100644 EonaCat.Network/System/Sockets/Web/WebSocket.cs create mode 100644 EonaCat.Network/System/Sockets/Web/WebSocketException.cs create mode 100644 EonaCat.Network/System/Sockets/Web/WebSocketFrame.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/WebSocketSecureClient.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/WebSocketSecureServer.cs create mode 100644 EonaCat.Network/System/Sockets/Web/WebSocketState.cs delete mode 100644 EonaCat.Network/System/Sockets/Web/WebSocketTransportContext.cs rename EonaCat.Network/System/Web/{ => Attributes}/DynamicRouteAttribute.cs (94%) rename EonaCat.Network/System/Web/{ => Attributes}/ParameterRouteAttribute.cs (94%) rename EonaCat.Network/System/Web/{ => Attributes}/StaticRouteAttribute.cs (94%) rename EonaCat.Network/System/Web/{ => EventArgs}/ConnectionReceivedEventArgs.cs (91%) rename EonaCat.Network/System/Web/{ => EventArgs}/ExceptionEventArgs.cs (98%) rename EonaCat.Network/System/Web/{ => EventArgs}/RequestEventArgs.cs (100%) rename EonaCat.Network/System/Web/{ => EventArgs}/ResponseEventArgs.cs (100%) rename EonaCat.Network/System/Web/{ => Extensions}/ObjectExtensions.cs (100%) rename EonaCat.Network/System/Web/{ => Helpers}/SerializationHelper.cs (90%) diff --git a/EonaCat.Network.Tester/EonaCat.Network.Tester.csproj b/EonaCat.Network.Tester/EonaCat.Network.Tester.csproj new file mode 100644 index 0000000..3b0107d --- /dev/null +++ b/EonaCat.Network.Tester/EonaCat.Network.Tester.csproj @@ -0,0 +1,14 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + diff --git a/EonaCat.Network.Tester/Program.cs b/EonaCat.Network.Tester/Program.cs new file mode 100644 index 0000000..ed78016 --- /dev/null +++ b/EonaCat.Network.Tester/Program.cs @@ -0,0 +1,130 @@ +// 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 EonaCat.Network; +using System.Reflection; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +internal class Program +{ + private static WebSocket _client; + private static WSServer _server; + + private static async Task Main(string[] args) + { + // Menu loop + while (true) + { + Console.WriteLine("0. Create a HTTPS certificate"); + Console.WriteLine("1. Start the server and client"); + Console.WriteLine("2. Send Message"); + Console.WriteLine("3. Quit"); + + var choice = Console.ReadLine(); + + switch (choice) + { + case "0": + CreateCertificate(); + break; + + case "1": + await CreateServerAndClientAsync(); + break; + + case "2": + if (_client != null) + { + Console.Write("Enter message: "); + var message = Console.ReadLine(); + _client.Send(message); + } + break; + + case "3": + _client?.Close(); + return; + + default: + Console.WriteLine("Invalid choice. Try again."); + break; + } + } + } + + private static async Task CreateServerAndClientAsync() + { + var serverUri = "wss://localhost:8443"; + var clientUri = "wss://localhost:8443/Welcome"; + var clientName = "TestClient"; + var certificatePath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "localhost.pfx"); + var certificatePassword = ""; + var requiredPassword = ""; + + // Start the server + StartServer(serverUri, certificatePath, certificatePassword, requiredPassword); + + // Start the client in the main thread + _client = new WebSocket(clientUri); + _client.SslConfiguration.Certificates.Add(new X509Certificate(certificatePath, certificatePassword)); + _client.OnConnect += (sender, e) => Console.WriteLine($"Connected to server"); + _client.OnMessageReceived += (sender, e) => Console.WriteLine($"Received message from server: {e.Data}"); + _client.OnDisconnect += (sender, e) => Console.WriteLine($"Disconnected from server: {e.Code} : {e.Reason}"); + _client.OnError += (sender, e) => Console.WriteLine($"Error: {sender}\n{e}"); + + _client.ConnectAsync(); + + Console.WriteLine("Connected to the server."); + } + + private static void CreateCertificate() + { + Console.Write("Enter hostname: (default: localhost) "); + string hostname = Console.ReadLine(); + + if (string.IsNullOrWhiteSpace(hostname)) + { + hostname = "localhost"; + } + + int days = 30; + Console.Write("Enter days until expiration: (default: 30 days) "); + if (int.TryParse(Console.ReadLine(), out int givenDays)) + { + days = givenDays; + } + + Console.Write("Enter password, enter to skip: "); + string password = Console.ReadLine(); + + RSA rsa = RSA.Create(); + + // Create a certificate request with the specified subject and key pair + CertificateRequest request = new CertificateRequest( + $"CN={hostname}", + rsa, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1); + + // Create a self-signed certificate from the certificate request + X509Certificate2 certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddDays(days)); + + // Export the certificate to a file with password + byte[] certBytes = string.IsNullOrEmpty(password) + ? certificate.Export(X509ContentType.Pfx) + : certificate.Export(X509ContentType.Pfx, password); + File.WriteAllBytes($"{hostname}.pfx", certBytes); + + Console.WriteLine($"Certificate for {hostname} created successfully and will expire on {certificate.NotAfter}."); + Console.WriteLine($"Path: {Path.Combine(AppContext.BaseDirectory, hostname)}.pfx"); + } + + private static void StartServer(string serverUri, string certificatePath, string certificatePassword, string requiredPassword) + { + _server = new WSServer(serverUri); + _server.SslConfiguration.Certificate = new X509Certificate2(certificatePath, certificatePassword); + _server.AddEndpoint("/Welcome"); + _server.Start(); + } +} \ No newline at end of file diff --git a/EonaCat.Network.sln b/EonaCat.Network.sln index b6b6029..3b815a2 100644 --- a/EonaCat.Network.sln +++ b/EonaCat.Network.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EonaCat.Network", "EonaCat.Network\EonaCat.Network.csproj", "{11B9181D-7186-4D81-A5D3-4804E9A61BA6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.Network.Tester", "EonaCat.Network.Tester\EonaCat.Network.Tester.csproj", "{5CC25A51-3832-44CE-9940-AB9A26F21FFD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,10 +17,10 @@ Global {11B9181D-7186-4D81-A5D3-4804E9A61BA6}.Debug|Any CPU.Build.0 = Debug|Any CPU {11B9181D-7186-4D81-A5D3-4804E9A61BA6}.Release|Any CPU.ActiveCfg = Release|Any CPU {11B9181D-7186-4D81-A5D3-4804E9A61BA6}.Release|Any CPU.Build.0 = Release|Any CPU - {14643574-C40B-4268-A3EA-15C132B56EDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {14643574-C40B-4268-A3EA-15C132B56EDB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {14643574-C40B-4268-A3EA-15C132B56EDB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {14643574-C40B-4268-A3EA-15C132B56EDB}.Release|Any CPU.Build.0 = Release|Any CPU + {5CC25A51-3832-44CE-9940-AB9A26F21FFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5CC25A51-3832-44CE-9940-AB9A26F21FFD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5CC25A51-3832-44CE-9940-AB9A26F21FFD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5CC25A51-3832-44CE-9940-AB9A26F21FFD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/EonaCat.Network/Constants.cs b/EonaCat.Network/Constants.cs new file mode 100644 index 0000000..ae9cd07 --- /dev/null +++ b/EonaCat.Network/Constants.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace EonaCat.Network +{ + internal class Constants + { + public static string Version { get; set; } = "1.1.2"; + } +} diff --git a/EonaCat.Network/EonaCat.Network.csproj b/EonaCat.Network/EonaCat.Network.csproj index 00ea79e..240d545 100644 --- a/EonaCat.Network/EonaCat.Network.csproj +++ b/EonaCat.Network/EonaCat.Network.csproj @@ -16,9 +16,9 @@ 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.1 - 1.1.0.1 - 1.1.0.1 + 1.1.2 + 1.1.2 + 1.1.2 icon.png @@ -50,6 +50,7 @@ + diff --git a/EonaCat.Network/NetworkHelper.cs b/EonaCat.Network/Helpers/NetworkHelper.cs similarity index 99% rename from EonaCat.Network/NetworkHelper.cs rename to EonaCat.Network/Helpers/NetworkHelper.cs index ddd847b..38d651a 100644 --- a/EonaCat.Network/NetworkHelper.cs +++ b/EonaCat.Network/Helpers/NetworkHelper.cs @@ -1,12 +1,12 @@ -using System; -using System.Net.Sockets; -using System.Text; -using EonaCat.LogSystem; +using EonaCat.LogSystem; using EonaCat.Quic; using EonaCat.Quic.Connections; using EonaCat.Quic.Events; using EonaCat.Quic.Helpers; using EonaCat.Quic.Streams; +using System; +using System.Net.Sockets; +using System.Text; namespace EonaCat.Network { diff --git a/EonaCat.Network/System/BlockChain/Block.cs b/EonaCat.Network/System/BlockChain/Block.cs new file mode 100644 index 0000000..dc93b1d --- /dev/null +++ b/EonaCat.Network/System/BlockChain/Block.cs @@ -0,0 +1,54 @@ +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 new file mode 100644 index 0000000..e1c3779 --- /dev/null +++ b/EonaCat.Network/System/BlockChain/BlockChain.cs @@ -0,0 +1,106 @@ +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 new file mode 100644 index 0000000..1dacfad --- /dev/null +++ b/EonaCat.Network/System/BlockChain/ClientServerExample.cs @@ -0,0 +1,75 @@ +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 new file mode 100644 index 0000000..4466da3 --- /dev/null +++ b/EonaCat.Network/System/BlockChain/P2PClient.cs @@ -0,0 +1,83 @@ +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)) + { + WebSocket webSocket = new WebSocket(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 new file mode 100644 index 0000000..824e79d --- /dev/null +++ b/EonaCat.Network/System/BlockChain/P2PServer.cs @@ -0,0 +1,51 @@ +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 new file mode 100644 index 0000000..a34fa96 --- /dev/null +++ b/EonaCat.Network/System/BlockChain/Transaction.cs @@ -0,0 +1,16 @@ +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/Quic/Connections/ConnectionPool.cs b/EonaCat.Network/System/Quic/Connections/ConnectionPool.cs index ae1417f..1cf9caf 100644 --- a/EonaCat.Network/System/Quic/Connections/ConnectionPool.cs +++ b/EonaCat.Network/System/Quic/Connections/ConnectionPool.cs @@ -1,8 +1,8 @@ -using System; -using System.Collections.Generic; -using EonaCat.Quic.Infrastructure; +using EonaCat.Quic.Infrastructure; using EonaCat.Quic.Infrastructure.Settings; using EonaCat.Quic.InternalInfrastructure; +using System; +using System.Collections.Generic; namespace EonaCat.Quic.Connections { @@ -19,11 +19,11 @@ namespace EonaCat.Quic.Connections /// Starting point for connection identifiers. /// ConnectionId's are incremented sequentially by 1. /// - private static NumberSpace _ns = new NumberSpace(QuicSettings.MaximumConnectionIds); + private static readonly NumberSpace _ns = new NumberSpace(QuicSettings.MaximumConnectionIds); - private static Dictionary _pool = new Dictionary(); + private static readonly Dictionary _pool = new Dictionary(); - private static List _draining = new List(); + private static readonly List _draining = new List(); /// /// Adds a connection to the connection pool. @@ -32,15 +32,19 @@ namespace EonaCat.Quic.Connections /// /// Connection Id /// - public static bool AddConnection(ConnectionData connection, out UInt64 availableConnectionId) + public static bool AddConnection(ConnectionData connection, out ulong availableConnectionId) { availableConnectionId = 0; if (_pool.ContainsKey(connection.ConnectionId.Value)) + { return false; + } if (_pool.Count > QuicSettings.MaximumConnectionIds) + { return false; + } availableConnectionId = _ns.Get(); @@ -50,16 +54,20 @@ namespace EonaCat.Quic.Connections return true; } - public static void RemoveConnection(UInt64 id) + public static void RemoveConnection(ulong id) { if (_pool.ContainsKey(id)) + { _pool.Remove(id); + } } - public static QuicConnection Find(UInt64 id) + public static QuicConnection Find(ulong id) { if (_pool.ContainsKey(id) == false) + { return null; + } return _pool[id]; } diff --git a/EonaCat.Network/System/Quic/Connections/QuicConnection.cs b/EonaCat.Network/System/Quic/Connections/QuicConnection.cs index fb5899f..4c27f9e 100644 --- a/EonaCat.Network/System/Quic/Connections/QuicConnection.cs +++ b/EonaCat.Network/System/Quic/Connections/QuicConnection.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using EonaCat.Quic.Constants; +using EonaCat.Quic.Constants; using EonaCat.Quic.Events; using EonaCat.Quic.Exceptions; using EonaCat.Quic.Helpers; @@ -11,6 +9,8 @@ using EonaCat.Quic.Infrastructure.Packets; using EonaCat.Quic.Infrastructure.Settings; using EonaCat.Quic.InternalInfrastructure; using EonaCat.Quic.Streams; +using System; +using System.Collections.Generic; namespace EonaCat.Quic.Connections { @@ -22,17 +22,17 @@ namespace EonaCat.Quic.Connections private readonly NumberSpace _numberSpace = new NumberSpace(); private readonly PacketWireTransfer _pwt; - private UInt64 _currentTransferRate; + private ulong _currentTransferRate; private ConnectionState _state; private string _lastError; - private Dictionary _streams; + private readonly Dictionary _streams; public IntegerParts ConnectionId { get; private set; } public IntegerParts PeerConnectionId { get; private set; } public PacketCreator PacketCreator { get; private set; } - public UInt64 MaxData { get; private set; } - public UInt64 MaxStreams { get; private set; } + public ulong MaxData { get; private set; } + public ulong MaxStreams { get; private set; } public StreamOpenedEvent OnStreamOpened { get; set; } public ConnectionClosedEvent OnConnectionClosed { get; set; } @@ -44,11 +44,13 @@ namespace EonaCat.Quic.Connections /// A new stream instance or Null if the connection is terminated. public QuicStream CreateStream(StreamType type) { - UInt32 streamId = _numberSpace.Get(); + uint streamId = _numberSpace.Get(); if (_state != ConnectionState.Open) + { return null; + } - QuicStream stream = new QuicStream(this, new EonaCat.Quic.Helpers.StreamId(streamId, type)); + QuicStream stream = new QuicStream(this, new StreamId(streamId, type)); _streams.Add(streamId, stream); return stream; @@ -61,21 +63,44 @@ namespace EonaCat.Quic.Connections foreach (Frame frame in frames) { if (frame.Type == 0x01) + { OnRstStreamFrame(frame); + } + if (frame.Type == 0x04) + { OnRstStreamFrame(frame); + } + if (frame.Type >= 0x08 && frame.Type <= 0x0f) + { stream = OnStreamFrame(frame); + } + if (frame.Type == 0x10) + { OnMaxDataFrame(frame); + } + if (frame.Type == 0x11) + { OnMaxStreamDataFrame(frame); + } + if (frame.Type >= 0x12 && frame.Type <= 0x13) + { OnMaxStreamFrame(frame); + } + if (frame.Type == 0x14) + { OnDataBlockedFrame(frame); + } + if (frame.Type >= 0x1c && frame.Type <= 0x1d) + { OnConnectionCloseFrame(frame); + } } return stream; @@ -83,13 +108,15 @@ namespace EonaCat.Quic.Connections public void IncrementRate(int length) { - _currentTransferRate += (UInt32)length; + _currentTransferRate += (uint)length; } public bool MaximumReached() { if (_currentTransferRate >= MaxData) + { return true; + } return false; } @@ -128,10 +155,14 @@ namespace EonaCat.Quic.Connections { stream = new QuicStream(this, streamId); - if ((UInt64)_streams.Count < MaxStreams) + if ((ulong)_streams.Count < MaxStreams) + { _streams.Add(streamId.Id, stream); + } else + { SendMaximumStreamReachedError(); + } OnStreamOpened?.Invoke(stream); } @@ -149,7 +180,9 @@ namespace EonaCat.Quic.Connections { MaxDataFrame sf = (MaxDataFrame)frame; if (sf.MaximumData.Value > MaxData) + { MaxData = sf.MaximumData.Value; + } } private void OnMaxStreamDataFrame(Frame frame) @@ -168,7 +201,9 @@ namespace EonaCat.Quic.Connections { MaxStreamsFrame msf = (MaxStreamsFrame)frame; if (msf.MaximumStreams > MaxStreams) + { MaxStreams = msf.MaximumStreams.Value; + } } private void OnDataBlockedFrame(Frame frame) @@ -182,7 +217,10 @@ namespace EonaCat.Quic.Connections StreamId streamId = sdbf.StreamId; if (_streams.ContainsKey(streamId.Id) == false) + { return; + } + QuicStream stream = _streams[streamId.Id]; stream.ProcessStreamDataBlocked(sdbf); @@ -196,7 +234,7 @@ namespace EonaCat.Quic.Connections _currentTransferRate = 0; _state = ConnectionState.Open; _lastError = string.Empty; - _streams = new Dictionary(); + _streams = new Dictionary(); _pwt = connection.PWT; ConnectionId = connection.ConnectionId; @@ -240,7 +278,9 @@ namespace EonaCat.Quic.Connections if (_state == ConnectionState.Draining) { if (string.IsNullOrWhiteSpace(_lastError)) + { _lastError = "Protocol error"; + } TerminateConnection(); @@ -290,7 +330,9 @@ namespace EonaCat.Quic.Connections // Ignore empty packets if (data == null || data.Length <= 0) + { return true; + } bool result = _pwt.SendPacket(packet); diff --git a/EonaCat.Network/System/Quic/Context/QuicStreamContext.cs b/EonaCat.Network/System/Quic/Context/QuicStreamContext.cs index 77182b6..19316ec 100644 --- a/EonaCat.Network/System/Quic/Context/QuicStreamContext.cs +++ b/EonaCat.Network/System/Quic/Context/QuicStreamContext.cs @@ -1,5 +1,5 @@ -using System; -using EonaCat.Quic.Streams; +using EonaCat.Quic.Streams; +using System; namespace EonaCat.Quic.Context { @@ -24,7 +24,7 @@ namespace EonaCat.Quic.Context /// /// Unique stream identifier /// - public UInt64 StreamId { get; private set; } + public ulong StreamId { get; private set; } /// /// Send data to the client. @@ -34,11 +34,15 @@ namespace EonaCat.Quic.Context public bool Send(byte[] data) { if (Stream.CanSendData() == false) + { return false; + } // Ignore empty packets if (data == null || data.Length <= 0) + { return true; + } // Packet packet = ConnectionContext.Connection.PacketCreator.CreateDataPacket(StreamId, data); diff --git a/EonaCat.Network/System/Quic/Exceptions/ConnectionException.cs b/EonaCat.Network/System/Quic/Exceptions/ConnectionException.cs index 7036515..b723049 100644 --- a/EonaCat.Network/System/Quic/Exceptions/ConnectionException.cs +++ b/EonaCat.Network/System/Quic/Exceptions/ConnectionException.cs @@ -7,7 +7,7 @@ namespace EonaCat.Quic.Exceptions public class ConnectionException : Exception { - public ConnectionException(string message) : base(message) + public ConnectionException(string message) : base($"EonaCat Network: {message}") { } } diff --git a/EonaCat.Network/System/Quic/Exceptions/ServerNotStartedException.cs b/EonaCat.Network/System/Quic/Exceptions/ServerNotStartedException.cs index 2c5ce33..16ab1af 100644 --- a/EonaCat.Network/System/Quic/Exceptions/ServerNotStartedException.cs +++ b/EonaCat.Network/System/Quic/Exceptions/ServerNotStartedException.cs @@ -10,7 +10,7 @@ namespace EonaCat.Quic.Exceptions public ServerNotStartedException() { } - public ServerNotStartedException(string message) : base(message) + public ServerNotStartedException(string message) : base($"EonaCat Network: {message}") { } } diff --git a/EonaCat.Network/System/Quic/Exceptions/StreamException.cs b/EonaCat.Network/System/Quic/Exceptions/StreamException.cs index c82c0de..a5205c5 100644 --- a/EonaCat.Network/System/Quic/Exceptions/StreamException.cs +++ b/EonaCat.Network/System/Quic/Exceptions/StreamException.cs @@ -10,7 +10,7 @@ namespace EonaCat.Quic.Exceptions public StreamException() { } - public StreamException(string message) : base(message) + public StreamException(string message) : base($"EonaCat Network: {message}") { } } diff --git a/EonaCat.Network/System/Quic/Helpers/ByteArray.cs b/EonaCat.Network/System/Quic/Helpers/ByteArray.cs index 98dfe28..ea82068 100644 --- a/EonaCat.Network/System/Quic/Helpers/ByteArray.cs +++ b/EonaCat.Network/System/Quic/Helpers/ByteArray.cs @@ -46,18 +46,18 @@ namespace EonaCat.Quic.Helpers return ReadBytes(count.Value); } - public UInt16 ReadUInt16() + public ushort ReadUInt16() { byte[] bytes = ReadBytes(2); - UInt16 result = ByteHelpers.ToUInt16(bytes); + ushort result = ByteHelpers.ToUInt16(bytes); return result; } - public UInt32 ReadUInt32() + public uint ReadUInt32() { byte[] bytes = ReadBytes(4); - UInt32 result = ByteHelpers.ToUInt32(bytes); + uint result = ByteHelpers.ToUInt32(bytes); return result; } diff --git a/EonaCat.Network/System/Quic/Helpers/ByteHelpers.cs b/EonaCat.Network/System/Quic/Helpers/ByteHelpers.cs index e52c73e..80012f5 100644 --- a/EonaCat.Network/System/Quic/Helpers/ByteHelpers.cs +++ b/EonaCat.Network/System/Quic/Helpers/ByteHelpers.cs @@ -8,29 +8,35 @@ namespace EonaCat.Quic.Helpers public static class ByteHelpers { - public static byte[] GetBytes(UInt64 integer) + public static byte[] GetBytes(ulong integer) { byte[] result = BitConverter.GetBytes(integer); if (BitConverter.IsLittleEndian) + { Array.Reverse(result); + } return result; } - public static byte[] GetBytes(UInt32 integer) + public static byte[] GetBytes(uint integer) { byte[] result = BitConverter.GetBytes(integer); if (BitConverter.IsLittleEndian) + { Array.Reverse(result); + } return result; } - public static byte[] GetBytes(UInt16 integer) + public static byte[] GetBytes(ushort integer) { byte[] result = BitConverter.GetBytes(integer); if (BitConverter.IsLittleEndian) + { Array.Reverse(result); + } return result; } @@ -42,32 +48,38 @@ namespace EonaCat.Quic.Helpers return result; } - public static UInt64 ToUInt64(byte[] data) + public static ulong ToUInt64(byte[] data) { if (BitConverter.IsLittleEndian) + { Array.Reverse(data); + } - UInt64 result = BitConverter.ToUInt64(data, 0); + ulong result = BitConverter.ToUInt64(data, 0); return result; } - public static UInt32 ToUInt32(byte[] data) + public static uint ToUInt32(byte[] data) { if (BitConverter.IsLittleEndian) + { Array.Reverse(data); + } - UInt32 result = BitConverter.ToUInt32(data, 0); + uint result = BitConverter.ToUInt32(data, 0); return result; } - public static UInt16 ToUInt16(byte[] data) + public static ushort ToUInt16(byte[] data) { if (BitConverter.IsLittleEndian) + { Array.Reverse(data); + } - UInt16 result = BitConverter.ToUInt16(data, 0); + ushort result = BitConverter.ToUInt16(data, 0); return result; } diff --git a/EonaCat.Network/System/Quic/Helpers/IntegerParts.cs b/EonaCat.Network/System/Quic/Helpers/IntegerParts.cs index 53484e9..01f0d04 100644 --- a/EonaCat.Network/System/Quic/Helpers/IntegerParts.cs +++ b/EonaCat.Network/System/Quic/Helpers/IntegerParts.cs @@ -7,29 +7,25 @@ namespace EonaCat.Quic.Helpers public class IntegerParts { - public const UInt64 MaxValue = 18446744073709551615; + public const ulong MaxValue = 18446744073709551615; - private UInt64 _integer; + public ulong Value { get; } - public UInt64 Value - { get { return _integer; } } + public byte Size => RequiredBytes(Value); - public byte Size - { get { return RequiredBytes(Value); } } - - public IntegerParts(UInt64 integer) + public IntegerParts(ulong integer) { - _integer = integer; + Value = integer; } public byte[] ToByteArray() { - return Encode(this._integer); + return Encode(this.Value); } public static implicit operator byte[](IntegerParts integer) { - return Encode(integer._integer); + return Encode(integer.Value); } public static implicit operator IntegerParts(byte[] bytes) @@ -37,17 +33,17 @@ namespace EonaCat.Quic.Helpers return new IntegerParts(Decode(bytes)); } - public static implicit operator IntegerParts(UInt64 integer) + public static implicit operator IntegerParts(ulong integer) { return new IntegerParts(integer); } - public static implicit operator UInt64(IntegerParts integer) + public static implicit operator ulong(IntegerParts integer) { - return integer._integer; + return integer.Value; } - public static byte[] Encode(UInt64 integer) + public static byte[] Encode(ulong integer) { byte requiredBytes = RequiredBytes(integer); int offset = 8 - requiredBytes; @@ -60,32 +56,42 @@ namespace EonaCat.Quic.Helpers return result; } - public static UInt64 Decode(byte[] bytes) + public static ulong Decode(byte[] bytes) { int i = 8 - bytes.Length; byte[] buffer = new byte[8]; Buffer.BlockCopy(bytes, 0, buffer, i, bytes.Length); - UInt64 res = ByteHelpers.ToUInt64(buffer); + ulong res = ByteHelpers.ToUInt64(buffer); return res; } - private static byte RequiredBytes(UInt64 integer) + private static byte RequiredBytes(ulong integer) { byte result = 0; if (integer <= byte.MaxValue) /* 255 */ + { result = 1; - else if (integer <= UInt16.MaxValue) /* 65535 */ + } + else if (integer <= ushort.MaxValue) /* 65535 */ + { result = 2; - else if (integer <= UInt32.MaxValue) /* 4294967295 */ + } + else if (integer <= uint.MaxValue) /* 4294967295 */ + { result = 4; - else if (integer <= UInt64.MaxValue) /* 18446744073709551615 */ + } + else if (integer <= ulong.MaxValue) /* 18446744073709551615 */ + { result = 8; + } else + { throw new ArgumentOutOfRangeException("Value is larger than GranularInteger.MaxValue."); + } return result; } diff --git a/EonaCat.Network/System/Quic/Helpers/StreamId.cs b/EonaCat.Network/System/Quic/Helpers/StreamId.cs index b350e41..8259ae4 100644 --- a/EonaCat.Network/System/Quic/Helpers/StreamId.cs +++ b/EonaCat.Network/System/Quic/Helpers/StreamId.cs @@ -15,15 +15,15 @@ namespace EonaCat.Quic.Helpers public class StreamId { - public UInt64 Id { get; } - public UInt64 IntegerValue { get; } + public ulong Id { get; } + public ulong IntegerValue { get; } public StreamType Type { get; private set; } - public StreamId(UInt64 id, StreamType type) + public StreamId(ulong id, StreamType type) { Id = id; Type = type; - IntegerValue = id << 2 | (UInt64)type; + IntegerValue = id << 2 | (ulong)type; } public static implicit operator byte[](StreamId id) @@ -36,7 +36,7 @@ namespace EonaCat.Quic.Helpers return Decode(data); } - public static implicit operator UInt64(StreamId streamId) + public static implicit operator ulong(StreamId streamId) { return streamId.Id; } @@ -46,9 +46,9 @@ namespace EonaCat.Quic.Helpers return Decode(ByteHelpers.GetBytes(integer.Value)); } - public static byte[] Encode(UInt64 id, StreamType type) + public static byte[] Encode(ulong id, StreamType type) { - UInt64 identifier = id << 2 | (UInt64)type; + ulong identifier = id << 2 | (ulong)type; byte[] result = ByteHelpers.GetBytes(identifier); @@ -58,9 +58,9 @@ namespace EonaCat.Quic.Helpers public static StreamId Decode(byte[] data) { StreamId result; - UInt64 id = ByteHelpers.ToUInt64(data); - UInt64 identifier = id >> 2; - UInt64 type = (UInt64)(0x03 & id); + ulong id = ByteHelpers.ToUInt64(data); + ulong identifier = id >> 2; + ulong type = 0x03 & id; StreamType streamType = (StreamType)type; result = new StreamId(identifier, streamType); diff --git a/EonaCat.Network/System/Quic/Helpers/VariableInteger.cs b/EonaCat.Network/System/Quic/Helpers/VariableInteger.cs index 87d9ec9..a1f979c 100644 --- a/EonaCat.Network/System/Quic/Helpers/VariableInteger.cs +++ b/EonaCat.Network/System/Quic/Helpers/VariableInteger.cs @@ -7,21 +7,18 @@ namespace EonaCat.Quic.Helpers public class IntegerVar { - public const UInt64 MaxValue = 4611686018427387903; + public const ulong MaxValue = 4611686018427387903; - private UInt64 _integer; + public ulong Value { get; } - public UInt64 Value - { get { return _integer; } } - - public IntegerVar(UInt64 integer) + public IntegerVar(ulong integer) { - _integer = integer; + Value = integer; } public static implicit operator byte[](IntegerVar integer) { - return Encode(integer._integer); + return Encode(integer.Value); } public static implicit operator IntegerVar(byte[] bytes) @@ -29,14 +26,14 @@ namespace EonaCat.Quic.Helpers return new IntegerVar(Decode(bytes)); } - public static implicit operator IntegerVar(UInt64 integer) + public static implicit operator IntegerVar(ulong integer) { return new IntegerVar(integer); } - public static implicit operator UInt64(IntegerVar integer) + public static implicit operator ulong(IntegerVar integer) { - return integer._integer; + return integer.Value; } public static implicit operator IntegerVar(StreamId streamId) @@ -53,22 +50,32 @@ namespace EonaCat.Quic.Helpers public byte[] ToByteArray() { - return Encode(this._integer); + return Encode(this.Value); } - public static byte[] Encode(UInt64 integer) + public static byte[] Encode(ulong integer) { int requiredBytes = 0; if (integer <= byte.MaxValue >> 2) /* 63 */ + { requiredBytes = 1; - else if (integer <= UInt16.MaxValue >> 2) /* 16383 */ + } + else if (integer <= ushort.MaxValue >> 2) /* 16383 */ + { requiredBytes = 2; - else if (integer <= UInt32.MaxValue >> 2) /* 1073741823 */ + } + else if (integer <= uint.MaxValue >> 2) /* 1073741823 */ + { requiredBytes = 4; - else if (integer <= UInt64.MaxValue >> 2) /* 4611686018427387903 */ + } + else if (integer <= ulong.MaxValue >> 2) /* 4611686018427387903 */ + { requiredBytes = 8; + } else + { throw new ArgumentOutOfRangeException("Value is larger than IntegerVar.MaxValue."); + } int offset = 8 - requiredBytes; @@ -83,7 +90,7 @@ namespace EonaCat.Quic.Helpers return result; } - public static UInt64 Decode(byte[] bytes) + public static ulong Decode(byte[] bytes) { int i = 8 - bytes.Length; byte[] buffer = new byte[8]; @@ -91,7 +98,7 @@ namespace EonaCat.Quic.Helpers Buffer.BlockCopy(bytes, 0, buffer, i, bytes.Length); buffer[i] = (byte)(buffer[i] & (255 >> 2)); - UInt64 res = ByteHelpers.ToUInt64(buffer); + ulong res = ByteHelpers.ToUInt64(buffer); return res; } diff --git a/EonaCat.Network/System/Quic/Infrastructure/ErrorCodes.cs b/EonaCat.Network/System/Quic/Infrastructure/ErrorCodes.cs index 0f20d17..cf96230 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/ErrorCodes.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/ErrorCodes.cs @@ -5,7 +5,7 @@ namespace EonaCat.Quic.Infrastructure // 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 ErrorCode : UInt16 + public enum ErrorCode : ushort { NO_ERROR = 0x0, INTERNAL_ERROR = 0x1, diff --git a/EonaCat.Network/System/Quic/Infrastructure/Exceptions/ProtocolException.cs b/EonaCat.Network/System/Quic/Infrastructure/Exceptions/ProtocolException.cs index 1a37dc0..1f3a83d 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Exceptions/ProtocolException.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Exceptions/ProtocolException.cs @@ -11,7 +11,7 @@ namespace EonaCat.Quic.Infrastructure.Exceptions { } - public ProtocolException(string message) : base(message) + public ProtocolException(string message) : base($"EonaCat Network: {message}") { } } diff --git a/EonaCat.Network/System/Quic/Infrastructure/FrameParser.cs b/EonaCat.Network/System/Quic/Infrastructure/FrameParser.cs index 892de6d..3e6485c 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/FrameParser.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/FrameParser.cs @@ -8,7 +8,7 @@ namespace EonaCat.Quic.Infrastructure public class FrameParser { - private ByteArray _array; + private readonly ByteArray _array; public FrameParser(ByteArray array) { @@ -147,7 +147,9 @@ namespace EonaCat.Quic.Infrastructure } if (result != null) + { result.Decode(_array); + } return result; } diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/AckFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/AckFrame.cs index f67c2d7..058fab6 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/AckFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/AckFrame.cs @@ -1,5 +1,5 @@ -using System; -using EonaCat.Quic.Helpers; +using EonaCat.Quic.Helpers; +using System; namespace EonaCat.Quic.Infrastructure.Frames { diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/ConnectionCloseFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/ConnectionCloseFrame.cs index afb4875..29f98eb 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/ConnectionCloseFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/ConnectionCloseFrame.cs @@ -1,6 +1,6 @@ -using System; +using EonaCat.Quic.Helpers; +using System; using System.Collections.Generic; -using EonaCat.Quic.Helpers; namespace EonaCat.Quic.Infrastructure.Frames { @@ -29,11 +29,11 @@ namespace EonaCat.Quic.Infrastructure.Frames { ActualType = 0x1c; - ErrorCode = (UInt64)error; - FrameType = new IntegerVar((UInt64)frameType); + ErrorCode = (ulong)error; + FrameType = new IntegerVar(frameType); if (!string.IsNullOrWhiteSpace(reason)) { - ReasonPhraseLength = new IntegerVar((UInt64)reason.Length); + ReasonPhraseLength = new IntegerVar((ulong)reason.Length); } else { @@ -70,7 +70,7 @@ namespace EonaCat.Quic.Infrastructure.Frames if (string.IsNullOrWhiteSpace(ReasonPhrase) == false) { - byte[] rpl = new IntegerVar((UInt64)ReasonPhrase.Length); + byte[] rpl = new IntegerVar((ulong)ReasonPhrase.Length); result.AddRange(rpl); byte[] reasonPhrase = ByteHelpers.GetBytes(ReasonPhrase); diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/CryptoFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/CryptoFrame.cs index 913e3e5..4235ef0 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/CryptoFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/CryptoFrame.cs @@ -1,5 +1,5 @@ -using System; -using EonaCat.Quic.Helpers; +using EonaCat.Quic.Helpers; +using System; namespace EonaCat.Quic.Infrastructure.Frames { diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/DataBlockedFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/DataBlockedFrame.cs index 790e21b..4f90abb 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/DataBlockedFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/DataBlockedFrame.cs @@ -1,6 +1,6 @@ -using System; +using EonaCat.Quic.Helpers; +using System; using System.Collections.Generic; -using EonaCat.Quic.Helpers; namespace EonaCat.Quic.Infrastructure.Frames { @@ -15,7 +15,7 @@ namespace EonaCat.Quic.Infrastructure.Frames { } - public DataBlockedFrame(UInt64 dataLimit) + public DataBlockedFrame(ulong dataLimit) { MaximumData = dataLimit; } diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxDataFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxDataFrame.cs index 35d622c..8185194 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxDataFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxDataFrame.cs @@ -1,12 +1,12 @@ -using System.Collections.Generic; -using EonaCat.Quic.Helpers; +using EonaCat.Quic.Helpers; +using System.Collections.Generic; namespace EonaCat.Quic.Infrastructure.Frames { public class MaxDataFrame : Frame { // This file is part of the EonaCat project(s) which is released under the Apache License. - // Copyright EonaCat (Jeroen Saey) + // Copyright EonaCat (Jeroen Saey) // See file LICENSE or go to https://EonaCat.com/License for full license details. public override byte Type => 0x10; diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxStreamDataFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxStreamDataFrame.cs index 3dd9dc2..b3f650c 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxStreamDataFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxStreamDataFrame.cs @@ -1,6 +1,6 @@ -using System; +using EonaCat.Quic.Helpers; +using System; using System.Collections.Generic; -using EonaCat.Quic.Helpers; namespace EonaCat.Quic.Infrastructure.Frames { @@ -19,7 +19,7 @@ namespace EonaCat.Quic.Infrastructure.Frames { } - public MaxStreamDataFrame(UInt64 streamId, UInt64 maximumStreamData) + public MaxStreamDataFrame(ulong streamId, ulong maximumStreamData) { StreamId = streamId; MaximumStreamData = maximumStreamData; diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxStreamsFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxStreamsFrame.cs index 9c46ac8..9144a48 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxStreamsFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxStreamsFrame.cs @@ -1,6 +1,6 @@ -using System; +using EonaCat.Quic.Helpers; +using System; using System.Collections.Generic; -using EonaCat.Quic.Helpers; namespace EonaCat.Quic.Infrastructure.Frames { @@ -15,7 +15,7 @@ namespace EonaCat.Quic.Infrastructure.Frames { } - public MaxStreamsFrame(UInt64 maximumStreamId, StreamType appliesTo) + public MaxStreamsFrame(ulong maximumStreamId, StreamType appliesTo) { MaximumStreams = new IntegerVar(maximumStreamId); } diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/NewConnectionIdFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/NewConnectionIdFrame.cs index 9c2c165..f5dae09 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/NewConnectionIdFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/NewConnectionIdFrame.cs @@ -1,5 +1,5 @@ -using System; -using EonaCat.Quic.Helpers; +using EonaCat.Quic.Helpers; +using System; namespace EonaCat.Quic.Infrastructure.Frames { diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/NewTokenFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/NewTokenFrame.cs index 9b2a9be..6637993 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/NewTokenFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/NewTokenFrame.cs @@ -1,5 +1,5 @@ -using System; -using EonaCat.Quic.Helpers; +using EonaCat.Quic.Helpers; +using System; namespace EonaCat.Quic.Infrastructure.Frames { diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/PaddingFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/PaddingFrame.cs index 8c23103..a7bb3b5 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/PaddingFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/PaddingFrame.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; -using EonaCat.Quic.Helpers; +using EonaCat.Quic.Helpers; +using System.Collections.Generic; namespace EonaCat.Quic.Infrastructure.Frames { diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/PathChallengeFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/PathChallengeFrame.cs index 6e17429..1326b47 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/PathChallengeFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/PathChallengeFrame.cs @@ -1,5 +1,5 @@ -using System; -using EonaCat.Quic.Helpers; +using EonaCat.Quic.Helpers; +using System; namespace EonaCat.Quic.Infrastructure.Frames { diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/PathResponseFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/PathResponseFrame.cs index 1a3aa7e..ece907d 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/PathResponseFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/PathResponseFrame.cs @@ -1,5 +1,5 @@ -using System; -using EonaCat.Quic.Helpers; +using EonaCat.Quic.Helpers; +using System; namespace EonaCat.Quic.Infrastructure.Frames { diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/PingFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/PingFrame.cs index 77afb8d..eb80c0d 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/PingFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/PingFrame.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; -using EonaCat.Quic.Helpers; +using EonaCat.Quic.Helpers; +using System.Collections.Generic; namespace EonaCat.Quic.Infrastructure.Frames { diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/ResetStreamFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/ResetStreamFrame.cs index 62e1c37..1ed9c2a 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/ResetStreamFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/ResetStreamFrame.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; -using EonaCat.Quic.Helpers; +using EonaCat.Quic.Helpers; +using System.Collections.Generic; namespace EonaCat.Quic.Infrastructure.Frames { diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/RetireConnectionIdFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/RetireConnectionIdFrame.cs index 05abcfa..33be918 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/RetireConnectionIdFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/RetireConnectionIdFrame.cs @@ -1,5 +1,5 @@ -using System; -using EonaCat.Quic.Helpers; +using EonaCat.Quic.Helpers; +using System; namespace EonaCat.Quic.Infrastructure.Frames { diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/StopSendingFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/StopSendingFrame.cs index 1f10cc3..9f2145a 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/StopSendingFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/StopSendingFrame.cs @@ -1,5 +1,5 @@ -using System; -using EonaCat.Quic.Helpers; +using EonaCat.Quic.Helpers; +using System; namespace EonaCat.Quic.Infrastructure.Frames { diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamDataBlockedFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamDataBlockedFrame.cs index ab27995..545066e 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamDataBlockedFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamDataBlockedFrame.cs @@ -1,6 +1,6 @@ -using System; +using EonaCat.Quic.Helpers; +using System; using System.Collections.Generic; -using EonaCat.Quic.Helpers; namespace EonaCat.Quic.Infrastructure.Frames { @@ -17,7 +17,7 @@ namespace EonaCat.Quic.Infrastructure.Frames { } - public StreamDataBlockedFrame(UInt64 streamId, UInt64 streamDataLimit) + public StreamDataBlockedFrame(ulong streamId, ulong streamDataLimit) { StreamId = streamId; MaximumStreamData = streamDataLimit; diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamFrame.cs index 831643d..5401840 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamFrame.cs @@ -1,6 +1,6 @@ -using System; +using EonaCat.Quic.Helpers; +using System; using System.Collections.Generic; -using EonaCat.Quic.Helpers; namespace EonaCat.Quic.Infrastructure.Frames { @@ -22,12 +22,12 @@ namespace EonaCat.Quic.Infrastructure.Frames { } - public StreamFrame(UInt64 streamId, byte[] data, UInt64 offset, bool eos) + public StreamFrame(ulong streamId, byte[] data, ulong offset, bool eos) { StreamId = streamId; StreamData = data; Offset = offset; - Length = (UInt64)data.Length; + Length = (ulong)data.Length; EndOfStream = eos; } @@ -41,11 +41,19 @@ namespace EonaCat.Quic.Infrastructure.Frames StreamId = array.ReadIntegerVar(); if (OFF_BIT > 0) + { Offset = array.ReadIntegerVar(); + } + if (LEN_BIT > 0) + { Length = array.ReadIntegerVar(); + } + if (FIN_BIT > 0) + { EndOfStream = true; + } StreamData = array.ReadBytes((int)Length.Value); } @@ -53,11 +61,19 @@ namespace EonaCat.Quic.Infrastructure.Frames public override byte[] Encode() { if (Offset != null && Offset.Value > 0) + { ActualType = (byte)(ActualType | 0x04); + } + if (Length != null && Length.Value > 0) + { ActualType = (byte)(ActualType | 0x02); + } + if (EndOfStream == true) + { ActualType = (byte)(ActualType | 0x01); + } byte OFF_BIT = (byte)(ActualType & 0x04); byte LEN_BIT = (byte)(ActualType & 0x02); @@ -69,10 +85,14 @@ namespace EonaCat.Quic.Infrastructure.Frames result.AddRange(streamId); if (OFF_BIT > 0) + { result.AddRange(Offset.ToByteArray()); + } if (LEN_BIT > 0) + { result.AddRange(Length.ToByteArray()); + } result.AddRange(StreamData); diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamsBlockedFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamsBlockedFrame.cs index ad6eaaa..767aaff 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamsBlockedFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamsBlockedFrame.cs @@ -1,5 +1,5 @@ -using System; -using EonaCat.Quic.Helpers; +using EonaCat.Quic.Helpers; +using System; namespace EonaCat.Quic.Infrastructure.Frames { diff --git a/EonaCat.Network/System/Quic/Infrastructure/NumberSpace.cs b/EonaCat.Network/System/Quic/Infrastructure/NumberSpace.cs index 574bbcc..749852d 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/NumberSpace.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/NumberSpace.cs @@ -7,14 +7,14 @@ namespace EonaCat.Quic.Infrastructure public class NumberSpace { - private UInt32 _max = UInt32.MaxValue; - private UInt32 _n = 0; + private readonly uint _max = uint.MaxValue; + private uint _n = 0; public NumberSpace() { } - public NumberSpace(UInt32 max) + public NumberSpace(uint max) { _max = max; } @@ -24,10 +24,12 @@ namespace EonaCat.Quic.Infrastructure return _n == _max; } - public UInt32 Get() + public uint Get() { if (_n >= _max) + { return 0; + } _n++; return _n; diff --git a/EonaCat.Network/System/Quic/Infrastructure/PacketProcessing/InitialPacketCreator.cs b/EonaCat.Network/System/Quic/Infrastructure/PacketProcessing/InitialPacketCreator.cs index 9917b22..d5858b7 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/PacketProcessing/InitialPacketCreator.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/PacketProcessing/InitialPacketCreator.cs @@ -22,7 +22,9 @@ namespace EonaCat.Quic.Infrastructure.PacketProcessing int padding = QuicSettings.PMTU - length; for (int i = 0; i < padding; i++) + { packet.AttachFrame(new PaddingFrame()); + } return packet; } diff --git a/EonaCat.Network/System/Quic/Infrastructure/PacketProcessing/PacketCreator.cs b/EonaCat.Network/System/Quic/Infrastructure/PacketProcessing/PacketCreator.cs index fcd52d1..9d5fad6 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/PacketProcessing/PacketCreator.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/PacketProcessing/PacketCreator.cs @@ -1,7 +1,7 @@ -using System; -using EonaCat.Quic.Helpers; +using EonaCat.Quic.Helpers; using EonaCat.Quic.Infrastructure.Frames; using EonaCat.Quic.Infrastructure.Packets; +using System; namespace EonaCat.Quic.Infrastructure.PacketProcessing { @@ -32,7 +32,7 @@ namespace EonaCat.Quic.Infrastructure.PacketProcessing return packet; } - public ShortHeaderPacket CreateDataPacket(UInt64 streamId, byte[] data, UInt64 offset, bool eos) + public ShortHeaderPacket CreateDataPacket(ulong streamId, byte[] data, ulong offset, bool eos) { ShortHeaderPacket packet = new ShortHeaderPacket(_peerConnectionId.Size); packet.PacketNumber = _ns.Get(); diff --git a/EonaCat.Network/System/Quic/Infrastructure/PacketType.cs b/EonaCat.Network/System/Quic/Infrastructure/PacketType.cs index 2ac7a91..7d27293 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/PacketType.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/PacketType.cs @@ -5,7 +5,7 @@ namespace EonaCat.Quic.Infrastructure // 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 PacketType : UInt16 + public enum PacketType : ushort { Initial = 0x0, ZeroRTTProtected = 0x1, diff --git a/EonaCat.Network/System/Quic/Infrastructure/Packets/InitialPacket.cs b/EonaCat.Network/System/Quic/Infrastructure/Packets/InitialPacket.cs index 50b9180..b6a0f34 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Packets/InitialPacket.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Packets/InitialPacket.cs @@ -1,6 +1,6 @@ -using System; +using EonaCat.Quic.Helpers; +using System; using System.Collections.Generic; -using EonaCat.Quic.Helpers; namespace EonaCat.Quic.Infrastructure.Packets { @@ -44,15 +44,21 @@ namespace EonaCat.Quic.Infrastructure.Packets DestinationConnectionIdLength = array.ReadByte(); if (DestinationConnectionIdLength > 0) + { DestinationConnectionId = array.ReadGranularInteger(DestinationConnectionIdLength); + } SourceConnectionIdLength = array.ReadByte(); if (SourceConnectionIdLength > 0) + { SourceConnectionId = array.ReadGranularInteger(SourceConnectionIdLength); + } TokenLength = array.ReadIntegerVar(); if (TokenLength > 0) + { Token = array.ReadBytes(TokenLength); + } Length = array.ReadIntegerVar(); PacketNumber = array.ReadGranularInteger(pnSize); @@ -72,13 +78,18 @@ namespace EonaCat.Quic.Infrastructure.Packets result.Add(DestinationConnectionId.Size); if (DestinationConnectionId.Size > 0) + { result.AddRange(DestinationConnectionId.ToByteArray()); + } + result.Add(SourceConnectionId.Size); if (SourceConnectionId.Size > 0) + { result.AddRange(SourceConnectionId.ToByteArray()); + } byte[] tokenLength = new IntegerVar(0); - byte[] length = new IntegerVar(PacketNumber.Size + (UInt64)frames.Length); + byte[] length = new IntegerVar(PacketNumber.Size + (ulong)frames.Length); result.AddRange(tokenLength); result.AddRange(length); diff --git a/EonaCat.Network/System/Quic/Infrastructure/Packets/LongHeaderPacket.cs b/EonaCat.Network/System/Quic/Infrastructure/Packets/LongHeaderPacket.cs index b70938a..5c34fdb 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Packets/LongHeaderPacket.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Packets/LongHeaderPacket.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; -using EonaCat.Quic.Helpers; +using EonaCat.Quic.Helpers; +using System.Collections.Generic; namespace EonaCat.Quic.Infrastructure.Packets { @@ -42,11 +42,15 @@ namespace EonaCat.Quic.Infrastructure.Packets DestinationConnectionIdLength = array.ReadByte(); if (DestinationConnectionIdLength > 0) + { DestinationConnectionId = array.ReadGranularInteger(DestinationConnectionIdLength); + } SourceConnectionIdLength = array.ReadByte(); if (SourceConnectionIdLength > 0) + { SourceConnectionId = array.ReadGranularInteger(SourceConnectionIdLength); + } this.DecodeFrames(array); } @@ -62,11 +66,15 @@ namespace EonaCat.Quic.Infrastructure.Packets result.Add(DestinationConnectionId.Size); if (DestinationConnectionId.Size > 0) + { result.AddRange(DestinationConnectionId.ToByteArray()); + } result.Add(SourceConnectionId.Size); if (SourceConnectionId.Size > 0) + { result.AddRange(SourceConnectionId.ToByteArray()); + } result.AddRange(frames); diff --git a/EonaCat.Network/System/Quic/Infrastructure/Packets/Packet.cs b/EonaCat.Network/System/Quic/Infrastructure/Packets/Packet.cs index fff6728..49d3625 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Packets/Packet.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Packets/Packet.cs @@ -1,9 +1,9 @@ -using System; -using System.Collections.Generic; -using EonaCat.Quic.Helpers; +using EonaCat.Quic.Helpers; using EonaCat.Quic.Infrastructure.Exceptions; using EonaCat.Quic.Infrastructure.Frames; using EonaCat.Quic.Infrastructure.Settings; +using System; +using System.Collections.Generic; namespace EonaCat.Quic.Infrastructure.Packets { @@ -18,7 +18,7 @@ namespace EonaCat.Quic.Infrastructure.Packets protected List _frames = new List(); public abstract byte Type { get; } - public UInt32 Version { get; set; } + public uint Version { get; set; } public abstract byte[] Encode(); @@ -43,13 +43,17 @@ namespace EonaCat.Quic.Infrastructure.Packets { result = factory.GetFrame(); if (result != null) + { _frames.Add(result); + } frames++; } if (array.HasData()) + { throw new ProtocolException("Unexpected number of frames or possibly corrupted frame was sent."); + } } public virtual byte[] EncodeFrames() diff --git a/EonaCat.Network/System/Quic/Infrastructure/Packets/ShortHeaderPacket.cs b/EonaCat.Network/System/Quic/Infrastructure/Packets/ShortHeaderPacket.cs index e874177..fe25cdd 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Packets/ShortHeaderPacket.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Packets/ShortHeaderPacket.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; -using EonaCat.Quic.Helpers; +using EonaCat.Quic.Helpers; +using System.Collections.Generic; namespace EonaCat.Quic.Infrastructure.Packets { diff --git a/EonaCat.Network/System/Quic/Infrastructure/Packets/Unpacker.cs b/EonaCat.Network/System/Quic/Infrastructure/Packets/Unpacker.cs index 764706c..a4bff1c 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Packets/Unpacker.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Packets/Unpacker.cs @@ -23,7 +23,9 @@ } if (result == null) + { return null; + } result.Decode(data); @@ -33,18 +35,31 @@ public QuicPacketType GetPacketType(byte[] data) { if (data == null || data.Length <= 0) + { return QuicPacketType.Broken; + } byte type = data[0]; if ((type & 0xC0) == 0xC0) + { return QuicPacketType.Initial; + } + if ((type & 0x40) == 0x40) + { return QuicPacketType.ShortHeader; + } + if ((type & 0x80) == 0x80) + { return QuicPacketType.VersionNegotiation; + } + if ((type & 0xE0) == 0xE0) + { return QuicPacketType.LongHeader; + } return QuicPacketType.Broken; } diff --git a/EonaCat.Network/System/Quic/Infrastructure/Settings/QuicVersion.cs b/EonaCat.Network/System/Quic/Infrastructure/Settings/QuicVersion.cs index 418d845..a3548e7 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Settings/QuicVersion.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Settings/QuicVersion.cs @@ -10,6 +10,6 @@ namespace EonaCat.Quic.Infrastructure.Settings { public const int CurrentVersion = 16; - public static readonly List SupportedVersions = new List() { 15, 16 }; + public static readonly List SupportedVersions = new List() { 15, 16 }; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/InternalInfrastructure/PacketWireTransfer.cs b/EonaCat.Network/System/Quic/InternalInfrastructure/PacketWireTransfer.cs index 81870da..8d7b4c4 100644 --- a/EonaCat.Network/System/Quic/InternalInfrastructure/PacketWireTransfer.cs +++ b/EonaCat.Network/System/Quic/InternalInfrastructure/PacketWireTransfer.cs @@ -1,7 +1,7 @@ -using System.Net; -using System.Net.Sockets; -using EonaCat.Quic.Exceptions; +using EonaCat.Quic.Exceptions; using EonaCat.Quic.Infrastructure.Packets; +using System.Net; +using System.Net.Sockets; namespace EonaCat.Quic.InternalInfrastructure { @@ -10,10 +10,10 @@ namespace EonaCat.Quic.InternalInfrastructure internal class PacketWireTransfer { - private UdpClient _client; + private readonly UdpClient _client; private IPEndPoint _peerEndpoint; - private Unpacker _unpacker; + private readonly Unpacker _unpacker; public PacketWireTransfer(UdpClient client, IPEndPoint peerEndpoint) { @@ -28,7 +28,9 @@ namespace EonaCat.Quic.InternalInfrastructure // Await response for sucessfull connection creation by the server byte[] peerData = _client.Receive(ref _peerEndpoint); if (peerData == null) + { throw new ConnectionException("Server did not respond properly."); + } Packet packet = _unpacker.Unpack(peerData); diff --git a/EonaCat.Network/System/Quic/QuicClient.cs b/EonaCat.Network/System/Quic/QuicClient.cs index 625815a..0aac34f 100644 --- a/EonaCat.Network/System/Quic/QuicClient.cs +++ b/EonaCat.Network/System/Quic/QuicClient.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using EonaCat.Quic.Connections; +using EonaCat.Quic.Connections; using EonaCat.Quic.Exceptions; using EonaCat.Quic.Helpers; using EonaCat.Quic.Infrastructure.Frames; @@ -11,6 +6,11 @@ using EonaCat.Quic.Infrastructure.PacketProcessing; using EonaCat.Quic.Infrastructure.Packets; using EonaCat.Quic.Infrastructure.Settings; using EonaCat.Quic.InternalInfrastructure; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; namespace EonaCat.Quic { @@ -23,12 +23,12 @@ namespace EonaCat.Quic public class QuicClient : QuicTransport { private IPEndPoint _peerIp; - private UdpClient _client; + private readonly UdpClient _client; private QuicConnection _connection; - private InitialPacketCreator _packetCreator; + private readonly InitialPacketCreator _packetCreator; - private UInt64 _maximumStreams = QuicSettings.MaximumStreamId; + private ulong _maximumStreams = QuicSettings.MaximumStreamId; private PacketWireTransfer _pwt; public QuicClient() @@ -98,7 +98,9 @@ namespace EonaCat.Quic // Break out if the first Padding Frame has been reached if (frame is PaddingFrame) + { break; + } } } diff --git a/EonaCat.Network/System/Quic/QuicServer.cs b/EonaCat.Network/System/Quic/QuicServer.cs index db1e251..10c13b0 100644 --- a/EonaCat.Network/System/Quic/QuicServer.cs +++ b/EonaCat.Network/System/Quic/QuicServer.cs @@ -1,8 +1,4 @@ -using System; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using EonaCat.Quic.Connections; +using EonaCat.Quic.Connections; using EonaCat.Quic.Constants; using EonaCat.Quic.Events; using EonaCat.Quic.Helpers; @@ -12,6 +8,10 @@ using EonaCat.Quic.Infrastructure.PacketProcessing; using EonaCat.Quic.Infrastructure.Packets; using EonaCat.Quic.Infrastructure.Settings; using EonaCat.Quic.InternalInfrastructure; +using System; +using System.Linq; +using System.Net; +using System.Net.Sockets; namespace EonaCat.Quic { @@ -30,7 +30,7 @@ namespace EonaCat.Quic private UdpClient _client; - private int _port; + private readonly int _port; private readonly string _hostname; private bool _started; @@ -93,7 +93,9 @@ namespace EonaCat.Quic public void Close() { if (_started) + { _client.Close(); + } } /// @@ -105,7 +107,7 @@ namespace EonaCat.Quic private QuicConnection ProcessInitialPacket(Packet packet, IPEndPoint endPoint) { QuicConnection result = null; - UInt64 availableConnectionId; + ulong availableConnectionId; byte[] data; // Unsupported version. Version negotiation packet is sent only on initial connection. All other packets are dropped. (5.2.2 / 16th draft) if (packet.Version != QuicVersion.CurrentVersion || !QuicVersion.SupportedVersions.Contains(packet.Version)) @@ -146,7 +148,9 @@ namespace EonaCat.Quic data = ip.Encode(); int dataSent = _client.Send(data, data.Length, endPoint); if (dataSent > 0) + { return result; + } return null; } diff --git a/EonaCat.Network/System/Quic/QuicTransport.cs b/EonaCat.Network/System/Quic/QuicTransport.cs index d77fc28..2bd8d08 100644 --- a/EonaCat.Network/System/Quic/QuicTransport.cs +++ b/EonaCat.Network/System/Quic/QuicTransport.cs @@ -20,7 +20,9 @@ namespace EonaCat.Quic // No suitable connection found. Discard the packet. if (connection == null) + { return; + } connection.ProcessFrames(shp.GetFrames()); } diff --git a/EonaCat.Network/System/Quic/Streams/QuicStream.cs b/EonaCat.Network/System/Quic/Streams/QuicStream.cs index f497555..7dfaa60 100644 --- a/EonaCat.Network/System/Quic/Streams/QuicStream.cs +++ b/EonaCat.Network/System/Quic/Streams/QuicStream.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using EonaCat.Quic.Connections; +using EonaCat.Quic.Connections; using EonaCat.Quic.Constants; using EonaCat.Quic.Events; using EonaCat.Quic.Exceptions; @@ -9,6 +6,9 @@ using EonaCat.Quic.Helpers; using EonaCat.Quic.Infrastructure.Frames; using EonaCat.Quic.Infrastructure.Packets; using EonaCat.Quic.Infrastructure.Settings; +using System; +using System.Collections.Generic; +using System.Linq; namespace EonaCat.Quic.Streams { @@ -20,11 +20,11 @@ namespace EonaCat.Quic.Streams /// public class QuicStream { - private SortedList _data = new SortedList(); - private QuicConnection _connection; - private UInt64 _maximumStreamData; - private UInt64 _currentTransferRate; - private UInt64 _sendOffset; + private readonly SortedList _data = new SortedList(); + private readonly QuicConnection _connection; + private ulong _maximumStreamData; + private ulong _currentTransferRate; + private ulong _sendOffset; public StreamState State { get; set; } public StreamType Type { get; set; } @@ -32,13 +32,7 @@ namespace EonaCat.Quic.Streams public StreamDataReceivedEvent OnStreamDataReceived { get; set; } - public byte[] Data - { - get - { - return _data.SelectMany(v => v.Value).ToArray(); - } - } + public byte[] Data => _data.SelectMany(v => v.Value).ToArray(); public QuicStream(QuicConnection connection, StreamId streamId) { @@ -55,7 +49,9 @@ namespace EonaCat.Quic.Streams public bool Send(byte[] data) { if (Type == StreamType.ServerUnidirectional) + { throw new StreamException("Cannot send data on unidirectional stream."); + } _connection.IncrementRate(data.Length); @@ -73,16 +69,20 @@ namespace EonaCat.Quic.Streams } byte[] buffer = new byte[dataSize]; - Buffer.BlockCopy(data, (Int32)_sendOffset, buffer, 0, dataSize); + Buffer.BlockCopy(data, (int)_sendOffset, buffer, 0, dataSize); ShortHeaderPacket packet = _connection.PacketCreator.CreateDataPacket(this.StreamId.IntegerValue, buffer, _sendOffset, eos); if (i == 0 && data.Length >= QuicSettings.MaxStreamData) - packet.AttachFrame(new MaxStreamDataFrame(this.StreamId.IntegerValue, (UInt64)(data.Length + 1))); + { + packet.AttachFrame(new MaxStreamDataFrame(this.StreamId.IntegerValue, (ulong)(data.Length + 1))); + } if (_connection.MaximumReached()) - packet.AttachFrame(new StreamDataBlockedFrame(StreamId.IntegerValue, (UInt64)data.Length)); + { + packet.AttachFrame(new StreamDataBlockedFrame(StreamId.IntegerValue, (ulong)data.Length)); + } - _sendOffset += (UInt64)buffer.Length; + _sendOffset += (ulong)buffer.Length; _connection.SendData(packet); } @@ -97,7 +97,9 @@ namespace EonaCat.Quic.Streams public byte[] Receive() { if (Type == StreamType.ClientUnidirectional) + { throw new StreamException("Cannot receive data on unidirectional stream."); + } while (!IsStreamFull() || State == StreamState.Receive) { @@ -115,7 +117,7 @@ namespace EonaCat.Quic.Streams _data.Clear(); } - public void SetMaximumStreamData(UInt64 maximumData) + public void SetMaximumStreamData(ulong maximumData) { _maximumStreamData = maximumData; } @@ -123,10 +125,14 @@ namespace EonaCat.Quic.Streams public bool CanSendData() { if (Type == StreamType.ServerUnidirectional || Type == StreamType.ClientUnidirectional) + { return false; + } if (State == StreamState.Receive || State == StreamState.SizeKnown) + { return true; + } return false; } @@ -134,7 +140,9 @@ namespace EonaCat.Quic.Streams public bool IsOpen() { if (State == StreamState.DataReceived || State == StreamState.ResetReceived) + { return false; + } return true; } @@ -143,7 +151,9 @@ namespace EonaCat.Quic.Streams { // Do not accept data if the stream is reset. if (State == StreamState.ResetReceived) + { return; + } byte[] data = frame.StreamData; if (frame.Offset != null) @@ -158,9 +168,11 @@ namespace EonaCat.Quic.Streams // Either this frame marks the end of the stream, // or fin frame came before the data frames if (frame.EndOfStream) + { State = StreamState.SizeKnown; + } - _currentTransferRate += (UInt64)data.Length; + _currentTransferRate += (ulong)data.Length; // Terminate connection if maximum stream data is reached if (_currentTransferRate >= _maximumStreamData) @@ -187,14 +199,16 @@ namespace EonaCat.Quic.Streams private bool IsStreamFull() { - UInt64 length = 0; + ulong length = 0; foreach (var kvp in _data) { if (kvp.Key > 0 && kvp.Key != length) + { return false; + } - length += (UInt64)kvp.Value.Length; + length += (ulong)kvp.Value.Length; } return true; diff --git a/EonaCat.Network/System/Sockets/RemoteInfo.cs b/EonaCat.Network/System/Sockets/RemoteInfo.cs index 8cbf966..26508b7 100644 --- a/EonaCat.Network/System/Sockets/RemoteInfo.cs +++ b/EonaCat.Network/System/Sockets/RemoteInfo.cs @@ -1,4 +1,7 @@ -using System.Net; +// 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.Sockets; namespace EonaCat.Network @@ -17,4 +20,4 @@ namespace EonaCat.Network public string ClientId { get; internal set; } public string ClientName { get; internal set; } } -} +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Tcp/SocketTcpClient.cs b/EonaCat.Network/System/Sockets/Tcp/SocketTcpClient.cs index af498b3..d46a65f 100644 --- a/EonaCat.Network/System/Sockets/Tcp/SocketTcpClient.cs +++ b/EonaCat.Network/System/Sockets/Tcp/SocketTcpClient.cs @@ -1,12 +1,10 @@ using System; -using System.ComponentModel; using System.Net; using System.Net.Sockets; using System.Threading.Tasks; namespace EonaCat.Network { - public class SocketTcpClient { private const int BUFFER_SIZE = 4096; @@ -44,7 +42,7 @@ namespace EonaCat.Network { if (!IPAddress.TryParse(ipAddress, out IPAddress ip)) { - throw new Exception("Invalid ipAddress given"); + throw new Exception("EonaCat Network: Invalid ipAddress given"); } return CreateSocketTcpClientAsync(ip, port); @@ -121,4 +119,4 @@ namespace EonaCat.Network socket.Close(); } } -} +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Tcp/SocketTcpServer.cs b/EonaCat.Network/System/Sockets/Tcp/SocketTcpServer.cs index 62811a8..75297b1 100644 --- a/EonaCat.Network/System/Sockets/Tcp/SocketTcpServer.cs +++ b/EonaCat.Network/System/Sockets/Tcp/SocketTcpServer.cs @@ -1,8 +1,8 @@ -using System.Net.Sockets; +using System; using System.Net; -using System; -using System.Threading.Tasks; +using System.Net.Sockets; using System.Threading; +using System.Threading.Tasks; namespace EonaCat.Network { diff --git a/EonaCat.Network/System/Sockets/Udp/SocketUdpClient.cs b/EonaCat.Network/System/Sockets/Udp/SocketUdpClient.cs index cbb52f1..b2b6949 100644 --- a/EonaCat.Network/System/Sockets/Udp/SocketUdpClient.cs +++ b/EonaCat.Network/System/Sockets/Udp/SocketUdpClient.cs @@ -1,8 +1,8 @@ -using System.Net.Sockets; +using System; using System.Net; -using System.Threading.Tasks; -using System; +using System.Net.Sockets; using System.Threading; +using System.Threading.Tasks; namespace EonaCat.Network { @@ -112,4 +112,4 @@ namespace EonaCat.Network _udpClient.Close(); } } -} +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Udp/SocketUdpServer.cs b/EonaCat.Network/System/Sockets/Udp/SocketUdpServer.cs index 263723f..d0c55ed 100644 --- a/EonaCat.Network/System/Sockets/Udp/SocketUdpServer.cs +++ b/EonaCat.Network/System/Sockets/Udp/SocketUdpServer.cs @@ -1,10 +1,10 @@ -using System.Net.Sockets; +using System; using System.Net; -using System; -using System.Runtime.InteropServices; using System.Net.NetworkInformation; -using System.Threading.Tasks; +using System.Net.Sockets; +using System.Runtime.InteropServices; using System.Threading; +using System.Threading.Tasks; namespace EonaCat.Network { @@ -57,7 +57,7 @@ namespace EonaCat.Network { if (!IPAddress.TryParse(ipAddress, out IPAddress ip)) { - throw new Exception("Invalid ipAddress given"); + throw new Exception("EonaCat Network: Invalid ipAddress given"); } CreateUdpServer(ip, port); @@ -148,5 +148,4 @@ namespace EonaCat.Network } } } - -} +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/ByteOrder.cs b/EonaCat.Network/System/Sockets/Web/ByteOrder.cs new file mode 100644 index 0000000..5e784d6 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/ByteOrder.cs @@ -0,0 +1,12 @@ +// 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 new file mode 100644 index 0000000..01ffeae --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/CloseStatusCode.cs @@ -0,0 +1,34 @@ +// 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 + { + Normal = 1000, + + Away = 1001, + + ProtocolError = 1002, + + UnsupportedData = 1003, + + Undefined = 1004, + + NoStatus = 1005, + + Abnormal = 1006, + + InvalidData = 1007, + + PolicyViolation = 1008, + + TooBig = 1009, + + MandatoryExtension = 1010, + + ServerError = 1011, + + 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 new file mode 100644 index 0000000..ada0444 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/CompressionMethod.cs @@ -0,0 +1,12 @@ +// 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 new file mode 100644 index 0000000..2ca1941 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationBase.cs @@ -0,0 +1,129 @@ +// 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 res = new NameValueCollection(); + foreach (var param in value.SplitHeaderValue(',')) + { + var i = param.IndexOf('='); + var name = i > 0 ? param.Substring(0, i).Trim() : null; + var val = i < 0 + ? param.Trim().Trim('"') + : i < param.Length - 1 + ? param.Substring(i + 1).Trim().Trim('"') + : string.Empty; + + res.Add(name, val); + } + + return res; + } + + /// + /// 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 new file mode 100644 index 0000000..7b98248 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationChallenge.cs @@ -0,0 +1,153 @@ +// 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. + /// + /// The authentication scheme. + /// The collection of authentication parameters. + private AuthenticationChallenge(AuthenticationSchemes scheme, NameValueCollection parameters) + : base(scheme, parameters) + { + } + + /// + /// 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"; + } + } + + /// + /// 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"]); + } + + /// + /// Gets the Digest authentication string representation of the authentication challenge. + /// + /// A string representing the Digest authentication challenge. + internal override string ToDigestString() + { + var output = new StringBuilder(DIGEST_SIZE); + + var domain = Parameters["domain"]; + if (domain != null) + { + output.AppendFormat($"Digest realm=\"{Parameters["realm"]}\", domain=\"{domain}\", nonce=\"{Parameters["nonce"]}\""); + } + else + { + output.AppendFormat($"Digest realm=\"{Parameters["realm"]}\", nonce=\"{Parameters["nonce"]}\""); + } + + var opaque = Parameters["opaque"]; + if (opaque != null) + { + output.AppendFormat($", opaque=\"{opaque}\""); + } + + var stale = Parameters["stale"]; + if (stale != null) + { + output.AppendFormat($", stale={stale}"); + } + + var algorithm = Parameters["algorithm"]; + if (algorithm != null) + { + output.AppendFormat($", algorithm={algorithm}"); + } + + var qop = Parameters["qop"]; + if (qop != null) + { + output.AppendFormat($", qop=\"{qop}\""); + } + + return output.ToString(); + } + } +} \ 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 new file mode 100644 index 0000000..0bdf2d5 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationResponse.cs @@ -0,0 +1,357 @@ +// 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. + /// + /// The authentication scheme. + /// The collection of authentication parameters. + private AuthenticationResponse(AuthenticationSchemes scheme, NameValueCollection parameters) + : base(scheme, parameters) + { + } + + /// + /// 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(); + } + } + + /// + /// Gets the nonce count. + /// + internal uint NonceCount => _nonceCount < uint.MaxValue + ? _nonceCount + : 0; + + /// + /// 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"]; + + /// + /// 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); + } + + /// + /// 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(); + res["username"] = user; + res["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(); + } + + /// + /// 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; + } + } +} \ 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 new file mode 100644 index 0000000..be5cbc0 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Authentication/AuthenticationSchemes.cs @@ -0,0 +1,31 @@ +// 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 new file mode 100644 index 0000000..ded296b --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Authentication/NetworkCredential.cs @@ -0,0 +1,113 @@ +// 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 string _domain; + private static readonly string[] _noRoles; + 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 new file mode 100644 index 0000000..3942cfb --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Chunks/Chunk.cs @@ -0,0 +1,56 @@ +// 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 new file mode 100644 index 0000000..a7a9e4a --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Chunks/ChunkStream.cs @@ -0,0 +1,398 @@ +// 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 int _chunkRead; + private int _chunkSize; + private readonly List _chunks; + private bool _foundSPCode; + private readonly StringBuilder _saved; + 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 web headers associated with the chunk stream. + /// + internal WebHeaderCollection Headers { get; } + + /// + /// 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; + + private int read(byte[] buffer, int offset, int count) + { + var nread = 0; + + var cnt = _chunks.Count; + for (var i = 0; i < cnt; i++) + { + var chunk = _chunks[i]; + if (chunk == null) + { + continue; + } + + if (chunk.ReadLeft == 0) + { + _chunks[i] = null; + continue; + } + + nread += chunk.Read(buffer, offset + nread, count - nread); + if (nread == count) + { + break; + } + } + + return nread; + } + + private static string RemoveChunkExtension(string value) + { + var index = value.IndexOf(';'); + return index > -1 ? value.Substring(0, index) : value; + } + + 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 static void ThrowProtocolViolation(string message) + { + throw new WebException($"EonaCat Network: {message}", null, WebExceptionStatus.ServerProtocolViolation, null); + } + + private void Write(byte[] buffer, ref int offset, int length) + { + if (_state == InputChunkState.End) + { + ThrowProtocolViolation("The chunks were ended."); + } + + if (_state == InputChunkState.None) + { + _state = SetChunkSize(buffer, ref offset, length); + if (_state == InputChunkState.None) + { + return; + } + + _saved.Length = 0; + _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; + } + + /// + /// 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); + } + + /// + /// 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); + } + } +} \ 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 new file mode 100644 index 0000000..e76eb43 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Chunks/ChunkedRequestStream.cs @@ -0,0 +1,208 @@ +// 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; } + + 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); + } + } + + /// + /// 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); + } + } +} \ 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 new file mode 100644 index 0000000..692a5d1 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Collections/QueryStringCollection.cs @@ -0,0 +1,41 @@ +// 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 new file mode 100644 index 0000000..f9c43ca --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Collections/WebHeaderCollection.cs @@ -0,0 +1,603 @@ +// 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 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) } + }; + } + + /// + /// 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 WebHeaderCollection() + { + } + + internal HttpHeaderType State => _state; + + public override string[] AllKeys => base.AllKeys; + + public override int Count => base.Count; + + 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 override KeysCollection Keys => base.Keys; + + private void add(string name, string value, bool ignoreRestricted) + { + var act = ignoreRestricted + ? (Action)addWithoutCheckingNameAndRestricted + : addWithoutCheckingName; + + DoWithCheckingState(act, CheckName(name), value, true); + } + + private void addWithoutCheckingName(string name, string value) + { + DoWithoutCheckingName(base.Add, name, value); + } + + private void addWithoutCheckingNameAndRestricted(string name, string value) + { + base.Add(name, CheckValue(value)); + } + + private static int checkColonSeparated(string header) + { + var idx = header.IndexOf(':'); + if (idx == -1) + { + throw new ArgumentException("No colon could be found.", 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 void CheckRestricted(string name) + { + if (!_internallyUsed && isRestricted(name, true)) + { + throw new ArgumentException("This header must be modified with the appropiate property."); + } + } + + private void CheckState(bool response) + { + if (_state == HttpHeaderType.Unspecified) + { + return; + } + + if (response && _state == HttpHeaderType.Request) + { + throw new InvalidOperationException( + "This collection has already been used to store the request headers."); + } + + if (!response && _state == HttpHeaderType.Response) + { + throw new InvalidOperationException( + "This collection has already been used to store the response headers."); + } + } + + private static string CheckValue(string value) + { + if (value == null || value.Length == 0) + { + return string.Empty; + } + + value = value.Trim(); + if (value.Length > 65535) + { + throw new ArgumentOutOfRangeException(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) + { + HttpHeaderInfo info; + return _headers.TryGetValue(key, out info) ? info.Name : string.Empty; + } + + private void DoWithCheckingState( + Action action, string name, string value, bool setState) + { + var type = CheckHeaderType(name); + if (type == HttpHeaderType.Request) + { + DoWithCheckingState(action, name, value, false, setState); + } + else if (type == HttpHeaderType.Response) + { + DoWithCheckingState(action, name, value, true, setState); + } + else + { + action(name, value); + } + } + + private void DoWithCheckingState( + Action action, string name, string value, bool response, bool setState) + { + CheckState(response); + action(name, value); + if (setState && _state == HttpHeaderType.Unspecified) + { + _state = response ? HttpHeaderType.Response : HttpHeaderType.Request; + } + } + + private void DoWithoutCheckingName(Action action, string name, string value) + { + CheckRestricted(name); + action(name, CheckValue(value)); + } + + private static HttpHeaderInfo GetHeaderInfo(string name) + { + foreach (var info in _headers.Values) + { + if (info.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) + { + return info; + } + } + + return null; + } + + private static bool isRestricted(string name, bool response) + { + var info = GetHeaderInfo(name); + return info != null && info.IsRestricted(response); + } + + private void removeWithoutCheckingName(string name, string unuse) + { + CheckRestricted(name); + base.Remove(name); + } + + private void setWithoutCheckingName(string name, string value) + { + DoWithoutCheckingName(base.Set, name, value); + } + + /// + /// 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 void InternalRemove(string name) + { + base.Remove(name); + } + + internal void InternalSet(string header, bool response) + { + var pos = checkColonSeparated(header); + InternalSet(header.Substring(0, pos), header.Substring(pos + 1), response); + } + + internal void InternalSet(string name, string value, bool response) + { + value = CheckValue(value); + if (IsMultiValue(name, response)) + { + base.Add(name, value); + } + else + { + base.Set(name, value); + } + } + + internal static bool IsHeaderName(string name) + { + return name != null && name.Length > 0 && name.IsToken(); + } + + internal static bool IsHeaderValue(string value) + { + return value.IsText(); + } + + internal static bool IsMultiValue(string headerName, bool response) + { + if (headerName == null || headerName.Length == 0) + { + return false; + } + + var info = GetHeaderInfo(headerName); + return info != null && info.IsMultiValue(response); + } + + internal string ToStringMultiValue(bool response) + { + var buff = new StringBuilder(); + Count.Times( + i => + { + var key = GetKey(i); + if (IsMultiValue(key, response)) + { + foreach (var val in GetValues(i)) + { + buff.AppendFormat($"{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); + } + + 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); + } + + 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; + } + + [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)); + }); + } + + 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 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(); + } + + [SecurityPermission( + SecurityAction.LinkDemand, + Flags = SecurityPermissionFlag.SerializationFormatter, + SerializationFormatter = true)] + void ISerializable.GetObjectData( + SerializationInfo serializationInfo, StreamingContext streamingContext) + { + GetObjectData(serializationInfo, streamingContext); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Context/HttpListenerWebSocketContext.cs b/EonaCat.Network/System/Sockets/Web/Core/Context/HttpListenerWebSocketContext.cs new file mode 100644 index 0000000..11d4e91 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Context/HttpListenerWebSocketContext.cs @@ -0,0 +1,111 @@ +// 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 HttpListenerWebSocketContext : WebSocketContext + { + private readonly HttpListenerContext _context; + private readonly WebSocket _websocket; + + /// + /// Initializes a new instance of the class. + /// + /// The associated with the WebSocket connection. + /// The WebSocket protocol negotiated during the connection. + internal HttpListenerWebSocketContext(HttpListenerContext context, string protocol) + { + _context = context; + _websocket = new WebSocket(this, protocol); + } + + /// + /// Gets the stream of the underlying TCP connection. + /// + internal Stream Stream => _context.Connection.Stream; + + 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 WebSocket WebSocket => _websocket; + + /// + /// 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); + } + + /// + public override string ToString() + { + return _context.Request.ToString(); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Context/TcpListenerWebSocketContext.cs b/EonaCat.Network/System/Sockets/Web/Core/Context/TcpListenerWebSocketContext.cs new file mode 100644 index 0000000..e5cf357 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Context/TcpListenerWebSocketContext.cs @@ -0,0 +1,231 @@ +// 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 TcpListenerWebSocketContext : WebSocketContext + { + private CookieCollection _cookies; + private NameValueCollection _queryString; + private WebRequest _request; + private readonly bool _secure; + private readonly TcpClient _tcpClient; + private readonly Uri _uri; + private IPrincipal _user; + private readonly WebSocket _websocket; + + /// + /// 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 configuration for secure connections. + /// The logger for logging. + internal TcpListenerWebSocketContext( + TcpClient tcpClient, + string protocol, + bool secure, + SSLConfigurationServer 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 WebSocket(this, protocol); + } + + /// + /// Gets the stream of the underlying TCP connection. + /// + internal Stream Stream { get; } + + 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 != null ? _uri.Query : null, 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 WebSocket WebSocket => _websocket; + + /// + /// 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); + } + + /// + public override string ToString() + { + return _request.ToString(); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/Core/Context/WebSocketContext.cs b/EonaCat.Network/System/Sockets/Web/Core/Context/WebSocketContext.cs new file mode 100644 index 0000000..d6c45c0 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Context/WebSocketContext.cs @@ -0,0 +1,112 @@ +// 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 WebSocketContext + { + /// + /// Initializes a new instance of the class. + /// + protected WebSocketContext() + { + } + + /// + /// 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 WebSocket 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 new file mode 100644 index 0000000..e53d0fa --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Cookies/Cookie.cs @@ -0,0 +1,593 @@ +// 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 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 static readonly char[] _reservedCharsForName; + private static readonly char[] _reservedCharsForValue; + private bool _secure; + private readonly DateTime _timestamp; + 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; + } + + 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 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 + { + string msg; + if (!canSetName(value, out msg)) + { + throw new CookieException(msg); + } + + _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."); + } + + string err; + if (!tryCreatePorts(value, out _ports, out err)) + { + throw new CookieException( + string.Format( + "The value specified for the Port attribute contains an invalid value: {0}", err)); + } + + _port = value; + } + } + + public bool Secure + { + get + { + return _secure; + } + + set + { + _secure = value; + } + } + + public DateTime TimeStamp => _timestamp; + + public string Value + { + get + { + return _value; + } + + set + { + string msg; + if (!canSetValue(value, out msg)) + { + throw new CookieException(msg); + } + + _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; + } + } + + 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 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(); + } + + 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; + } + + // 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; + } + + /// + 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); + } + } +} \ 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 new file mode 100644 index 0000000..36e27ca --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Cookies/CookieCollection.cs @@ -0,0 +1,547 @@ +// 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 _sync; + + /// + /// Initializes a new instance of the class. + /// + public CookieCollection() + { + _list = new List(); + } + + + internal IList List => _list; + + internal IEnumerable Sorted + { + get + { + var list = new List(_list); + if (list.Count > 1) + { + list.Sort(compareCookieWithinSorted); + } + + return 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 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; + } + } + + /// + /// Gets an object that can be used to synchronize access to the collection. + /// + public object SyncRoot => _sync ??= ((ICollection)_list).SyncRoot; + + 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()); + } + + DateTime expires; + if (!DateTime.TryParseExact( + buff.ToString(), + new[] { "ddd, dd'-'MMM'-'yyyy HH':'mm':'ss 'GMT'", "r" }, + CultureInfo.CreateSpecificCulture("en-US"), + DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, + out expires)) + { + expires = DateTime.Now; + } + + if (cookie != null && cookie.Expires == DateTime.MinValue) + { + cookie.Expires = expires.ToLocalTime(); + } + } + 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 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; + } + + private static string[] splitCookieHeaderValue(string value) + { + return new List(value.SplitHeaderValue(',', ';')).ToArray(); + } + + /// + /// 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); + } + } + + /// + /// 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(); + } + } +} \ 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 new file mode 100644 index 0000000..1fe2ae0 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Cookies/CookieException.cs @@ -0,0 +1,81 @@ +// 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 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) + { + } + + /// + /// Initializes a new instance of the class. + /// + public CookieException() + : base() + { + } + + /// + /// 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 new file mode 100644 index 0000000..d869e7e --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Endpoints/EndPointListener.cs @@ -0,0 +1,533 @@ +// 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 List _all; // host == '+' + private static readonly string _defaultCertFolderPath; + private readonly IPEndPoint _endpoint; + private Dictionary _prefixes; + private readonly Socket _socket; + private List _unhandled; // host == '*' + private readonly Dictionary _unregistered; + private readonly object _unregisteredSync; + + static EndPointListener() + { + _defaultCertFolderPath = + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + } + + internal EndPointListener( + IPEndPoint endpoint, + bool secure, + string certificateFolderPath, + SSLConfigurationServer 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; + SslConfiguration = new SSLConfigurationServer(sslConfig); + SslConfiguration.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 configuration for the secure endpoint. + /// + public SSLConfigurationServer SslConfiguration { get; } + + private static void addSpecial(List prefixes, HttpListenerPrefix prefix) + { + var path = prefix.Path; + foreach (var pref in prefixes) + { + if (pref.Path == path) + { + throw new HttpListenerException(87, "The prefix is already in use."); + } + } + + prefixes.Add(prefix); + } + + private static RSACryptoServiceProvider createRSAFromFile(string filename) + { + byte[] pvk = null; + using (var fs = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + pvk = new byte[fs.Length]; + fs.Read(pvk, 0, pvk.Length); + } + + var rsa = new RSACryptoServiceProvider(); + rsa.ImportCspBlob(pvk); + + return rsa; + } + + private static X509Certificate2 getCertificate( + int port, string folderPath, X509Certificate2 defaultCertificate + ) + { + if (folderPath == null || folderPath.Length == 0) + { + folderPath = _defaultCertFolderPath; + } + + try + { + var cer = Path.Combine(folderPath, string.Format("{0}.cer", port)); + var key = Path.Combine(folderPath, string.Format("{0}.key", port)); + if (File.Exists(cer) && File.Exists(key)) + { + var cert = new X509Certificate2(cer); + cert.PrivateKey = createRSAFromFile(key); + + return cert; + } + } + catch + { + } + + return defaultCertificate; + } + + private void leaveIfNoPrefix() + { + if (_prefixes.Count > 0) + { + return; + } + + var prefs = _unhandled; + if (prefs != null && prefs.Count > 0) + { + return; + } + + prefs = _all; + if (prefs != null && prefs.Count > 0) + { + return; + } + + EndPointManager.RemoveEndPoint(_endpoint); + } + + private static void onAccept(IAsyncResult asyncResult) + { + var lsnr = (EndPointListener)asyncResult.AsyncState; + + Socket sock = null; + try + { + sock = lsnr._socket.EndAccept(asyncResult); + } + catch (SocketException) + { + // TODO: Should log the error code when this class has a logging. + } + catch (ObjectDisposedException) + { + return; + } + + try + { + lsnr._socket.BeginAccept(onAccept, lsnr); + } + catch + { + if (sock != null) + { + sock.Close(); + } + + return; + } + + if (sock == null) + { + return; + } + + processAccepted(sock, lsnr); + } + + private static void processAccepted(Socket socket, EndPointListener listener) + { + HttpConnection conn = null; + try + { + conn = new HttpConnection(socket, listener); + lock (listener._unregisteredSync) + { + listener._unregistered[conn] = conn; + } + + conn.BeginReadRequest(); + } + catch + { + if (conn != null) + { + conn.Close(true); + return; + } + + socket.Close(); + } + } + + private static bool removeSpecial(List prefixes, HttpListenerPrefix prefix) + { + var path = prefix.Path; + var cnt = prefixes.Count; + for (var i = 0; i < cnt; i++) + { + if (prefixes[i].Path == path) + { + prefixes.RemoveAt(i); + return true; + } + } + + return false; + } + + private static HttpListener searchHttpListenerFromSpecial( + string path, List prefixes + ) + { + if (prefixes == null) + { + return null; + } + + HttpListener bestMatch = null; + + var bestLen = -1; + foreach (var pref in prefixes) + { + var prefPath = pref.Path; + + var len = prefPath.Length; + if (len < bestLen) + { + continue; + } + + if (path.StartsWith(prefPath)) + { + bestLen = len; + bestMatch = pref.Listener; + } + } + + return bestMatch; + } + + internal static bool CertificateExists(int port, string folderPath) + { + if (folderPath == null || folderPath.Length == 0) + { + folderPath = _defaultCertFolderPath; + } + + var cer = Path.Combine(folderPath, string.Format("{0}.cer", port)); + var key = Path.Combine(folderPath, string.Format("{0}.key", port)); + + return File.Exists(cer) && File.Exists(key); + } + + internal void RemoveConnection(HttpConnection connection) + { + lock (_unregisteredSync) + { + _unregistered.Remove(connection); + } + } + + internal bool TrySearchHttpListener(Uri uri, out HttpListener listener) + { + listener = null; + + if (uri == null) + { + return false; + } + + var host = uri.Host; + var dns = Uri.CheckHostName(host) == UriHostNameType.Dns; + var port = uri.Port.ToString(); + var path = HttpUtility.UrlDecode(uri.AbsolutePath); + var pathSlash = path[path.Length - 1] != '/' ? path + "/" : path; + + if (host != null && host.Length > 0) + { + var bestLen = -1; + foreach (var pref in _prefixes.Keys) + { + if (dns) + { + var prefHost = pref.Host; + if (Uri.CheckHostName(prefHost) == UriHostNameType.Dns && prefHost != host) + { + continue; + } + } + + if (pref.Port != port) + { + continue; + } + + var prefPath = pref.Path; + + var len = prefPath.Length; + if (len < bestLen) + { + continue; + } + + if (path.StartsWith(prefPath) || pathSlash.StartsWith(prefPath)) + { + bestLen = len; + listener = _prefixes[pref]; + } + } + + if (bestLen != -1) + { + return true; + } + } + + var prefs = _unhandled; + listener = searchHttpListenerFromSpecial(path, prefs); + if (listener == null && pathSlash != path) + { + listener = searchHttpListenerFromSpecial(pathSlash, prefs); + } + + if (listener != null) + { + return true; + } + + prefs = _all; + listener = searchHttpListenerFromSpecial(path, prefs); + if (listener == null && pathSlash != path) + { + listener = searchHttpListenerFromSpecial(pathSlash, prefs); + } + + return listener != null; + } + + public void AddPrefix(HttpListenerPrefix prefix, HttpListener listener) + { + List current, future; + if (prefix.Host == "*") + { + do + { + current = _unhandled; + future = current != null + ? new List(current) + : new List(); + + prefix.Listener = listener; + addSpecial(future, prefix); + } + while (Interlocked.CompareExchange(ref _unhandled, future, current) != current); + + return; + } + + if (prefix.Host == "+") + { + do + { + current = _all; + future = current != null + ? new List(current) + : new List(); + + prefix.Listener = listener; + addSpecial(future, prefix); + } + while (Interlocked.CompareExchange(ref _all, future, current) != current); + + return; + } + + Dictionary prefs, prefs2; + do + { + prefs = _prefixes; + if (prefs.ContainsKey(prefix)) + { + if (prefs[prefix] != listener) + { + throw new HttpListenerException( + 87, string.Format("There's another listener for {0}.", prefix) + ); + } + + return; + } + + prefs2 = new Dictionary(prefs); + prefs2[prefix] = listener; + } + while (Interlocked.CompareExchange(ref _prefixes, prefs2, prefs) != prefs); + } + + public void Close() + { + _socket.Close(); + + HttpConnection[] conns = null; + lock (_unregisteredSync) + { + if (_unregistered.Count == 0) + { + return; + } + + var keys = _unregistered.Keys; + conns = new HttpConnection[keys.Count]; + keys.CopyTo(conns, 0); + _unregistered.Clear(); + } + + for (var i = conns.Length - 1; i >= 0; i--) + { + conns[i].Close(true); + } + } + + public void RemovePrefix(HttpListenerPrefix prefix, HttpListener listener) + { + List current, future; + if (prefix.Host == "*") + { + do + { + current = _unhandled; + if (current == null) + { + break; + } + + future = new List(current); + if (!removeSpecial(future, prefix)) + { + break; // The prefix wasn't found. + } + } + while (Interlocked.CompareExchange(ref _unhandled, future, current) != current); + + leaveIfNoPrefix(); + return; + } + + if (prefix.Host == "+") + { + do + { + current = _all; + if (current == null) + { + break; + } + + future = new List(current); + if (!removeSpecial(future, prefix)) + { + break; // The prefix wasn't found. + } + } + while (Interlocked.CompareExchange(ref _all, future, current) != current); + + leaveIfNoPrefix(); + return; + } + + Dictionary prefs, prefs2; + do + { + prefs = _prefixes; + if (!prefs.ContainsKey(prefix)) + { + break; + } + + prefs2 = new Dictionary(prefs); + prefs2.Remove(prefix); + } + while (Interlocked.CompareExchange(ref _prefixes, prefs2, prefs) != prefs); + + leaveIfNoPrefix(); + } + } +} \ 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 new file mode 100644 index 0000000..acfb1a0 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Endpoints/EndPointManager.cs @@ -0,0 +1,237 @@ +// 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() + { + } + + 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."); + } + + int port; + if (!int.TryParse(pref.Port, out port)) + { + throw new HttpListenerException(87, "Includes an invalid port."); + } + + if (!port.IsPortNumber()) + { + throw new HttpListenerException(87, "Includes an invalid port."); + } + + var path = pref.Path; + if (path.IndexOf('%') != -1) + { + throw new HttpListenerException(87, "Includes an invalid path."); + } + + if (path.IndexOf("//", StringComparison.Ordinal) != -1) + { + throw new HttpListenerException(87, "Includes an invalid path."); + } + + var endpoint = new IPEndPoint(addr, port); + + EndPointListener lsnr; + if (_endpoints.TryGetValue(endpoint, out lsnr)) + { + if (lsnr.IsSecure ^ pref.IsSecure) + { + throw new HttpListenerException(87, "Includes an invalid scheme."); + } + } + else + { + lsnr = + new EndPointListener( + endpoint, + pref.IsSecure, + listener.CertificateFolderPath, + listener.SslConfiguration, + listener.ReuseAddress + ); + + _endpoints.Add(endpoint, lsnr); + } + + lsnr.AddPrefix(pref, listener); + } + + private static IPAddress convertToIPAddress(string hostname) + { + 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; + } + + int port; + if (!int.TryParse(pref.Port, out port)) + { + return; + } + + if (!port.IsPortNumber()) + { + return; + } + + var path = pref.Path; + if (path.IndexOf('%') != -1) + { + return; + } + + if (path.IndexOf("//", StringComparison.Ordinal) != -1) + { + return; + } + + var endpoint = new IPEndPoint(addr, port); + + EndPointListener lsnr; + if (!_endpoints.TryGetValue(endpoint, out lsnr)) + { + return; + } + + if (lsnr.IsSecure ^ pref.IsSecure) + { + return; + } + + lsnr.RemovePrefix(pref, listener); + } + + /// + /// 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) + { + EndPointListener lsnr; + if (!_endpoints.TryGetValue(endpoint, out lsnr)) + { + return false; + } + + _endpoints.Remove(endpoint); + lsnr.Close(); + + return true; + } + } + + /// + /// 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); + } + } + } +} \ 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 new file mode 100644 index 0000000..b770b76 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpBasicIdentity.cs @@ -0,0 +1,31 @@ +// 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 new file mode 100644 index 0000000..eee29df --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpConnection.cs @@ -0,0 +1,633 @@ +// 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 byte[] _buffer; + private const int _bufferLength = 8192; + private HttpListenerContext _context; + private bool _contextRegistered; + private StringBuilder _currentLine; + private InputState _inputState; + private RequestStream _inputStream; + private HttpListener _lastListener; + private LineState _lineState; + private readonly EndPointListener _listener; + private ResponseStream _outputStream; + private int _position; + private MemoryStream _requestBuffer; + private Socket _socket; + private readonly object _sync; + private int _timeout; + private readonly Dictionary _timeoutCanceled; + 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 netStream = new NetworkStream(socket, false); + if (IsSecure) + { + var conf = listener.SslConfiguration; + var sslStream = new SslStream(netStream, false, conf.ClientCertificateValidationCallback); + sslStream.AuthenticateAsServer( + conf.Certificate, + conf.IsClientCertificateRequired, + conf.SslProtocols, + conf.CheckForCertificateRevocation + ); + + Stream = sslStream; + } + else + { + Stream = netStream; + } + + _sync = new object(); + _timeout = 90000; // 90k ms for first request, 15k ms from then on. + _timeoutCanceled = new Dictionary(); + _timer = new Timer(onTimeout, this, Timeout.Infinite, Timeout.Infinite); + + init(); + } + + /// + /// 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; } + + private void close() + { + lock (_sync) + { + if (_socket == null) + { + return; + } + + disposeTimer(); + disposeRequestBuffer(); + disposeStream(); + closeSocket(); + } + + unregisterContext(); + removeConnection(); + } + + private void closeSocket() + { + try + { + _socket.Shutdown(SocketShutdown.Both); + } + catch + { + } + + _socket.Close(); + _socket = null; + } + + private void disposeRequestBuffer() + { + if (_requestBuffer == null) + { + return; + } + + _requestBuffer.Dispose(); + _requestBuffer = null; + } + + private void disposeStream() + { + if (Stream == null) + { + return; + } + + _inputStream = null; + _outputStream = null; + + Stream.Dispose(); + Stream = null; + } + + private void disposeTimer() + { + if (_timer == null) + { + return; + } + + try + { + _timer.Change(Timeout.Infinite, Timeout.Infinite); + } + catch + { + } + + _timer.Dispose(); + _timer = null; + } + + private void init() + { + _context = new HttpListenerContext(this); + _inputState = InputState.RequestLine; + _inputStream = null; + _lineState = LineState.None; + _outputStream = null; + _position = 0; + _requestBuffer = new MemoryStream(); + } + + private static void onRead(IAsyncResult asyncResult) + { + var conn = (HttpConnection)asyncResult.AsyncState; + if (conn._socket == null) + { + return; + } + + lock (conn._sync) + { + if (conn._socket == null) + { + return; + } + + var nread = -1; + var len = 0; + try + { + var current = conn.Reuses; + if (!conn._timeoutCanceled[current]) + { + conn._timer.Change(Timeout.Infinite, Timeout.Infinite); + conn._timeoutCanceled[current] = true; + } + + nread = conn.Stream.EndRead(asyncResult); + conn._requestBuffer.Write(conn._buffer, 0, nread); + len = (int)conn._requestBuffer.Length; + } + catch (Exception ex) + { + if (conn._requestBuffer != null && conn._requestBuffer.Length > 0) + { + conn.SendError(ex.Message, 400); + return; + } + + conn.close(); + return; + } + + if (nread <= 0) + { + conn.close(); + return; + } + + if (conn.processInput(conn._requestBuffer.GetBuffer(), len)) + { + if (!conn._context.HasError) + { + conn._context.Request.FinishInitialization(); + } + + if (conn._context.HasError) + { + conn.SendError(); + return; + } + + HttpListener lsnr; + if (!conn._listener.TrySearchHttpListener(conn._context.Request.Url, out lsnr)) + { + conn.SendError(null, 404); + return; + } + + if (conn._lastListener != lsnr) + { + conn.removeConnection(); + if (!lsnr.AddConnection(conn)) + { + conn.close(); + return; + } + + conn._lastListener = lsnr; + } + + conn._context.Listener = lsnr; + if (!conn._context.Authenticate()) + { + return; + } + + if (conn._context.Register()) + { + conn._contextRegistered = true; + } + + return; + } + + conn.Stream.BeginRead(conn._buffer, 0, _bufferLength, onRead, conn); + } + } + + private static void onTimeout(object state) + { + var conn = (HttpConnection)state; + var current = conn.Reuses; + if (conn._socket == null) + { + return; + } + + lock (conn._sync) + { + if (conn._socket == null) + { + return; + } + + if (conn._timeoutCanceled[current]) + { + return; + } + + conn.SendError(null, 408); + } + } + + 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 ex) + { + _context.ErrorMessage = ex.Message; + return true; + } + + _position += nread; + if (_position >= 32768) + { + _context.ErrorMessage = "Headers too long"; + return true; + } + + return false; + } + + private string readLineFrom(byte[] buffer, int offset, int length, out int read) + { + read = 0; + + for (var i = offset; i < length && _lineState != LineState.Lf; i++) + { + read++; + + var b = buffer[i]; + if (b == 13) + { + _lineState = LineState.Cr; + } + else if (b == 10) + { + _lineState = LineState.Lf; + } + else + { + _currentLine.Append((char)b); + } + } + + if (_lineState != LineState.Lf) + { + return null; + } + + var line = _currentLine.ToString(); + + _currentLine.Length = 0; + _lineState = LineState.None; + + return line; + } + + private void removeConnection() + { + if (_lastListener != null) + { + _lastListener.RemoveConnection(this); + } + else + { + _listener.RemoveConnection(this); + } + } + + private void unregisterContext() + { + if (!_contextRegistered) + { + return; + } + + _context.Unregister(); + _contextRegistered = false; + } + + /// + /// Closes the connection. + /// + /// True to force close, false otherwise. + internal void Close(bool force) + { + if (_socket == null) + { + return; + } + + lock (_sync) + { + if (_socket == null) + { + return; + } + + if (!force) + { + GetResponseStream().Close(false); + if (!_context.Response.CloseConnection && _context.Request.FlushInput()) + { + // Don't close. Keep working. + Reuses++; + disposeRequestBuffer(); + unregisterContext(); + init(); + BeginReadRequest(); + + return; + } + } + else if (_outputStream != null) + { + _outputStream.Close(true); + } + + close(); + } + } + + /// + /// Initiates reading the request. + /// + public void BeginReadRequest() + { + _buffer ??= new byte[_bufferLength]; + + if (Reuses == 1) + { + _timeout = 15000; + } + + try + { + _timeoutCanceled.Add(Reuses, false); + _timer.Change(_timeout, Timeout.Infinite); + Stream.BeginRead(_buffer, 0, _bufferLength, onRead, this); + } + catch + { + close(); + } + } + + /// + /// 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 (_sync) + { + 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 (_sync) + { + 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 (_sync) + { + if (_socket == null) + { + return; + } + + try + { + var res = _context.Response; + res.StatusCode = status; + res.ContentType = "text/html"; + + var content = new StringBuilder(64); + content.AppendFormat("EonaCat.Network Error

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

", message); + } + else + { + content.Append(""); + } + + var enc = Encoding.UTF8; + var entity = enc.GetBytes(content.ToString()); + res.ContentEncoding = enc; + res.ContentLength64 = entity.LongLength; + + res.Close(entity, true); + } + catch + { + Close(true); + } + } + } + } +} \ 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 new file mode 100644 index 0000000..f767012 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpDigestIdentity.cs @@ -0,0 +1,93 @@ +// 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); + copied["password"] = password; + copied["realm"] = realm; + copied["method"] = method; + copied["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 new file mode 100644 index 0000000..e8bdec0 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpHeaderInfo.cs @@ -0,0 +1,75 @@ +// 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 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 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. + /// + /// 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 new file mode 100644 index 0000000..40b51d1 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpHeaderType.cs @@ -0,0 +1,49 @@ +// 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 new file mode 100644 index 0000000..757d5a8 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListener.cs @@ -0,0 +1,619 @@ +// 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 AuthenticationSchemes _authSchemes; + private Func _authSchemeSelector; + private string _certFolderPath; + private readonly Dictionary _connections; + private readonly object _connectionsSync; + private readonly List _ctxQueue; + private readonly object _ctxQueueSync; + private readonly Dictionary _ctxRegistry; + private readonly object _ctxRegistrySync; + private static readonly string _defaultRealm; + private bool _ignoreWriteExceptions; + private volatile bool _listening; + private readonly HttpListenerPrefixCollection _prefixes; + private string _realm; + private SSLConfigurationServer _sslConfig; + private Func _userCredFinder; + private readonly List _waitQueue; + private readonly object _waitQueueSync; + + static HttpListener() + { + _defaultRealm = "SECRET AREA"; + } + + /// + /// Initializes a new instance of the class. + /// + public HttpListener() + { + _authSchemes = AuthenticationSchemes.Anonymous; + + _connections = new Dictionary(); + _connectionsSync = ((ICollection)_connections).SyncRoot; + + _ctxQueue = new List(); + _ctxQueueSync = ((ICollection)_ctxQueue).SyncRoot; + + _ctxRegistry = new Dictionary(); + _ctxRegistrySync = ((ICollection)_ctxRegistry).SyncRoot; + + _prefixes = new HttpListenerPrefixCollection(this); + + _waitQueue = new List(); + _waitQueueSync = ((ICollection)_waitQueue).SyncRoot; + } + + internal bool IsDisposed { get; private set; } + + internal bool ReuseAddress { get; set; } + + /// + /// 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 static bool IsSupported => true; + + public HttpListenerPrefixCollection Prefixes + { + get + { + CheckDisposed(); + return _prefixes; + } + } + + public string Realm + { + get + { + CheckDisposed(); + return _realm; + } + + set + { + CheckDisposed(); + _realm = value; + } + } + + public SSLConfigurationServer SslConfiguration + { + get + { + CheckDisposed(); + return _sslConfig ??= new SSLConfigurationServer(); + } + + 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; + } + } + + private void cleanupConnections() + { + HttpConnection[] conns = null; + lock (_connectionsSync) + { + if (_connections.Count == 0) + { + return; + } + + // Need to copy this since closing will call the RemoveConnection method. + var keys = _connections.Keys; + conns = new HttpConnection[keys.Count]; + keys.CopyTo(conns, 0); + _connections.Clear(); + } + + for (var i = conns.Length - 1; i >= 0; i--) + { + conns[i].Close(true); + } + } + + private void cleanupContextQueue(bool sendServiceUnavailable) + { + HttpListenerContext[] ctxs = null; + lock (_ctxQueueSync) + { + if (_ctxQueue.Count == 0) + { + return; + } + + ctxs = _ctxQueue.ToArray(); + _ctxQueue.Clear(); + } + + if (!sendServiceUnavailable) + { + return; + } + + foreach (var ctx in ctxs) + { + var res = ctx.Response; + res.StatusCode = (int)HttpStatusCode.ServiceUnavailable; + res.Close(); + } + } + + private void cleanupContextRegistry() + { + HttpListenerContext[] ctxs = null; + lock (_ctxRegistrySync) + { + if (_ctxRegistry.Count == 0) + { + return; + } + + // Need to copy this since closing will call the UnregisterContext method. + var keys = _ctxRegistry.Keys; + ctxs = new HttpListenerContext[keys.Count]; + keys.CopyTo(ctxs, 0); + _ctxRegistry.Clear(); + } + + for (var i = ctxs.Length - 1; i >= 0; i--) + { + ctxs[i].Connection.Close(true); + } + } + + private void cleanupWaitQueue(Exception exception) + { + HttpListenerAsyncResult[] aress = null; + lock (_waitQueueSync) + { + if (_waitQueue.Count == 0) + { + return; + } + + aress = _waitQueue.ToArray(); + _waitQueue.Clear(); + } + + foreach (var ares in aress) + { + ares.Complete(exception); + } + } + + private void close(bool force) + { + if (_listening) + { + _listening = false; + EndPointManager.RemoveListener(this); + } + + lock (_ctxRegistrySync) + { + cleanupContextQueue(!force); + } + + cleanupContextRegistry(); + cleanupConnections(); + cleanupWaitQueue(new ObjectDisposedException(GetType().ToString())); + + IsDisposed = true; + } + + private HttpListenerAsyncResult getAsyncResultFromQueue() + { + if (_waitQueue.Count == 0) + { + return null; + } + + var ares = _waitQueue[0]; + _waitQueue.RemoveAt(0); + + return ares; + } + + private HttpListenerContext getContextFromQueue() + { + if (_ctxQueue.Count == 0) + { + return null; + } + + var ctx = _ctxQueue[0]; + _ctxQueue.RemoveAt(0); + + return ctx; + } + + internal bool AddConnection(HttpConnection connection) + { + if (!_listening) + { + return false; + } + + lock (_connectionsSync) + { + if (!_listening) + { + return false; + } + + _connections[connection] = connection; + return true; + } + } + + internal HttpListenerAsyncResult BeginGetContext(HttpListenerAsyncResult asyncResult) + { + lock (_ctxRegistrySync) + { + if (!_listening) + { + throw new HttpListenerException(995); + } + + var ctx = getContextFromQueue(); + if (ctx == null) + { + _waitQueue.Add(asyncResult); + } + else + { + asyncResult.Complete(ctx, true); + } + + return asyncResult; + } + } + + internal void CheckDisposed() + { + if (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 (_ctxRegistrySync) + { + if (!_listening) + { + return false; + } + + _ctxRegistry[context] = context; + + var ares = getAsyncResultFromQueue(); + if (ares == null) + { + _ctxQueue.Add(context); + } + else + { + ares.Complete(context); + } + + return true; + } + } + + internal void RemoveConnection(HttpConnection connection) + { + lock (_connectionsSync) + { + _connections.Remove(connection); + } + } + + internal AuthenticationSchemes SelectAuthenticationScheme(HttpListenerRequest request) + { + var selector = _authSchemeSelector; + if (selector == null) + { + return _authSchemes; + } + + try + { + return selector(request); + } + catch + { + return AuthenticationSchemes.None; + } + } + + internal void UnregisterContext(HttpListenerContext context) + { + lock (_ctxRegistrySync) + { + _ctxRegistry.Remove(context); + } + } + + /// + /// 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); + } + + /// + /// 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 ares) + { + throw new ArgumentException("A wrong IAsyncResult.", nameof(asyncResult)); + } + + if (ares.EndCalled) + { + throw new InvalidOperationException("This IAsyncResult cannot be reused."); + } + + ares.EndCalled = true; + if (!ares.IsCompleted) + { + ares.AsyncWaitHandle.WaitOne(); + } + + return ares.GetContext(); // This may throw an exception. + } + + /// + /// Gets 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 ares = BeginGetContext(new HttpListenerAsyncResult(null, null)); + ares.InGet = true; + + return EndGetContext(ares); + } + + /// + /// 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 (_ctxRegistrySync) + { + cleanupContextQueue(true); + } + + cleanupContextRegistry(); + cleanupConnections(); + cleanupWaitQueue(new HttpListenerException(995, "The listener is closed.")); + } + + /// + /// Disposes of the resources used by the . + /// + void IDisposable.Dispose() + { + if (IsDisposed) + { + return; + } + + close(true); + } + } +} \ 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 new file mode 100644 index 0000000..8a65029 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerAsyncResult.cs @@ -0,0 +1,165 @@ +// 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 bool _completed; + private HttpListenerContext _context; + private Exception _exception; + private readonly object _sync; + 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; + _sync = new object(); + } + + /// + /// 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; } + + /// + /// 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 (_sync) + { + 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 (_sync) + { + return _completed; + } + } + } + + // Private method to complete the asynchronous operation + private static void complete(HttpListenerAsyncResult asyncResult) + { + lock (asyncResult._sync) + { + asyncResult._completed = true; + + var waitHandle = asyncResult._waitHandle; + if (waitHandle != null) + { + waitHandle.Set(); + } + } + + var callback = asyncResult._callback; + if (callback == null) + { + return; + } + + ThreadPool.QueueUserWorkItem( + state => + { + try + { + callback(asyncResult); + } + catch + { + } + }, + null + ); + } + + /// + /// 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; + } + } +} \ 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 new file mode 100644 index 0000000..aa7f1f2 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerContext.cs @@ -0,0 +1,152 @@ +// 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 HttpListenerWebSocketContext _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 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; } + + /// + /// 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; } + + /// + /// 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); + } + + /// + /// Accepts a WebSocket connection with the specified protocol. + /// + /// The WebSocket subprotocol to negotiate. + /// The for the WebSocket connection. + public HttpListenerWebSocketContext 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 HttpListenerWebSocketContext(this, protocol); + return _websocketContext; + } + } +} \ 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 new file mode 100644 index 0000000..00d1e85 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerException.cs @@ -0,0 +1,56 @@ +// 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. + /// + protected HttpListenerException( + SerializationInfo serializationInfo, StreamingContext streamingContext) + : base(serializationInfo, streamingContext) + { + } + + /// + /// 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}") + { + } + + /// + /// 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 new file mode 100644 index 0000000..0db7458 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerPrefix.cs @@ -0,0 +1,167 @@ +// 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; } + + 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); + } + + /// + /// 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; + } + } +} \ 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 new file mode 100644 index 0000000..0749aa7 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerPrefixCollection.cs @@ -0,0 +1,154 @@ +// 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(); + } + + /// + /// 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; + } + + /// + /// 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(); + } + } +} \ 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 new file mode 100644 index 0000000..51ce181 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerRequest.cs @@ -0,0 +1,417 @@ +// 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.Security.Cryptography.X509Certificates; +using System.Text; + +namespace EonaCat.Network +{ + /// + /// Represents an HTTP listener request. + /// + public sealed class HttpListenerRequest + { + private static readonly byte[] _100continue; + private bool _chunked; + private Encoding _contentEncoding; + private bool _contentLengthSet; + private readonly HttpListenerContext _context; + private CookieCollection _cookies; + private readonly WebHeaderCollection _headers; + 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, Encoding.UTF8); + + /// + /// 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; } + + private static bool tryCreateVersion(string version, out Version result) + { + try + { + result = new Version(version); + return true; + } + catch + { + result = null; + return false; + } + } + + 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") + { + long len; + if (long.TryParse(val, out 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 ares = InputStream.BeginRead(buff, 0, len, null, null); + if (!ares.IsCompleted && !ares.AsyncWaitHandle.WaitOne(100)) + { + return false; + } + + if (InputStream.EndRead(ares) <= 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)"; + } + } + + 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(); + } + } +} \ 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 new file mode 100644 index 0000000..502b40a --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpListenerResponse.cs @@ -0,0 +1,655 @@ +// 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 EonaCat.Logger; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Text; + +namespace EonaCat.Network +{ + /// + /// Represents an HTTP listener response. + /// + public sealed class HttpListenerResponse : IDisposable + { + private Encoding _contentEncoding; + private long _contentLength; + private string _contentType; + private readonly HttpListenerContext _context; + 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; + } + + Uri uri = null; + if (!value.MaybeUri() || !Uri.TryCreate(value, UriKind.Absolute, out 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) + { + if (_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)); + } + + Uri uri = null; + if (!url.MaybeUri() || !Uri.TryCreate(url, UriKind.Absolute, out 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 new file mode 100644 index 0000000..d80d379 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpRequestHeader.cs @@ -0,0 +1,101 @@ +// 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 new file mode 100644 index 0000000..a17222a --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpResponseHeader.cs @@ -0,0 +1,79 @@ +// 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 new file mode 100644 index 0000000..fa19756 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpStatusCode.cs @@ -0,0 +1,103 @@ +// 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 new file mode 100644 index 0000000..5009b81 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpStreamAsyncResult.cs @@ -0,0 +1,87 @@ +// 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 bool _completed; + private readonly object _sync; + private ManualResetEvent _waitHandle; + + internal HttpStreamAsyncResult(AsyncCallback callback, object state) + { + _callback = callback; + AsyncState = state; + _sync = new object(); + } + + 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; } + + public object AsyncState { get; } + + public WaitHandle AsyncWaitHandle + { + get + { + lock (_sync) + { + return _waitHandle ??= new ManualResetEvent(_completed); + } + } + } + + public bool CompletedSynchronously => SyncRead == Count; + + public bool IsCompleted + { + get + { + lock (_sync) + { + return _completed; + } + } + } + + internal void Complete() + { + lock (_sync) + { + if (_completed) + { + return; + } + + _completed = true; + + if (_waitHandle != null) + { + _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 new file mode 100644 index 0000000..21d2212 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpUtility.cs @@ -0,0 +1,1366 @@ +// 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 Dictionary _entities; + private static readonly char[] _hexChars = "0123456789abcdef".ToCharArray(); + private static readonly object _sync = new object(); + + 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 (_sync) + { + if (_entities == null) + { + initEntities(); + } + + 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 void initEntities() + { + // Build the dictionary of HTML entity references. + // This list comes from the HTML 4.01 W3C recommendation. + _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 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); + } + + 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()) + { + Uri uri; + var valid = Uri.TryCreate(requestUri, UriKind.Absolute, out 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); + + Uri res; + if (!Uri.TryCreate(url, UriKind.Absolute, out 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(); + } + } + + 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()); + } + } + } +} \ 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 new file mode 100644 index 0000000..859d74e --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Http/HttpVersion.cs @@ -0,0 +1,18 @@ +// 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 HttpVersion + { + public static readonly Version Version10 = new Version(1, 0); + + public static readonly Version Version11 = new Version(1, 1); + + 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 new file mode 100644 index 0000000..b3ce4e3 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/InputChunkState.cs @@ -0,0 +1,14 @@ +// 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 new file mode 100644 index 0000000..ba8d311 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/InputState.cs @@ -0,0 +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. + +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 new file mode 100644 index 0000000..6d421d9 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/LineState.cs @@ -0,0 +1,12 @@ +// 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, + Cr, + Lf + } +} \ 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 new file mode 100644 index 0000000..b44eb20 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Logger.cs @@ -0,0 +1,205 @@ +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; +using System.Numerics; + +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 + { + internal static string LoggingDirectory { get; private set; } + private static readonly LogManager _logManager; + + internal static bool IsLoggingDirectorySet => !string.IsNullOrWhiteSpace(LoggingDirectory); + private static bool HasBeenSetup { get; set; } + internal static bool DisableConsole { get; set; } + internal static bool IsLoggingEnabled { get; set; } + + static Logger() + { + _logManager = new LogManager(new LoggerSettings { RemoveMessagePrefix = true }); + _logManager.OnException += _logManager_OnException; + } + + 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; + } + + private static void _logManager_OnException(object? sender, ErrorMessage e) + { + Console.WriteLine(e.Message); + if (e.Exception != null) + { + Console.WriteLine(e.Exception); + } + } + + internal static void AddGrayLogServer(string hostname, int port) + { + _logManager.Settings.GrayLogServers.Add(new GrayLogServer(hostname, port)); + } + + internal static bool RemoveGrayLogServer(GrayLogServer grayLogServer) + { + return _logManager.Settings.GrayLogServers.Remove(grayLogServer); + } + + internal static void GrayLogState(bool state) + { + _logManager.Settings.SendToGrayLogServers = state; + } + + 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 bool RemoveSplunkServer(SplunkServer splunkServer) + { + return _logManager.Settings.SplunkServers.Remove(splunkServer); + } + + internal static void SplunkState(bool state) + { + _logManager.Settings.SendToSplunkServers = state; + } + + internal static void AddSyslogServer(string ipAddress, int port) + { + _logManager.Settings.SysLogServers.Add(new SyslogServer(ipAddress, port)); + } + + internal static bool RemoveSyslogServer(SyslogServer syslogServer) + { + return _logManager.Settings.SysLogServers.Remove(syslogServer); + } + + internal static void SysLogState(bool state) + { + _logManager.Settings.SendToSyslogServers = 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); + } + + 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 (!HasBeenSetup) + { + Setup(); + } + + if (DisableConsole) + { + writeToConsole = false; + } + + 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); + } + } + + 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 (!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 (!HasBeenSetup) + { + Setup(); + } + + if (isCriticalException) + { + _logManager.Write(message, ELogType.CRITICAL, writeToConsole); + } + else + { + _logManager.Write(message, ELogType.ERROR, writeToConsole); + } + } + + 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); + } + + 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 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 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); + } + } +} \ 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 new file mode 100644 index 0000000..432afbf --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/ReadBufferState.cs @@ -0,0 +1,28 @@ +// 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/SSL/SSLConfigurationClient.cs b/EonaCat.Network/System/Sockets/Web/Core/SSL/SSLConfigurationClient.cs new file mode 100644 index 0000000..8e8acfd --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/SSL/SSLConfigurationClient.cs @@ -0,0 +1,114 @@ +// 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 SSLConfigurationClient + { + private LocalCertificateSelectionCallback _clientCertSelectionCallback; + private X509CertificateCollection _clientCertificates; + private RemoteCertificateValidationCallback _serverCertValidationCallback; + + public SSLConfigurationClient() + { + SslProtocols = SslProtocols.Tls12; + } + + public SSLConfigurationClient(string targetHost) + { + TargetHost = targetHost; + SslProtocols = SslProtocols.Tls12; + } + + public SSLConfigurationClient(SSLConfigurationClient configuration) + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + CheckForCertificateRevocation = configuration.CheckForCertificateRevocation; + _clientCertSelectionCallback = configuration._clientCertSelectionCallback; + _clientCertificates = configuration._clientCertificates; + SslProtocols = configuration.SslProtocols; + _serverCertValidationCallback = configuration._serverCertValidationCallback; + TargetHost = configuration.TargetHost; + } + + public bool CheckForCertificateRevocation { get; set; } + + public X509CertificateCollection Certificates + { + get + { + _clientCertificates ??= new X509CertificateCollection(); + return _clientCertificates; + } + + set + { + _clientCertificates = value; + } + } + + public LocalCertificateSelectionCallback ClientCertificateSelectionCallback + { + get + { + _clientCertSelectionCallback ??= SelectClientCertificate; + + return _clientCertSelectionCallback; + } + + set + { + _clientCertSelectionCallback = value; + } + } + + public SslProtocols SslProtocols { get; set; } + + public RemoteCertificateValidationCallback ServerCertificateValidationCallback + { + get + { + _serverCertValidationCallback ??= ValidateServerCertificate; + + return _serverCertValidationCallback; + } + + set + { + _serverCertValidationCallback = value; + } + } + + 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/SSL/SSLConfigurationServer.cs b/EonaCat.Network/System/Sockets/Web/Core/SSL/SSLConfigurationServer.cs new file mode 100644 index 0000000..f8c22d0 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/SSL/SSLConfigurationServer.cs @@ -0,0 +1,73 @@ +// 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 SSLConfigurationServer + { + private RemoteCertificateValidationCallback _clientCertValidationCallback; + + public SSLConfigurationServer() + { + SslProtocols = SslProtocols.Tls12; + } + + public SSLConfigurationServer(X509Certificate2 certificate) + { + Certificate = certificate; + SslProtocols = SslProtocols.Tls12; + } + + public SSLConfigurationServer(SSLConfigurationServer configuration) + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + CheckForCertificateRevocation = configuration.CheckForCertificateRevocation; + IsClientCertificateRequired = configuration.IsClientCertificateRequired; + _clientCertValidationCallback = configuration._clientCertValidationCallback; + SslProtocols = configuration.SslProtocols; + Certificate = configuration.Certificate; + } + + public bool CheckForCertificateRevocation { get; set; } + + public bool IsClientCertificateRequired { get; set; } + + public RemoteCertificateValidationCallback ClientCertificateValidationCallback + { + get + { + _clientCertValidationCallback ??= ValidateClientCertificate; + + return _clientCertValidationCallback; + } + + set + { + _clientCertValidationCallback = value; + } + } + + public SslProtocols SslProtocols { get; set; } + + public X509Certificate2 Certificate { 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 new file mode 100644 index 0000000..e0d0546 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Stream/RequestStream.cs @@ -0,0 +1,238 @@ +// 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 long _bodyLeft; + private readonly byte[] _buffer; + private int _count; + private bool _disposed; + private int _offset; + private readonly Stream _stream; + + 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(); + } + } + + // 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 len = buffer.Length; + if (offset + count > len) + { + throw new ArgumentException( + "The sum of 'offset' and 'count' is greater than 'buffer' length."); + } + + if (_bodyLeft == 0) + { + return -1; + } + + if (_count == 0 || count == 0) + { + return 0; + } + + if (count > _count) + { + count = _count; + } + + if (_bodyLeft > 0 && count > _bodyLeft) + { + count = (int)_bodyLeft; + } + + Buffer.BlockCopy(_buffer, _offset, buffer, offset, count); + _offset += count; + _count -= count; + if (_bodyLeft > 0) + { + _bodyLeft -= count; + } + + return count; + } + + public override IAsyncResult BeginRead( + byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().ToString()); + } + + var nread = fillFromBuffer(buffer, offset, count); + if (nread > 0 || nread == -1) + { + var ares = new HttpStreamAsyncResult(callback, state); + ares.Buffer = buffer; + ares.Offset = offset; + ares.Count = count; + ares.SyncRead = nread > 0 ? nread : 0; + ares.Complete(); + + return ares; + } + + // Avoid reading past the end of the request to allow for HTTP pipelining. + if (_bodyLeft >= 0 && count > _bodyLeft) + { + count = (int)_bodyLeft; + } + + return _stream.BeginRead(buffer, offset, count, callback, state); + } + + public override IAsyncResult BeginWrite( + byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + throw new NotSupportedException(); + } + + public override void Close() + { + _disposed = true; + } + + public override int EndRead(IAsyncResult asyncResult) + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().ToString()); + } + + if (asyncResult == null) + { + throw new ArgumentNullException(nameof(asyncResult)); + } + + if (asyncResult is HttpStreamAsyncResult) + { + var ares = (HttpStreamAsyncResult)asyncResult; + if (!ares.IsCompleted) + { + ares.AsyncWaitHandle.WaitOne(); + } + + return ares.SyncRead; + } + + // Close on exception? + var nread = _stream.EndRead(asyncResult); + if (nread > 0 && _bodyLeft > 0) + { + _bodyLeft -= nread; + } + + return nread; + } + + public override void EndWrite(IAsyncResult asyncResult) + { + throw new NotSupportedException(); + } + + public override void Flush() + { + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().ToString()); + } + + // Call the fillFromBuffer method to check for buffer boundaries even when _bodyLeft is 0. + var nread = fillFromBuffer(buffer, offset, count); + if (nread == -1) // No more bytes available (Content-Length). + { + return 0; + } + + if (nread > 0) + { + return nread; + } + + nread = _stream.Read(buffer, offset, count); + if (nread > 0 && _bodyLeft > 0) + { + _bodyLeft -= nread; + } + + return nread; + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + } +} \ 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 new file mode 100644 index 0000000..c04f56a --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Core/Stream/ResponseStream.cs @@ -0,0 +1,286 @@ +// 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 MemoryStream _body; + private static readonly byte[] _crlf = new byte[] { 13, 10 }; + private bool _disposed; + private HttpListenerResponse _response; + private bool _sendChunked; + private Stream _stream; + private readonly Action _write; + private Action _writeBody; + private readonly Action _writeChunked; + + 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(); + } + } + + 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 static byte[] getChunkSizeBytes(int size, bool final) + { + return Encoding.ASCII.GetBytes(string.Format("{0:x}\r\n{1}", size, final ? "\r\n" : "")); + } + + private void writeChunked(byte[] buffer, int offset, int count) + { + var size = getChunkSizeBytes(count, false); + _stream.Write(size, 0, size.Length); + _stream.Write(buffer, offset, count); + _stream.Write(_crlf, 0, 2); + } + + private void writeChunkedWithoutThrowingException(byte[] buffer, int offset, int count) + { + try + { + writeChunked(buffer, offset, count); + } + catch + { + } + } + + private void writeWithoutThrowingException(byte[] buffer, int offset, int count) + { + try + { + _stream.Write(buffer, offset, count); + } + catch + { + } + } + + 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); + } + + public override IAsyncResult BeginRead( + byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + throw new NotSupportedException(); + } + + public override IAsyncResult BeginWrite( + byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().ToString()); + } + + return _body.BeginWrite(buffer, offset, count, callback, state); + } + + public override void Close() + { + Close(false); + } + + protected override void Dispose(bool disposing) + { + Close(!disposing); + } + + public override int EndRead(IAsyncResult asyncResult) + { + throw new NotSupportedException(); + } + + public override void EndWrite(IAsyncResult asyncResult) + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().ToString()); + } + + _body.EndWrite(asyncResult); + } + + public override void Flush() + { + if (!_disposed && (_sendChunked || _response.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); + } + } +} \ 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 new file mode 100644 index 0000000..bee44a3 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Endpoints/WelcomeEndpoint.cs @@ -0,0 +1,17 @@ +// 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 new file mode 100644 index 0000000..907fc06 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/EventArgs/CloseEventArgs.cs @@ -0,0 +1,48 @@ +// 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() + { + PayloadData = PayloadData.Empty; + } + + internal CloseEventArgs(ushort code) + : this(code, null) + { + } + + internal CloseEventArgs(CloseStatusCode code) + : this((ushort)code, null) + { + } + + internal CloseEventArgs(PayloadData payloadData) + { + PayloadData = payloadData; + } + + internal CloseEventArgs(ushort code, string reason) + { + PayloadData = new PayloadData(code, reason); + } + + internal CloseEventArgs(CloseStatusCode code, string reason) + : this((ushort)code, reason) + { + } + + internal PayloadData PayloadData { get; } + + public ushort Code => PayloadData.Code; + + public string Reason => PayloadData.Reason ?? string.Empty; + + public bool WasClean { get; internal set; } + } +} \ 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 new file mode 100644 index 0000000..deac474 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/EventArgs/ErrorEventArgs.cs @@ -0,0 +1,25 @@ +// 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 new file mode 100644 index 0000000..6ce3eb0 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/EventArgs/MessageEventArgs.cs @@ -0,0 +1,74 @@ +// 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 string _data; + private bool _dataSet; + private readonly byte[] _rawData; + + internal MessageEventArgs(WebSocketFrame frame) + { + Opcode = frame.Opcode; + _rawData = frame.PayloadData.ApplicationData; + } + + internal MessageEventArgs(Opcode opcode, byte[] rawData) + { + if ((ulong)rawData.LongLength > PayloadData.MaxLength) + { + throw new WebSocketException(CloseStatusCode.TooBig); + } + + Opcode = opcode; + _rawData = rawData; + } + + internal Opcode Opcode { get; } + + public string Data + { + get + { + setData(); + return _data; + } + } + + public bool IsBinary => Opcode == Opcode.Binary; + + public bool IsPing => Opcode == Opcode.Ping; + + public bool IsText => Opcode == Opcode.Text; + + public byte[] RawData + { + get + { + setData(); + return _rawData; + } + } + + private void setData() + { + if (_dataSet) + { + return; + } + + if (Opcode == Opcode.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 new file mode 100644 index 0000000..b21a19b --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Extensions.cs @@ -0,0 +1,1644 @@ +// 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.Net.Sockets; +using System.Text; + +namespace EonaCat.Network +{ + public static class Ext + { + private static readonly byte[] _last = new byte[] { 0x00 }; + private static readonly int _retry = 5; + private const string _tspecials = "()<>@,;:\\\"/[]?={} \t"; + private const int BUFFER_SIZE = 1024; + + 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(); + } + } + + 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) + { + if (completed != null) + { + completed(); + } + + return; + } + + destination.Write(buff, 0, nread); + source.BeginRead(buff, 0, bufferLength, callback, null); + } + catch (Exception ex) + { + if (error != null) + { + error(ex); + } + } + }; + + try + { + source.BeginRead(buff, 0, bufferLength, callback, null); + } + catch (Exception ex) + { + if (error != null) + { + error(ex); + } + } + } + + internal static byte[] Decompress(this byte[] data, CompressionMethod method) + { + return method == CompressionMethod.Deflate + ? data.decompress() + : data; + } + + internal static Stream Decompress(this Stream stream, CompressionMethod method) + { + return method == CompressionMethod.Deflate + ? stream.decompress() + : stream; + } + + internal static byte[] DecompressToArray(this Stream stream, CompressionMethod method) + { + return method == CompressionMethod.Deflate + ? stream.decompressToArray() + : stream.ToByteArray(); + } + + 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.MandatoryExtension + ? "WebSocket client didn't receive expected extension(s)." + : code == CloseStatusCode.ServerError + ? "WebSocket server got an internal error." + : code == CloseStatusCode.TlsHandshakeFailure + ? "An error has occurred during a TLS handshake." + : string.Empty; + } + + internal static 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 Opcode opcode) + { + return opcode >= Opcode.Close; + } + + internal static bool IsData(this byte opcode) + { + return opcode == 0x1 || opcode == 0x2; + } + + internal static bool IsData(this Opcode opcode) + { + return opcode == Opcode.Text || opcode == Opcode.Binary; + } + + internal static bool IsPortNumber(this int value) + { + return value > 0 && value < 65536; + } + + internal static bool IsReserved(this ushort code) + { + return code == 1004 + || code == 1005 + || code == 1006 + || code == 1015; + } + + internal static bool IsReserved(this CloseStatusCode code) + { + return code == CloseStatusCode.Undefined + || code == CloseStatusCode.NoStatus + || code == CloseStatusCode.Abnormal + || code == CloseStatusCode.TlsHandshakeFailure; + } + + internal static bool IsSupported(this byte opcode) + { + return Enum.IsDefined(typeof(Opcode), opcode); + } + + internal static bool IsText(this string value) + { + var len = value.Length; + + for (var i = 0; i < len; i++) + { + var c = value[i]; + if (c < 0x20) + { + if (!"\r\n\t".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 == 0 || nread == length) + { + if (completed != null) + { + completed(buff.SubArray(0, offset + nread)); + } + + return; + } + + retry = 0; + + offset += nread; + length -= nread; + + stream.BeginRead(buff, offset, length, callback, null); + } + catch (Exception ex) + { + if (error != null) + { + error(ex); + } + } + }; + + try + { + stream.BeginRead(buff, offset, length, callback, null); + } + catch (Exception ex) + { + if (error != null) + { + error(ex); + } + } + } + + internal static void ReadBytesAsync( + this Stream stream, + long length, + int bufferLength, + Action completed, + Action error + ) + { + var dest = new MemoryStream(); + var buff = new byte[bufferLength]; + var retry = 0; + + Action read = null; + read = + len => + { + if (len < bufferLength) + { + bufferLength = (int)len; + } + + stream.BeginRead( + buff, + 0, + bufferLength, + ar => + { + try + { + var nread = stream.EndRead(ar); + if (nread > 0) + { + 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(); + if (error != null) + { + error(ex); + } + } + }, + null + ); + }; + + try + { + read(length); + } + catch (Exception ex) + { + dest.Dispose(); + if (error != null) + { + error(ex); + } + } + } + + internal static string RemovePrefix(this string value, params string[] prefixes) + { + var idx = 0; + foreach (var prefix in prefixes) + { + if (value.StartsWith(prefix)) + { + idx = prefix.Length; + break; + } + } + + return idx > 0 ? value.Substring(idx) : value; + } + + internal static T[] Reverse(this T[] array) + { + var len = array.Length; + var reverse = new T[len]; + + var 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) + { + if (value == null || value.Length == 0) + { + return null; + } + + System.Net.IPAddress addr; + if (System.Net.IPAddress.TryParse(value, out addr)) + { + return addr; + } + + try + { + var addrs = System.Net.Dns.GetHostAddresses(value); + return addrs[0]; + } + catch + { + return null; + } + } + + internal static List ToList(this IEnumerable source) + { + return new List(source); + } + + internal static string ToString( + this System.Net.IPAddress address, bool bracketIPv6 + ) + { + return bracketIPv6 && address.AddressFamily == AddressFamily.InterNetworkV6 + ? string.Format("[{0}]", address.ToString()) + : address.ToString(); + } + + internal static ushort ToUInt16(this byte[] source, ByteOrder sourceOrder) + { + return BitConverter.ToUInt16(source.ToHostOrder(sourceOrder), 0); + } + + internal static ulong ToUInt64(this byte[] source, ByteOrder sourceOrder) + { + return BitConverter.ToUInt64(source.ToHostOrder(sourceOrder), 0); + } + + internal static 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, + () => + { + if (completed != null) + { + completed(); + } + + input.Dispose(); + }, + ex => + { + input.Dispose(); + if (error != null) + { + error(ex); + } + }); + } + + 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) + { + if (eventHandler != null) + { + eventHandler(sender, e); + } + } + + public static void Emit( + this EventHandler eventHandler, object sender, TEventArgs e) + where TEventArgs : EventArgs + { + if (eventHandler != null) + { + eventHandler(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)); + } + + 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; + } + } + + 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) + : WebSocket.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 ret; + Uri.TryCreate( + value, value.MaybeUri() ? UriKind.Absolute : UriKind.Relative, out 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(); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/FinalFrame.cs b/EonaCat.Network/System/Sockets/Web/FinalFrame.cs new file mode 100644 index 0000000..1b1f32e --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/FinalFrame.cs @@ -0,0 +1,12 @@ +// 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 new file mode 100644 index 0000000..14a2d0f --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Mask.cs @@ -0,0 +1,12 @@ +// 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/Opcode.cs b/EonaCat.Network/System/Sockets/Web/Opcode.cs new file mode 100644 index 0000000..b305857 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Opcode.cs @@ -0,0 +1,20 @@ +// 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 Opcode : byte + { + Cont = 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/PayloadData.cs b/EonaCat.Network/System/Sockets/Web/PayloadData.cs new file mode 100644 index 0000000..d55082d --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/PayloadData.cs @@ -0,0 +1,142 @@ +// 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 PayloadData : IEnumerable + { + private ushort _code; + private bool _codeSet; + private readonly byte[] _data; + private readonly long _length; + private string _reason; + private bool _reasonSet; + + public static readonly PayloadData Empty; + + public static readonly ulong MaxLength; + + static PayloadData() + { + Empty = new PayloadData(); + MaxLength = long.MaxValue; + } + + internal PayloadData() + { + _code = 1005; + _reason = string.Empty; + + _data = WebSocket.EmptyBytes; + + _codeSet = true; + _reasonSet = true; + } + + internal PayloadData(byte[] data) + : this(data, data.LongLength) + { + } + + internal PayloadData(byte[] data, long length) + { + _data = data; + _length = length; + } + + internal PayloadData(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)1005; + + _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) + : WebSocket.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/Rsv.cs b/EonaCat.Network/System/Sockets/Web/Rsv.cs new file mode 100644 index 0000000..de067d4 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Rsv.cs @@ -0,0 +1,12 @@ +// 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 Rsv : 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 new file mode 100644 index 0000000..acf1465 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Server/HttpRequestEventArgs.cs @@ -0,0 +1,103 @@ +// 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; + + private string createFilePath(string childPath) + { + childPath = childPath.TrimStart('/', '\\'); + return new StringBuilder(_docRootPath, 32) + .AppendFormat("/{0}", childPath) + .ToString() + .Replace('\\', '/'); + } + + private static bool tryReadFile(string path, out byte[] contents) + { + contents = null; + + if (!File.Exists(path)) + { + return false; + } + + try + { + contents = File.ReadAllBytes(path); + } + catch + { + return false; + } + + return true; + } + + 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)); + } + + byte[] contents; + tryReadFile(createFilePath(path), out 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); + } + } +} \ 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 new file mode 100644 index 0000000..ce95433 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Server/HttpServer.cs @@ -0,0 +1,886 @@ +// 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; +using System.Threading; + +namespace EonaCat.Network +{ + public class HttpServer + { + private string _docRootPath; + private string _hostname; + private HttpListener _listener; + private Thread _receiveThread; + private volatile ServerState _state; + private object _sync; + + public HttpServer() + { + init("*", System.Net.IPAddress.Any, 80, false); + } + + public HttpServer(int port) + : this(port, port == 443) + { + } + + public HttpServer(string url) + { + if (url == null) + { + throw new ArgumentNullException(nameof(url)); + } + + if (url.Length == 0) + { + throw new ArgumentException("An empty string.", nameof(url)); + } + + Uri uri; + string msg; + if (!tryCreateUri(url, out uri, out msg)) + { + throw new ArgumentException(msg, nameof(url)); + } + + var host = uri.GetDnsSafeHost(true); + + var addr = host.ToIPAddress(); + if (addr == null) + { + msg = "The host part could not be converted to an IP address."; + throw new ArgumentException(msg, nameof(url)); + } + + if (!addr.IsLocal()) + { + msg = "The IP address of the host is not a local IP address."; + throw new ArgumentException(msg, nameof(url)); + } + + init(host, addr, uri.Port, uri.Scheme == "https"); + } + + public HttpServer(int port, bool secure) + { + if (!port.IsPortNumber()) + { + var msg = "Less than 1 or greater than 65535."; + throw new ArgumentOutOfRangeException(nameof(port), msg); + } + + 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 msg = "Less than 1 or greater than 65535."; + throw new ArgumentOutOfRangeException(nameof(port), msg); + } + + init(address.ToString(true), address, port, secure); + } + + public System.Net.IPAddress Address { get; private set; } + + public AuthenticationSchemes AuthenticationSchemes + { + get + { + return _listener.AuthenticationSchemes; + } + + set + { + string msg; + if (!canSet(out msg)) + { + Logger.Warning(msg); + return; + } + + lock (_sync) + { + if (!canSet(out msg)) + { + Logger.Warning(msg); + 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)); + } + + string msg; + if (!canSet(out msg)) + { + Logger.Warning(msg); + return; + } + + lock (_sync) + { + if (!canSet(out msg)) + { + Logger.Warning(msg); + return; + } + + _docRootPath = value; + } + } + } + + public bool IsListening => _state == ServerState.Start; + + public bool IsSecure { get; private set; } + + public bool KeepClean + { + get + { + return WebSocketServices.AutoCleanSessions; + } + + set + { + WebSocketServices.AutoCleanSessions = value; + } + } + + public bool IsLoggingEnabled { get; private set; } + + public int Port { get; private set; } + + public string Realm + { + get + { + return _listener.Realm; + } + + set + { + string msg; + if (!canSet(out msg)) + { + Logger.Warning(msg); + return; + } + + lock (_sync) + { + if (!canSet(out msg)) + { + Logger.Warning(msg); + return; + } + + _listener.Realm = value; + } + } + } + + public bool ReuseAddress + { + get + { + return _listener.ReuseAddress; + } + + set + { + string msg; + if (!canSet(out msg)) + { + Logger.Warning(msg); + return; + } + + lock (_sync) + { + if (!canSet(out msg)) + { + Logger.Warning(msg); + return; + } + + _listener.ReuseAddress = value; + } + } + } + + public SSLConfigurationServer SslConfiguration + { + get + { + if (!IsSecure) + { + var msg = "This instance does not provide secure connections."; + throw new InvalidOperationException(msg); + } + + return _listener.SslConfiguration; + } + } + + public Func UserCredentialsFinder + { + get + { + return _listener.UserCredentialsFinder; + } + + set + { + string msg; + if (!canSet(out msg)) + { + Logger.Warning(msg); + return; + } + + lock (_sync) + { + if (!canSet(out msg)) + { + Logger.Warning(msg); + return; + } + + _listener.UserCredentialsFinder = value; + } + } + } + + public TimeSpan WaitTime + { + get + { + return WebSocketServices.WaitTime; + } + + set + { + WebSocketServices.WaitTime = value; + } + } + + public WSEndpointManager WebSocketServices { get; private set; } + + 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; + + private void abort() + { + lock (_sync) + { + if (_state != ServerState.Start) + { + return; + } + + _state = ServerState.ShuttingDown; + } + + try + { + try + { + WebSocketServices.Stop(1006, string.Empty); + } + finally + { + _listener.Abort(); + } + } + catch + { + } + + _state = ServerState.Stop; + } + + private bool canSet(out string message) + { + message = null; + + if (_state == ServerState.Start) + { + message = "The server has already started."; + return false; + } + + if (_state == ServerState.ShuttingDown) + { + message = "The server is shutting down."; + return false; + } + + return true; + } + + private bool checkCertificate(out string message) + { + message = null; + + var byUser = _listener.SslConfiguration.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 string createFilePath(string childPath) + { + childPath = childPath.TrimStart('/', '\\'); + return new StringBuilder(_docRootPath, 32) + .AppendFormat("/{0}", childPath) + .ToString() + .Replace('\\', '/'); + } + + private static HttpListener createListener( + string hostname, int port, bool secure + ) + { + var lsnr = new HttpListener(); + + var schm = secure ? "https" : "http"; + var pref = string.Format("{0}://{1}:{2}/", schm, hostname, port); + lsnr.Prefixes.Add(pref); + + return lsnr; + } + + private void init( + string hostname, System.Net.IPAddress address, int port, bool secure + ) + { + _hostname = hostname; + Address = address; + Port = port; + IsSecure = secure; + + _docRootPath = "./Public"; + _listener = createListener(_hostname, Port, IsSecure); + WebSocketServices = new WSEndpointManager(); + _sync = new object(); + } + + private void processRequest(HttpListenerContext context) + { + var method = context.Request.HttpMethod; + var evt = method == "GET" + ? OnGet + : method == "HEAD" + ? OnHead + : method == "POST" + ? OnPost + : method == "PUT" + ? OnPut + : method == "DELETE" + ? OnDelete + : method == "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(HttpListenerWebSocketContext context) + { + var path = context.RequestUri.AbsolutePath; + + WSEndpointHost host; + if (!WebSocketServices.InternalTryGetServiceHost(path, out host)) + { + context.Close(HttpStatusCode.NotImplemented); + return; + } + + host.StartSession(context); + } + + private void receiveRequest() + { + while (true) + { + HttpListenerContext ctx = null; + try + { + ctx = _listener.GetContext(); + ThreadPool.QueueUserWorkItem( + state => + { + try + { + if (ctx.Request.IsUpgradeTo("websocket")) + { + processRequest(ctx.AcceptWebSocket(null)); + return; + } + + processRequest(ctx); + } + catch (Exception ex) + { + Logger.Error(ex.Message); + Logger.Debug(ex.ToString()); + + ctx.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()); + + if (ctx != null) + { + ctx.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 (_sync) + { + if (_state == ServerState.Start) + { + Logger.Info("The server has already started."); + return; + } + + if (_state == ServerState.ShuttingDown) + { + Logger.Warning("The server is shutting down."); + return; + } + + WebSocketServices.Start(); + + try + { + startReceiving(); + } + catch + { + WebSocketServices.Stop(1011, string.Empty); + throw; + } + + _state = ServerState.Start; + } + } + + private void startReceiving() + { + try + { + _listener.Start(); + } + catch (Exception ex) + { + var msg = "The underlying listener has failed to start."; + throw new InvalidOperationException(msg, ex); + } + + _receiveThread = new Thread(new ThreadStart(receiveRequest)); + _receiveThread.IsBackground = true; + _receiveThread.Start(); + } + + private void stop(ushort code, string reason) + { + if (_state == ServerState.Ready) + { + 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 (_sync) + { + 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 + { + WebSocketServices.Stop(code, reason); + } + catch + { + threw = true; + throw; + } + finally + { + try + { + stopReceiving(5000); + } + catch + { + if (!threw) + { + throw; + } + } + } + } + finally + { + _state = ServerState.Stop; + } + } + + private void stopReceiving(int millisecondsTimeout) + { + _listener.Stop(); + _receiveThread.Join(millisecondsTimeout); + } + + private static bool tryCreateUri( + string uriString, out Uri result, out string message + ) + { + result = null; + message = null; + + var uri = uriString.ToUri(); + if (uri == null) + { + message = "An invalid URI string."; + return false; + } + + if (!uri.IsAbsoluteUri) + { + message = "A relative URI."; + return false; + } + + var schm = uri.Scheme; + if (!(schm == "http" || schm == "https")) + { + message = "The scheme part is not 'http' or 'https'."; + return false; + } + + if (uri.PathAndQuery != "/") + { + message = "It includes either or both path and query components."; + return false; + } + + if (uri.Fragment.Length > 0) + { + message = "It includes the fragment component."; + return false; + } + + if (uri.Port == 0) + { + message = "The port part is zero."; + return false; + } + + result = uri; + return true; + } + + public void AddWebSocketService(string path) + where TEndpoint : WSEndpoint, new() + { + WebSocketServices.AddService(path, null); + } + + public void AddWebSocketService( + string path, Action initializer + ) + where TEndpoint : WSEndpoint, new() + { + WebSocketServices.AddService(path, initializer); + } + + public bool RemoveWebSocketService(string path) + { + return WebSocketServices.RemoveService(path); + } + + public void Start() + { + if (IsSecure) + { + string msg; + if (!checkCertificate(out msg)) + { + throw new InvalidOperationException(msg); + } + } + + start(); + } + + public void Stop() + { + stop(1005, string.Empty); + } + + public void Stop(ushort code, string reason) + { + if (!code.IsCloseStatusCode()) + { + var msg = "Less than 1000 or greater than 4999."; + throw new ArgumentOutOfRangeException(nameof(code), msg); + } + + if (code == 1010) + { + var msg = "1010 cannot be used."; + throw new ArgumentException(msg, nameof(code)); + } + + if (!reason.IsNullOrEmpty()) + { + if (code == 1005) + { + var msg = "1005 cannot be used."; + throw new ArgumentException(msg, nameof(code)); + } + + byte[] bytes; + if (!reason.TryGetUTF8EncodedBytes(out bytes)) + { + var msg = "It could not be UTF-8-encoded."; + throw new ArgumentException(msg, nameof(reason)); + } + + if (bytes.Length > 123) + { + var msg = "Its size is greater than 123 bytes."; + throw new ArgumentOutOfRangeException(nameof(reason), msg); + } + } + + stop(code, reason); + } + + public void Stop(CloseStatusCode code, string reason) + { + if (code == CloseStatusCode.MandatoryExtension) + { + var msg = "MandatoryExtension cannot be used."; + throw new ArgumentException(msg, nameof(code)); + } + + if (!reason.IsNullOrEmpty()) + { + if (code == CloseStatusCode.NoStatus) + { + var msg = "NoStatus cannot be used."; + throw new ArgumentException(msg, nameof(code)); + } + + byte[] bytes; + if (!reason.TryGetUTF8EncodedBytes(out bytes)) + { + var msg = "It could not be UTF-8-encoded."; + throw new ArgumentException(msg, nameof(reason)); + } + + if (bytes.Length > 123) + { + var msg = "Its size is greater than 123 bytes."; + throw new ArgumentOutOfRangeException(nameof(reason), msg); + } + } + + stop((ushort)code, reason); + } + } +} \ 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 new file mode 100644 index 0000000..bc5bcc7 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Server/IWSSession.cs @@ -0,0 +1,20 @@ +// 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 + { + WebSocketContext Context { get; } + + string ID { get; } + + string Protocol { get; } + + DateTime StartTime { get; } + + WebSocketState 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 new file mode 100644 index 0000000..3fbfeef --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Server/ServerState.cs @@ -0,0 +1,13 @@ +// 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 + { + Ready, + 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 new file mode 100644 index 0000000..f3f5cdf --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Server/WSEndpoint.cs @@ -0,0 +1,235 @@ +// 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 WebSocket _websocket; + + protected WSEndpoint() + { + StartTime = DateTime.MaxValue; + } + + protected WSSessionManager Sessions { get; private set; } + + public WebSocketContext 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 != WebSocketState.Connecting) + { + return; + } + + if (value != null && (value.Length == 0 || !value.IsToken())) + { + return; + } + + _protocol = value; + } + } + + public DateTime StartTime { get; private set; } + + public WebSocketState State => _websocket != null ? _websocket.ReadyState : WebSocketState.Connecting; + + private string checkHandshakeRequest(WebSocketContext 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(); + } + + internal void Start(WebSocketContext 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 waitTime = sessions.WaitTime; + if (waitTime != _websocket.WaitTime) + { + _websocket.WaitTime = waitTime; + } + + _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) + { + if (_websocket != null) + { + _websocket.Send(data); + } + } + + protected void Send(FileInfo file) + { + if (_websocket != null) + { + _websocket.Send(file); + } + } + + protected void Send(string data) + { + if (_websocket != null) + { + _websocket.Send(data); + } + } + + protected void SendAsync(byte[] data, Action completed) + { + if (_websocket != null) + { + _websocket.SendAsync(data, completed); + } + } + + protected void SendAsync(FileInfo file, Action completed) + { + if (_websocket != null) + { + _websocket.SendAsync(file, completed); + } + } + + protected void SendAsync(string data, Action completed) + { + if (_websocket != null) + { + _websocket.SendAsync(data, completed); + } + } + + protected void SendAsync(Stream stream, int length, Action completed) + { + if (_websocket != null) + { + _websocket.SendAsync(stream, length, completed); + } + } + } +} \ 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 new file mode 100644 index 0000000..72f588f --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Server/WSEndpointHost.cs @@ -0,0 +1,67 @@ +// 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(); + } + + internal ServerState State => Sessions.State; + + public bool KeepClean + { + get + { + return Sessions.KeepClean; + } + + set + { + Sessions.KeepClean = value; + } + } + + public string Path { get; } + + public WSSessionManager Sessions { get; } + + public abstract Type EndpointType { get; } + + public TimeSpan WaitTime + { + get + { + return Sessions.WaitTime; + } + + set + { + Sessions.WaitTime = value; + } + } + + internal void Start() + { + Sessions.Start(); + } + + internal void StartSession(WebSocketContext context) + { + CreateSession().Start(context, Sessions); + } + + internal void Stop(ushort code, string reason) + { + Sessions.Stop(code, reason); + } + + 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 new file mode 100644 index 0000000..b4ccee5 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Server/WSEndpointManager.cs @@ -0,0 +1,514 @@ +// 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.Threading; + +namespace EonaCat.Network +{ + public class WSEndpointManager + { + private volatile bool _clean; + private readonly Dictionary _hosts; + private volatile ServerState _state; + private readonly object _sync; + private TimeSpan _waitTime; + + internal WSEndpointManager() + { + _clean = true; + _hosts = new Dictionary(); + _state = ServerState.Ready; + _sync = ((ICollection)_hosts).SyncRoot; + _waitTime = TimeSpan.FromSeconds(1); + } + + public int Count + { + get + { + lock (_sync) + { + return _hosts.Count; + } + } + } + + public IEnumerable Hosts + { + get + { + lock (_sync) + { + return _hosts.Values.ToList(); + } + } + } + + 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 msg = "It includes either or both query and fragment components."; + throw new ArgumentException(msg, nameof(path)); + } + + WSEndpointHost host; + InternalTryGetServiceHost(path, out host); + + return host; + } + } + + public bool AutoCleanSessions + { + get + { + return _clean; + } + + set + { + string msg; + if (!canSet(out msg)) + { + Logger.Warning(msg); + return; + } + + lock (_sync) + { + if (!canSet(out msg)) + { + Logger.Warning(msg); + return; + } + + foreach (var host in _hosts.Values) + { + host.KeepClean = value; + } + + _clean = value; + } + } + } + + public IEnumerable Paths + { + get + { + lock (_sync) + { + return _hosts.Keys.ToList(); + } + } + } + + public TimeSpan WaitTime + { + get + { + return _waitTime; + } + + set + { + if (value <= TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(value), "Zero or less."); + } + + string msg; + if (!canSet(out msg)) + { + Logger.Warning(msg); + return; + } + + lock (_sync) + { + if (!canSet(out msg)) + { + Logger.Warning(msg); + return; + } + + foreach (var host in _hosts.Values) + { + host.WaitTime = value; + } + + _waitTime = value; + } + } + } + + private void broadcast(Opcode 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); + } + + if (completed != null) + { + completed(); + } + } + catch (Exception ex) + { + Logger.Error(ex, "Could not broadcast"); + } + finally + { + cache.Clear(); + } + } + + private void broadcast(Opcode opcode, Stream stream, Action completed) + { + var cache = new Dictionary(); + + try + { + foreach (var host in Hosts) + { + if (_state != ServerState.Start) + { + Logger.Error("The server is shutting down."); + break; + } + + host.Sessions.Broadcast(opcode, stream, cache); + } + + if (completed != null) + { + completed(); + } + } + catch (Exception ex) + { + Logger.Error(ex, "Could not broadcast"); + } + finally + { + foreach (var cached in cache.Values) + { + cached.Dispose(); + } + + cache.Clear(); + } + } + + private void broadcastAsync(Opcode opcode, byte[] data, Action completed) + { + ThreadPool.QueueUserWorkItem( + state => broadcast(opcode, data, completed) + ); + } + + private void broadcastAsync(Opcode opcode, Stream stream, Action completed) + { + ThreadPool.QueueUserWorkItem( + state => broadcast(opcode, stream, completed) + ); + } + + private Dictionary> broadping( + byte[] frameAsBytes, TimeSpan timeout + ) + { + var ret = new Dictionary>(); + + foreach (var host in Hosts) + { + if (_state != ServerState.Start) + { + Logger.Error("The server is shutting down."); + break; + } + + var res = host.Sessions.Broadping(frameAsBytes, timeout); + ret.Add(host.Path, res); + } + + return ret; + } + + private bool canSet(out string message) + { + message = null; + + if (_state == ServerState.Start) + { + message = "The server has already started."; + return false; + } + + if (_state == ServerState.ShuttingDown) + { + message = "The server is shutting down."; + return false; + } + + return true; + } + + internal void Add(string path, Func creator) + where TEndpoint : WSEndpoint + { + path = HttpUtility.UrlDecode(path).TrimSlashFromEnd(); + + lock (_sync) + { + WSEndpointHost host; + if (_hosts.TryGetValue(path, out host)) + { + throw new ArgumentException("Already in use.", nameof(path)); + } + + host = new WebSocketEndpointHost( + path, creator, null + ); + + if (!_clean) + { + host.KeepClean = false; + } + + if (_waitTime != host.WaitTime) + { + host.WaitTime = _waitTime; + } + + if (_state == ServerState.Start) + { + host.Start(); + } + + _hosts.Add(path, host); + } + } + + internal bool InternalTryGetServiceHost( + string path, out WSEndpointHost host + ) + { + path = HttpUtility.UrlDecode(path).TrimSlashFromEnd(); + + lock (_sync) + { + return _hosts.TryGetValue(path, out host); + } + } + + internal void Start() + { + lock (_sync) + { + foreach (var host in _hosts.Values) + { + host.Start(); + } + + _state = ServerState.Start; + } + } + + internal void Stop(ushort code, string reason) + { + lock (_sync) + { + _state = ServerState.ShuttingDown; + + foreach (var host in _hosts.Values) + { + host.Stop(code, reason); + } + + _state = ServerState.Stop; + } + } + + public void AddService( + 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 msg = "It includes either or both query and fragment components."; + throw new ArgumentException(msg, nameof(path)); + } + + path = HttpUtility.UrlDecode(path).TrimSlashFromEnd(); + + lock (_sync) + { + WSEndpointHost host; + if (_hosts.TryGetValue(path, out host)) + { + throw new ArgumentException("Already in use.", nameof(path)); + } + + host = new WebSocketEndpointHost( + path, () => new TEndpoint(), initializer + ); + + if (!_clean) + { + host.KeepClean = false; + } + + if (_waitTime != host.WaitTime) + { + host.WaitTime = _waitTime; + } + + if (_state == ServerState.Start) + { + host.Start(); + } + + _hosts.Add(path, host); + } + } + + public void Clear() + { + List hosts = null; + + lock (_sync) + { + hosts = _hosts.Values.ToList(); + _hosts.Clear(); + } + + foreach (var host in hosts) + { + if (host.State == ServerState.Start) + { + host.Stop(1001, string.Empty); + } + } + } + + public bool RemoveService(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 msg = "It includes either or both query and fragment components."; + throw new ArgumentException(msg, nameof(path)); + } + + path = HttpUtility.UrlDecode(path).TrimSlashFromEnd(); + + WSEndpointHost host; + lock (_sync) + { + if (!_hosts.TryGetValue(path, out host)) + { + return false; + } + + _hosts.Remove(path); + } + + if (host.State == ServerState.Start) + { + host.Stop(1001, string.Empty); + } + + return true; + } + + public bool TryGetServiceHost(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 msg = "It includes either or both query and fragment components."; + throw new ArgumentException(msg, nameof(path)); + } + + return InternalTryGetServiceHost(path, out host); + } + } +} \ 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 new file mode 100644 index 0000000..a929cc4 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Server/WSServer.cs @@ -0,0 +1,808 @@ +// 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 + { + public bool IsConsoleLoggingEnabled { get; set; } + public bool IsLoggingEnabled { get; set; } + private bool _allowForwardedRequest; + private AuthenticationSchemes _authSchemes; + private static readonly string _defaultRealm; + private bool _dnsStyle; + private string _hostname; + private TcpListener _listener; + private string _realm; + private string _realmInUse; + private Thread _receiveThread; + private bool _reuseAddress; + private SSLConfigurationServer _sslConfig; + private SSLConfigurationServer _sslConfigInUse; + private volatile ServerState _state; + private object _sync; + private Func _userCredentialsFinder; + + static WSServer() + { + _defaultRealm = "SECRET AREA"; + } + + public WSServer() + { + var addr = System.Net.IPAddress.Any; + init(addr.ToString(), addr, 80, false); + } + + public WSServer(int port) + : this(port, port == 443) + { + } + + public WSServer(string url) + { + if (url == null) + { + throw new ArgumentNullException(nameof(url)); + } + + if (url.Length == 0) + { + throw new ArgumentException("An empty string.", nameof(url)); + } + + Uri uri; + string msg; + if (!tryCreateUri(url, out uri, out msg)) + { + throw new ArgumentException(msg, nameof(url)); + } + + var host = uri.DnsSafeHost; + + var addr = host.ToIPAddress(); + if (addr == null) + { + msg = "The host part could not be converted to an IP address."; + throw new ArgumentException(msg, nameof(url)); + } + + if (!addr.IsLocal()) + { + msg = "The IP address of the host is not a local IP address."; + throw new ArgumentException(msg, nameof(url)); + } + + init(host, addr, uri.Port, uri.Scheme == "wss"); + } + + public WSServer(int port, bool secure) + { + if (!port.IsPortNumber()) + { + var msg = "Less than 1 or greater than 65535."; + throw new ArgumentOutOfRangeException(nameof(port), msg); + } + + 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 msg = "Less than 1 or greater than 65535."; + throw new ArgumentOutOfRangeException(nameof(port), msg); + } + + init(address.ToString(), address, port, secure); + } + + public System.Net.IPAddress Address { get; private set; } + + public bool AllowForwardedRequest + { + get + { + return _allowForwardedRequest; + } + + set + { + string msg; + if (!CanSet(out msg)) + { + Logger.Warning(msg); + return; + } + + lock (_sync) + { + if (!CanSet(out msg)) + { + Logger.Warning(msg); + return; + } + + _allowForwardedRequest = value; + } + } + } + + public AuthenticationSchemes AuthenticationSchemes + { + get + { + return _authSchemes; + } + + set + { + string msg; + if (!CanSet(out msg)) + { + Logger.Warning(msg); + return; + } + + lock (_sync) + { + if (!CanSet(out msg)) + { + Logger.Warning(msg); + return; + } + + _authSchemes = value; + } + } + } + + public bool IsListening => _state == ServerState.Start; + + public bool IsSecure { get; private set; } + + /// + /// Determines if sessions need to be removed automatically + /// + public bool AutoCleanSessions + { + get + { + return Endpoints.AutoCleanSessions; + } + + set + { + Endpoints.AutoCleanSessions = value; + } + } + + public int Port { get; private set; } + + public string Realm + { + get + { + return _realm; + } + + set + { + string msg; + if (!CanSet(out msg)) + { + Logger.Warning(msg); + return; + } + + lock (_sync) + { + if (!CanSet(out msg)) + { + Logger.Warning(msg); + return; + } + + _realm = value; + } + } + } + + public bool ReuseAddress + { + get + { + return _reuseAddress; + } + + set + { + string msg; + if (!CanSet(out msg)) + { + Logger.Warning(msg); + return; + } + + lock (_sync) + { + if (!CanSet(out msg)) + { + Logger.Warning(msg); + return; + } + + _reuseAddress = value; + } + } + } + + public SSLConfigurationServer SslConfiguration + { + get + { + if (!IsSecure) + { + var msg = "This instance does not provide secure connections."; + throw new InvalidOperationException(msg); + } + + return GetSslConfiguration(); + } + } + + public Func FindCredentials + { + get + { + return _userCredentialsFinder; + } + + set + { + string message; + if (!CanSet(out message)) + { + Logger.Warning(message); + return; + } + + lock (_sync) + { + if (!CanSet(out message)) + { + Logger.Warning(message); + return; + } + + _userCredentialsFinder = value; + } + } + } + + public TimeSpan WaitTime + { + get + { + return Endpoints.WaitTime; + } + + set + { + Endpoints.WaitTime = value; + } + } + + public WSEndpointManager Endpoints { get; private set; } + + private void abort() + { + lock (_sync) + { + if (_state != ServerState.Start) + { + return; + } + + _state = ServerState.ShuttingDown; + } + + try + { + try + { + _listener.Stop(); + } + finally + { + Endpoints.Stop(1006, 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 static bool CheckSslConfiguration( + SSLConfigurationServer configuration, out string message + ) + { + message = null; + + if (configuration.Certificate == null) + { + message = "There is no server certificate for secure connections."; + return false; + } + + return true; + } + + private string GetRealm() + { + var realm = _realm; + return realm != null && realm.Length > 0 ? realm : _defaultRealm; + } + + private SSLConfigurationServer GetSslConfiguration() + { + _sslConfig ??= new SSLConfigurationServer(); + + 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(); + _sync = new object(); + } + + private void processRequest(TcpListenerWebSocketContext 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; + } + } + + WSEndpointHost host; + if (!Endpoints.InternalTryGetServiceHost(uri.AbsolutePath, out host)) + { + context.Close(HttpStatusCode.NotImplemented); + return; + } + + host.StartSession(context); + } + + private void receiveRequest() + { + while (true) + { + TcpClient cl = null; + try + { + cl = _listener.AcceptTcpClient(); + ThreadPool.QueueUserWorkItem( + state => + { + try + { + var ctx = new TcpListenerWebSocketContext( + cl, null, IsSecure, _sslConfigInUse); + + if (!ctx.Authenticate(_authSchemes, _realmInUse, _userCredentialsFinder)) + { + return; + } + + processRequest(ctx); + } + 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()); + + if (cl != null) + { + cl.Close(); + } + + break; + } + } + + if (_state != ServerState.ShuttingDown) + { + abort(); + } + } + + private void start(SSLConfigurationServer 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 (_sync) + { + 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(1011, string.Empty); + throw; + } + + _state = ServerState.Start; + } + } + + private void startReceiving() + { + if (_reuseAddress) + { + _listener.Server.SetSocketOption( + SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true + ); + } + + try + { + _listener.Start(); + } + catch (Exception ex) + { + var msg = "The underlying listener has failed to start."; + throw new InvalidOperationException(msg, ex); + } + + _receiveThread = new Thread(new ThreadStart(receiveRequest)); + _receiveThread.IsBackground = true; + _receiveThread.Start(); + } + + private void stop(ushort code, string reason) + { + if (_state == ServerState.Ready) + { + 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 (_sync) + { + 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 msg = "The underlying listener has failed to stop."; + throw new InvalidOperationException(msg, ex); + } + + _receiveThread.Join(millisecondsTimeout); + } + + private static bool tryCreateUri( + string uriString, out Uri result, out string message + ) + { + if (!uriString.TryCreateWebSocketUri(out result, out message)) + { + return false; + } + + if (result.PathAndQuery != "/") + { + result = null; + message = "It includes either or both path and query components."; + + return false; + } + + return true; + } + + public void AddEndpoint(string path) where TEndpoint : WSEndpoint, new() + { + Endpoints.AddService(path, null); + } + + public void AddEndpoint(string path, Action initializer) where TEndpoint : WSEndpoint, new() + { + Endpoints.AddService(path, initializer); + } + + public bool RemoveEndpoint(string path) + { + return Endpoints.RemoveService(path); + } + + public void Start() + { + SSLConfigurationServer sslConfig = null; + + if (IsSecure) + { + sslConfig = new SSLConfigurationServer(GetSslConfiguration()); + + string message; + if (!CheckSslConfiguration(sslConfig, out message)) + { + throw new InvalidOperationException(message); + } + } + + start(sslConfig); + } + + public void Stop() + { + stop(1005, string.Empty); + } + + public void Stop(ushort code, string reason) + { + if (!code.IsCloseStatusCode()) + { + var msg = "Less than 1000 or greater than 4999."; + throw new ArgumentOutOfRangeException(nameof(code), msg); + } + + if (code == 1010) + { + var msg = "1010 cannot be used."; + throw new ArgumentException(msg, nameof(code)); + } + + if (!reason.IsNullOrEmpty()) + { + if (code == 1005) + { + var msg = "1005 cannot be used."; + throw new ArgumentException(msg, nameof(code)); + } + + byte[] bytes; + if (!reason.TryGetUTF8EncodedBytes(out bytes)) + { + var msg = "It could not be UTF-8-encoded."; + throw new ArgumentException(msg, nameof(reason)); + } + + if (bytes.Length > 123) + { + var msg = "Its size is greater than 123 bytes."; + throw new ArgumentOutOfRangeException(nameof(reason), msg); + } + } + + stop(code, reason); + } + + public void Stop(CloseStatusCode code, string reason) + { + if (code == CloseStatusCode.MandatoryExtension) + { + var msg = "MandatoryExtension cannot be used."; + throw new ArgumentException(msg, nameof(code)); + } + + if (!reason.IsNullOrEmpty()) + { + if (code == CloseStatusCode.NoStatus) + { + var msg = "NoStatus cannot be used."; + throw new ArgumentException(msg, nameof(code)); + } + + byte[] bytes; + if (!reason.TryGetUTF8EncodedBytes(out bytes)) + { + var msg = "It could not be UTF-8-encoded."; + throw new ArgumentException(msg, nameof(reason)); + } + + if (bytes.Length > 123) + { + var msg = "Its size is greater than 123 bytes."; + throw new ArgumentOutOfRangeException(nameof(reason), msg); + } + } + + stop((ushort)code, reason); + } + } +} \ 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 new file mode 100644 index 0000000..71a5515 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Server/WSSessionManager.cs @@ -0,0 +1,893 @@ +// 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 volatile bool _clean; + private readonly object _forSweep; + private readonly Dictionary _sessions; + private volatile ServerState _state; + private volatile bool _sweeping; + private System.Timers.Timer _sweepTimer; + private readonly object _sync; + private TimeSpan _waitTime; + + internal WSSessionManager() + { + _clean = true; + _forSweep = new object(); + _sessions = new Dictionary(); + _state = ServerState.Ready; + _sync = ((ICollection)_sessions).SyncRoot; + _waitTime = TimeSpan.FromSeconds(1); + + setSweepTimer(60000); + } + + internal ServerState State => _state; + + public IEnumerable ActiveIDs + { + get + { + foreach (var res in broadping(WebSocketFrame.EmptyPingBytes)) + { + if (res.Value) + { + yield return res.Key; + } + } + } + } + + public int Count + { + get + { + lock (_sync) + { + return _sessions.Count; + } + } + } + + public IEnumerable IDs + { + get + { + if (_state != ServerState.Start) + { + return Enumerable.Empty(); + } + + lock (_sync) + { + if (_state != ServerState.Start) + { + return Enumerable.Empty(); + } + + return _sessions.Keys.ToList(); + } + } + } + + public IEnumerable InactiveIDs + { + get + { + foreach (var res in broadping(WebSocketFrame.EmptyPingBytes)) + { + if (!res.Value) + { + yield return res.Key; + } + } + } + } + + 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)); + } + + IWSSession session; + tryGetSession(id, out session); + + return session; + } + } + + public bool KeepClean + { + get + { + return _clean; + } + + set + { + string msg; + if (!canSet(out msg)) + { + Logger.Warning(msg); + return; + } + + lock (_sync) + { + if (!canSet(out msg)) + { + Logger.Warning(msg); + return; + } + + _clean = value; + } + } + } + + public IEnumerable Sessions + { + get + { + if (_state != ServerState.Start) + { + return Enumerable.Empty(); + } + + lock (_sync) + { + if (_state != ServerState.Start) + { + return Enumerable.Empty(); + } + + return _sessions.Values.ToList(); + } + } + } + + public TimeSpan WaitTime + { + get + { + return _waitTime; + } + + set + { + if (value <= TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(value), "Zero or less."); + } + + string msg; + if (!canSet(out msg)) + { + Logger.Warning(msg); + return; + } + + lock (_sync) + { + if (!canSet(out msg)) + { + Logger.Warning(msg); + return; + } + + _waitTime = value; + } + } + } + + private void broadcast(Opcode opcode, byte[] data, Action completed) + { + var cache = new Dictionary(); + + try + { + foreach (var session in Sessions) + { + if (_state != ServerState.Start) + { + Logger.Error("The service is shutting down."); + break; + } + + session.Context.WebSocket.Send(opcode, data, cache); + } + + if (completed != null) + { + completed(); + } + } + catch (Exception ex) + { + Logger.Error(ex.Message); + Logger.Debug(ex.ToString()); + } + finally + { + cache.Clear(); + } + } + + private void broadcast(Opcode opcode, Stream stream, Action completed) + { + var cache = new Dictionary(); + + try + { + foreach (var session in Sessions) + { + if (_state != ServerState.Start) + { + Logger.Error("The service is shutting down."); + break; + } + + session.Context.WebSocket.Send(opcode, stream, cache); + } + + if (completed != null) + { + completed(); + } + } + 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(Opcode opcode, byte[] data, Action completed) + { + ThreadPool.QueueUserWorkItem( + state => broadcast(opcode, data, completed) + ); + } + + private void broadcastAsync(Opcode opcode, Stream stream, Action completed) + { + ThreadPool.QueueUserWorkItem( + state => broadcast(opcode, stream, completed) + ); + } + + private Dictionary broadping(byte[] frameAsBytes) + { + var ret = new Dictionary(); + + foreach (var session in Sessions) + { + if (_state != ServerState.Start) + { + Logger.Error("The service is shutting down."); + break; + } + + var res = session.Context.WebSocket.Ping(frameAsBytes, _waitTime); + ret.Add(session.ID, res); + } + + return ret; + } + + private bool canSet(out string message) + { + message = null; + + if (_state == ServerState.Start) + { + message = "The service has already started."; + return false; + } + + if (_state == ServerState.ShuttingDown) + { + message = "The service is shutting down."; + return false; + } + + return true; + } + + private static string createID() + { + return Guid.NewGuid().ToString("N"); + } + + private void setSweepTimer(double interval) + { + _sweepTimer = new System.Timers.Timer(interval); + _sweepTimer.Elapsed += (sender, e) => Sweep(); + } + + private void stop(PayloadData payloadData, bool send) + { + var bytes = send + ? WebSocketFrame.CreateCloseFrame(payloadData, false).ToArray() + : null; + + lock (_sync) + { + _state = ServerState.ShuttingDown; + + _sweepTimer.Enabled = false; + foreach (var session in _sessions.Values.ToList()) + { + session.Context.WebSocket.Close(payloadData, bytes); + } + + _state = ServerState.Stop; + } + } + + private bool tryGetSession(string id, out IWSSession session) + { + session = null; + + if (_state != ServerState.Start) + { + return false; + } + + lock (_sync) + { + if (_state != ServerState.Start) + { + return false; + } + + return _sessions.TryGetValue(id, out session); + } + } + + internal string Add(IWSSession session) + { + lock (_sync) + { + if (_state != ServerState.Start) + { + return null; + } + + var id = createID(); + _sessions.Add(id, session); + + return id; + } + } + + internal void Broadcast( + Opcode opcode, byte[] data, Dictionary cache + ) + { + foreach (var session in Sessions) + { + if (_state != ServerState.Start) + { + Logger.Error("The service is shutting down."); + break; + } + + session.Context.WebSocket.Send(opcode, data, cache); + } + } + + internal void Broadcast( + Opcode opcode, Stream stream, Dictionary cache + ) + { + foreach (var session in Sessions) + { + if (_state != ServerState.Start) + { + Logger.Error("The service is shutting down."); + break; + } + + session.Context.WebSocket.Send(opcode, stream, cache); + } + } + + internal Dictionary Broadping( + byte[] frameAsBytes, TimeSpan timeout + ) + { + var ret = new Dictionary(); + + foreach (var session in Sessions) + { + if (_state != ServerState.Start) + { + Logger.Error("The service is shutting down."); + break; + } + + var res = session.Context.WebSocket.Ping(frameAsBytes, timeout); + ret.Add(session.ID, res); + } + + return ret; + } + + internal bool Remove(string id) + { + lock (_sync) + { + return _sessions.Remove(id); + } + } + + internal void Start() + { + lock (_sync) + { + _sweepTimer.Enabled = _clean; + _state = ServerState.Start; + } + } + + internal void Stop(ushort code, string reason) + { + if (code == 1005) + { // == no status + stop(PayloadData.Empty, true); + return; + } + + stop(new PayloadData(code, reason), !code.IsReserved()); + } + + public void Broadcast(byte[] data) + { + if (_state != ServerState.Start) + { + var msg = "The current state of the manager is not Start."; + throw new InvalidOperationException(msg); + } + + if (data == null) + { + throw new ArgumentNullException(nameof(data)); + } + + if (data.LongLength <= WebSocket.FragmentLength) + { + broadcast(Opcode.Binary, data, null); + } + else + { + broadcast(Opcode.Binary, new MemoryStream(data), null); + } + } + + public void Broadcast(string data) + { + if (_state != ServerState.Start) + { + var msg = "The current state of the manager is not Start."; + throw new InvalidOperationException(msg); + } + + if (data == null) + { + throw new ArgumentNullException(nameof(data)); + } + + byte[] bytes; + if (!data.TryGetUTF8EncodedBytes(out bytes)) + { + var msg = "It could not be UTF-8-encoded."; + throw new ArgumentException(msg, nameof(data)); + } + + if (bytes.LongLength <= WebSocket.FragmentLength) + { + broadcast(Opcode.Text, bytes, null); + } + else + { + broadcast(Opcode.Text, new MemoryStream(bytes), null); + } + } + + public void Broadcast(Stream stream, int length) + { + if (_state != ServerState.Start) + { + var msg = "The current state of the manager is not Start."; + throw new InvalidOperationException(msg); + } + + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + if (!stream.CanRead) + { + var msg = "It cannot be read."; + throw new ArgumentException(msg, nameof(stream)); + } + + if (length < 1) + { + var msg = "Less than 1."; + throw new ArgumentException(msg, nameof(length)); + } + + var bytes = stream.ReadBytes(length); + + var len = bytes.Length; + if (len == 0) + { + var msg = "No data could be read from it."; + throw new ArgumentException(msg, nameof(stream)); + } + + if (len < length) + { + Logger.Warning( + string.Format( + "Only {0} byte(s) of data could be read from the stream.", + len + ) + ); + } + + if (len <= WebSocket.FragmentLength) + { + broadcast(Opcode.Binary, bytes, null); + } + else + { + broadcast(Opcode.Binary, new MemoryStream(bytes), null); + } + } + + public void BroadcastAsync(byte[] data, Action completed) + { + if (_state != ServerState.Start) + { + var msg = "The current state of the manager is not Start."; + throw new InvalidOperationException(msg); + } + + if (data == null) + { + throw new ArgumentNullException(nameof(data)); + } + + if (data.LongLength <= WebSocket.FragmentLength) + { + broadcastAsync(Opcode.Binary, data, completed); + } + else + { + broadcastAsync(Opcode.Binary, new MemoryStream(data), completed); + } + } + + public void BroadcastAsync(string data, Action completed) + { + if (_state != ServerState.Start) + { + var msg = "The current state of the manager is not Start."; + throw new InvalidOperationException(msg); + } + + if (data == null) + { + throw new ArgumentNullException(nameof(data)); + } + + byte[] bytes; + if (!data.TryGetUTF8EncodedBytes(out bytes)) + { + var msg = "It could not be UTF-8-encoded."; + throw new ArgumentException(msg, nameof(data)); + } + + if (bytes.LongLength <= WebSocket.FragmentLength) + { + broadcastAsync(Opcode.Text, bytes, completed); + } + else + { + broadcastAsync(Opcode.Text, new MemoryStream(bytes), completed); + } + } + + public void BroadcastAsync(Stream stream, int length, Action completed) + { + if (_state != ServerState.Start) + { + var msg = "The current state of the manager is not Start."; + throw new InvalidOperationException(msg); + } + + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + if (!stream.CanRead) + { + var msg = "It cannot be read."; + throw new ArgumentException(msg, nameof(stream)); + } + + if (length < 1) + { + var msg = "Less than 1."; + throw new ArgumentException(msg, nameof(length)); + } + + var bytes = stream.ReadBytes(length); + + var len = bytes.Length; + if (len == 0) + { + var msg = "No data could be read from it."; + throw new ArgumentException(msg, nameof(stream)); + } + + if (len < length) + { + Logger.Warning( + string.Format( + "Only {0} byte(s) of data could be read from the stream.", + len + ) + ); + } + + if (len <= WebSocket.FragmentLength) + { + broadcastAsync(Opcode.Binary, bytes, completed); + } + else + { + broadcastAsync(Opcode.Binary, new MemoryStream(bytes), completed); + } + } + + public void CloseSession(string id) + { + IWSSession session; + if (!TryGetSession(id, out session)) + { + var msg = "The session could not be found."; + throw new InvalidOperationException(msg); + } + + session.Context.WebSocket.Close(); + } + + public void CloseSession(string id, ushort code, string reason) + { + IWSSession session; + if (!TryGetSession(id, out session)) + { + var msg = "The session could not be found."; + throw new InvalidOperationException(msg); + } + + session.Context.WebSocket.Close(code, reason); + } + + public void CloseSession(string id, CloseStatusCode code, string reason) + { + IWSSession session; + if (!TryGetSession(id, out session)) + { + var msg = "The session could not be found."; + throw new InvalidOperationException(msg); + } + + session.Context.WebSocket.Close(code, reason); + } + + public bool PingTo(string id) + { + IWSSession session; + if (!TryGetSession(id, out session)) + { + var msg = "The session could not be found."; + throw new InvalidOperationException(msg); + } + + return session.Context.WebSocket.Ping(); + } + + public bool PingTo(string message, string id) + { + IWSSession session; + if (!TryGetSession(id, out session)) + { + var msg = "The session could not be found."; + throw new InvalidOperationException(msg); + } + + return session.Context.WebSocket.Ping(message); + } + + public void SendTo(byte[] data, string id) + { + IWSSession session; + if (!TryGetSession(id, out session)) + { + var msg = "The session could not be found."; + throw new InvalidOperationException(msg); + } + + session.Context.WebSocket.Send(data); + } + + public void SendTo(string data, string id) + { + IWSSession session; + if (!TryGetSession(id, out session)) + { + var msg = "The session could not be found."; + throw new InvalidOperationException(msg); + } + + session.Context.WebSocket.Send(data); + } + + public void SendTo(Stream stream, int length, string id) + { + IWSSession session; + if (!TryGetSession(id, out session)) + { + var msg = "The session could not be found."; + throw new InvalidOperationException(msg); + } + + session.Context.WebSocket.Send(stream, length); + } + + public void SendToAsync(byte[] data, string id, Action completed) + { + IWSSession session; + if (!TryGetSession(id, out session)) + { + var msg = "The session could not be found."; + throw new InvalidOperationException(msg); + } + + session.Context.WebSocket.SendAsync(data, completed); + } + + public void SendToAsync(string data, string id, Action completed) + { + IWSSession session; + if (!TryGetSession(id, out session)) + { + var msg = "The session could not be found."; + throw new InvalidOperationException(msg); + } + + session.Context.WebSocket.SendAsync(data, completed); + } + + public void SendToAsync( + Stream stream, int length, string id, Action completed + ) + { + IWSSession session; + if (!TryGetSession(id, out session)) + { + var msg = "The session could not be found."; + throw new InvalidOperationException(msg); + } + + session.Context.WebSocket.SendAsync(stream, length, completed); + } + + public void Sweep() + { + if (_sweeping) + { + Logger.Info("The sweeping is already in progress."); + return; + } + + lock (_forSweep) + { + if (_sweeping) + { + Logger.Info("The sweeping is already in progress."); + return; + } + + _sweeping = true; + } + + foreach (var id in InactiveIDs) + { + if (_state != ServerState.Start) + { + break; + } + + lock (_sync) + { + if (_state != ServerState.Start) + { + break; + } + + IWSSession session; + if (_sessions.TryGetValue(id, out session)) + { + var state = session.State; + if (state == WebSocketState.Open) + { + session.Context.WebSocket.Close(CloseStatusCode.Abnormal); + } + else if (state == WebSocketState.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); + } + } +} \ 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 new file mode 100644 index 0000000..ec50ed6 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/Server/WebSocketEndpointHost.cs @@ -0,0 +1,55 @@ +// 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); + + private Func createCreator( + Func creator, Action initializer + ) + { + if (initializer == null) + { + return creator; + } + + return () => + { + var ret = creator(); + initializer(ret); + + return ret; + }; + } + + protected override WSEndpoint CreateSession() + { + return _creator(); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/WebBase.cs b/EonaCat.Network/System/Sockets/Web/WebBase.cs new file mode 100644 index 0000000..8b6ead5 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/WebBase.cs @@ -0,0 +1,164 @@ +// 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 + { + private const int _headersMaxLength = 8192; + internal byte[] EntityBodyData; + + protected const string CrLf = "\r\n"; + + 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; } + + private static byte[] readEntityBody(Stream stream, string length) + { + long len; + if (!long.TryParse(length, out 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 WebSocketException("The length of header part is greater than the max length."); + } + + return Encoding.UTF8.GetString(buff.ToArray()) + .Replace(CrLf + " ", " ") + .Replace(CrLf + "\t", " ") + .Split(new[] { CrLf }, StringSplitOptions.RemoveEmptyEntries); + } + + 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 msg = timeout + ? "A timeout has occurred while reading an HTTP request/response." + : exception != null + ? "An exception has occurred while reading an HTTP request/response." + : null; + + if (msg != null) + { + throw new WebSocketException(msg, exception); + } + + return http; + } + + public byte[] ToByteArray() + { + return Encoding.UTF8.GetBytes(ToString()); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/WebRequest.cs b/EonaCat.Network/System/Sockets/Web/WebRequest.cs new file mode 100644 index 0000000..ced217f --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/WebRequest.cs @@ -0,0 +1,173 @@ +// 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.Reflection; +using System.Text; + +namespace EonaCat.Network +{ + internal class WebRequest : WebBase + { + private bool _websocketRequest; + private bool _websocketRequestSet; + + private WebRequest(string method, string uri, Version version, NameValueCollection headers) + : base(version, headers) + { + HttpMethod = method; + RequestUri = uri; + } + + internal WebRequest(string method, string uri) + : this(method, uri, HttpVersion.Version11, new NameValueCollection()) + { + Headers["User-Agent"] = $"EonaCat.Network/{Constants.Version}"; + } + + 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; } + + 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 WebResponse GetResponse(Stream stream, int millisecondsTimeout) + { + var buff = ToByteArray(); + stream.Write(buff, 0, buff.Length); + + return Read(stream, WebResponse.Parse, millisecondsTimeout); + } + + 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); + } + + 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(); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/WebResponse.cs b/EonaCat.Network/System/Sockets/Web/WebResponse.cs new file mode 100644 index 0000000..3353bdf --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/WebResponse.cs @@ -0,0 +1,144 @@ +// 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.Reflection; +using System.Text; + +namespace EonaCat.Network +{ + internal class WebResponse : WebBase + { + private WebResponse(string code, string reason, Version version, NameValueCollection headers) + : base(version, headers) + { + StatusCode = code; + Reason = reason; + } + + 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}"; + } + + 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; } + + 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); + } + + 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(); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/WebSocket.cs b/EonaCat.Network/System/Sockets/Web/WebSocket.cs new file mode 100644 index 0000000..f54dfbf --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/WebSocket.cs @@ -0,0 +1,3056 @@ +// 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.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace EonaCat.Network +{ + public class WebSocket : IDisposable + { + private AuthenticationChallenge _authChallenge; + private string _base64Key; + private readonly bool _client; + private Action _closeContext; + private CompressionMethod _compression; + private WebSocketContext _context; + private bool _isRedirectionEnabled; + private string _extensions; + private bool _extensionsRequested; + private object _forMessageEventQueue; + private object _forPing; + private object _forSend; + private object _forState; + private MemoryStream _fragmentsBuffer; + private bool _fragmentsCompressed; + private Opcode _fragmentsOpcode; + private const string _guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + private bool _inContinuation; + private volatile bool _inMessage; + private static readonly int _maxRetryCountForConnect; + private readonly Action _message; + private Queue _messageEventQueue; + private uint _nonceCount; + private string _origin; + private ManualResetEvent _pongReceived; + private bool _preAuth; + private string _protocol; + private readonly string[] _protocols; + private bool _protocolsRequested; + private NetworkCredential _proxyCredentials; + private Uri _proxyUri; + private volatile WebSocketState _readyState; + private ManualResetEvent _receivingExited; + private int _retryCountForConnect; + private SSLConfigurationClient _sslConfig; + private Stream _stream; + private TcpClient _tcpClient; + private Uri _uri; + private const string _version = "13"; + private TimeSpan _waitTime; + + internal static readonly byte[] EmptyBytes; + + public static int FragmentLength; + + internal static readonly RandomNumberGenerator RandomNumber; + + static WebSocket() + { + _maxRetryCountForConnect = 10; + EmptyBytes = new byte[0]; + FragmentLength = 1016; + RandomNumber = new RNGCryptoServiceProvider(); + } + + // As server + internal WebSocket(HttpListenerWebSocketContext context, string protocol) + { + _context = context; + _protocol = protocol; + + _closeContext = context.Close; + _message = messages; + IsSecure = context.IsSecureConnection; + _stream = context.Stream; + _waitTime = TimeSpan.FromSeconds(1); + + init(); + } + + // As server + internal WebSocket(TcpListenerWebSocketContext context, string protocol) + { + _context = context; + _protocol = protocol; + + _closeContext = context.Close; + _message = messages; + IsSecure = context.IsSecureConnection; + _stream = context.Stream; + _waitTime = TimeSpan.FromSeconds(1); + + init(); + } + + public WebSocket(string url, params string[] protocols) + { + if (url == null) + { + throw new ArgumentNullException(nameof(url)); + } + + if (url.Length == 0) + { + throw new ArgumentException("An empty string.", nameof(url)); + } + + string msg; + if (!url.TryCreateWebSocketUri(out _uri, out msg)) + { + throw new ArgumentException(msg, nameof(url)); + } + + if (protocols != null && protocols.Length > 0) + { + if (!checkProtocols(protocols, out msg)) + { + throw new ArgumentException(msg, nameof(protocols)); + } + + _protocols = protocols; + } + + _base64Key = CreateBase64Key(); + _client = true; + _message = messagec; + IsSecure = _uri.Scheme == "wss"; + _waitTime = TimeSpan.FromSeconds(5); + + init(); + } + + internal CookieCollection CookieCollection { get; private set; } + + // As server + internal Func CustomHandshakeRequestChecker { get; set; } + + internal bool HasMessage + { + get + { + lock (_forMessageEventQueue) + { + return _messageEventQueue.Count > 0; + } + } + } + + // As server + internal bool IgnoreExtensions { get; set; } + + internal bool IsConnected => _readyState == WebSocketState.Open || _readyState == WebSocketState.Closing; + + public CompressionMethod Compression + { + get + { + return _compression; + } + + set + { + string msg = null; + + if (!_client) + { + msg = "The set operation cannot be used by servers."; + throw new InvalidOperationException(msg); + } + + if (!canSet(out msg)) + { + Logger.Warning(msg); + return; + } + + lock (_forState) + { + if (!canSet(out msg)) + { + Logger.Warning(msg); + return; + } + + _compression = value; + } + } + } + + public IEnumerable Cookies + { + get + { + lock (CookieCollection.SyncRoot) + { + foreach (Cookie cookie in CookieCollection) + { + yield return cookie; + } + } + } + } + + public NetworkCredential Credentials { get; private set; } + + public bool CallMessageOnPing { get; set; } + + public bool IsRedirectionEnabled + { + get + { + return _isRedirectionEnabled; + } + + set + { + string msg = null; + + if (!_client) + { + msg = "The set operation cannot be used by servers."; + throw new InvalidOperationException(msg); + } + + if (!canSet(out msg)) + { + Logger.Warning(msg); + return; + } + + lock (_forState) + { + if (!canSet(out msg)) + { + Logger.Warning(msg); + return; + } + + _isRedirectionEnabled = value; + } + } + } + + public string Extensions => _extensions ?? string.Empty; + + public bool IsAlive => ping(EmptyBytes); + + public bool IsSecure { get; private set; } + + public string Origin + { + get + { + return _origin; + } + + set + { + string msg = null; + + if (!_client) + { + msg = "This instance is not a client."; + throw new InvalidOperationException(msg); + } + + if (!value.IsNullOrEmpty()) + { + Uri uri; + if (!Uri.TryCreate(value, UriKind.Absolute, out uri)) + { + msg = "Not an absolute URI string."; + throw new ArgumentException(msg, value); + } + + if (uri.Segments.Length > 1) + { + msg = "It includes the path segments."; + throw new ArgumentException(msg, value); + } + } + + if (!canSet(out msg)) + { + Logger.Warning(msg); + return; + } + + lock (_forState) + { + if (!canSet(out msg)) + { + Logger.Warning(msg); + return; + } + + _origin = !value.IsNullOrEmpty() ? value.TrimEnd('/') : value; + } + } + } + + public string Protocol + { + get + { + return _protocol ?? string.Empty; + } + + internal set + { + _protocol = value; + } + } + + public WebSocketState ReadyState => _readyState; + + public SSLConfigurationClient SslConfiguration + { + get + { + if (!_client) + { + var msg = "This instance is not a client."; + throw new InvalidOperationException(msg); + } + + if (!IsSecure) + { + var msg = "This instance does not use a secure connection."; + throw new InvalidOperationException(msg); + } + + _sslConfig ??= new SSLConfigurationClient(_uri.DnsSafeHost); + + return _sslConfig; + } + } + + public Uri Url => _client ? _uri : _context.RequestUri; + + public TimeSpan WaitTime + { + get + { + return _waitTime; + } + + set + { + if (value <= TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(value), "Zero or less."); + } + + string msg; + if (!canSet(out msg)) + { + Logger.Warning(msg); + return; + } + + lock (_forState) + { + if (!canSet(out msg)) + { + Logger.Warning(msg); + return; + } + + _waitTime = value; + } + } + } + + public event EventHandler OnDisconnect; + + public event EventHandler OnError; + + public event EventHandler OnMessageReceived; + + public event EventHandler OnConnect; + + // As server + private bool accept() + { + lock (_forState) + { + string msg; + if (!checkIfAvailable(true, false, false, false, out msg)) + { + Logger.Error(msg); + error("An error has occurred in accepting.", null); + + return false; + } + + try + { + if (!acceptHandshake()) + { + return false; + } + + _readyState = WebSocketState.Open; + } + catch (Exception ex) + { + Logger.Error(ex.ToString()); + fatal("An exception has occurred while accepting.", ex); + + return false; + } + + return true; + } + } + + // As server + private bool acceptHandshake() + { + Logger.Debug(string.Format("A request from {0}:\n{1}", _context.UserEndPoint, _context)); + + string msg; + if (!checkHandshakeRequest(_context, out msg)) + { + sendHttpResponse(createHandshakeFailureResponse(HttpStatusCode.BadRequest)); + + Logger.Error(msg); + fatal("An error has occurred while accepting.", CloseStatusCode.ProtocolError); + + return false; + } + + if (!customCheckHandshakeRequest(_context, out msg)) + { + sendHttpResponse(createHandshakeFailureResponse(HttpStatusCode.BadRequest)); + + Logger.Error(msg); + 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 == WebSocketState.Open) + { + message = "The connection has already been established."; + return false; + } + + if (_readyState == WebSocketState.Closing) + { + message = "The connection is closing."; + return false; + } + + return true; + } + + // As server + private bool checkHandshakeRequest(WebSocketContext context, out string message) + { + message = null; + + if (context.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; + } + + // As client + 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 == WebSocketState.Connecting) + { + message = "This operation is not available in: connecting"; + return false; + } + + if (!open && _readyState == WebSocketState.Open) + { + message = "This operation is not available in: open"; + return false; + } + + if (!closing && _readyState == WebSocketState.Closing) + { + message = "This operation is not available in: closing"; + return false; + } + + if (!closed && _readyState == WebSocketState.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 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; + } + + Uri uri; + if (!Uri.TryCreate(url, UriKind.Absolute, out 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 checkReceivedFrame(WebSocketFrame frame, out string message) + { + message = null; + + var masked = frame.IsMasked; + if (_client && masked) + { + message = "A frame from the server is masked."; + return false; + } + + if (!_client && !masked) + { + message = "A frame from a client is not masked."; + return false; + } + + if (_inContinuation && frame.IsData) + { + message = "A data frame has been received while receiving continuation frames."; + return false; + } + + if (frame.IsCompressed && _compression == CompressionMethod.None) + { + message = "A compressed frame has been received without any agreement for it."; + return false; + } + + if (frame.Rsv2 == Rsv.On) + { + message = "The RSV2 of a frame is non-zero without any negotiation for it."; + return false; + } + + if (frame.Rsv3 == Rsv.On) + { + message = "The RSV3 of a frame is non-zero without any negotiation for it."; + return false; + } + + return true; + } + + private void close(ushort code, string reason) + { + if (_readyState == WebSocketState.Closing) + { + Logger.Info("The closing is already in progress."); + return; + } + + if (_readyState == WebSocketState.Closed) + { + Logger.Info("The connection has already been closed."); + return; + } + + if (code == 1005) + { // == no status + close(PayloadData.Empty, true, true, false); + return; + } + + var send = !code.IsReserved(); + close(new PayloadData(code, reason), send, send, false); + } + + private void close( + PayloadData payloadData, bool send, bool receive, bool received + ) + { + lock (_forState) + { + if (_readyState == WebSocketState.Closing) + { + Logger.Info("The closing is already in progress."); + return; + } + + if (_readyState == WebSocketState.Closed) + { + Logger.Info("The connection has already been closed."); + return; + } + + send = send && _readyState == WebSocketState.Open; + receive = send && receive; + + _readyState = WebSocketState.Closing; + } + + Logger.Trace("Begin closing the connection."); + + var res = closeHandshake(payloadData, send, receive, received); + releaseResources(); + + Logger.Trace("End closing the connection."); + + _readyState = WebSocketState.Closed; + + var e = new CloseEventArgs(payloadData); + e.WasClean = res; + + try + { + 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 == WebSocketState.Closing) + { + Logger.Info("The closing is already in progress."); + return; + } + + if (_readyState == WebSocketState.Closed) + { + Logger.Info("The connection has already been closed."); + return; + } + + if (code == 1005) + { // == no status + closeAsync(PayloadData.Empty, true, true, false); + return; + } + + var send = !code.IsReserved(); + closeAsync(new PayloadData(code, reason), send, send, false); + } + + private async Task closeAsync(PayloadData payloadData, bool send, bool receive, bool received) + { + await Task.Run(() => close(payloadData, send, receive, received)); + } + + private bool closeHandshake(byte[] frameAsBytes, bool receive, bool received) + { + var sent = frameAsBytes != null && sendBytes(frameAsBytes); + + var wait = !received && sent && receive && _receivingExited != null; + if (wait) + { + received = _receivingExited.WaitOne(_waitTime); + } + + var ret = sent && received; + + Logger.Debug( + string.Format( + "Was clean?: {0}\n sent: {1}\n received: {2}", ret, sent, received + ) + ); + + return ret; + } + + private bool closeHandshake( + PayloadData payloadData, bool send, bool receive, bool received + ) + { + var sent = false; + if (send) + { + var frame = WebSocketFrame.CreateCloseFrame(payloadData, _client); + sent = sendBytes(frame.ToArray()); + + if (_client) + { + frame.Unmask(); + } + } + + var wait = !received && sent && receive && _receivingExited != null; + if (wait) + { + received = _receivingExited.WaitOne(_waitTime); + } + + var ret = sent && received; + + Logger.Debug( + string.Format( + "Was clean?: {0}\n sent: {1}\n received: {2}", ret, sent, received + ) + ); + + return ret; + } + + // As client + private bool connect() + { + lock (_forState) + { + string msg; + if (!checkIfAvailable(true, false, false, true, out msg)) + { + Logger.Error(msg); + 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 = WebSocketState.Connecting; + + try + { + doHandshake(); + } + catch (Exception ex) + { + _retryCountForConnect++; + Logger.Error(ex.ToString()); + fatal("An exception has occurred while connecting.", ex); + + return false; + } + + _retryCountForConnect = 1; + _readyState = WebSocketState.Open; + + return true; + } + } + + // As client + private string createExtensions() + { + var buff = new StringBuilder(80); + + if (_compression != CompressionMethod.None) + { + var str = _compression.ToExtensionString( + "server_no_context_takeover", "client_no_context_takeover"); + + buff.AppendFormat("{0}, ", str); + } + + var len = buff.Length; + if (len > 2) + { + buff.Length = len - 2; + return buff.ToString(); + } + + return null; + } + + // As server + private WebResponse createHandshakeFailureResponse(HttpStatusCode code) + { + var ret = WebResponse.CreateCloseResponse(code); + ret.Headers["Sec-WebSocket-Version"] = _version; + + return ret; + } + + // As client + 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; + if (_protocolsRequested) + { + headers["Sec-WebSocket-Protocol"] = _protocols.ToString(", "); + } + + _extensionsRequested = _compression != CompressionMethod.None; + if (_extensionsRequested) + { + headers["Sec-WebSocket-Extensions"] = createExtensions(); + } + + headers["Sec-WebSocket-Version"] = _version; + + AuthenticationResponse authRes = null; + if (_authChallenge != null && Credentials != null) + { + authRes = new AuthenticationResponse(_authChallenge, Credentials, _nonceCount); + _nonceCount = authRes.NonceCount; + } + else if (_preAuth) + { + authRes = new AuthenticationResponse(Credentials); + } + + if (authRes != null) + { + headers["Authorization"] = authRes.ToString(); + } + + if (CookieCollection.Count > 0) + { + ret.SetCookies(CookieCollection); + } + + return ret; + } + + // As server + 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; + } + + // As server + private bool customCheckHandshakeRequest(WebSocketContext context, out string message) + { + message = null; + return CustomHandshakeRequestChecker == null + || (message = CustomHandshakeRequestChecker(context)) == null; + } + + private MessageEventArgs dequeueFromMessageEventQueue() + { + lock (_forMessageEventQueue) + { + return _messageEventQueue.Count > 0 ? _messageEventQueue.Dequeue() : null; + } + } + + // As client + private void doHandshake() + { + setClientStream(); + var res = sendHandshakeRequest(); + + string msg; + if (!checkHandshakeResponse(res, out msg)) + { + throw new WebSocketException(CloseStatusCode.ProtocolError, msg); + } + + if (_protocolsRequested) + { + _protocol = res.Headers["Sec-WebSocket-Protocol"]; + } + + if (_extensionsRequested) + { + processSecWebSocketExtensionsServerHeader(res.Headers["Sec-WebSocket-Extensions"]); + } + + processCookies(res.Cookies); + } + + private void enqueueToMessageEventQueue(MessageEventArgs e) + { + lock (_forMessageEventQueue) + { + _messageEventQueue.Enqueue(e); + } + } + + private void error(string message, Exception exception) + { + try + { + OnError.Emit(this, new ErrorEventArgs(message, exception)); + } + catch (Exception ex) + { + Logger.Error(ex.ToString()); + } + } + + private void fatal(string message, Exception exception) + { + var code = exception is WebSocketException + ? ((WebSocketException)exception).Code + : CloseStatusCode.Abnormal; + + fatal(message, (ushort)code); + } + + private void fatal(string message, ushort code) + { + var payload = new PayloadData(code, message); + close(payload, !code.IsReserved(), false, false); + } + + private void fatal(string message, CloseStatusCode code) + { + fatal(message, (ushort)code); + } + + private void init() + { + _compression = CompressionMethod.None; + CookieCollection = new CookieCollection(); + _forPing = new object(); + _forSend = new object(); + _forState = new object(); + _messageEventQueue = new Queue(); + _forMessageEventQueue = ((ICollection)_messageEventQueue).SyncRoot; + _readyState = WebSocketState.Connecting; + } + + private void message() + { + MessageEventArgs e = null; + lock (_forMessageEventQueue) + { + if (_inMessage || _messageEventQueue.Count == 0 || _readyState != WebSocketState.Open) + { + return; + } + + _inMessage = true; + e = _messageEventQueue.Dequeue(); + } + + _message(e); + } + + private void messagec(MessageEventArgs e) + { + do + { + try + { + OnMessageReceived.Emit(this, e); + } + catch (Exception ex) + { + Logger.Error(ex.ToString()); + error("An error has occurred during an OnMessage event.", ex); + } + + lock (_forMessageEventQueue) + { + if (_messageEventQueue.Count == 0 || _readyState != WebSocketState.Open) + { + _inMessage = false; + break; + } + + e = _messageEventQueue.Dequeue(); + } + } + while (true); + } + + private void messages(MessageEventArgs e) + { + try + { + OnMessageReceived.Emit(this, e); + } + catch (Exception ex) + { + Logger.Error(ex.ToString()); + error("An error has occurred during an OnMessage event.", ex); + } + + lock (_forMessageEventQueue) + { + if (_messageEventQueue.Count == 0 || _readyState != WebSocketState.Open) + { + _inMessage = false; + return; + } + + e = _messageEventQueue.Dequeue(); + } + + ThreadPool.QueueUserWorkItem(state => messages(e)); + } + + private void open() + { + _inMessage = true; + startReceiving(); + try + { + 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 (_forMessageEventQueue) + { + if (_messageEventQueue.Count == 0 || _readyState != WebSocketState.Open) + { + _inMessage = false; + return; + } + + e = _messageEventQueue.Dequeue(); + } + + Task.Run(() => _message(e)); + } + + private bool ping(byte[] data) + { + if (_readyState != WebSocketState.Open) + { + return false; + } + + var pongReceived = _pongReceived; + if (pongReceived == null) + { + return false; + } + + lock (_forPing) + { + try + { + pongReceived.Reset(); + if (!send(FinalFrame.Final, Opcode.Ping, data, false)) + { + return false; + } + + return pongReceived.WaitOne(_waitTime); + } + catch (ObjectDisposedException) + { + return false; + } + } + } + + private bool processCloseFrame(WebSocketFrame frame) + { + var payload = frame.PayloadData; + close(payload, !payload.HasReservedCode, false, true); + + return false; + } + + // As client + private void processCookies(CookieCollection cookies) + { + if (cookies.Count == 0) + { + return; + } + + CookieCollection.SetOrRemove(cookies); + } + + private bool processDataFrame(WebSocketFrame frame) + { + enqueueToMessageEventQueue( + frame.IsCompressed + ? new MessageEventArgs( + frame.Opcode, frame.PayloadData.ApplicationData.Decompress(_compression)) + : new MessageEventArgs(frame)); + + return true; + } + + private bool processFragmentFrame(WebSocketFrame frame) + { + if (!_inContinuation) + { + // Must process first fragment. + if (frame.IsContinuation) + { + return true; + } + + _fragmentsOpcode = frame.Opcode; + _fragmentsCompressed = frame.IsCompressed; + _fragmentsBuffer = new MemoryStream(); + _inContinuation = true; + } + + _fragmentsBuffer.WriteBytes(frame.PayloadData.ApplicationData, 1024); + if (frame.IsFinal) + { + using (_fragmentsBuffer) + { + var data = _fragmentsCompressed + ? _fragmentsBuffer.DecompressToArray(_compression) + : _fragmentsBuffer.ToArray(); + + enqueueToMessageEventQueue(new MessageEventArgs(_fragmentsOpcode, data)); + } + + _fragmentsBuffer = null; + _inContinuation = false; + } + + return true; + } + + private bool processPingFrame(WebSocketFrame frame) + { + Logger.Trace("A ping was received."); + + var pong = WebSocketFrame.CreatePongFrame(frame.PayloadData, _client); + + lock (_forState) + { + if (_readyState != WebSocketState.Open) + { + Logger.Error("The connection is closing."); + return true; + } + + if (!sendBytes(pong.ToArray())) + { + return false; + } + } + + Logger.Trace("A pong to this ping has been sent."); + + if (CallMessageOnPing) + { + if (_client) + { + pong.Unmask(); + } + + enqueueToMessageEventQueue(new MessageEventArgs(frame)); + } + + return true; + } + + private bool processPongFrame(WebSocketFrame frame) + { + Logger.Trace("A pong was received."); + + try + { + _pongReceived.Set(); + } + catch (NullReferenceException ex) + { + Logger.Error(ex.Message); + Logger.Debug(ex.ToString()); + + return false; + } + catch (ObjectDisposedException ex) + { + Logger.Error(ex.Message); + Logger.Debug(ex.ToString()); + + return false; + } + + Logger.Trace("It has been signaled."); + + return true; + } + + private bool processReceivedFrame(WebSocketFrame frame) + { + string msg; + if (!checkReceivedFrame(frame, out msg)) + { + throw new WebSocketException(CloseStatusCode.ProtocolError, msg); + } + + frame.Unmask(); + return frame.IsFragment + ? processFragmentFrame(frame) + : frame.IsData + ? processDataFrame(frame) + : frame.IsPing + ? processPingFrame(frame) + : frame.IsPong + ? processPongFrame(frame) + : frame.IsClose + ? processCloseFrame(frame) + : processUnsupportedFrame(frame); + } + + // As server + private void processSecWebSocketExtensionsClientHeader(string value) + { + if (value == null) + { + return; + } + + var buff = new StringBuilder(80); + + var comp = false; + foreach (var e in value.SplitHeaderValue(',')) + { + var ext = e.Trim(); + if (!comp && ext.IsCompressionExtension(CompressionMethod.Deflate)) + { + _compression = CompressionMethod.Deflate; + buff.AppendFormat( + "{0}, ", + _compression.ToExtensionString( + "client_no_context_takeover", "server_no_context_takeover" + ) + ); + + comp = true; + } + } + + var len = buff.Length; + if (len > 2) + { + buff.Length = len - 2; + _extensions = buff.ToString(); + } + } + + // As client + private void processSecWebSocketExtensionsServerHeader(string value) + { + if (value == null) + { + _compression = CompressionMethod.None; + return; + } + + _extensions = value; + } + + // As server + private void processSecWebSocketProtocolHeader(IEnumerable values) + { + if (values.Contains(p => p == _protocol)) + { + return; + } + + _protocol = null; + } + + private bool processUnsupportedFrame(WebSocketFrame frame) + { + Logger.Error("An unsupported frame:" + frame.PrintToString(false)); + fatal("There is no way to handle it.", CloseStatusCode.PolicyViolation); + + return false; + } + + // As client + private void releaseClientResources() + { + if (_stream != null) + { + _stream.Dispose(); + _stream = null; + } + + if (_tcpClient != null) + { + _tcpClient.Close(); + _tcpClient = null; + } + } + + private void releaseCommonResources() + { + if (_fragmentsBuffer != null) + { + _fragmentsBuffer.Dispose(); + _fragmentsBuffer = null; + _inContinuation = false; + } + + if (_pongReceived != null) + { + _pongReceived.Close(); + _pongReceived = null; + } + + if (_receivingExited != null) + { + _receivingExited.Close(); + _receivingExited = null; + } + } + + private void releaseResources() + { + if (_client) + { + releaseClientResources(); + } + else + { + releaseServerResources(); + } + + releaseCommonResources(); + } + + // As server + private void releaseServerResources() + { + if (_closeContext == null) + { + return; + } + + _closeContext(); + _closeContext = null; + _stream = null; + _context = null; + } + + private bool send(Opcode opcode, Stream stream) + { + lock (_forSend) + { + var src = stream; + var compressed = false; + var sent = false; + try + { + if (_compression != CompressionMethod.None) + { + stream = stream.Compress(_compression); + compressed = true; + } + + sent = send(opcode, stream, compressed); + if (!sent) + { + error("A send has been interrupted.", null); + } + } + catch (Exception ex) + { + Logger.Error(ex.ToString()); + error("An error has occurred during a send.", ex); + } + finally + { + if (compressed) + { + stream.Dispose(); + } + + src.Dispose(); + } + + return sent; + } + } + + private bool send(Opcode opcode, Stream stream, bool compressed) + { + var len = stream.Length; + if (len == 0) + { + return send(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, Opcode.Cont, buff, false); + + if (!sent) + { + return false; + } + } + + // End + if (rem == 0) + { + rem = FragmentLength; + } + else + { + buff = new byte[rem]; + } + + return stream.Read(buff, 0, rem) == rem + && send(FinalFrame.Final, Opcode.Cont, buff, false); + } + + private bool send(FinalFrame fin, Opcode opcode, byte[] data, bool compressed) + { + lock (_forState) + { + if (_readyState != WebSocketState.Open) + { + Logger.Error("The connection is closing."); + return false; + } + + var frame = new WebSocketFrame(fin, opcode, data, compressed, _client); + return sendBytes(frame.ToArray()); + } + } + + private void sendAsync(Opcode opcode, Stream stream, Action completed) + { + Func sender = send; + 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; + } + + // As client + private WebResponse sendHandshakeRequest() + { + var req = createHandshakeRequest(); + var res = sendHttpRequest(req, 90000); + if (res.IsUnauthorized) + { + var chal = res.Headers["WWW-Authenticate"]; + Logger.Warning(string.Format("Received an authentication requirement for '{0}'.", chal)); + if (chal.IsNullOrEmpty()) + { + Logger.Error("No authentication challenge is specified."); + return res; + } + + _authChallenge = AuthenticationChallenge.Parse(chal); + if (_authChallenge == null) + { + Logger.Error("An invalid authentication challenge is specified."); + return res; + } + + if (Credentials != null && + (!_preAuth || _authChallenge.Scheme == AuthenticationSchemes.Digest)) + { + if (res.HasConnectionClose) + { + releaseClientResources(); + setClientStream(); + } + + var authRes = new AuthenticationResponse(_authChallenge, Credentials, _nonceCount); + _nonceCount = authRes.NonceCount; + req.Headers["Authorization"] = authRes.ToString(); + res = sendHttpRequest(req, 15000); + } + } + + if (res.IsRedirect) + { + var url = res.Headers["Location"]; + Logger.Warning(string.Format("Received a redirection to '{0}'.", url)); + if (_isRedirectionEnabled) + { + if (url.IsNullOrEmpty()) + { + Logger.Error("No url to redirect is located."); + return res; + } + + Uri uri; + string msg; + if (!url.TryCreateWebSocketUri(out uri, out msg)) + { + Logger.Error("An invalid url to redirect is located: " + msg); + return res; + } + + releaseClientResources(); + + _uri = uri; + IsSecure = uri.Scheme == "wss"; + + setClientStream(); + return sendHandshakeRequest(); + } + } + + return res; + } + + // As client + 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; + } + + // As server + private bool sendHttpResponse(WebResponse response) + { + Logger.Debug("Response to request:\n" + response.ToString()); + return sendBytes(response.ToByteArray()); + } + + // As client + private void sendProxyConnectRequest() + { + var req = WebRequest.CreateConnectRequest(_uri); + var res = sendHttpRequest(req, 90000); + if (res.IsProxyAuthenticationRequired) + { + var chal = res.Headers["Proxy-Authenticate"]; + Logger.Warning( + string.Format("Received a proxy authentication requirement for '{0}'.", chal)); + + if (chal.IsNullOrEmpty()) + { + throw new WebSocketException("No proxy authentication challenge is specified."); + } + + var authChal = AuthenticationChallenge.Parse(chal); + if (authChal == null) + { + throw new WebSocketException("An invalid proxy authentication challenge is specified."); + } + + if (_proxyCredentials != null) + { + if (res.HasConnectionClose) + { + releaseClientResources(); + _tcpClient = new TcpClient(_proxyUri.DnsSafeHost, _proxyUri.Port); + _stream = _tcpClient.GetStream(); + } + + var authRes = new AuthenticationResponse(authChal, _proxyCredentials, 0); + req.Headers["Proxy-Authorization"] = authRes.ToString(); + res = sendHttpRequest(req, 15000); + } + + if (res.IsProxyAuthenticationRequired) + { + throw new WebSocketException("A proxy authentication is required."); + } + } + + if (res.StatusCode[0] != '2') + { + throw new WebSocketException( + "The proxy has failed a connection to the requested host and port."); + } + } + + // As client + private void setClientStream() + { + if (_proxyUri != null) + { + _tcpClient = new TcpClient(_proxyUri.DnsSafeHost, _proxyUri.Port); + _stream = _tcpClient.GetStream(); + sendProxyConnectRequest(); + } + else + { + _tcpClient = new TcpClient(_uri.DnsSafeHost, _uri.Port); + _stream = _tcpClient.GetStream(); + } + + if (IsSecure) + { + var conf = SslConfiguration; + var host = conf.TargetHost; + if (host != _uri.DnsSafeHost) + { + throw new WebSocketException( + CloseStatusCode.TlsHandshakeFailure, "An invalid host name is specified."); + } + + try + { + var sslStream = new SslStream( + _stream, + false, + conf.ServerCertificateValidationCallback, + conf.ClientCertificateSelectionCallback); + + sslStream.AuthenticateAsClient( + host, + conf.Certificates, + conf.SslProtocols, + conf.CheckForCertificateRevocation); + + _stream = sslStream; + } + catch (Exception ex) + { + throw new WebSocketException(CloseStatusCode.TlsHandshakeFailure, ex); + } + } + } + + private void startReceiving() + { + if (_messageEventQueue.Count > 0) + { + _messageEventQueue.Clear(); + } + + _pongReceived = new ManualResetEvent(false); + _receivingExited = new ManualResetEvent(false); + + Action receive = null; + receive = + () => + WebSocketFrame.ReadFrameAsync( + _stream, + false, + frame => + { + if (!processReceivedFrame(frame) || _readyState == WebSocketState.Closed) + { + var exited = _receivingExited; + if (exited != null) + { + exited.Set(); + } + + return; + } + + // Receive next asap because the Ping or Close needs a response to it. + receive(); + + if (_inMessage || !HasMessage || _readyState != WebSocketState.Open) + { + return; + } + + message(); + }, + ex => + { + Logger.Error(ex.ToString()); + fatal("An exception has occurred while receiving.", ex); + } + ); + + receive(); + } + + // As client + private bool validateSecWebSocketAcceptHeader(string value) + { + return value != null && value == CreateResponseKey(_base64Key); + } + + // As server + private bool validateSecWebSocketExtensionsClientHeader(string value) + { + return value == null || value.Length > 0; + } + + // As client + private bool validateSecWebSocketExtensionsServerHeader(string value) + { + if (value == null) + { + return true; + } + + if (value.Length == 0) + { + return false; + } + + if (!_extensionsRequested) + { + return false; + } + + var comp = _compression != CompressionMethod.None; + foreach (var e in value.SplitHeaderValue(',')) + { + var ext = e.Trim(); + if (comp && ext.IsCompressionExtension(_compression)) + { + if (!ext.Contains("server_no_context_takeover")) + { + Logger.Error("The server hasn't sent back 'server_no_context_takeover'."); + return false; + } + + if (!ext.Contains("client_no_context_takeover")) + { + Logger.Warning("The server hasn't sent back 'client_no_context_takeover'."); + } + + var method = _compression.ToExtensionString(); + var invalid = + ext.SplitHeaderValue(';').Contains( + t => + { + t = t.Trim(); + return t != method + && t != "server_no_context_takeover" + && t != "client_no_context_takeover"; + } + ); + + if (invalid) + { + return false; + } + } + else + { + return false; + } + } + + return true; + } + + // As server + private bool validateSecWebSocketKeyHeader(string value) + { + return value != null && value.Length > 0; + } + + // As server + private bool validateSecWebSocketProtocolClientHeader(string value) + { + return value == null || value.Length > 0; + } + + // As client + private bool validateSecWebSocketProtocolServerHeader(string value) + { + if (value == null) + { + return !_protocolsRequested; + } + + if (value.Length == 0) + { + return false; + } + + return _protocolsRequested && _protocols.Contains(p => p == value); + } + + // As server + private bool validateSecWebSocketVersionClientHeader(string value) + { + return value != null && value == _version; + } + + // As client + private bool validateSecWebSocketVersionServerHeader(string value) + { + return value == null || value == _version; + } + + // As server + internal void Close(WebResponse response) + { + _readyState = WebSocketState.Closing; + + sendHttpResponse(response); + releaseServerResources(); + + _readyState = WebSocketState.Closed; + } + + // As server + internal void Close(HttpStatusCode code) + { + Close(createHandshakeFailureResponse(code)); + } + + // As server + internal void Close(PayloadData payloadData, byte[] frameAsBytes) + { + lock (_forState) + { + if (_readyState == WebSocketState.Closing) + { + Logger.Info("The closing is already in progress."); + return; + } + + if (_readyState == WebSocketState.Closed) + { + Logger.Info("The connection has already been closed."); + return; + } + + _readyState = WebSocketState.Closing; + } + + Logger.Trace("Begin closing the connection."); + + var sent = frameAsBytes != null && sendBytes(frameAsBytes); + var received = sent && _receivingExited != null +&& _receivingExited.WaitOne(_waitTime); + + var res = sent && received; + + Logger.Debug( + string.Format( + "Was clean?: {0}\n sent: {1}\n received: {2}", res, sent, received + ) + ); + + releaseServerResources(); + releaseCommonResources(); + + Logger.Trace("End closing the connection."); + + _readyState = WebSocketState.Closed; + + var e = new CloseEventArgs(payloadData); + e.WasClean = res; + + try + { + OnDisconnect.Emit(this, e); + } + catch (Exception ex) + { + Logger.Error(ex.ToString()); + } + } + + // As client + internal static string CreateBase64Key() + { + var src = new byte[16]; + RandomNumber.GetBytes(src); + + return Convert.ToBase64String(src); + } + + internal static string CreateResponseKey(string base64Key) + { + var buff = new StringBuilder(base64Key, 64); + buff.Append(_guid); + SHA1 sha1 = new SHA1CryptoServiceProvider(); + var src = sha1.ComputeHash(buff.ToString().UTF8Encode()); + + return Convert.ToBase64String(src); + } + + // As server + internal void InternalAccept() + { + try + { + if (!acceptHandshake()) + { + return; + } + + _readyState = WebSocketState.Open; + } + catch (Exception ex) + { + Logger.Error(ex.ToString()); + fatal("An exception has occurred while accepting.", ex); + + return; + } + + open(); + } + + // As server + internal bool Ping(byte[] frameAsBytes, TimeSpan timeout) + { + if (_readyState != WebSocketState.Open) + { + return false; + } + + var pongReceived = _pongReceived; + if (pongReceived == null) + { + return false; + } + + lock (_forPing) + { + try + { + pongReceived.Reset(); + + lock (_forState) + { + if (_readyState != WebSocketState.Open) + { + return false; + } + + if (!sendBytes(frameAsBytes)) + { + return false; + } + } + + return pongReceived.WaitOne(timeout); + } + catch (ObjectDisposedException) + { + return false; + } + } + } + + // As server + internal void Send( + Opcode opcode, byte[] data, Dictionary cache + ) + { + lock (_forSend) + { + lock (_forState) + { + if (_readyState != WebSocketState.Open) + { + Logger.Error("The connection is closing."); + return; + } + + byte[] found; + if (!cache.TryGetValue(_compression, out found)) + { + found = new WebSocketFrame( + FinalFrame.Final, + opcode, + data.Compress(_compression), + _compression != CompressionMethod.None, + false + ) + .ToArray(); + + cache.Add(_compression, found); + } + + sendBytes(found); + } + } + } + + // As server + internal void Send( + Opcode opcode, Stream stream, Dictionary cache + ) + { + lock (_forSend) + { + Stream found; + if (!cache.TryGetValue(_compression, out found)) + { + found = stream.Compress(_compression); + cache.Add(_compression, found); + } + else + { + found.Position = 0; + } + + send(opcode, found, _compression != CompressionMethod.None); + } + } + + public void Accept() + { + string msg; + if (!checkIfAvailable(false, true, true, false, false, false, out msg)) + { + Logger.Error(msg); + error("An error has occurred in accepting.", null); + + return; + } + + if (accept()) + { + open(); + } + } + + public void AcceptAsync() + { + string msg; + if (!checkIfAvailable(false, true, true, false, false, false, out msg)) + { + Logger.Error(msg); + error("An error has occurred in accepting.", null); + return; + } + + Func acceptor = accept; + Task.Run(() => + { + if (acceptor()) + { + open(); + } + }); + } + + public void Close() + { + close(1005, string.Empty); + } + + public void Close(ushort code) + { + if (!code.IsCloseStatusCode()) + { + var msg = "Less than 1000 or greater than 4999."; + throw new ArgumentOutOfRangeException(nameof(code), msg); + } + + if (_client && code == 1011) + { + var msg = "1011 cannot be used."; + throw new ArgumentException(msg, nameof(code)); + } + + if (!_client && code == 1010) + { + var msg = "1010 cannot be used."; + throw new ArgumentException(msg, nameof(code)); + } + + close(code, string.Empty); + } + + public void Close(CloseStatusCode code) + { + if (_client && code == CloseStatusCode.ServerError) + { + var msg = "ServerError cannot be used."; + throw new ArgumentException(msg, nameof(code)); + } + + if (!_client && code == CloseStatusCode.MandatoryExtension) + { + var msg = "MandatoryExtension cannot be used."; + throw new ArgumentException(msg, nameof(code)); + } + + close((ushort)code, string.Empty); + } + + public void Close(ushort code, string reason) + { + if (!code.IsCloseStatusCode()) + { + var msg = "Less than 1000 or greater than 4999."; + throw new ArgumentOutOfRangeException(nameof(code), msg); + } + + if (_client && code == 1011) + { + var msg = "1011 cannot be used."; + throw new ArgumentException(msg, nameof(code)); + } + + if (!_client && code == 1010) + { + var msg = "1010 cannot be used."; + throw new ArgumentException(msg, nameof(code)); + } + + if (reason.IsNullOrEmpty()) + { + close(code, string.Empty); + return; + } + + if (code == 1005) + { + var msg = "1005 cannot be used."; + throw new ArgumentException(msg, nameof(code)); + } + + byte[] bytes; + if (!reason.TryGetUTF8EncodedBytes(out bytes)) + { + var msg = "It could not be UTF-8-encoded."; + throw new ArgumentException(msg, nameof(reason)); + } + + if (bytes.Length > 123) + { + var msg = "Its size is greater than 123 bytes."; + throw new ArgumentOutOfRangeException(nameof(reason), msg); + } + + close(code, reason); + } + + public void Close(CloseStatusCode code, string reason) + { + if (_client && code == CloseStatusCode.ServerError) + { + var msg = "ServerError cannot be used."; + throw new ArgumentException(msg, nameof(code)); + } + + if (!_client && code == CloseStatusCode.MandatoryExtension) + { + var msg = "MandatoryExtension cannot be used."; + throw new ArgumentException(msg, nameof(code)); + } + + if (reason.IsNullOrEmpty()) + { + close((ushort)code, string.Empty); + return; + } + + if (code == CloseStatusCode.NoStatus) + { + var msg = "NoStatus cannot be used."; + throw new ArgumentException(msg, nameof(code)); + } + + byte[] bytes; + if (!reason.TryGetUTF8EncodedBytes(out bytes)) + { + var msg = "It could not be UTF-8-encoded."; + throw new ArgumentException(msg, nameof(reason)); + } + + if (bytes.Length > 123) + { + var msg = "Its size is greater than 123 bytes."; + throw new ArgumentOutOfRangeException(nameof(reason), msg); + } + + close((ushort)code, reason); + } + + public void CloseAsync() + { + closeAsync(1005, string.Empty); + } + + public void CloseAsync(ushort code) + { + if (!code.IsCloseStatusCode()) + { + var msg = "Less than 1000 or greater than 4999."; + throw new ArgumentOutOfRangeException(nameof(code), msg); + } + + if (_client && code == 1011) + { + var msg = "1011 cannot be used."; + throw new ArgumentException(msg, nameof(code)); + } + + if (!_client && code == 1010) + { + var msg = "1010 cannot be used."; + throw new ArgumentException(msg, nameof(code)); + } + + closeAsync(code, string.Empty); + } + + public void CloseAsync(CloseStatusCode code) + { + if (_client && code == CloseStatusCode.ServerError) + { + var msg = "ServerError cannot be used."; + throw new ArgumentException(msg, nameof(code)); + } + + if (!_client && code == CloseStatusCode.MandatoryExtension) + { + var msg = "MandatoryExtension cannot be used."; + throw new ArgumentException(msg, nameof(code)); + } + + closeAsync((ushort)code, string.Empty); + } + + public void CloseAsync(ushort code, string reason) + { + if (!code.IsCloseStatusCode()) + { + var msg = "Less than 1000 or greater than 4999."; + throw new ArgumentOutOfRangeException(nameof(code), msg); + } + + if (_client && code == 1011) + { + var msg = "1011 cannot be used."; + throw new ArgumentException(msg, nameof(code)); + } + + if (!_client && code == 1010) + { + var msg = "1010 cannot be used."; + throw new ArgumentException(msg, nameof(code)); + } + + if (reason.IsNullOrEmpty()) + { + closeAsync(code, string.Empty); + return; + } + + if (code == 1005) + { + var msg = "1005 cannot be used."; + throw new ArgumentException(msg, nameof(code)); + } + + byte[] bytes; + if (!reason.TryGetUTF8EncodedBytes(out bytes)) + { + var msg = "It could not be UTF-8-encoded."; + throw new ArgumentException(msg, nameof(reason)); + } + + if (bytes.Length > 123) + { + var msg = "Its size is greater than 123 bytes."; + throw new ArgumentOutOfRangeException(nameof(reason), msg); + } + + closeAsync(code, reason); + } + + public void CloseAsync(CloseStatusCode code, string reason) + { + if (_client && code == CloseStatusCode.ServerError) + { + var msg = "ServerError cannot be used."; + throw new ArgumentException(msg, nameof(code)); + } + + if (!_client && code == CloseStatusCode.MandatoryExtension) + { + var msg = "MandatoryExtension cannot be used."; + throw new ArgumentException(msg, nameof(code)); + } + + if (reason.IsNullOrEmpty()) + { + closeAsync((ushort)code, string.Empty); + return; + } + + if (code == CloseStatusCode.NoStatus) + { + var msg = "NoStatus cannot be used."; + throw new ArgumentException(msg, nameof(code)); + } + + byte[] bytes; + if (!reason.TryGetUTF8EncodedBytes(out bytes)) + { + var msg = "It could not be UTF-8-encoded."; + throw new ArgumentException(msg, nameof(reason)); + } + + if (bytes.Length > 123) + { + var msg = "Its size is greater than 123 bytes."; + throw new ArgumentOutOfRangeException(nameof(reason), msg); + } + + closeAsync((ushort)code, reason); + } + + public void Connect() + { + string msg; + if (!checkIfAvailable(true, false, true, false, false, true, out msg)) + { + Logger.Error(msg); + error("An error has occurred in connecting.", null); + + return; + } + + if (connect()) + { + open(); + } + } + + public void ConnectAsync() + { + string msg; + if (!checkIfAvailable(true, false, true, false, false, true, out msg)) + { + Logger.Error(msg); + error("An error has occurred in connecting.", null); + return; + } + + Func connector = connect; + Task.Run(() => + { + if (connector()) + { + open(); + } + }); + } + + public bool Ping() + { + return ping(EmptyBytes); + } + + public bool Ping(string message) + { + if (message.IsNullOrEmpty()) + { + return ping(EmptyBytes); + } + + byte[] bytes; + if (!message.TryGetUTF8EncodedBytes(out bytes)) + { + var msg = "It could not be UTF-8-encoded."; + throw new ArgumentException(msg, nameof(message)); + } + + if (bytes.Length > 125) + { + var msg = "Its size is greater than 125 bytes."; + throw new ArgumentOutOfRangeException(nameof(message), msg); + } + + return ping(bytes); + } + + public void Send(byte[] data) + { + if (_readyState != WebSocketState.Open) + { + var msg = "The current state of the connection is not Open."; + throw new InvalidOperationException(msg); + } + + if (data == null) + { + throw new ArgumentNullException(nameof(data)); + } + + send(Opcode.Binary, new MemoryStream(data)); + } + + public void Send(FileInfo fileInfo) + { + if (_readyState != WebSocketState.Open) + { + var msg = "The current state of the connection is not Open."; + throw new InvalidOperationException(msg); + } + + if (fileInfo == null) + { + throw new ArgumentNullException(nameof(fileInfo)); + } + + if (!fileInfo.Exists) + { + var msg = "The file does not exist."; + throw new ArgumentException(msg, nameof(fileInfo)); + } + + FileStream stream; + if (!fileInfo.TryOpenRead(out stream)) + { + var msg = "The file could not be opened."; + throw new ArgumentException(msg, nameof(fileInfo)); + } + + send(Opcode.Binary, stream); + } + + public void Send(string data) + { + if (_readyState != WebSocketState.Open) + { + var msg = "The current state of the connection is not Open."; + throw new InvalidOperationException(msg); + } + + if (data == null) + { + throw new ArgumentNullException(nameof(data)); + } + + byte[] bytes; + if (!data.TryGetUTF8EncodedBytes(out bytes)) + { + var msg = "It could not be UTF-8-encoded."; + throw new ArgumentException(msg, nameof(data)); + } + + send(Opcode.Text, new MemoryStream(bytes)); + } + + public void Send(Stream stream, int length) + { + if (_readyState != WebSocketState.Open) + { + var msg = "The current state of the connection is not Open."; + throw new InvalidOperationException(msg); + } + + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + if (!stream.CanRead) + { + var msg = "It cannot be read."; + throw new ArgumentException(msg, nameof(stream)); + } + + if (length < 1) + { + var msg = "Less than 1."; + throw new ArgumentException(msg, nameof(length)); + } + + var bytes = stream.ReadBytes(length); + + var len = bytes.Length; + if (len == 0) + { + var msg = "No data could be read from it."; + throw new ArgumentException(msg, nameof(stream)); + } + + if (len < length) + { + Logger.Warning( + string.Format( + "Only {0} byte(s) of data could be read from the stream.", + len + ) + ); + } + + send(Opcode.Binary, new MemoryStream(bytes)); + } + + public void SendAsync(byte[] data, Action completed = null) + { + if (_readyState != WebSocketState.Open) + { + var msg = "The current state of the connection is not Open."; + throw new InvalidOperationException(msg); + } + + if (data == null) + { + throw new ArgumentNullException(nameof(data)); + } + + sendAsync(Opcode.Binary, new MemoryStream(data), completed); + } + + public void SendAsync(FileInfo fileInfo, Action completed) + { + if (_readyState != WebSocketState.Open) + { + var msg = "The current state of the connection is not Open."; + throw new InvalidOperationException(msg); + } + + if (fileInfo == null) + { + throw new ArgumentNullException(nameof(fileInfo)); + } + + if (!fileInfo.Exists) + { + var msg = "The file does not exist."; + throw new ArgumentException(msg, nameof(fileInfo)); + } + + FileStream stream; + if (!fileInfo.TryOpenRead(out stream)) + { + var msg = "The file could not be opened."; + throw new ArgumentException(msg, nameof(fileInfo)); + } + + sendAsync(Opcode.Binary, stream, completed); + } + + public void SendAsync(string data, Action completed) + { + if (_readyState != WebSocketState.Open) + { + var msg = "The current state of the connection is not Open."; + throw new InvalidOperationException(msg); + } + + if (data == null) + { + throw new ArgumentNullException(nameof(data)); + } + + byte[] bytes; + if (!data.TryGetUTF8EncodedBytes(out bytes)) + { + var msg = "It could not be UTF-8-encoded."; + throw new ArgumentException(msg, nameof(data)); + } + + sendAsync(Opcode.Text, new MemoryStream(bytes), completed); + } + + public void SendAsync(Stream stream, int length, Action completed) + { + if (_readyState != WebSocketState.Open) + { + var msg = "The current state of the connection is not Open."; + throw new InvalidOperationException(msg); + } + + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + if (!stream.CanRead) + { + var msg = "It cannot be read."; + throw new ArgumentException(msg, nameof(stream)); + } + + if (length < 1) + { + var msg = "Less than 1."; + throw new ArgumentException(msg, nameof(length)); + } + + var bytes = stream.ReadBytes(length); + + var len = bytes.Length; + if (len == 0) + { + var msg = "No data could be read from it."; + throw new ArgumentException(msg, nameof(stream)); + } + + if (len < length) + { + Logger.Warning( + string.Format( + "Only {0} byte(s) of data could be read from the stream.", + len + ) + ); + } + + sendAsync(Opcode.Binary, new MemoryStream(bytes), completed); + } + + public void SetCookie(Cookie cookie) + { + string msg; + if (!checkIfAvailable(true, false, true, false, false, true, out msg)) + { + Logger.Error(msg); + 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 (_forState) + { + if (!checkIfAvailable(true, false, false, true, out msg)) + { + Logger.Error(msg); + 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) + { + string msg; + if (!checkIfAvailable(true, false, true, false, false, true, out msg)) + { + Logger.Error(msg); + error("An error has occurred in setting the credentials.", null); + + return; + } + + if (!checkParametersForSetCredentials(username, password, out msg)) + { + Logger.Error(msg); + error("An error has occurred in setting the credentials.", null); + + return; + } + + lock (_forState) + { + if (!checkIfAvailable(true, false, false, true, out msg)) + { + Logger.Error(msg); + 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) + { + string msg; + if (!checkIfAvailable(true, false, true, false, false, true, out msg)) + { + Logger.Error(msg); + error("An error has occurred in setting the proxy.", null); + + return; + } + + if (!checkParametersForSetProxy(url, username, password, out msg)) + { + Logger.Error(msg); + error("An error has occurred in setting the proxy.", null); + + return; + } + + lock (_forState) + { + if (!checkIfAvailable(true, false, false, true, out msg)) + { + Logger.Error(msg); + 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) + ); + } + } + + void IDisposable.Dispose() + { + close(1001, string.Empty); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/WebSocketException.cs b/EonaCat.Network/System/Sockets/Web/WebSocketException.cs new file mode 100644 index 0000000..da5149a --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/WebSocketException.cs @@ -0,0 +1,55 @@ +// 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 WebSocketException : Exception + { + internal WebSocketException() + : this(CloseStatusCode.Abnormal, null, null) + { + } + + internal WebSocketException(Exception innerException) + : this(CloseStatusCode.Abnormal, null, innerException) + { + } + + internal WebSocketException(string message) + : this(CloseStatusCode.Abnormal, message, null) + { + } + + internal WebSocketException(CloseStatusCode code) + : this(code, null, null) + { + } + + internal WebSocketException(string message, Exception innerException) + : this(CloseStatusCode.Abnormal, message, innerException) + { + } + + internal WebSocketException(CloseStatusCode code, Exception innerException) + : this(code, null, innerException) + { + } + + internal WebSocketException(CloseStatusCode code, string message) + : this(code, message, null) + { + } + + internal WebSocketException( + CloseStatusCode code, string message, Exception innerException + ) + : base("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/WebSocketFrame.cs b/EonaCat.Network/System/Sockets/Web/WebSocketFrame.cs new file mode 100644 index 0000000..9bce617 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/WebSocketFrame.cs @@ -0,0 +1,668 @@ +// 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 WebSocketFrame : IEnumerable + { + private const int BUFFER_SIZE = 1024; + + internal static readonly byte[] EmptyPingBytes; + + static WebSocketFrame() + { + EmptyPingBytes = CreatePingFrame(false).ToArray(); + } + + private WebSocketFrame() + { + } + + internal WebSocketFrame(Opcode opcode, PayloadData payloadData, bool mask) + : this(FinalFrame.Final, opcode, payloadData, false, mask) + { + } + + internal WebSocketFrame(FinalFrame finalFrame, Opcode opcode, byte[] data, bool compressed, bool mask) + : this(finalFrame, opcode, new PayloadData(data), compressed, mask) + { + } + + internal WebSocketFrame( + FinalFrame fin, Opcode opcode, PayloadData payloadData, bool compressed, bool mask) + { + Fin = fin; + Rsv1 = opcode.IsData() && compressed ? Rsv.On : Rsv.Off; + Rsv2 = Rsv.Off; + Rsv3 = Rsv.Off; + Opcode = opcode; + + var len = payloadData.Length; + if (len < 126) + { + PayloadLength = (byte)len; + ExtendedPayloadLength = WebSocket.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(); + payloadData.Mask(MaskingKey); + } + else + { + Mask = Mask.Off; + MaskingKey = WebSocket.EmptyBytes; + } + + PayloadData = payloadData; + } + + 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 byte[] ExtendedPayloadLength { get; private set; } + + public FinalFrame Fin { get; private set; } + + public bool IsBinary => Opcode == Opcode.Binary; + + public bool IsClose => Opcode == Opcode.Close; + + public bool IsCompressed => Rsv1 == Rsv.On; + + public bool IsContinuation => Opcode == Opcode.Cont; + + public bool IsControl => Opcode >= Opcode.Close; + + public bool IsData => Opcode == Opcode.Text || Opcode == Opcode.Binary; + + public bool IsFinal => Fin == FinalFrame.Final; + + public bool IsFragment => Fin == FinalFrame.More || Opcode == Opcode.Cont; + + public bool IsMasked => Mask == Mask.On; + + public bool IsPing => Opcode == Opcode.Ping; + + public bool IsPong => Opcode == Opcode.Pong; + + public bool IsText => Opcode == Opcode.Text; + + public ulong Length => 2 + (ulong)(ExtendedPayloadLength.Length + MaskingKey.Length) + PayloadData.Length; + + public Mask Mask { get; private set; } + + public byte[] MaskingKey { get; private set; } + + public Opcode Opcode { get; private set; } + + public PayloadData PayloadData { get; private set; } + + public byte PayloadLength { get; private set; } + + public Rsv Rsv1 { get; private set; } + + public Rsv Rsv2 { get; private set; } + + public Rsv Rsv3 { get; private set; } + + private static byte[] createMaskingKey() + { + var key = new byte[4]; + WebSocket.RandomNumber.GetBytes(key); + + return key; + } + + private static string dump(WebSocketFrame frame) + { + var len = frame.Length; + var cnt = (long)(len / 4); + var rem = (int)(len % 4); + + int cntDigit; + string cntFmt; + if (cnt < 10000) + { + cntDigit = 4; + cntFmt = "{0,4}"; + } + else if (cnt < 0x010000) + { + cntDigit = 4; + cntFmt = "{0,4:X}"; + } + else if (cnt < 0x0100000000) + { + cntDigit = 8; + cntFmt = "{0,8:X}"; + } + else + { + cntDigit = 16; + cntFmt = "{0,16:X}"; + } + + var spFmt = string.Format("{{0,{0}}}", cntDigit); + var headerFmt = string.Format(@" +{0} 01234567 89ABCDEF 01234567 89ABCDEF +{0}+--------+--------+--------+--------+\n", spFmt); + var lineFmt = string.Format("{0}|{{1,8}} {{2,8}} {{3,8}} {{4,8}}|\n", cntFmt); + var footerFmt = string.Format("{0}+--------+--------+--------+--------+", spFmt); + + var output = new StringBuilder(64); + Func> linePrinter = () => + { + long lineCnt = 0; + return (arg1, arg2, arg3, arg4) => + output.AppendFormat(lineFmt, ++lineCnt, arg1, arg2, arg3, arg4); + }; + var printLine = linePrinter(); + + output.AppendFormat(headerFmt, string.Empty); + + var bytes = frame.ToArray(); + for (long i = 0; i <= cnt; i++) + { + var j = i * 4; + if (i < cnt) + { + printLine( + Convert.ToString(bytes[j], 2).PadLeft(8, '0'), + Convert.ToString(bytes[j + 1], 2).PadLeft(8, '0'), + Convert.ToString(bytes[j + 2], 2).PadLeft(8, '0'), + Convert.ToString(bytes[j + 3], 2).PadLeft(8, '0')); + + continue; + } + + if (rem > 0) + { + printLine( + Convert.ToString(bytes[j], 2).PadLeft(8, '0'), + rem >= 2 ? Convert.ToString(bytes[j + 1], 2).PadLeft(8, '0') : string.Empty, + rem == 3 ? Convert.ToString(bytes[j + 2], 2).PadLeft(8, '0') : string.Empty, + string.Empty); + } + } + + output.AppendFormat(footerFmt, string.Empty); + return output.ToString(); + } + + private static string print(WebSocketFrame frame) + { + // Payload Length + var payloadLen = frame.PayloadLength; + + // Extended Payload Length + var extPayloadLen = payloadLen > 125 ? frame.FullPayloadLength.ToString() : string.Empty; + + // Masking Key + var maskingKey = BitConverter.ToString(frame.MaskingKey); + + // Payload Data + var payload = payloadLen == 0 + ? string.Empty + : payloadLen > 125 + ? "---" + : frame.IsText && !(frame.IsFragment || frame.IsMasked || frame.IsCompressed) + ? frame.PayloadData.ApplicationData.UTF8Decode() + : frame.PayloadData.ToString(); + + var fmt = @" + FIN: {0} + RSV1: {1} + RSV2: {2} + RSV3: {3} + Opcode: {4} + MASK: {5} + Payload Length: {6} +Extended Payload Length: {7} + Masking Key: {8} + Payload Data: {9}"; + + return string.Format( + fmt, + frame.Fin, + frame.Rsv1, + frame.Rsv2, + frame.Rsv3, + frame.Opcode, + frame.Mask, + payloadLen, + extPayloadLen, + maskingKey, + payload); + } + + private static WebSocketFrame processHeader(byte[] header) + { + if (header.Length != 2) + { + throw new WebSocketException("The header 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 ? Rsv.On : Rsv.Off; + + // RSV2 + var rsv2 = (header[0] & 0x20) == 0x20 ? Rsv.On : Rsv.Off; + + // RSV3 + var rsv3 = (header[0] & 0x10) == 0x10 ? Rsv.On : Rsv.Off; + + // Opcode + var opcode = (byte)(header[0] & 0x0f); + + // MASK + var mask = (header[1] & 0x80) == 0x80 ? Mask.On : Mask.Off; + + // Payload Length + var payloadLen = (byte)(header[1] & 0x7f); + + var err = !opcode.IsSupported() + ? "An unsupported opcode." + : !opcode.IsData() && rsv1 == Rsv.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 WebSocketException(CloseStatusCode.ProtocolError, err); + } + + var frame = new WebSocketFrame(); + frame.Fin = fin; + frame.Rsv1 = rsv1; + frame.Rsv2 = rsv2; + frame.Rsv3 = rsv3; + frame.Opcode = (Opcode)opcode; + frame.Mask = mask; + frame.PayloadLength = payloadLen; + + return frame; + } + + private static WebSocketFrame readExtendedPayloadLength(Stream stream, WebSocketFrame frame) + { + var len = frame.ExtendedPayloadLengthCount; + if (len == 0) + { + frame.ExtendedPayloadLength = WebSocket.EmptyBytes; + return frame; + } + + var bytes = stream.ReadBytes(len); + if (bytes.Length != len) + { + throw new WebSocketException( + "The extended payload length of a frame cannot be read from the stream."); + } + + frame.ExtendedPayloadLength = bytes; + return frame; + } + + private static void readExtendedPayloadLengthAsync( + Stream stream, + WebSocketFrame frame, + Action completed, + Action error) + { + var len = frame.ExtendedPayloadLengthCount; + if (len == 0) + { + frame.ExtendedPayloadLength = WebSocket.EmptyBytes; + completed(frame); + + return; + } + + stream.ReadBytesAsync( + len, + bytes => + { + if (bytes.Length != len) + { + throw new WebSocketException( + "The extended payload length of a frame cannot be read from the stream."); + } + + frame.ExtendedPayloadLength = bytes; + completed(frame); + }, + error); + } + + private static WebSocketFrame readHeader(Stream stream) + { + return processHeader(stream.ReadBytes(2)); + } + + private static void readHeaderAsync( + Stream stream, Action completed, Action error) + { + stream.ReadBytesAsync(2, bytes => completed(processHeader(bytes)), error); + } + + private static WebSocketFrame readMaskingKey(Stream stream, WebSocketFrame frame) + { + var len = frame.IsMasked ? 4 : 0; + if (len == 0) + { + frame.MaskingKey = WebSocket.EmptyBytes; + return frame; + } + + var bytes = stream.ReadBytes(len); + if (bytes.Length != len) + { + throw new WebSocketException("The masking key of a frame cannot be read from the stream."); + } + + frame.MaskingKey = bytes; + return frame; + } + + private static void readMaskingKeyAsync( + Stream stream, + WebSocketFrame frame, + Action completed, + Action error) + { + var len = frame.IsMasked ? 4 : 0; + if (len == 0) + { + frame.MaskingKey = WebSocket.EmptyBytes; + completed(frame); + + return; + } + + stream.ReadBytesAsync( + len, + bytes => + { + if (bytes.Length != len) + { + throw new WebSocketException( + "The masking key of a frame cannot be read from the stream."); + } + + frame.MaskingKey = bytes; + completed(frame); + }, + error); + } + + private static WebSocketFrame readPayloadData(Stream stream, WebSocketFrame frame) + { + var len = frame.FullPayloadLength; + if (len == 0) + { + frame.PayloadData = PayloadData.Empty; + return frame; + } + + if (len > PayloadData.MaxLength) + { + throw new WebSocketException(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 WebSocketException( + "The payload data of a frame cannot be read from the stream."); + } + + frame.PayloadData = new PayloadData(bytes, llen); + return frame; + } + + private static void readPayloadDataAsync( + Stream stream, + WebSocketFrame frame, + Action completed, + Action error) + { + var len = frame.FullPayloadLength; + if (len == 0) + { + frame.PayloadData = PayloadData.Empty; + completed(frame); + + return; + } + + if (len > PayloadData.MaxLength) + { + throw new WebSocketException(CloseStatusCode.TooBig, "A frame has a long payload length."); + } + + var llen = (long)len; + Action compl = bytes => + { + if (bytes.LongLength != llen) + { + throw new WebSocketException( + "The payload data of a frame cannot be read from the stream."); + } + + frame.PayloadData = new PayloadData(bytes, llen); + completed(frame); + }; + + if (frame.PayloadLength < 127) + { + stream.ReadBytesAsync((int)len, compl, error); + return; + } + + stream.ReadBytesAsync(llen, BUFFER_SIZE, compl, error); + } + + internal static WebSocketFrame CreateCloseFrame( + PayloadData payloadData, bool mask + ) + { + return new WebSocketFrame( + FinalFrame.Final, Opcode.Close, payloadData, false, mask + ); + } + + internal static WebSocketFrame CreatePingFrame(bool mask) + { + return new WebSocketFrame( + FinalFrame.Final, Opcode.Ping, PayloadData.Empty, false, mask + ); + } + + internal static WebSocketFrame CreatePingFrame(byte[] data, bool mask) + { + return new WebSocketFrame( + FinalFrame.Final, Opcode.Ping, new PayloadData(data), false, mask + ); + } + + internal static WebSocketFrame CreatePongFrame( + PayloadData payloadData, bool mask + ) + { + return new WebSocketFrame( + FinalFrame.Final, Opcode.Pong, payloadData, false, mask + ); + } + + internal static WebSocketFrame ReadFrame(Stream stream, bool unmask) + { + var frame = readHeader(stream); + readExtendedPayloadLength(stream, frame); + readMaskingKey(stream, frame); + readPayloadData(stream, frame); + + if (unmask) + { + frame.Unmask(); + } + + return frame; + } + + internal static void ReadFrameAsync( + Stream stream, + bool unmask, + Action completed, + Action error + ) + { + readHeaderAsync( + stream, + frame => + readExtendedPayloadLengthAsync( + stream, + frame, + frame1 => + readMaskingKeyAsync( + stream, + frame1, + frame2 => + readPayloadDataAsync( + stream, + frame2, + frame3 => + { + if (unmask) + { + frame3.Unmask(); + } + + completed(frame3); + }, + error + ), + error + ), + error + ), + error + ); + } + + internal void Unmask() + { + if (Mask == Mask.Off) + { + return; + } + + Mask = Mask.Off; + PayloadData.Mask(MaskingKey); + MaskingKey = WebSocket.EmptyBytes; + } + + public IEnumerator GetEnumerator() + { + foreach (var b in ToArray()) + { + yield return b; + } + } + + public void Print(bool dumped) + { + Console.WriteLine(dumped ? dump(this) : print(this)); + } + + public string PrintToString(bool dumped) + { + return dumped ? dump(this) : print(this); + } + + public byte[] ToArray() + { + using (var buff = new MemoryStream()) + { + var header = (int)Fin; + header = (header << 1) + (int)Rsv1; + header = (header << 1) + (int)Rsv2; + header = (header << 1) + (int)Rsv3; + header = (header << 4) + (int)Opcode; + header = (header << 1) + (int)Mask; + header = (header << 7) + 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 = PayloadData.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()); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/WebSocketSecureClient.cs b/EonaCat.Network/System/Sockets/Web/WebSocketSecureClient.cs deleted file mode 100644 index 2d4688f..0000000 --- a/EonaCat.Network/System/Sockets/Web/WebSocketSecureClient.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System; -using System.Net; -using System.Net.WebSockets; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace EonaCat.Network -{ - public class WebSocketSecureClient - { - private const int BUFFER_SIZE = 4096; - - /// - /// OnConnect event - /// - public event Action OnConnect; - - /// - /// OnReceive event - /// - public event Action OnReceive; - - /// - /// OnDisconnect event - /// - public event Action OnDisconnect; - - /// - /// OnError event - /// - public event Action OnError; - - private ClientWebSocket _webSocket; - - /// - /// The client name to be sent when connecting (optional). - /// - public string ClientName { get; set; } - - /// - /// Create secure WebSocket client with certificate support - /// - /// - /// The client certificate for the connection - /// The password for the connection - /// The keepalive interval in seconds for the connection - /// The cookies to be send with the connection - /// - public Task ConnectAsync(string uri, X509Certificate2 clientCertificate = null, string password = null, int keepAliveIntervalSeconds = 30, CookieContainer? cookieContainer = null) - { - return CreateWebSocketClientAsync(uri, clientCertificate, password, keepAliveIntervalSeconds, cookieContainer); - } - - private async Task CreateWebSocketClientAsync(string uri, X509Certificate2 clientCertificate, string password, int keepAliveIntervalSeconds = 30, CookieContainer? cookieContainer = null) - { - _webSocket = new ClientWebSocket(); - - if (clientCertificate != null) - { - _webSocket.Options.ClientCertificates.Add(clientCertificate); - } - - if (!string.IsNullOrEmpty(password)) - { - var passwordBytes = Encoding.UTF8.GetBytes(password); - _webSocket.Options.SetRequestHeader("Password", Convert.ToBase64String(passwordBytes)); - } - - if (!string.IsNullOrEmpty(ClientName)) - { - _webSocket.Options.SetRequestHeader("ClientName", ClientName); - } - - if (cookieContainer != null && cookieContainer.Count > 0) - { - // Manually set cookies in the request header - _webSocket.Options.SetRequestHeader("Cookie", cookieContainer.GetCookieHeader(new Uri(uri))); - } - - if (keepAliveIntervalSeconds > 0) - { - _webSocket.Options.KeepAliveInterval = TimeSpan.FromSeconds(keepAliveIntervalSeconds); - } - - try - { - Uri serverUri = new Uri(uri); - await _webSocket.ConnectAsync(serverUri, CancellationToken.None).ConfigureAwait(false); - OnConnect?.Invoke(new RemoteInfo { IsWebSocket = true }); - _ = StartReceivingAsync(); - } - catch (Exception ex) - { - OnError?.Invoke(ex, $"Exception: {ex.Message}"); - Disconnect(); - } - } - - private async Task StartReceivingAsync() - { - byte[] buffer = new byte[BUFFER_SIZE]; - while (_webSocket.State == WebSocketState.Open) - { - try - { - var result = await _webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None).ConfigureAwait(false); - if (result.Count > 0) - { - byte[] data = new byte[result.Count]; - Buffer.BlockCopy(buffer, 0, data, 0, result.Count); - OnReceive?.Invoke(new RemoteInfo { IsWebSocket = true, Data = data }); - } - else - { - break; - } - } - catch (Exception ex) - { - OnError?.Invoke(ex, $"Exception: {ex.Message}"); - break; - } - } - OnDisconnect?.Invoke(new RemoteInfo { IsWebSocket = true }); - } - - /// - /// Send data - /// - /// - /// - public Task SendAsync(byte[] data) - { - return _webSocket.SendAsync(new ArraySegment(data), WebSocketMessageType.Binary, true, CancellationToken.None); - } - - /// - /// Disconnect - /// - public void Disconnect() - { - _webSocket?.CloseAsync(WebSocketCloseStatus.NormalClosure, "Disconnecting", CancellationToken.None); - } - } -} diff --git a/EonaCat.Network/System/Sockets/Web/WebSocketSecureServer.cs b/EonaCat.Network/System/Sockets/Web/WebSocketSecureServer.cs deleted file mode 100644 index e9c8229..0000000 --- a/EonaCat.Network/System/Sockets/Web/WebSocketSecureServer.cs +++ /dev/null @@ -1,280 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.WebSockets; -using System.Security.Authentication; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace EonaCat.Network -{ - public class WebSocketSecureServer - { - private const int BUFFER_SIZE = 4096; - - /// - /// OnConnect event - /// - public event Action OnConnect; - - /// - /// OnReceive event - /// - public event Action OnReceive; - - /// - /// OnSend event - /// - public event Action OnSend; - - /// - /// OnDisconnect event - /// - public event Action OnDisconnect; - - /// - /// The TLS version to be used by the server - /// - public SslProtocols TlsVersion { get; set; } = SslProtocols.Tls12; - - /// - /// OnError event - /// - public event Action OnError; - - private readonly HttpListener _listener; - private CancellationTokenSource _cancellationTokenSource; - private Task _acceptTask; - private readonly X509Certificate2 _serverCertificate; - private readonly string _requiredPassword; - private bool _passwordProtectionEnabled; - - private readonly Dictionary _connectedClients = new Dictionary(); - private readonly Dictionary _clientNames = new Dictionary(); - - public CookieContainer Cookies { get; private set; } = new CookieContainer(); - public bool IsDebugHttpConnection { get; set; } - - /// - /// Create secure WebSocket server with certificate support - /// - /// Array of URI prefixes to listen on, e.g., "https://localhost:8443/" - /// The server certificate for HTTPS - /// The password required from clients for the connection (optional) - public WebSocketSecureServer(List uriPrefixes, X509Certificate2 serverCertificate = null, string requiredPassword = null) - { - _listener = new HttpListener(); - _serverCertificate = serverCertificate; - _requiredPassword = requiredPassword; - _passwordProtectionEnabled = !string.IsNullOrEmpty(requiredPassword); - - foreach (var uriPrefix in uriPrefixes) - { - if (uriPrefix.StartsWith("https")) - { - _listener.Prefixes.Add(uriPrefix); - } - else if (uriPrefix.StartsWith("wss")) - { - _listener.Prefixes.Add(uriPrefix.Replace("wss", "https")); - } - else - { - throw new ArgumentException("Invalid URI prefix. Use 'https' or 'wss' prefixes."); - } - } - } - - /// - /// Start secure WebSocket server - /// - /// - /// - public Task StartAsync(CancellationToken cancellationToken = default) - { - _listener.Start(); - _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - _acceptTask = AcceptConnectionsAsync(_cancellationTokenSource.Token); - return _acceptTask; - } - - private async Task AcceptConnectionsAsync(CancellationToken cancellationToken) - { - while (!cancellationToken.IsCancellationRequested) - { - try - { - var context = await _listener.GetContextAsync().ConfigureAwait(false); - - if (IsDebugHttpConnection) - { - // Debug connection - DebugHttpsConnection(context); - } - - if (context.Request.IsWebSocketRequest) - { - if (_passwordProtectionEnabled) - { - var password = context.Request.Headers["Password"]; - - if (password != _requiredPassword) - { - context.Response.StatusCode = 401; // Unauthorized - context.Response.Close(); - continue; - } - } - - // Manually set cookies in the HTTP response header - if (Cookies.Count > 0) - { - context.Response.Headers.Add("Set-Cookie", Cookies.GetCookieHeader(context.Request.Url)); - } - - string clientName = null; - foreach (var key in context.Request.Headers.AllKeys) - { - if (key == "ClientName") - { - clientName = context.Request.Headers["ClientName"]; - break; - } - } - - var webSocketContext = await context.AcceptWebSocketAsync(subProtocol: null).ConfigureAwait(false); - _ = HandleWebSocketConnectionAsync(webSocketContext.WebSocket, clientName, cancellationToken); - } - else - { - context.Response.StatusCode = 400; - context.Response.Close(); - } - } - catch (Exception ex) - { - OnError?.Invoke(ex, $"Exception: {ex.Message}"); - } - } - } - - - private async Task HandleWebSocketConnectionAsync(WebSocket webSocket, string clientName, CancellationToken cancellationToken) - { - string clientId = Guid.NewGuid().ToString(); - _connectedClients.Add(clientId, webSocket); - - if (!string.IsNullOrEmpty(clientName)) - { - _clientNames.TryAdd(clientId, clientName); - } - - OnConnect?.Invoke(new RemoteInfo { IsWebSocket = true, ClientId = clientId, ClientName = clientName }); - - byte[] buffer = new byte[BUFFER_SIZE]; - while (webSocket.State == WebSocketState.Open) - { - try - { - var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken).ConfigureAwait(false); - if (result.Count > 0) - { - byte[] data = new byte[result.Count]; - Buffer.BlockCopy(buffer, 0, data, 0, result.Count); - OnReceive?.Invoke(new RemoteInfo { IsWebSocket = true, ClientId = clientId, ClientName = clientName, Data = data }); - } - else - { - break; - } - } - catch (Exception ex) - { - OnError?.Invoke(ex, $"Exception: {ex.Message}"); - break; - } - } - - OnDisconnect?.Invoke(new RemoteInfo { IsWebSocket = true, ClientId = clientId, ClientName = clientName }); - _connectedClients.Remove(clientId); - _clientNames.Remove(clientId); - } - - /// - /// Stop secure WebSocket server - /// - /// - public async Task StopAsync() - { - _cancellationTokenSource.Cancel(); - _listener.Stop(); - if (_acceptTask != null) - { - try - { - await _acceptTask.ConfigureAwait(false); - } - catch (AggregateException) { } - } - } - - /// - /// Debug the HTTPS connection information - /// - /// HttpListenerContext representing the incoming connection - private void DebugHttpsConnection(HttpListenerContext context) - { - try - { - Console.WriteLine($"Incoming HTTPS Connection from: {context.Request.RemoteEndPoint}"); - - // Output headers - Console.WriteLine("Headers:"); - foreach (string key in context.Request.Headers.Keys) - { - Console.WriteLine($"{key}: {context.Request.Headers[key]}"); - } - - // Check if the connection is WebSocket request - if (context.Request.IsWebSocketRequest) - { - Console.WriteLine("WebSocket Request detected."); - - // Output WebSocket-specific information - if (_passwordProtectionEnabled) - { - var password = context.Request.Headers["Password"]; - Console.WriteLine($"Password: {password}"); - } - - if (Cookies.Count > 0) - { - Console.WriteLine($"Cookies: {Cookies.GetCookieHeader(context.Request.Url)}"); - } - - foreach (var key in context.Request.Headers.AllKeys) - { - if (key == "ClientName") - { - var clientName = context.Request.Headers["ClientName"]; - Console.WriteLine($"ClientName: {clientName}"); - break; - } - } - } - else - { - Console.WriteLine("Invalid WebSocket Request. Closing connection."); - context.Response.StatusCode = 400; - context.Response.Close(); - } - } - catch (Exception ex) - { - Console.WriteLine($"Error while debugging HTTPS connection: {ex.Message}"); - } - } - } -} diff --git a/EonaCat.Network/System/Sockets/Web/WebSocketState.cs b/EonaCat.Network/System/Sockets/Web/WebSocketState.cs new file mode 100644 index 0000000..31e49ed --- /dev/null +++ b/EonaCat.Network/System/Sockets/Web/WebSocketState.cs @@ -0,0 +1,16 @@ +// 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 WebSocketState : ushort + { + Connecting = 0, + + Open = 1, + + Closing = 2, + + Closed = 3 + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Web/WebSocketTransportContext.cs b/EonaCat.Network/System/Sockets/Web/WebSocketTransportContext.cs deleted file mode 100644 index 484d7a3..0000000 --- a/EonaCat.Network/System/Sockets/Web/WebSocketTransportContext.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Security.Authentication; - -namespace EonaCat.Network -{ - public class WebSocketTransportContext - { - public SslProtocols TlsVersion { get; set; } - } -} diff --git a/EonaCat.Network/System/Tools/FileReader.cs b/EonaCat.Network/System/Tools/FileReader.cs index c618411..2bd2ca6 100644 --- a/EonaCat.Network/System/Tools/FileReader.cs +++ b/EonaCat.Network/System/Tools/FileReader.cs @@ -2,8 +2,8 @@ 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. +// 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 { diff --git a/EonaCat.Network/System/Tools/Helpers.cs b/EonaCat.Network/System/Tools/Helpers.cs index f182cc7..0d77071 100644 --- a/EonaCat.Network/System/Tools/Helpers.cs +++ b/EonaCat.Network/System/Tools/Helpers.cs @@ -3,8 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Sockets; -using System.Security.Cryptography.X509Certificates; -using System.Text; +using System.Runtime.InteropServices; using System.Text.RegularExpressions; using System.Threading; @@ -34,7 +33,7 @@ namespace EonaCat.Network // Check if (!(IPv4Verify(IPPrefix + DStart) && IPv4Verify(IPPrefix + DEnd))) { - throw new Exception("Wrong Scan Parameters"); + throw new Exception("EonaCat Network: Wrong Scan Parameters"); } if (DStart > DEnd) diff --git a/EonaCat.Network/System/Web/AccessControlManager.cs b/EonaCat.Network/System/Web/AccessControlManager.cs index b79ab4f..312860d 100644 --- a/EonaCat.Network/System/Web/AccessControlManager.cs +++ b/EonaCat.Network/System/Web/AccessControlManager.cs @@ -1,5 +1,5 @@ -using System; -using IpMatcher; +using IpMatcher; +using System; namespace EonaCat.Network { @@ -46,8 +46,10 @@ namespace EonaCat.Network /// True if permitted. public bool Permit(string ip) { - if (String.IsNullOrEmpty(ip)) + if (string.IsNullOrEmpty(ip)) + { throw new ArgumentNullException(nameof(ip)); + } switch (Mode) { @@ -56,7 +58,10 @@ namespace EonaCat.Network case AccessControlMode.DefaultPermit: if (DenyList.MatchExists(ip)) + { return false; + } + return true; default: diff --git a/EonaCat.Network/System/Web/AccessControlMode.cs b/EonaCat.Network/System/Web/AccessControlMode.cs index 519e686..c706115 100644 --- a/EonaCat.Network/System/Web/AccessControlMode.cs +++ b/EonaCat.Network/System/Web/AccessControlMode.cs @@ -1,5 +1,5 @@ -using System.Runtime.Serialization; -using EonaCat.Json.Converters; +using EonaCat.Json.Converters; +using System.Runtime.Serialization; namespace EonaCat.Network { @@ -9,7 +9,7 @@ namespace EonaCat.Network /// /// Access control mode of operation. /// - [EonaCat.Json.Converter(typeof(StringEnumConverter))] + [Json.Converter(typeof(StringEnumConverter))] public enum AccessControlMode { /// diff --git a/EonaCat.Network/System/Web/DynamicRouteAttribute.cs b/EonaCat.Network/System/Web/Attributes/DynamicRouteAttribute.cs similarity index 94% rename from EonaCat.Network/System/Web/DynamicRouteAttribute.cs rename to EonaCat.Network/System/Web/Attributes/DynamicRouteAttribute.cs index dbef73b..4f682be 100644 --- a/EonaCat.Network/System/Web/DynamicRouteAttribute.cs +++ b/EonaCat.Network/System/Web/Attributes/DynamicRouteAttribute.cs @@ -44,10 +44,15 @@ namespace EonaCat.Network Path = new Regex(path); Method = method; - if (!String.IsNullOrEmpty(guid)) + if (!string.IsNullOrEmpty(guid)) + { GUID = guid; + } + if (metadata != null) + { Metadata = metadata; + } } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/ParameterRouteAttribute.cs b/EonaCat.Network/System/Web/Attributes/ParameterRouteAttribute.cs similarity index 94% rename from EonaCat.Network/System/Web/ParameterRouteAttribute.cs rename to EonaCat.Network/System/Web/Attributes/ParameterRouteAttribute.cs index 9e82c2b..57a2ddf 100644 --- a/EonaCat.Network/System/Web/ParameterRouteAttribute.cs +++ b/EonaCat.Network/System/Web/Attributes/ParameterRouteAttribute.cs @@ -44,10 +44,15 @@ namespace EonaCat.Network Path = path; Method = method; - if (!String.IsNullOrEmpty(guid)) + if (!string.IsNullOrEmpty(guid)) + { GUID = guid; + } + if (metadata != null) + { Metadata = metadata; + } } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/StaticRouteAttribute.cs b/EonaCat.Network/System/Web/Attributes/StaticRouteAttribute.cs similarity index 94% rename from EonaCat.Network/System/Web/StaticRouteAttribute.cs rename to EonaCat.Network/System/Web/Attributes/StaticRouteAttribute.cs index 4194628..ee9f57f 100644 --- a/EonaCat.Network/System/Web/StaticRouteAttribute.cs +++ b/EonaCat.Network/System/Web/Attributes/StaticRouteAttribute.cs @@ -43,10 +43,15 @@ namespace EonaCat.Network Path = path; Method = method; - if (!String.IsNullOrEmpty(guid)) + if (!string.IsNullOrEmpty(guid)) + { GUID = guid; + } + if (metadata != null) + { Metadata = metadata; + } } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/ContentRoute.cs b/EonaCat.Network/System/Web/ContentRoute.cs index 7b4ff8b..168b3e9 100644 --- a/EonaCat.Network/System/Web/ContentRoute.cs +++ b/EonaCat.Network/System/Web/ContentRoute.cs @@ -1,5 +1,5 @@ -using System; -using EonaCat.Json; +using EonaCat.Json; +using System; namespace EonaCat.Network { @@ -44,15 +44,23 @@ namespace EonaCat.Network /// User-supplied metadata. public ContentRoute(string path, bool isDirectory, string guid = null, object metadata = null) { - if (String.IsNullOrEmpty(path)) + if (string.IsNullOrEmpty(path)) + { throw new ArgumentNullException(nameof(path)); + } + Path = path.ToLower(); IsDirectory = isDirectory; - if (!String.IsNullOrEmpty(guid)) + if (!string.IsNullOrEmpty(guid)) + { GUID = guid; + } + if (metadata != null) + { Metadata = metadata; + } } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/ContentRouteManager.cs b/EonaCat.Network/System/Web/ContentRouteManager.cs index 99bcb39..53ee031 100644 --- a/EonaCat.Network/System/Web/ContentRouteManager.cs +++ b/EonaCat.Network/System/Web/ContentRouteManager.cs @@ -23,18 +23,23 @@ namespace EonaCat.Network } set { - if (String.IsNullOrEmpty(value)) + if (string.IsNullOrEmpty(value)) + { _BaseDirectory = AppDomain.CurrentDomain.BaseDirectory; + } else { if (!Directory.Exists(value)) - throw new DirectoryNotFoundException("The requested directory '" + value + "' was not found or not accessible."); + { + throw new DirectoryNotFoundException("EonaCat Network: The requested directory '" + value + "' was not found or not accessible."); + } + _BaseDirectory = value; } } } - private List _Routes = new List(); + private readonly List _Routes = new List(); private readonly object _Lock = new object(); private string _BaseDirectory = AppDomain.CurrentDomain.BaseDirectory; @@ -54,8 +59,11 @@ namespace EonaCat.Network /// User-supplied metadata. public void Add(string path, bool isDirectory, string guid = null, object metadata = null) { - if (String.IsNullOrEmpty(path)) + if (string.IsNullOrEmpty(path)) + { throw new ArgumentNullException(nameof(path)); + } + Add(new ContentRoute(path, isDirectory, guid, metadata)); } @@ -65,12 +73,16 @@ namespace EonaCat.Network /// URL path. public void Remove(string path) { - if (String.IsNullOrEmpty(path)) + if (string.IsNullOrEmpty(path)) + { throw new ArgumentNullException(nameof(path)); + } ContentRoute r = Get(path); if (r == null) + { return; + } lock (_Lock) { @@ -87,14 +99,21 @@ namespace EonaCat.Network /// ContentRoute if the route exists, otherwise null. public ContentRoute Get(string path) { - if (String.IsNullOrEmpty(path)) + if (string.IsNullOrEmpty(path)) + { throw new ArgumentNullException(nameof(path)); + } path = path.ToLower(); if (!path.StartsWith("/")) + { path = "/" + path; + } + if (!path.EndsWith("/")) + { path = path + "/"; + } lock (_Lock) { @@ -103,12 +122,16 @@ namespace EonaCat.Network if (curr.IsDirectory) { if (path.StartsWith(curr.Path.ToLower())) + { return curr; + } } else { if (path.Equals(curr.Path.ToLower())) + { return curr; + } } } @@ -123,12 +146,16 @@ namespace EonaCat.Network /// True if exists. public bool Exists(string path) { - if (String.IsNullOrEmpty(path)) + if (string.IsNullOrEmpty(path)) + { throw new ArgumentNullException(nameof(path)); + } path = path.ToLower(); if (!path.StartsWith("/")) + { path = "/" + path; + } lock (_Lock) { @@ -137,12 +164,16 @@ namespace EonaCat.Network if (curr.IsDirectory) { if (path.StartsWith(curr.Path.ToLower())) + { return true; + } } else { if (path.Equals(curr.Path.ToLower())) + { return true; + } } } } @@ -159,13 +190,17 @@ namespace EonaCat.Network public bool Match(string path, out ContentRoute route) { route = null; - if (String.IsNullOrEmpty(path)) + if (string.IsNullOrEmpty(path)) + { throw new ArgumentNullException(nameof(path)); + } path = path.ToLower(); string dirPath = path; if (!dirPath.EndsWith("/")) + { dirPath = dirPath + "/"; + } lock (_Lock) { @@ -196,13 +231,20 @@ namespace EonaCat.Network private void Add(ContentRoute route) { if (route == null) + { throw new ArgumentNullException(nameof(route)); + } route.Path = route.Path.ToLower(); if (!route.Path.StartsWith("/")) + { route.Path = "/" + route.Path; + } + if (route.IsDirectory && !route.Path.EndsWith("/")) + { route.Path = route.Path + "/"; + } if (Exists(route.Path)) { @@ -218,7 +260,9 @@ namespace EonaCat.Network private void Remove(ContentRoute route) { if (route == null) + { throw new ArgumentNullException(nameof(route)); + } lock (_Lock) { diff --git a/EonaCat.Network/System/Web/ContentRouteProcessor.cs b/EonaCat.Network/System/Web/ContentRouteProcessor.cs index 8f6d8b4..9572414 100644 --- a/EonaCat.Network/System/Web/ContentRouteProcessor.cs +++ b/EonaCat.Network/System/Web/ContentRouteProcessor.cs @@ -28,12 +28,14 @@ namespace EonaCat.Network /// public FileShare ContentFileShare = FileShare.Read; - private ContentRouteManager _Routes; + private readonly ContentRouteManager _Routes; internal ContentRouteHandler(ContentRouteManager routes) { if (routes == null) + { throw new ArgumentNullException(nameof(routes)); + } _Routes = routes; } @@ -41,11 +43,19 @@ namespace EonaCat.Network internal async Task Process(HttpContext ctx, CancellationToken token) { if (ctx == null) + { throw new ArgumentNullException(nameof(ctx)); + } + if (ctx.Request == null) + { throw new ArgumentNullException(nameof(ctx.Request)); + } + if (ctx.Response == null) + { throw new ArgumentNullException(nameof(ctx.Response)); + } if (ctx.Request.Method != HttpMethod.GET && ctx.Request.Method != HttpMethod.HEAD) @@ -56,16 +66,20 @@ namespace EonaCat.Network } string filePath = ctx.Request.Url.RawWithoutQuery; - if (!String.IsNullOrEmpty(filePath)) + if (!string.IsNullOrEmpty(filePath)) { while (filePath.StartsWith("/")) + { filePath = filePath.Substring(1); + } } string baseDirectory = _Routes.BaseDirectory; baseDirectory = baseDirectory.Replace("\\", "/"); if (!baseDirectory.EndsWith("/")) + { baseDirectory += "/"; + } filePath = baseDirectory + filePath; filePath = filePath.Replace("+", " ").Replace("%20", " "); @@ -109,8 +123,10 @@ namespace EonaCat.Network private string GetContentType(string path) { - if (String.IsNullOrEmpty(path)) + if (string.IsNullOrEmpty(path)) + { return "application/octet-stream"; + } int idx = path.LastIndexOf("."); if (idx >= 0) diff --git a/EonaCat.Network/System/Web/DynamicRoute.cs b/EonaCat.Network/System/Web/DynamicRoute.cs index 0f1e20f..ee82a2c 100644 --- a/EonaCat.Network/System/Web/DynamicRoute.cs +++ b/EonaCat.Network/System/Web/DynamicRoute.cs @@ -1,7 +1,7 @@ -using System; +using EonaCat.Json; +using System; using System.Text.RegularExpressions; using System.Threading.Tasks; -using EonaCat.Json; namespace EonaCat.Network { @@ -54,18 +54,28 @@ namespace EonaCat.Network public DynamicRoute(HttpMethod method, Regex path, Func handler, string guid = null, object metadata = null) { if (path == null) + { throw new ArgumentNullException(nameof(path)); + } + if (handler == null) + { throw new ArgumentNullException(nameof(handler)); + } Method = method; Path = path; Handler = handler; - if (!String.IsNullOrEmpty(guid)) + if (!string.IsNullOrEmpty(guid)) + { GUID = guid; + } + if (metadata != null) + { Metadata = metadata; + } } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/DynamicRouteManager.cs b/EonaCat.Network/System/Web/DynamicRouteManager.cs index e91a5a0..e832c59 100644 --- a/EonaCat.Network/System/Web/DynamicRouteManager.cs +++ b/EonaCat.Network/System/Web/DynamicRouteManager.cs @@ -1,9 +1,9 @@ -using System; +using RegexMatcher; +using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; -using RegexMatcher; namespace EonaCat.Network { @@ -19,24 +19,17 @@ namespace EonaCat.Network /// Directly access the underlying regular expression matching library. /// This is helpful in case you want to specify the matching behavior should multiple matches exist. ///
- public Matcher Matcher - { - get - { - return _Matcher; - } - } + public Matcher Matcher { get; } = new Matcher(); - private Matcher _Matcher = new Matcher(); private readonly object _Lock = new object(); - private Dictionary> _Routes = new Dictionary>(); + private readonly Dictionary> _Routes = new Dictionary>(); /// /// Instantiate the object. /// public DynamicRouteManager() { - _Matcher.MatchPreference = MatchPreferenceType.LongestFirst; + Matcher.MatchPreference = MatchPreferenceType.LongestFirst; } /// @@ -50,15 +43,20 @@ namespace EonaCat.Network public void Add(HttpMethod method, Regex path, Func handler, string guid = null, object metadata = null) { if (path == null) + { throw new ArgumentNullException(nameof(path)); + } + if (handler == null) + { throw new ArgumentNullException(nameof(handler)); + } lock (_Lock) { DynamicRoute dr = new DynamicRoute(method, path, handler); - _Matcher.Add( + Matcher.Add( new Regex(BuildConsolidatedRegex(method, path)), dr); @@ -74,11 +72,13 @@ namespace EonaCat.Network public void Remove(HttpMethod method, Regex path) { if (path == null) + { throw new ArgumentNullException(nameof(path)); + } lock (_Lock) { - _Matcher.Remove( + Matcher.Remove( new Regex(BuildConsolidatedRegex(method, path))); if (_Routes.Any(r => r.Key.Method == method && r.Key.Path.Equals(path))) @@ -104,7 +104,9 @@ namespace EonaCat.Network public bool Exists(HttpMethod method, Regex path) { if (path == null) + { throw new ArgumentNullException(nameof(path)); + } lock (_Lock) { @@ -122,12 +124,14 @@ namespace EonaCat.Network public Func Match(HttpMethod method, string rawUrl, out DynamicRoute dr) { dr = null; - if (String.IsNullOrEmpty(rawUrl)) + if (string.IsNullOrEmpty(rawUrl)) + { throw new ArgumentNullException(nameof(rawUrl)); + } object val = null; - if (_Matcher.Match( + if (Matcher.Match( BuildConsolidatedRegex(method, rawUrl), out val)) { diff --git a/EonaCat.Network/System/Web/EonaCatWebserver.cs b/EonaCat.Network/System/Web/EonaCatWebserver.cs index e4fce9d..4ac97b2 100644 --- a/EonaCat.Network/System/Web/EonaCatWebserver.cs +++ b/EonaCat.Network/System/Web/EonaCatWebserver.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Net; using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -19,24 +18,12 @@ namespace EonaCat.Network /// /// Indicates whether or not the server is listening. /// - public bool IsListening - { - get - { - return (_HttpListener != null) ? _HttpListener.IsListening : false; - } - } + public bool IsListening => (_HttpListener != null) && _HttpListener.IsListening; /// /// Number of requests being serviced currently. /// - public int RequestCount - { - get - { - return _RequestCount; - } - } + public int RequestCount => _RequestCount; /// /// EonaCat webserver settings. @@ -50,9 +37,13 @@ namespace EonaCat.Network set { if (value == null) + { _Settings = new EonaCatWebserverSettings(); + } else + { _Settings = value; + } } } @@ -68,9 +59,13 @@ namespace EonaCat.Network set { if (value == null) + { _Routes = new EonaCatWebserverRoutes(); + } else + { _Routes = value; + } } } @@ -89,11 +84,11 @@ namespace EonaCat.Network /// public EonaCatWebserverPages Pages { get; private set; } = new EonaCatWebserverPages(); - private string _Header = "[EonaCat] "; - private Assembly _Assembly = Assembly.GetCallingAssembly(); + private readonly string _Header = "[EonaCat] "; + private readonly Assembly _Assembly = Assembly.GetCallingAssembly(); private EonaCatWebserverSettings _Settings = new EonaCatWebserverSettings(); private EonaCatWebserverRoutes _Routes = new EonaCatWebserverRoutes(); - private HttpListener _HttpListener = new HttpListener(); + private System.Net.HttpListener _HttpListener = new System.Net.HttpListener(); private int _RequestCount = 0; private CancellationTokenSource _TokenSource = new CancellationTokenSource(); @@ -128,10 +123,15 @@ namespace EonaCat.Network /// Method used when a request is received and no matching routes are found. Commonly used as the 404 handler when routes are used. public WebServer(string hostname, int port, bool ssl = false, Func defaultRoute = null) { - if (String.IsNullOrEmpty(hostname)) + if (string.IsNullOrEmpty(hostname)) + { hostname = "localhost"; + } + if (port < 1) + { throw new ArgumentOutOfRangeException(nameof(port)); + } _Settings = new EonaCatWebserverSettings(hostname, port, ssl); _Routes.Default = defaultRoute; @@ -147,9 +147,14 @@ namespace EonaCat.Network public WebServer(List hostnames, int port, bool ssl = false, Func defaultRoute = null) { if (hostnames == null || hostnames.Count < 1) + { hostnames = new List { "localhost" }; + } + if (port < 1) + { throw new ArgumentOutOfRangeException(nameof(port)); + } _Settings = new EonaCatWebserverSettings(hostnames, port, ssl); _Routes.Default = defaultRoute; @@ -172,9 +177,11 @@ namespace EonaCat.Network public void Start(CancellationToken token = default) { if (_HttpListener != null && _HttpListener.IsListening) + { throw new InvalidOperationException("EonaCat Webserver is already listening."); + } - _HttpListener = new HttpListener(); + _HttpListener = new System.Net.HttpListener(); LoadRoutes(); Statistics = new EonaCatWebserverStatistics(); @@ -199,9 +206,11 @@ namespace EonaCat.Network public Task StartAsync(CancellationToken token = default) { if (_HttpListener != null && _HttpListener.IsListening) + { throw new InvalidOperationException("EonaCat Webserver is already listening."); + } - _HttpListener = new HttpListener(); + _HttpListener = new System.Net.HttpListener(); LoadRoutes(); Statistics = new EonaCatWebserverStatistics(); @@ -225,7 +234,9 @@ namespace EonaCat.Network public void Stop() { if (!_HttpListener.IsListening) + { throw new InvalidOperationException("EonaCat Webserver is already stopped."); + } if (_HttpListener != null && _HttpListener.IsListening) { @@ -346,7 +357,7 @@ namespace EonaCat.Network } else { - object instance = Activator.CreateInstance(method.DeclaringType ?? throw new Exception("Declaring class is null")); + object instance = Activator.CreateInstance(method.DeclaringType ?? throw new Exception("EonaCat Network: Declaring class is null")); return (Func)Delegate.CreateDelegate(typeof(Func), instance, method); } } @@ -363,7 +374,7 @@ namespace EonaCat.Network continue; } - HttpListenerContext listenerCtx = await _HttpListener.GetContextAsync().ConfigureAwait(false); + System.Net.HttpListenerContext listenerCtx = await _HttpListener.GetContextAsync().ConfigureAwait(false); Interlocked.Increment(ref _RequestCount); HttpContext ctx = null; diff --git a/EonaCat.Network/System/Web/EonaCatWebserverEvents.cs b/EonaCat.Network/System/Web/EonaCatWebserverEvents.cs index 5b478d4..aa0d4c0 100644 --- a/EonaCat.Network/System/Web/EonaCatWebserverEvents.cs +++ b/EonaCat.Network/System/Web/EonaCatWebserverEvents.cs @@ -119,7 +119,9 @@ namespace EonaCat.Network private void WrappedEventHandler(Action action, string handler, object sender) { if (action == null) + { return; + } try { diff --git a/EonaCat.Network/System/Web/EonaCatWebserverPages.cs b/EonaCat.Network/System/Web/EonaCatWebserverPages.cs index c462516..4651d20 100644 --- a/EonaCat.Network/System/Web/EonaCatWebserverPages.cs +++ b/EonaCat.Network/System/Web/EonaCatWebserverPages.cs @@ -22,7 +22,10 @@ namespace EonaCat.Network set { if (value == null) + { throw new ArgumentNullException(nameof(Default404Page)); + } + _Default404Page = value; } } @@ -39,7 +42,10 @@ namespace EonaCat.Network set { if (value == null) + { throw new ArgumentNullException(nameof(Default500Page)); + } + _Default500Page = value; } } @@ -76,10 +82,15 @@ namespace EonaCat.Network /// Content. public Page(string contentType, string content) { - if (String.IsNullOrEmpty(contentType)) + if (string.IsNullOrEmpty(contentType)) + { throw new ArgumentNullException(nameof(contentType)); - if (String.IsNullOrEmpty(content)) + } + + if (string.IsNullOrEmpty(content)) + { throw new ArgumentNullException(nameof(content)); + } ContentType = contentType; Content = content; diff --git a/EonaCat.Network/System/Web/EonaCatWebserverRoutes.cs b/EonaCat.Network/System/Web/EonaCatWebserverRoutes.cs index 85af759..0ed7af9 100644 --- a/EonaCat.Network/System/Web/EonaCatWebserverRoutes.cs +++ b/EonaCat.Network/System/Web/EonaCatWebserverRoutes.cs @@ -26,9 +26,13 @@ namespace EonaCat.Network set { if (value == null) + { _Preflight = PreflightInternal; + } else + { _Preflight = value; + } } } @@ -51,7 +55,10 @@ namespace EonaCat.Network set { if (value == null) + { throw new ArgumentNullException(nameof(Content)); + } + _Content = value; } } @@ -68,7 +75,10 @@ namespace EonaCat.Network set { if (value == null) + { throw new ArgumentNullException(nameof(ContentHandler)); + } + _ContentHandler = value; } } @@ -85,7 +95,10 @@ namespace EonaCat.Network set { if (value == null) + { throw new ArgumentNullException(nameof(Static)); + } + _Static = value; } } @@ -102,7 +115,10 @@ namespace EonaCat.Network set { if (value == null) + { throw new ArgumentNullException(nameof(Parameter)); + } + _Parameter = value; } } @@ -119,7 +135,10 @@ namespace EonaCat.Network set { if (value == null) + { throw new ArgumentNullException(nameof(Dynamic)); + } + _Dynamic = value; } } @@ -127,26 +146,15 @@ namespace EonaCat.Network /// /// Default route; used when no other routes match. /// - public Func Default - { - get - { - return _Default; - } - set - { - _Default = value; - } - } + public Func Default { get; set; } = null; - private EonaCatWebserverSettings _Settings = new EonaCatWebserverSettings(); + private readonly EonaCatWebserverSettings _Settings = new EonaCatWebserverSettings(); private ContentRouteManager _Content = new ContentRouteManager(); private ContentRouteHandler _ContentHandler = null; private StaticRouteManager _Static = new StaticRouteManager(); private ParameterRouteManager _Parameter = new ParameterRouteManager(); private DynamicRouteManager _Dynamic = new DynamicRouteManager(); - private Func _Default = null; private Func _Preflight = null; /// @@ -163,14 +171,16 @@ namespace EonaCat.Network /// public EonaCatWebserverRoutes(EonaCatWebserverSettings settings, Func defaultRoute) { - if (settings == null) - settings = new EonaCatWebserverSettings(); + settings ??= new EonaCatWebserverSettings(); + if (defaultRoute == null) + { throw new ArgumentNullException(nameof(defaultRoute)); + } _Settings = settings; _Preflight = PreflightInternal; - _Default = defaultRoute; + Default = defaultRoute; _ContentHandler = new ContentRouteHandler(_Content); } @@ -183,11 +193,17 @@ namespace EonaCat.Network { foreach (KeyValuePair curr in ctx.Request.Headers) { - if (String.IsNullOrEmpty(curr.Key)) + if (string.IsNullOrEmpty(curr.Key)) + { continue; - if (String.IsNullOrEmpty(curr.Value)) + } + + if (string.IsNullOrEmpty(curr.Value)) + { continue; - if (String.Compare(curr.Key.ToLower(), "access-control-request-headers") == 0) + } + + if (string.Compare(curr.Key.ToLower(), "access-control-request-headers") == 0) { requestedHeaders = curr.Value.Split(','); break; @@ -202,10 +218,16 @@ namespace EonaCat.Network int addedCount = 0; foreach (string curr in requestedHeaders) { - if (String.IsNullOrEmpty(curr)) + if (string.IsNullOrEmpty(curr)) + { continue; + } + if (addedCount > 0) + { headers += ", "; + } + headers += ", " + curr; addedCount++; } diff --git a/EonaCat.Network/System/Web/EonaCatWebserverSettings.cs b/EonaCat.Network/System/Web/EonaCatWebserverSettings.cs index 7b58bec..15a9625 100644 --- a/EonaCat.Network/System/Web/EonaCatWebserverSettings.cs +++ b/EonaCat.Network/System/Web/EonaCatWebserverSettings.cs @@ -23,9 +23,15 @@ namespace EonaCat.Network set { if (value == null) + { throw new ArgumentNullException(nameof(Prefixes)); + } + if (value.Count < 1) + { throw new ArgumentException("At least one prefix must be specified."); + } + _Prefixes = value; } } @@ -42,7 +48,10 @@ namespace EonaCat.Network set { if (value == null) + { throw new ArgumentNullException(nameof(IO)); + } + _IO = value; } } @@ -59,7 +68,10 @@ namespace EonaCat.Network set { if (value == null) + { throw new ArgumentNullException(nameof(Ssl)); + } + _Ssl = value; } } @@ -76,7 +88,10 @@ namespace EonaCat.Network set { if (value == null) + { throw new ArgumentNullException(nameof(Headers)); + } + _Headers = value; } } @@ -93,7 +108,10 @@ namespace EonaCat.Network set { if (value == null) + { throw new ArgumentNullException(nameof(AccessControl)); + } + _AccessControl = value; } } @@ -111,7 +129,10 @@ namespace EonaCat.Network set { if (value == null) + { throw new ArgumentNullException(nameof(Debug)); + } + _Debug = value; } } @@ -148,16 +169,26 @@ namespace EonaCat.Network /// Enable or disable SSL. public EonaCatWebserverSettings(string hostname, int port, bool ssl = false) { - if (String.IsNullOrEmpty(hostname)) + if (string.IsNullOrEmpty(hostname)) + { hostname = "localhost"; + } + if (port < 0) + { throw new ArgumentOutOfRangeException(nameof(port)); + } string prefix = "http"; if (ssl) + { prefix += "s://" + hostname + ":" + port + "/"; + } else + { prefix += "://" + hostname + ":" + port + "/"; + } + _Prefixes.Add(prefix); _Ssl.Enable = ssl; } @@ -170,18 +201,25 @@ namespace EonaCat.Network /// Enable or disable SSL. public EonaCatWebserverSettings(List hostnames, int port, bool ssl = false) { - if (hostnames == null) - hostnames = new List { "localhost" }; + hostnames ??= new List { "localhost" }; + if (port < 0) + { throw new ArgumentOutOfRangeException(nameof(port)); + } foreach (string hostname in hostnames) { string prefix = "http"; if (ssl) + { prefix += "s://" + hostname + ":" + port + "/"; + } else + { prefix += "://" + hostname + ":" + port + "/"; + } + _Prefixes.Add(prefix); } @@ -205,7 +243,10 @@ namespace EonaCat.Network set { if (value < 1) + { throw new ArgumentOutOfRangeException(nameof(StreamBufferSize)); + } + _StreamBufferSize = value; } } @@ -222,7 +263,10 @@ namespace EonaCat.Network set { if (value < 1) + { throw new ArgumentException("Maximum requests must be greater than zero."); + } + _MaxRequests = value; } } diff --git a/EonaCat.Network/System/Web/EonaCatWebserverStatistics.cs b/EonaCat.Network/System/Web/EonaCatWebserverStatistics.cs index 4805ea7..3d9d1a5 100644 --- a/EonaCat.Network/System/Web/EonaCatWebserverStatistics.cs +++ b/EonaCat.Network/System/Web/EonaCatWebserverStatistics.cs @@ -15,24 +15,12 @@ namespace EonaCat.Network /// /// The time at which the client or server was started. /// - public DateTime StartTime - { - get - { - return _StartTime; - } - } + public DateTime StartTime { get; } = DateTime.Now.ToUniversalTime(); /// /// The amount of time which the client or server has been up. /// - public TimeSpan UpTime - { - get - { - return DateTime.Now.ToUniversalTime() - _StartTime; - } - } + public TimeSpan UpTime => DateTime.Now.ToUniversalTime() - StartTime; /// /// The number of payload bytes received (incoming request body). @@ -64,10 +52,9 @@ namespace EonaCat.Network } } - private DateTime _StartTime = DateTime.Now.ToUniversalTime(); private long _ReceivedPayloadBytes = 0; private long _SentPayloadBytes = 0; - private long[] _RequestsByMethod; // _RequestsByMethod[(int)HttpMethod.Xyz] = Count + private readonly long[] _RequestsByMethod; // _RequestsByMethod[(int)HttpMethod.Xyz] = Count /// /// Initialize the statistics object. @@ -79,7 +66,9 @@ namespace EonaCat.Network foreach (var value in Enum.GetValues(typeof(HttpMethod))) { if ((int)value > max) + { max = (int)value; + } } _RequestsByMethod = new long[max + 1]; @@ -111,7 +100,9 @@ namespace EonaCat.Network } if (!foundAtLeastOne) + { sb.AppendLine(" (none)"); + } return sb.ToString(); } @@ -125,7 +116,9 @@ namespace EonaCat.Network Interlocked.Exchange(ref _SentPayloadBytes, 0); for (int i = 0; i < _RequestsByMethod.Length; i++) + { Interlocked.Exchange(ref _RequestsByMethod[i], 0); + } } /// diff --git a/EonaCat.Network/System/Web/ConnectionReceivedEventArgs.cs b/EonaCat.Network/System/Web/EventArgs/ConnectionReceivedEventArgs.cs similarity index 91% rename from EonaCat.Network/System/Web/ConnectionReceivedEventArgs.cs rename to EonaCat.Network/System/Web/EventArgs/ConnectionReceivedEventArgs.cs index 5353321..b48f52c 100644 --- a/EonaCat.Network/System/Web/ConnectionReceivedEventArgs.cs +++ b/EonaCat.Network/System/Web/EventArgs/ConnectionReceivedEventArgs.cs @@ -27,10 +27,16 @@ namespace EonaCat.Network /// Request TCP port. public ConnectionEventArgs(string ip, int port) { - if (String.IsNullOrEmpty(ip)) + if (string.IsNullOrEmpty(ip)) + { throw new ArgumentNullException(nameof(ip)); + } + if (port < 0) + { throw new ArgumentOutOfRangeException(nameof(port)); + } + Ip = ip; Port = port; } diff --git a/EonaCat.Network/System/Web/ExceptionEventArgs.cs b/EonaCat.Network/System/Web/EventArgs/ExceptionEventArgs.cs similarity index 98% rename from EonaCat.Network/System/Web/ExceptionEventArgs.cs rename to EonaCat.Network/System/Web/EventArgs/ExceptionEventArgs.cs index 7468dc6..a88e051 100644 --- a/EonaCat.Network/System/Web/ExceptionEventArgs.cs +++ b/EonaCat.Network/System/Web/EventArgs/ExceptionEventArgs.cs @@ -74,7 +74,10 @@ namespace EonaCat.Network get { if (Exception != null) + { return Exception.ToJson(true); + } + return null; } } diff --git a/EonaCat.Network/System/Web/RequestEventArgs.cs b/EonaCat.Network/System/Web/EventArgs/RequestEventArgs.cs similarity index 100% rename from EonaCat.Network/System/Web/RequestEventArgs.cs rename to EonaCat.Network/System/Web/EventArgs/RequestEventArgs.cs diff --git a/EonaCat.Network/System/Web/ResponseEventArgs.cs b/EonaCat.Network/System/Web/EventArgs/ResponseEventArgs.cs similarity index 100% rename from EonaCat.Network/System/Web/ResponseEventArgs.cs rename to EonaCat.Network/System/Web/EventArgs/ResponseEventArgs.cs diff --git a/EonaCat.Network/System/Web/ObjectExtensions.cs b/EonaCat.Network/System/Web/Extensions/ObjectExtensions.cs similarity index 100% rename from EonaCat.Network/System/Web/ObjectExtensions.cs rename to EonaCat.Network/System/Web/Extensions/ObjectExtensions.cs diff --git a/EonaCat.Network/System/Web/SerializationHelper.cs b/EonaCat.Network/System/Web/Helpers/SerializationHelper.cs similarity index 90% rename from EonaCat.Network/System/Web/SerializationHelper.cs rename to EonaCat.Network/System/Web/Helpers/SerializationHelper.cs index 44baa42..24d50cc 100644 --- a/EonaCat.Network/System/Web/SerializationHelper.cs +++ b/EonaCat.Network/System/Web/Helpers/SerializationHelper.cs @@ -1,5 +1,5 @@ -using System; -using EonaCat.Json; +using EonaCat.Json; +using System; namespace EonaCat.Network { @@ -10,15 +10,21 @@ namespace EonaCat.Network { internal static T DeserializeJson(string json) { - if (String.IsNullOrEmpty(json)) + if (string.IsNullOrEmpty(json)) + { throw new ArgumentNullException(nameof(json)); + } + return JsonHelper.ToObject(json); } internal static string SerializeJson(object obj, bool pretty) { if (obj == null) + { return null; + } + string json; if (pretty) diff --git a/EonaCat.Network/System/Web/HttpContext.cs b/EonaCat.Network/System/Web/HttpContext.cs index e27dc54..0f35a50 100644 --- a/EonaCat.Network/System/Web/HttpContext.cs +++ b/EonaCat.Network/System/Web/HttpContext.cs @@ -1,6 +1,5 @@ -using System; -using System.Net; -using EonaCat.Json; +using EonaCat.Json; +using System; namespace EonaCat.Network { @@ -42,7 +41,7 @@ namespace EonaCat.Network [JsonProperty(Order = 999)] public object Metadata { get; set; } = null; - private HttpListenerContext _Context = null; + private readonly System.Net.HttpListenerContext _Context = null; /// /// Instantiate the object. @@ -51,12 +50,18 @@ namespace EonaCat.Network { } - internal HttpContext(HttpListenerContext ctx, EonaCatWebserverSettings settings, EonaCatWebserverEvents events) + internal HttpContext(System.Net.HttpListenerContext ctx, EonaCatWebserverSettings settings, EonaCatWebserverEvents events) { if (ctx == null) + { throw new ArgumentNullException(nameof(ctx)); + } + if (events == null) + { throw new ArgumentNullException(nameof(events)); + } + _Context = ctx; Request = new HttpRequest(ctx); Response = new HttpResponse(Request, ctx, settings, events); diff --git a/EonaCat.Network/System/Web/HttpMethod.cs b/EonaCat.Network/System/Web/HttpMethod.cs index 11866d3..acd2dab 100644 --- a/EonaCat.Network/System/Web/HttpMethod.cs +++ b/EonaCat.Network/System/Web/HttpMethod.cs @@ -1,5 +1,5 @@ -using System.Runtime.Serialization; -using EonaCat.Json.Converters; +using EonaCat.Json.Converters; +using System.Runtime.Serialization; namespace EonaCat.Network { @@ -9,7 +9,7 @@ namespace EonaCat.Network /// /// HTTP methods, i.e. GET, PUT, POST, DELETE, etc. /// - [EonaCat.Json.Converter(typeof(StringEnumConverter))] + [Json.Converter(typeof(StringEnumConverter))] public enum HttpMethod { /// diff --git a/EonaCat.Network/System/Web/HttpRequest.cs b/EonaCat.Network/System/Web/HttpRequest.cs index 2e67245..67c9ed5 100644 --- a/EonaCat.Network/System/Web/HttpRequest.cs +++ b/EonaCat.Network/System/Web/HttpRequest.cs @@ -1,4 +1,5 @@ -using System; +using EonaCat.Json; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -6,7 +7,6 @@ using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; -using EonaCat.Json; namespace EonaCat.Network { @@ -124,7 +124,10 @@ namespace EonaCat.Network get { if (_DataAsBytes != null) + { return _DataAsBytes; + } + if (Data != null && ContentLength > 0) { _DataAsBytes = ReadStreamFully(Data); @@ -143,12 +146,17 @@ namespace EonaCat.Network get { if (_DataAsBytes != null) + { return Encoding.UTF8.GetString(_DataAsBytes); + } + if (Data != null && ContentLength > 0) { _DataAsBytes = ReadStreamFully(Data); if (_DataAsBytes != null) + { return Encoding.UTF8.GetString(_DataAsBytes); + } } return null; } @@ -158,9 +166,9 @@ namespace EonaCat.Network /// The original HttpListenerContext from which the HttpRequest was constructed. /// [JsonIgnore] - public HttpListenerContext ListenerContext; + public System.Net.HttpListenerContext ListenerContext; - private Uri _Uri = null; + private readonly Uri _Uri = null; private byte[] _DataAsBytes = null; /// @@ -175,12 +183,17 @@ namespace EonaCat.Network /// Instantiate the object using an HttpListenerContext. /// /// HttpListenerContext. - public HttpRequest(HttpListenerContext ctx) + public HttpRequest(System.Net.HttpListenerContext ctx) { if (ctx == null) + { throw new ArgumentNullException(nameof(ctx)); + } + if (ctx.Request == null) + { throw new ArgumentNullException(nameof(ctx.Request)); + } ListenerContext = ctx; Keepalive = ctx.Request.KeepAlive; @@ -209,19 +222,32 @@ namespace EonaCat.Network foreach (KeyValuePair curr in Headers) { - if (String.IsNullOrEmpty(curr.Key)) + if (string.IsNullOrEmpty(curr.Key)) + { continue; - if (String.IsNullOrEmpty(curr.Value)) + } + + if (string.IsNullOrEmpty(curr.Value)) + { continue; + } if (curr.Key.ToLower().Equals("transfer-encoding")) { if (curr.Value.ToLower().Contains("chunked")) + { ChunkedTransfer = true; + } + if (curr.Value.ToLower().Contains("gzip")) + { Gzip = true; + } + if (curr.Value.ToLower().Contains("deflate")) + { Deflate = true; + } } else if (curr.Key.ToLower().Equals("x-amz-content-sha256")) { @@ -242,16 +268,24 @@ namespace EonaCat.Network /// public string RetrieveHeaderValue(string key) { - if (String.IsNullOrEmpty(key)) + if (string.IsNullOrEmpty(key)) + { throw new ArgumentNullException(nameof(key)); + } + if (Headers != null && Headers.Count > 0) { foreach (KeyValuePair curr in Headers) { - if (String.IsNullOrEmpty(curr.Key)) + if (string.IsNullOrEmpty(curr.Key)) + { continue; - if (String.Compare(curr.Key.ToLower(), key.ToLower()) == 0) + } + + if (string.Compare(curr.Key.ToLower(), key.ToLower()) == 0) + { return curr.Value; + } } } @@ -259,10 +293,15 @@ namespace EonaCat.Network { foreach (KeyValuePair curr in Query.Elements) { - if (String.IsNullOrEmpty(curr.Key)) + if (string.IsNullOrEmpty(curr.Key)) + { continue; - if (String.Compare(curr.Key.ToLower(), key.ToLower()) == 0) + } + + if (string.Compare(curr.Key.ToLower(), key.ToLower()) == 0) + { return curr.Value; + } } } @@ -277,8 +316,10 @@ namespace EonaCat.Network /// True if exists. public bool HeaderExists(string key, bool caseSensitive) { - if (String.IsNullOrEmpty(key)) + if (string.IsNullOrEmpty(key)) + { throw new ArgumentNullException(nameof(key)); + } if (Headers != null && Headers.Count > 0) { @@ -290,10 +331,15 @@ namespace EonaCat.Network { foreach (KeyValuePair header in Headers) { - if (String.IsNullOrEmpty(header.Key)) + if (string.IsNullOrEmpty(header.Key)) + { continue; + } + if (header.Key.ToLower().Trim().Equals(key)) + { return true; + } } } } @@ -309,8 +355,10 @@ namespace EonaCat.Network /// True if exists. public bool QuerystringExists(string key, bool caseSensitive) { - if (String.IsNullOrEmpty(key)) + if (string.IsNullOrEmpty(key)) + { throw new ArgumentNullException(nameof(key)); + } if (Query != null && Query.Elements != null && Query.Elements.Count > 0) { @@ -322,10 +370,15 @@ namespace EonaCat.Network { foreach (KeyValuePair queryElement in Query.Elements) { - if (String.IsNullOrEmpty(queryElement.Key)) + if (string.IsNullOrEmpty(queryElement.Key)) + { continue; + } + if (queryElement.Key.ToLower().Trim().Equals(key)) + { return true; + } } } } @@ -364,7 +417,9 @@ namespace EonaCat.Network string[] lenParts = lenStr.Split(new char[] { ';' }, 2); chunk.Length = int.Parse(lenParts[0], NumberStyles.HexNumber); if (lenParts.Length >= 2) + { chunk.Metadata = lenParts[1]; + } } else { @@ -404,7 +459,9 @@ namespace EonaCat.Network if (bytesRead > 0) { if (buffer[0] == 10) + { break; + } } } @@ -420,15 +477,20 @@ namespace EonaCat.Network public T DataAsJsonObject() where T : class { string json = DataAsString; - if (String.IsNullOrEmpty(json)) + if (string.IsNullOrEmpty(json)) + { return null; + } + return SerializationHelper.DeserializeJson(json); } private static Dictionary AddToDict(string key, string val, Dictionary existing) { - if (String.IsNullOrEmpty(key)) + if (string.IsNullOrEmpty(key)) + { return existing; + } Dictionary ret = new Dictionary(); @@ -441,8 +503,11 @@ namespace EonaCat.Network { if (existing.ContainsKey(key)) { - if (String.IsNullOrEmpty(val)) + if (string.IsNullOrEmpty(val)) + { return existing; + } + string tempVal = existing[key]; tempVal += "," + val; existing.Remove(key); @@ -460,7 +525,9 @@ namespace EonaCat.Network private byte[] AppendBytes(byte[] orig, byte[] append) { if (orig == null && append == null) + { return null; + } byte[] ret = null; @@ -487,9 +554,14 @@ namespace EonaCat.Network private byte[] StreamToBytes(Stream input) { if (input == null) + { throw new ArgumentNullException(nameof(input)); + } + if (!input.CanRead) + { throw new InvalidOperationException("Input stream is not readable"); + } byte[] buffer = new byte[16 * 1024]; using (MemoryStream ms = new MemoryStream()) @@ -508,9 +580,14 @@ namespace EonaCat.Network private void ReadStreamFully() { if (Data == null) + { return; + } + if (!Data.CanRead) + { return; + } if (_DataAsBytes == null) { @@ -524,9 +601,14 @@ namespace EonaCat.Network { Chunk chunk = ReadChunk().Result; if (chunk.Data != null && chunk.Data.Length > 0) + { _DataAsBytes = AppendBytes(_DataAsBytes, chunk.Data); + } + if (chunk.IsFinalChunk) + { break; + } } } } @@ -535,9 +617,14 @@ namespace EonaCat.Network private byte[] ReadStreamFully(Stream input) { if (input == null) + { throw new ArgumentNullException(nameof(input)); + } + if (!input.CanRead) + { throw new InvalidOperationException("Input stream is not readable"); + } byte[] buffer = new byte[16 * 1024]; using (MemoryStream ms = new MemoryStream()) @@ -583,10 +670,15 @@ namespace EonaCat.Network /// TCP port from which the request originated on the requestor. public SourceDetails(string ip, int port) { - if (String.IsNullOrEmpty(ip)) + if (string.IsNullOrEmpty(ip)) + { throw new ArgumentNullException(nameof(ip)); + } + if (port < 0) + { throw new ArgumentOutOfRangeException(nameof(port)); + } IpAddress = ip; Port = port; @@ -623,7 +715,7 @@ namespace EonaCat.Network string hostname = Hostname; string[] ret; - if (!String.IsNullOrEmpty(hostname)) + if (!string.IsNullOrEmpty(hostname)) { if (!IPAddress.TryParse(hostname, out _)) { @@ -658,12 +750,20 @@ namespace EonaCat.Network /// Hostname. public DestinationDetails(string ip, int port, string hostname) { - if (String.IsNullOrEmpty(ip)) + if (string.IsNullOrEmpty(ip)) + { throw new ArgumentNullException(nameof(ip)); + } + if (port < 0) + { throw new ArgumentOutOfRangeException(nameof(port)); - if (String.IsNullOrEmpty(hostname)) + } + + if (string.IsNullOrEmpty(hostname)) + { throw new ArgumentNullException(nameof(hostname)); + } IpAddress = ip; Port = port; @@ -693,12 +793,16 @@ namespace EonaCat.Network { get { - if (!String.IsNullOrEmpty(RawWithQuery)) + if (!string.IsNullOrEmpty(RawWithQuery)) { if (RawWithQuery.Contains("?")) + { return RawWithQuery.Substring(0, RawWithQuery.IndexOf("?")); + } else + { return RawWithQuery; + } } else { @@ -716,14 +820,23 @@ namespace EonaCat.Network { string rawUrl = RawWithoutQuery; - if (!String.IsNullOrEmpty(rawUrl)) + if (!string.IsNullOrEmpty(rawUrl)) { while (rawUrl.Contains("//")) + { rawUrl = rawUrl.Replace("//", "/"); + } + while (rawUrl.StartsWith("/")) + { rawUrl = rawUrl.Substring(1); + } + while (rawUrl.EndsWith("/")) + { rawUrl = rawUrl.Substring(0, rawUrl.Length - 1); + } + string[] encoded = rawUrl.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries); if (encoded != null && encoded.Length > 0) { @@ -761,10 +874,15 @@ namespace EonaCat.Network /// Raw URL. public UrlDetails(string fullUrl, string rawUrl) { - if (String.IsNullOrEmpty(fullUrl)) + if (string.IsNullOrEmpty(fullUrl)) + { throw new ArgumentNullException(nameof(fullUrl)); - if (String.IsNullOrEmpty(rawUrl)) + } + + if (string.IsNullOrEmpty(rawUrl)) + { throw new ArgumentNullException(nameof(rawUrl)); + } Full = fullUrl; RawWithQuery = rawUrl; @@ -803,7 +921,7 @@ namespace EonaCat.Network { Dictionary ret = new Dictionary(); string qs = Querystring; - if (!String.IsNullOrEmpty(qs)) + if (!string.IsNullOrEmpty(qs)) { string[] queries = qs.Split(new char[] { '&' }, StringSplitOptions.RemoveEmptyEntries); if (queries.Length > 0) @@ -840,13 +958,15 @@ namespace EonaCat.Network /// Full URL. public QueryDetails(string fullUrl) { - if (String.IsNullOrEmpty(fullUrl)) + if (string.IsNullOrEmpty(fullUrl)) + { throw new ArgumentNullException(nameof(fullUrl)); + } _FullUrl = fullUrl; } - private string _FullUrl = null; + private readonly string _FullUrl = null; } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/HttpResponse.cs b/EonaCat.Network/System/Web/HttpResponse.cs index a10fba1..f25d1c9 100644 --- a/EonaCat.Network/System/Web/HttpResponse.cs +++ b/EonaCat.Network/System/Web/HttpResponse.cs @@ -1,12 +1,11 @@ -using System; +using EonaCat.Json; +using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; -using EonaCat.Json; namespace EonaCat.Network { @@ -43,16 +42,20 @@ namespace EonaCat.Network set { if (value == null) + { _Headers = new Dictionary(); + } else + { _Headers = value; + } } } /// /// User-supplied content-type to include in the response. /// - public string ContentType = String.Empty; + public string ContentType = string.Empty; /// /// The length of the supplied response data. @@ -73,12 +76,17 @@ namespace EonaCat.Network get { if (_DataAsBytes != null) - return Encoding.UTF8.GetString(_DataAsBytes); - if (_Data != null && ContentLength > 0) { - _DataAsBytes = ReadStreamFully(_Data); + return Encoding.UTF8.GetString(_DataAsBytes); + } + + if (Data != null && ContentLength > 0) + { + _DataAsBytes = ReadStreamFully(Data); if (_DataAsBytes != null) + { return Encoding.UTF8.GetString(_DataAsBytes); + } } return null; } @@ -93,10 +101,13 @@ namespace EonaCat.Network get { if (_DataAsBytes != null) - return _DataAsBytes; - if (_Data != null && ContentLength > 0) { - _DataAsBytes = ReadStreamFully(_Data); + return _DataAsBytes; + } + + if (Data != null && ContentLength > 0) + { + _DataAsBytes = ReadStreamFully(Data); return _DataAsBytes; } return null; @@ -107,35 +118,20 @@ namespace EonaCat.Network /// Response data stream sent to the requestor. /// [JsonIgnore] - public MemoryStream Data - { - get - { - return _Data; - } - } + public MemoryStream Data { get; private set; } = null; - internal bool ResponseSent - { - get - { - return _ResponseSent; - } - } + internal bool ResponseSent { get; private set; } = false; - private HttpRequest _Request = null; - private HttpListenerContext _Context = null; - private HttpListenerResponse _Response = null; - private Stream _OutputStream = null; + private readonly HttpRequest _Request = null; + private readonly System.Net.HttpListenerContext _Context = null; + private readonly System.Net.HttpListenerResponse _Response = null; + private readonly Stream _OutputStream = null; private bool _HeadersSent = false; - private bool _ResponseSent = false; - - private EonaCatWebserverSettings _Settings = new EonaCatWebserverSettings(); - private EonaCatWebserverEvents _Events = new EonaCatWebserverEvents(); + private readonly EonaCatWebserverSettings _Settings = new EonaCatWebserverSettings(); + private readonly EonaCatWebserverEvents _Events = new EonaCatWebserverEvents(); private Dictionary _Headers = new Dictionary(); private byte[] _DataAsBytes = null; - private MemoryStream _Data = null; /// /// Instantiate the object. @@ -144,16 +140,27 @@ namespace EonaCat.Network { } - internal HttpResponse(HttpRequest req, HttpListenerContext ctx, EonaCatWebserverSettings settings, EonaCatWebserverEvents events) + internal HttpResponse(HttpRequest req, System.Net.HttpListenerContext ctx, EonaCatWebserverSettings settings, EonaCatWebserverEvents events) { if (req == null) + { throw new ArgumentNullException(nameof(req)); + } + if (ctx == null) + { throw new ArgumentNullException(nameof(ctx)); + } + if (settings == null) + { throw new ArgumentNullException(nameof(settings)); + } + if (events == null) + { throw new ArgumentNullException(nameof(events)); + } _Request = req; _Context = ctx; @@ -172,20 +179,26 @@ namespace EonaCat.Network public async Task Send(CancellationToken token = default) { if (ChunkedTransfer) + { throw new IOException("Response is configured to use chunked transfer-encoding. Use SendChunk() and SendFinalChunk()."); + } try { if (!_HeadersSent) + { SendHeaders(); + } await _OutputStream.FlushAsync(token).ConfigureAwait(false); _OutputStream.Close(); if (_Response != null) + { _Response.Close(); + } - _ResponseSent = true; + ResponseSent = true; return true; } catch (Exception) @@ -203,21 +216,28 @@ namespace EonaCat.Network public async Task Send(long contentLength, CancellationToken token = default) { if (ChunkedTransfer) + { throw new IOException("Response is configured to use chunked transfer-encoding. Use SendChunk() and SendFinalChunk()."); + } + ContentLength = contentLength; try { if (!_HeadersSent) + { SendHeaders(); + } await _OutputStream.FlushAsync(token).ConfigureAwait(false); _OutputStream.Close(); if (_Response != null) + { _Response.Close(); + } - _ResponseSent = true; + ResponseSent = true; return true; } catch (Exception) @@ -235,19 +255,24 @@ namespace EonaCat.Network public async Task Send(string data, CancellationToken token = default) { if (ChunkedTransfer) + { throw new IOException("Response is configured to use chunked transfer-encoding. Use SendChunk() and SendFinalChunk()."); + } + if (!_HeadersSent) + { SendHeaders(); + } byte[] bytes = null; - if (!String.IsNullOrEmpty(data)) + if (!string.IsNullOrEmpty(data)) { bytes = Encoding.UTF8.GetBytes(data); - _Data = new MemoryStream(); - await _Data.WriteAsync(bytes, 0, bytes.Length, token).ConfigureAwait(false); - _Data.Seek(0, SeekOrigin.Begin); + Data = new MemoryStream(); + await Data.WriteAsync(bytes, 0, bytes.Length, token).ConfigureAwait(false); + Data.Seek(0, SeekOrigin.Begin); _Response.ContentLength64 = bytes.Length; ContentLength = bytes.Length; @@ -271,9 +296,11 @@ namespace EonaCat.Network _OutputStream.Close(); if (_Response != null) + { _Response.Close(); + } - _ResponseSent = true; + ResponseSent = true; return true; } catch (Exception) @@ -291,15 +318,20 @@ namespace EonaCat.Network public async Task Send(byte[] data, CancellationToken token = default) { if (ChunkedTransfer) + { throw new IOException("Response is configured to use chunked transfer-encoding. Use SendChunk() and SendFinalChunk()."); + } + if (!_HeadersSent) + { SendHeaders(); + } if (data != null && data.Length > 0) { - _Data = new MemoryStream(); - await _Data.WriteAsync(data, 0, data.Length, token).ConfigureAwait(false); - _Data.Seek(0, SeekOrigin.Begin); + Data = new MemoryStream(); + await Data.WriteAsync(data, 0, data.Length, token).ConfigureAwait(false); + Data.Seek(0, SeekOrigin.Begin); _Response.ContentLength64 = data.Length; ContentLength = data.Length; @@ -323,9 +355,11 @@ namespace EonaCat.Network _OutputStream.Close(); if (_Response != null) + { _Response.Close(); + } - _ResponseSent = true; + ResponseSent = true; return true; } catch (Exception) @@ -344,10 +378,15 @@ namespace EonaCat.Network public async Task Send(long contentLength, Stream stream, CancellationToken token = default) { if (ChunkedTransfer) + { throw new IOException("Response is configured to use chunked transfer-encoding. Use SendChunk() and SendFinalChunk()."); + } + ContentLength = contentLength; if (!_HeadersSent) + { SendHeaders(); + } try { @@ -357,7 +396,7 @@ namespace EonaCat.Network { long bytesRemaining = contentLength; - _Data = new MemoryStream(); + Data = new MemoryStream(); while (bytesRemaining > 0) { @@ -366,7 +405,7 @@ namespace EonaCat.Network bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, token).ConfigureAwait(false); if (bytesRead > 0) { - await _Data.WriteAsync(buffer, 0, bytesRead, token).ConfigureAwait(false); + await Data.WriteAsync(buffer, 0, bytesRead, token).ConfigureAwait(false); await _OutputStream.WriteAsync(buffer, 0, bytesRead, token).ConfigureAwait(false); bytesRemaining -= bytesRead; } @@ -375,7 +414,7 @@ namespace EonaCat.Network stream.Close(); stream.Dispose(); - _Data.Seek(0, SeekOrigin.Begin); + Data.Seek(0, SeekOrigin.Begin); } } @@ -383,9 +422,11 @@ namespace EonaCat.Network _OutputStream.Close(); if (_Response != null) + { _Response.Close(); + } - _ResponseSent = true; + ResponseSent = true; return true; } catch (Exception) @@ -404,17 +445,27 @@ namespace EonaCat.Network public async Task SendChunk(byte[] chunk, int numBytes, CancellationToken token = default) { if (!ChunkedTransfer) + { throw new IOException("Response is not configured to use chunked transfer-encoding. Set ChunkedTransfer to true first, otherwise use Send()."); + } + if (!_HeadersSent) + { SendHeaders(); + } if (chunk != null && chunk.Length > 0) + { ContentLength += chunk.Length; + } try { if (chunk == null || chunk.Length < 1) + { chunk = new byte[0]; + } + await _OutputStream.WriteAsync(chunk, 0, numBytes, token).ConfigureAwait(false); await _OutputStream.FlushAsync(token).ConfigureAwait(false); } @@ -435,17 +486,26 @@ namespace EonaCat.Network public async Task SendFinalChunk(byte[] chunk, int numBytes, CancellationToken token = default) { if (!ChunkedTransfer) + { throw new IOException("Response is not configured to use chunked transfer-encoding. Set ChunkedTransfer to true first, otherwise use Send()."); + } + if (!_HeadersSent) + { SendHeaders(); + } if (chunk != null && chunk.Length > 0) + { ContentLength += chunk.Length; + } try { if (chunk != null && chunk.Length > 0) + { await _OutputStream.WriteAsync(chunk, 0, numBytes, token).ConfigureAwait(false); + } byte[] endChunk = new byte[0]; await _OutputStream.WriteAsync(endChunk, 0, endChunk.Length, token).ConfigureAwait(false); @@ -454,9 +514,11 @@ namespace EonaCat.Network _OutputStream.Close(); if (_Response != null) + { _Response.Close(); + } - _ResponseSent = true; + ResponseSent = true; return true; } catch (Exception) @@ -473,15 +535,20 @@ namespace EonaCat.Network public T DataAsJsonObject() where T : class { string json = DataAsString; - if (String.IsNullOrEmpty(json)) + if (string.IsNullOrEmpty(json)) + { return null; + } + return SerializationHelper.DeserializeJson(json); } private void SendHeaders() { if (_HeadersSent) + { throw new IOException("Headers already sent."); + } _Response.ContentLength64 = ContentLength; _Response.StatusCode = StatusCode; @@ -493,8 +560,11 @@ namespace EonaCat.Network { foreach (KeyValuePair header in Headers) { - if (String.IsNullOrEmpty(header.Key)) + if (string.IsNullOrEmpty(header.Key)) + { continue; + } + _Response.AddHeader(header.Key, header.Value); } } @@ -514,7 +584,7 @@ namespace EonaCat.Network { try { - _Response.Headers.Remove(HttpResponseHeader.Server); + _Response.Headers.Remove(System.Net.HttpResponseHeader.Server); _Response.AddHeader("Server", "EonaCat Server"); } catch (Exception) @@ -606,9 +676,14 @@ namespace EonaCat.Network private byte[] ReadStreamFully(Stream input) { if (input == null) + { throw new ArgumentNullException(nameof(input)); + } + if (!input.CanRead) + { throw new InvalidOperationException("Input stream is not readable"); + } byte[] buffer = new byte[16 * 1024]; using (MemoryStream ms = new MemoryStream()) diff --git a/EonaCat.Network/System/Web/MimeTypes.cs b/EonaCat.Network/System/Web/MimeTypes.cs index fe9c3cc..fe061e9 100644 --- a/EonaCat.Network/System/Web/MimeTypes.cs +++ b/EonaCat.Network/System/Web/MimeTypes.cs @@ -603,8 +603,10 @@ namespace EonaCat.Network /// String containing MIME type. public static string GetFromExtension(string extension) { - if (String.IsNullOrEmpty(nameof(extension))) + if (string.IsNullOrEmpty(nameof(extension))) + { return null; + } if (!extension.StartsWith(".")) { diff --git a/EonaCat.Network/System/Web/ParameterRoute.cs b/EonaCat.Network/System/Web/ParameterRoute.cs index ddd2bf0..ded0cb7 100644 --- a/EonaCat.Network/System/Web/ParameterRoute.cs +++ b/EonaCat.Network/System/Web/ParameterRoute.cs @@ -1,6 +1,6 @@ -using System; +using EonaCat.Json; +using System; using System.Threading.Tasks; -using EonaCat.Json; namespace EonaCat.Network { @@ -52,19 +52,29 @@ namespace EonaCat.Network /// User-supplied metadata. public ParameterRoute(HttpMethod method, string path, Func handler, string guid = null, object metadata = null) { - if (String.IsNullOrEmpty(path)) + if (string.IsNullOrEmpty(path)) + { throw new ArgumentNullException(nameof(path)); + } + if (handler == null) + { throw new ArgumentNullException(nameof(handler)); + } Method = method; Path = path; Handler = handler; - if (!String.IsNullOrEmpty(guid)) + if (!string.IsNullOrEmpty(guid)) + { GUID = guid; + } + if (metadata != null) + { Metadata = metadata; + } } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/ParameterRouteManager.cs b/EonaCat.Network/System/Web/ParameterRouteManager.cs index fe63d69..6a6db5a 100644 --- a/EonaCat.Network/System/Web/ParameterRouteManager.cs +++ b/EonaCat.Network/System/Web/ParameterRouteManager.cs @@ -1,8 +1,8 @@ -using System; +using EonaCat.UrlMatch; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using EonaCat.UrlMatch; namespace EonaCat.Network { @@ -20,17 +20,10 @@ namespace EonaCat.Network /// Directly access the underlying URL matching library. /// This is helpful in case you want to specify the matching behavior should multiple matches exist. /// - public Matcher Matcher - { - get - { - return _Matcher; - } - } + public Matcher Matcher { get; } = new Matcher(); - private Matcher _Matcher = new Matcher(); private readonly object _Lock = new object(); - private Dictionary> _Routes = new Dictionary>(); + private readonly Dictionary> _Routes = new Dictionary>(); /// /// Instantiate the object. @@ -49,10 +42,15 @@ namespace EonaCat.Network /// User-supplied metadata. public void Add(HttpMethod method, string path, Func handler, string guid = null, object metadata = null) { - if (String.IsNullOrEmpty(path)) + if (string.IsNullOrEmpty(path)) + { throw new ArgumentNullException(nameof(path)); + } + if (handler == null) + { throw new ArgumentNullException(nameof(handler)); + } lock (_Lock) { @@ -68,8 +66,10 @@ namespace EonaCat.Network /// URL path. public void Remove(HttpMethod method, string path) { - if (String.IsNullOrEmpty(path)) + if (string.IsNullOrEmpty(path)) + { throw new ArgumentNullException(nameof(path)); + } lock (_Lock) { @@ -95,8 +95,10 @@ namespace EonaCat.Network /// ParameterRoute if the route exists, otherwise null. public ParameterRoute Get(HttpMethod method, string path) { - if (String.IsNullOrEmpty(path)) + if (string.IsNullOrEmpty(path)) + { throw new ArgumentNullException(nameof(path)); + } lock (_Lock) { @@ -117,8 +119,10 @@ namespace EonaCat.Network /// True if exists. public bool Exists(HttpMethod method, string path) { - if (String.IsNullOrEmpty(path)) + if (string.IsNullOrEmpty(path)) + { throw new ArgumentNullException(nameof(path)); + } lock (_Lock) { @@ -138,8 +142,10 @@ namespace EonaCat.Network { pr = null; vals = null; - if (String.IsNullOrEmpty(path)) + if (string.IsNullOrEmpty(path)) + { throw new ArgumentNullException(nameof(path)); + } string consolidatedPath = BuildConsolidatedPath(method, path); @@ -147,7 +153,7 @@ namespace EonaCat.Network { foreach (KeyValuePair> route in _Routes) { - if (_Matcher.Match( + if (Matcher.Match( consolidatedPath, BuildConsolidatedPath(route.Key.Method, route.Key.Path), out vals)) diff --git a/EonaCat.Network/System/Web/RouteTypeEnum.cs b/EonaCat.Network/System/Web/RouteTypeEnum.cs index 1701369..79c52e5 100644 --- a/EonaCat.Network/System/Web/RouteTypeEnum.cs +++ b/EonaCat.Network/System/Web/RouteTypeEnum.cs @@ -1,5 +1,5 @@ -using System.Runtime.Serialization; -using EonaCat.Json.Converters; +using EonaCat.Json.Converters; +using System.Runtime.Serialization; namespace EonaCat.Network { @@ -9,7 +9,7 @@ namespace EonaCat.Network /// /// Route type. /// - [EonaCat.Json.Converter(typeof(StringEnumConverter))] + [Json.Converter(typeof(StringEnumConverter))] public enum RouteTypeEnum { /// diff --git a/EonaCat.Network/System/Web/StaticRoute.cs b/EonaCat.Network/System/Web/StaticRoute.cs index af9edd2..aed6c3f 100644 --- a/EonaCat.Network/System/Web/StaticRoute.cs +++ b/EonaCat.Network/System/Web/StaticRoute.cs @@ -1,6 +1,6 @@ -using System; +using EonaCat.Json; +using System; using System.Threading.Tasks; -using EonaCat.Json; namespace EonaCat.Network { @@ -52,25 +52,40 @@ namespace EonaCat.Network /// User-supplied metadata. public StaticRoute(HttpMethod method, string path, Func handler, string guid = null, object metadata = null) { - if (String.IsNullOrEmpty(path)) + if (string.IsNullOrEmpty(path)) + { throw new ArgumentNullException(nameof(path)); + } + if (handler == null) + { throw new ArgumentNullException(nameof(handler)); + } Method = method; Path = path.ToLower(); if (!Path.StartsWith("/")) + { Path = "/" + Path; + } + if (!Path.EndsWith("/")) + { Path = Path + "/"; + } Handler = handler; - if (!String.IsNullOrEmpty(guid)) + if (!string.IsNullOrEmpty(guid)) + { GUID = guid; + } + if (metadata != null) + { Metadata = metadata; + } } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/StaticRouteManager.cs b/EonaCat.Network/System/Web/StaticRouteManager.cs index 883bee7..86de9b3 100644 --- a/EonaCat.Network/System/Web/StaticRouteManager.cs +++ b/EonaCat.Network/System/Web/StaticRouteManager.cs @@ -13,7 +13,7 @@ namespace EonaCat.Network /// public class StaticRouteManager { - private List _Routes = new List(); + private readonly List _Routes = new List(); private readonly object _Lock = new object(); /// @@ -33,10 +33,15 @@ namespace EonaCat.Network /// User-supplied metadata. public void Add(HttpMethod method, string path, Func handler, string guid = null, object metadata = null) { - if (String.IsNullOrEmpty(path)) + if (string.IsNullOrEmpty(path)) + { throw new ArgumentNullException(nameof(path)); + } + if (handler == null) + { throw new ArgumentNullException(nameof(handler)); + } StaticRoute r = new StaticRoute(method, path, handler, guid, metadata); Add(r); @@ -49,8 +54,10 @@ namespace EonaCat.Network /// URL path. public void Remove(HttpMethod method, string path) { - if (String.IsNullOrEmpty(path)) + if (string.IsNullOrEmpty(path)) + { throw new ArgumentNullException(nameof(path)); + } StaticRoute r = Get(method, path); if (r == null || r == default(StaticRoute)) @@ -76,14 +83,21 @@ namespace EonaCat.Network /// StaticRoute if the route exists, otherwise null. public StaticRoute Get(HttpMethod method, string path) { - if (String.IsNullOrEmpty(path)) + if (string.IsNullOrEmpty(path)) + { throw new ArgumentNullException(nameof(path)); + } path = path.ToLower(); if (!path.StartsWith("/")) + { path = "/" + path; + } + if (!path.EndsWith("/")) + { path = path + "/"; + } lock (_Lock) { @@ -107,14 +121,21 @@ namespace EonaCat.Network /// True if exists. public bool Exists(HttpMethod method, string path) { - if (String.IsNullOrEmpty(path)) + if (string.IsNullOrEmpty(path)) + { throw new ArgumentNullException(nameof(path)); + } path = path.ToLower(); if (!path.StartsWith("/")) + { path = "/" + path; + } + if (!path.EndsWith("/")) + { path = path + "/"; + } lock (_Lock) { @@ -138,14 +159,21 @@ namespace EonaCat.Network public Func Match(HttpMethod method, string path, out StaticRoute route) { route = null; - if (String.IsNullOrEmpty(path)) + if (string.IsNullOrEmpty(path)) + { throw new ArgumentNullException(nameof(path)); + } path = path.ToLower(); if (!path.StartsWith("/")) + { path = "/" + path; + } + if (!path.EndsWith("/")) + { path = path + "/"; + } lock (_Lock) { @@ -165,13 +193,20 @@ namespace EonaCat.Network private void Add(StaticRoute route) { if (route == null) + { throw new ArgumentNullException(nameof(route)); + } route.Path = route.Path.ToLower(); if (!route.Path.StartsWith("/")) + { route.Path = "/" + route.Path; + } + if (!route.Path.EndsWith("/")) + { route.Path = route.Path + "/"; + } if (Exists(route.Method, route.Path)) { @@ -187,7 +222,9 @@ namespace EonaCat.Network private void Remove(StaticRoute route) { if (route == null) + { throw new ArgumentNullException(nameof(route)); + } lock (_Lock) {