From d8b820ad7fbf6737bd7d4081b3ff5bc30b13ede2 Mon Sep 17 00:00:00 2001 From: EonaCat Date: Wed, 11 Jan 2023 10:33:32 +0100 Subject: [PATCH] Initial version --- .gitignore | 76 +- EonaCat.Network.sln | 29 + EonaCat.Network/EonaCat.Network.csproj | 56 + EonaCat.Network/EonaCat.Network.sln | 24 + EonaCat.Network/NetworkHelper.cs | 356 ++++ .../System/Quic/Connections/ConnectionPool.cs | 67 + .../Quic/Connections/ConnectionState.cs | 13 + .../System/Quic/Connections/QuicConnection.cs | 300 +++ .../System/Quic/Constants/ErrorConstants.cs | 13 + .../System/Quic/Context/QuicStreamContext.cs | 74 + .../System/Quic/Events/DelegateDefinitions.cs | 16 + .../System/Quic/Events/QuicEventArgs.cs | 19 + .../Quic/Exceptions/ConnectionException.cs | 14 + .../Exceptions/ServerNotStartedException.cs | 17 + .../System/Quic/Exceptions/StreamException.cs | 17 + .../System/Quic/Helpers/ByteArray.cs | 99 + .../System/Quic/Helpers/ByteHelpers.cs | 82 + .../System/Quic/Helpers/IntegerParts.cs | 93 + .../System/Quic/Helpers/StreamId.cs | 71 + .../System/Quic/Helpers/VariableInteger.cs | 99 + .../System/Quic/Infrastructure/ErrorCodes.cs | 28 + .../Exceptions/ProtocolException.cs | 18 + .../System/Quic/Infrastructure/FrameParser.cs | 155 ++ .../Quic/Infrastructure/Frames/AckFrame.cs | 23 + .../Frames/ConnectionCloseFrame.cs | 83 + .../Quic/Infrastructure/Frames/CryptoFrame.cs | 22 + .../Infrastructure/Frames/DataBlockedFrame.cs | 38 + .../Quic/Infrastructure/Frames/Frame.cs | 19 + .../Infrastructure/Frames/MaxDataFrame.cs | 31 + .../Frames/MaxStreamDataFrame.cs | 46 + .../Infrastructure/Frames/MaxStreamsFrame.cs | 39 + .../Frames/NewConnectionIdFrame.cs | 23 + .../Infrastructure/Frames/NewTokenFrame.cs | 23 + .../Infrastructure/Frames/PaddingFrame.cs | 26 + .../Frames/PathChallengeFrame.cs | 23 + .../Frames/PathResponseFrame.cs | 23 + .../Quic/Infrastructure/Frames/PingFrame.cs | 26 + .../Infrastructure/Frames/ResetStreamFrame.cs | 36 + .../Frames/RetireConnectionIdFrame.cs | 23 + .../Infrastructure/Frames/StopSendingFrame.cs | 23 + .../Frames/StreamDataBlockedFrame.cs | 44 + .../Quic/Infrastructure/Frames/StreamFrame.cs | 82 + .../Frames/StreamsBlockedFrame.cs | 23 + .../System/Quic/Infrastructure/NumberSpace.cs | 36 + .../PacketProcessing/InitialPacketCreator.cs | 35 + .../PacketProcessing/PacketCreator.cs | 55 + .../System/Quic/Infrastructure/PacketType.cs | 15 + .../Infrastructure/Packets/InitialPacket.cs | 91 + .../Packets/LongHeaderPacket.cs | 90 + .../Quic/Infrastructure/Packets/Packet.cs | 66 + .../Packets/ShortHeaderPacket.cs | 52 + .../Quic/Infrastructure/Packets/Unpacker.cs | 52 + .../Packets/VersionNegotiationPacket.cs | 22 + .../Quic/Infrastructure/QuicPacketType.cs | 14 + .../Infrastructure/Settings/QuicSettings.cs | 55 + .../Infrastructure/Settings/QuicVersion.cs | 15 + .../InternalInfrastructure/ConnectionData.cs | 21 + .../PacketWireTransfer.cs | 52 + EonaCat.Network/System/Quic/QuicClient.cs | 116 ++ EonaCat.Network/System/Quic/QuicServer.cs | 154 ++ EonaCat.Network/System/Quic/QuicTransport.cs | 28 + .../System/Quic/Streams/QuicStream.cs | 203 ++ .../System/Quic/Streams/StreamState.cs | 14 + EonaCat.Network/System/Tcp/TcpClient.cs | 132 ++ .../System/Tcp/TcpConnectedPeer.cs | 138 ++ EonaCat.Network/System/Tcp/TcpServer.cs | 210 ++ EonaCat.Network/System/Tools/FileReader.cs | 83 + EonaCat.Network/System/Tools/FileWriter.cs | 57 + EonaCat.Network/System/Tools/Helpers.cs | 175 ++ EonaCat.Network/System/Tools/RegEx.cs | 106 + EonaCat.Network/System/Tools/Token.cs | 97 + EonaCat.Network/System/Udp/Udp.cs | 156 ++ .../System/Web/AccessControlManager.cs | 67 + .../System/Web/AccessControlMode.cs | 27 + EonaCat.Network/System/Web/Chunk.cs | 35 + .../System/Web/ConnectionReceivedEventArgs.cs | 38 + EonaCat.Network/System/Web/ContentRoute.cs | 58 + .../System/Web/ContentRouteManager.cs | 231 +++ .../System/Web/ContentRouteProcessor.cs | 142 ++ EonaCat.Network/System/Web/DynamicRoute.cs | 71 + .../System/Web/DynamicRouteAttribute.cs | 53 + .../System/Web/DynamicRouteManager.cs | 163 ++ .../System/Web/EonaCatWebserver.cs | 595 ++++++ .../System/Web/EonaCatWebserver.xml | 1811 +++++++++++++++++ .../System/Web/EonaCatWebserverEvents.cs | 134 ++ .../System/Web/EonaCatWebserverPages.cs | 89 + .../System/Web/EonaCatWebserverRoutes.cs | 223 ++ .../System/Web/EonaCatWebserverSettings.cs | 362 ++++ .../System/Web/EonaCatWebserverStatistics.cs | 156 ++ .../System/Web/ExceptionEventArgs.cs | 100 + EonaCat.Network/System/Web/HttpContext.cs | 65 + EonaCat.Network/System/Web/HttpMethod.cs | 69 + EonaCat.Network/System/Web/HttpRequest.cs | 852 ++++++++ EonaCat.Network/System/Web/HttpResponse.cs | 628 ++++++ EonaCat.Network/System/Web/MimeTypes.cs | 618 ++++++ .../System/Web/ObjectExtensions.cs | 47 + EonaCat.Network/System/Web/ParameterRoute.cs | 70 + .../System/Web/ParameterRouteAttribute.cs | 53 + .../System/Web/ParameterRouteManager.cs | 169 ++ .../System/Web/RequestEventArgs.cs | 60 + .../System/Web/ResponseEventArgs.cs | 83 + EonaCat.Network/System/Web/RouteTypeEnum.cs | 45 + .../System/Web/SerializationHelper.cs | 47 + EonaCat.Network/System/Web/StaticRoute.cs | 76 + .../System/Web/StaticRouteAttribute.cs | 52 + .../System/Web/StaticRouteManager.cs | 200 ++ EonaCat.Network/icon.png | Bin 0 -> 89562 bytes LICENSE | 193 +- README.md | 6 +- icon.png | Bin 0 -> 89562 bytes 110 files changed, 12341 insertions(+), 99 deletions(-) create mode 100644 EonaCat.Network.sln create mode 100644 EonaCat.Network/EonaCat.Network.csproj create mode 100644 EonaCat.Network/EonaCat.Network.sln create mode 100644 EonaCat.Network/NetworkHelper.cs create mode 100644 EonaCat.Network/System/Quic/Connections/ConnectionPool.cs create mode 100644 EonaCat.Network/System/Quic/Connections/ConnectionState.cs create mode 100644 EonaCat.Network/System/Quic/Connections/QuicConnection.cs create mode 100644 EonaCat.Network/System/Quic/Constants/ErrorConstants.cs create mode 100644 EonaCat.Network/System/Quic/Context/QuicStreamContext.cs create mode 100644 EonaCat.Network/System/Quic/Events/DelegateDefinitions.cs create mode 100644 EonaCat.Network/System/Quic/Events/QuicEventArgs.cs create mode 100644 EonaCat.Network/System/Quic/Exceptions/ConnectionException.cs create mode 100644 EonaCat.Network/System/Quic/Exceptions/ServerNotStartedException.cs create mode 100644 EonaCat.Network/System/Quic/Exceptions/StreamException.cs create mode 100644 EonaCat.Network/System/Quic/Helpers/ByteArray.cs create mode 100644 EonaCat.Network/System/Quic/Helpers/ByteHelpers.cs create mode 100644 EonaCat.Network/System/Quic/Helpers/IntegerParts.cs create mode 100644 EonaCat.Network/System/Quic/Helpers/StreamId.cs create mode 100644 EonaCat.Network/System/Quic/Helpers/VariableInteger.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/ErrorCodes.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/Exceptions/ProtocolException.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/FrameParser.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/Frames/AckFrame.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/Frames/ConnectionCloseFrame.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/Frames/CryptoFrame.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/Frames/DataBlockedFrame.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/Frames/Frame.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/Frames/MaxDataFrame.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/Frames/MaxStreamDataFrame.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/Frames/MaxStreamsFrame.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/Frames/NewConnectionIdFrame.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/Frames/NewTokenFrame.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/Frames/PaddingFrame.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/Frames/PathChallengeFrame.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/Frames/PathResponseFrame.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/Frames/PingFrame.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/Frames/ResetStreamFrame.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/Frames/RetireConnectionIdFrame.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/Frames/StopSendingFrame.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/Frames/StreamDataBlockedFrame.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/Frames/StreamFrame.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/Frames/StreamsBlockedFrame.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/NumberSpace.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/PacketProcessing/InitialPacketCreator.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/PacketProcessing/PacketCreator.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/PacketType.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/Packets/InitialPacket.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/Packets/LongHeaderPacket.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/Packets/Packet.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/Packets/ShortHeaderPacket.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/Packets/Unpacker.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/Packets/VersionNegotiationPacket.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/QuicPacketType.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/Settings/QuicSettings.cs create mode 100644 EonaCat.Network/System/Quic/Infrastructure/Settings/QuicVersion.cs create mode 100644 EonaCat.Network/System/Quic/InternalInfrastructure/ConnectionData.cs create mode 100644 EonaCat.Network/System/Quic/InternalInfrastructure/PacketWireTransfer.cs create mode 100644 EonaCat.Network/System/Quic/QuicClient.cs create mode 100644 EonaCat.Network/System/Quic/QuicServer.cs create mode 100644 EonaCat.Network/System/Quic/QuicTransport.cs create mode 100644 EonaCat.Network/System/Quic/Streams/QuicStream.cs create mode 100644 EonaCat.Network/System/Quic/Streams/StreamState.cs create mode 100644 EonaCat.Network/System/Tcp/TcpClient.cs create mode 100644 EonaCat.Network/System/Tcp/TcpConnectedPeer.cs create mode 100644 EonaCat.Network/System/Tcp/TcpServer.cs create mode 100644 EonaCat.Network/System/Tools/FileReader.cs create mode 100644 EonaCat.Network/System/Tools/FileWriter.cs create mode 100644 EonaCat.Network/System/Tools/Helpers.cs create mode 100644 EonaCat.Network/System/Tools/RegEx.cs create mode 100644 EonaCat.Network/System/Tools/Token.cs create mode 100644 EonaCat.Network/System/Udp/Udp.cs create mode 100644 EonaCat.Network/System/Web/AccessControlManager.cs create mode 100644 EonaCat.Network/System/Web/AccessControlMode.cs create mode 100644 EonaCat.Network/System/Web/Chunk.cs create mode 100644 EonaCat.Network/System/Web/ConnectionReceivedEventArgs.cs create mode 100644 EonaCat.Network/System/Web/ContentRoute.cs create mode 100644 EonaCat.Network/System/Web/ContentRouteManager.cs create mode 100644 EonaCat.Network/System/Web/ContentRouteProcessor.cs create mode 100644 EonaCat.Network/System/Web/DynamicRoute.cs create mode 100644 EonaCat.Network/System/Web/DynamicRouteAttribute.cs create mode 100644 EonaCat.Network/System/Web/DynamicRouteManager.cs create mode 100644 EonaCat.Network/System/Web/EonaCatWebserver.cs create mode 100644 EonaCat.Network/System/Web/EonaCatWebserver.xml create mode 100644 EonaCat.Network/System/Web/EonaCatWebserverEvents.cs create mode 100644 EonaCat.Network/System/Web/EonaCatWebserverPages.cs create mode 100644 EonaCat.Network/System/Web/EonaCatWebserverRoutes.cs create mode 100644 EonaCat.Network/System/Web/EonaCatWebserverSettings.cs create mode 100644 EonaCat.Network/System/Web/EonaCatWebserverStatistics.cs create mode 100644 EonaCat.Network/System/Web/ExceptionEventArgs.cs create mode 100644 EonaCat.Network/System/Web/HttpContext.cs create mode 100644 EonaCat.Network/System/Web/HttpMethod.cs create mode 100644 EonaCat.Network/System/Web/HttpRequest.cs create mode 100644 EonaCat.Network/System/Web/HttpResponse.cs create mode 100644 EonaCat.Network/System/Web/MimeTypes.cs create mode 100644 EonaCat.Network/System/Web/ObjectExtensions.cs create mode 100644 EonaCat.Network/System/Web/ParameterRoute.cs create mode 100644 EonaCat.Network/System/Web/ParameterRouteAttribute.cs create mode 100644 EonaCat.Network/System/Web/ParameterRouteManager.cs create mode 100644 EonaCat.Network/System/Web/RequestEventArgs.cs create mode 100644 EonaCat.Network/System/Web/ResponseEventArgs.cs create mode 100644 EonaCat.Network/System/Web/RouteTypeEnum.cs create mode 100644 EonaCat.Network/System/Web/SerializationHelper.cs create mode 100644 EonaCat.Network/System/Web/StaticRoute.cs create mode 100644 EonaCat.Network/System/Web/StaticRouteAttribute.cs create mode 100644 EonaCat.Network/System/Web/StaticRouteManager.cs create mode 100644 EonaCat.Network/icon.png create mode 100644 icon.png diff --git a/.gitignore b/.gitignore index 1998960..e3ba347 100644 --- a/.gitignore +++ b/.gitignore @@ -14,9 +14,6 @@ # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs -# Mono auto generated files -mono_crash.* - # Build results [Dd]ebug/ [Dd]ebugPublic/ @@ -24,14 +21,10 @@ mono_crash.* [Rr]eleases/ x64/ x86/ -[Ww][Ii][Nn]32/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ -[Ll]ogs/ # Visual Studio 2015/2017 cache/options directory .vs/ @@ -45,10 +38,9 @@ Generated\ Files/ [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* -# NUnit +# NUNIT *.VisualState.xml TestResult.xml -nunit-*.xml # Build Results of an ATL Project [Dd]ebugPS/ @@ -63,9 +55,6 @@ project.lock.json project.fragment.lock.json artifacts/ -# ASP.NET Scaffolding -ScaffoldingReadMe.txt - # StyleCop StyleCopReport.xml @@ -91,7 +80,6 @@ StyleCopReport.xml *.tmp_proj *_wpftmp.csproj *.log -*.tlog *.vspscc *.vssscc .builds @@ -133,6 +121,9 @@ _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user +# JustCode is a .NET coding add-in +.JustCode + # TeamCity is a build add-in _TeamCity* @@ -143,11 +134,6 @@ _TeamCity* .axoCover/* !.axoCover/settings.json -# Coverlet is a free, cross platform Code Coverage Tool -coverage*.json -coverage*.xml -coverage*.info - # Visual Studio code coverage results *.coverage *.coveragexml @@ -195,8 +181,6 @@ PublishScripts/ # NuGet Packages *.nupkg -# NuGet Symbol Packages -*.snupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. @@ -207,9 +191,6 @@ PublishScripts/ *.nuget.props *.nuget.targets -# Nuget personal access tokens and Credentials -nuget.config - # Microsoft Azure Build Output csx/ *.build.csdef @@ -224,14 +205,12 @@ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx -*.appxbundle -*.appxupload # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache -!?*.[Cc]ache/ +!*.[Cc]ache/ # Others ClientBin/ @@ -275,9 +254,6 @@ ServiceFabricBackup/ *.bim.layout *.bim_*.settings *.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl # Microsoft Fakes FakesAssemblies/ @@ -313,6 +289,10 @@ paket-files/ # FAKE - F# Make .fake/ +# JetBrains Rider +.idea/ +*.sln.iml + # CodeRush personal settings .cr/personal @@ -354,48 +334,10 @@ ASALocalRun/ # Local History for Visual Studio .localhistory/ -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ - -# Fody - auto-generated XML schema -FodyWeavers.xsd - -# VS Code files for those working on multiple tools -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -*.code-workspace - -# Local History for Visual Studio Code -.history/ - -# Windows Installer files from build outputs -*.cab -*.msi -*.msix -*.msm -*.msp - -# JetBrains Rider -.idea/ -*.sln.iml - # ---> VisualStudioCode .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json -*.code-workspace - -# Local History for Visual Studio Code -.history/ diff --git a/EonaCat.Network.sln b/EonaCat.Network.sln new file mode 100644 index 0000000..b6b6029 --- /dev/null +++ b/EonaCat.Network.sln @@ -0,0 +1,29 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +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 +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {11B9181D-7186-4D81-A5D3-4804E9A61BA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B839F1D9-578B-4D9F-A8E5-43763F9B1C57} + EndGlobalSection +EndGlobal diff --git a/EonaCat.Network/EonaCat.Network.csproj b/EonaCat.Network/EonaCat.Network.csproj new file mode 100644 index 0000000..10ef845 --- /dev/null +++ b/EonaCat.Network/EonaCat.Network.csproj @@ -0,0 +1,56 @@ + + + Latest + + netstandard2.0; + netstandard2.1; + net5.0; + net6.0; + net7.0; + + true + EonaCat.Network + EonaCat (Jeroen Saey) + EonaCat (Jeroen Saey) + EonaCat.Network + EonaCat (Jeroen Saey) + https://www.nuget.org/packages/EonaCat.Network/ + EonaCat, Network, .NET Standard, EonaCatHelpers, Jeroen, Saey, Protocol, Quic, UDP, TCP, Web, Server + + EonaCat Networking library with Quic, TCP, UDP and a Webserver + 1.0.0 + 1.0.0.0 + 1.0.0.0 + icon.png + + + + false + True + README.md + False + EonaCat.Network + True + LICENSE + + + + + True + \ + + + True + \ + + + True + \ + + + + + + + + diff --git a/EonaCat.Network/EonaCat.Network.sln b/EonaCat.Network/EonaCat.Network.sln new file mode 100644 index 0000000..5439be1 --- /dev/null +++ b/EonaCat.Network/EonaCat.Network.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31605.320 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EonaCat.Network", "EonaCat.Network.csproj", "{FDB16914-DCF7-42BE-8D87-77DB21D68B37}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FDB16914-DCF7-42BE-8D87-77DB21D68B37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FDB16914-DCF7-42BE-8D87-77DB21D68B37}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FDB16914-DCF7-42BE-8D87-77DB21D68B37}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FDB16914-DCF7-42BE-8D87-77DB21D68B37}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {8CF19687-8676-41EE-BEFE-19CBCEDA4699} + EndGlobalSection +EndGlobal diff --git a/EonaCat.Network/NetworkHelper.cs b/EonaCat.Network/NetworkHelper.cs new file mode 100644 index 0000000..821d46b --- /dev/null +++ b/EonaCat.Network/NetworkHelper.cs @@ -0,0 +1,356 @@ +using System; +using System.Text; +using EonaCat.LogSystem; +using EonaCat.Quic; +using EonaCat.Quic.Connections; +using EonaCat.Quic.Events; +using EonaCat.Quic.Helpers; +using EonaCat.Quic.Streams; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public static class NetworkHelper + { + internal static Logging Logger = new Logging(); + private static QuicServer _quicServer; + + public static event EventHandler OnQuicClientConnected; + + public static event EventHandler OnQuicStreamOpened; + + public static event EventHandler OnQuicStreamDataReceived; + + /// + /// Start a Quic server + /// + /// true, start successfully, false did not start successfully. + /// The ip bound to the server + /// The listening port. (default: 11000) + public static bool QuicStartServer(string ip, int port = 11000) + { + _quicServer = new QuicServer(ip, port); + _quicServer.OnClientConnected += ClientConnected; + _quicServer.Start(); + Logger.Info($"The Quic server has been successfully started on ip '{ip}' and port: {port}"); + return true; + } + + /// + /// Fired when Client is connected + /// + /// The new connection + private static void ClientConnected(QuicConnection connection) + { + OnQuicClientConnected?.Invoke(null, new QuicConnectionEventArgs { Connection = connection }); + connection.OnStreamOpened += StreamOpened; + } + + private static void StreamOpened(QuicStream stream) + { + OnQuicStreamOpened?.Invoke(null, new QuicStreamEventArgs { Stream = stream }); + stream.OnStreamDataReceived += StreamDataReceived; + } + + private static void StreamDataReceived(QuicStream stream, byte[] data) + { + OnQuicStreamDataReceived?.Invoke(null, new QuicStreamEventArgs { Stream = stream, Data = data }); + } + + public static QuicStream QuicStartClient(string ip, int port = 11000, StreamType streamType = StreamType.ClientBidirectional) + { + QuicClient client = new QuicClient(); + + // Connect to peer (Server) + QuicConnection connection = client.Connect(ip, port); + + // Create a data stream + return connection.CreateStream(streamType); + } + + /// + /// Stop the Quic server + /// + /// + public static bool QuicStopServer() + { + if (_quicServer != null) + { + _quicServer.Close(); + Logger.Info($"The Quic server has been successfully stopped"); + } + return true; + } + + /// + /// Start a Tcp server + /// + /// true, start successfully, false did not start successfully. + /// The callback function triggered when there is a client connection. The value passed in is the only client Token generated by the server. + /// The callback function is triggered when the client receives information. The value passed in is the client Token and the string message received by the client. The returned string message is directly returned to the client If it is null, do not reply. + /// The ip bound to the server. (default: ALL INTERFACES) + /// The listening port. (default: 8081) + /// ip type v4 or v6. + /// Allowed concurrent connections (default: 10000) + public static bool TcpStart( + Action OnConnectCallBack, + Func OnReceivedCallBack, + int port = 8081, + IPType iPType = IPType.IPv4, + string ip = "0.0.0.0", + int listen = 10000) + { + try + { + _tcpServer = new TcpServer(ip, port, iPType, OnConnectCallBack, OnReceivedCallBack, listen); + + _tcpServer.RunServer(); + + Logger.Info($"The TCP server has been successfully started on port: {port}"); + return true; + } + catch (Exception exception) + { + Logger.Exception(exception); + + return false; + } + } + + /// + /// Stop Tcp server + /// + public static void TcpStop() + { + if (_tcpServer != null) + { + _tcpServer.ShutDownServer(); + } + } + + /// + /// Broadcast a message to all clients on Tcp + /// + /// Message. + public static void TcpBroadCast(byte[] message) + { + if (_tcpServer != null) + { + _tcpServer.BroadCastMessageToAllClients(message); + } + } + + /// + /// Send a message to the specified client according to the client Token + /// + /// true, sent successfully, false did not send successfully. + /// Client Token. + /// The message to be sent. + public static bool TcpSend(string clientToken, byte[] message) + { + if (Token.Instance[clientToken] == null) + { + return false; + } + + // no socket or no connection + if (Token.Instance[clientToken].SocketHandler == null || !Token.Instance[clientToken].SocketHandler.Connected) + { + return false; + } + + Token.Instance[clientToken].SendDataToClient(message); + + return true; + } + + /// + /// Close a client node + /// + /// Client Token. + public static void TcpCloseClient(string clientToken) + { + try + { + if (Token.Instance[clientToken] != null) + { + if (Token.Instance[clientToken].SocketHandler != null) + { + Token.Instance[clientToken].SocketHandler.Close(); + } + } + } + finally + { + // Assign null to delete the client Token + Token.Instance[clientToken] = null; + } + } + + /// + /// handler + /// + private static TcpServer _tcpServer; + + /// + /// Start a Tcp client + /// + /// true, i started successfully, false did not start successfully. + /// The IP to connect. + /// Port. + /// The callback function when a message is received. The value passed in is the received message. The returned string is sent directly to the server. + /// If null is returned, it will not be sent. + public static bool TcpStart(string ip, int port, Func OnReceived, IPType ipType = IPType.IPv4) + { + _tcpClient = new TcpClient(ip, port, OnReceived, ipType); + return true; + } + + /// + /// Tcp client sends a message + /// + /// Message. + public static void TcpSend(byte[] msg) + { + if (_tcpClient == null) + { + return; + } + + _tcpClient.SendDataToServer(msg); + } + + /// + /// handler + /// + private static TcpClient _tcpClient; + + /// + /// Start Udp server + /// + /// true, start successfully, false did not start successfully. + /// Port. (default: 8082) + /// The return method of the received message accepts the incoming Endpoint source and string message, returns the string message, and returns the string type. The return value is directly returned to the client. If it is null, no reply is made. + /// IPv4 or IPv6. + /// Buffer size. + public static bool UdpStart(Func ResponseCallBack = null, int port = 8082, IPType iPType = IPType.IPv4, int bufferSize = 1024) + { + try + { + _udpHandler = new Udp(port, iPType, ResponseCallBack, bufferSize); + + _udpHandler.runServer(); + + Logger.Info($"UDP server has been successfully started on port: {port}"); + return true; + } + catch (Exception e) + { + Logger.Exception(e, "UDP Start exception"); + + return false; + } + } + + /// + /// Stop Udp server + /// + public static void UdpStop() + { + if (_udpHandler != null) + { + _udpHandler.StopServer(); + } + } + + /// + /// Set the Udp server's fallback method + /// + /// The method of returning the received message. Accept the incoming Endpoint source and string message and return the string type return value to directly reply to the client. If it is null, no reply. + public static void UDPCallBack(Func responseCallBack) + { + if (_udpHandler != null) + { + _udpHandler.ResponseCallback = responseCallBack; + } + } + + /// + /// Send a Udp message + /// + /// Ip. + /// Port. + /// Message. + public static void UdpSend(string ip, int port, byte[] message, IPType ipType = IPType.IPv4) + { + if (_udpHandler == null) + { + _udpHandler = new Udp(8083, ipType); + } + + _udpHandler.SendTo(new System.Net.IPEndPoint(System.Net.IPAddress.Parse(ip), port), message); + } + + /// + /// Send a Udp message + /// + /// Target. + /// Message. + public static void UdpSend(System.Net.EndPoint target, byte[] message, IPType ipType = IPType.IPv4) + { + if (_udpHandler != null) + { + _udpHandler = new Udp(8083, ipType); + } + + _udpHandler.SendTo(target, message); + } + + /// + /// Get Udp server handle + /// + /// The UDP server. + public static Udp GetUDPServer() + { + return _udpHandler; + } + + /// + /// handler + /// + private static Udp _udpHandler; + + /// + /// Character bit encoding type web + /// + public static Encoding GlobalEncoding = Encoding.UTF8; + + private static void Main(string[] args) + { + TcpStart((s) => { Console.WriteLine(s + " connected"); }, (id, b) => { Console.WriteLine(GlobalEncoding.GetString(b)); return b; }, 8081, IPType.IPv4); + UdpStart((id, b) => { Console.WriteLine(GlobalEncoding.GetString(b)); return b; }, 8082, IPType.IPv4); + + for (int i = 0; i < 5000; i++) + { + TcpStart("127.0.0.1", 8081, (byte[] receivedStr) => { return new byte[0]; }, IPType.IPv4); + } + + for (int i = 0; i < 5000; i++) + { + UdpSend("127.0.0.1", 8082, Encoding.ASCII.GetBytes("HOI"), IPType.IPv4); + } + Console.ReadLine(); + } + } + + /// + /// IP type enumeration + /// + public enum IPType : byte + { + IPv4, + IPv6 + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Connections/ConnectionPool.cs b/EonaCat.Network/System/Quic/Connections/ConnectionPool.cs new file mode 100644 index 0000000..ae1417f --- /dev/null +++ b/EonaCat.Network/System/Quic/Connections/ConnectionPool.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using EonaCat.Quic.Infrastructure; +using EonaCat.Quic.Infrastructure.Settings; +using EonaCat.Quic.InternalInfrastructure; + +namespace EonaCat.Quic.Connections +{ + // 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. + + /// + /// Since UDP is a stateless protocol, the ConnectionPool is used as a Conenction Manager to + /// route packets to the right "Connection". + /// + internal static class ConnectionPool + { + /// + /// Starting point for connection identifiers. + /// ConnectionId's are incremented sequentially by 1. + /// + private static NumberSpace _ns = new NumberSpace(QuicSettings.MaximumConnectionIds); + + private static Dictionary _pool = new Dictionary(); + + private static List _draining = new List(); + + /// + /// Adds a connection to the connection pool. + /// For now assume that the client connection id is valid, and just send it back. + /// Later this should change in a way that the server validates, and regenerates a connection Id. + /// + /// Connection Id + /// + public static bool AddConnection(ConnectionData connection, out UInt64 availableConnectionId) + { + availableConnectionId = 0; + + if (_pool.ContainsKey(connection.ConnectionId.Value)) + return false; + + if (_pool.Count > QuicSettings.MaximumConnectionIds) + return false; + + availableConnectionId = _ns.Get(); + + connection.PeerConnectionId = connection.ConnectionId; + _pool.Add(availableConnectionId, new QuicConnection(connection)); + + return true; + } + + public static void RemoveConnection(UInt64 id) + { + if (_pool.ContainsKey(id)) + _pool.Remove(id); + } + + public static QuicConnection Find(UInt64 id) + { + if (_pool.ContainsKey(id) == false) + return null; + + return _pool[id]; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Connections/ConnectionState.cs b/EonaCat.Network/System/Quic/Connections/ConnectionState.cs new file mode 100644 index 0000000..80aa483 --- /dev/null +++ b/EonaCat.Network/System/Quic/Connections/ConnectionState.cs @@ -0,0 +1,13 @@ +namespace EonaCat.Quic.Connections +{ + // 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 ConnectionState + { + Open, + Closing, + Closed, + Draining + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Connections/QuicConnection.cs b/EonaCat.Network/System/Quic/Connections/QuicConnection.cs new file mode 100644 index 0000000..fb5899f --- /dev/null +++ b/EonaCat.Network/System/Quic/Connections/QuicConnection.cs @@ -0,0 +1,300 @@ +using System; +using System.Collections.Generic; +using EonaCat.Quic.Constants; +using EonaCat.Quic.Events; +using EonaCat.Quic.Exceptions; +using EonaCat.Quic.Helpers; +using EonaCat.Quic.Infrastructure; +using EonaCat.Quic.Infrastructure.Frames; +using EonaCat.Quic.Infrastructure.PacketProcessing; +using EonaCat.Quic.Infrastructure.Packets; +using EonaCat.Quic.Infrastructure.Settings; +using EonaCat.Quic.InternalInfrastructure; +using EonaCat.Quic.Streams; + +namespace EonaCat.Quic.Connections +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class QuicConnection + { + private readonly NumberSpace _numberSpace = new NumberSpace(); + private readonly PacketWireTransfer _pwt; + + private UInt64 _currentTransferRate; + private ConnectionState _state; + private string _lastError; + private 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 StreamOpenedEvent OnStreamOpened { get; set; } + public ConnectionClosedEvent OnConnectionClosed { get; set; } + + /// + /// Creates a new stream for sending/receiving data. + /// + /// Type of the stream (Uni-Bidirectional) + /// A new stream instance or Null if the connection is terminated. + public QuicStream CreateStream(StreamType type) + { + UInt32 streamId = _numberSpace.Get(); + if (_state != ConnectionState.Open) + return null; + + QuicStream stream = new QuicStream(this, new EonaCat.Quic.Helpers.StreamId(streamId, type)); + _streams.Add(streamId, stream); + + return stream; + } + + public QuicStream ProcessFrames(List frames) + { + QuicStream stream = null; + + 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; + } + + public void IncrementRate(int length) + { + _currentTransferRate += (UInt32)length; + } + + public bool MaximumReached() + { + if (_currentTransferRate >= MaxData) + return true; + + return false; + } + + private void OnConnectionCloseFrame(Frame frame) + { + ConnectionCloseFrame ccf = (ConnectionCloseFrame)frame; + _state = ConnectionState.Draining; + _lastError = ccf.ReasonPhrase; + + OnConnectionClosed?.Invoke(this); + } + + private void OnRstStreamFrame(Frame frame) + { + ResetStreamFrame rsf = (ResetStreamFrame)frame; + if (_streams.ContainsKey(rsf.StreamId)) + { + // Find and reset the stream + QuicStream stream = _streams[rsf.StreamId]; + stream.ResetStream(rsf); + + // Remove the stream from the connection + _streams.Remove(rsf.StreamId); + } + } + + private QuicStream OnStreamFrame(Frame frame) + { + QuicStream stream; + + StreamFrame sf = (StreamFrame)frame; + StreamId streamId = sf.StreamId; + + if (_streams.ContainsKey(streamId.Id) == false) + { + stream = new QuicStream(this, streamId); + + if ((UInt64)_streams.Count < MaxStreams) + _streams.Add(streamId.Id, stream); + else + SendMaximumStreamReachedError(); + + OnStreamOpened?.Invoke(stream); + } + else + { + stream = _streams[streamId.Id]; + } + + stream.ProcessData(sf); + + return stream; + } + + private void OnMaxDataFrame(Frame frame) + { + MaxDataFrame sf = (MaxDataFrame)frame; + if (sf.MaximumData.Value > MaxData) + MaxData = sf.MaximumData.Value; + } + + private void OnMaxStreamDataFrame(Frame frame) + { + MaxStreamDataFrame msdf = (MaxStreamDataFrame)frame; + StreamId streamId = msdf.StreamId; + if (_streams.ContainsKey(streamId.Id)) + { + // Find and set the new maximum stream data on the stream + QuicStream stream = _streams[streamId.Id]; + stream.SetMaximumStreamData(msdf.MaximumStreamData.Value); + } + } + + private void OnMaxStreamFrame(Frame frame) + { + MaxStreamsFrame msf = (MaxStreamsFrame)frame; + if (msf.MaximumStreams > MaxStreams) + MaxStreams = msf.MaximumStreams.Value; + } + + private void OnDataBlockedFrame(Frame frame) + { + TerminateConnection(); + } + + private void OnStreamDataBlockedFrame(Frame frame) + { + StreamDataBlockedFrame sdbf = (StreamDataBlockedFrame)frame; + StreamId streamId = sdbf.StreamId; + + if (_streams.ContainsKey(streamId.Id) == false) + return; + QuicStream stream = _streams[streamId.Id]; + + stream.ProcessStreamDataBlocked(sdbf); + + // Remove the stream from the connection + _streams.Remove(streamId.Id); + } + + internal QuicConnection(ConnectionData connection) + { + _currentTransferRate = 0; + _state = ConnectionState.Open; + _lastError = string.Empty; + _streams = new Dictionary(); + _pwt = connection.PWT; + + ConnectionId = connection.ConnectionId; + PeerConnectionId = connection.PeerConnectionId; + // Also creates a new number space + PacketCreator = new PacketCreator(ConnectionId, PeerConnectionId); + MaxData = QuicSettings.MaxData; + MaxStreams = QuicSettings.MaximumStreamId; + } + + public QuicStream OpenStream() + { + QuicStream stream = null; + + while (stream == null) + { + Packet packet = _pwt.ReadPacket(); + if (packet is ShortHeaderPacket shp) + { + stream = ProcessFrames(shp.GetFrames()); + } + } + + return stream; + } + + /// + /// Client only! + /// + /// + internal void ReceivePacket() + { + Packet packet = _pwt.ReadPacket(); + + if (packet is ShortHeaderPacket shp) + { + ProcessFrames(shp.GetFrames()); + } + + // If the connection has been closed + if (_state == ConnectionState.Draining) + { + if (string.IsNullOrWhiteSpace(_lastError)) + _lastError = "Protocol error"; + + TerminateConnection(); + + throw new ConnectionException(_lastError); + } + } + + internal bool SendData(Packet packet) + { + return _pwt.SendPacket(packet); + } + + internal void TerminateConnection() + { + _state = ConnectionState.Draining; + _streams.Clear(); + + ConnectionPool.RemoveConnection(this.ConnectionId); + } + + internal void SendMaximumStreamReachedError() + { + ShortHeaderPacket packet = PacketCreator.CreateConnectionClosePacket(Infrastructure.ErrorCode.STREAM_LIMIT_ERROR, 0x00, ErrorConstants.MaxNumberOfStreams); + Send(packet); + } + + /// + /// Used to send protocol packets to the peer. + /// + /// + /// + internal bool Send(Packet packet) + { + // Encode the packet + byte[] data = packet.Encode(); + + // Increment the connection transfer rate + IncrementRate(data.Length); + + // If the maximum transfer rate is reached, send FLOW_CONTROL_ERROR + if (MaximumReached()) + { + packet = PacketCreator.CreateConnectionClosePacket(Infrastructure.ErrorCode.FLOW_CONTROL_ERROR, 0x00, ErrorConstants.MaxDataTransfer); + + TerminateConnection(); + } + + // Ignore empty packets + if (data == null || data.Length <= 0) + return true; + + bool result = _pwt.SendPacket(packet); + + return result; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Constants/ErrorConstants.cs b/EonaCat.Network/System/Quic/Constants/ErrorConstants.cs new file mode 100644 index 0000000..33b2a02 --- /dev/null +++ b/EonaCat.Network/System/Quic/Constants/ErrorConstants.cs @@ -0,0 +1,13 @@ +namespace EonaCat.Quic.Constants +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class ErrorConstants + { + public const string ServerTooBusy = "The server is too busy to process your request."; + public const string MaxDataTransfer = "Maximum data transfer reached."; + public const string MaxNumberOfStreams = "Maximum number of streams reached."; + public const string PMTUNotReached = "PMTU have not been reached."; + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Context/QuicStreamContext.cs b/EonaCat.Network/System/Quic/Context/QuicStreamContext.cs new file mode 100644 index 0000000..77182b6 --- /dev/null +++ b/EonaCat.Network/System/Quic/Context/QuicStreamContext.cs @@ -0,0 +1,74 @@ +using System; +using EonaCat.Quic.Streams; + +namespace EonaCat.Quic.Context +{ + // 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. + + /// + /// Wrapper to represent the stream. + /// + public class QuicStreamContext + { + ///// + ///// The connection's context. + ///// + //public QuicContext ConnectionContext { get; set; } + + /// + /// Data received + /// + public byte[] Data { get; private set; } + + /// + /// Unique stream identifier + /// + public UInt64 StreamId { get; private set; } + + /// + /// Send data to the client. + /// + /// + /// + 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); + + // bool result = ConnectionContext.Send(packet); + + //return result; + + return false; + } + + public void Close() + { + // TODO: Close out the stream by sending appropriate packets to the peer + } + + internal QuicStream Stream { get; set; } + + /// + /// Internal constructor to prevent creating the context outside the scope of Quic. + /// + /// + internal QuicStreamContext(QuicStream stream) + { + Stream = stream; + StreamId = stream.StreamId; + } + + internal void SetData(byte[] data) + { + Data = data; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Events/DelegateDefinitions.cs b/EonaCat.Network/System/Quic/Events/DelegateDefinitions.cs new file mode 100644 index 0000000..f27ada8 --- /dev/null +++ b/EonaCat.Network/System/Quic/Events/DelegateDefinitions.cs @@ -0,0 +1,16 @@ +using EonaCat.Quic.Connections; +using EonaCat.Quic.Streams; + +namespace EonaCat.Quic.Events +{ + // 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 delegate void ClientConnectedEvent(QuicConnection connection); + + public delegate void StreamOpenedEvent(QuicStream stream); + + public delegate void StreamDataReceivedEvent(QuicStream stream, byte[] data); + + public delegate void ConnectionClosedEvent(QuicConnection connection); +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Events/QuicEventArgs.cs b/EonaCat.Network/System/Quic/Events/QuicEventArgs.cs new file mode 100644 index 0000000..eb806a5 --- /dev/null +++ b/EonaCat.Network/System/Quic/Events/QuicEventArgs.cs @@ -0,0 +1,19 @@ +using EonaCat.Quic.Connections; +using EonaCat.Quic.Streams; + +namespace EonaCat.Quic.Events +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class QuicStreamEventArgs + { + public QuicStream Stream { get; set; } + public byte[] Data { get; set; } + } + + public class QuicConnectionEventArgs + { + public QuicConnection Connection { get; set; } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Exceptions/ConnectionException.cs b/EonaCat.Network/System/Quic/Exceptions/ConnectionException.cs new file mode 100644 index 0000000..7036515 --- /dev/null +++ b/EonaCat.Network/System/Quic/Exceptions/ConnectionException.cs @@ -0,0 +1,14 @@ +using System; + +namespace EonaCat.Quic.Exceptions +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class ConnectionException : Exception + { + public ConnectionException(string message) : base(message) + { + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Exceptions/ServerNotStartedException.cs b/EonaCat.Network/System/Quic/Exceptions/ServerNotStartedException.cs new file mode 100644 index 0000000..2c5ce33 --- /dev/null +++ b/EonaCat.Network/System/Quic/Exceptions/ServerNotStartedException.cs @@ -0,0 +1,17 @@ +using System; + +namespace EonaCat.Quic.Exceptions +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class ServerNotStartedException : Exception + { + public ServerNotStartedException() + { } + + public ServerNotStartedException(string message) : base(message) + { + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Exceptions/StreamException.cs b/EonaCat.Network/System/Quic/Exceptions/StreamException.cs new file mode 100644 index 0000000..c82c0de --- /dev/null +++ b/EonaCat.Network/System/Quic/Exceptions/StreamException.cs @@ -0,0 +1,17 @@ +using System; + +namespace EonaCat.Quic.Exceptions +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class StreamException : Exception + { + public StreamException() + { } + + public StreamException(string message) : base(message) + { + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Helpers/ByteArray.cs b/EonaCat.Network/System/Quic/Helpers/ByteArray.cs new file mode 100644 index 0000000..98dfe28 --- /dev/null +++ b/EonaCat.Network/System/Quic/Helpers/ByteArray.cs @@ -0,0 +1,99 @@ +using System; + +namespace EonaCat.Quic.Helpers +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class ByteArray + { + private readonly byte[] _array; + private readonly int _length; + + private int _offset; + + public ByteArray(byte[] array) + { + _array = array; + _length = array.Length; + _offset = 0; + } + + public byte ReadByte() + { + byte result = _array[_offset++]; + return result; + } + + public byte PeekByte() + { + byte result = _array[_offset]; + return result; + } + + public byte[] ReadBytes(int count) + { + byte[] bytes = new byte[count]; + Buffer.BlockCopy(_array, _offset, bytes, 0, count); + + _offset += count; + + return bytes; + } + + public byte[] ReadBytes(IntegerVar count) + { + return ReadBytes(count.Value); + } + + public UInt16 ReadUInt16() + { + byte[] bytes = ReadBytes(2); + UInt16 result = ByteHelpers.ToUInt16(bytes); + + return result; + } + + public UInt32 ReadUInt32() + { + byte[] bytes = ReadBytes(4); + UInt32 result = ByteHelpers.ToUInt32(bytes); + + return result; + } + + public IntegerVar ReadIntegerVar() + { + // Set Token Length and Token + byte initial = PeekByte(); + int size = IntegerVar.Size(initial); + + byte[] bytes = new byte[size]; + Buffer.BlockCopy(_array, _offset, bytes, 0, size); + _offset += size; + + return bytes; + } + + public IntegerParts ReadGranularInteger(int size) + { + byte[] data = ReadBytes(size); + IntegerParts result = data; + + return result; + } + + public StreamId ReadStreamId() + { + byte[] streamId = ReadBytes(8); + StreamId result = streamId; + + return result; + } + + public bool HasData() + { + return _offset < _length; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Helpers/ByteHelpers.cs b/EonaCat.Network/System/Quic/Helpers/ByteHelpers.cs new file mode 100644 index 0000000..e52c73e --- /dev/null +++ b/EonaCat.Network/System/Quic/Helpers/ByteHelpers.cs @@ -0,0 +1,82 @@ +using System; +using System.Text; + +namespace EonaCat.Quic.Helpers +{ + // 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 static class ByteHelpers + { + public static byte[] GetBytes(UInt64 integer) + { + byte[] result = BitConverter.GetBytes(integer); + if (BitConverter.IsLittleEndian) + Array.Reverse(result); + + return result; + } + + public static byte[] GetBytes(UInt32 integer) + { + byte[] result = BitConverter.GetBytes(integer); + if (BitConverter.IsLittleEndian) + Array.Reverse(result); + + return result; + } + + public static byte[] GetBytes(UInt16 integer) + { + byte[] result = BitConverter.GetBytes(integer); + if (BitConverter.IsLittleEndian) + Array.Reverse(result); + + return result; + } + + public static byte[] GetBytes(string str) + { + byte[] result = Encoding.UTF8.GetBytes(str); + + return result; + } + + public static UInt64 ToUInt64(byte[] data) + { + if (BitConverter.IsLittleEndian) + Array.Reverse(data); + + UInt64 result = BitConverter.ToUInt64(data, 0); + + return result; + } + + public static UInt32 ToUInt32(byte[] data) + { + if (BitConverter.IsLittleEndian) + Array.Reverse(data); + + UInt32 result = BitConverter.ToUInt32(data, 0); + + return result; + } + + public static UInt16 ToUInt16(byte[] data) + { + if (BitConverter.IsLittleEndian) + Array.Reverse(data); + + UInt16 result = BitConverter.ToUInt16(data, 0); + + return result; + } + + public static string GetString(byte[] str) + { + string result = Encoding.UTF8.GetString(str); + + return result; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Helpers/IntegerParts.cs b/EonaCat.Network/System/Quic/Helpers/IntegerParts.cs new file mode 100644 index 0000000..53484e9 --- /dev/null +++ b/EonaCat.Network/System/Quic/Helpers/IntegerParts.cs @@ -0,0 +1,93 @@ +using System; + +namespace EonaCat.Quic.Helpers +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class IntegerParts + { + public const UInt64 MaxValue = 18446744073709551615; + + private UInt64 _integer; + + public UInt64 Value + { get { return _integer; } } + + public byte Size + { get { return RequiredBytes(Value); } } + + public IntegerParts(UInt64 integer) + { + _integer = integer; + } + + public byte[] ToByteArray() + { + return Encode(this._integer); + } + + public static implicit operator byte[](IntegerParts integer) + { + return Encode(integer._integer); + } + + public static implicit operator IntegerParts(byte[] bytes) + { + return new IntegerParts(Decode(bytes)); + } + + public static implicit operator IntegerParts(UInt64 integer) + { + return new IntegerParts(integer); + } + + public static implicit operator UInt64(IntegerParts integer) + { + return integer._integer; + } + + public static byte[] Encode(UInt64 integer) + { + byte requiredBytes = RequiredBytes(integer); + int offset = 8 - requiredBytes; + + byte[] uInt64Bytes = ByteHelpers.GetBytes(integer); + + byte[] result = new byte[requiredBytes]; + Buffer.BlockCopy(uInt64Bytes, offset, result, 0, requiredBytes); + + return result; + } + + public static UInt64 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); + + return res; + } + + private static byte RequiredBytes(UInt64 integer) + { + byte result = 0; + + if (integer <= byte.MaxValue) /* 255 */ + result = 1; + else if (integer <= UInt16.MaxValue) /* 65535 */ + result = 2; + else if (integer <= UInt32.MaxValue) /* 4294967295 */ + result = 4; + else if (integer <= UInt64.MaxValue) /* 18446744073709551615 */ + result = 8; + else + throw new ArgumentOutOfRangeException("Value is larger than GranularInteger.MaxValue."); + + return result; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Helpers/StreamId.cs b/EonaCat.Network/System/Quic/Helpers/StreamId.cs new file mode 100644 index 0000000..b350e41 --- /dev/null +++ b/EonaCat.Network/System/Quic/Helpers/StreamId.cs @@ -0,0 +1,71 @@ +using System; + +namespace EonaCat.Quic.Helpers +{ + // 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 StreamType + { + ClientBidirectional = 0x0, + ServerBidirectional = 0x1, + ClientUnidirectional = 0x2, + ServerUnidirectional = 0x3 + } + + public class StreamId + { + public UInt64 Id { get; } + public UInt64 IntegerValue { get; } + public StreamType Type { get; private set; } + + public StreamId(UInt64 id, StreamType type) + { + Id = id; + Type = type; + IntegerValue = id << 2 | (UInt64)type; + } + + public static implicit operator byte[](StreamId id) + { + return Encode(id.Id, id.Type); + } + + public static implicit operator StreamId(byte[] data) + { + return Decode(data); + } + + public static implicit operator UInt64(StreamId streamId) + { + return streamId.Id; + } + + public static implicit operator StreamId(IntegerVar integer) + { + return Decode(ByteHelpers.GetBytes(integer.Value)); + } + + public static byte[] Encode(UInt64 id, StreamType type) + { + UInt64 identifier = id << 2 | (UInt64)type; + + byte[] result = ByteHelpers.GetBytes(identifier); + + return result; + } + + public static StreamId Decode(byte[] data) + { + StreamId result; + UInt64 id = ByteHelpers.ToUInt64(data); + UInt64 identifier = id >> 2; + UInt64 type = (UInt64)(0x03 & id); + StreamType streamType = (StreamType)type; + + result = new StreamId(identifier, streamType); + + return result; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Helpers/VariableInteger.cs b/EonaCat.Network/System/Quic/Helpers/VariableInteger.cs new file mode 100644 index 0000000..87d9ec9 --- /dev/null +++ b/EonaCat.Network/System/Quic/Helpers/VariableInteger.cs @@ -0,0 +1,99 @@ +using System; + +namespace EonaCat.Quic.Helpers +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class IntegerVar + { + public const UInt64 MaxValue = 4611686018427387903; + + private UInt64 _integer; + + public UInt64 Value + { get { return _integer; } } + + public IntegerVar(UInt64 integer) + { + _integer = integer; + } + + public static implicit operator byte[](IntegerVar integer) + { + return Encode(integer._integer); + } + + public static implicit operator IntegerVar(byte[] bytes) + { + return new IntegerVar(Decode(bytes)); + } + + public static implicit operator IntegerVar(UInt64 integer) + { + return new IntegerVar(integer); + } + + public static implicit operator UInt64(IntegerVar integer) + { + return integer._integer; + } + + public static implicit operator IntegerVar(StreamId streamId) + { + return new IntegerVar(streamId.IntegerValue); + } + + public static int Size(byte firstByte) + { + int result = (int)Math.Pow(2, (firstByte >> 6)); + + return result; + } + + public byte[] ToByteArray() + { + return Encode(this._integer); + } + + public static byte[] Encode(UInt64 integer) + { + int requiredBytes = 0; + if (integer <= byte.MaxValue >> 2) /* 63 */ + requiredBytes = 1; + else if (integer <= UInt16.MaxValue >> 2) /* 16383 */ + requiredBytes = 2; + else if (integer <= UInt32.MaxValue >> 2) /* 1073741823 */ + requiredBytes = 4; + else if (integer <= UInt64.MaxValue >> 2) /* 4611686018427387903 */ + requiredBytes = 8; + else + throw new ArgumentOutOfRangeException("Value is larger than IntegerVar.MaxValue."); + + int offset = 8 - requiredBytes; + + byte[] uInt64Bytes = ByteHelpers.GetBytes(integer); + byte first = uInt64Bytes[offset]; + first = (byte)(first | (requiredBytes / 2) << 6); + uInt64Bytes[offset] = first; + + byte[] result = new byte[requiredBytes]; + Buffer.BlockCopy(uInt64Bytes, offset, result, 0, requiredBytes); + + return result; + } + + public static UInt64 Decode(byte[] bytes) + { + int i = 8 - bytes.Length; + byte[] buffer = new byte[8]; + + Buffer.BlockCopy(bytes, 0, buffer, i, bytes.Length); + buffer[i] = (byte)(buffer[i] & (255 >> 2)); + + UInt64 res = ByteHelpers.ToUInt64(buffer); + + return res; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/ErrorCodes.cs b/EonaCat.Network/System/Quic/Infrastructure/ErrorCodes.cs new file mode 100644 index 0000000..0f20d17 --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/ErrorCodes.cs @@ -0,0 +1,28 @@ +using System; + +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 + { + NO_ERROR = 0x0, + INTERNAL_ERROR = 0x1, + CONNECTION_REFUSED = 0x2, + FLOW_CONTROL_ERROR = 0x3, + STREAM_LIMIT_ERROR = 0x4, + STREAM_STATE_ERROR = 0x5, + FINAL_SIZE_ERROR = 0x6, + FRAME_ENCODING_ERROR = 0x7, + TRANSPORT_PARAMETER_ERROR = 0x8, + CONNECTION_ID_LIMIT_ERROR = 0x9, + PROTOCOL_VIOLATION = 0xA, + INVALID_TOKEN = 0xB, + APPLICATION_ERROR = 0xC, + CRYPTO_BUFFER_EXCEEDED = 0xD, + KEY_UPDATE_ERROR = 0xE, + AEAD_LIMIT_REACHED = 0xF, + CRYPTO_ERROR = 0x100 + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Exceptions/ProtocolException.cs b/EonaCat.Network/System/Quic/Infrastructure/Exceptions/ProtocolException.cs new file mode 100644 index 0000000..1a37dc0 --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/Exceptions/ProtocolException.cs @@ -0,0 +1,18 @@ +using System; + +namespace EonaCat.Quic.Infrastructure.Exceptions +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class ProtocolException : Exception + { + public ProtocolException() + { + } + + public ProtocolException(string message) : base(message) + { + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/FrameParser.cs b/EonaCat.Network/System/Quic/Infrastructure/FrameParser.cs new file mode 100644 index 0000000..892de6d --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/FrameParser.cs @@ -0,0 +1,155 @@ +using EonaCat.Quic.Helpers; +using EonaCat.Quic.Infrastructure.Frames; + +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 class FrameParser + { + private ByteArray _array; + + public FrameParser(ByteArray array) + { + _array = array; + } + + public Frame GetFrame() + { + Frame result; + byte frameType = _array.PeekByte(); + switch (frameType) + { + case 0x00: + result = new PaddingFrame(); + break; + + case 0x01: + result = new PingFrame(); + break; + + case 0x02: + result = new AckFrame(); + break; + + case 0x03: + result = new AckFrame(); + break; + + case 0x04: + result = new ResetStreamFrame(); + break; + + case 0x05: + result = new StopSendingFrame(); + break; + + case 0x06: + result = new CryptoFrame(); + break; + + case 0x07: + result = new NewTokenFrame(); + break; + + case 0x08: + result = new StreamFrame(); + break; + + case 0x09: + result = new StreamFrame(); + break; + + case 0x0a: + result = new StreamFrame(); + break; + + case 0x0b: + result = new StreamFrame(); + break; + + case 0x0c: + result = new StreamFrame(); + break; + + case 0x0d: + result = new StreamFrame(); + break; + + case 0x0e: + result = new StreamFrame(); + break; + + case 0x0f: + result = new StreamFrame(); + break; + + case 0x10: + result = new MaxDataFrame(); + break; + + case 0x11: + result = new MaxStreamDataFrame(); + break; + + case 0x12: + result = new MaxStreamsFrame(); + break; + + case 0x13: + result = new MaxStreamsFrame(); + break; + + case 0x14: + result = new DataBlockedFrame(); + break; + + case 0x15: + result = new StreamDataBlockedFrame(); + break; + + case 0x16: + result = new StreamsBlockedFrame(); + break; + + case 0x17: + result = new StreamsBlockedFrame(); + break; + + case 0x18: + result = new NewConnectionIdFrame(); + break; + + case 0x19: + result = new RetireConnectionIdFrame(); + break; + + case 0x1a: + result = new PathChallengeFrame(); + break; + + case 0x1b: + result = new PathResponseFrame(); + break; + + case 0x1c: + result = new ConnectionCloseFrame(); + break; + + case 0x1d: + result = new ConnectionCloseFrame(); + break; + + default: + result = null; + break; + } + + if (result != null) + result.Decode(_array); + + return result; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/AckFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/AckFrame.cs new file mode 100644 index 0000000..f67c2d7 --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/AckFrame.cs @@ -0,0 +1,23 @@ +using System; +using EonaCat.Quic.Helpers; + +namespace EonaCat.Quic.Infrastructure.Frames +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class AckFrame : Frame + { + public override byte Type => 0x02; + + public override void Decode(ByteArray array) + { + throw new NotImplementedException(); + } + + public override byte[] Encode() + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/ConnectionCloseFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/ConnectionCloseFrame.cs new file mode 100644 index 0000000..afb4875 --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/ConnectionCloseFrame.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using EonaCat.Quic.Helpers; + +namespace EonaCat.Quic.Infrastructure.Frames +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class ConnectionCloseFrame : Frame + { + public byte ActualType { get; set; } + public override byte Type => 0x1c; + public IntegerVar ErrorCode { get; set; } + public IntegerVar FrameType { get; set; } + public IntegerVar ReasonPhraseLength { get; set; } + public string ReasonPhrase { get; set; } + + public ConnectionCloseFrame() + { + ErrorCode = 0; + ReasonPhraseLength = new IntegerVar(0); + } + + /// + /// 0x1d not yet supported (Application Protocol Error) + /// + public ConnectionCloseFrame(ErrorCode error, byte frameType, string reason) + { + ActualType = 0x1c; + + ErrorCode = (UInt64)error; + FrameType = new IntegerVar((UInt64)frameType); + if (!string.IsNullOrWhiteSpace(reason)) + { + ReasonPhraseLength = new IntegerVar((UInt64)reason.Length); + } + else + { + ReasonPhraseLength = new IntegerVar(0); + } + + ReasonPhrase = reason; + } + + public override void Decode(ByteArray array) + { + ActualType = array.ReadByte(); + ErrorCode = array.ReadIntegerVar(); + if (ActualType == 0x1c) + { + FrameType = array.ReadIntegerVar(); + } + + ReasonPhraseLength = array.ReadIntegerVar(); + + byte[] rp = array.ReadBytes((int)ReasonPhraseLength.Value); + ReasonPhrase = ByteHelpers.GetString(rp); + } + + public override byte[] Encode() + { + List result = new List(); + result.Add(ActualType); + result.AddRange(ErrorCode.ToByteArray()); + if (ActualType == 0x1c) + { + result.AddRange(FrameType.ToByteArray()); + } + + if (string.IsNullOrWhiteSpace(ReasonPhrase) == false) + { + byte[] rpl = new IntegerVar((UInt64)ReasonPhrase.Length); + result.AddRange(rpl); + + byte[] reasonPhrase = ByteHelpers.GetBytes(ReasonPhrase); + result.AddRange(reasonPhrase); + } + + return result.ToArray(); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/CryptoFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/CryptoFrame.cs new file mode 100644 index 0000000..913e3e5 --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/CryptoFrame.cs @@ -0,0 +1,22 @@ +using System; +using EonaCat.Quic.Helpers; + +namespace EonaCat.Quic.Infrastructure.Frames +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + public class CryptoFrame : Frame + { + public override byte Type => 0x06; + + public override void Decode(ByteArray array) + { + throw new NotImplementedException(); + } + + public override byte[] Encode() + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/DataBlockedFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/DataBlockedFrame.cs new file mode 100644 index 0000000..790e21b --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/DataBlockedFrame.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using EonaCat.Quic.Helpers; + +namespace EonaCat.Quic.Infrastructure.Frames +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + public class DataBlockedFrame : Frame + { + public override byte Type => 0x14; + public IntegerVar MaximumData { get; set; } + + public DataBlockedFrame() + { + } + + public DataBlockedFrame(UInt64 dataLimit) + { + MaximumData = dataLimit; + } + + public override void Decode(ByteArray array) + { + byte type = array.ReadByte(); + MaximumData = array.ReadIntegerVar(); + } + + public override byte[] Encode() + { + List result = new List(); + result.Add(Type); + result.AddRange(MaximumData.ToByteArray()); + + return result.ToArray(); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/Frame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/Frame.cs new file mode 100644 index 0000000..47a731b --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/Frame.cs @@ -0,0 +1,19 @@ +using EonaCat.Quic.Helpers; + +namespace EonaCat.Quic.Infrastructure.Frames +{ + // 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. + + /// + /// Data encapsulation unit for a Packet. + /// + public abstract class Frame + { + public abstract byte Type { get; } + + public abstract byte[] Encode(); + + public abstract void Decode(ByteArray array); + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxDataFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxDataFrame.cs new file mode 100644 index 0000000..35d622c --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxDataFrame.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using EonaCat.Quic.Helpers; + +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) + // See file LICENSE or go to https://EonaCat.com/License for full license details. + + public override byte Type => 0x10; + public IntegerVar MaximumData { get; set; } + + public override void Decode(ByteArray array) + { + array.ReadByte(); + MaximumData = array.ReadIntegerVar(); + } + + public override byte[] Encode() + { + List result = new List(); + + result.Add(Type); + result.AddRange(MaximumData.ToByteArray()); + + return result.ToArray(); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxStreamDataFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxStreamDataFrame.cs new file mode 100644 index 0000000..3dd9dc2 --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxStreamDataFrame.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using EonaCat.Quic.Helpers; + +namespace EonaCat.Quic.Infrastructure.Frames +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class MaxStreamDataFrame : Frame + { + public override byte Type => 0x11; + public IntegerVar StreamId { get; set; } + public IntegerVar MaximumStreamData { get; set; } + + public StreamId ConvertedStreamId { get; set; } + + public MaxStreamDataFrame() + { + } + + public MaxStreamDataFrame(UInt64 streamId, UInt64 maximumStreamData) + { + StreamId = streamId; + MaximumStreamData = maximumStreamData; + } + + public override void Decode(ByteArray array) + { + byte type = array.ReadByte(); + StreamId = array.ReadIntegerVar(); + MaximumStreamData = array.ReadIntegerVar(); + } + + public override byte[] Encode() + { + List result = new List(); + + result.Add(Type); + result.AddRange(StreamId.ToByteArray()); + result.AddRange(MaximumStreamData.ToByteArray()); + + return result.ToArray(); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxStreamsFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxStreamsFrame.cs new file mode 100644 index 0000000..9c46ac8 --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxStreamsFrame.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using EonaCat.Quic.Helpers; + +namespace EonaCat.Quic.Infrastructure.Frames +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + public class MaxStreamsFrame : Frame + { + public override byte Type => 0x12; + public IntegerVar MaximumStreams { get; set; } + + public MaxStreamsFrame() + { + } + + public MaxStreamsFrame(UInt64 maximumStreamId, StreamType appliesTo) + { + MaximumStreams = new IntegerVar(maximumStreamId); + } + + public override void Decode(ByteArray array) + { + byte type = array.ReadByte(); + MaximumStreams = array.ReadIntegerVar(); + } + + public override byte[] Encode() + { + List result = new List(); + + result.Add(Type); + result.AddRange(MaximumStreams.ToByteArray()); + + return result.ToArray(); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/NewConnectionIdFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/NewConnectionIdFrame.cs new file mode 100644 index 0000000..9c2c165 --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/NewConnectionIdFrame.cs @@ -0,0 +1,23 @@ +using System; +using EonaCat.Quic.Helpers; + +namespace EonaCat.Quic.Infrastructure.Frames +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class NewConnectionIdFrame : Frame + { + public override byte Type => 0x18; + + public override void Decode(ByteArray array) + { + throw new NotImplementedException(); + } + + public override byte[] Encode() + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/NewTokenFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/NewTokenFrame.cs new file mode 100644 index 0000000..9b2a9be --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/NewTokenFrame.cs @@ -0,0 +1,23 @@ +using System; +using EonaCat.Quic.Helpers; + +namespace EonaCat.Quic.Infrastructure.Frames +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class NewTokenFrame : Frame + { + public override byte Type => 0x07; + + public override void Decode(ByteArray array) + { + throw new NotImplementedException(); + } + + public override byte[] Encode() + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/PaddingFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/PaddingFrame.cs new file mode 100644 index 0000000..8c23103 --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/PaddingFrame.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using EonaCat.Quic.Helpers; + +namespace EonaCat.Quic.Infrastructure.Frames +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class PaddingFrame : Frame + { + public override byte Type => 0x00; + + public override void Decode(ByteArray array) + { + byte type = array.ReadByte(); + } + + public override byte[] Encode() + { + List data = new List(); + data.Add(Type); + + return data.ToArray(); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/PathChallengeFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/PathChallengeFrame.cs new file mode 100644 index 0000000..6e17429 --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/PathChallengeFrame.cs @@ -0,0 +1,23 @@ +using System; +using EonaCat.Quic.Helpers; + +namespace EonaCat.Quic.Infrastructure.Frames +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class PathChallengeFrame : Frame + { + public override byte Type => 0x1a; + + public override void Decode(ByteArray array) + { + throw new NotImplementedException(); + } + + public override byte[] Encode() + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/PathResponseFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/PathResponseFrame.cs new file mode 100644 index 0000000..1a3aa7e --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/PathResponseFrame.cs @@ -0,0 +1,23 @@ +using System; +using EonaCat.Quic.Helpers; + +namespace EonaCat.Quic.Infrastructure.Frames +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class PathResponseFrame : Frame + { + public override byte Type => 0x1b; + + public override void Decode(ByteArray array) + { + throw new NotImplementedException(); + } + + public override byte[] Encode() + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/PingFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/PingFrame.cs new file mode 100644 index 0000000..77afb8d --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/PingFrame.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using EonaCat.Quic.Helpers; + +namespace EonaCat.Quic.Infrastructure.Frames +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class PingFrame : Frame + { + public override byte Type => 0x01; + + public override void Decode(ByteArray array) + { + byte type = array.ReadByte(); + } + + public override byte[] Encode() + { + List data = new List(); + data.Add(Type); + + return data.ToArray(); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/ResetStreamFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/ResetStreamFrame.cs new file mode 100644 index 0000000..62e1c37 --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/ResetStreamFrame.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using EonaCat.Quic.Helpers; + +namespace EonaCat.Quic.Infrastructure.Frames +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class ResetStreamFrame : Frame + { + public override byte Type => 0x04; + public IntegerVar StreamId { get; set; } + public IntegerVar ApplicationProtocolErrorCode { get; set; } + public IntegerVar FinalSize { get; set; } + + public override void Decode(ByteArray array) + { + byte type = array.ReadByte(); + StreamId = array.ReadIntegerVar(); + ApplicationProtocolErrorCode = array.ReadIntegerVar(); + FinalSize = array.ReadIntegerVar(); + } + + public override byte[] Encode() + { + List result = new List(); + + result.Add(Type); + result.AddRange(StreamId.ToByteArray()); + result.AddRange(ApplicationProtocolErrorCode.ToByteArray()); + result.AddRange(FinalSize.ToByteArray()); + + return result.ToArray(); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/RetireConnectionIdFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/RetireConnectionIdFrame.cs new file mode 100644 index 0000000..05abcfa --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/RetireConnectionIdFrame.cs @@ -0,0 +1,23 @@ +using System; +using EonaCat.Quic.Helpers; + +namespace EonaCat.Quic.Infrastructure.Frames +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class RetireConnectionIdFrame : Frame + { + public override byte Type => 0x19; + + public override void Decode(ByteArray array) + { + throw new NotImplementedException(); + } + + public override byte[] Encode() + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/StopSendingFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/StopSendingFrame.cs new file mode 100644 index 0000000..1f10cc3 --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/StopSendingFrame.cs @@ -0,0 +1,23 @@ +using System; +using EonaCat.Quic.Helpers; + +namespace EonaCat.Quic.Infrastructure.Frames +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class StopSendingFrame : Frame + { + public override byte Type => 0x05; + + public override void Decode(ByteArray array) + { + throw new NotImplementedException(); + } + + public override byte[] Encode() + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamDataBlockedFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamDataBlockedFrame.cs new file mode 100644 index 0000000..ab27995 --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamDataBlockedFrame.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using EonaCat.Quic.Helpers; + +namespace EonaCat.Quic.Infrastructure.Frames +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class StreamDataBlockedFrame : Frame + { + public override byte Type => 0x15; + public IntegerVar StreamId { get; set; } + public IntegerVar MaximumStreamData { get; set; } + + public StreamDataBlockedFrame() + { + } + + public StreamDataBlockedFrame(UInt64 streamId, UInt64 streamDataLimit) + { + StreamId = streamId; + MaximumStreamData = streamDataLimit; + } + + public override void Decode(ByteArray array) + { + byte type = array.ReadByte(); + StreamId = array.ReadIntegerVar(); + MaximumStreamData = array.ReadIntegerVar(); + } + + public override byte[] Encode() + { + List result = new List(); + + result.Add(Type); + result.AddRange(StreamId.ToByteArray()); + result.AddRange(MaximumStreamData.ToByteArray()); + + return result.ToArray(); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamFrame.cs new file mode 100644 index 0000000..831643d --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamFrame.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using EonaCat.Quic.Helpers; + +namespace EonaCat.Quic.Infrastructure.Frames +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class StreamFrame : Frame + { + public byte ActualType = 0x08; + + public override byte Type => 0x08; + public IntegerVar StreamId { get; set; } + public IntegerVar Offset { get; set; } + public IntegerVar Length { get; set; } + public byte[] StreamData { get; set; } + public bool EndOfStream { get; set; } + + public StreamFrame() + { + } + + public StreamFrame(UInt64 streamId, byte[] data, UInt64 offset, bool eos) + { + StreamId = streamId; + StreamData = data; + Offset = offset; + Length = (UInt64)data.Length; + EndOfStream = eos; + } + + public override void Decode(ByteArray array) + { + byte type = array.ReadByte(); + + byte OFF_BIT = (byte)(type & 0x04); + byte LEN_BIT = (byte)(type & 0x02); + byte FIN_BIT = (byte)(type & 0x01); + + 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); + } + + 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); + byte FIN_BIT = (byte)(ActualType & 0x01); + + List result = new List(); + result.Add(ActualType); + byte[] streamId = StreamId; + result.AddRange(streamId); + + if (OFF_BIT > 0) + result.AddRange(Offset.ToByteArray()); + + if (LEN_BIT > 0) + result.AddRange(Length.ToByteArray()); + + result.AddRange(StreamData); + + return result.ToArray(); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamsBlockedFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamsBlockedFrame.cs new file mode 100644 index 0000000..ad6eaaa --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamsBlockedFrame.cs @@ -0,0 +1,23 @@ +using System; +using EonaCat.Quic.Helpers; + +namespace EonaCat.Quic.Infrastructure.Frames +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class StreamsBlockedFrame : Frame + { + public override byte Type => 0x16; + + public override void Decode(ByteArray array) + { + throw new NotImplementedException(); + } + + public override byte[] Encode() + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/NumberSpace.cs b/EonaCat.Network/System/Quic/Infrastructure/NumberSpace.cs new file mode 100644 index 0000000..574bbcc --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/NumberSpace.cs @@ -0,0 +1,36 @@ +using System; + +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 class NumberSpace + { + private UInt32 _max = UInt32.MaxValue; + private UInt32 _n = 0; + + public NumberSpace() + { + } + + public NumberSpace(UInt32 max) + { + _max = max; + } + + public bool IsMax() + { + return _n == _max; + } + + public UInt32 Get() + { + if (_n >= _max) + return 0; + + _n++; + return _n; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/PacketProcessing/InitialPacketCreator.cs b/EonaCat.Network/System/Quic/Infrastructure/PacketProcessing/InitialPacketCreator.cs new file mode 100644 index 0000000..9917b22 --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/PacketProcessing/InitialPacketCreator.cs @@ -0,0 +1,35 @@ +using EonaCat.Quic.Helpers; +using EonaCat.Quic.Infrastructure.Frames; +using EonaCat.Quic.Infrastructure.Packets; +using EonaCat.Quic.Infrastructure.Settings; + +namespace EonaCat.Quic.Infrastructure.PacketProcessing +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class InitialPacketCreator + { + public InitialPacket CreateInitialPacket(IntegerParts sourceConnectionId, IntegerParts destinationConnectionId) + { + InitialPacket packet = new InitialPacket(destinationConnectionId, sourceConnectionId); + packet.PacketNumber = 0; + packet.SourceConnectionId = sourceConnectionId; + packet.DestinationConnectionId = destinationConnectionId; + packet.Version = QuicVersion.CurrentVersion; + + int length = packet.Encode().Length; + int padding = QuicSettings.PMTU - length; + + for (int i = 0; i < padding; i++) + packet.AttachFrame(new PaddingFrame()); + + return packet; + } + + public VersionNegotiationPacket CreateVersionNegotiationPacket() + { + return new VersionNegotiationPacket(); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/PacketProcessing/PacketCreator.cs b/EonaCat.Network/System/Quic/Infrastructure/PacketProcessing/PacketCreator.cs new file mode 100644 index 0000000..fcd52d1 --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/PacketProcessing/PacketCreator.cs @@ -0,0 +1,55 @@ +using System; +using EonaCat.Quic.Helpers; +using EonaCat.Quic.Infrastructure.Frames; +using EonaCat.Quic.Infrastructure.Packets; + +namespace EonaCat.Quic.Infrastructure.PacketProcessing +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class PacketCreator + { + private readonly NumberSpace _ns; + private readonly IntegerParts _connectionId; + private readonly IntegerParts _peerConnectionId; + + public PacketCreator(IntegerParts connectionId, IntegerParts peerConnectionId) + { + _ns = new NumberSpace(); + + _connectionId = connectionId; + _peerConnectionId = peerConnectionId; + } + + public ShortHeaderPacket CreateConnectionClosePacket(ErrorCode code, byte frameType, string reason) + { + ShortHeaderPacket packet = new ShortHeaderPacket(_peerConnectionId.Size); + packet.PacketNumber = _ns.Get(); + packet.DestinationConnectionId = (byte)_peerConnectionId; + packet.AttachFrame(new ConnectionCloseFrame(code, frameType, reason)); + + return packet; + } + + public ShortHeaderPacket CreateDataPacket(UInt64 streamId, byte[] data, UInt64 offset, bool eos) + { + ShortHeaderPacket packet = new ShortHeaderPacket(_peerConnectionId.Size); + packet.PacketNumber = _ns.Get(); + packet.DestinationConnectionId = (byte)_peerConnectionId; + packet.AttachFrame(new StreamFrame(streamId, data, offset, eos)); + + return packet; + } + + public InitialPacket CreateServerBusyPacket() + { + return new InitialPacket(0, 0); + } + + public ShortHeaderPacket CreateShortHeaderPacket() + { + return new ShortHeaderPacket(0); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/PacketType.cs b/EonaCat.Network/System/Quic/Infrastructure/PacketType.cs new file mode 100644 index 0000000..2ac7a91 --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/PacketType.cs @@ -0,0 +1,15 @@ +using System; + +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 + { + Initial = 0x0, + ZeroRTTProtected = 0x1, + Handshake = 0x2, + RetryPacket = 0x3 + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Packets/InitialPacket.cs b/EonaCat.Network/System/Quic/Infrastructure/Packets/InitialPacket.cs new file mode 100644 index 0000000..50b9180 --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/Packets/InitialPacket.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using EonaCat.Quic.Helpers; + +namespace EonaCat.Quic.Infrastructure.Packets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class InitialPacket : Packet + { + public override byte Type => 0b1100_1100; //0xDC; // 1101 1100 + + public byte DestinationConnectionIdLength { get; set; } + public IntegerParts DestinationConnectionId { get; set; } + public byte SourceConnectionIdLength { get; set; } + public IntegerParts SourceConnectionId { get; set; } + public IntegerVar TokenLength { get; set; } + public byte[] Token { get; set; } + public IntegerVar Length { get; set; } + public IntegerParts PacketNumber { get; set; } + + public InitialPacket() + { + } + + public InitialPacket(IntegerParts destinationConnectionId, IntegerParts sourceConnectionId) + { + DestinationConnectionIdLength = destinationConnectionId.Size; + DestinationConnectionId = destinationConnectionId; + + SourceConnectionIdLength = sourceConnectionId.Size; + SourceConnectionId = sourceConnectionId; + } + + public override void Decode(byte[] packet) + { + ByteArray array = new ByteArray(packet); + byte type = array.ReadByte(); + // Size of the packet PacketNumber is determined by the last 2 bits of the Type. + int pnSize = (type & 0x03) + 1; + + Version = array.ReadUInt32(); + + 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); + + Length = Length - PacketNumber.Size; + + this.DecodeFrames(array); + } + + public override byte[] Encode() + { + byte[] frames = EncodeFrames(); + + List result = new List(); + result.Add((byte)(Type | (PacketNumber.Size - 1))); + result.AddRange(ByteHelpers.GetBytes(Version)); + + 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); + + result.AddRange(tokenLength); + result.AddRange(length); + result.AddRange(PacketNumber.ToByteArray()); + result.AddRange(frames); + + return result.ToArray(); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Packets/LongHeaderPacket.cs b/EonaCat.Network/System/Quic/Infrastructure/Packets/LongHeaderPacket.cs new file mode 100644 index 0000000..b70938a --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/Packets/LongHeaderPacket.cs @@ -0,0 +1,90 @@ +using System.Collections.Generic; +using EonaCat.Quic.Helpers; + +namespace EonaCat.Quic.Infrastructure.Packets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class LongHeaderPacket : Packet + { + public override byte Type => 0b1100_0000; // 1100 0000 + + public byte DestinationConnectionIdLength { get; set; } + public IntegerParts DestinationConnectionId { get; set; } + public byte SourceConnectionIdLength { get; set; } + public IntegerParts SourceConnectionId { get; set; } + + public PacketType PacketType { get; set; } + + public LongHeaderPacket() + { + } + + public LongHeaderPacket(PacketType packetType, IntegerParts destinationConnectionId, IntegerParts sourceConnectionId) + { + PacketType = packetType; + DestinationConnectionIdLength = destinationConnectionId.Size; + DestinationConnectionId = destinationConnectionId; + + SourceConnectionIdLength = sourceConnectionId.Size; + SourceConnectionId = sourceConnectionId; + } + + public override void Decode(byte[] packet) + { + ByteArray array = new ByteArray(packet); + + byte type = array.ReadByte(); + PacketType = DecodeTypeFiled(type); + + Version = array.ReadUInt32(); + + DestinationConnectionIdLength = array.ReadByte(); + if (DestinationConnectionIdLength > 0) + DestinationConnectionId = array.ReadGranularInteger(DestinationConnectionIdLength); + + SourceConnectionIdLength = array.ReadByte(); + if (SourceConnectionIdLength > 0) + SourceConnectionId = array.ReadGranularInteger(SourceConnectionIdLength); + + this.DecodeFrames(array); + } + + public override byte[] Encode() + { + byte[] frames = EncodeFrames(); + + List result = new List(); + + result.Add(EncodeTypeField()); + result.AddRange(ByteHelpers.GetBytes(Version)); + + 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); + + return result.ToArray(); + } + + private byte EncodeTypeField() + { + byte type = (byte)(Type | ((byte)PacketType << 4) & 0b0011_0000); + + return type; + } + + private PacketType DecodeTypeFiled(byte type) + { + PacketType result = (PacketType)((type & 0b0011_0000) >> 4); + + return result; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Packets/Packet.cs b/EonaCat.Network/System/Quic/Infrastructure/Packets/Packet.cs new file mode 100644 index 0000000..fff6728 --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/Packets/Packet.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using EonaCat.Quic.Helpers; +using EonaCat.Quic.Infrastructure.Exceptions; +using EonaCat.Quic.Infrastructure.Frames; +using EonaCat.Quic.Infrastructure.Settings; + +namespace EonaCat.Quic.Infrastructure.Packets +{ + // 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. + + /// + /// Base data transfer unit of QUIC Transport. + /// + public abstract class Packet + { + protected List _frames = new List(); + public abstract byte Type { get; } + + public UInt32 Version { get; set; } + + public abstract byte[] Encode(); + + public abstract void Decode(byte[] packet); + + public virtual void AttachFrame(Frame frame) + { + _frames.Add(frame); + } + + public virtual List GetFrames() + { + return _frames; + } + + public virtual void DecodeFrames(ByteArray array) + { + FrameParser factory = new FrameParser(array); + Frame result; + int frames = 0; + while (array.HasData() && frames <= QuicSettings.PMTU) + { + 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() + { + List result = new List(); + foreach (Frame frame in _frames) + { + result.AddRange(frame.Encode()); + } + + return result.ToArray(); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Packets/ShortHeaderPacket.cs b/EonaCat.Network/System/Quic/Infrastructure/Packets/ShortHeaderPacket.cs new file mode 100644 index 0000000..e874177 --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/Packets/ShortHeaderPacket.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using EonaCat.Quic.Helpers; + +namespace EonaCat.Quic.Infrastructure.Packets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class ShortHeaderPacket : Packet + { + public byte ActualType = 0b0100_0000; + public override byte Type => 0b0100_0000; + + public IntegerParts DestinationConnectionId { get; set; } + public IntegerParts PacketNumber { get; set; } + + // Field not transferred! Only the connection knows about the length of the ConnectionId + public byte DestinationConnectionIdLength { get; set; } + + public ShortHeaderPacket(byte destinationConnectionIdLength) + { + DestinationConnectionIdLength = destinationConnectionIdLength; + } + + public override void Decode(byte[] packet) + { + ByteArray array = new ByteArray(packet); + byte type = array.ReadByte(); + DestinationConnectionId = array.ReadGranularInteger(DestinationConnectionIdLength); + + int pnSize = (type & 0x03) + 1; + PacketNumber = array.ReadBytes(pnSize); + + DecodeFrames(array); + } + + public override byte[] Encode() + { + byte[] frames = EncodeFrames(); + + List result = new List(); + result.Add((byte)(Type | (PacketNumber.Size - 1))); + result.AddRange(DestinationConnectionId.ToByteArray()); + + byte[] pnBytes = PacketNumber; + result.AddRange(pnBytes); + result.AddRange(frames); + + return result.ToArray(); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Packets/Unpacker.cs b/EonaCat.Network/System/Quic/Infrastructure/Packets/Unpacker.cs new file mode 100644 index 0000000..764706c --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/Packets/Unpacker.cs @@ -0,0 +1,52 @@ +namespace EonaCat.Quic.Infrastructure.Packets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class Unpacker + { + public Packet Unpack(byte[] data) + { + Packet result = null; + + QuicPacketType type = GetPacketType(data); + switch (type) + { + case QuicPacketType.Initial: + result = new InitialPacket(); + break; + + // Should be passed by the QuicConnection to the PacketWireTransfer -> Unpacker + case QuicPacketType.ShortHeader: + result = new ShortHeaderPacket(1); + break; + } + + if (result == null) + return null; + + result.Decode(data); + + return result; + } + + 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; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Packets/VersionNegotiationPacket.cs b/EonaCat.Network/System/Quic/Infrastructure/Packets/VersionNegotiationPacket.cs new file mode 100644 index 0000000..b5d9f71 --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/Packets/VersionNegotiationPacket.cs @@ -0,0 +1,22 @@ +using System; + +namespace EonaCat.Quic.Infrastructure.Packets +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class VersionNegotiationPacket : Packet + { + public override byte Type => throw new NotImplementedException(); + + public override void Decode(byte[] packet) + { + throw new NotImplementedException(); + } + + public override byte[] Encode() + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/QuicPacketType.cs b/EonaCat.Network/System/Quic/Infrastructure/QuicPacketType.cs new file mode 100644 index 0000000..5e05c5d --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/QuicPacketType.cs @@ -0,0 +1,14 @@ +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 QuicPacketType + { + Initial, + LongHeader, + ShortHeader, + VersionNegotiation, + Broken + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Settings/QuicSettings.cs b/EonaCat.Network/System/Quic/Infrastructure/Settings/QuicSettings.cs new file mode 100644 index 0000000..1f86979 --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/Settings/QuicSettings.cs @@ -0,0 +1,55 @@ +namespace EonaCat.Quic.Infrastructure.Settings +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class QuicSettings + { + /// + /// Path Maximum Transmission Unit. Indicates the mandatory initial packet capacity, and the maximum UDP packet capacity. + /// + public const int PMTU = 1200; + + /// + /// Does the server want the first connected client to decide it's initial connection id? + /// + public const bool CanAcceptInitialClientConnectionId = false; + + /// + /// TBD. quic-transport 5.1. + /// + public const int MaximumConnectionIds = 8; + + /// + /// Maximum number of streams that connection can handle. + /// + public const int MaximumStreamId = 128; + + /// + /// Maximum packets that can be transferred before any data transfer (loss of packets, packet resent, infinite ack loop) + /// + public const int MaximumInitialPacketNumber = 100; + + /// + /// Should the server buffer packets that came before the initial packet? + /// + public const bool ShouldBufferPacketsBeforeConnection = false; + + /// + /// Limit the maximum number of frames a packet can carry. + /// + public const int MaximumFramesPerPacket = 10; + + /// + /// Maximum data that can be transferred for a Connection. + /// Currently 10MB. + /// + public const int MaxData = 10 * 1000 * 1000; + + /// + /// Maximum data that can be transferred for a Stream. + /// Currently 0.078125 MB, which is MaxData / MaximumStreamId + /// + public const int MaxStreamData = 78125; + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Settings/QuicVersion.cs b/EonaCat.Network/System/Quic/Infrastructure/Settings/QuicVersion.cs new file mode 100644 index 0000000..418d845 --- /dev/null +++ b/EonaCat.Network/System/Quic/Infrastructure/Settings/QuicVersion.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; + +namespace EonaCat.Quic.Infrastructure.Settings +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class QuicVersion + { + public const int CurrentVersion = 16; + + public static readonly List SupportedVersions = new List() { 15, 16 }; + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/InternalInfrastructure/ConnectionData.cs b/EonaCat.Network/System/Quic/InternalInfrastructure/ConnectionData.cs new file mode 100644 index 0000000..e9d9210 --- /dev/null +++ b/EonaCat.Network/System/Quic/InternalInfrastructure/ConnectionData.cs @@ -0,0 +1,21 @@ +using EonaCat.Quic.Helpers; + +namespace EonaCat.Quic.InternalInfrastructure +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + internal class ConnectionData + { + public PacketWireTransfer PWT { get; set; } + public IntegerParts ConnectionId { get; set; } + public IntegerParts PeerConnectionId { get; set; } + + public ConnectionData(PacketWireTransfer pwt, IntegerParts connectionId, IntegerParts peerConnnectionId) + { + PWT = pwt; + ConnectionId = connectionId; + PeerConnectionId = peerConnnectionId; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/InternalInfrastructure/PacketWireTransfer.cs b/EonaCat.Network/System/Quic/InternalInfrastructure/PacketWireTransfer.cs new file mode 100644 index 0000000..81870da --- /dev/null +++ b/EonaCat.Network/System/Quic/InternalInfrastructure/PacketWireTransfer.cs @@ -0,0 +1,52 @@ +using System.Net; +using System.Net.Sockets; +using EonaCat.Quic.Exceptions; +using EonaCat.Quic.Infrastructure.Packets; + +namespace EonaCat.Quic.InternalInfrastructure +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + internal class PacketWireTransfer + { + private UdpClient _client; + private IPEndPoint _peerEndpoint; + + private Unpacker _unpacker; + + public PacketWireTransfer(UdpClient client, IPEndPoint peerEndpoint) + { + _client = client; + _peerEndpoint = peerEndpoint; + + _unpacker = new Unpacker(); + } + + public Packet ReadPacket() + { + // 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); + + return packet; + } + + public bool SendPacket(Packet packet) + { + byte[] data = packet.Encode(); + + int sent = _client.Send(data, data.Length, _peerEndpoint); + + return sent > 0; + } + + public IPEndPoint LastTransferEndpoint() + { + return _peerEndpoint; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/QuicClient.cs b/EonaCat.Network/System/Quic/QuicClient.cs new file mode 100644 index 0000000..625815a --- /dev/null +++ b/EonaCat.Network/System/Quic/QuicClient.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using EonaCat.Quic.Connections; +using EonaCat.Quic.Exceptions; +using EonaCat.Quic.Helpers; +using EonaCat.Quic.Infrastructure.Frames; +using EonaCat.Quic.Infrastructure.PacketProcessing; +using EonaCat.Quic.Infrastructure.Packets; +using EonaCat.Quic.Infrastructure.Settings; +using EonaCat.Quic.InternalInfrastructure; + +namespace EonaCat.Quic +{ + // 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. + + /// + /// Quic Client. Used for sending and receiving data from a Quic Server. + /// + public class QuicClient : QuicTransport + { + private IPEndPoint _peerIp; + private UdpClient _client; + + private QuicConnection _connection; + private InitialPacketCreator _packetCreator; + + private UInt64 _maximumStreams = QuicSettings.MaximumStreamId; + private PacketWireTransfer _pwt; + + public QuicClient() + { + _client = new UdpClient(); + _packetCreator = new InitialPacketCreator(); + } + + /// + /// Connect to a remote server. + /// + /// Ip Address + /// Port + /// + public QuicConnection Connect(string ip, int port) + { + // Establish socket connection + var ipEntry = Uri.CheckHostName(ip); + IPAddress ipAddress; + if (ipEntry == UriHostNameType.Dns) + { + ipAddress = Dns.GetHostEntry(ip).AddressList?.FirstOrDefault(); + } + else + { + ipAddress = IPAddress.Parse(ip); + } + _peerIp = new IPEndPoint(ipAddress, port); + + // Initialize packet reader + _pwt = new PacketWireTransfer(_client, _peerIp); + + // Start initial protocol process + InitialPacket connectionPacket = _packetCreator.CreateInitialPacket(0, 0); + + // Send the initial packet + _pwt.SendPacket(connectionPacket); + + // Await response for sucessfull connection creation by the server + InitialPacket packet = (InitialPacket)_pwt.ReadPacket(); + + HandleInitialFrames(packet); + EstablishConnection(packet.SourceConnectionId, packet.SourceConnectionId); + + return _connection; + } + + /// + /// Handles initial packet's frames. (In most cases protocol frames) + /// + /// + private void HandleInitialFrames(Packet packet) + { + List frames = packet.GetFrames(); + for (int i = frames.Count - 1; i > 0; i--) + { + Frame frame = frames[i]; + if (frame is ConnectionCloseFrame ccf) + { + throw new ConnectionException(ccf.ReasonPhrase); + } + + if (frame is MaxStreamsFrame msf) + { + _maximumStreams = msf.MaximumStreams.Value; + } + + // Break out if the first Padding Frame has been reached + if (frame is PaddingFrame) + break; + } + } + + /// + /// Create a new connection + /// + /// + /// + private void EstablishConnection(IntegerParts connectionId, IntegerParts peerConnectionId) + { + ConnectionData connection = new ConnectionData(_pwt, connectionId, peerConnectionId); + _connection = new QuicConnection(connection); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/QuicServer.cs b/EonaCat.Network/System/Quic/QuicServer.cs new file mode 100644 index 0000000..db1e251 --- /dev/null +++ b/EonaCat.Network/System/Quic/QuicServer.cs @@ -0,0 +1,154 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using EonaCat.Quic.Connections; +using EonaCat.Quic.Constants; +using EonaCat.Quic.Events; +using EonaCat.Quic.Helpers; +using EonaCat.Quic.Infrastructure; +using EonaCat.Quic.Infrastructure.Frames; +using EonaCat.Quic.Infrastructure.PacketProcessing; +using EonaCat.Quic.Infrastructure.Packets; +using EonaCat.Quic.Infrastructure.Settings; +using EonaCat.Quic.InternalInfrastructure; + +namespace EonaCat.Quic +{ + // 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. + + /// + /// Quic Server - a Quic server that processes incoming connections and if possible sends back data on it's peers. + /// + public class QuicServer : QuicTransport + { + private readonly Unpacker _unpacker; + private readonly InitialPacketCreator _packetCreator; + + private PacketWireTransfer _pwt; + + private UdpClient _client; + + private int _port; + private readonly string _hostname; + private bool _started; + + public event ClientConnectedEvent OnClientConnected; + + /// + /// Create a new instance of QuicListener. + /// + /// The port that the server will listen on. + public QuicServer(string hostName, int port) + { + _started = false; + _port = port; + _hostname = hostName; + + _unpacker = new Unpacker(); + _packetCreator = new InitialPacketCreator(); + } + + /// + /// Starts the listener. + /// + public void Start() + { + var ipEntry = Uri.CheckHostName(_hostname); + IPAddress ipAddress; + if (ipEntry == UriHostNameType.Dns) + { + ipAddress = Dns.GetHostEntry(_hostname).AddressList?.FirstOrDefault(); + } + else + { + ipAddress = IPAddress.Parse(_hostname); + } + + _client = new UdpClient(new IPEndPoint(ipAddress, _port)); + _started = true; + _pwt = new PacketWireTransfer(_client, null); + + while (true) + { + Packet packet = _pwt.ReadPacket(); + if (packet is InitialPacket) + { + QuicConnection connection = ProcessInitialPacket(packet, _pwt.LastTransferEndpoint()); + + OnClientConnected?.Invoke(connection); + } + + if (packet is ShortHeaderPacket) + { + ProcessShortHeaderPacket(packet); + } + } + } + + /// + /// Stops the listener. + /// + public void Close() + { + if (_started) + _client.Close(); + } + + /// + /// Processes incomming initial packet and creates or halts a connection. + /// + /// Initial Packet + /// Peer's endpoint + /// + private QuicConnection ProcessInitialPacket(Packet packet, IPEndPoint endPoint) + { + QuicConnection result = null; + UInt64 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)) + { + VersionNegotiationPacket vnp = _packetCreator.CreateVersionNegotiationPacket(); + data = vnp.Encode(); + + _client.Send(data, data.Length, endPoint); + return null; + } + + InitialPacket cast = packet as InitialPacket; + InitialPacket ip = _packetCreator.CreateInitialPacket(0, cast.SourceConnectionId); + + // Protocol violation if the initial packet is smaller than the PMTU. (pt. 14 / 16th draft) + if (cast.Encode().Length < QuicSettings.PMTU) + { + ip.AttachFrame(new ConnectionCloseFrame(ErrorCode.PROTOCOL_VIOLATION, 0x00, ErrorConstants.PMTUNotReached)); + } + else if (ConnectionPool.AddConnection(new ConnectionData(new PacketWireTransfer(_client, endPoint), cast.SourceConnectionId, 0), out availableConnectionId) == true) + { + // Tell the peer the available connection id + ip.SourceConnectionId = (byte)availableConnectionId; + + // We're including the maximum possible stream id during the connection handshake. (4.5 / 16th draft) + ip.AttachFrame(new MaxStreamsFrame(QuicSettings.MaximumStreamId, StreamType.ServerBidirectional)); + + // Set the return result + result = ConnectionPool.Find(availableConnectionId); + } + else + { + // Not accepting connections. Send initial packet with CONNECTION_CLOSE frame. + // Maximum buffer size should be set in QuicSettings. + ip.AttachFrame(new ConnectionCloseFrame(ErrorCode.CONNECTION_REFUSED, 0x00, ErrorConstants.ServerTooBusy)); + } + + data = ip.Encode(); + int dataSent = _client.Send(data, data.Length, endPoint); + if (dataSent > 0) + return result; + + return null; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/QuicTransport.cs b/EonaCat.Network/System/Quic/QuicTransport.cs new file mode 100644 index 0000000..d77fc28 --- /dev/null +++ b/EonaCat.Network/System/Quic/QuicTransport.cs @@ -0,0 +1,28 @@ +using EonaCat.Quic.Connections; +using EonaCat.Quic.Infrastructure.Packets; + +namespace EonaCat.Quic +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class QuicTransport + { + /// + /// Processes short header packet, by distributing the frames towards connections. + /// + /// + protected void ProcessShortHeaderPacket(Packet packet) + { + ShortHeaderPacket shp = (ShortHeaderPacket)packet; + + QuicConnection connection = ConnectionPool.Find(shp.DestinationConnectionId); + + // No suitable connection found. Discard the packet. + if (connection == null) + return; + + connection.ProcessFrames(shp.GetFrames()); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Streams/QuicStream.cs b/EonaCat.Network/System/Quic/Streams/QuicStream.cs new file mode 100644 index 0000000..f497555 --- /dev/null +++ b/EonaCat.Network/System/Quic/Streams/QuicStream.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using EonaCat.Quic.Connections; +using EonaCat.Quic.Constants; +using EonaCat.Quic.Events; +using EonaCat.Quic.Exceptions; +using EonaCat.Quic.Helpers; +using EonaCat.Quic.Infrastructure.Frames; +using EonaCat.Quic.Infrastructure.Packets; +using EonaCat.Quic.Infrastructure.Settings; + +namespace EonaCat.Quic.Streams +{ + // 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. + + /// + /// Virtual multiplexing channel. + /// + public class QuicStream + { + private SortedList _data = new SortedList(); + private QuicConnection _connection; + private UInt64 _maximumStreamData; + private UInt64 _currentTransferRate; + private UInt64 _sendOffset; + + public StreamState State { get; set; } + public StreamType Type { get; set; } + public StreamId StreamId { get; } + + public StreamDataReceivedEvent OnStreamDataReceived { get; set; } + + public byte[] Data + { + get + { + return _data.SelectMany(v => v.Value).ToArray(); + } + } + + public QuicStream(QuicConnection connection, StreamId streamId) + { + StreamId = streamId; + Type = streamId.Type; + + _maximumStreamData = QuicSettings.MaxStreamData; + _currentTransferRate = 0; + _sendOffset = 0; + + _connection = connection; + } + + public bool Send(byte[] data) + { + if (Type == StreamType.ServerUnidirectional) + throw new StreamException("Cannot send data on unidirectional stream."); + + _connection.IncrementRate(data.Length); + + int numberOfPackets = (data.Length / QuicSettings.PMTU) + 1; + int leftoverCarry = data.Length % QuicSettings.PMTU; + + for (int i = 0; i < numberOfPackets; i++) + { + bool eos = false; + int dataSize = QuicSettings.PMTU; + if (i == numberOfPackets - 1) + { + eos = true; + dataSize = leftoverCarry; + } + + byte[] buffer = new byte[dataSize]; + Buffer.BlockCopy(data, (Int32)_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))); + + if (_connection.MaximumReached()) + packet.AttachFrame(new StreamDataBlockedFrame(StreamId.IntegerValue, (UInt64)data.Length)); + + _sendOffset += (UInt64)buffer.Length; + + _connection.SendData(packet); + } + + return true; + } + + /// + /// Client only! + /// + /// + public byte[] Receive() + { + if (Type == StreamType.ClientUnidirectional) + throw new StreamException("Cannot receive data on unidirectional stream."); + + while (!IsStreamFull() || State == StreamState.Receive) + { + _connection.ReceivePacket(); + } + + return Data; + } + + public void ResetStream(ResetStreamFrame frame) + { + // Reset the state + State = StreamState.ResetReceived; + // Clear data + _data.Clear(); + } + + public void SetMaximumStreamData(UInt64 maximumData) + { + _maximumStreamData = maximumData; + } + + public bool CanSendData() + { + if (Type == StreamType.ServerUnidirectional || Type == StreamType.ClientUnidirectional) + return false; + + if (State == StreamState.Receive || State == StreamState.SizeKnown) + return true; + + return false; + } + + public bool IsOpen() + { + if (State == StreamState.DataReceived || State == StreamState.ResetReceived) + return false; + + return true; + } + + public void ProcessData(StreamFrame frame) + { + // Do not accept data if the stream is reset. + if (State == StreamState.ResetReceived) + return; + + byte[] data = frame.StreamData; + if (frame.Offset != null) + { + _data.Add(frame.Offset.Value, frame.StreamData); + } + else + { + _data.Add(0, frame.StreamData); + } + + // 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; + + // Terminate connection if maximum stream data is reached + if (_currentTransferRate >= _maximumStreamData) + { + ShortHeaderPacket errorPacket = _connection.PacketCreator.CreateConnectionClosePacket(Infrastructure.ErrorCode.FLOW_CONTROL_ERROR, frame.ActualType, ErrorConstants.MaxDataTransfer); + _connection.SendData(errorPacket); + _connection.TerminateConnection(); + + return; + } + + if (State == StreamState.SizeKnown && IsStreamFull()) + { + State = StreamState.DataReceived; + + OnStreamDataReceived?.Invoke(this, Data); + } + } + + public void ProcessStreamDataBlocked(StreamDataBlockedFrame frame) + { + State = StreamState.DataReceived; + } + + private bool IsStreamFull() + { + UInt64 length = 0; + + foreach (var kvp in _data) + { + if (kvp.Key > 0 && kvp.Key != length) + return false; + + length += (UInt64)kvp.Value.Length; + } + + return true; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Streams/StreamState.cs b/EonaCat.Network/System/Quic/Streams/StreamState.cs new file mode 100644 index 0000000..68a6b4c --- /dev/null +++ b/EonaCat.Network/System/Quic/Streams/StreamState.cs @@ -0,0 +1,14 @@ +namespace EonaCat.Quic.Streams +{ + // 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 StreamState + { + Receive, + SizeKnown, + DataReceived, + DataRead, + ResetReceived + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Tcp/TcpClient.cs b/EonaCat.Network/System/Tcp/TcpClient.cs new file mode 100644 index 0000000..119ac4e --- /dev/null +++ b/EonaCat.Network/System/Tcp/TcpClient.cs @@ -0,0 +1,132 @@ +using System; +using System.Net; +using System.Net.Sockets; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class TcpClient + { + protected Socket clientSocket; + + protected byte[] receiveBuffer = new byte[1024]; + + protected int BufferSize = 1024; + + protected byte[] sendBuffer; + + protected bool isSpitePackage; + + protected int restPackage = -1; + + protected int bufferIndex; + + protected int maxSinglePacketSize = 1024; + + public Func OnReceived; + + public TcpClient(string serverIpAddress, int serverIpPort, Func OnReceived, IPType ipType = IPType.IPv4) + { + if (ipType == IPType.IPv6) + { + clientSocket = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp); + } + else + { + clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + } + + IPAddress ipAddress = IPAddress.Parse(serverIpAddress); + + IPEndPoint ipEndpoint = new IPEndPoint(ipAddress, serverIpPort); + + this.OnReceived = OnReceived; + + clientSocket.BeginConnect(ipEndpoint, new AsyncCallback(ConnectAsynCallBack), clientSocket); + } + + private void ConnectAsynCallBack(IAsyncResult ar) + { + Socket socketHandler = (Socket)ar.AsyncState; + + try + { + socketHandler.EndConnect(ar); + socketHandler.BeginReceive( + receiveBuffer, + 0, + BufferSize, + SocketFlags.None, + new AsyncCallback(ReceivedAsynCallBack), + socketHandler + ); + } + catch (Exception e) + { + NetworkHelper.Logger.Error("[TcpClient] The remote computer rejected this request" + e.Message + "\n"); + } + } + + private void ReceivedAsynCallBack(IAsyncResult ar) + { + Socket socketHandler = (Socket)ar.AsyncState; + + int byteLength = socketHandler.EndReceive(ar); + + if (byteLength > 0) + { + if (OnReceived != null) + { + byte[] result = OnReceived(receiveBuffer); + + if (result != null && result.Length > 0) + { + SendDataToServer(result); + } + } + } + + socketHandler.BeginReceive( + receiveBuffer, + 0, + BufferSize, + SocketFlags.None, + new AsyncCallback(ReceivedAsynCallBack), + socketHandler + ); + } + + private void SendAsynCallBack(IAsyncResult ar) + { + try + { + Socket socketHandler = (Socket)ar.AsyncState; + socketHandler.EndSend(ar); + } + catch (Exception e) + { + NetworkHelper.Logger.Exception(e); + } + } + + public void SendDataToServer(byte[] msg) + { + if (!clientSocket.Connected) + { + NetworkHelper.Logger.Error("TcpClient is not connected, cannot send data"); + return; + } + + clientSocket.BeginSend( + msg, + 0, + msg.Length, + 0, + new AsyncCallback(SendAsynCallBack), + clientSocket + ); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Tcp/TcpConnectedPeer.cs b/EonaCat.Network/System/Tcp/TcpConnectedPeer.cs new file mode 100644 index 0000000..c0eeae0 --- /dev/null +++ b/EonaCat.Network/System/Tcp/TcpConnectedPeer.cs @@ -0,0 +1,138 @@ +using System; +using System.Net.Sockets; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class TcpConnectedPeer + { + public string Token; + + /// + /// The callback function when the message is received The return value is directly returned to the client. If null is returned, no reply + /// + public Func ResponseCallBack; + + /// + /// Send a message to the client of this node + /// + /// message. + public void SendDataToClient(byte[] message) + { + try + { + this.SocketHandler.BeginSend( + message, + 0, + message.Length, + SocketFlags.None, + new AsyncCallback(PeerSendCallBack), this.SocketHandler); + } + catch (Exception exception) + { + NetworkHelper.Logger.Exception(exception); + SocketHandler.Close(); + } + } + + public TcpServer Server { get; private set; } + + public Socket SocketHandler { get; private set; } + + public Action OnDisconnected; + + public int BufferSize = 1024; + private byte[] buffer; + + public TcpConnectedPeer( + string token, + Socket socket, + Func OnReceived, + TcpServer fromServer) + { + Token = token; + SocketHandler = socket; + ResponseCallBack = OnReceived; + + buffer = new byte[BufferSize]; + + SocketHandler.BeginReceive( + buffer, + 0, + BufferSize, + SocketFlags.None, + new AsyncCallback(PeerReceiveCallBack), + SocketHandler + ); + } + + private void PeerReceiveCallBack(IAsyncResult ar) + { + Socket _clientHander = (Socket)ar.AsyncState; + + int byteLength = 0; + + byte[] receivedData; + + byteLength = _clientHander.EndReceive(ar); + + receivedData = buffer; + + buffer = null; + + buffer = new byte[BufferSize]; + + if (byteLength > 0) + { + _clientHander.BeginReceive( + buffer, + 0, + BufferSize, + SocketFlags.None, + new AsyncCallback(PeerReceiveCallBack), + _clientHander + ); + } + else + { + SocketHandler.Close(); + OnDisconnected?.Invoke(null); + return; + } + + try + { + if (ResponseCallBack != null) + { + byte[] result = (ResponseCallBack(Token, receivedData)); + + if (result != null && result.Length > 0) + { + SendDataToClient(result); + } + } + } + catch (Exception exception) + { + NetworkHelper.Logger.Exception(exception); + } + } + + private void PeerSendCallBack(IAsyncResult ar) + { + try + { + Socket handler = (Socket)ar.AsyncState; + int SendBytesLength = handler.EndSend(ar); + } + catch (Exception e) + { + SocketHandler.Shutdown(SocketShutdown.Both); + SocketHandler.Close(); + OnDisconnected?.Invoke(e); + } + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Tcp/TcpServer.cs b/EonaCat.Network/System/Tcp/TcpServer.cs new file mode 100644 index 0000000..0ead6a0 --- /dev/null +++ b/EonaCat.Network/System/Tcp/TcpServer.cs @@ -0,0 +1,210 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class TcpServer + { + private readonly List ClientTokens = new List(); + private readonly Token Token = Token.Instance; + + public void BroadCastMessageToAllClients(byte[] msg, string exclusive = "") + { + foreach (string item in ClientTokens) + { + if (Token[item] != null /*&& item!= exclusive*/) + { + Token[item].SendDataToClient(msg); + } + } + } + + public void SendMessageToClient(string clientToken, byte[] msg) + { + if (Token[clientToken] != null) + { + Token[clientToken].SendDataToClient(msg); + } + } + + public void RunServer() + { + IPAddress ipAddress; + + switch (transportProtocol) + { + case IPType.IPv4: + { + ipAddress = IPAddress.Any; + } + break; + + case IPType.IPv6: + { + ipAddress = IPAddress.IPv6Any; + } + break; + + default: + { + NetworkHelper.Logger.Error("Tcp Server IPv4 or IPv6 Setting Error!\n"); + return; + } + } + + if (this.ipAddress != "0.0.0.0") + { + try + { + ipAddress = IPAddress.Parse(this.ipAddress); + } + catch (Exception e) + { + NetworkHelper.Logger.Exception(e, "Tcp Server Wrong Ip"); + } + } + + IPEndPoint ipEndpoint; + + try + { + ipEndpoint = new IPEndPoint(ipAddress, port); + } + catch (Exception e) + { + NetworkHelper.Logger.Exception(e, "Tcp Server Wrong Port"); + return; + } + + switch (transportProtocol) + { + case IPType.IPv4: + { + listener = + new Socket( + AddressFamily.InterNetwork, + SocketType.Stream, + ProtocolType.Tcp); + } + break; + + case IPType.IPv6: + { + listener = + new Socket( + AddressFamily.InterNetworkV6, + SocketType.Stream, + ProtocolType.Tcp); + } + break; + + default: + { + NetworkHelper.Logger.Error("Tcp Server Socket Initialize error (at IPv4 or IPv6)\n"); + return; + } + } + + listener.Bind(ipEndpoint); + + listener.Listen(ConcurrencyVolumn); + + listener.BeginAccept(new AsyncCallback(AcceptCallBack), listener); + } + + public void ShutDownServer() + { + listener.Shutdown(SocketShutdown.Both); + listener.Close(); + + foreach (string client in ClientTokens) + { + try + { + Token[client].SocketHandler.Close(); + Token[client].SocketHandler.Shutdown(SocketShutdown.Both); + } + catch { } + } + + try + { + TearDown?.Invoke(); + } + catch (Exception e) + { + NetworkHelper.Logger.Exception(e); + } + + ClientTokens.Clear(); + + Token.Clear(); + } + + public Action TearDown; + + /// + /// Pass in the client Token + /// + public Action OnClientConnected; + + /// + /// Incoming client Token, message msg + /// + public Func OnClientReceived; + + private void AcceptCallBack(IAsyncResult ar) + { + Socket ListenerHandler = (Socket)ar.AsyncState; + + Socket ClientHandler = ListenerHandler.EndAccept(ar); + + ListenerHandler.BeginAccept(new AsyncCallback(AcceptCallBack), ListenerHandler); + + string token = Token.GenerateGuid(); + + Token[token] = new TcpConnectedPeer(token, ClientHandler, OnClientReceived, this); + + try + { + OnClientConnected?.Invoke(Token[token].Token + $" #{Token.Count}"); + } + catch (Exception e) + { + NetworkHelper.Logger.Exception(e, "Tcp Server AcceptCallBack:::::::"); + } + } + + private readonly string ipAddress; + private readonly int port; + private readonly int ConcurrencyVolumn; + private readonly IPType transportProtocol; + private Socket listener; + + public TcpServer( + string _IPAddress, + int _port, + IPType _transProtocol, + Action OnClientConnected, + Func OnClientReceived, + int _ConcurrencyVolumn) + { + ipAddress = _IPAddress; + + port = _port; + + ConcurrencyVolumn = _ConcurrencyVolumn; + + transportProtocol = _transProtocol; + + this.OnClientConnected = OnClientConnected; + + this.OnClientReceived = OnClientReceived; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Tools/FileReader.cs b/EonaCat.Network/System/Tools/FileReader.cs new file mode 100644 index 0000000..c618411 --- /dev/null +++ b/EonaCat.Network/System/Tools/FileReader.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using System.IO; +using System.Text; + + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + +namespace EonaCat.Network +{ + public class FileReader + { + /// + /// Read a configuration file (varName=varValue format #beginning with comments) + /// + /// Parameter list. + /// File path. + public static Dictionary ReadFile(string filePath) + { + FileReader handler = new FileReader(filePath); + + Dictionary result = handler.Read(); + + handler = null; + + return result; + } + + private readonly StreamReader streamReader; + + public FileReader(string filePath) : this(filePath, NetworkHelper.GlobalEncoding) + { + } + + public FileReader(string filePath, Encoding codingType) + { + streamReader = new StreamReader(filePath, codingType); + } + + // return parameters + public Dictionary Read() + { + Dictionary result = new Dictionary(); + + string line; + + while ((line = streamReader.ReadLine()) != null) + { + if (line.Length > 0) + { + if (line.Substring(0, 1) == @"#") + { + line = ""; + + continue; // annotation line + } + } + else // empty line + { + continue; + } + + string[] strPair = line.Split('='); + + line = ""; + + if (strPair.Length == 2) + { + result.Add( + strPair[0].ToUpper().Replace("\"", "").Replace("\'", "").TrimStart().TrimEnd(), + strPair[1].Replace("\"", "").Replace("\'", "").TrimStart().TrimEnd()); + } + else + { + continue; + } + } + + streamReader.Close(); + + return result; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Tools/FileWriter.cs b/EonaCat.Network/System/Tools/FileWriter.cs new file mode 100644 index 0000000..494f5cc --- /dev/null +++ b/EonaCat.Network/System/Tools/FileWriter.cs @@ -0,0 +1,57 @@ +using System.IO; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class FileWriter + { + /// + /// Write a file + /// + /// File address + /// Write content. + public static void WriteFile(string filePath, string content) + { + FileWriter handler = new FileWriter(filePath); + + handler.Write(content, false); + + handler.Finished(); + + handler = null; + } + + private readonly FileStream fileStream; + private readonly StreamWriter streamWriter; + + public FileWriter(string filePath) + { + fileStream = new FileStream(filePath, FileMode.OpenOrCreate); + + streamWriter = new StreamWriter(fileStream); + } + + public void Write(string content, bool isLine) + { + if (isLine) + { + streamWriter.WriteLine(content); + } + else + { + streamWriter.Write(content); + } + + streamWriter.Flush(); + } + + public void Finished() + { + streamWriter.Close(); + + fileStream.Close(); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Tools/Helpers.cs b/EonaCat.Network/System/Tools/Helpers.cs new file mode 100644 index 0000000..2c60508 --- /dev/null +++ b/EonaCat.Network/System/Tools/Helpers.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Text.RegularExpressions; +using System.Threading; +using EonaCat.Helpers.Extensions; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class Helpers + { + private Helpers() + { } + + /// + /// Port scan + /// + /// the c prefix of IP such as "192.168.0." + /// Start of segment D such as 1. + /// The end of section D such as 255. + /// Detected port. + /// return of the detection result Pass in the IPAndPort terminal class object and the boolean status of whether it is turned on. + public static void IPv4ScanPort(string IPPrefix, int DStart, int DEnd, int port, Action ConnectEvent) + { + // Configure CallBack Event + ConnectedEvent = ConnectEvent; + + // Check + if (!(IPv4Verify(IPPrefix + DStart) && IPv4Verify(IPPrefix + DEnd))) + { + throw new Exception("Wrong Scan Parameters"); + } + + if (DStart > DEnd) + { + int temp = DEnd; + + DEnd = DStart; + + DStart = temp; + } + + // Init + scanThreads = new List(); + + // Scan + for (int i = DStart; i <= DEnd; i++) + { + string ip = IPPrefix + i; + + scanThreads.Add(new Thread(new ParameterizedThreadStart(ScanOne))); + + if (scanThreads.Any()) + { + scanThreads[scanThreads.Count - 1].Start(new IPAndPort(ip, port)); + } + } + + return; + } + + /// + /// Stop all port scanning threads + /// + public static void StopPortScan() + { + if (scanThreads == null) + { + return; + } + + if (scanThreads.Count == 0) + { + return; + } + + foreach (Thread t in scanThreads) + { + if (t == null) + { + continue; + } + + t.Abort(); + } + + scanThreads.Clear(); + } + + private static List scanThreads; + private static Action ConnectedEvent; + + private static void ScanOne(object _para) + { + IPAndPort para = (IPAndPort)_para; + + if (para.port == 0 || para.ip == null || para.ip == string.Empty) + { + NetworkHelper.Logger.Error("Wrong IP or Port"); + return; + } + + Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + + try + { + NetworkHelper.Logger.Debug(string.Format("try to connect {0}: ar {1}", para.ip, para.port)); + socket.Connect(para.ip, para.port); + } + catch (Exception e) + { + NetworkHelper.Logger.Error(string.Format(" {0}: at {1} is close ,error msg :{2}", para.ip, para.port, e.Message)); + } + finally + { + if (ConnectedEvent != null) + { + if (socket.Connected) + { + ConnectedEvent(para, true); + } + else + { + ConnectedEvent(para, false); + } + } + + socket.Close(); + socket = null; + } + } + + public static bool IPv4Verify(string IP) + { + return Regex.IsMatch(IP, @"^((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)$"); + } + + /// + /// Get the internal network IP + /// + /// The local ip. + public static string[] GetLocalIP() + { + string name = Dns.GetHostName(); + + List result = new List(); + + IPAddress[] iPs = Dns.GetHostAddresses(name); + + foreach (IPAddress ip in iPs) + { + result.Add(ip.ToString()); + } + + return result.ToArray(); + } + } + + public class IPAndPort + { + public string ip; + + public int port; + + public IPAndPort(string _ip, int _port) + { + this.ip = _ip; + this.port = _port; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Tools/RegEx.cs b/EonaCat.Network/System/Tools/RegEx.cs new file mode 100644 index 0000000..5625fe7 --- /dev/null +++ b/EonaCat.Network/System/Tools/RegEx.cs @@ -0,0 +1,106 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class RegEx + { + /// + /// Match everything from the source + /// + /// matched data. + /// source text. + /// matches the prefix. + /// match suffix. + public static List FindAll(string sourceData, string regexPrefix, string regexPostFix) + { + return FindAll(sourceData, regexPrefix, regexPostFix, false, true); + } + + /// + /// Match everything from the source. + /// + /// matched data. + /// source text. + /// Regular expression. + /// If set to true Ignore case. + public static List FindAll(string sourceData, string regexPattern, bool ignoreCase) + { + List result = new List(); + + MatchCollection matches; + + if (ignoreCase) + { + matches = Regex.Matches(sourceData, regexPattern, RegexOptions.IgnoreCase); + } + else + { + matches = Regex.Matches(sourceData, regexPattern); + } + + foreach (Match matchItem in matches) + { + result.Add(matchItem.Value); + } + + return result; + } + + /// + /// Match everything from the source + /// + /// matched data. + /// source text. + /// match prefix. + /// match suffix. + /// If set to true only extract numbers. + public static List FindAll(string sourceData, string regexPrefix, string regexPostFix, bool OnlyDigit) + { + return FindAll(sourceData, regexPrefix, regexPostFix, OnlyDigit, true); + } + + /// + /// Match everything from the source + /// + /// matched data. + /// source text. + /// match prefix. + /// match suffix. + /// If set to true only extract numbers. + /// If set to true Ignore case. + public static List FindAll(string sourceData, string regexPreFix, string regexPostFix, bool OnlyDigit, bool ignoreCase) + { + List result = new List(); + + MatchCollection matches; + + if (ignoreCase) + { + matches = Regex.Matches(sourceData, + regexPreFix + (OnlyDigit ? @"(\d*?)" : @"(.*?)") + regexPostFix, + RegexOptions.IgnoreCase); + } + else + { + matches = Regex.Matches(sourceData, + regexPreFix + (OnlyDigit ? @"(\d*?)" : @"(.*?)") + regexPostFix); + } + + foreach (Match matchItem in matches) + { + result.Add(matchItem.Value + .Replace(regexPreFix, "") + .Replace(regexPostFix, "")); + } + + return result; + } + + private RegEx() + { } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Tools/Token.cs b/EonaCat.Network/System/Tools/Token.cs new file mode 100644 index 0000000..1c71de3 --- /dev/null +++ b/EonaCat.Network/System/Tools/Token.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class Token + { + /// + /// Randomly generate a unique Token string + /// + /// The generated token string. + public static string GenerateGuid() + { + lock (_tokens) + { + string guid = Guid.NewGuid().ToString(); + if (_tokens.Keys.ToList().Contains(guid)) + { + return GenerateGuid(); + } + return guid; + } + } + + public int Count => _tokens.Count; + + /// + /// Each token saves a custom object + /// + /// Token. + public TcpConnectedPeer this[string token] + { + get + { + lock (_tokens) + { + if (_tokens.Keys.Contains(token)) + { + return _tokens[token]; + } + else + { + return null; + } + } + } + + set + { + lock (_tokens) + { + if (value != null) + { + _tokens[token] = value; + } + else + if (_tokens.Keys.Contains(token)) + { + _tokens.Remove(token); + } + } + } + } + + /// + /// All the saved token data objects are cleared and the duplicate detection is also reset + /// + public void Clear() + { + _tokens.Clear(); + } + + private static readonly Dictionary _tokens = new Dictionary(); + + private Token() + { } + + private static Token instance; + + public static Token Instance + { + get + { + if (instance == null) + { + instance = new Token(); + } + + return instance; + } + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Udp/Udp.cs b/EonaCat.Network/System/Udp/Udp.cs new file mode 100644 index 0000000..858867f --- /dev/null +++ b/EonaCat.Network/System/Udp/Udp.cs @@ -0,0 +1,156 @@ +using System; +using System.Net; +using System.Net.Sockets; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + public class Udp + { + public Socket socketHandler { get; private set; } + + public Func ResponseCallback; + private IPEndPoint InitEndPoint; + private readonly IPType IPType; + private readonly int bufferSize; + private readonly int port; + + public Udp(int Port = 8085, IPType IpType = IPType.IPv4, Func ResponseCallBack = null, int bufferSize = 1024) + { + ResponseCallback = ResponseCallBack; + + IPType = IpType; + + port = Port; + + this.bufferSize = bufferSize; + + init(); + } + + public void runServer() + { + if (socketHandler == null) + { + NetworkHelper.Logger.Error("IP Type Error at UDPServer Init"); + return; + } + + UdpPeer peerInit = new UdpPeer(socketHandler, bufferSize, IPType); + + socketHandler.BeginReceiveFrom + (peerInit.buffer, 0, bufferSize, SocketFlags.None, + ref peerInit.remoteEndPoint, new AsyncCallback(BeginResponseCallBack), peerInit); + } + + public void StopServer() + { + socketHandler.Close(); + socketHandler = null; + } + + public void SendTo(EndPoint endPoint, byte[] msg) + { + if (socketHandler == null) + { + init(); + } + + this.socketHandler.BeginSendTo + (msg, 0, msg.Length, SocketFlags.None, endPoint, new AsyncCallback(BeginSendToCallBack), null); + } + + private void BeginResponseCallBack(IAsyncResult ar) + { + UdpPeer peer = (UdpPeer)ar.AsyncState; + + int rev = peer.serverSocket.EndReceiveFrom(ar, ref peer.remoteEndPoint); + + if (rev > 0) + { + if (ResponseCallback != null) + { + byte[] result = ResponseCallback(peer.remoteEndPoint, peer.buffer); + + if (result != null && result.Length > 0) + { + SendTo(peer.remoteEndPoint, result); + } + + peer.ResetBuffer(); + } + + socketHandler.BeginReceiveFrom + (peer.buffer, 0, bufferSize, SocketFlags.None, + ref peer.remoteEndPoint, new AsyncCallback(BeginResponseCallBack), peer); + } + } + + private void BeginSendToCallBack(IAsyncResult ar) + { + socketHandler.EndSendTo(ar); + } + + private void init() + { + if (IPType == IPType.IPv4) + { + socketHandler = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + InitEndPoint = new IPEndPoint(IPAddress.Any, port); + } + else if (IPType == IPType.IPv6) + { + socketHandler = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp); + InitEndPoint = new IPEndPoint(IPAddress.IPv6Any, port); + } + socketHandler.Bind(InitEndPoint); + } + } + + public class UdpPeer + { + public EndPoint remoteEndPoint; + public byte[] buffer; + public Socket serverSocket; + private readonly int bufferSize; + private readonly IPType iPType = IPType.IPv4; + + public UdpPeer(Socket _serverSocket, int bufferSize, IPType iPType) + { + serverSocket = _serverSocket; + + this.iPType = iPType; + + if (this.iPType == IPType.IPv4) + { + remoteEndPoint = new IPEndPoint(IPAddress.Any, 0); + } + else + { + remoteEndPoint = new IPEndPoint(IPAddress.IPv6Any, 0); + } + + buffer = new byte[bufferSize]; + + this.bufferSize = bufferSize; + + this.iPType = iPType; + } + + public void ResetBuffer() + { + buffer = new byte[bufferSize]; + + if (iPType == IPType.IPv4) + { + remoteEndPoint = new IPEndPoint(IPAddress.Any, 0); + } + else + { + remoteEndPoint = new IPEndPoint(IPAddress.IPv6Any, 0); + } + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Web/AccessControlManager.cs b/EonaCat.Network/System/Web/AccessControlManager.cs new file mode 100644 index 0000000..b79ab4f --- /dev/null +++ b/EonaCat.Network/System/Web/AccessControlManager.cs @@ -0,0 +1,67 @@ +using System; +using IpMatcher; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Access control manager. Dictates which connections are permitted or denied. + /// + public class AccessControlManager + { + /// + /// Matcher to match denied addresses. + /// + public Matcher DenyList = new Matcher(); + + /// + /// Matcher to match permitted addresses. + /// + public Matcher PermitList = new Matcher(); + + /// + /// Access control mode, either DefaultPermit or DefaultDeny. + /// DefaultPermit: allow everything, except for those explicitly denied. + /// DefaultDeny: deny everything, except for those explicitly permitted. + /// + public AccessControlMode Mode = AccessControlMode.DefaultPermit; + + /// + /// Instantiate the object. + /// + /// Access control mode. + public AccessControlManager(AccessControlMode mode) + { + Mode = mode; + } + + /// + /// Permit or deny a request based on IP address. + /// When operating in 'default deny', only specified entries are permitted. + /// When operating in 'default permit', everything is allowed unless explicitly denied. + /// + /// The IP address to evaluate. + /// True if permitted. + public bool Permit(string ip) + { + if (String.IsNullOrEmpty(ip)) + throw new ArgumentNullException(nameof(ip)); + + switch (Mode) + { + case AccessControlMode.DefaultDeny: + return PermitList.MatchExists(ip); + + case AccessControlMode.DefaultPermit: + if (DenyList.MatchExists(ip)) + return false; + return true; + + default: + throw new ArgumentException("Unknown access control mode: " + Mode.ToString()); + } + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Web/AccessControlMode.cs b/EonaCat.Network/System/Web/AccessControlMode.cs new file mode 100644 index 0000000..519e686 --- /dev/null +++ b/EonaCat.Network/System/Web/AccessControlMode.cs @@ -0,0 +1,27 @@ +using System.Runtime.Serialization; +using EonaCat.Json.Converters; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Access control mode of operation. + /// + [EonaCat.Json.Converter(typeof(StringEnumConverter))] + public enum AccessControlMode + { + /// + /// Permit requests from any endpoint by default. + /// + [EnumMember(Value = "DefaultPermit")] + DefaultPermit, + + /// + /// Deny requests from any endpoint by default. + /// + [EnumMember(Value = "DefaultDeny")] + DefaultDeny + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Web/Chunk.cs b/EonaCat.Network/System/Web/Chunk.cs new file mode 100644 index 0000000..36160ef --- /dev/null +++ b/EonaCat.Network/System/Web/Chunk.cs @@ -0,0 +1,35 @@ +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// A chunk of data, used when reading from a request where the Transfer-Encoding header includes 'chunked'. + /// + public class Chunk + { + /// + /// Length of the data. + /// + public int Length = 0; + + /// + /// Data. + /// + public byte[] Data = null; + + /// + /// Any additional metadata that appears on the length line after the length hex value and semicolon. + /// + public string Metadata = null; + + /// + /// Indicates whether or not this is the final chunk, i.e. the chunk length received was zero. + /// + public bool IsFinalChunk = false; + + internal Chunk() + { + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Web/ConnectionReceivedEventArgs.cs b/EonaCat.Network/System/Web/ConnectionReceivedEventArgs.cs new file mode 100644 index 0000000..5353321 --- /dev/null +++ b/EonaCat.Network/System/Web/ConnectionReceivedEventArgs.cs @@ -0,0 +1,38 @@ +using System; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Connection event arguments. + /// + public class ConnectionEventArgs : EventArgs + { + /// + /// Requestor IP address. + /// + public string Ip { get; private set; } = null; + + /// + /// Request TCP port. + /// + public int Port { get; private set; } = 0; + + /// + /// Connection event arguments. + /// + /// Requestor IP address. + /// Request TCP port. + public ConnectionEventArgs(string ip, int port) + { + if (String.IsNullOrEmpty(ip)) + throw new ArgumentNullException(nameof(ip)); + if (port < 0) + throw new ArgumentOutOfRangeException(nameof(port)); + Ip = ip; + Port = port; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Web/ContentRoute.cs b/EonaCat.Network/System/Web/ContentRoute.cs new file mode 100644 index 0000000..7b4ff8b --- /dev/null +++ b/EonaCat.Network/System/Web/ContentRoute.cs @@ -0,0 +1,58 @@ +using System; +using EonaCat.Json; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Assign a method handler for when requests are received matching the supplied method and path. + /// + public class ContentRoute + { + /// + /// Globally-unique identifier. + /// + [JsonProperty(Order = -1)] + public string GUID { get; set; } = Guid.NewGuid().ToString(); + + /// + /// The pattern against which the raw URL should be matched. + /// + [JsonProperty(Order = 0)] + public string Path { get; set; } = null; + + /// + /// Indicates whether or not the path specifies a directory. If so, any matching URL will be handled by the specified handler. + /// + [JsonProperty(Order = 1)] + public bool IsDirectory { get; set; } = false; + + /// + /// User-supplied metadata. + /// + [JsonProperty(Order = 999)] + public object Metadata { get; set; } = null; + + /// + /// Create a new route object. + /// + /// The pattern against which the raw URL should be matched. + /// Indicates whether or not the path specifies a directory. If so, any matching URL will be handled by the specified handler. + /// Globally-unique identifier. + /// User-supplied metadata. + public ContentRoute(string path, bool isDirectory, string guid = null, object metadata = null) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); + Path = path.ToLower(); + IsDirectory = isDirectory; + + 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 new file mode 100644 index 0000000..99bcb39 --- /dev/null +++ b/EonaCat.Network/System/Web/ContentRouteManager.cs @@ -0,0 +1,231 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Content route manager. Content routes are used for GET and HEAD requests to specific files or entire directories. + /// + public class ContentRouteManager + { + /// + /// Base directory for files and directories accessible via content routes. + /// + public string BaseDirectory + { + get + { + return _BaseDirectory; + } + set + { + 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."); + _BaseDirectory = value; + } + } + } + + private List _Routes = new List(); + private readonly object _Lock = new object(); + private string _BaseDirectory = AppDomain.CurrentDomain.BaseDirectory; + + /// + /// Instantiate the object. + /// + public ContentRouteManager() + { + } + + /// + /// Add a route. + /// + /// URL path, i.e. /path/to/resource. + /// True if the path represents a directory. + /// Globally-unique identifier. + /// User-supplied metadata. + public void Add(string path, bool isDirectory, string guid = null, object metadata = null) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); + Add(new ContentRoute(path, isDirectory, guid, metadata)); + } + + /// + /// Remove a route. + /// + /// URL path. + public void Remove(string path) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); + + ContentRoute r = Get(path); + if (r == null) + return; + + lock (_Lock) + { + _Routes.Remove(r); + } + + return; + } + + /// + /// Retrieve a content route. + /// + /// URL path. + /// ContentRoute if the route exists, otherwise null. + public ContentRoute Get(string 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) + { + foreach (ContentRoute curr in _Routes) + { + if (curr.IsDirectory) + { + if (path.StartsWith(curr.Path.ToLower())) + return curr; + } + else + { + if (path.Equals(curr.Path.ToLower())) + return curr; + } + } + + return null; + } + } + + /// + /// Check if a content route exists. + /// + /// URL path. + /// True if exists. + public bool Exists(string path) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); + + path = path.ToLower(); + if (!path.StartsWith("/")) + path = "/" + path; + + lock (_Lock) + { + foreach (ContentRoute curr in _Routes) + { + if (curr.IsDirectory) + { + if (path.StartsWith(curr.Path.ToLower())) + return true; + } + else + { + if (path.Equals(curr.Path.ToLower())) + return true; + } + } + } + + return false; + } + + /// + /// Retrieve a content route. + /// + /// URL path. + /// Matching route. + /// True if a match exists. + public bool Match(string path, out ContentRoute route) + { + route = null; + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); + + path = path.ToLower(); + string dirPath = path; + if (!dirPath.EndsWith("/")) + dirPath = dirPath + "/"; + + lock (_Lock) + { + foreach (ContentRoute curr in _Routes) + { + if (curr.IsDirectory) + { + if (dirPath.StartsWith(curr.Path.ToLower())) + { + route = curr; + return true; + } + } + else + { + if (path.Equals(curr.Path.ToLower())) + { + route = curr; + return true; + } + } + } + + return false; + } + } + + 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)) + { + return; + } + + lock (_Lock) + { + _Routes.Add(route); + } + } + + private void Remove(ContentRoute route) + { + if (route == null) + throw new ArgumentNullException(nameof(route)); + + lock (_Lock) + { + _Routes.Remove(route); + } + + return; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Web/ContentRouteProcessor.cs b/EonaCat.Network/System/Web/ContentRouteProcessor.cs new file mode 100644 index 0000000..8f6d8b4 --- /dev/null +++ b/EonaCat.Network/System/Web/ContentRouteProcessor.cs @@ -0,0 +1,142 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Content route handler. Handles GET and HEAD requests to content routes for files and directories. + /// + public class ContentRouteHandler + { + /// + /// The FileMode value to use when accessing files within a content route via a FileStream. Default is FileMode.Open. + /// + public FileMode ContentFileMode = FileMode.Open; + + /// + /// The FileAccess value to use when accessing files within a content route via a FileStream. Default is FileAccess.Read. + /// + public FileAccess ContentFileAccess = FileAccess.Read; + + /// + /// The FileShare value to use when accessing files within a content route via a FileStream. Default is FileShare.Read. + /// + public FileShare ContentFileShare = FileShare.Read; + + private ContentRouteManager _Routes; + + internal ContentRouteHandler(ContentRouteManager routes) + { + if (routes == null) + throw new ArgumentNullException(nameof(routes)); + + _Routes = routes; + } + + 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) + { + Set500Response(ctx); + await ctx.Response.Send(token).ConfigureAwait(false); + return; + } + + string filePath = ctx.Request.Url.RawWithoutQuery; + 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", " "); + + string contentType = GetContentType(filePath); + + if (!File.Exists(filePath)) + { + Set404Response(ctx); + await ctx.Response.Send(token).ConfigureAwait(false); + return; + } + + FileInfo fi = new FileInfo(filePath); + long contentLength = fi.Length; + + if (ctx.Request.Method == HttpMethod.GET) + { + FileStream fs = new FileStream(filePath, ContentFileMode, ContentFileAccess, ContentFileShare); + ctx.Response.StatusCode = 200; + ctx.Response.ContentLength = contentLength; + ctx.Response.ContentType = GetContentType(filePath); + await ctx.Response.Send(contentLength, fs, token).ConfigureAwait(false); + return; + } + else if (ctx.Request.Method == HttpMethod.HEAD) + { + ctx.Response.StatusCode = 200; + ctx.Response.ContentLength = contentLength; + ctx.Response.ContentType = GetContentType(filePath); + await ctx.Response.Send(contentLength, token).ConfigureAwait(false); + return; + } + else + { + Set500Response(ctx); + await ctx.Response.Send(token).ConfigureAwait(false); + return; + } + } + + private string GetContentType(string path) + { + if (String.IsNullOrEmpty(path)) + return "application/octet-stream"; + + int idx = path.LastIndexOf("."); + if (idx >= 0) + { + return MimeTypes.GetFromExtension(path.Substring(idx)); + } + + return "application/octet-stream"; + } + + private void Set204Response(HttpContext ctx) + { + ctx.Response.StatusCode = 204; + ctx.Response.ContentLength = 0; + } + + private void Set404Response(HttpContext ctx) + { + ctx.Response.StatusCode = 404; + ctx.Response.ContentLength = 0; + } + + private void Set500Response(HttpContext ctx) + { + ctx.Response.StatusCode = 500; + ctx.Response.ContentLength = 0; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Web/DynamicRoute.cs b/EonaCat.Network/System/Web/DynamicRoute.cs new file mode 100644 index 0000000..0f1e20f --- /dev/null +++ b/EonaCat.Network/System/Web/DynamicRoute.cs @@ -0,0 +1,71 @@ +using System; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using EonaCat.Json; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Assign a method handler for when requests are received matching the supplied method and path regex. + /// + public class DynamicRoute + { + /// + /// Globally-unique identifier. + /// + [JsonProperty(Order = -1)] + public string GUID { get; set; } = Guid.NewGuid().ToString(); + + /// + /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + /// + [JsonProperty(Order = 0)] + public HttpMethod Method { get; set; } = HttpMethod.GET; + + /// + /// The pattern against which the raw URL should be matched. + /// + [JsonProperty(Order = 1)] + public Regex Path { get; set; } = null; + + /// + /// The handler for the dynamic route. + /// + [JsonIgnore] + public Func Handler { get; set; } = null; + + /// + /// User-supplied metadata. + /// + [JsonProperty(Order = 999)] + public object Metadata { get; set; } = null; + + /// + /// Create a new route object. + /// + /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + /// The pattern against which the raw URL should be matched. + /// The method that should be called to handle the request. + /// Globally-unique identifier. + /// User-supplied metadata. + 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)) + GUID = guid; + if (metadata != null) + Metadata = metadata; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Web/DynamicRouteAttribute.cs b/EonaCat.Network/System/Web/DynamicRouteAttribute.cs new file mode 100644 index 0000000..dbef73b --- /dev/null +++ b/EonaCat.Network/System/Web/DynamicRouteAttribute.cs @@ -0,0 +1,53 @@ +using System; +using System.Text.RegularExpressions; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Attribute that is used to mark methods as a dynamic route. + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class DynamicRouteAttribute : Attribute + { + /// + /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + /// + public HttpMethod Method = HttpMethod.GET; + + /// + /// The pattern against which the raw URL should be matched. Must be convertible to a regular expression. + /// + public Regex Path = null; + + /// + /// Globally-unique identifier. + /// + public string GUID { get; set; } = Guid.NewGuid().ToString(); + + /// + /// User-supplied metadata. + /// + public object Metadata { get; set; } = null; + + /// + /// Instantiate the object. + /// + /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + /// The regular expression pattern against which the raw URL should be matched. + /// Globally-unique identifier. + /// User-supplied metadata. + public DynamicRouteAttribute(HttpMethod method, string path, string guid = null, object metadata = null) + { + Path = new Regex(path); + Method = method; + + 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 new file mode 100644 index 0000000..e91a5a0 --- /dev/null +++ b/EonaCat.Network/System/Web/DynamicRouteManager.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using RegexMatcher; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Dynamic route manager. Dynamic routes are used for requests using any HTTP method to any path that can be matched by regular expression. + /// + public class DynamicRouteManager + { + /// + /// 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; + } + } + + private Matcher _Matcher = new Matcher(); + private readonly object _Lock = new object(); + private Dictionary> _Routes = new Dictionary>(); + + /// + /// Instantiate the object. + /// + public DynamicRouteManager() + { + _Matcher.MatchPreference = MatchPreferenceType.LongestFirst; + } + + /// + /// Add a route. + /// + /// The HTTP method. + /// URL path, i.e. /path/to/resource. + /// Method to invoke. + /// Globally-unique identifier. + /// User-supplied metadata. + 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( + new Regex(BuildConsolidatedRegex(method, path)), + dr); + + _Routes.Add(new DynamicRoute(method, path, handler, guid, metadata), handler); + } + } + + /// + /// Remove a route. + /// + /// The HTTP method. + /// URL path. + public void Remove(HttpMethod method, Regex path) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + + lock (_Lock) + { + _Matcher.Remove( + new Regex(BuildConsolidatedRegex(method, path))); + + if (_Routes.Any(r => r.Key.Method == method && r.Key.Path.Equals(path))) + { + List removeList = _Routes.Where(r => r.Key.Method == method && r.Key.Path.Equals(path)) + .Select(r => r.Key) + .ToList(); + + foreach (DynamicRoute remove in removeList) + { + _Routes.Remove(remove); + } + } + } + } + + /// + /// Check if a content route exists. + /// + /// The HTTP method. + /// URL path. + /// True if exists. + public bool Exists(HttpMethod method, Regex path) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + + lock (_Lock) + { + return _Routes.Any(r => r.Key.Method == method && r.Key.Path.Equals(path)); + } + } + + /// + /// Match a request method and URL to a handler method. + /// + /// The HTTP method. + /// URL path. + /// Matching route. + /// Method to invoke. + public Func Match(HttpMethod method, string rawUrl, out DynamicRoute dr) + { + dr = null; + if (String.IsNullOrEmpty(rawUrl)) + throw new ArgumentNullException(nameof(rawUrl)); + + object val = null; + + if (_Matcher.Match( + BuildConsolidatedRegex(method, rawUrl), + out val)) + { + if (val == null) + { + return null; + } + else + { + lock (_Lock) + { + dr = (DynamicRoute)val; + return dr.Handler; + } + } + } + + return null; + } + + private string BuildConsolidatedRegex(HttpMethod method, string rawUrl) + { + rawUrl = rawUrl.Replace("^", ""); + return method.ToString() + " " + rawUrl; + } + + private string BuildConsolidatedRegex(HttpMethod method, Regex path) + { + string pathString = path.ToString().Replace("^", ""); + return method.ToString() + " " + pathString; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Web/EonaCatWebserver.cs b/EonaCat.Network/System/Web/EonaCatWebserver.cs new file mode 100644 index 0000000..e4fce9d --- /dev/null +++ b/EonaCat.Network/System/Web/EonaCatWebserver.cs @@ -0,0 +1,595 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// EonaCat Webserver. + /// + public class WebServer : IDisposable + { + /// + /// Indicates whether or not the server is listening. + /// + public bool IsListening + { + get + { + return (_HttpListener != null) ? _HttpListener.IsListening : false; + } + } + + /// + /// Number of requests being serviced currently. + /// + public int RequestCount + { + get + { + return _RequestCount; + } + } + + /// + /// EonaCat webserver settings. + /// + public EonaCatWebserverSettings Settings + { + get + { + return _Settings; + } + set + { + if (value == null) + _Settings = new EonaCatWebserverSettings(); + else + _Settings = value; + } + } + + /// + /// EonaCat webserver routes. + /// + public EonaCatWebserverRoutes Routes + { + get + { + return _Routes; + } + set + { + if (value == null) + _Routes = new EonaCatWebserverRoutes(); + else + _Routes = value; + } + } + + /// + /// EonaCat webserver statistics. + /// + public EonaCatWebserverStatistics Statistics { get; private set; } = new EonaCatWebserverStatistics(); + + /// + /// Set specific actions/callbacks to use when events are raised. + /// + public EonaCatWebserverEvents Events { get; private set; } = new EonaCatWebserverEvents(); + + /// + /// Default pages served by the EonaCat webserver. + /// + public EonaCatWebserverPages Pages { get; private set; } = new EonaCatWebserverPages(); + + private string _Header = "[EonaCat] "; + private Assembly _Assembly = Assembly.GetCallingAssembly(); + private EonaCatWebserverSettings _Settings = new EonaCatWebserverSettings(); + private EonaCatWebserverRoutes _Routes = new EonaCatWebserverRoutes(); + private HttpListener _HttpListener = new HttpListener(); + private int _RequestCount = 0; + + private CancellationTokenSource _TokenSource = new CancellationTokenSource(); + private CancellationToken _Token; + private Task _AcceptConnections = null; + + /// + /// Creates a new instance of the EonaCat webserver. + /// If you do not provide a settings object, default settings will be used, which will cause EonaCat Webserver to listen on http://127.0.0.1:8000, and send events to the console. + /// + /// EonaCat webserver settings. + /// 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(EonaCatWebserverSettings settings = null, Func defaultRoute = null) + { + if (settings == null) + { + settings = new EonaCatWebserverSettings(); + settings.Prefixes.Add("http://127.0.0.1:8000/"); + Events.Logger = Console.WriteLine; + } + + _Settings = settings; + _Routes.Default = defaultRoute; + } + + /// + /// Creates a new instance of the EonaCat webserver. + /// + /// Hostname or IP address on which to listen. + /// TCP port on which to listen. + /// Specify whether or not SSL should be used (HTTPS). + /// 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)) + hostname = "localhost"; + if (port < 1) + throw new ArgumentOutOfRangeException(nameof(port)); + + _Settings = new EonaCatWebserverSettings(hostname, port, ssl); + _Routes.Default = defaultRoute; + } + + /// + /// Creates a new instance of the EonaCat webserver. + /// + /// Hostnames or IP addresses on which to listen. Note: multiple listener endpoints are not supported on all platforms. + /// TCP port on which to listen. + /// Specify whether or not SSL should be used (HTTPS). + /// 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(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; + } + + /// + /// Tear down the server and dispose of background workers. + /// Do not use this object after disposal. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Start accepting new connections. + /// + /// Cancellation token useful for canceling the server. + public void Start(CancellationToken token = default) + { + if (_HttpListener != null && _HttpListener.IsListening) + throw new InvalidOperationException("EonaCat Webserver is already listening."); + + _HttpListener = new HttpListener(); + + LoadRoutes(); + Statistics = new EonaCatWebserverStatistics(); + + _TokenSource = CancellationTokenSource.CreateLinkedTokenSource(token); + _Token = token; + + foreach (string prefix in _Settings.Prefixes) + { + _HttpListener.Prefixes.Add(prefix); + } + + _HttpListener.Start(); + _AcceptConnections = Task.Run(() => AcceptConnections(_Token), _Token); + } + + /// + /// Start accepting new connections. + /// + /// Cancellation token useful for canceling the server. + /// Task. + public Task StartAsync(CancellationToken token = default) + { + if (_HttpListener != null && _HttpListener.IsListening) + throw new InvalidOperationException("EonaCat Webserver is already listening."); + + _HttpListener = new HttpListener(); + + LoadRoutes(); + Statistics = new EonaCatWebserverStatistics(); + + _TokenSource = CancellationTokenSource.CreateLinkedTokenSource(token); + _Token = token; + + foreach (string prefix in _Settings.Prefixes) + { + _HttpListener.Prefixes.Add(prefix); + } + + _HttpListener.Start(); + _AcceptConnections = Task.Run(() => AcceptConnections(_Token), _Token); + return _AcceptConnections; + } + + /// + /// Stop accepting new connections. + /// + public void Stop() + { + if (!_HttpListener.IsListening) + throw new InvalidOperationException("EonaCat Webserver is already stopped."); + + if (_HttpListener != null && _HttpListener.IsListening) + { + _HttpListener.Stop(); + } + + if (_TokenSource != null && !_TokenSource.IsCancellationRequested) + { + _TokenSource.Cancel(); + } + } + + /// + /// Tear down the server and dispose of background workers. + /// Do not use this object after disposal. + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (_HttpListener != null && _HttpListener.IsListening) + { + Stop(); + + _HttpListener.Close(); + } + + Events.HandleServerDisposing(this, EventArgs.Empty); + + _HttpListener = null; + _Settings = null; + _Routes = null; + _TokenSource = null; + _AcceptConnections = null; + + Events = null; + Statistics = null; + } + } + + private void LoadRoutes() + { + var staticRoutes = _Assembly + .GetTypes() // Get all classes from assembly + .SelectMany(x => x.GetMethods()) // Get all methods from assembly + .Where(IsStaticRoute); // Only select methods that are valid routes + + var parameterRoutes = _Assembly + .GetTypes() // Get all classes from assembly + .SelectMany(x => x.GetMethods()) // Get all methods from assembly + .Where(IsParameterRoute); // Only select methods that are valid routes + + var dynamicRoutes = _Assembly + .GetTypes() // Get all classes from assembly + .SelectMany(x => x.GetMethods()) // Get all methods from assembly + .Where(IsDynamicRoute); // Only select methods that are valid routes + + foreach (var staticRoute in staticRoutes) + { + var attribute = staticRoute.GetCustomAttributes().OfType().First(); + if (!_Routes.Static.Exists(attribute.Method, attribute.Path)) + { + Events.Logger?.Invoke(_Header + "adding static route " + attribute.Method.ToString() + " " + attribute.Path); + _Routes.Static.Add(attribute.Method, attribute.Path, ToRouteMethod(staticRoute), attribute.GUID, attribute.Metadata); + } + } + + foreach (var parameterRoute in parameterRoutes) + { + var attribute = parameterRoute.GetCustomAttributes().OfType().First(); + if (!_Routes.Parameter.Exists(attribute.Method, attribute.Path)) + { + Events.Logger?.Invoke(_Header + "adding parameter route " + attribute.Method.ToString() + " " + attribute.Path); + _Routes.Parameter.Add(attribute.Method, attribute.Path, ToRouteMethod(parameterRoute), attribute.GUID, attribute.Metadata); + } + } + + foreach (var dynamicRoute in dynamicRoutes) + { + var attribute = dynamicRoute.GetCustomAttributes().OfType().First(); + if (!_Routes.Dynamic.Exists(attribute.Method, attribute.Path)) + { + Events.Logger?.Invoke(_Header + "adding dynamic route " + attribute.Method.ToString() + " " + attribute.Path); + _Routes.Dynamic.Add(attribute.Method, attribute.Path, ToRouteMethod(dynamicRoute), attribute.GUID, attribute.Metadata); + } + } + } + + private bool IsStaticRoute(MethodInfo method) + { + return method.GetCustomAttributes().OfType().Any() + && method.ReturnType == typeof(Task) + && method.GetParameters().Length == 1 + && method.GetParameters().First().ParameterType == typeof(HttpContext); + } + + private bool IsParameterRoute(MethodInfo method) + { + return method.GetCustomAttributes().OfType().Any() + && method.ReturnType == typeof(Task) + && method.GetParameters().Length == 1 + && method.GetParameters().First().ParameterType == typeof(HttpContext); + } + + private bool IsDynamicRoute(MethodInfo method) + { + return method.GetCustomAttributes().OfType().Any() + && method.ReturnType == typeof(Task) + && method.GetParameters().Length == 1 + && method.GetParameters().First().ParameterType == typeof(HttpContext); + } + + private Func ToRouteMethod(MethodInfo method) + { + if (method.IsStatic) + { + return (Func)Delegate.CreateDelegate(typeof(Func), method); + } + else + { + object instance = Activator.CreateInstance(method.DeclaringType ?? throw new Exception("Declaring class is null")); + return (Func)Delegate.CreateDelegate(typeof(Func), instance, method); + } + } + + private async Task AcceptConnections(CancellationToken token) + { + try + { + while (_HttpListener.IsListening) + { + if (_RequestCount >= _Settings.IO.MaxRequests) + { + await Task.Delay(100, token).ConfigureAwait(false); + continue; + } + + HttpListenerContext listenerCtx = await _HttpListener.GetContextAsync().ConfigureAwait(false); + Interlocked.Increment(ref _RequestCount); + HttpContext ctx = null; + + Task unawaited = Task.Run(async () => + { + DateTime startTime = DateTime.Now; + + try + { + Events.HandleConnectionReceived(this, new ConnectionEventArgs( + listenerCtx.Request.RemoteEndPoint.Address.ToString(), + listenerCtx.Request.RemoteEndPoint.Port)); + + ctx = new HttpContext(listenerCtx, _Settings, Events); + + Events.HandleRequestReceived(this, new RequestEventArgs(ctx)); + + if (_Settings.Debug.Requests) + { + Events.Logger?.Invoke( + _Header + ctx.Request.Source.IpAddress + ":" + ctx.Request.Source.Port + " " + + ctx.Request.Method.ToString() + " " + ctx.Request.Url.RawWithoutQuery); + } + + Statistics.IncrementRequestCounter(ctx.Request.Method); + Statistics.IncrementReceivedPayloadBytes(ctx.Request.ContentLength); + + if (!_Settings.AccessControl.Permit(ctx.Request.Source.IpAddress)) + { + Events.HandleRequestDenied(this, new RequestEventArgs(ctx)); + + if (_Settings.Debug.AccessControl) + { + Events.Logger?.Invoke(_Header + ctx.Request.Source.IpAddress + ":" + ctx.Request.Source.Port + " denied due to access control"); + } + + listenerCtx.Response.StatusCode = 403; + listenerCtx.Response.Close(); + return; + } + + if (ctx.Request.Method == HttpMethod.OPTIONS) + { + if (_Routes.Preflight != null) + { + if (_Settings.Debug.Routing) + { + Events.Logger?.Invoke( + _Header + "preflight route for " + ctx.Request.Source.IpAddress + ":" + ctx.Request.Source.Port + " " + + ctx.Request.Method.ToString() + " " + ctx.Request.Url.RawWithoutQuery); + } + + await _Routes.Preflight(ctx).ConfigureAwait(false); + return; + } + } + + bool terminate = false; + if (_Routes.PreRouting != null) + { + terminate = await _Routes.PreRouting(ctx).ConfigureAwait(false); + if (terminate) + { + if (_Settings.Debug.Routing) + { + Events.Logger?.Invoke( + _Header + "prerouting terminated connection for " + ctx.Request.Source.IpAddress + ":" + ctx.Request.Source.Port + " " + + ctx.Request.Method.ToString() + " " + ctx.Request.Url.RawWithoutQuery); + } + + return; + } + } + + if (ctx.Request.Method == HttpMethod.GET || ctx.Request.Method == HttpMethod.HEAD) + { + ContentRoute cr = null; + if (_Routes.Content.Match(ctx.Request.Url.RawWithoutQuery, out cr)) + { + if (_Settings.Debug.Routing) + { + Events.Logger?.Invoke( + _Header + "content route for " + ctx.Request.Source.IpAddress + ":" + ctx.Request.Source.Port + " " + + ctx.Request.Method.ToString() + " " + ctx.Request.Url.RawWithoutQuery); + } + + ctx.RouteType = RouteTypeEnum.Content; + ctx.Route = cr; + await _Routes.ContentHandler.Process(ctx, token).ConfigureAwait(false); + return; + } + } + + StaticRoute sr = null; + Func handler = _Routes.Static.Match(ctx.Request.Method, ctx.Request.Url.RawWithoutQuery, out sr); + if (handler != null) + { + if (_Settings.Debug.Routing) + { + Events.Logger?.Invoke( + _Header + "static route for " + ctx.Request.Source.IpAddress + ":" + ctx.Request.Source.Port + " " + + ctx.Request.Method.ToString() + " " + ctx.Request.Url.RawWithoutQuery); + } + + ctx.RouteType = RouteTypeEnum.Static; + ctx.Route = sr; + await handler(ctx).ConfigureAwait(false); + return; + } + + ParameterRoute pr = null; + Dictionary parameters = null; + handler = _Routes.Parameter.Match(ctx.Request.Method, ctx.Request.Url.RawWithoutQuery, out parameters, out pr); + if (handler != null) + { + ctx.Request.Url.Parameters = new Dictionary(parameters); + + if (_Settings.Debug.Routing) + { + Events.Logger?.Invoke( + _Header + "parameter route for " + ctx.Request.Source.IpAddress + ":" + ctx.Request.Source.Port + " " + + ctx.Request.Method.ToString() + " " + ctx.Request.Url.RawWithoutQuery); + } + + ctx.RouteType = RouteTypeEnum.Parameter; + ctx.Route = pr; + await handler(ctx).ConfigureAwait(false); + return; + } + + DynamicRoute dr = null; + handler = _Routes.Dynamic.Match(ctx.Request.Method, ctx.Request.Url.RawWithoutQuery, out dr); + if (handler != null) + { + if (_Settings.Debug.Routing) + { + Events.Logger?.Invoke( + _Header + "dynamic route for " + ctx.Request.Source.IpAddress + ":" + ctx.Request.Source.Port + " " + + ctx.Request.Method.ToString() + " " + ctx.Request.Url.RawWithoutQuery); + } + + ctx.RouteType = RouteTypeEnum.Dynamic; + ctx.Route = dr; + await handler(ctx).ConfigureAwait(false); + return; + } + + if (_Routes.Default != null) + { + if (_Settings.Debug.Routing) + { + Events.Logger?.Invoke( + _Header + "default route for " + ctx.Request.Source.IpAddress + ":" + ctx.Request.Source.Port + " " + + ctx.Request.Method.ToString() + " " + ctx.Request.Url.RawWithoutQuery); + } + + ctx.RouteType = RouteTypeEnum.Default; + await _Routes.Default(ctx).ConfigureAwait(false); + return; + } + else + { + if (_Settings.Debug.Routing) + { + Events.Logger?.Invoke( + _Header + "default route not found for " + ctx.Request.Source.IpAddress + ":" + ctx.Request.Source.Port + " " + + ctx.Request.Method.ToString() + " " + ctx.Request.Url.RawWithoutQuery); + } + + ctx.Response.StatusCode = 404; + ctx.Response.ContentType = Pages.Default404Page.ContentType; + await ctx.Response.Send(Pages.Default404Page.Content).ConfigureAwait(false); + return; + } + } + catch (Exception eInner) + { + ctx.Response.StatusCode = 500; + ctx.Response.ContentType = Pages.Default500Page.ContentType; + await ctx.Response.Send(Pages.Default500Page.Content).ConfigureAwait(false); + Events.HandleExceptionEncountered(this, new ExceptionEventArgs(ctx, eInner)); + } + finally + { + Interlocked.Decrement(ref _RequestCount); + + if (ctx != null && ctx.Response != null && ctx.Response.ResponseSent) + { + Events.HandleResponseSent(this, new ResponseEventArgs(ctx, TotalMsFrom(startTime))); + Statistics.IncrementSentPayloadBytes(ctx.Response.ContentLength); + } + } + }, token); + } + } + catch (TaskCanceledException) + { + } + catch (OperationCanceledException) + { + } + catch (HttpListenerException) + { + } + catch (Exception e) + { + Events.HandleExceptionEncountered(this, new ExceptionEventArgs(null, e)); + } + finally + { + Events.HandleServerStopped(this, EventArgs.Empty); + } + } + + private double TotalMsFrom(DateTime startTime) + { + try + { + DateTime endTime = DateTime.Now; + TimeSpan totalTime = (endTime - startTime); + return totalTime.TotalMilliseconds; + } + catch (Exception) + { + return -1; + } + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Web/EonaCatWebserver.xml b/EonaCat.Network/System/Web/EonaCatWebserver.xml new file mode 100644 index 0000000..da1009e --- /dev/null +++ b/EonaCat.Network/System/Web/EonaCatWebserver.xml @@ -0,0 +1,1811 @@ + + + + EonaCatWebServer + + + + + Access control manager. Dictates which connections are permitted or denied. + + + + + Matcher to match denied addresses. + + + + + Matcher to match permitted addresses. + + + + + Access control mode, either DefaultPermit or DefaultDeny. + DefaultPermit: allow everything, except for those explicitly denied. + DefaultDeny: deny everything, except for those explicitly permitted. + + + + + Instantiate the object. + + Access control mode. + + + + Permit or deny a request based on IP address. + When operating in 'default deny', only specified entries are permitted. + When operating in 'default permit', everything is allowed unless explicitly denied. + + The IP address to evaluate. + True if permitted. + + + + Access control mode of operation. + + + + + Permit requests from any endpoint by default. + + + + + Deny requests from any endpoint by default. + + + + + A chunk of data, used when reading from a request where the Transfer-Encoding header includes 'chunked'. + + + + + Length of the data. + + + + + Data. + + + + + Any additional metadata that appears on the length line after the length hex value and semicolon. + + + + + Indicates whether or not this is the final chunk, i.e. the chunk length received was zero. + + + + + Connection event arguments. + + + + + Requestor IP address. + + + + + Request TCP port. + + + + + Connection event arguments. + + Requestor IP address. + Request TCP port. + + + + Assign a method handler for when requests are received matching the supplied method and path. + + + + + Globally-unique identifier. + + + + + The pattern against which the raw URL should be matched. + + + + + Indicates whether or not the path specifies a directory. If so, any matching URL will be handled by the specified handler. + + + + + User-supplied metadata. + + + + + Create a new route object. + + The pattern against which the raw URL should be matched. + Indicates whether or not the path specifies a directory. If so, any matching URL will be handled by the specified handler. + Globally-unique identifier. + User-supplied metadata. + + + + Content route manager. Content routes are used for GET and HEAD requests to specific files or entire directories. + + + + + Base directory for files and directories accessible via content routes. + + + + + Instantiate the object. + + + + + Add a route. + + URL path, i.e. /path/to/resource. + True if the path represents a directory. + Globally-unique identifier. + User-supplied metadata. + + + + Remove a route. + + URL path. + + + + Retrieve a content route. + + URL path. + ContentRoute if the route exists, otherwise null. + + + + Check if a content route exists. + + URL path. + True if exists. + + + + Retrieve a content route. + + URL path. + Matching route. + True if a match exists. + + + + Content route handler. Handles GET and HEAD requests to content routes for files and directories. + + + + + The FileMode value to use when accessing files within a content route via a FileStream. Default is FileMode.Open. + + + + + The FileAccess value to use when accessing files within a content route via a FileStream. Default is FileAccess.Read. + + + + + The FileShare value to use when accessing files within a content route via a FileStream. Default is FileShare.Read. + + + + + Assign a method handler for when requests are received matching the supplied method and path regex. + + + + + Globally-unique identifier. + + + + + The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + + + + + The pattern against which the raw URL should be matched. + + + + + The handler for the dynamic route. + + + + + User-supplied metadata. + + + + + Create a new route object. + + The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + The pattern against which the raw URL should be matched. + The method that should be called to handle the request. + Globally-unique identifier. + User-supplied metadata. + + + + Attribute that is used to mark methods as a dynamic route. + + + + + The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + + + + + The pattern against which the raw URL should be matched. Must be convertible to a regular expression. + + + + + Globally-unique identifier. + + + + + User-supplied metadata. + + + + + Instantiate the object. + + The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + The regular expression pattern against which the raw URL should be matched. + Globally-unique identifier. + User-supplied metadata. + + + + Dynamic route manager. Dynamic routes are used for requests using any HTTP method to any path that can be matched by regular expression. + + + + + Directly access the underlying regular expression matching library. + This is helpful in case you want to specify the matching behavior should multiple matches exist. + + + + + Instantiate the object. + + + + + Add a route. + + The HTTP method. + URL path, i.e. /path/to/resource. + Method to invoke. + Globally-unique identifier. + User-supplied metadata. + + + + Remove a route. + + The HTTP method. + URL path. + + + + Check if a content route exists. + + The HTTP method. + URL path. + True if exists. + + + + Match a request method and URL to a handler method. + + The HTTP method. + URL path. + Matching route. + Method to invoke. + + + + Exception event arguments. + + + + + IP address. + + + + + Port number. + + + + + HTTP method. + + + + + URL. + + + + + Request query. + + + + + Request headers. + + + + + Content length. + + + + + Response status. + + + + + Response headers. + + + + + Response content length. + + + + + Exception. + + + + + JSON string of the Exception. + + + + + HTTP context including both request and response. + + + + + The HTTP request that was received. + + + + + Type of route. + + + + + Matched route. + + + + + The HTTP response that will be sent. This object is preconstructed on your behalf and can be modified directly. + + + + + User-supplied metadata. + + + + + Instantiate the object. + + + + + HTTP methods, i.e. GET, PUT, POST, DELETE, etc. + + + + + HTTP GET. + + + + + HTTP HEAD. + + + + + HTTP PUT. + + + + + HTTP POST. + + + + + HTTP DELETE. + + + + + HTTP PATCH. + + + + + HTTP CONNECT. + + + + + HTTP OPTIONS. + + + + + HTTP TRACE. + + + + + HTTP request. + + + + + UTC timestamp from when the request was received. + + + + + Thread ID on which the request exists. + + + + + The protocol and version. + + + + + Source (requestor) IP and port information. + + + + + Destination IP and port information. + + + + + The HTTP method used in the request. + + + + + URL details. + + + + + Query details. + + + + + The headers found in the request. + + + + + Specifies whether or not the client requested HTTP keepalives. + + + + + Indicates whether or not chunked transfer encoding was detected. + + + + + Indicates whether or not the payload has been gzip compressed. + + + + + Indicates whether or not the payload has been deflate compressed. + + + + + The useragent specified in the request. + + + + + The content type as specified by the requestor (client). + + + + + The number of bytes in the request body. + + + + + The stream from which to read the request body sent by the requestor (client). + + + + + Retrieve the request body as a byte array. This will fully read the stream. + + + + + Retrieve the request body as a string. This will fully read the stream. + + + + + The original HttpListenerContext from which the HttpRequest was constructed. + + + + + HTTP request. + + + + + HTTP request. + Instantiate the object using an HttpListenerContext. + + HttpListenerContext. + + + + Retrieve a specified header value from either the headers or the querystring (case insensitive). + + + + + + + Determine if a header exists. + + Header key. + Specify whether a case sensitive search should be used. + True if exists. + + + + Determine if a querystring entry exists. + + Querystring key. + Specify whether a case sensitive search should be used. + True if exists. + + + + For chunked transfer-encoded requests, read the next chunk. + It is strongly recommended that you use the ChunkedTransfer parameter before invoking this method. + + Cancellation token useful for canceling the request. + Chunk. + + + + Read the data stream fully and convert the data to the object type specified using JSON deserialization. + Note: if you use this method, you will not be able to read from the data stream afterward. + + Type. + Object of type specified. + + + + Source details. + + + + + IP address of the requestor. + + + + + TCP port from which the request originated on the requestor. + + + + + Source details. + + + + + Source details. + + IP address of the requestor. + TCP port from which the request originated on the requestor. + + + + Destination details. + + + + + IP address to which the request was made. + + + + + TCP port on which the request was received. + + + + + Hostname to which the request was directed. + + + + + Hostname elements. + + + + + Destination details. + + + + + Source details. + + IP address to which the request was made. + TCP port on which the request was received. + Hostname. + + + + URL details. + + + + + Full URL. + + + + + Raw URL with query. + + + + + Raw URL without query. + + + + + Raw URL elements. + + + + + Parameters found within the URL, if using parameter routes. + + + + + URL details. + + + + + URL details. + + Full URL. + Raw URL. + + + + Query details. + + + + + Querystring, excluding the leading '?'. + + + + + Query elements. + + + + + Query details. + + + + + Query details. + + Full URL. + + + + HTTP response. + + + + + The HTTP status code to return to the requestor (client). + + + + + The HTTP status description to return to the requestor (client). + + + + + User-supplied headers to include in the response. + + + + + User-supplied content-type to include in the response. + + + + + The length of the supplied response data. + + + + + Indicates whether or not chunked transfer encoding should be indicated in the response. + + + + + Retrieve the response body sent using a Send() or SendAsync() method. + + + + + Retrieve the response body sent using a Send() or SendAsync() method. + + + + + Response data stream sent to the requestor. + + + + + Instantiate the object. + + + + + Send headers and no data to the requestor and terminate the connection. + + Cancellation token useful for canceling the request. + True if successful. + + + + Send headers with a specified content length and no data to the requestor and terminate the connection. Useful for HEAD requests where the content length must be set. + + Cancellation token useful for canceling the request. + Content length. + True if successful. + + + + Send headers and data to the requestor and terminate the connection. + + Data. + Cancellation token useful for canceling the request. + True if successful. + + + + Send headers and data to the requestor and terminate the connection. + + Data. + Cancellation token useful for canceling the request. + True if successful. + + + + Send headers and data to the requestor and terminate. + + Number of bytes to send. + Stream containing the data. + Cancellation token useful for canceling the request. + True if successful. + + + + Send headers (if not already sent) and a chunk of data using chunked transfer-encoding, and keep the connection in-tact. + + Chunk of data. + Number of bytes to send from the chunk, i.e. the actual data size (for example, return value of FileStream.ReadAsync(buffer, 0, buffer.Length)). + Cancellation token useful for canceling the request. + True if successful. + + + + Send headers (if not already sent) and the final chunk of data using chunked transfer-encoding and terminate the connection. + + Chunk of data./// Number of bytes to send from the chunk, i.e. the actual data size (for example, return value of FileStream.ReadAsync(buffer, 0, buffer.Length)). + Cancellation token useful for canceling the request. + True if successful. + + + + Convert the response data sent using a Send() method to the object type specified using JSON deserialization. + + Type. + Object of type specified. + + + + MIME types and file extensions. + + + + + Instantiates the object. + + + + + Retrieve MIME type from file extension. + + File extension. + String containing MIME type. + + + + Object extensions. + + + + + Return a JSON string of the object. + + Object. + Enable or disable pretty print. + JSON string. + + + + Assign a method handler for when requests are received matching the supplied method and path containing parameters. + + + + + Globally-unique identifier. + + + + + The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + + + + + The pattern against which the raw URL should be matched. + + + + + The handler for the parameter route. + + + + + User-supplied metadata. + + + + + Create a new route object. + + The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + The pattern against which the raw URL should be matched. + The method that should be called to handle the request. + Globally-unique identifier. + User-supplied metadata. + + + + Attribute that is used to mark methods as a parameter route. + + + + + The path to match, i.e. /{version}/api/{id}. + If a match is found, the Dictionary found in HttpRequest.Url.Parameters will contain keys for 'version' and 'id'. + + + + + The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + + + + + Globally-unique identifier. + + + + + User-supplied metadata. + + + + + Instantiate the object. + + The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + The path to match, i.e. /{version}/api/{id}. + Globally-unique identifier. + User-supplied metadata. + + + + Parameter route manager. Parameter routes are used for requests using any HTTP method to any path where parameters are defined in the URL. + For example, /{version}/api. + For a matching URL, the HttpRequest.Url.Parameters will contain a key called 'version' with the value found in the URL. + + + + + Directly access the underlying URL matching library. + This is helpful in case you want to specify the matching behavior should multiple matches exist. + + + + + Instantiate the object. + + + + + Add a route. + + The HTTP method. + URL path, i.e. /path/to/resource. + Method to invoke. + Globally-unique identifier. + User-supplied metadata. + + + + Remove a route. + + The HTTP method. + URL path. + + + + Retrieve a parameter route. + + The HTTP method. + URL path. + ParameterRoute if the route exists, otherwise null. + + + + Check if a content route exists. + + The HTTP method. + URL path. + True if exists. + + + + Match a request method and URL to a handler method. + + The HTTP method. + URL path. + Values extracted from the URL. + Matching route. + True if match exists. + + + + Request event arguments. + + + + + IP address. + + + + + Port number. + + + + + HTTP method. + + + + + URL. + + + + + Query found in the URL. + + + + + Request headers. + + + + + Content length. + + + + + Response event arguments. + + + + + IP address. + + + + + Port number. + + + + + HTTP method. + + + + + URL. + + + + + Request query. + + + + + Request headers. + + + + + Content length. + + + + + Response status. + + + + + Response headers. + + + + + Response content length. + + + + + Total time in processing the request and sending the response, in milliseconds. + + + + + Route type. + + + + + Default route. + + + + + Content route. + + + + + Static route. + + + + + Parameter route. + + + + + Dynamic route. + + + + + Assign a method handler for when requests are received matching the supplied method and path. + + + + + Globally-unique identifier. + + + + + The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + + + + + The raw URL, i.e. /foo/bar/. Be sure this begins and ends with '/'. + + + + + The handler for the static route. + + + + + User-supplied metadata. + + + + + Create a new route object. + + The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + The raw URL, i.e. /foo/bar/. Be sure this begins and ends with '/'. + The method that should be called to handle the request. + Globally-unique identifier. + User-supplied metadata. + + + + Attribute that is used to mark methods as a static route. + + + + + The raw URL, i.e. /foo/bar/. Be sure this begins and ends with '/'. + + + + + The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + + + + + Globally-unique identifier. + + + + + User-supplied metadata. + + + + + Instantiate the object. + + The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + The raw URL, i.e. /foo/bar/. Be sure this begins and ends with '/'. + Globally-unique identifier. + User-supplied metadata. + + + + Static route manager. Static routes are used for requests using any HTTP method to a specific path. + + + + + Instantiate the object. + + + + + Add a route. + + The HTTP method. + URL path, i.e. /path/to/resource. + Method to invoke. + Globally-unique identifier. + User-supplied metadata. + + + + Remove a route. + + The HTTP method. + URL path. + + + + Retrieve a static route. + + The HTTP method. + URL path. + StaticRoute if the route exists, otherwise null. + + + + Check if a static route exists. + + The HTTP method. + URL path. + True if exists. + + + + Match a request method and URL to a handler method. + + The HTTP method. + URL path. + Matching route. + Method to invoke. + + + + EonaCat webserver. + + + + + Indicates whether or not the server is listening. + + + + + Number of requests being serviced currently. + + + + + EonaCat webserver settings. + + + + + EonaCat webserver routes. + + + + + EonaCat webserver statistics. + + + + + Set specific actions/callbacks to use when events are raised. + + + + + Default pages served by the EonaCat webserver. + + + + + Creates a new instance of the EonaCat webserver. + If you do not provide a settings object, default settings will be used, which will cause the EonaCat Webserver to listen on http://127.0.0.1:8000, and send events to the console. + + EonaCat webserver settings. + Method used when a request is received and no matching routes are found. Commonly used as the 404 handler when routes are used. + + + + Creates a new instance of the EonaCat webserver. + + Hostname or IP address on which to listen. + TCP port on which to listen. + Specify whether or not SSL should be used (HTTPS). + Method used when a request is received and no matching routes are found. Commonly used as the 404 handler when routes are used. + + + + Creates a new instance of the EonaCat webserver. + + Hostnames or IP addresses on which to listen. Note: multiple listener endpoints are not supported on all platforms. + TCP port on which to listen. + Specify whether or not SSL should be used (HTTPS). + Method used when a request is received and no matching routes are found. Commonly used as the 404 handler when routes are used. + + + + Tear down the server and dispose of background workers. + Do not use this object after disposal. + + + + + Start accepting new connections. + + Cancellation token useful for canceling the server. + + + + Start accepting new connections. + + Cancellation token useful for canceling the server. + Task. + + + + Stop accepting new connections. + + + + + Tear down the server and dispose of background workers. + Do not use this object after disposal. + + + + + Callbacks/actions to use when various events are encountered. + + + + + Method to use for sending log messages. + + + + + Event to fire when a connection is received. + + + + + Event to fire when a request is received. + + + + + Event to fire when a request is denied due to access control. + + + + + Event to fire when a requestor disconnected unexpectedly. + + + + + Event to fire when a response is sent. + + + + + Event to fire when an exception is encountered. + + + + + Event to fire when the server is started. + + + + + Event to fire when the server is stopped. + + + + + Event to fire when the server is being disposed. + + + + + Instantiate the object. + + + + + Default pages served by the EonaCat webserver. + + + + + Page displayed when sending a 404 due to a lack of a route. + + + + + Page displayed when sending a 500 due to an exception is unhandled within your routes. + + + + + Default pages served by the EonaCat webserver. + + + + + Page served by the EonaCat webserver. + + + + + Content type. + + + + + Content. + + + + + Page served by the EonaCat webserver. + + Content type. + Content. + + + + EonaCat webserver routes. + + + + + Function to call when a preflight (OPTIONS) request is received. + Often used to handle CORS. + Leave null to use the default OPTIONS handler. + + + + + Function to call prior to routing. + Return 'true' if the connection should be terminated. + Return 'false' to allow the connection to continue routing. + + + + + Content routes; i.e. routes to specific files or folders for GET and HEAD requests. + + + + + Handler for content route requests. + + + + + Static routes; i.e. routes with explicit matching and any HTTP method. + + + + + Parameter routes; i.e. routes with parameters embedded in the URL, such as /{version}/api/{id}. + + + + + Dynamic routes; i.e. routes with regex matching and any HTTP method. + + + + + Default route; used when no other routes match. + + + + + Instantiate the object using default settings. + + + + + Instantiate the object using default settings and the specified default route. + + + + + EonaCat webserver settings. + + + + + Prefixes on which to listen. + + + + + Input-output settings. + + + + + SSL settings. + + + + + Headers that will be added to every response unless previously set. + + + + + Access control manager, i.e. default mode of operation, permit list, and deny list. + + + + + Debug logging settings. + Be sure to set Events.Logger in order to receive debug messages. + + + + + EonaCat webserver settings. + + + + + EonaCat webserver settings. + + The hostname on which to listen. + The port on which to listen. + Enable or disable SSL. + + + + EonaCat webserver settings. + + The hostnames on which to listen. + The port on which to listen. + Enable or disable SSL. + + + + Input-output settings. + + + + + Buffer size to use when interacting with streams. + + + + + Maximum number of concurrent requests. + + + + + Input-output settings. + + + + + SSL settings. + + + + + Enable or disable SSL. + + + + + Require mutual authentication. + + + + + Accept invalid certificates including self-signed and those that are unable to be verified. + + + + + SSL settings. + + + + + Headers that will be added to every response unless previously set. + + + + + Access-Control-Allow-Origin header. + + + + + Access-Control-Allow-Methods header. + + + + + Access-Control-Allow-Headers header. + + + + + Access-Control-Expose-Headers header. + + + + + Accept header. + + + + + Accept-Language header. + + + + + Accept-Charset header. + + + + + Connection header. + + + + + Host header. + + + + + Headers that will be added to every response unless previously set. + + + + + Debug logging settings. + Be sure to set Events.Logger in order to receive debug messages. + + + + + Enable or disable debug logging of access control. + + + + + Enable or disable debug logging of routing. + + + + + Enable or disable debug logging of requests. + + + + + Enable or disable debug logging of responses. + + + + + Debug logging settings. + Be sure to set Events.Logger in order to receive debug messages. + + + + + EonaCat webserver statistics. + + + + + The time at which the client or server was started. + + + + + The amount of time which the client or server has been up. + + + + + The number of payload bytes received (incoming request body). + + + + + The number of payload bytes sent (outgoing request body). + + + + + Initialize the statistics object. + + + + + Human-readable version of the object. + + String. + + + + Reset statistics other than StartTime and UpTime. + + + + + Retrieve the number of requests received using a specific HTTP method. + + HTTP method. + Number of requests received using this method. + + + \ No newline at end of file diff --git a/EonaCat.Network/System/Web/EonaCatWebserverEvents.cs b/EonaCat.Network/System/Web/EonaCatWebserverEvents.cs new file mode 100644 index 0000000..5b478d4 --- /dev/null +++ b/EonaCat.Network/System/Web/EonaCatWebserverEvents.cs @@ -0,0 +1,134 @@ +using System; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Callbacks/actions to use when various events are encountered. + /// + public class EonaCatWebserverEvents + { + /// + /// Method to use for sending log messages. + /// + public Action Logger = null; + + /// + /// Event to fire when a connection is received. + /// + public event EventHandler ConnectionReceived = delegate + { }; + + /// + /// Event to fire when a request is received. + /// + public event EventHandler RequestReceived = delegate + { }; + + /// + /// Event to fire when a request is denied due to access control. + /// + public event EventHandler RequestDenied = delegate + { }; + + /// + /// Event to fire when a requestor disconnected unexpectedly. + /// + public event EventHandler RequestorDisconnected = delegate + { }; + + /// + /// Event to fire when a response is sent. + /// + public event EventHandler ResponseSent = delegate + { }; + + /// + /// Event to fire when an exception is encountered. + /// + public event EventHandler ExceptionEncountered = delegate + { }; + + /// + /// Event to fire when the server is started. + /// + public event EventHandler ServerStarted = delegate + { }; + + /// + /// Event to fire when the server is stopped. + /// + public event EventHandler ServerStopped = delegate + { }; + + /// + /// Event to fire when the server is being disposed. + /// + public event EventHandler ServerDisposing = delegate + { }; + + /// + /// Instantiate the object. + /// + public EonaCatWebserverEvents() + { + } + + internal void HandleConnectionReceived(object sender, ConnectionEventArgs args) + { + WrappedEventHandler(() => ConnectionReceived?.Invoke(sender, args), "ConnectionReceived", sender); + } + + internal void HandleRequestReceived(object sender, RequestEventArgs args) + { + WrappedEventHandler(() => RequestReceived?.Invoke(sender, args), "RequestReceived", sender); + } + + internal void HandleRequestDenied(object sender, RequestEventArgs args) + { + WrappedEventHandler(() => RequestDenied?.Invoke(sender, args), "RequestDenied", sender); + } + + internal void HandleResponseSent(object sender, ResponseEventArgs args) + { + WrappedEventHandler(() => ResponseSent?.Invoke(sender, args), "ResponseSent", sender); + } + + internal void HandleExceptionEncountered(object sender, ExceptionEventArgs args) + { + WrappedEventHandler(() => ExceptionEncountered?.Invoke(sender, args), "ExceptionEncountered", sender); + } + + internal void HandleServerStarted(object sender, EventArgs args) + { + WrappedEventHandler(() => ServerStarted?.Invoke(sender, args), "ServerStarted", sender); + } + + internal void HandleServerStopped(object sender, EventArgs args) + { + WrappedEventHandler(() => ServerStopped?.Invoke(sender, args), "ServerStopped", sender); + } + + internal void HandleServerDisposing(object sender, EventArgs args) + { + WrappedEventHandler(() => ServerDisposing?.Invoke(sender, args), "ServerDisposing", sender); + } + + private void WrappedEventHandler(Action action, string handler, object sender) + { + if (action == null) + return; + + try + { + action.Invoke(); + } + catch (Exception e) + { + Logger?.Invoke("Event handler exception in " + handler + ": " + Environment.NewLine + e.ToJson(true)); + } + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Web/EonaCatWebserverPages.cs b/EonaCat.Network/System/Web/EonaCatWebserverPages.cs new file mode 100644 index 0000000..c462516 --- /dev/null +++ b/EonaCat.Network/System/Web/EonaCatWebserverPages.cs @@ -0,0 +1,89 @@ +using System; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Default pages served by the EonaCat webserver. + /// + public class EonaCatWebserverPages + { + /// + /// Page displayed when sending a 404 due to a lack of a route. + /// + public Page Default404Page + { + get + { + return _Default404Page; + } + set + { + if (value == null) + throw new ArgumentNullException(nameof(Default404Page)); + _Default404Page = value; + } + } + + /// + /// Page displayed when sending a 500 due to an exception is unhandled within your routes. + /// + public Page Default500Page + { + get + { + return _Default500Page; + } + set + { + if (value == null) + throw new ArgumentNullException(nameof(Default500Page)); + _Default500Page = value; + } + } + + private Page _Default404Page = new Page("text/plain", "Not found"); + private Page _Default500Page = new Page("text/plain", "Internal server error"); + + /// + /// Default pages served by the EonaCat webserver. + /// + public EonaCatWebserverPages() + { + } + + /// + /// Page served by the EonaCat webserver. + /// + public class Page + { + /// + /// Content type. + /// + public string ContentType { get; private set; } = null; + + /// + /// Content. + /// + public string Content { get; private set; } = null; + + /// + /// Page served by the EonaCat webserver. + /// + /// Content type. + /// Content. + public Page(string contentType, string content) + { + if (String.IsNullOrEmpty(contentType)) + throw new ArgumentNullException(nameof(contentType)); + if (String.IsNullOrEmpty(content)) + throw new ArgumentNullException(nameof(content)); + + ContentType = contentType; + Content = content; + } + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Web/EonaCatWebserverRoutes.cs b/EonaCat.Network/System/Web/EonaCatWebserverRoutes.cs new file mode 100644 index 0000000..85af759 --- /dev/null +++ b/EonaCat.Network/System/Web/EonaCatWebserverRoutes.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// EonaCat webserver routes. + /// + public class EonaCatWebserverRoutes + { + /// + /// Function to call when a preflight (OPTIONS) request is received. + /// Often used to handle CORS. + /// Leave null to use the default OPTIONS handler. + /// + public Func Preflight + { + get + { + return _Preflight; + } + set + { + if (value == null) + _Preflight = PreflightInternal; + else + _Preflight = value; + } + } + + /// + /// Function to call prior to routing. + /// Return 'true' if the connection should be terminated. + /// Return 'false' to allow the connection to continue routing. + /// + public Func> PreRouting = null; + + /// + /// Content routes; i.e. routes to specific files or folders for GET and HEAD requests. + /// + public ContentRouteManager Content + { + get + { + return _Content; + } + set + { + if (value == null) + throw new ArgumentNullException(nameof(Content)); + _Content = value; + } + } + + /// + /// Handler for content route requests. + /// + public ContentRouteHandler ContentHandler + { + get + { + return _ContentHandler; + } + set + { + if (value == null) + throw new ArgumentNullException(nameof(ContentHandler)); + _ContentHandler = value; + } + } + + /// + /// Static routes; i.e. routes with explicit matching and any HTTP method. + /// + public StaticRouteManager Static + { + get + { + return _Static; + } + set + { + if (value == null) + throw new ArgumentNullException(nameof(Static)); + _Static = value; + } + } + + /// + /// Parameter routes; i.e. routes with parameters embedded in the URL, such as /{version}/api/{id}. + /// + public ParameterRouteManager Parameter + { + get + { + return _Parameter; + } + set + { + if (value == null) + throw new ArgumentNullException(nameof(Parameter)); + _Parameter = value; + } + } + + /// + /// Dynamic routes; i.e. routes with regex matching and any HTTP method. + /// + public DynamicRouteManager Dynamic + { + get + { + return _Dynamic; + } + set + { + if (value == null) + throw new ArgumentNullException(nameof(Dynamic)); + _Dynamic = value; + } + } + + /// + /// Default route; used when no other routes match. + /// + public Func Default + { + get + { + return _Default; + } + set + { + _Default = value; + } + } + + private 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; + + /// + /// Instantiate the object using default settings. + /// + public EonaCatWebserverRoutes() + { + _Preflight = PreflightInternal; + _ContentHandler = new ContentRouteHandler(_Content); + } + + /// + /// Instantiate the object using default settings and the specified default route. + /// + public EonaCatWebserverRoutes(EonaCatWebserverSettings settings, Func defaultRoute) + { + if (settings == null) + settings = new EonaCatWebserverSettings(); + if (defaultRoute == null) + throw new ArgumentNullException(nameof(defaultRoute)); + + _Settings = settings; + _Preflight = PreflightInternal; + _Default = defaultRoute; + _ContentHandler = new ContentRouteHandler(_Content); + } + + private async Task PreflightInternal(HttpContext ctx) + { + ctx.Response.StatusCode = 200; + + string[] requestedHeaders = null; + if (ctx.Request.Headers != null) + { + foreach (KeyValuePair curr in ctx.Request.Headers) + { + if (String.IsNullOrEmpty(curr.Key)) + continue; + if (String.IsNullOrEmpty(curr.Value)) + continue; + if (String.Compare(curr.Key.ToLower(), "access-control-request-headers") == 0) + { + requestedHeaders = curr.Value.Split(','); + break; + } + } + } + + string headers = ""; + + if (requestedHeaders != null) + { + int addedCount = 0; + foreach (string curr in requestedHeaders) + { + if (String.IsNullOrEmpty(curr)) + continue; + if (addedCount > 0) + headers += ", "; + headers += ", " + curr; + addedCount++; + } + } + + foreach (KeyValuePair header in _Settings.Headers) + { + ctx.Response.Headers.Add(header.Key, header.Value); + } + + ctx.Response.ContentLength = 0; + await ctx.Response.Send().ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Web/EonaCatWebserverSettings.cs b/EonaCat.Network/System/Web/EonaCatWebserverSettings.cs new file mode 100644 index 0000000..7b58bec --- /dev/null +++ b/EonaCat.Network/System/Web/EonaCatWebserverSettings.cs @@ -0,0 +1,362 @@ +using System; +using System.Collections.Generic; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// EonaCat webserver settings. + /// + public class EonaCatWebserverSettings + { + /// + /// Prefixes on which to listen. + /// + public List Prefixes + { + get + { + return _Prefixes; + } + 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; + } + } + + /// + /// Input-output settings. + /// + public IOSettings IO + { + get + { + return _IO; + } + set + { + if (value == null) + throw new ArgumentNullException(nameof(IO)); + _IO = value; + } + } + + /// + /// SSL settings. + /// + public SslSettings Ssl + { + get + { + return _Ssl; + } + set + { + if (value == null) + throw new ArgumentNullException(nameof(Ssl)); + _Ssl = value; + } + } + + /// + /// Headers that will be added to every response unless previously set. + /// + public Dictionary Headers + { + get + { + return _Headers; + } + set + { + if (value == null) + throw new ArgumentNullException(nameof(Headers)); + _Headers = value; + } + } + + /// + /// Access control manager, i.e. default mode of operation, permit list, and deny list. + /// + public AccessControlManager AccessControl + { + get + { + return _AccessControl; + } + set + { + if (value == null) + throw new ArgumentNullException(nameof(AccessControl)); + _AccessControl = value; + } + } + + /// + /// Debug logging settings. + /// Be sure to set Events.Logger in order to receive debug messages. + /// + public DebugSettings Debug + { + get + { + return _Debug; + } + set + { + if (value == null) + throw new ArgumentNullException(nameof(Debug)); + _Debug = value; + } + } + + private List _Prefixes = new List(); + private IOSettings _IO = new IOSettings(); + private SslSettings _Ssl = new SslSettings(); + private AccessControlManager _AccessControl = new AccessControlManager(AccessControlMode.DefaultPermit); + private DebugSettings _Debug = new DebugSettings(); + + private Dictionary _Headers = new Dictionary + { + { "Access-Control-Allow-Origin", "*" }, + { "Access-Control-Allow-Methods", "OPTIONS, HEAD, GET, PUT, POST, DELETE" }, + { "Access-Control-Allow-Headers", "*" }, + { "Accept", "*/*" }, + { "Accept-Language", "en-US, en" }, + { "Accept-Charset", "ISO-8859-1, utf-8" }, + { "Connection", "close" } + }; + + /// + /// EonaCat webserver settings. + /// + public EonaCatWebserverSettings() + { + } + + /// + /// EonaCat webserver settings. + /// + /// The hostname on which to listen. + /// The port on which to listen. + /// Enable or disable SSL. + public EonaCatWebserverSettings(string hostname, int port, bool ssl = false) + { + 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; + } + + /// + /// EonaCat webserver settings. + /// + /// The hostnames on which to listen. + /// The port on which to listen. + /// Enable or disable SSL. + public EonaCatWebserverSettings(List hostnames, int port, bool ssl = false) + { + if (hostnames == null) + 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); + } + + _Ssl.Enable = ssl; + } + + /// + /// Input-output settings. + /// + public class IOSettings + { + /// + /// Buffer size to use when interacting with streams. + /// + public int StreamBufferSize + { + get + { + return _StreamBufferSize; + } + set + { + if (value < 1) + throw new ArgumentOutOfRangeException(nameof(StreamBufferSize)); + _StreamBufferSize = value; + } + } + + /// + /// Maximum number of concurrent requests. + /// + public int MaxRequests + { + get + { + return _MaxRequests; + } + set + { + if (value < 1) + throw new ArgumentException("Maximum requests must be greater than zero."); + _MaxRequests = value; + } + } + + private int _StreamBufferSize = 65536; + private int _MaxRequests = 1024; + + /// + /// Input-output settings. + /// + public IOSettings() + { + } + } + + /// + /// SSL settings. + /// + public class SslSettings + { + /// + /// Enable or disable SSL. + /// + public bool Enable = false; + + /// + /// Require mutual authentication. + /// + public bool MutuallyAuthenticate = false; + + /// + /// Accept invalid certificates including self-signed and those that are unable to be verified. + /// + public bool AcceptInvalidAcertificates = true; + + /// + /// SSL settings. + /// + internal SslSettings() + { + } + } + + /// + /// Headers that will be added to every response unless previously set. + /// + public class HeaderSettings + { + /// + /// Access-Control-Allow-Origin header. + /// + public string AccessControlAllowOrigin = "*"; + + /// + /// Access-Control-Allow-Methods header. + /// + public string AccessControlAllowMethods = "OPTIONS, HEAD, GET, PUT, POST, DELETE"; + + /// + /// Access-Control-Allow-Headers header. + /// + public string AccessControlAllowHeaders = "*"; + + /// + /// Access-Control-Expose-Headers header. + /// + public string AccessControlExposeHeaders = ""; + + /// + /// Accept header. + /// + public string Accept = "*/*"; + + /// + /// Accept-Language header. + /// + public string AcceptLanguage = "en-US, en"; + + /// + /// Accept-Charset header. + /// + public string AcceptCharset = "ISO-8859-1, utf-8"; + + /// + /// Connection header. + /// + public string Connection = "close"; + + /// + /// Host header. + /// + public string Host = null; + + /// + /// Headers that will be added to every response unless previously set. + /// + public HeaderSettings() + { + } + } + + /// + /// Debug logging settings. + /// Be sure to set Events.Logger in order to receive debug messages. + /// + public class DebugSettings + { + /// + /// Enable or disable debug logging of access control. + /// + public bool AccessControl = false; + + /// + /// Enable or disable debug logging of routing. + /// + public bool Routing = false; + + /// + /// Enable or disable debug logging of requests. + /// + public bool Requests = false; + + /// + /// Enable or disable debug logging of responses. + /// + public bool Responses = false; + + /// + /// Debug logging settings. + /// Be sure to set Events.Logger in order to receive debug messages. + /// + public DebugSettings() + { + } + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Web/EonaCatWebserverStatistics.cs b/EonaCat.Network/System/Web/EonaCatWebserverStatistics.cs new file mode 100644 index 0000000..4805ea7 --- /dev/null +++ b/EonaCat.Network/System/Web/EonaCatWebserverStatistics.cs @@ -0,0 +1,156 @@ +using System; +using System.Text; +using System.Threading; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// EonaCat webserver statistics. + /// + public class EonaCatWebserverStatistics + { + /// + /// The time at which the client or server was started. + /// + public DateTime StartTime + { + get + { + return _StartTime; + } + } + + /// + /// The amount of time which the client or server has been up. + /// + public TimeSpan UpTime + { + get + { + return DateTime.Now.ToUniversalTime() - _StartTime; + } + } + + /// + /// The number of payload bytes received (incoming request body). + /// + public long ReceivedPayloadBytes + { + get + { + return _ReceivedPayloadBytes; + } + internal set + { + _ReceivedPayloadBytes = value; + } + } + + /// + /// The number of payload bytes sent (outgoing request body). + /// + public long SentPayloadBytes + { + get + { + return _SentPayloadBytes; + } + internal set + { + _SentPayloadBytes = value; + } + } + + private DateTime _StartTime = DateTime.Now.ToUniversalTime(); + private long _ReceivedPayloadBytes = 0; + private long _SentPayloadBytes = 0; + private long[] _RequestsByMethod; // _RequestsByMethod[(int)HttpMethod.Xyz] = Count + + /// + /// Initialize the statistics object. + /// + public EonaCatWebserverStatistics() + { + // Calculating the length for _RequestsByMethod array + int max = 0; + foreach (var value in Enum.GetValues(typeof(HttpMethod))) + { + if ((int)value > max) + max = (int)value; + } + + _RequestsByMethod = new long[max + 1]; + } + + /// + /// Human-readable version of the object. + /// + /// String. + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + sb.AppendLine($"--- Statistics ---"); + sb.AppendLine($" Start Time : {StartTime}"); + sb.AppendLine($" Up Time : {UpTime}"); + sb.AppendLine($" Received Payload Bytes : {ReceivedPayloadBytes.ToString("N0")} bytes"); + sb.AppendLine($" Sent Payload Bytes : {SentPayloadBytes.ToString("N0")} bytes"); + sb.AppendLine($" Requests By Method : "); + + bool foundAtLeastOne = false; + for (int i = 0; i < _RequestsByMethod.Length; i++) + { + var count = Interlocked.Read(ref _RequestsByMethod[i]); + if (count > 0) + { + foundAtLeastOne = true; + sb.AppendLine($" {((HttpMethod)i).ToString().PadRight(18)} : {count.ToString("N0")}"); + } + } + + if (!foundAtLeastOne) + sb.AppendLine(" (none)"); + + return sb.ToString(); + } + + /// + /// Reset statistics other than StartTime and UpTime. + /// + public void Reset() + { + Interlocked.Exchange(ref _ReceivedPayloadBytes, 0); + Interlocked.Exchange(ref _SentPayloadBytes, 0); + + for (int i = 0; i < _RequestsByMethod.Length; i++) + Interlocked.Exchange(ref _RequestsByMethod[i], 0); + } + + /// + /// Retrieve the number of requests received using a specific HTTP method. + /// + /// HTTP method. + /// Number of requests received using this method. + public long RequestCountByMethod(HttpMethod method) + { + return Interlocked.Read(ref _RequestsByMethod[(int)method]); + } + + internal void IncrementRequestCounter(HttpMethod method) + { + Interlocked.Increment(ref _RequestsByMethod[(int)method]); + } + + internal void IncrementReceivedPayloadBytes(long len) + { + Interlocked.Add(ref _ReceivedPayloadBytes, len); + } + + internal void IncrementSentPayloadBytes(long len) + { + Interlocked.Add(ref _SentPayloadBytes, len); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Web/ExceptionEventArgs.cs b/EonaCat.Network/System/Web/ExceptionEventArgs.cs new file mode 100644 index 0000000..7468dc6 --- /dev/null +++ b/EonaCat.Network/System/Web/ExceptionEventArgs.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Exception event arguments. + /// + public class ExceptionEventArgs : EventArgs + { + /// + /// IP address. + /// + public string Ip { get; private set; } = null; + + /// + /// Port number. + /// + public int Port { get; private set; } = 0; + + /// + /// HTTP method. + /// + public HttpMethod Method { get; private set; } = HttpMethod.GET; + + /// + /// URL. + /// + public string Url { get; private set; } = null; + + /// + /// Request query. + /// + public Dictionary Query { get; private set; } = new Dictionary(); + + /// + /// Request headers. + /// + public Dictionary RequestHeaders { get; private set; } = new Dictionary(); + + /// + /// Content length. + /// + public long RequestContentLength { get; private set; } = 0; + + /// + /// Response status. + /// + public int StatusCode { get; private set; } = 0; + + /// + /// Response headers. + /// + public Dictionary ResponseHeaders { get; private set; } = new Dictionary(); + + /// + /// Response content length. + /// + public long? ResponseContentLength { get; private set; } = 0; + + /// + /// Exception. + /// + public Exception Exception { get; private set; } = null; + + /// + /// JSON string of the Exception. + /// + public string Json + { + get + { + if (Exception != null) + return Exception.ToJson(true); + return null; + } + } + + internal ExceptionEventArgs(HttpContext ctx, Exception e) + { + if (ctx != null) + { + Ip = ctx.Request.Source.IpAddress; + Port = ctx.Request.Source.Port; + Method = ctx.Request.Method; + Url = ctx.Request.Url.Full; + Query = ctx.Request.Query.Elements; + RequestHeaders = ctx.Request.Headers; + RequestContentLength = ctx.Request.ContentLength; + StatusCode = ctx.Response.StatusCode; + ResponseContentLength = ctx.Response.ContentLength; + } + + Exception = e; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Web/HttpContext.cs b/EonaCat.Network/System/Web/HttpContext.cs new file mode 100644 index 0000000..e27dc54 --- /dev/null +++ b/EonaCat.Network/System/Web/HttpContext.cs @@ -0,0 +1,65 @@ +using System; +using System.Net; +using EonaCat.Json; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// HTTP context including both request and response. + /// + public class HttpContext + { + /// + /// The HTTP request that was received. + /// + [JsonProperty(Order = -1)] + public HttpRequest Request { get; private set; } = null; + + /// + /// Type of route. + /// + [JsonProperty(Order = 0)] + public RouteTypeEnum? RouteType { get; internal set; } = null; + + /// + /// Matched route. + /// + [JsonProperty(Order = 1)] + public object Route { get; internal set; } = null; + + /// + /// The HTTP response that will be sent. This object is preconstructed on your behalf and can be modified directly. + /// + [JsonProperty(Order = 998)] + public HttpResponse Response { get; private set; } = null; + + /// + /// User-supplied metadata. + /// + [JsonProperty(Order = 999)] + public object Metadata { get; set; } = null; + + private HttpListenerContext _Context = null; + + /// + /// Instantiate the object. + /// + public HttpContext() + { + } + + internal HttpContext(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); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Web/HttpMethod.cs b/EonaCat.Network/System/Web/HttpMethod.cs new file mode 100644 index 0000000..11866d3 --- /dev/null +++ b/EonaCat.Network/System/Web/HttpMethod.cs @@ -0,0 +1,69 @@ +using System.Runtime.Serialization; +using EonaCat.Json.Converters; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// HTTP methods, i.e. GET, PUT, POST, DELETE, etc. + /// + [EonaCat.Json.Converter(typeof(StringEnumConverter))] + public enum HttpMethod + { + /// + /// HTTP GET. + /// + [EnumMember(Value = "GET")] + GET, + + /// + /// HTTP HEAD. + /// + [EnumMember(Value = "HEAD")] + HEAD, + + /// + /// HTTP PUT. + /// + [EnumMember(Value = "PUT")] + PUT, + + /// + /// HTTP POST. + /// + [EnumMember(Value = "POST")] + POST, + + /// + /// HTTP DELETE. + /// + [EnumMember(Value = "DELETE")] + DELETE, + + /// + /// HTTP PATCH. + /// + [EnumMember(Value = "PATCH")] + PATCH, + + /// + /// HTTP CONNECT. + /// + [EnumMember(Value = "CONNECT")] + CONNECT, + + /// + /// HTTP OPTIONS. + /// + [EnumMember(Value = "OPTIONS")] + OPTIONS, + + /// + /// HTTP TRACE. + /// + [EnumMember(Value = "TRACE")] + TRACE + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Web/HttpRequest.cs b/EonaCat.Network/System/Web/HttpRequest.cs new file mode 100644 index 0000000..2e67245 --- /dev/null +++ b/EonaCat.Network/System/Web/HttpRequest.cs @@ -0,0 +1,852 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using EonaCat.Json; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// HTTP request. + /// + public class HttpRequest + { + /// + /// UTC timestamp from when the request was received. + /// + [JsonProperty(Order = -10)] + public DateTime TimestampUtc { get; private set; } = DateTime.Now.ToUniversalTime(); + + /// + /// Thread ID on which the request exists. + /// + [JsonProperty(Order = -9)] + public int ThreadId { get; private set; } = Thread.CurrentThread.ManagedThreadId; + + /// + /// The protocol and version. + /// + [JsonProperty(Order = -8)] + public string ProtocolVersion { get; set; } = null; + + /// + /// Source (requestor) IP and port information. + /// + [JsonProperty(Order = -7)] + public SourceDetails Source { get; set; } = new SourceDetails(); + + /// + /// Destination IP and port information. + /// + [JsonProperty(Order = -6)] + public DestinationDetails Destination { get; set; } = new DestinationDetails(); + + /// + /// The HTTP method used in the request. + /// + [JsonProperty(Order = -5)] + public HttpMethod Method { get; set; } = HttpMethod.GET; + + /// + /// URL details. + /// + [JsonProperty(Order = -4)] + public UrlDetails Url { get; set; } = new UrlDetails(); + + /// + /// Query details. + /// + [JsonProperty(Order = -3)] + public QueryDetails Query { get; set; } = new QueryDetails(); + + /// + /// The headers found in the request. + /// + [JsonProperty(Order = -2)] + public Dictionary Headers { get; set; } = new Dictionary(); + + /// + /// Specifies whether or not the client requested HTTP keepalives. + /// + public bool Keepalive { get; set; } = false; + + /// + /// Indicates whether or not chunked transfer encoding was detected. + /// + public bool ChunkedTransfer { get; set; } = false; + + /// + /// Indicates whether or not the payload has been gzip compressed. + /// + public bool Gzip { get; set; } = false; + + /// + /// Indicates whether or not the payload has been deflate compressed. + /// + public bool Deflate { get; set; } = false; + + /// + /// The useragent specified in the request. + /// + public string Useragent { get; set; } = null; + + /// + /// The content type as specified by the requestor (client). + /// + [JsonProperty(Order = 990)] + public string ContentType { get; set; } = null; + + /// + /// The number of bytes in the request body. + /// + [JsonProperty(Order = 991)] + public long ContentLength { get; private set; } = 0; + + /// + /// The stream from which to read the request body sent by the requestor (client). + /// + [JsonIgnore] + public Stream Data; + + /// + /// Retrieve the request body as a byte array. This will fully read the stream. + /// + [JsonIgnore] + public byte[] DataAsBytes + { + get + { + if (_DataAsBytes != null) + return _DataAsBytes; + if (Data != null && ContentLength > 0) + { + _DataAsBytes = ReadStreamFully(Data); + return _DataAsBytes; + } + return null; + } + } + + /// + /// Retrieve the request body as a string. This will fully read the stream. + /// + [JsonIgnore] + public string DataAsString + { + 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; + } + } + + /// + /// The original HttpListenerContext from which the HttpRequest was constructed. + /// + [JsonIgnore] + public HttpListenerContext ListenerContext; + + private Uri _Uri = null; + private byte[] _DataAsBytes = null; + + /// + /// HTTP request. + /// + public HttpRequest() + { + } + + /// + /// HTTP request. + /// Instantiate the object using an HttpListenerContext. + /// + /// HttpListenerContext. + public HttpRequest(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; + ContentLength = ctx.Request.ContentLength64; + Useragent = ctx.Request.UserAgent; + ContentType = ctx.Request.ContentType; + + _Uri = new Uri(ctx.Request.Url.ToString().Trim()); + + ThreadId = Thread.CurrentThread.ManagedThreadId; + TimestampUtc = DateTime.Now.ToUniversalTime(); + ProtocolVersion = "HTTP/" + ctx.Request.ProtocolVersion.ToString(); + Source = new SourceDetails(ctx.Request.RemoteEndPoint.Address.ToString(), ctx.Request.RemoteEndPoint.Port); + Destination = new DestinationDetails(ctx.Request.LocalEndPoint.Address.ToString(), ctx.Request.LocalEndPoint.Port, _Uri.Host); + Method = (HttpMethod)Enum.Parse(typeof(HttpMethod), ctx.Request.HttpMethod, true); + Url = new UrlDetails(ctx.Request.Url.ToString().Trim(), ctx.Request.RawUrl.ToString().Trim()); + Query = new QueryDetails(Url.Full); + + Headers = new Dictionary(); + for (int i = 0; i < ctx.Request.Headers.Count; i++) + { + string key = ctx.Request.Headers.GetKey(i); + string val = ctx.Request.Headers.Get(i); + Headers = AddToDict(key, val, Headers); + } + + foreach (KeyValuePair curr in Headers) + { + if (String.IsNullOrEmpty(curr.Key)) + continue; + 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")) + { + if (curr.Value.ToLower().Contains("streaming")) + { + ChunkedTransfer = true; + } + } + } + + Data = ctx.Request.InputStream; + } + + /// + /// Retrieve a specified header value from either the headers or the querystring (case insensitive). + /// + /// + /// + public string RetrieveHeaderValue(string 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)) + continue; + if (String.Compare(curr.Key.ToLower(), key.ToLower()) == 0) + return curr.Value; + } + } + + if (Query != null && Query.Elements != null && Query.Elements.Count > 0) + { + foreach (KeyValuePair curr in Query.Elements) + { + if (String.IsNullOrEmpty(curr.Key)) + continue; + if (String.Compare(curr.Key.ToLower(), key.ToLower()) == 0) + return curr.Value; + } + } + + return null; + } + + /// + /// Determine if a header exists. + /// + /// Header key. + /// Specify whether a case sensitive search should be used. + /// True if exists. + public bool HeaderExists(string key, bool caseSensitive) + { + if (String.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key)); + + if (Headers != null && Headers.Count > 0) + { + if (caseSensitive) + { + return Headers.ContainsKey(key); + } + else + { + foreach (KeyValuePair header in Headers) + { + if (String.IsNullOrEmpty(header.Key)) + continue; + if (header.Key.ToLower().Trim().Equals(key)) + return true; + } + } + } + + return false; + } + + /// + /// Determine if a querystring entry exists. + /// + /// Querystring key. + /// Specify whether a case sensitive search should be used. + /// True if exists. + public bool QuerystringExists(string key, bool caseSensitive) + { + if (String.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key)); + + if (Query != null && Query.Elements != null && Query.Elements.Count > 0) + { + if (caseSensitive) + { + return Query.Elements.ContainsKey(key); + } + else + { + foreach (KeyValuePair queryElement in Query.Elements) + { + if (String.IsNullOrEmpty(queryElement.Key)) + continue; + if (queryElement.Key.ToLower().Trim().Equals(key)) + return true; + } + } + } + + return false; + } + + /// + /// For chunked transfer-encoded requests, read the next chunk. + /// It is strongly recommended that you use the ChunkedTransfer parameter before invoking this method. + /// + /// Cancellation token useful for canceling the request. + /// Chunk. + public async Task ReadChunk(CancellationToken token = default) + { + Chunk chunk = new Chunk(); + + byte[] buffer = new byte[1]; + byte[] lenBytes = null; + int bytesRead = 0; + + while (true) + { + bytesRead = await Data.ReadAsync(buffer, 0, buffer.Length, token).ConfigureAwait(false); + if (bytesRead > 0) + { + lenBytes = AppendBytes(lenBytes, buffer); + string lenStr = Encoding.UTF8.GetString(lenBytes); + + if (lenBytes[lenBytes.Length - 1] == 10) + { + lenStr = lenStr.Trim(); + + if (lenStr.Contains(";")) + { + string[] lenParts = lenStr.Split(new char[] { ';' }, 2); + chunk.Length = int.Parse(lenParts[0], NumberStyles.HexNumber); + if (lenParts.Length >= 2) + chunk.Metadata = lenParts[1]; + } + else + { + chunk.Length = int.Parse(lenStr, NumberStyles.HexNumber); + } + + break; + } + } + } + + if (chunk.Length > 0) + { + chunk.IsFinalChunk = false; + buffer = new byte[chunk.Length]; + bytesRead = await Data.ReadAsync(buffer, 0, buffer.Length, token).ConfigureAwait(false); + if (bytesRead == chunk.Length) + { + chunk.Data = new byte[chunk.Length]; + Buffer.BlockCopy(buffer, 0, chunk.Data, 0, chunk.Length); + } + else + { + throw new IOException("Expected " + chunk.Length + " bytes but only read " + bytesRead + " bytes in chunk."); + } + } + else + { + chunk.IsFinalChunk = true; + } + + buffer = new byte[1]; + + while (true) + { + bytesRead = await Data.ReadAsync(buffer, 0, buffer.Length, token).ConfigureAwait(false); + if (bytesRead > 0) + { + if (buffer[0] == 10) + break; + } + } + + return chunk; + } + + /// + /// Read the data stream fully and convert the data to the object type specified using JSON deserialization. + /// Note: if you use this method, you will not be able to read from the data stream afterward. + /// + /// Type. + /// Object of type specified. + public T DataAsJsonObject() where T : class + { + string json = DataAsString; + if (String.IsNullOrEmpty(json)) + return null; + return SerializationHelper.DeserializeJson(json); + } + + private static Dictionary AddToDict(string key, string val, Dictionary existing) + { + if (String.IsNullOrEmpty(key)) + return existing; + + Dictionary ret = new Dictionary(); + + if (existing == null) + { + ret.Add(key, val); + return ret; + } + else + { + if (existing.ContainsKey(key)) + { + if (String.IsNullOrEmpty(val)) + return existing; + string tempVal = existing[key]; + tempVal += "," + val; + existing.Remove(key); + existing.Add(key, tempVal); + return existing; + } + else + { + existing.Add(key, val); + return existing; + } + } + } + + private byte[] AppendBytes(byte[] orig, byte[] append) + { + if (orig == null && append == null) + return null; + + byte[] ret = null; + + if (append == null) + { + ret = new byte[orig.Length]; + Buffer.BlockCopy(orig, 0, ret, 0, orig.Length); + return ret; + } + + if (orig == null) + { + ret = new byte[append.Length]; + Buffer.BlockCopy(append, 0, ret, 0, append.Length); + return ret; + } + + ret = new byte[orig.Length + append.Length]; + Buffer.BlockCopy(orig, 0, ret, 0, orig.Length); + Buffer.BlockCopy(append, 0, ret, orig.Length, append.Length); + return ret; + } + + 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()) + { + int read; + + while ((read = input.Read(buffer, 0, buffer.Length)) > 0) + { + ms.Write(buffer, 0, read); + } + + return ms.ToArray(); + } + } + + private void ReadStreamFully() + { + if (Data == null) + return; + if (!Data.CanRead) + return; + + if (_DataAsBytes == null) + { + if (!ChunkedTransfer) + { + _DataAsBytes = StreamToBytes(Data); + } + else + { + while (true) + { + Chunk chunk = ReadChunk().Result; + if (chunk.Data != null && chunk.Data.Length > 0) + _DataAsBytes = AppendBytes(_DataAsBytes, chunk.Data); + if (chunk.IsFinalChunk) + break; + } + } + } + } + + 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()) + { + int read; + + while ((read = input.Read(buffer, 0, buffer.Length)) > 0) + { + ms.Write(buffer, 0, read); + } + + byte[] ret = ms.ToArray(); + return ret; + } + } + + /// + /// Source details. + /// + public class SourceDetails + { + /// + /// IP address of the requestor. + /// + public string IpAddress { get; set; } = null; + + /// + /// TCP port from which the request originated on the requestor. + /// + public int Port { get; set; } = 0; + + /// + /// Source details. + /// + public SourceDetails() + { + } + + /// + /// Source details. + /// + /// IP address of the requestor. + /// TCP port from which the request originated on the requestor. + public SourceDetails(string ip, int port) + { + if (String.IsNullOrEmpty(ip)) + throw new ArgumentNullException(nameof(ip)); + if (port < 0) + throw new ArgumentOutOfRangeException(nameof(port)); + + IpAddress = ip; + Port = port; + } + } + + /// + /// Destination details. + /// + public class DestinationDetails + { + /// + /// IP address to which the request was made. + /// + public string IpAddress { get; set; } = null; + + /// + /// TCP port on which the request was received. + /// + public int Port { get; set; } = 0; + + /// + /// Hostname to which the request was directed. + /// + public string Hostname { get; set; } = null; + + /// + /// Hostname elements. + /// + public string[] HostnameElements + { + get + { + string hostname = Hostname; + string[] ret; + + if (!String.IsNullOrEmpty(hostname)) + { + if (!IPAddress.TryParse(hostname, out _)) + { + ret = hostname.Split(new char[] { '.' }, StringSplitOptions.RemoveEmptyEntries); + return ret; + } + else + { + ret = new string[1]; + ret[0] = hostname; + return ret; + } + } + + ret = new string[0]; + return ret; + } + } + + /// + /// Destination details. + /// + public DestinationDetails() + { + } + + /// + /// Source details. + /// + /// IP address to which the request was made. + /// TCP port on which the request was received. + /// Hostname. + public DestinationDetails(string ip, int port, string hostname) + { + if (String.IsNullOrEmpty(ip)) + throw new ArgumentNullException(nameof(ip)); + if (port < 0) + throw new ArgumentOutOfRangeException(nameof(port)); + if (String.IsNullOrEmpty(hostname)) + throw new ArgumentNullException(nameof(hostname)); + + IpAddress = ip; + Port = port; + Hostname = hostname; + } + } + + /// + /// URL details. + /// + public class UrlDetails + { + /// + /// Full URL. + /// + public string Full { get; set; } = null; + + /// + /// Raw URL with query. + /// + public string RawWithQuery { get; set; } = null; + + /// + /// Raw URL without query. + /// + public string RawWithoutQuery + { + get + { + if (!String.IsNullOrEmpty(RawWithQuery)) + { + if (RawWithQuery.Contains("?")) + return RawWithQuery.Substring(0, RawWithQuery.IndexOf("?")); + else + return RawWithQuery; + } + else + { + return null; + } + } + } + + /// + /// Raw URL elements. + /// + public string[] Elements + { + get + { + string rawUrl = RawWithoutQuery; + + 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) + { + string[] decoded = new string[encoded.Length]; + for (int i = 0; i < encoded.Length; i++) + { + decoded[i] = WebUtility.UrlDecode(encoded[i]); + } + + return decoded; + } + } + + string[] ret = new string[0]; + return ret; + } + } + + /// + /// Parameters found within the URL, if using parameter routes. + /// + public Dictionary Parameters { get; set; } = new Dictionary(); + + /// + /// URL details. + /// + public UrlDetails() + { + } + + /// + /// URL details. + /// + /// Full URL. + /// Raw URL. + public UrlDetails(string fullUrl, string rawUrl) + { + if (String.IsNullOrEmpty(fullUrl)) + throw new ArgumentNullException(nameof(fullUrl)); + if (String.IsNullOrEmpty(rawUrl)) + throw new ArgumentNullException(nameof(rawUrl)); + + Full = fullUrl; + RawWithQuery = rawUrl; + } + } + + /// + /// Query details. + /// + public class QueryDetails + { + /// + /// Querystring, excluding the leading '?'. + /// + public string Querystring + { + get + { + if (_FullUrl.Contains("?")) + { + return _FullUrl.Substring(_FullUrl.IndexOf("?") + 1, (_FullUrl.Length - _FullUrl.IndexOf("?") - 1)); + } + else + { + return null; + } + } + } + + /// + /// Query elements. + /// + public Dictionary Elements + { + get + { + Dictionary ret = new Dictionary(); + string qs = Querystring; + if (!String.IsNullOrEmpty(qs)) + { + string[] queries = qs.Split(new char[] { '&' }, StringSplitOptions.RemoveEmptyEntries); + if (queries.Length > 0) + { + for (int i = 0; i < queries.Length; i++) + { + string[] queryParts = queries[i].Split('='); + if (queryParts != null && queryParts.Length == 2) + { + ret = AddToDict(queryParts[0], queryParts[1], ret); + } + else if (queryParts != null && queryParts.Length == 1) + { + ret = AddToDict(queryParts[0], null, ret); + } + } + } + } + + return ret; + } + } + + /// + /// Query details. + /// + public QueryDetails() + { + } + + /// + /// Query details. + /// + /// Full URL. + public QueryDetails(string fullUrl) + { + if (String.IsNullOrEmpty(fullUrl)) + throw new ArgumentNullException(nameof(fullUrl)); + + _FullUrl = fullUrl; + } + + private 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 new file mode 100644 index 0000000..a10fba1 --- /dev/null +++ b/EonaCat.Network/System/Web/HttpResponse.cs @@ -0,0 +1,628 @@ +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 +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// HTTP response. + /// + public class HttpResponse + { + /// + /// The HTTP status code to return to the requestor (client). + /// + [JsonProperty(Order = -3)] + public int StatusCode = 200; + + /// + /// The HTTP status description to return to the requestor (client). + /// + [JsonProperty(Order = -2)] + public string StatusDescription = "OK"; + + /// + /// User-supplied headers to include in the response. + /// + [JsonProperty(Order = -1)] + public Dictionary Headers + { + get + { + return _Headers; + } + set + { + if (value == null) + _Headers = new Dictionary(); + else + _Headers = value; + } + } + + /// + /// User-supplied content-type to include in the response. + /// + public string ContentType = String.Empty; + + /// + /// The length of the supplied response data. + /// + public long ContentLength = 0; + + /// + /// Indicates whether or not chunked transfer encoding should be indicated in the response. + /// + public bool ChunkedTransfer = false; + + /// + /// Retrieve the response body sent using a Send() or SendAsync() method. + /// + [JsonIgnore] + public string DataAsString + { + 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; + } + } + + /// + /// Retrieve the response body sent using a Send() or SendAsync() method. + /// + [JsonIgnore] + public byte[] DataAsBytes + { + get + { + if (_DataAsBytes != null) + return _DataAsBytes; + if (_Data != null && ContentLength > 0) + { + _DataAsBytes = ReadStreamFully(_Data); + return _DataAsBytes; + } + return null; + } + } + + /// + /// Response data stream sent to the requestor. + /// + [JsonIgnore] + public MemoryStream Data + { + get + { + return _Data; + } + } + + internal bool ResponseSent + { + get + { + return _ResponseSent; + } + } + + private HttpRequest _Request = null; + private HttpListenerContext _Context = null; + private HttpListenerResponse _Response = null; + private Stream _OutputStream = null; + private bool _HeadersSent = false; + private bool _ResponseSent = false; + + private EonaCatWebserverSettings _Settings = new EonaCatWebserverSettings(); + private EonaCatWebserverEvents _Events = new EonaCatWebserverEvents(); + + private Dictionary _Headers = new Dictionary(); + private byte[] _DataAsBytes = null; + private MemoryStream _Data = null; + + /// + /// Instantiate the object. + /// + public HttpResponse() + { + } + + internal HttpResponse(HttpRequest req, 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; + _Response = _Context.Response; + _Settings = settings; + _Events = events; + + _OutputStream = _Response.OutputStream; + } + + /// + /// Send headers and no data to the requestor and terminate the connection. + /// + /// Cancellation token useful for canceling the request. + /// True if successful. + 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; + return true; + } + catch (Exception) + { + return false; + } + } + + /// + /// Send headers with a specified content length and no data to the requestor and terminate the connection. Useful for HEAD requests where the content length must be set. + /// + /// Cancellation token useful for canceling the request. + /// Content length. + /// True if successful. + 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; + return true; + } + catch (Exception) + { + return false; + } + } + + /// + /// Send headers and data to the requestor and terminate the connection. + /// + /// Data. + /// Cancellation token useful for canceling the request. + /// True if successful. + 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)) + { + bytes = Encoding.UTF8.GetBytes(data); + + _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; + } + else + { + _Response.ContentLength64 = 0; + } + + try + { + if (_Request.Method != HttpMethod.HEAD) + { + if (bytes != null && bytes.Length > 0) + { + await _OutputStream.WriteAsync(bytes, 0, bytes.Length, token).ConfigureAwait(false); + } + } + + await _OutputStream.FlushAsync(token).ConfigureAwait(false); + _OutputStream.Close(); + + if (_Response != null) + _Response.Close(); + + _ResponseSent = true; + return true; + } + catch (Exception) + { + return false; + } + } + + /// + /// Send headers and data to the requestor and terminate the connection. + /// + /// Data. + /// Cancellation token useful for canceling the request. + /// True if successful. + 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); + + _Response.ContentLength64 = data.Length; + ContentLength = data.Length; + } + else + { + _Response.ContentLength64 = 0; + } + + try + { + if (_Request.Method != HttpMethod.HEAD) + { + if (data != null && data.Length > 0) + { + await _OutputStream.WriteAsync(data, 0, data.Length, token).ConfigureAwait(false); + } + } + + await _OutputStream.FlushAsync(token).ConfigureAwait(false); + _OutputStream.Close(); + + if (_Response != null) + _Response.Close(); + + _ResponseSent = true; + return true; + } + catch (Exception) + { + return false; + } + } + + /// + /// Send headers and data to the requestor and terminate. + /// + /// Number of bytes to send. + /// Stream containing the data. + /// Cancellation token useful for canceling the request. + /// True if successful. + 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 + { + if (_Request.Method != HttpMethod.HEAD) + { + if (stream != null && stream.CanRead && contentLength > 0) + { + long bytesRemaining = contentLength; + + _Data = new MemoryStream(); + + while (bytesRemaining > 0) + { + int bytesRead = 0; + byte[] buffer = new byte[_Settings.IO.StreamBufferSize]; + bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, token).ConfigureAwait(false); + if (bytesRead > 0) + { + await _Data.WriteAsync(buffer, 0, bytesRead, token).ConfigureAwait(false); + await _OutputStream.WriteAsync(buffer, 0, bytesRead, token).ConfigureAwait(false); + bytesRemaining -= bytesRead; + } + } + + stream.Close(); + stream.Dispose(); + + _Data.Seek(0, SeekOrigin.Begin); + } + } + + await _OutputStream.FlushAsync(token).ConfigureAwait(false); + _OutputStream.Close(); + + if (_Response != null) + _Response.Close(); + + _ResponseSent = true; + return true; + } + catch (Exception) + { + return false; + } + } + + /// + /// Send headers (if not already sent) and a chunk of data using chunked transfer-encoding, and keep the connection in-tact. + /// + /// Chunk of data. + /// Number of bytes to send from the chunk, i.e. the actual data size (for example, return value of FileStream.ReadAsync(buffer, 0, buffer.Length)). + /// Cancellation token useful for canceling the request. + /// True if successful. + 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); + } + catch (Exception) + { + return false; + } + + return true; + } + + /// + /// Send headers (if not already sent) and the final chunk of data using chunked transfer-encoding and terminate the connection. + /// + /// Chunk of data./// Number of bytes to send from the chunk, i.e. the actual data size (for example, return value of FileStream.ReadAsync(buffer, 0, buffer.Length)). + /// Cancellation token useful for canceling the request. + /// True if successful. + 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); + + await _OutputStream.FlushAsync(token).ConfigureAwait(false); + _OutputStream.Close(); + + if (_Response != null) + _Response.Close(); + + _ResponseSent = true; + return true; + } + catch (Exception) + { + return false; + } + } + + /// + /// Convert the response data sent using a Send() method to the object type specified using JSON deserialization. + /// + /// Type. + /// Object of type specified. + public T DataAsJsonObject() where T : class + { + string json = DataAsString; + 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; + _Response.StatusDescription = GetStatusDescription(StatusCode); + _Response.SendChunked = ChunkedTransfer; + _Response.ContentType = ContentType; + + if (Headers != null && Headers.Count > 0) + { + foreach (KeyValuePair header in Headers) + { + if (String.IsNullOrEmpty(header.Key)) + continue; + _Response.AddHeader(header.Key, header.Value); + } + } + + if (_Settings.Headers != null) + { + foreach (KeyValuePair header in _Settings.Headers) + { + if (!Headers.Any(h => h.Key.ToLower().Equals(header.Key.ToLower()))) + { + _Response.AddHeader(header.Key, header.Value); + } + } + } + + if (_Response.Headers != null && _Response.Headers.HasKeys()) + { + try + { + _Response.Headers.Remove(HttpResponseHeader.Server); + _Response.AddHeader("Server", "EonaCat Server"); + } + catch (Exception) + { + // do nothing + } + } + + _HeadersSent = true; + } + + private string GetStatusDescription(int statusCode) + { + switch (statusCode) + { + case 200: + return "OK"; + + case 201: + return "Created"; + + case 301: + return "Moved Permanently"; + + case 302: + return "Moved Temporarily"; + + case 304: + return "Not Modified"; + + case 400: + return "Bad Request"; + + case 401: + return "Unauthorized"; + + case 403: + return "Forbidden"; + + case 404: + return "Not Found"; + + case 405: + return "Method Not Allowed"; + + case 429: + return "Too Many Requests"; + + case 500: + return "Internal Server Error"; + + case 501: + return "Not Implemented"; + + case 503: + return "Service Unavailable"; + + default: + return "Unknown Status"; + } + } + + private byte[] PackageChunk(byte[] chunk) + { + if (chunk == null || chunk.Length < 1) + { + return Encoding.UTF8.GetBytes("0\r\n\r\n"); + } + + MemoryStream ms = new MemoryStream(); + + string newlineStr = "\r\n"; + byte[] newline = Encoding.UTF8.GetBytes(newlineStr); + + string chunkLenHex = chunk.Length.ToString("X"); + byte[] chunkLen = Encoding.UTF8.GetBytes(chunkLenHex); + + ms.Write(chunkLen, 0, chunkLen.Length); + ms.Write(newline, 0, newline.Length); + ms.Write(chunk, 0, chunk.Length); + ms.Write(newline, 0, newline.Length); + ms.Seek(0, SeekOrigin.Begin); + + byte[] ret = ms.ToArray(); + + return ret; + } + + 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()) + { + int read; + + while ((read = input.Read(buffer, 0, buffer.Length)) > 0) + { + ms.Write(buffer, 0, read); + } + + byte[] ret = ms.ToArray(); + return ret; + } + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Web/MimeTypes.cs b/EonaCat.Network/System/Web/MimeTypes.cs new file mode 100644 index 0000000..fe9c3cc --- /dev/null +++ b/EonaCat.Network/System/Web/MimeTypes.cs @@ -0,0 +1,618 @@ +using System; +using System.Collections.Generic; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// MIME types and file extensions. + /// + public class MimeTypes + { + private static readonly IDictionary data = new Dictionary(StringComparer.InvariantCultureIgnoreCase) { + {".323", "text/h323"}, + {".3g2", "video/3gpp2"}, + {".3gp", "video/3gpp"}, + {".3gp2", "video/3gpp2"}, + {".3gpp", "video/3gpp"}, + {".7z", "application/x-7z-compressed"}, + {".aa", "audio/audible"}, + {".AAC", "audio/aac"}, + {".aaf", "application/octet-stream"}, + {".aax", "audio/vnd.audible.aax"}, + {".ac3", "audio/ac3"}, + {".aca", "application/octet-stream"}, + {".accda", "application/msaccess.addin"}, + {".accdb", "application/msaccess"}, + {".accdc", "application/msaccess.cab"}, + {".accde", "application/msaccess"}, + {".accdr", "application/msaccess.runtime"}, + {".accdt", "application/msaccess"}, + {".accdw", "application/msaccess.webapplication"}, + {".accft", "application/msaccess.ftemplate"}, + {".acx", "application/internet-property-stream"}, + {".AddIn", "text/xml"}, + {".ade", "application/msaccess"}, + {".adobebridge", "application/x-bridge-url"}, + {".adp", "application/msaccess"}, + {".ADT", "audio/vnd.dlna.adts"}, + {".ADTS", "audio/aac"}, + {".afm", "application/octet-stream"}, + {".ai", "application/postscript"}, + {".aif", "audio/x-aiff"}, + {".aifc", "audio/aiff"}, + {".aiff", "audio/aiff"}, + {".air", "application/vnd.adobe.air-application-installer-package+zip"}, + {".amc", "application/x-mpeg"}, + {".application", "application/x-ms-application"}, + {".art", "image/x-jg"}, + {".asa", "application/xml"}, + {".asax", "application/xml"}, + {".ascx", "application/xml"}, + {".asd", "application/octet-stream"}, + {".asf", "video/x-ms-asf"}, + {".ashx", "application/xml"}, + {".asi", "application/octet-stream"}, + {".asm", "text/plain"}, + {".asmx", "application/xml"}, + {".aspx", "application/xml"}, + {".asr", "video/x-ms-asf"}, + {".asx", "video/x-ms-asf"}, + {".atom", "application/atom+xml"}, + {".au", "audio/basic"}, + {".avi", "video/x-msvideo"}, + {".axs", "application/olescript"}, + {".bas", "text/plain"}, + {".bcpio", "application/x-bcpio"}, + {".bin", "application/octet-stream"}, + {".bmp", "image/bmp"}, + {".c", "text/plain"}, + {".cab", "application/octet-stream"}, + {".caf", "audio/x-caf"}, + {".calx", "application/vnd.ms-office.calx"}, + {".cat", "application/vnd.ms-pki.seccat"}, + {".cc", "text/plain"}, + {".cd", "text/plain"}, + {".cdda", "audio/aiff"}, + {".cdf", "application/x-cdf"}, + {".cer", "application/x-x509-ca-cert"}, + {".chm", "application/octet-stream"}, + {".class", "application/x-java-applet"}, + {".clp", "application/x-msclip"}, + {".cmx", "image/x-cmx"}, + {".cnf", "text/plain"}, + {".cod", "image/cis-cod"}, + {".config", "application/xml"}, + {".contact", "text/x-ms-contact"}, + {".coverage", "application/xml"}, + {".cpio", "application/x-cpio"}, + {".cpp", "text/plain"}, + {".crd", "application/x-mscardfile"}, + {".crl", "application/pkix-crl"}, + {".crt", "application/x-x509-ca-cert"}, + {".cs", "text/plain"}, + {".csdproj", "text/plain"}, + {".csh", "application/x-csh"}, + {".csproj", "text/plain"}, + {".css", "text/css"}, + {".csv", "text/csv"}, + {".cur", "application/octet-stream"}, + {".cxx", "text/plain"}, + {".dat", "application/octet-stream"}, + {".datasource", "application/xml"}, + {".dbproj", "text/plain"}, + {".dcr", "application/x-director"}, + {".def", "text/plain"}, + {".deploy", "application/octet-stream"}, + {".der", "application/x-x509-ca-cert"}, + {".dgml", "application/xml"}, + {".dib", "image/bmp"}, + {".dif", "video/x-dv"}, + {".dir", "application/x-director"}, + {".disco", "text/xml"}, + {".divx", "video/divx"}, + {".dll", "application/x-msdownload"}, + {".dll.config", "text/xml"}, + {".dlm", "text/dlm"}, + {".doc", "application/msword"}, + {".docm", "application/vnd.ms-word.document.macroEnabled.12"}, + {".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, + {".dot", "application/msword"}, + {".dotm", "application/vnd.ms-word.template.macroEnabled.12"}, + {".dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template"}, + {".dsp", "application/octet-stream"}, + {".dsw", "text/plain"}, + {".dtd", "text/xml"}, + {".dtsConfig", "text/xml"}, + {".dv", "video/x-dv"}, + {".dvi", "application/x-dvi"}, + {".dwf", "drawing/x-dwf"}, + {".dwp", "application/octet-stream"}, + {".dxr", "application/x-director"}, + {".eml", "message/rfc822"}, + {".emz", "application/octet-stream"}, + {".eot", "application/octet-stream"}, + {".eps", "application/postscript"}, + {".etl", "application/etl"}, + {".etx", "text/x-setext"}, + {".evy", "application/envoy"}, + {".exe", "application/octet-stream"}, + {".exe.config", "text/xml"}, + {".fdf", "application/vnd.fdf"}, + {".fif", "application/fractals"}, + {".filters", "Application/xml"}, + {".fla", "application/octet-stream"}, + {".flr", "x-world/x-vrml"}, + {".flv", "video/x-flv"}, + {".fsscript", "application/fsharp-script"}, + {".fsx", "application/fsharp-script"}, + {".generictest", "application/xml"}, + {".gif", "image/gif"}, + {".group", "text/x-ms-group"}, + {".gsm", "audio/x-gsm"}, + {".gtar", "application/x-gtar"}, + {".gz", "application/x-gzip"}, + {".h", "text/plain"}, + {".hdf", "application/x-hdf"}, + {".hdml", "text/x-hdml"}, + {".hhc", "application/x-oleobject"}, + {".hhk", "application/octet-stream"}, + {".hhp", "application/octet-stream"}, + {".hlp", "application/winhlp"}, + {".hpp", "text/plain"}, + {".hqx", "application/mac-binhex40"}, + {".hta", "application/hta"}, + {".htc", "text/x-component"}, + {".htm", "text/html"}, + {".html", "text/html"}, + {".htt", "text/webviewhtml"}, + {".hxa", "application/xml"}, + {".hxc", "application/xml"}, + {".hxd", "application/octet-stream"}, + {".hxe", "application/xml"}, + {".hxf", "application/xml"}, + {".hxh", "application/octet-stream"}, + {".hxi", "application/octet-stream"}, + {".hxk", "application/xml"}, + {".hxq", "application/octet-stream"}, + {".hxr", "application/octet-stream"}, + {".hxs", "application/octet-stream"}, + {".hxt", "text/html"}, + {".hxv", "application/xml"}, + {".hxw", "application/octet-stream"}, + {".hxx", "text/plain"}, + {".i", "text/plain"}, + {".ico", "image/x-icon"}, + {".ics", "application/octet-stream"}, + {".idl", "text/plain"}, + {".ief", "image/ief"}, + {".iii", "application/x-iphone"}, + {".inc", "text/plain"}, + {".inf", "application/octet-stream"}, + {".inl", "text/plain"}, + {".ins", "application/x-internet-signup"}, + {".ipa", "application/x-itunes-ipa"}, + {".ipg", "application/x-itunes-ipg"}, + {".ipproj", "text/plain"}, + {".ipsw", "application/x-itunes-ipsw"}, + {".iqy", "text/x-ms-iqy"}, + {".isp", "application/x-internet-signup"}, + {".ite", "application/x-itunes-ite"}, + {".itlp", "application/x-itunes-itlp"}, + {".itms", "application/x-itunes-itms"}, + {".itpc", "application/x-itunes-itpc"}, + {".IVF", "video/x-ivf"}, + {".jar", "application/java-archive"}, + {".java", "application/octet-stream"}, + {".jck", "application/liquidmotion"}, + {".jcz", "application/liquidmotion"}, + {".jfif", "image/pjpeg"}, + {".jnlp", "application/x-java-jnlp-file"}, + {".jpb", "application/octet-stream"}, + {".jpe", "image/jpeg"}, + {".jpeg", "image/jpeg"}, + {".jpg", "image/jpeg"}, + {".js", "application/x-javascript"}, + {".json", "application/json"}, + {".jsx", "text/jscript"}, + {".jsxbin", "text/plain"}, + {".latex", "application/x-latex"}, + {".library-ms", "application/windows-library+xml"}, + {".lit", "application/x-ms-reader"}, + {".loadtest", "application/xml"}, + {".lpk", "application/octet-stream"}, + {".lsf", "video/x-la-asf"}, + {".lst", "text/plain"}, + {".lsx", "video/x-la-asf"}, + {".lzh", "application/octet-stream"}, + {".m13", "application/x-msmediaview"}, + {".m14", "application/x-msmediaview"}, + {".m1v", "video/mpeg"}, + {".m2t", "video/vnd.dlna.mpeg-tts"}, + {".m2ts", "video/vnd.dlna.mpeg-tts"}, + {".m2v", "video/mpeg"}, + {".m3u", "audio/x-mpegurl"}, + {".m3u8", "audio/x-mpegurl"}, + {".m4a", "audio/m4a"}, + {".m4b", "audio/m4b"}, + {".m4p", "audio/m4p"}, + {".m4r", "audio/x-m4r"}, + {".m4v", "video/x-m4v"}, + {".mac", "image/x-macpaint"}, + {".mak", "text/plain"}, + {".man", "application/x-troff-man"}, + {".manifest", "application/x-ms-manifest"}, + {".map", "text/plain"}, + {".master", "application/xml"}, + {".mda", "application/msaccess"}, + {".mdb", "application/x-msaccess"}, + {".mde", "application/msaccess"}, + {".mdp", "application/octet-stream"}, + {".me", "application/x-troff-me"}, + {".mfp", "application/x-shockwave-flash"}, + {".mht", "message/rfc822"}, + {".mhtml", "message/rfc822"}, + {".mid", "audio/mid"}, + {".midi", "audio/mid"}, + {".mix", "application/octet-stream"}, + {".mk", "text/plain"}, + {".mmf", "application/x-smaf"}, + {".mno", "text/xml"}, + {".mny", "application/x-msmoney"}, + {".mod", "video/mpeg"}, + {".mov", "video/quicktime"}, + {".movie", "video/x-sgi-movie"}, + {".mp2", "video/mpeg"}, + {".mp2v", "video/mpeg"}, + {".mp3", "audio/mpeg"}, + {".mp4", "video/mp4"}, + {".mp4v", "video/mp4"}, + {".mpa", "video/mpeg"}, + {".mpe", "video/mpeg"}, + {".mpeg", "video/mpeg"}, + {".mpf", "application/vnd.ms-mediapackage"}, + {".mpg", "video/mpeg"}, + {".mpp", "application/vnd.ms-project"}, + {".mpv2", "video/mpeg"}, + {".mqv", "video/quicktime"}, + {".ms", "application/x-troff-ms"}, + {".msi", "application/octet-stream"}, + {".mso", "application/octet-stream"}, + {".mts", "video/vnd.dlna.mpeg-tts"}, + {".mtx", "application/xml"}, + {".mvb", "application/x-msmediaview"}, + {".mvc", "application/x-miva-compiled"}, + {".mxp", "application/x-mmxp"}, + {".nc", "application/x-netcdf"}, + {".nsc", "video/x-ms-asf"}, + {".nws", "message/rfc822"}, + {".ocx", "application/octet-stream"}, + {".oda", "application/oda"}, + {".odb", "application/vnd.oasis.opendocument.database"}, + {".odc", "application/vnd.oasis.opendocument.chart"}, + {".odf", "application/vnd.oasis.opendocument.formula"}, + {".odg", "application/vnd.oasis.opendocument.graphics"}, + {".odh", "text/plain"}, + {".odi", "application/vnd.oasis.opendocument.image"}, + {".odl", "text/plain"}, + {".odm", "application/vnd.oasis.opendocument.text-master"}, + {".odp", "application/vnd.oasis.opendocument.presentation"}, + {".ods", "application/vnd.oasis.opendocument.spreadsheet"}, + {".odt", "application/vnd.oasis.opendocument.text"}, + {".ogv", "video/ogg"}, + {".one", "application/onenote"}, + {".onea", "application/onenote"}, + {".onepkg", "application/onenote"}, + {".onetmp", "application/onenote"}, + {".onetoc", "application/onenote"}, + {".onetoc2", "application/onenote"}, + {".orderedtest", "application/xml"}, + {".osdx", "application/opensearchdescription+xml"}, + {".otg", "application/vnd.oasis.opendocument.graphics-template"}, + {".oth", "application/vnd.oasis.opendocument.text-web"}, + {".otp", "application/vnd.oasis.opendocument.presentation-template"}, + {".ots", "application/vnd.oasis.opendocument.spreadsheet-template"}, + {".ott", "application/vnd.oasis.opendocument.text-template"}, + {".oxt", "application/vnd.openofficeorg.extension"}, + {".p10", "application/pkcs10"}, + {".p12", "application/x-pkcs12"}, + {".p7b", "application/x-pkcs7-certificates"}, + {".p7c", "application/pkcs7-mime"}, + {".p7m", "application/pkcs7-mime"}, + {".p7r", "application/x-pkcs7-certreqresp"}, + {".p7s", "application/pkcs7-signature"}, + {".pbm", "image/x-portable-bitmap"}, + {".pcast", "application/x-podcast"}, + {".pct", "image/pict"}, + {".pcx", "application/octet-stream"}, + {".pcz", "application/octet-stream"}, + {".pdf", "application/pdf"}, + {".pfb", "application/octet-stream"}, + {".pfm", "application/octet-stream"}, + {".pfx", "application/x-pkcs12"}, + {".pgm", "image/x-portable-graymap"}, + {".pic", "image/pict"}, + {".pict", "image/pict"}, + {".pkgdef", "text/plain"}, + {".pkgundef", "text/plain"}, + {".pko", "application/vnd.ms-pki.pko"}, + {".pls", "audio/scpls"}, + {".pma", "application/x-perfmon"}, + {".pmc", "application/x-perfmon"}, + {".pml", "application/x-perfmon"}, + {".pmr", "application/x-perfmon"}, + {".pmw", "application/x-perfmon"}, + {".png", "image/png"}, + {".pnm", "image/x-portable-anymap"}, + {".pnt", "image/x-macpaint"}, + {".pntg", "image/x-macpaint"}, + {".pnz", "image/png"}, + {".pot", "application/vnd.ms-powerpoint"}, + {".potm", "application/vnd.ms-powerpoint.template.macroEnabled.12"}, + {".potx", "application/vnd.openxmlformats-officedocument.presentationml.template"}, + {".ppa", "application/vnd.ms-powerpoint"}, + {".ppam", "application/vnd.ms-powerpoint.addin.macroEnabled.12"}, + {".ppm", "image/x-portable-pixmap"}, + {".pps", "application/vnd.ms-powerpoint"}, + {".ppsm", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12"}, + {".ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow"}, + {".ppt", "application/vnd.ms-powerpoint"}, + {".pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12"}, + {".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"}, + {".prf", "application/pics-rules"}, + {".prm", "application/octet-stream"}, + {".prx", "application/octet-stream"}, + {".ps", "application/postscript"}, + {".psc1", "application/PowerShell"}, + {".psd", "application/octet-stream"}, + {".psess", "application/xml"}, + {".psm", "application/octet-stream"}, + {".psp", "application/octet-stream"}, + {".pub", "application/x-mspublisher"}, + {".pwz", "application/vnd.ms-powerpoint"}, + {".qht", "text/x-html-insertion"}, + {".qhtm", "text/x-html-insertion"}, + {".qt", "video/quicktime"}, + {".qti", "image/x-quicktime"}, + {".qtif", "image/x-quicktime"}, + {".qtl", "application/x-quicktimeplayer"}, + {".qxd", "application/octet-stream"}, + {".ra", "audio/x-pn-realaudio"}, + {".ram", "audio/x-pn-realaudio"}, + {".rar", "application/octet-stream"}, + {".ras", "image/x-cmu-raster"}, + {".rat", "application/rat-file"}, + {".rc", "text/plain"}, + {".rc2", "text/plain"}, + {".rct", "text/plain"}, + {".rdlc", "application/xml"}, + {".resx", "application/xml"}, + {".rf", "image/vnd.rn-realflash"}, + {".rgb", "image/x-rgb"}, + {".rgs", "text/plain"}, + {".rm", "application/vnd.rn-realmedia"}, + {".rmi", "audio/mid"}, + {".rmp", "application/vnd.rn-rn_music_package"}, + {".roff", "application/x-troff"}, + {".rpm", "audio/x-pn-realaudio-plugin"}, + {".rqy", "text/x-ms-rqy"}, + {".rtf", "application/rtf"}, + {".rtx", "text/richtext"}, + {".ruleset", "application/xml"}, + {".s", "text/plain"}, + {".safariextz", "application/x-safari-safariextz"}, + {".scd", "application/x-msschedule"}, + {".sct", "text/scriptlet"}, + {".sd2", "audio/x-sd2"}, + {".sdp", "application/sdp"}, + {".sea", "application/octet-stream"}, + {".searchConnector-ms", "application/windows-search-connector+xml"}, + {".setpay", "application/set-payment-initiation"}, + {".setreg", "application/set-registration-initiation"}, + {".settings", "application/xml"}, + {".sgimb", "application/x-sgimb"}, + {".sgml", "text/sgml"}, + {".sh", "application/x-sh"}, + {".shar", "application/x-shar"}, + {".shtml", "text/html"}, + {".sit", "application/x-stuffit"}, + {".sitemap", "application/xml"}, + {".skin", "application/xml"}, + {".sldm", "application/vnd.ms-powerpoint.slide.macroEnabled.12"}, + {".sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide"}, + {".slk", "application/vnd.ms-excel"}, + {".sln", "text/plain"}, + {".slupkg-ms", "application/x-ms-license"}, + {".smd", "audio/x-smd"}, + {".smi", "application/octet-stream"}, + {".smx", "audio/x-smd"}, + {".smz", "audio/x-smd"}, + {".snd", "audio/basic"}, + {".snippet", "application/xml"}, + {".snp", "application/octet-stream"}, + {".sol", "text/plain"}, + {".sor", "text/plain"}, + {".spc", "application/x-pkcs7-certificates"}, + {".spl", "application/futuresplash"}, + {".src", "application/x-wais-source"}, + {".srf", "text/plain"}, + {".SSISDeploymentManifest", "text/xml"}, + {".ssm", "application/streamingmedia"}, + {".sst", "application/vnd.ms-pki.certstore"}, + {".stl", "application/vnd.ms-pki.stl"}, + {".sv4cpio", "application/x-sv4cpio"}, + {".sv4crc", "application/x-sv4crc"}, + {".svc", "application/xml"}, + {".svg", "image/svg+xml"}, + {".swf", "application/x-shockwave-flash"}, + {".t", "application/x-troff"}, + {".tar", "application/x-tar"}, + {".tcl", "application/x-tcl"}, + {".testrunconfig", "application/xml"}, + {".testsettings", "application/xml"}, + {".tex", "application/x-tex"}, + {".texi", "application/x-texinfo"}, + {".texinfo", "application/x-texinfo"}, + {".tgz", "application/x-compressed"}, + {".thmx", "application/vnd.ms-officetheme"}, + {".thn", "application/octet-stream"}, + {".tif", "image/tiff"}, + {".tiff", "image/tiff"}, + {".tlh", "text/plain"}, + {".tli", "text/plain"}, + {".toc", "application/octet-stream"}, + {".tr", "application/x-troff"}, + {".trm", "application/x-msterminal"}, + {".trx", "application/xml"}, + {".ts", "video/vnd.dlna.mpeg-tts"}, + {".tsv", "text/tab-separated-values"}, + {".ttf", "application/octet-stream"}, + {".tts", "video/vnd.dlna.mpeg-tts"}, + {".txt", "text/plain"}, + {".u32", "application/octet-stream"}, + {".uls", "text/iuls"}, + {".user", "text/plain"}, + {".ustar", "application/x-ustar"}, + {".vb", "text/plain"}, + {".vbdproj", "text/plain"}, + {".vbk", "video/mpeg"}, + {".vbproj", "text/plain"}, + {".vbs", "text/vbscript"}, + {".vcf", "text/x-vcard"}, + {".vcproj", "Application/xml"}, + {".vcs", "text/plain"}, + {".vcxproj", "Application/xml"}, + {".vddproj", "text/plain"}, + {".vdp", "text/plain"}, + {".vdproj", "text/plain"}, + {".vdx", "application/vnd.ms-visio.viewer"}, + {".vml", "text/xml"}, + {".vscontent", "application/xml"}, + {".vsct", "text/xml"}, + {".vsd", "application/vnd.visio"}, + {".vsi", "application/ms-vsi"}, + {".vsix", "application/vsix"}, + {".vsixlangpack", "text/xml"}, + {".vsixmanifest", "text/xml"}, + {".vsmdi", "application/xml"}, + {".vspscc", "text/plain"}, + {".vss", "application/vnd.visio"}, + {".vsscc", "text/plain"}, + {".vssettings", "text/xml"}, + {".vssscc", "text/plain"}, + {".vst", "application/vnd.visio"}, + {".vstemplate", "text/xml"}, + {".vsto", "application/x-ms-vsto"}, + {".vsw", "application/vnd.visio"}, + {".vsx", "application/vnd.visio"}, + {".vtx", "application/vnd.visio"}, + {".wav", "audio/wav"}, + {".wave", "audio/wav"}, + {".wax", "audio/x-ms-wax"}, + {".wbk", "application/msword"}, + {".wbmp", "image/vnd.wap.wbmp"}, + {".wcm", "application/vnd.ms-works"}, + {".wdb", "application/vnd.ms-works"}, + {".wdp", "image/vnd.ms-photo"}, + {".webarchive", "application/x-safari-webarchive"}, + {".webm", "video/webm"}, + {".webtest", "application/xml"}, + {".wiq", "application/xml"}, + {".wiz", "application/msword"}, + {".wks", "application/vnd.ms-works"}, + {".WLMP", "application/wlmoviemaker"}, + {".wlpginstall", "application/x-wlpg-detect"}, + {".wlpginstall3", "application/x-wlpg3-detect"}, + {".wm", "video/x-ms-wm"}, + {".wma", "audio/x-ms-wma"}, + {".wmd", "application/x-ms-wmd"}, + {".wmf", "application/x-msmetafile"}, + {".wml", "text/vnd.wap.wml"}, + {".wmlc", "application/vnd.wap.wmlc"}, + {".wmls", "text/vnd.wap.wmlscript"}, + {".wmlsc", "application/vnd.wap.wmlscriptc"}, + {".wmp", "video/x-ms-wmp"}, + {".wmv", "video/x-ms-wmv"}, + {".wmx", "video/x-ms-wmx"}, + {".wmz", "application/x-ms-wmz"}, + {".wpl", "application/vnd.ms-wpl"}, + {".wps", "application/vnd.ms-works"}, + {".wri", "application/x-mswrite"}, + {".wrl", "x-world/x-vrml"}, + {".wrz", "x-world/x-vrml"}, + {".wsc", "text/scriptlet"}, + {".wsdl", "text/xml"}, + {".wvx", "video/x-ms-wvx"}, + {".x", "application/directx"}, + {".xaf", "x-world/x-vrml"}, + {".xaml", "application/xaml+xml"}, + {".xap", "application/x-silverlight-app"}, + {".xbap", "application/x-ms-xbap"}, + {".xbm", "image/x-xbitmap"}, + {".xdr", "text/plain"}, + {".xht", "application/xhtml+xml"}, + {".xhtml", "application/xhtml+xml"}, + {".xla", "application/vnd.ms-excel"}, + {".xlam", "application/vnd.ms-excel.addin.macroEnabled.12"}, + {".xlc", "application/vnd.ms-excel"}, + {".xld", "application/vnd.ms-excel"}, + {".xlk", "application/vnd.ms-excel"}, + {".xll", "application/vnd.ms-excel"}, + {".xlm", "application/vnd.ms-excel"}, + {".xls", "application/vnd.ms-excel"}, + {".xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12"}, + {".xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12"}, + {".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, + {".xlt", "application/vnd.ms-excel"}, + {".xltm", "application/vnd.ms-excel.template.macroEnabled.12"}, + {".xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template"}, + {".xlw", "application/vnd.ms-excel"}, + {".xml", "text/xml"}, + {".xmta", "application/xml"}, + {".xof", "x-world/x-vrml"}, + {".XOML", "text/plain"}, + {".xpm", "image/x-xpixmap"}, + {".xps", "application/vnd.ms-xpsdocument"}, + {".xrm-ms", "text/xml"}, + {".xsc", "application/xml"}, + {".xsd", "text/xml"}, + {".xsf", "text/xml"}, + {".xsl", "text/xml"}, + {".xslt", "text/xml"}, + {".xsn", "application/octet-stream"}, + {".xss", "application/xml"}, + {".xtp", "application/octet-stream"}, + {".xwd", "image/x-xwindowdump"}, + {".z", "application/x-compress"}, + {".zip", "application/zip"}, + }; + + /// + /// Instantiates the object. + /// + public MimeTypes() + { + } + + /// + /// Retrieve MIME type from file extension. + /// + /// File extension. + /// String containing MIME type. + public static string GetFromExtension(string extension) + { + if (String.IsNullOrEmpty(nameof(extension))) + return null; + + if (!extension.StartsWith(".")) + { + extension = "." + extension; + } + + string mime; + return data.TryGetValue(extension.ToLower(), out mime) ? mime : "application/octet-stream"; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Web/ObjectExtensions.cs b/EonaCat.Network/System/Web/ObjectExtensions.cs new file mode 100644 index 0000000..6a358fb --- /dev/null +++ b/EonaCat.Network/System/Web/ObjectExtensions.cs @@ -0,0 +1,47 @@ +using EonaCat.Json; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Object extensions. + /// + public static class ObjectExtensions + { + /// + /// Return a JSON string of the object. + /// + /// Object. + /// Enable or disable pretty print. + /// JSON string. + public static string ToJson(this object obj, bool pretty = false) + { + string json; + + if (pretty) + { + json = JsonHelper.ToJson( + obj, + Formatting.Indented, + new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore, + DateTimeZoneHandling = DateTimeZoneHandling.Local, + }); + } + else + { + json = JsonHelper.ToJson(obj, + new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore, + DateTimeZoneHandling = DateTimeZoneHandling.Local + }); + } + + return json; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Web/ParameterRoute.cs b/EonaCat.Network/System/Web/ParameterRoute.cs new file mode 100644 index 0000000..ddd2bf0 --- /dev/null +++ b/EonaCat.Network/System/Web/ParameterRoute.cs @@ -0,0 +1,70 @@ +using System; +using System.Threading.Tasks; +using EonaCat.Json; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Assign a method handler for when requests are received matching the supplied method and path containing parameters. + /// + public class ParameterRoute + { + /// + /// Globally-unique identifier. + /// + [JsonProperty(Order = -1)] + public string GUID { get; set; } = Guid.NewGuid().ToString(); + + /// + /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + /// + [JsonProperty(Order = 0)] + public HttpMethod Method { get; set; } = HttpMethod.GET; + + /// + /// The pattern against which the raw URL should be matched. + /// + [JsonProperty(Order = 1)] + public string Path { get; set; } = null; + + /// + /// The handler for the parameter route. + /// + [JsonIgnore] + public Func Handler { get; set; } = null; + + /// + /// User-supplied metadata. + /// + [JsonProperty(Order = 999)] + public object Metadata { get; set; } = null; + + /// + /// Create a new route object. + /// + /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + /// The pattern against which the raw URL should be matched. + /// The method that should be called to handle the request. + /// Globally-unique identifier. + /// User-supplied metadata. + public ParameterRoute(HttpMethod method, string path, Func handler, string guid = null, object metadata = null) + { + 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)) + 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/ParameterRouteAttribute.cs new file mode 100644 index 0000000..9e82c2b --- /dev/null +++ b/EonaCat.Network/System/Web/ParameterRouteAttribute.cs @@ -0,0 +1,53 @@ +using System; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Attribute that is used to mark methods as a parameter route. + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class ParameterRouteAttribute : Attribute + { + /// + /// The path to match, i.e. /{version}/api/{id}. + /// If a match is found, the Dictionary found in HttpRequest.Url.Parameters will contain keys for 'version' and 'id'. + /// + public string Path = null; + + /// + /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + /// + public HttpMethod Method = HttpMethod.GET; + + /// + /// Globally-unique identifier. + /// + public string GUID { get; set; } = Guid.NewGuid().ToString(); + + /// + /// User-supplied metadata. + /// + public object Metadata { get; set; } = null; + + /// + /// Instantiate the object. + /// + /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + /// The path to match, i.e. /{version}/api/{id}. + /// Globally-unique identifier. + /// User-supplied metadata. + public ParameterRouteAttribute(HttpMethod method, string path, string guid = null, object metadata = null) + { + Path = path; + Method = method; + + 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 new file mode 100644 index 0000000..fe63d69 --- /dev/null +++ b/EonaCat.Network/System/Web/ParameterRouteManager.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using EonaCat.UrlMatch; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Parameter route manager. Parameter routes are used for requests using any HTTP method to any path where parameters are defined in the URL. + /// For example, /{version}/api. + /// For a matching URL, the HttpRequest.Url.Parameters will contain a key called 'version' with the value found in the URL. + /// + public class ParameterRouteManager + { + /// + /// 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; + } + } + + private Matcher _Matcher = new Matcher(); + private readonly object _Lock = new object(); + private Dictionary> _Routes = new Dictionary>(); + + /// + /// Instantiate the object. + /// + public ParameterRouteManager() + { + } + + /// + /// Add a route. + /// + /// The HTTP method. + /// URL path, i.e. /path/to/resource. + /// Method to invoke. + /// Globally-unique identifier. + /// User-supplied metadata. + public void Add(HttpMethod method, string path, Func handler, string guid = null, object metadata = null) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); + if (handler == null) + throw new ArgumentNullException(nameof(handler)); + + lock (_Lock) + { + ParameterRoute pr = new ParameterRoute(method, path, handler, guid, metadata); + _Routes.Add(pr, handler); + } + } + + /// + /// Remove a route. + /// + /// The HTTP method. + /// URL path. + public void Remove(HttpMethod method, string path) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); + + lock (_Lock) + { + if (_Routes.Any(r => r.Key.Method == method && r.Key.Path.Equals(path))) + { + List removeList = _Routes.Where(r => r.Key.Method == method && r.Key.Path.Equals(path)) + .Select(r => r.Key) + .ToList(); + + foreach (ParameterRoute remove in removeList) + { + _Routes.Remove(remove); + } + } + } + } + + /// + /// Retrieve a parameter route. + /// + /// The HTTP method. + /// URL path. + /// ParameterRoute if the route exists, otherwise null. + public ParameterRoute Get(HttpMethod method, string path) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); + + lock (_Lock) + { + if (_Routes.Any(r => r.Key.Method == method && r.Key.Path.Equals(path))) + { + return _Routes.First(r => r.Key.Method == method && r.Key.Path.Equals(path)).Key; + } + } + + return null; + } + + /// + /// Check if a content route exists. + /// + /// The HTTP method. + /// URL path. + /// True if exists. + public bool Exists(HttpMethod method, string path) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); + + lock (_Lock) + { + return _Routes.Any(r => r.Key.Method == method && r.Key.Path.Equals(path)); + } + } + + /// + /// Match a request method and URL to a handler method. + /// + /// The HTTP method. + /// URL path. + /// Values extracted from the URL. + /// Matching route. + /// True if match exists. + public Func Match(HttpMethod method, string path, out Dictionary vals, out ParameterRoute pr) + { + pr = null; + vals = null; + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); + + string consolidatedPath = BuildConsolidatedPath(method, path); + + lock (_Lock) + { + foreach (KeyValuePair> route in _Routes) + { + if (_Matcher.Match( + consolidatedPath, + BuildConsolidatedPath(route.Key.Method, route.Key.Path), + out vals)) + { + pr = route.Key; + return route.Value; + } + } + } + + return null; + } + + private string BuildConsolidatedPath(HttpMethod method, string path) + { + return method.ToString() + " " + path; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Web/RequestEventArgs.cs b/EonaCat.Network/System/Web/RequestEventArgs.cs new file mode 100644 index 0000000..bf6543f --- /dev/null +++ b/EonaCat.Network/System/Web/RequestEventArgs.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Request event arguments. + /// + public class RequestEventArgs : EventArgs + { + /// + /// IP address. + /// + public string Ip { get; private set; } = null; + + /// + /// Port number. + /// + public int Port { get; private set; } = 0; + + /// + /// HTTP method. + /// + public HttpMethod Method { get; private set; } = HttpMethod.GET; + + /// + /// URL. + /// + public string Url { get; private set; } = null; + + /// + /// Query found in the URL. + /// + public Dictionary Query { get; private set; } = new Dictionary(); + + /// + /// Request headers. + /// + public Dictionary Headers { get; private set; } = new Dictionary(); + + /// + /// Content length. + /// + public long ContentLength { get; private set; } = 0; + + internal RequestEventArgs(HttpContext ctx) + { + Ip = ctx.Request.Source.IpAddress; + Port = ctx.Request.Source.Port; + Method = ctx.Request.Method; + Url = ctx.Request.Url.Full; + Query = ctx.Request.Query.Elements; + Headers = ctx.Request.Headers; + ContentLength = ctx.Request.ContentLength; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Web/ResponseEventArgs.cs b/EonaCat.Network/System/Web/ResponseEventArgs.cs new file mode 100644 index 0000000..0366bfc --- /dev/null +++ b/EonaCat.Network/System/Web/ResponseEventArgs.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Response event arguments. + /// + public class ResponseEventArgs : EventArgs + { + /// + /// IP address. + /// + public string Ip { get; private set; } = null; + + /// + /// Port number. + /// + public int Port { get; private set; } = 0; + + /// + /// HTTP method. + /// + public HttpMethod Method { get; private set; } = HttpMethod.GET; + + /// + /// URL. + /// + public string Url { get; private set; } = null; + + /// + /// Request query. + /// + public Dictionary Query { get; private set; } = new Dictionary(); + + /// + /// Request headers. + /// + public Dictionary RequestHeaders { get; private set; } = new Dictionary(); + + /// + /// Content length. + /// + public long RequestContentLength { get; private set; } = 0; + + /// + /// Response status. + /// + public int StatusCode { get; private set; } = 0; + + /// + /// Response headers. + /// + public Dictionary ResponseHeaders { get; private set; } = new Dictionary(); + + /// + /// Response content length. + /// + public long? ResponseContentLength { get; private set; } = 0; + + /// + /// Total time in processing the request and sending the response, in milliseconds. + /// + public double TotalMs { get; private set; } = 0; + + internal ResponseEventArgs(HttpContext ctx, double totalMs) + { + Ip = ctx.Request.Source.IpAddress; + Port = ctx.Request.Source.Port; + Method = ctx.Request.Method; + Url = ctx.Request.Url.Full; + Query = ctx.Request.Query.Elements; + RequestHeaders = ctx.Request.Headers; + RequestContentLength = ctx.Request.ContentLength; + StatusCode = ctx.Response.StatusCode; + ResponseContentLength = ctx.Response.ContentLength; + TotalMs = totalMs; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Web/RouteTypeEnum.cs b/EonaCat.Network/System/Web/RouteTypeEnum.cs new file mode 100644 index 0000000..1701369 --- /dev/null +++ b/EonaCat.Network/System/Web/RouteTypeEnum.cs @@ -0,0 +1,45 @@ +using System.Runtime.Serialization; +using EonaCat.Json.Converters; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Route type. + /// + [EonaCat.Json.Converter(typeof(StringEnumConverter))] + public enum RouteTypeEnum + { + /// + /// Default route. + /// + [EnumMember(Value = "Default")] + Default, + + /// + /// Content route. + /// + [EnumMember(Value = "Content")] + Content, + + /// + /// Static route. + /// + [EnumMember(Value = "Static")] + Static, + + /// + /// Parameter route. + /// + [EnumMember(Value = "Parameter")] + Parameter, + + /// + /// Dynamic route. + /// + [EnumMember(Value = "Dynamic")] + Dynamic + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Web/SerializationHelper.cs b/EonaCat.Network/System/Web/SerializationHelper.cs new file mode 100644 index 0000000..44baa42 --- /dev/null +++ b/EonaCat.Network/System/Web/SerializationHelper.cs @@ -0,0 +1,47 @@ +using System; +using EonaCat.Json; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + internal class SerializationHelper + { + internal static T DeserializeJson(string 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) + { + json = JsonHelper.ToJson(obj, + Formatting.Indented, + new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore, + DateTimeZoneHandling = DateTimeZoneHandling.Local, + }); + } + else + { + json = JsonHelper.ToJson(obj, + new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore, + DateTimeZoneHandling = DateTimeZoneHandling.Local + }); + } + + return json; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Web/StaticRoute.cs b/EonaCat.Network/System/Web/StaticRoute.cs new file mode 100644 index 0000000..af9edd2 --- /dev/null +++ b/EonaCat.Network/System/Web/StaticRoute.cs @@ -0,0 +1,76 @@ +using System; +using System.Threading.Tasks; +using EonaCat.Json; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Assign a method handler for when requests are received matching the supplied method and path. + /// + public class StaticRoute + { + /// + /// Globally-unique identifier. + /// + [JsonProperty(Order = -1)] + public string GUID { get; set; } = Guid.NewGuid().ToString(); + + /// + /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + /// + [JsonProperty(Order = 0)] + public HttpMethod Method { get; set; } = HttpMethod.GET; + + /// + /// The raw URL, i.e. /foo/bar/. Be sure this begins and ends with '/'. + /// + [JsonProperty(Order = 1)] + public string Path { get; set; } = null; + + /// + /// The handler for the static route. + /// + [JsonIgnore] + public Func Handler { get; set; } = null; + + /// + /// User-supplied metadata. + /// + [JsonProperty(Order = 999)] + public object Metadata { get; set; } = null; + + /// + /// Create a new route object. + /// + /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + /// The raw URL, i.e. /foo/bar/. Be sure this begins and ends with '/'. + /// The method that should be called to handle the request. + /// Globally-unique identifier. + /// User-supplied metadata. + public StaticRoute(HttpMethod method, string path, Func handler, string guid = null, object metadata = null) + { + 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)) + 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/StaticRouteAttribute.cs new file mode 100644 index 0000000..4194628 --- /dev/null +++ b/EonaCat.Network/System/Web/StaticRouteAttribute.cs @@ -0,0 +1,52 @@ +using System; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Attribute that is used to mark methods as a static route. + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class StaticRouteAttribute : Attribute + { + /// + /// The raw URL, i.e. /foo/bar/. Be sure this begins and ends with '/'. + /// + public string Path = null; + + /// + /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + /// + public HttpMethod Method = HttpMethod.GET; + + /// + /// Globally-unique identifier. + /// + public string GUID { get; set; } = Guid.NewGuid().ToString(); + + /// + /// User-supplied metadata. + /// + public object Metadata { get; set; } = null; + + /// + /// Instantiate the object. + /// + /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + /// The raw URL, i.e. /foo/bar/. Be sure this begins and ends with '/'. + /// Globally-unique identifier. + /// User-supplied metadata. + public StaticRouteAttribute(HttpMethod method, string path, string guid = null, object metadata = null) + { + Path = path; + Method = method; + + 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 new file mode 100644 index 0000000..883bee7 --- /dev/null +++ b/EonaCat.Network/System/Web/StaticRouteManager.cs @@ -0,0 +1,200 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace EonaCat.Network +{ + // This file is part of the EonaCat project(s) which is released under the Apache License. + // See the LICENSE file or go to https://EonaCat.com/License for full license details. + + /// + /// Static route manager. Static routes are used for requests using any HTTP method to a specific path. + /// + public class StaticRouteManager + { + private List _Routes = new List(); + private readonly object _Lock = new object(); + + /// + /// Instantiate the object. + /// + public StaticRouteManager() + { + } + + /// + /// Add a route. + /// + /// The HTTP method. + /// URL path, i.e. /path/to/resource. + /// Method to invoke. + /// Globally-unique identifier. + /// User-supplied metadata. + public void Add(HttpMethod method, string path, Func handler, string guid = null, object metadata = null) + { + 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); + } + + /// + /// Remove a route. + /// + /// The HTTP method. + /// URL path. + public void Remove(HttpMethod method, string path) + { + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); + + StaticRoute r = Get(method, path); + if (r == null || r == default(StaticRoute)) + { + return; + } + else + { + lock (_Lock) + { + _Routes.Remove(r); + } + + return; + } + } + + /// + /// Retrieve a static route. + /// + /// The HTTP method. + /// URL path. + /// StaticRoute if the route exists, otherwise null. + public StaticRoute Get(HttpMethod method, string 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) + { + StaticRoute curr = _Routes.FirstOrDefault(i => i.Method == method && i.Path == path); + if (curr == null || curr == default(StaticRoute)) + { + return null; + } + else + { + return curr; + } + } + } + + /// + /// Check if a static route exists. + /// + /// The HTTP method. + /// URL path. + /// True if exists. + public bool Exists(HttpMethod method, string 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) + { + StaticRoute curr = _Routes.FirstOrDefault(i => i.Method == method && i.Path == path); + if (curr == null || curr == default(StaticRoute)) + { + return false; + } + } + + return true; + } + + /// + /// Match a request method and URL to a handler method. + /// + /// The HTTP method. + /// URL path. + /// Matching route. + /// Method to invoke. + public Func Match(HttpMethod method, string path, out StaticRoute route) + { + route = null; + if (String.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); + + path = path.ToLower(); + if (!path.StartsWith("/")) + path = "/" + path; + if (!path.EndsWith("/")) + path = path + "/"; + + lock (_Lock) + { + StaticRoute curr = _Routes.FirstOrDefault(i => i.Method == method && i.Path == path); + if (curr == null || curr == default(StaticRoute)) + { + return null; + } + else + { + route = curr; + return curr.Handler; + } + } + } + + 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)) + { + return; + } + + lock (_Lock) + { + _Routes.Add(route); + } + } + + private void Remove(StaticRoute route) + { + if (route == null) + throw new ArgumentNullException(nameof(route)); + + lock (_Lock) + { + _Routes.Remove(route); + } + + return; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/icon.png b/EonaCat.Network/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0595b89951c74a6c669463d4e2ba62274036fa30 GIT binary patch literal 89562 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 IMBDrS02~H<8~^|S literal 0 HcmV?d00001 diff --git a/LICENSE b/LICENSE index 137069b..bc121e6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,73 +1,206 @@ Apache License + Version 2.0, January 2004 -http://www.apache.org/licenses/ -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, +AND DISTRIBUTION -1. Definitions. + 1. Definitions. -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. +"License" shall mean the terms and conditions for use, reproduction, and distribution +as defined by Sections 1 through 9 of this document. -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct +or indirect, to cause the direction or management of such entity, whether +by contract or otherwise, or (ii) ownership of fifty percent (50%) or more +of the outstanding shares, or (iii) beneficial ownership of such entity. -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions +granted by this License. -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. +"Object" form shall mean any form resulting from mechanical transformation +or translation of a Source form, including but not limited to compiled object +code, generated documentation, and conversions to other media types. -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and +"Work" shall mean the work of authorship, whether in Source or Object form, +made available under the License, as indicated by a copyright notice that +is included in or attached to the work (an example is provided in the Appendix +below). - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and +"Derivative Works" shall mean any work, whether in Source or Object form, +that is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative +Works shall not include works that remain separable from, or merely link (or +bind by name) to the interfaces of, the Work and Derivative Works thereof. - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative +Works thereof, that is intentionally submitted to Licensor for inclusion in +the Work by the copyright owner or by an individual or Legal Entity authorized +to submit on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication +sent to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor +for the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently incorporated +within the Work. -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. +2. Grant of Copyright License. Subject to the terms and conditions of this +License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable copyright license to reproduce, prepare +Derivative Works of, publicly display, publicly perform, sublicense, and distribute +the Work and such Derivative Works in Source or Object form. -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. +3. Grant of Patent License. Subject to the terms and conditions of this License, +each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this section) patent +license to make, have made, use, offer to sell, sell, import, and otherwise +transfer the Work, where such license applies only to those patent claims +licensable by such Contributor that are necessarily infringed by their Contribution(s) +alone or by combination of their Contribution(s) with the Work to which such +Contribution(s) was submitted. If You institute patent litigation against +any entity (including a cross-claim or counterclaim in a lawsuit) alleging +that the Work or a Contribution incorporated within the Work constitutes direct +or contributory patent infringement, then any patent licenses granted to You +under this License for that Work shall terminate as of the date such litigation +is filed. -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. +4. Redistribution. You may reproduce and distribute copies of the Work or +Derivative Works thereof in any medium, with or without modifications, and +in Source or Object form, provided that You meet the following conditions: -END OF TERMS AND CONDITIONS +(a) You must give any other recipients of the Work or Derivative Works a copy +of this License; and + +(b) You must cause any modified files to carry prominent notices stating that +You changed the files; and + +(c) You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source +form of the Work, excluding those notices that do not pertain to any part +of the Derivative Works; and + +(d) If the Work includes a "NOTICE" text file as part of its distribution, +then any Derivative Works that You distribute must include a readable copy +of the attribution notices contained within such NOTICE file, excluding those +notices that do not pertain to any part of the Derivative Works, in at least +one of the following places: within a NOTICE text file distributed as part +of the Derivative Works; within the Source form or documentation, if provided +along with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents +of the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works +that You distribute, alongside or as an addendum to the NOTICE text from the +Work, provided that such additional attribution notices cannot be construed +as modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, +or distribution of Your modifications, or for any such Derivative Works as +a whole, provided Your use, reproduction, and distribution of the Work otherwise +complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any +Contribution intentionally submitted for inclusion in the Work by You to the +Licensor shall be under the terms and conditions of this License, without +any additional terms or conditions. Notwithstanding the above, nothing herein +shall supersede or modify the terms of any separate license agreement you +may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, +trademarks, service marks, or product names of the Licensor, except as required +for reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to +in writing, Licensor provides the Work (and each Contributor provides its +Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied, including, without limitation, any warranties +or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR +A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness +of using or redistributing the Work and assume any risks associated with Your +exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether +in tort (including negligence), contract, or otherwise, unless required by +applicable law (such as deliberate and grossly negligent acts) or agreed to +in writing, shall any Contributor be liable to You for damages, including +any direct, indirect, special, incidental, or consequential damages of any +character arising as a result of this License or out of the use or inability +to use the Work (including but not limited to damages for loss of goodwill, +work stoppage, computer failure or malfunction, or any and all other commercial +damages or losses), even if such Contributor has been advised of the possibility +of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work +or Derivative Works thereof, You may choose to offer, and charge a fee for, +acceptance of support, warranty, indemnity, or other liability obligations +and/or rights consistent with this License. However, in accepting such obligations, +You may act only on Your own behalf and on Your sole responsibility, not on +behalf of any other Contributor, and only if You agree to indemnify, defend, +and hold each Contributor harmless for any liability incurred by, or claims +asserted against, such Contributor by reason of your accepting any such warranty +or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own identifying +information. (Don't include the brackets!) The text should be enclosed in +the appropriate comment syntax for the file format. We also recommend that +a file or class name and description of purpose be included on the same "printed +page" as the copyright notice for easier identification within third-party +archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 +http://www.apache.org/licenses/LICENSE-2.0unless required by applicable law or agreed to in writing, software -Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index f7fa2f4..b1ea2f3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ -# EonaCat.Network +# EonaCat Network -EonaCat.Network \ No newline at end of file +------------ + +EonaCat Network Library \ No newline at end of file diff --git a/icon.png b/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0595b89951c74a6c669463d4e2ba62274036fa30 GIT binary patch literal 89562 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 IMBDrS02~H<8~^|S literal 0 HcmV?d00001