Initial version

This commit is contained in:
EonaCat 2023-01-11 10:33:32 +01:00
parent bad4fc7165
commit d8b820ad7f
110 changed files with 12341 additions and 99 deletions

76
.gitignore vendored
View File

@ -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/

29
EonaCat.Network.sln Normal file
View File

@ -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

View File

@ -0,0 +1,56 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<LangVersion>Latest</LangVersion>
<TargetFrameworks>
netstandard2.0;
netstandard2.1;
net5.0;
net6.0;
net7.0;
</TargetFrameworks>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageId>EonaCat.Network</PackageId>
<Authors>EonaCat (Jeroen Saey)</Authors>
<Company>EonaCat (Jeroen Saey)</Company>
<Product>EonaCat.Network</Product>
<Copyright>EonaCat (Jeroen Saey)</Copyright>
<PackageProjectUrl>https://www.nuget.org/packages/EonaCat.Network/</PackageProjectUrl>
<PackageTags>EonaCat, Network, .NET Standard, EonaCatHelpers, Jeroen, Saey, Protocol, Quic, UDP, TCP, Web, Server</PackageTags>
<PackageReleaseNotes></PackageReleaseNotes>
<Description>EonaCat Networking library with Quic, TCP, UDP and a Webserver</Description>
<Version>1.0.0</Version>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
<PackageIcon>icon.png</PackageIcon>
</PropertyGroup>
<PropertyGroup>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<PackageReadmeFile>README.md</PackageReadmeFile>
<SignAssembly>False</SignAssembly>
<Title>EonaCat.Network</Title>
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
</PropertyGroup>
<ItemGroup>
<None Include="..\icon.png">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Include="..\LICENSE">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Include="..\README.md">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="EonaCat.LogSystem" Version="1.0.0" />
<PackageReference Include="EonaCat.Matchers" Version="1.0.0" />
</ItemGroup>
</Project>

View File

@ -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

View File

@ -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<QuicConnectionEventArgs> OnQuicClientConnected;
public static event EventHandler<QuicStreamEventArgs> OnQuicStreamOpened;
public static event EventHandler<QuicStreamEventArgs> OnQuicStreamDataReceived;
/// <summary>
/// Start a Quic server
/// </summary>
/// <returns><c>true</c>, start successfully, <c>false</c> did not start successfully.</returns>
/// <param name="ip">The ip bound to the server</param>
/// <param name="port">The listening port. (default: 11000)</param>
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;
}
/// <summary>
/// Fired when Client is connected
/// </summary>
/// <param name="connection">The new connection</param>
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);
}
/// <summary>
/// Stop the Quic server
/// </summary>
/// <returns></returns>
public static bool QuicStopServer()
{
if (_quicServer != null)
{
_quicServer.Close();
Logger.Info($"The Quic server has been successfully stopped");
}
return true;
}
/// <summary>
/// Start a Tcp server
/// </summary>
/// <returns><c>true</c>, start successfully, <c>false</c> did not start successfully.</returns>
/// <param name="OnConnectCallBack">The callback function triggered when there is a client connection. The value passed in is the only client Token generated by the server.</param>
/// <param name="OnReceivedCallBack">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.</param>
/// <param name="ip">The ip bound to the server. (default: ALL INTERFACES)</param>
/// <param name="port">The listening port. (default: 8081)</param>
/// <param name="iPType">ip type v4 or v6.</param>
/// <param name="listen">Allowed concurrent connections (default: 10000)</param>
public static bool TcpStart(
Action<string> OnConnectCallBack,
Func<string, byte[], byte[]> 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;
}
}
/// <summary>
/// Stop Tcp server
/// </summary>
public static void TcpStop()
{
if (_tcpServer != null)
{
_tcpServer.ShutDownServer();
}
}
/// <summary>
/// Broadcast a message to all clients on Tcp
/// </summary>
/// <param name="message">Message.</param>
public static void TcpBroadCast(byte[] message)
{
if (_tcpServer != null)
{
_tcpServer.BroadCastMessageToAllClients(message);
}
}
/// <summary>
/// Send a message to the specified client according to the client Token
/// </summary>
/// <returns><c>true</c>, sent successfully, <c>false</c> did not send successfully.</returns>
/// <param name="clientToken">Client Token.</param>
/// <param name="message">The message to be sent.</param>
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;
}
/// <summary>
/// Close a client node
/// </summary>
/// <param name="clientToken">Client Token.</param>
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;
}
}
/// <summary>
/// handler
/// </summary>
private static TcpServer _tcpServer;
/// <summary>
/// Start a Tcp client
/// </summary>
/// <returns><c>true</c>, i started successfully, <c>false</c> did not start successfully.</returns>
/// <param name="ip">The IP to connect.</param>
/// <param name="port">Port.</param>
/// <param name="OnReceived">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.</param>
public static bool TcpStart(string ip, int port, Func<byte[], byte[]> OnReceived, IPType ipType = IPType.IPv4)
{
_tcpClient = new TcpClient(ip, port, OnReceived, ipType);
return true;
}
/// <summary>
/// Tcp client sends a message
/// </summary>
/// <param name="msg">Message.</param>
public static void TcpSend(byte[] msg)
{
if (_tcpClient == null)
{
return;
}
_tcpClient.SendDataToServer(msg);
}
/// <summary>
/// handler
/// </summary>
private static TcpClient _tcpClient;
/// <summary>
/// Start Udp server
/// </summary>
/// <returns><c>true</c>, start successfully, <c>false</c> did not start successfully.</returns>
/// <param name="port">Port. (default: 8082)</param>
/// <param name="ResponseCallBack"> 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.</param >
/// <param name="iPType">IPv4 or IPv6.</param>
/// <param name="bufferSize">Buffer size.</param>
public static bool UdpStart(Func<System.Net.EndPoint, byte[], byte[]> 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;
}
}
/// <summary>
/// Stop Udp server
/// </summary>
public static void UdpStop()
{
if (_udpHandler != null)
{
_udpHandler.StopServer();
}
}
/// <summary>
/// Set the Udp server's fallback method
/// </summary>
/// <param name="responseCallBack"> 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. </param>
public static void UDPCallBack(Func<System.Net.EndPoint, byte[], byte[]> responseCallBack)
{
if (_udpHandler != null)
{
_udpHandler.ResponseCallback = responseCallBack;
}
}
/// <summary>
/// Send a Udp message
/// </summary>
/// <param name="ip">Ip.</param>
/// <param name="port">Port.</param>
/// <param name="message">Message.</param>
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);
}
/// <summary>
/// Send a Udp message
/// </summary>
/// <param name="target">Target.</param>
/// <param name="message">Message.</param>
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);
}
/// <summary>
/// Get Udp server handle
/// </summary>
/// <returns>The UDP server.</returns>
public static Udp GetUDPServer()
{
return _udpHandler;
}
/// <summary>
/// handler
/// </summary>
private static Udp _udpHandler;
/// <summary>
/// Character bit encoding type web
/// </summary>
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();
}
}
/// <summary>
/// IP type enumeration
/// </summary>
public enum IPType : byte
{
IPv4,
IPv6
}
}

View File

@ -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.
/// <summary>
/// Since UDP is a stateless protocol, the ConnectionPool is used as a Conenction Manager to
/// route packets to the right "Connection".
/// </summary>
internal static class ConnectionPool
{
/// <summary>
/// Starting point for connection identifiers.
/// ConnectionId's are incremented sequentially by 1.
/// </summary>
private static NumberSpace _ns = new NumberSpace(QuicSettings.MaximumConnectionIds);
private static Dictionary<UInt64, QuicConnection> _pool = new Dictionary<UInt64, QuicConnection>();
private static List<QuicConnection> _draining = new List<QuicConnection>();
/// <summary>
/// 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.
/// </summary>
/// <param name="id">Connection Id</param>
/// <returns></returns>
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];
}
}
}

View File

@ -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
}
}

View File

@ -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<UInt64, QuicStream> _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; }
/// <summary>
/// Creates a new stream for sending/receiving data.
/// </summary>
/// <param name="type">Type of the stream (Uni-Bidirectional)</param>
/// <returns>A new stream instance or Null if the connection is terminated.</returns>
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<Frame> 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<UInt64, QuicStream>();
_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;
}
/// <summary>
/// Client only!
/// </summary>
/// <returns></returns>
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);
}
/// <summary>
/// Used to send protocol packets to the peer.
/// </summary>
/// <param name="packet"></param>
/// <returns></returns>
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;
}
}
}

View File

@ -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.";
}
}

View File

@ -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.
/// <summary>
/// Wrapper to represent the stream.
/// </summary>
public class QuicStreamContext
{
///// <summary>
///// The connection's context.
///// </summary>
//public QuicContext ConnectionContext { get; set; }
/// <summary>
/// Data received
/// </summary>
public byte[] Data { get; private set; }
/// <summary>
/// Unique stream identifier
/// </summary>
public UInt64 StreamId { get; private set; }
/// <summary>
/// Send data to the client.
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
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; }
/// <summary>
/// Internal constructor to prevent creating the context outside the scope of Quic.
/// </summary>
/// <param name="stream"></param>
internal QuicStreamContext(QuicStream stream)
{
Stream = stream;
StreamId = stream.StreamId;
}
internal void SetData(byte[] data)
{
Data = data;
}
}
}

View File

@ -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);
}

View File

@ -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; }
}
}

View File

@ -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)
{
}
}
}

View File

@ -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)
{
}
}
}

View File

@ -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)
{
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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
}
}

View File

@ -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)
{
}
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
/// <summary>
/// 0x1d not yet supported (Application Protocol Error)
/// </summary>
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<byte> result = new List<byte>();
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();
}
}
}

View File

@ -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();
}
}
}

View File

@ -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<byte> result = new List<byte>();
result.Add(Type);
result.AddRange(MaximumData.ToByteArray());
return result.ToArray();
}
}
}

View File

@ -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.
/// <summary>
/// Data encapsulation unit for a Packet.
/// </summary>
public abstract class Frame
{
public abstract byte Type { get; }
public abstract byte[] Encode();
public abstract void Decode(ByteArray array);
}
}

View File

@ -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<byte> result = new List<byte>();
result.Add(Type);
result.AddRange(MaximumData.ToByteArray());
return result.ToArray();
}
}
}

View File

@ -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<byte> result = new List<byte>();
result.Add(Type);
result.AddRange(StreamId.ToByteArray());
result.AddRange(MaximumStreamData.ToByteArray());
return result.ToArray();
}
}
}

View File

@ -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<byte> result = new List<byte>();
result.Add(Type);
result.AddRange(MaximumStreams.ToByteArray());
return result.ToArray();
}
}
}

View File

@ -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();
}
}
}

View File

@ -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();
}
}
}

View File

@ -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<byte> data = new List<byte>();
data.Add(Type);
return data.ToArray();
}
}
}

View File

@ -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();
}
}
}

View File

@ -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();
}
}
}

View File

@ -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<byte> data = new List<byte>();
data.Add(Type);
return data.ToArray();
}
}
}

View File

@ -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<byte> result = new List<byte>();
result.Add(Type);
result.AddRange(StreamId.ToByteArray());
result.AddRange(ApplicationProtocolErrorCode.ToByteArray());
result.AddRange(FinalSize.ToByteArray());
return result.ToArray();
}
}
}

View File

@ -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();
}
}
}

View File

@ -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();
}
}
}

View File

@ -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<byte> result = new List<byte>();
result.Add(Type);
result.AddRange(StreamId.ToByteArray());
result.AddRange(MaximumStreamData.ToByteArray());
return result.ToArray();
}
}
}

View File

@ -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<byte> result = new List<byte>();
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();
}
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}
}

View File

@ -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
}
}

View File

@ -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<byte> result = new List<byte>();
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();
}
}
}

View File

@ -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<byte> result = new List<byte>();
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;
}
}
}

View File

@ -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.
/// <summary>
/// Base data transfer unit of QUIC Transport.
/// </summary>
public abstract class Packet
{
protected List<Frame> _frames = new List<Frame>();
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<Frame> 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<byte> result = new List<byte>();
foreach (Frame frame in _frames)
{
result.AddRange(frame.Encode());
}
return result.ToArray();
}
}
}

View File

@ -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<byte> result = new List<byte>();
result.Add((byte)(Type | (PacketNumber.Size - 1)));
result.AddRange(DestinationConnectionId.ToByteArray());
byte[] pnBytes = PacketNumber;
result.AddRange(pnBytes);
result.AddRange(frames);
return result.ToArray();
}
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}
}

View File

@ -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
}
}

View File

@ -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
{
/// <summary>
/// Path Maximum Transmission Unit. Indicates the mandatory initial packet capacity, and the maximum UDP packet capacity.
/// </summary>
public const int PMTU = 1200;
/// <summary>
/// Does the server want the first connected client to decide it's initial connection id?
/// </summary>
public const bool CanAcceptInitialClientConnectionId = false;
/// <summary>
/// TBD. quic-transport 5.1.
/// </summary>
public const int MaximumConnectionIds = 8;
/// <summary>
/// Maximum number of streams that connection can handle.
/// </summary>
public const int MaximumStreamId = 128;
/// <summary>
/// Maximum packets that can be transferred before any data transfer (loss of packets, packet resent, infinite ack loop)
/// </summary>
public const int MaximumInitialPacketNumber = 100;
/// <summary>
/// Should the server buffer packets that came before the initial packet?
/// </summary>
public const bool ShouldBufferPacketsBeforeConnection = false;
/// <summary>
/// Limit the maximum number of frames a packet can carry.
/// </summary>
public const int MaximumFramesPerPacket = 10;
/// <summary>
/// Maximum data that can be transferred for a Connection.
/// Currently 10MB.
/// </summary>
public const int MaxData = 10 * 1000 * 1000;
/// <summary>
/// Maximum data that can be transferred for a Stream.
/// Currently 0.078125 MB, which is MaxData / MaximumStreamId
/// </summary>
public const int MaxStreamData = 78125;
}
}

View File

@ -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<UInt32> SupportedVersions = new List<UInt32>() { 15, 16 };
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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.
/// <summary>
/// Quic Client. Used for sending and receiving data from a Quic Server.
/// </summary>
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();
}
/// <summary>
/// Connect to a remote server.
/// </summary>
/// <param name="ip">Ip Address</param>
/// <param name="port">Port</param>
/// <returns></returns>
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;
}
/// <summary>
/// Handles initial packet's frames. (In most cases protocol frames)
/// </summary>
/// <param name="packet"></param>
private void HandleInitialFrames(Packet packet)
{
List<Frame> 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;
}
}
/// <summary>
/// Create a new connection
/// </summary>
/// <param name="connectionId"></param>
/// <param name="peerConnectionId"></param>
private void EstablishConnection(IntegerParts connectionId, IntegerParts peerConnectionId)
{
ConnectionData connection = new ConnectionData(_pwt, connectionId, peerConnectionId);
_connection = new QuicConnection(connection);
}
}
}

View File

@ -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.
/// <summary>
/// Quic Server - a Quic server that processes incoming connections and if possible sends back data on it's peers.
/// </summary>
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;
/// <summary>
/// Create a new instance of QuicListener.
/// </summary>
/// <param name="port">The port that the server will listen on.</param>
public QuicServer(string hostName, int port)
{
_started = false;
_port = port;
_hostname = hostName;
_unpacker = new Unpacker();
_packetCreator = new InitialPacketCreator();
}
/// <summary>
/// Starts the listener.
/// </summary>
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);
}
}
}
/// <summary>
/// Stops the listener.
/// </summary>
public void Close()
{
if (_started)
_client.Close();
}
/// <summary>
/// Processes incomming initial packet and creates or halts a connection.
/// </summary>
/// <param name="packet">Initial Packet</param>
/// <param name="endPoint">Peer's endpoint</param>
/// <returns></returns>
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;
}
}
}

View File

@ -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
{
/// <summary>
/// Processes short header packet, by distributing the frames towards connections.
/// </summary>
/// <param name="packet"></param>
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());
}
}
}

View File

@ -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.
/// <summary>
/// Virtual multiplexing channel.
/// </summary>
public class QuicStream
{
private SortedList<UInt64, byte[]> _data = new SortedList<ulong, byte[]>();
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;
}
/// <summary>
/// Client only!
/// </summary>
/// <returns></returns>
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;
}
}
}

View File

@ -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
}
}

View File

@ -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<byte[], byte[]> OnReceived;
public TcpClient(string serverIpAddress, int serverIpPort, Func<byte[], byte[]> 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
);
}
}
}

View File

@ -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;
/// <summary>
/// The callback function when the message is received The return value is directly returned to the client. If null is returned, no reply
/// </summary>
public Func<string, byte[], byte[]> ResponseCallBack;
/// <summary>
/// Send a message to the client of this node
/// </summary>
/// <param name="message">message.</param>
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<Exception> OnDisconnected;
public int BufferSize = 1024;
private byte[] buffer;
public TcpConnectedPeer(
string token,
Socket socket,
Func<string, byte[], byte[]> 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);
}
}
}
}

View File

@ -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<string> ClientTokens = new List<string>();
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;
/// <summary>
/// Pass in the client Token
/// </summary>
public Action<string> OnClientConnected;
/// <summary>
/// Incoming client Token, message msg
/// </summary>
public Func<string, byte[], byte[]> 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<string> OnClientConnected,
Func<string, byte[], byte[]> OnClientReceived,
int _ConcurrencyVolumn)
{
ipAddress = _IPAddress;
port = _port;
ConcurrencyVolumn = _ConcurrencyVolumn;
transportProtocol = _transProtocol;
this.OnClientConnected = OnClientConnected;
this.OnClientReceived = OnClientReceived;
}
}
}

View File

@ -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
{
/// <summary>
/// Read a configuration file (varName=varValue format #beginning with comments)
/// </summary>
/// <returns>Parameter list.</returns>
/// <param name="filePath">File path.</param>
public static Dictionary<string, string> ReadFile(string filePath)
{
FileReader handler = new FileReader(filePath);
Dictionary<string, string> 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<string, string> Read()
{
Dictionary<string, string> result = new Dictionary<string, string>();
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;
}
}
}

View File

@ -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
{
/// <summary>
/// Write a file
/// </summary>
/// <param name="filePath">File address</param>
/// <param name="content">Write content.</param>
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();
}
}
}

View File

@ -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()
{ }
/// <summary>
/// Port scan
/// </summary>
/// <param name="IPPrefix">the c prefix of IP such as "192.168.0." </param>
/// <param name="DStart">Start of segment D such as 1.</param>
/// <param name="DEnd">The end of section D such as 255.</param>
/// <param name="port">Detected port.</param>
/// <param name="ConnectEvent">return of the detection result Pass in the IPAndPort terminal class object and the boolean status of whether it is turned on.</param>
public static void IPv4ScanPort(string IPPrefix, int DStart, int DEnd, int port, Action<IPAndPort, bool> 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<Thread>();
// 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;
}
/// <summary>
/// Stop all port scanning threads
/// </summary>
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<Thread> scanThreads;
private static Action<IPAndPort, bool> 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?)$");
}
/// <summary>
/// Get the internal network IP
/// </summary>
/// <returns>The local ip.</returns>
public static string[] GetLocalIP()
{
string name = Dns.GetHostName();
List<string> result = new List<string>();
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;
}
}
}

View File

@ -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
{
/// <summary>
/// Match everything from the source
/// </summary>
/// <returns>matched data.</returns>
/// <param name="sourceData"> source text.</param>
/// <param name="regexPrefix"> matches the prefix.</param>
/// <param name="regexPostFix">match suffix.</param>
public static List<string> FindAll(string sourceData, string regexPrefix, string regexPostFix)
{
return FindAll(sourceData, regexPrefix, regexPostFix, false, true);
}
/// <summary>
/// Match everything from the source.
/// </summary>
/// <returns>matched data.</returns>
/// <param name="sourceData">source text.</param>
/// <param name="regexPattern">Regular expression.</param>
/// <param name="ignoreCase">If set to <c>true</c> Ignore case.</param>
public static List<string> FindAll(string sourceData, string regexPattern, bool ignoreCase)
{
List<string> result = new List<string>();
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;
}
/// <summary>
/// Match everything from the source
/// </summary>
/// <returns>matched data.</returns>
/// <param name="sourceData">source text.</param>
/// <param name="regexPrefix">match prefix.</param>
/// <param name="regexPostFix">match suffix.</param>
/// <param name="OnlyDigit">If set to <c>true</c> only extract numbers.</param>
public static List<string> FindAll(string sourceData, string regexPrefix, string regexPostFix, bool OnlyDigit)
{
return FindAll(sourceData, regexPrefix, regexPostFix, OnlyDigit, true);
}
/// <summary>
/// Match everything from the source
/// </summary>
/// <returns>matched data.</returns>
/// <param name="sourceData">source text.</param>
/// <param name="regexPreFix">match prefix.</param>
/// <param name="regexPostFix">match suffix.</param>
/// <param name="OnlyDigit">If set to <c>true</c> only extract numbers.</param>
/// <param name="ignoreCase">If set to <c>true</c> Ignore case.</param>
public static List<string> FindAll(string sourceData, string regexPreFix, string regexPostFix, bool OnlyDigit, bool ignoreCase)
{
List<string> result = new List<string>();
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()
{ }
}
}

View File

@ -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
{
/// <summary>
/// Randomly generate a unique Token string
/// </summary>
/// <returns>The generated token string.</returns>
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;
/// <summary>
/// Each token saves a custom object
/// </summary>
/// <param name="token">Token.</param>
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);
}
}
}
}
/// <summary>
/// All the saved token data objects are cleared and the duplicate detection is also reset
/// </summary>
public void Clear()
{
_tokens.Clear();
}
private static readonly Dictionary<string, TcpConnectedPeer> _tokens = new Dictionary<string, TcpConnectedPeer>();
private Token()
{ }
private static Token instance;
public static Token Instance
{
get
{
if (instance == null)
{
instance = new Token();
}
return instance;
}
}
}
}

View File

@ -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<EndPoint, byte[], byte[]> 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<EndPoint, byte[], byte[]> 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);
}
}
}
}

View File

@ -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.
/// <summary>
/// Access control manager. Dictates which connections are permitted or denied.
/// </summary>
public class AccessControlManager
{
/// <summary>
/// Matcher to match denied addresses.
/// </summary>
public Matcher DenyList = new Matcher();
/// <summary>
/// Matcher to match permitted addresses.
/// </summary>
public Matcher PermitList = new Matcher();
/// <summary>
/// Access control mode, either DefaultPermit or DefaultDeny.
/// DefaultPermit: allow everything, except for those explicitly denied.
/// DefaultDeny: deny everything, except for those explicitly permitted.
/// </summary>
public AccessControlMode Mode = AccessControlMode.DefaultPermit;
/// <summary>
/// Instantiate the object.
/// </summary>
/// <param name="mode">Access control mode.</param>
public AccessControlManager(AccessControlMode mode)
{
Mode = mode;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="ip">The IP address to evaluate.</param>
/// <returns>True if permitted.</returns>
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());
}
}
}
}

View File

@ -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.
/// <summary>
/// Access control mode of operation.
/// </summary>
[EonaCat.Json.Converter(typeof(StringEnumConverter))]
public enum AccessControlMode
{
/// <summary>
/// Permit requests from any endpoint by default.
/// </summary>
[EnumMember(Value = "DefaultPermit")]
DefaultPermit,
/// <summary>
/// Deny requests from any endpoint by default.
/// </summary>
[EnumMember(Value = "DefaultDeny")]
DefaultDeny
}
}

View File

@ -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.
/// <summary>
/// A chunk of data, used when reading from a request where the Transfer-Encoding header includes 'chunked'.
/// </summary>
public class Chunk
{
/// <summary>
/// Length of the data.
/// </summary>
public int Length = 0;
/// <summary>
/// Data.
/// </summary>
public byte[] Data = null;
/// <summary>
/// Any additional metadata that appears on the length line after the length hex value and semicolon.
/// </summary>
public string Metadata = null;
/// <summary>
/// Indicates whether or not this is the final chunk, i.e. the chunk length received was zero.
/// </summary>
public bool IsFinalChunk = false;
internal Chunk()
{
}
}
}

View File

@ -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.
/// <summary>
/// Connection event arguments.
/// </summary>
public class ConnectionEventArgs : EventArgs
{
/// <summary>
/// Requestor IP address.
/// </summary>
public string Ip { get; private set; } = null;
/// <summary>
/// Request TCP port.
/// </summary>
public int Port { get; private set; } = 0;
/// <summary>
/// Connection event arguments.
/// </summary>
/// <param name="ip">Requestor IP address.</param>
/// <param name="port">Request TCP port.</param>
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;
}
}
}

View File

@ -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.
/// <summary>
/// Assign a method handler for when requests are received matching the supplied method and path.
/// </summary>
public class ContentRoute
{
/// <summary>
/// Globally-unique identifier.
/// </summary>
[JsonProperty(Order = -1)]
public string GUID { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// The pattern against which the raw URL should be matched.
/// </summary>
[JsonProperty(Order = 0)]
public string Path { get; set; } = null;
/// <summary>
/// Indicates whether or not the path specifies a directory. If so, any matching URL will be handled by the specified handler.
/// </summary>
[JsonProperty(Order = 1)]
public bool IsDirectory { get; set; } = false;
/// <summary>
/// User-supplied metadata.
/// </summary>
[JsonProperty(Order = 999)]
public object Metadata { get; set; } = null;
/// <summary>
/// Create a new route object.
/// </summary>
/// <param name="path">The pattern against which the raw URL should be matched.</param>
/// <param name="isDirectory">Indicates whether or not the path specifies a directory. If so, any matching URL will be handled by the specified handler.</param>
/// <param name="guid">Globally-unique identifier.</param>
/// <param name="metadata">User-supplied metadata.</param>
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;
}
}
}

View File

@ -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.
/// <summary>
/// Content route manager. Content routes are used for GET and HEAD requests to specific files or entire directories.
/// </summary>
public class ContentRouteManager
{
/// <summary>
/// Base directory for files and directories accessible via content routes.
/// </summary>
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<ContentRoute> _Routes = new List<ContentRoute>();
private readonly object _Lock = new object();
private string _BaseDirectory = AppDomain.CurrentDomain.BaseDirectory;
/// <summary>
/// Instantiate the object.
/// </summary>
public ContentRouteManager()
{
}
/// <summary>
/// Add a route.
/// </summary>
/// <param name="path">URL path, i.e. /path/to/resource.</param>
/// <param name="isDirectory">True if the path represents a directory.</param>
/// <param name="guid">Globally-unique identifier.</param>
/// <param name="metadata">User-supplied metadata.</param>
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));
}
/// <summary>
/// Remove a route.
/// </summary>
/// <param name="path">URL path.</param>
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;
}
/// <summary>
/// Retrieve a content route.
/// </summary>
/// <param name="path">URL path.</param>
/// <returns>ContentRoute if the route exists, otherwise null.</returns>
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;
}
}
/// <summary>
/// Check if a content route exists.
/// </summary>
/// <param name="path">URL path.</param>
/// <returns>True if exists.</returns>
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;
}
/// <summary>
/// Retrieve a content route.
/// </summary>
/// <param name="path">URL path.</param>
/// <param name="route">Matching route.</param>
/// <returns>True if a match exists.</returns>
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;
}
}
}

View File

@ -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.
/// <summary>
/// Content route handler. Handles GET and HEAD requests to content routes for files and directories.
/// </summary>
public class ContentRouteHandler
{
/// <summary>
/// The FileMode value to use when accessing files within a content route via a FileStream. Default is FileMode.Open.
/// </summary>
public FileMode ContentFileMode = FileMode.Open;
/// <summary>
/// The FileAccess value to use when accessing files within a content route via a FileStream. Default is FileAccess.Read.
/// </summary>
public FileAccess ContentFileAccess = FileAccess.Read;
/// <summary>
/// The FileShare value to use when accessing files within a content route via a FileStream. Default is FileShare.Read.
/// </summary>
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;
}
}
}

View File

@ -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.
/// <summary>
/// Assign a method handler for when requests are received matching the supplied method and path regex.
/// </summary>
public class DynamicRoute
{
/// <summary>
/// Globally-unique identifier.
/// </summary>
[JsonProperty(Order = -1)]
public string GUID { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// The HTTP method, i.e. GET, PUT, POST, DELETE, etc.
/// </summary>
[JsonProperty(Order = 0)]
public HttpMethod Method { get; set; } = HttpMethod.GET;
/// <summary>
/// The pattern against which the raw URL should be matched.
/// </summary>
[JsonProperty(Order = 1)]
public Regex Path { get; set; } = null;
/// <summary>
/// The handler for the dynamic route.
/// </summary>
[JsonIgnore]
public Func<HttpContext, Task> Handler { get; set; } = null;
/// <summary>
/// User-supplied metadata.
/// </summary>
[JsonProperty(Order = 999)]
public object Metadata { get; set; } = null;
/// <summary>
/// Create a new route object.
/// </summary>
/// <param name="method">The HTTP method, i.e. GET, PUT, POST, DELETE, etc.</param>
/// <param name="path">The pattern against which the raw URL should be matched.</param>
/// <param name="handler">The method that should be called to handle the request.</param>
/// <param name="guid">Globally-unique identifier.</param>
/// <param name="metadata">User-supplied metadata.</param>
public DynamicRoute(HttpMethod method, Regex path, Func<HttpContext, Task> 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;
}
}
}

View File

@ -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.
/// <summary>
/// Attribute that is used to mark methods as a dynamic route.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public sealed class DynamicRouteAttribute : Attribute
{
/// <summary>
/// The HTTP method, i.e. GET, PUT, POST, DELETE, etc.
/// </summary>
public HttpMethod Method = HttpMethod.GET;
/// <summary>
/// The pattern against which the raw URL should be matched. Must be convertible to a regular expression.
/// </summary>
public Regex Path = null;
/// <summary>
/// Globally-unique identifier.
/// </summary>
public string GUID { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// User-supplied metadata.
/// </summary>
public object Metadata { get; set; } = null;
/// <summary>
/// Instantiate the object.
/// </summary>
/// <param name="method">The HTTP method, i.e. GET, PUT, POST, DELETE, etc.</param>
/// <param name="path">The regular expression pattern against which the raw URL should be matched.</param>
/// <param name="guid">Globally-unique identifier.</param>
/// <param name="metadata">User-supplied metadata.</param>
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;
}
}
}

View File

@ -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.
/// <summary>
/// Dynamic route manager. Dynamic routes are used for requests using any HTTP method to any path that can be matched by regular expression.
/// </summary>
public class DynamicRouteManager
{
/// <summary>
/// Directly access the underlying regular expression matching library.
/// This is helpful in case you want to specify the matching behavior should multiple matches exist.
/// </summary>
public Matcher Matcher
{
get
{
return _Matcher;
}
}
private Matcher _Matcher = new Matcher();
private readonly object _Lock = new object();
private Dictionary<DynamicRoute, Func<HttpContext, Task>> _Routes = new Dictionary<DynamicRoute, Func<HttpContext, Task>>();
/// <summary>
/// Instantiate the object.
/// </summary>
public DynamicRouteManager()
{
_Matcher.MatchPreference = MatchPreferenceType.LongestFirst;
}
/// <summary>
/// Add a route.
/// </summary>
/// <param name="method">The HTTP method.</param>
/// <param name="path">URL path, i.e. /path/to/resource.</param>
/// <param name="handler">Method to invoke.</param>
/// <param name="guid">Globally-unique identifier.</param>
/// <param name="metadata">User-supplied metadata.</param>
public void Add(HttpMethod method, Regex path, Func<HttpContext, Task> 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);
}
}
/// <summary>
/// Remove a route.
/// </summary>
/// <param name="method">The HTTP method.</param>
/// <param name="path">URL path.</param>
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<DynamicRoute> 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);
}
}
}
}
/// <summary>
/// Check if a content route exists.
/// </summary>
/// <param name="method">The HTTP method.</param>
/// <param name="path">URL path.</param>
/// <returns>True if exists.</returns>
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));
}
}
/// <summary>
/// Match a request method and URL to a handler method.
/// </summary>
/// <param name="method">The HTTP method.</param>
/// <param name="rawUrl">URL path.</param>
/// <param name="dr">Matching route.</param>
/// <returns>Method to invoke.</returns>
public Func<HttpContext, Task> 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;
}
}
}

View File

@ -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.
/// <summary>
/// EonaCat Webserver.
/// </summary>
public class WebServer : IDisposable
{
/// <summary>
/// Indicates whether or not the server is listening.
/// </summary>
public bool IsListening
{
get
{
return (_HttpListener != null) ? _HttpListener.IsListening : false;
}
}
/// <summary>
/// Number of requests being serviced currently.
/// </summary>
public int RequestCount
{
get
{
return _RequestCount;
}
}
/// <summary>
/// EonaCat webserver settings.
/// </summary>
public EonaCatWebserverSettings Settings
{
get
{
return _Settings;
}
set
{
if (value == null)
_Settings = new EonaCatWebserverSettings();
else
_Settings = value;
}
}
/// <summary>
/// EonaCat webserver routes.
/// </summary>
public EonaCatWebserverRoutes Routes
{
get
{
return _Routes;
}
set
{
if (value == null)
_Routes = new EonaCatWebserverRoutes();
else
_Routes = value;
}
}
/// <summary>
/// EonaCat webserver statistics.
/// </summary>
public EonaCatWebserverStatistics Statistics { get; private set; } = new EonaCatWebserverStatistics();
/// <summary>
/// Set specific actions/callbacks to use when events are raised.
/// </summary>
public EonaCatWebserverEvents Events { get; private set; } = new EonaCatWebserverEvents();
/// <summary>
/// Default pages served by the EonaCat webserver.
/// </summary>
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;
/// <summary>
/// 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.
/// </summary>
/// <param name="settings">EonaCat webserver settings.</param>
/// <param name="defaultRoute">Method used when a request is received and no matching routes are found. Commonly used as the 404 handler when routes are used.</param>
public WebServer(EonaCatWebserverSettings settings = null, Func<HttpContext, Task> 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;
}
/// <summary>
/// Creates a new instance of the EonaCat webserver.
/// </summary>
/// <param name="hostname">Hostname or IP address on which to listen.</param>
/// <param name="port">TCP port on which to listen.</param>
/// <param name="ssl">Specify whether or not SSL should be used (HTTPS).</param>
/// <param name="defaultRoute">Method used when a request is received and no matching routes are found. Commonly used as the 404 handler when routes are used.</param>
public WebServer(string hostname, int port, bool ssl = false, Func<HttpContext, Task> 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;
}
/// <summary>
/// Creates a new instance of the EonaCat webserver.
/// </summary>
/// <param name="hostnames">Hostnames or IP addresses on which to listen. Note: multiple listener endpoints are not supported on all platforms.</param>
/// <param name="port">TCP port on which to listen.</param>
/// <param name="ssl">Specify whether or not SSL should be used (HTTPS).</param>
/// <param name="defaultRoute">Method used when a request is received and no matching routes are found. Commonly used as the 404 handler when routes are used.</param>
public WebServer(List<string> hostnames, int port, bool ssl = false, Func<HttpContext, Task> defaultRoute = null)
{
if (hostnames == null || hostnames.Count < 1)
hostnames = new List<string> { "localhost" };
if (port < 1)
throw new ArgumentOutOfRangeException(nameof(port));
_Settings = new EonaCatWebserverSettings(hostnames, port, ssl);
_Routes.Default = defaultRoute;
}
/// <summary>
/// Tear down the server and dispose of background workers.
/// Do not use this object after disposal.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Start accepting new connections.
/// </summary>
/// <param name="token">Cancellation token useful for canceling the server.</param>
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);
}
/// <summary>
/// Start accepting new connections.
/// </summary>
/// <param name="token">Cancellation token useful for canceling the server.</param>
/// <returns>Task.</returns>
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;
}
/// <summary>
/// Stop accepting new connections.
/// </summary>
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();
}
}
/// <summary>
/// Tear down the server and dispose of background workers.
/// Do not use this object after disposal.
/// </summary>
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<StaticRouteAttribute>().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<ParameterRouteAttribute>().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<DynamicRouteAttribute>().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<StaticRouteAttribute>().Any()
&& method.ReturnType == typeof(Task)
&& method.GetParameters().Length == 1
&& method.GetParameters().First().ParameterType == typeof(HttpContext);
}
private bool IsParameterRoute(MethodInfo method)
{
return method.GetCustomAttributes().OfType<ParameterRouteAttribute>().Any()
&& method.ReturnType == typeof(Task)
&& method.GetParameters().Length == 1
&& method.GetParameters().First().ParameterType == typeof(HttpContext);
}
private bool IsDynamicRoute(MethodInfo method)
{
return method.GetCustomAttributes().OfType<DynamicRouteAttribute>().Any()
&& method.ReturnType == typeof(Task)
&& method.GetParameters().Length == 1
&& method.GetParameters().First().ParameterType == typeof(HttpContext);
}
private Func<HttpContext, Task> ToRouteMethod(MethodInfo method)
{
if (method.IsStatic)
{
return (Func<HttpContext, Task>)Delegate.CreateDelegate(typeof(Func<HttpContext, Task>), method);
}
else
{
object instance = Activator.CreateInstance(method.DeclaringType ?? throw new Exception("Declaring class is null"));
return (Func<HttpContext, Task>)Delegate.CreateDelegate(typeof(Func<HttpContext, Task>), 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<HttpContext, Task> 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<string, string> 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<string, string>(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;
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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.
/// <summary>
/// Callbacks/actions to use when various events are encountered.
/// </summary>
public class EonaCatWebserverEvents
{
/// <summary>
/// Method to use for sending log messages.
/// </summary>
public Action<string> Logger = null;
/// <summary>
/// Event to fire when a connection is received.
/// </summary>
public event EventHandler<ConnectionEventArgs> ConnectionReceived = delegate
{ };
/// <summary>
/// Event to fire when a request is received.
/// </summary>
public event EventHandler<RequestEventArgs> RequestReceived = delegate
{ };
/// <summary>
/// Event to fire when a request is denied due to access control.
/// </summary>
public event EventHandler<RequestEventArgs> RequestDenied = delegate
{ };
/// <summary>
/// Event to fire when a requestor disconnected unexpectedly.
/// </summary>
public event EventHandler<RequestEventArgs> RequestorDisconnected = delegate
{ };
/// <summary>
/// Event to fire when a response is sent.
/// </summary>
public event EventHandler<ResponseEventArgs> ResponseSent = delegate
{ };
/// <summary>
/// Event to fire when an exception is encountered.
/// </summary>
public event EventHandler<ExceptionEventArgs> ExceptionEncountered = delegate
{ };
/// <summary>
/// Event to fire when the server is started.
/// </summary>
public event EventHandler ServerStarted = delegate
{ };
/// <summary>
/// Event to fire when the server is stopped.
/// </summary>
public event EventHandler ServerStopped = delegate
{ };
/// <summary>
/// Event to fire when the server is being disposed.
/// </summary>
public event EventHandler ServerDisposing = delegate
{ };
/// <summary>
/// Instantiate the object.
/// </summary>
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));
}
}
}
}

View File

@ -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.
/// <summary>
/// Default pages served by the EonaCat webserver.
/// </summary>
public class EonaCatWebserverPages
{
/// <summary>
/// Page displayed when sending a 404 due to a lack of a route.
/// </summary>
public Page Default404Page
{
get
{
return _Default404Page;
}
set
{
if (value == null)
throw new ArgumentNullException(nameof(Default404Page));
_Default404Page = value;
}
}
/// <summary>
/// Page displayed when sending a 500 due to an exception is unhandled within your routes.
/// </summary>
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");
/// <summary>
/// Default pages served by the EonaCat webserver.
/// </summary>
public EonaCatWebserverPages()
{
}
/// <summary>
/// Page served by the EonaCat webserver.
/// </summary>
public class Page
{
/// <summary>
/// Content type.
/// </summary>
public string ContentType { get; private set; } = null;
/// <summary>
/// Content.
/// </summary>
public string Content { get; private set; } = null;
/// <summary>
/// Page served by the EonaCat webserver.
/// </summary>
/// <param name="contentType">Content type.</param>
/// <param name="content">Content.</param>
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;
}
}
}
}

View File

@ -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.
/// <summary>
/// EonaCat webserver routes.
/// </summary>
public class EonaCatWebserverRoutes
{
/// <summary>
/// Function to call when a preflight (OPTIONS) request is received.
/// Often used to handle CORS.
/// Leave null to use the default OPTIONS handler.
/// </summary>
public Func<HttpContext, Task> Preflight
{
get
{
return _Preflight;
}
set
{
if (value == null)
_Preflight = PreflightInternal;
else
_Preflight = value;
}
}
/// <summary>
/// Function to call prior to routing.
/// Return 'true' if the connection should be terminated.
/// Return 'false' to allow the connection to continue routing.
/// </summary>
public Func<HttpContext, Task<bool>> PreRouting = null;
/// <summary>
/// Content routes; i.e. routes to specific files or folders for GET and HEAD requests.
/// </summary>
public ContentRouteManager Content
{
get
{
return _Content;
}
set
{
if (value == null)
throw new ArgumentNullException(nameof(Content));
_Content = value;
}
}
/// <summary>
/// Handler for content route requests.
/// </summary>
public ContentRouteHandler ContentHandler
{
get
{
return _ContentHandler;
}
set
{
if (value == null)
throw new ArgumentNullException(nameof(ContentHandler));
_ContentHandler = value;
}
}
/// <summary>
/// Static routes; i.e. routes with explicit matching and any HTTP method.
/// </summary>
public StaticRouteManager Static
{
get
{
return _Static;
}
set
{
if (value == null)
throw new ArgumentNullException(nameof(Static));
_Static = value;
}
}
/// <summary>
/// Parameter routes; i.e. routes with parameters embedded in the URL, such as /{version}/api/{id}.
/// </summary>
public ParameterRouteManager Parameter
{
get
{
return _Parameter;
}
set
{
if (value == null)
throw new ArgumentNullException(nameof(Parameter));
_Parameter = value;
}
}
/// <summary>
/// Dynamic routes; i.e. routes with regex matching and any HTTP method.
/// </summary>
public DynamicRouteManager Dynamic
{
get
{
return _Dynamic;
}
set
{
if (value == null)
throw new ArgumentNullException(nameof(Dynamic));
_Dynamic = value;
}
}
/// <summary>
/// Default route; used when no other routes match.
/// </summary>
public Func<HttpContext, Task> 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<HttpContext, Task> _Default = null;
private Func<HttpContext, Task> _Preflight = null;
/// <summary>
/// Instantiate the object using default settings.
/// </summary>
public EonaCatWebserverRoutes()
{
_Preflight = PreflightInternal;
_ContentHandler = new ContentRouteHandler(_Content);
}
/// <summary>
/// Instantiate the object using default settings and the specified default route.
/// </summary>
public EonaCatWebserverRoutes(EonaCatWebserverSettings settings, Func<HttpContext, Task> 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<string, string> 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<string, string> header in _Settings.Headers)
{
ctx.Response.Headers.Add(header.Key, header.Value);
}
ctx.Response.ContentLength = 0;
await ctx.Response.Send().ConfigureAwait(false);
}
}
}

View File

@ -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.
/// <summary>
/// EonaCat webserver settings.
/// </summary>
public class EonaCatWebserverSettings
{
/// <summary>
/// Prefixes on which to listen.
/// </summary>
public List<string> 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;
}
}
/// <summary>
/// Input-output settings.
/// </summary>
public IOSettings IO
{
get
{
return _IO;
}
set
{
if (value == null)
throw new ArgumentNullException(nameof(IO));
_IO = value;
}
}
/// <summary>
/// SSL settings.
/// </summary>
public SslSettings Ssl
{
get
{
return _Ssl;
}
set
{
if (value == null)
throw new ArgumentNullException(nameof(Ssl));
_Ssl = value;
}
}
/// <summary>
/// Headers that will be added to every response unless previously set.
/// </summary>
public Dictionary<string, string> Headers
{
get
{
return _Headers;
}
set
{
if (value == null)
throw new ArgumentNullException(nameof(Headers));
_Headers = value;
}
}
/// <summary>
/// Access control manager, i.e. default mode of operation, permit list, and deny list.
/// </summary>
public AccessControlManager AccessControl
{
get
{
return _AccessControl;
}
set
{
if (value == null)
throw new ArgumentNullException(nameof(AccessControl));
_AccessControl = value;
}
}
/// <summary>
/// Debug logging settings.
/// Be sure to set Events.Logger in order to receive debug messages.
/// </summary>
public DebugSettings Debug
{
get
{
return _Debug;
}
set
{
if (value == null)
throw new ArgumentNullException(nameof(Debug));
_Debug = value;
}
}
private List<string> _Prefixes = new List<string>();
private IOSettings _IO = new IOSettings();
private SslSettings _Ssl = new SslSettings();
private AccessControlManager _AccessControl = new AccessControlManager(AccessControlMode.DefaultPermit);
private DebugSettings _Debug = new DebugSettings();
private Dictionary<string, string> _Headers = new Dictionary<string, string>
{
{ "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" }
};
/// <summary>
/// EonaCat webserver settings.
/// </summary>
public EonaCatWebserverSettings()
{
}
/// <summary>
/// EonaCat webserver settings.
/// </summary>
/// <param name="hostname">The hostname on which to listen.</param>
/// <param name="port">The port on which to listen.</param>
/// <param name="ssl">Enable or disable SSL.</param>
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;
}
/// <summary>
/// EonaCat webserver settings.
/// </summary>
/// <param name="hostnames">The hostnames on which to listen.</param>
/// <param name="port">The port on which to listen.</param>
/// <param name="ssl">Enable or disable SSL.</param>
public EonaCatWebserverSettings(List<string> hostnames, int port, bool ssl = false)
{
if (hostnames == null)
hostnames = new List<string> { "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;
}
/// <summary>
/// Input-output settings.
/// </summary>
public class IOSettings
{
/// <summary>
/// Buffer size to use when interacting with streams.
/// </summary>
public int StreamBufferSize
{
get
{
return _StreamBufferSize;
}
set
{
if (value < 1)
throw new ArgumentOutOfRangeException(nameof(StreamBufferSize));
_StreamBufferSize = value;
}
}
/// <summary>
/// Maximum number of concurrent requests.
/// </summary>
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;
/// <summary>
/// Input-output settings.
/// </summary>
public IOSettings()
{
}
}
/// <summary>
/// SSL settings.
/// </summary>
public class SslSettings
{
/// <summary>
/// Enable or disable SSL.
/// </summary>
public bool Enable = false;
/// <summary>
/// Require mutual authentication.
/// </summary>
public bool MutuallyAuthenticate = false;
/// <summary>
/// Accept invalid certificates including self-signed and those that are unable to be verified.
/// </summary>
public bool AcceptInvalidAcertificates = true;
/// <summary>
/// SSL settings.
/// </summary>
internal SslSettings()
{
}
}
/// <summary>
/// Headers that will be added to every response unless previously set.
/// </summary>
public class HeaderSettings
{
/// <summary>
/// Access-Control-Allow-Origin header.
/// </summary>
public string AccessControlAllowOrigin = "*";
/// <summary>
/// Access-Control-Allow-Methods header.
/// </summary>
public string AccessControlAllowMethods = "OPTIONS, HEAD, GET, PUT, POST, DELETE";
/// <summary>
/// Access-Control-Allow-Headers header.
/// </summary>
public string AccessControlAllowHeaders = "*";
/// <summary>
/// Access-Control-Expose-Headers header.
/// </summary>
public string AccessControlExposeHeaders = "";
/// <summary>
/// Accept header.
/// </summary>
public string Accept = "*/*";
/// <summary>
/// Accept-Language header.
/// </summary>
public string AcceptLanguage = "en-US, en";
/// <summary>
/// Accept-Charset header.
/// </summary>
public string AcceptCharset = "ISO-8859-1, utf-8";
/// <summary>
/// Connection header.
/// </summary>
public string Connection = "close";
/// <summary>
/// Host header.
/// </summary>
public string Host = null;
/// <summary>
/// Headers that will be added to every response unless previously set.
/// </summary>
public HeaderSettings()
{
}
}
/// <summary>
/// Debug logging settings.
/// Be sure to set Events.Logger in order to receive debug messages.
/// </summary>
public class DebugSettings
{
/// <summary>
/// Enable or disable debug logging of access control.
/// </summary>
public bool AccessControl = false;
/// <summary>
/// Enable or disable debug logging of routing.
/// </summary>
public bool Routing = false;
/// <summary>
/// Enable or disable debug logging of requests.
/// </summary>
public bool Requests = false;
/// <summary>
/// Enable or disable debug logging of responses.
/// </summary>
public bool Responses = false;
/// <summary>
/// Debug logging settings.
/// Be sure to set Events.Logger in order to receive debug messages.
/// </summary>
public DebugSettings()
{
}
}
}
}

View File

@ -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.
/// <summary>
/// EonaCat webserver statistics.
/// </summary>
public class EonaCatWebserverStatistics
{
/// <summary>
/// The time at which the client or server was started.
/// </summary>
public DateTime StartTime
{
get
{
return _StartTime;
}
}
/// <summary>
/// The amount of time which the client or server has been up.
/// </summary>
public TimeSpan UpTime
{
get
{
return DateTime.Now.ToUniversalTime() - _StartTime;
}
}
/// <summary>
/// The number of payload bytes received (incoming request body).
/// </summary>
public long ReceivedPayloadBytes
{
get
{
return _ReceivedPayloadBytes;
}
internal set
{
_ReceivedPayloadBytes = value;
}
}
/// <summary>
/// The number of payload bytes sent (outgoing request body).
/// </summary>
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
/// <summary>
/// Initialize the statistics object.
/// </summary>
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];
}
/// <summary>
/// Human-readable version of the object.
/// </summary>
/// <returns>String.</returns>
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();
}
/// <summary>
/// Reset statistics other than StartTime and UpTime.
/// </summary>
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);
}
/// <summary>
/// Retrieve the number of requests received using a specific HTTP method.
/// </summary>
/// <param name="method">HTTP method.</param>
/// <returns>Number of requests received using this method.</returns>
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);
}
}
}

View File

@ -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.
/// <summary>
/// Exception event arguments.
/// </summary>
public class ExceptionEventArgs : EventArgs
{
/// <summary>
/// IP address.
/// </summary>
public string Ip { get; private set; } = null;
/// <summary>
/// Port number.
/// </summary>
public int Port { get; private set; } = 0;
/// <summary>
/// HTTP method.
/// </summary>
public HttpMethod Method { get; private set; } = HttpMethod.GET;
/// <summary>
/// URL.
/// </summary>
public string Url { get; private set; } = null;
/// <summary>
/// Request query.
/// </summary>
public Dictionary<string, string> Query { get; private set; } = new Dictionary<string, string>();
/// <summary>
/// Request headers.
/// </summary>
public Dictionary<string, string> RequestHeaders { get; private set; } = new Dictionary<string, string>();
/// <summary>
/// Content length.
/// </summary>
public long RequestContentLength { get; private set; } = 0;
/// <summary>
/// Response status.
/// </summary>
public int StatusCode { get; private set; } = 0;
/// <summary>
/// Response headers.
/// </summary>
public Dictionary<string, string> ResponseHeaders { get; private set; } = new Dictionary<string, string>();
/// <summary>
/// Response content length.
/// </summary>
public long? ResponseContentLength { get; private set; } = 0;
/// <summary>
/// Exception.
/// </summary>
public Exception Exception { get; private set; } = null;
/// <summary>
/// JSON string of the Exception.
/// </summary>
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;
}
}
}

View File

@ -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.
/// <summary>
/// HTTP context including both request and response.
/// </summary>
public class HttpContext
{
/// <summary>
/// The HTTP request that was received.
/// </summary>
[JsonProperty(Order = -1)]
public HttpRequest Request { get; private set; } = null;
/// <summary>
/// Type of route.
/// </summary>
[JsonProperty(Order = 0)]
public RouteTypeEnum? RouteType { get; internal set; } = null;
/// <summary>
/// Matched route.
/// </summary>
[JsonProperty(Order = 1)]
public object Route { get; internal set; } = null;
/// <summary>
/// The HTTP response that will be sent. This object is preconstructed on your behalf and can be modified directly.
/// </summary>
[JsonProperty(Order = 998)]
public HttpResponse Response { get; private set; } = null;
/// <summary>
/// User-supplied metadata.
/// </summary>
[JsonProperty(Order = 999)]
public object Metadata { get; set; } = null;
private HttpListenerContext _Context = null;
/// <summary>
/// Instantiate the object.
/// </summary>
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);
}
}
}

View File

@ -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.
/// <summary>
/// HTTP methods, i.e. GET, PUT, POST, DELETE, etc.
/// </summary>
[EonaCat.Json.Converter(typeof(StringEnumConverter))]
public enum HttpMethod
{
/// <summary>
/// HTTP GET.
/// </summary>
[EnumMember(Value = "GET")]
GET,
/// <summary>
/// HTTP HEAD.
/// </summary>
[EnumMember(Value = "HEAD")]
HEAD,
/// <summary>
/// HTTP PUT.
/// </summary>
[EnumMember(Value = "PUT")]
PUT,
/// <summary>
/// HTTP POST.
/// </summary>
[EnumMember(Value = "POST")]
POST,
/// <summary>
/// HTTP DELETE.
/// </summary>
[EnumMember(Value = "DELETE")]
DELETE,
/// <summary>
/// HTTP PATCH.
/// </summary>
[EnumMember(Value = "PATCH")]
PATCH,
/// <summary>
/// HTTP CONNECT.
/// </summary>
[EnumMember(Value = "CONNECT")]
CONNECT,
/// <summary>
/// HTTP OPTIONS.
/// </summary>
[EnumMember(Value = "OPTIONS")]
OPTIONS,
/// <summary>
/// HTTP TRACE.
/// </summary>
[EnumMember(Value = "TRACE")]
TRACE
}
}

View File

@ -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.
/// <summary>
/// HTTP request.
/// </summary>
public class HttpRequest
{
/// <summary>
/// UTC timestamp from when the request was received.
/// </summary>
[JsonProperty(Order = -10)]
public DateTime TimestampUtc { get; private set; } = DateTime.Now.ToUniversalTime();
/// <summary>
/// Thread ID on which the request exists.
/// </summary>
[JsonProperty(Order = -9)]
public int ThreadId { get; private set; } = Thread.CurrentThread.ManagedThreadId;
/// <summary>
/// The protocol and version.
/// </summary>
[JsonProperty(Order = -8)]
public string ProtocolVersion { get; set; } = null;
/// <summary>
/// Source (requestor) IP and port information.
/// </summary>
[JsonProperty(Order = -7)]
public SourceDetails Source { get; set; } = new SourceDetails();
/// <summary>
/// Destination IP and port information.
/// </summary>
[JsonProperty(Order = -6)]
public DestinationDetails Destination { get; set; } = new DestinationDetails();
/// <summary>
/// The HTTP method used in the request.
/// </summary>
[JsonProperty(Order = -5)]
public HttpMethod Method { get; set; } = HttpMethod.GET;
/// <summary>
/// URL details.
/// </summary>
[JsonProperty(Order = -4)]
public UrlDetails Url { get; set; } = new UrlDetails();
/// <summary>
/// Query details.
/// </summary>
[JsonProperty(Order = -3)]
public QueryDetails Query { get; set; } = new QueryDetails();
/// <summary>
/// The headers found in the request.
/// </summary>
[JsonProperty(Order = -2)]
public Dictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();
/// <summary>
/// Specifies whether or not the client requested HTTP keepalives.
/// </summary>
public bool Keepalive { get; set; } = false;
/// <summary>
/// Indicates whether or not chunked transfer encoding was detected.
/// </summary>
public bool ChunkedTransfer { get; set; } = false;
/// <summary>
/// Indicates whether or not the payload has been gzip compressed.
/// </summary>
public bool Gzip { get; set; } = false;
/// <summary>
/// Indicates whether or not the payload has been deflate compressed.
/// </summary>
public bool Deflate { get; set; } = false;
/// <summary>
/// The useragent specified in the request.
/// </summary>
public string Useragent { get; set; } = null;
/// <summary>
/// The content type as specified by the requestor (client).
/// </summary>
[JsonProperty(Order = 990)]
public string ContentType { get; set; } = null;
/// <summary>
/// The number of bytes in the request body.
/// </summary>
[JsonProperty(Order = 991)]
public long ContentLength { get; private set; } = 0;
/// <summary>
/// The stream from which to read the request body sent by the requestor (client).
/// </summary>
[JsonIgnore]
public Stream Data;
/// <summary>
/// Retrieve the request body as a byte array. This will fully read the stream.
/// </summary>
[JsonIgnore]
public byte[] DataAsBytes
{
get
{
if (_DataAsBytes != null)
return _DataAsBytes;
if (Data != null && ContentLength > 0)
{
_DataAsBytes = ReadStreamFully(Data);
return _DataAsBytes;
}
return null;
}
}
/// <summary>
/// Retrieve the request body as a string. This will fully read the stream.
/// </summary>
[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;
}
}
/// <summary>
/// The original HttpListenerContext from which the HttpRequest was constructed.
/// </summary>
[JsonIgnore]
public HttpListenerContext ListenerContext;
private Uri _Uri = null;
private byte[] _DataAsBytes = null;
/// <summary>
/// HTTP request.
/// </summary>
public HttpRequest()
{
}
/// <summary>
/// HTTP request.
/// Instantiate the object using an HttpListenerContext.
/// </summary>
/// <param name="ctx">HttpListenerContext.</param>
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<string, string>();
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<string, string> 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;
}
/// <summary>
/// Retrieve a specified header value from either the headers or the querystring (case insensitive).
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public string RetrieveHeaderValue(string key)
{
if (String.IsNullOrEmpty(key))
throw new ArgumentNullException(nameof(key));
if (Headers != null && Headers.Count > 0)
{
foreach (KeyValuePair<string, string> 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<string, string> curr in Query.Elements)
{
if (String.IsNullOrEmpty(curr.Key))
continue;
if (String.Compare(curr.Key.ToLower(), key.ToLower()) == 0)
return curr.Value;
}
}
return null;
}
/// <summary>
/// Determine if a header exists.
/// </summary>
/// <param name="key">Header key.</param>
/// <param name="caseSensitive">Specify whether a case sensitive search should be used.</param>
/// <returns>True if exists.</returns>
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<string, string> header in Headers)
{
if (String.IsNullOrEmpty(header.Key))
continue;
if (header.Key.ToLower().Trim().Equals(key))
return true;
}
}
}
return false;
}
/// <summary>
/// Determine if a querystring entry exists.
/// </summary>
/// <param name="key">Querystring key.</param>
/// <param name="caseSensitive">Specify whether a case sensitive search should be used.</param>
/// <returns>True if exists.</returns>
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<string, string> queryElement in Query.Elements)
{
if (String.IsNullOrEmpty(queryElement.Key))
continue;
if (queryElement.Key.ToLower().Trim().Equals(key))
return true;
}
}
}
return false;
}
/// <summary>
/// For chunked transfer-encoded requests, read the next chunk.
/// It is strongly recommended that you use the ChunkedTransfer parameter before invoking this method.
/// </summary>
/// <param name="token">Cancellation token useful for canceling the request.</param>
/// <returns>Chunk.</returns>
public async Task<Chunk> 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;
}
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="T">Type.</typeparam>
/// <returns>Object of type specified.</returns>
public T DataAsJsonObject<T>() where T : class
{
string json = DataAsString;
if (String.IsNullOrEmpty(json))
return null;
return SerializationHelper.DeserializeJson<T>(json);
}
private static Dictionary<string, string> AddToDict(string key, string val, Dictionary<string, string> existing)
{
if (String.IsNullOrEmpty(key))
return existing;
Dictionary<string, string> ret = new Dictionary<string, string>();
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;
}
}
/// <summary>
/// Source details.
/// </summary>
public class SourceDetails
{
/// <summary>
/// IP address of the requestor.
/// </summary>
public string IpAddress { get; set; } = null;
/// <summary>
/// TCP port from which the request originated on the requestor.
/// </summary>
public int Port { get; set; } = 0;
/// <summary>
/// Source details.
/// </summary>
public SourceDetails()
{
}
/// <summary>
/// Source details.
/// </summary>
/// <param name="ip">IP address of the requestor.</param>
/// <param name="port">TCP port from which the request originated on the requestor.</param>
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;
}
}
/// <summary>
/// Destination details.
/// </summary>
public class DestinationDetails
{
/// <summary>
/// IP address to which the request was made.
/// </summary>
public string IpAddress { get; set; } = null;
/// <summary>
/// TCP port on which the request was received.
/// </summary>
public int Port { get; set; } = 0;
/// <summary>
/// Hostname to which the request was directed.
/// </summary>
public string Hostname { get; set; } = null;
/// <summary>
/// Hostname elements.
/// </summary>
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;
}
}
/// <summary>
/// Destination details.
/// </summary>
public DestinationDetails()
{
}
/// <summary>
/// Source details.
/// </summary>
/// <param name="ip">IP address to which the request was made.</param>
/// <param name="port">TCP port on which the request was received.</param>
/// <param name="hostname">Hostname.</param>
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;
}
}
/// <summary>
/// URL details.
/// </summary>
public class UrlDetails
{
/// <summary>
/// Full URL.
/// </summary>
public string Full { get; set; } = null;
/// <summary>
/// Raw URL with query.
/// </summary>
public string RawWithQuery { get; set; } = null;
/// <summary>
/// Raw URL without query.
/// </summary>
public string RawWithoutQuery
{
get
{
if (!String.IsNullOrEmpty(RawWithQuery))
{
if (RawWithQuery.Contains("?"))
return RawWithQuery.Substring(0, RawWithQuery.IndexOf("?"));
else
return RawWithQuery;
}
else
{
return null;
}
}
}
/// <summary>
/// Raw URL elements.
/// </summary>
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;
}
}
/// <summary>
/// Parameters found within the URL, if using parameter routes.
/// </summary>
public Dictionary<string, string> Parameters { get; set; } = new Dictionary<string, string>();
/// <summary>
/// URL details.
/// </summary>
public UrlDetails()
{
}
/// <summary>
/// URL details.
/// </summary>
/// <param name="fullUrl">Full URL.</param>
/// <param name="rawUrl">Raw URL.</param>
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;
}
}
/// <summary>
/// Query details.
/// </summary>
public class QueryDetails
{
/// <summary>
/// Querystring, excluding the leading '?'.
/// </summary>
public string Querystring
{
get
{
if (_FullUrl.Contains("?"))
{
return _FullUrl.Substring(_FullUrl.IndexOf("?") + 1, (_FullUrl.Length - _FullUrl.IndexOf("?") - 1));
}
else
{
return null;
}
}
}
/// <summary>
/// Query elements.
/// </summary>
public Dictionary<string, string> Elements
{
get
{
Dictionary<string, string> ret = new Dictionary<string, string>();
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;
}
}
/// <summary>
/// Query details.
/// </summary>
public QueryDetails()
{
}
/// <summary>
/// Query details.
/// </summary>
/// <param name="fullUrl">Full URL.</param>
public QueryDetails(string fullUrl)
{
if (String.IsNullOrEmpty(fullUrl))
throw new ArgumentNullException(nameof(fullUrl));
_FullUrl = fullUrl;
}
private string _FullUrl = null;
}
}
}

View File

@ -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.
/// <summary>
/// HTTP response.
/// </summary>
public class HttpResponse
{
/// <summary>
/// The HTTP status code to return to the requestor (client).
/// </summary>
[JsonProperty(Order = -3)]
public int StatusCode = 200;
/// <summary>
/// The HTTP status description to return to the requestor (client).
/// </summary>
[JsonProperty(Order = -2)]
public string StatusDescription = "OK";
/// <summary>
/// User-supplied headers to include in the response.
/// </summary>
[JsonProperty(Order = -1)]
public Dictionary<string, string> Headers
{
get
{
return _Headers;
}
set
{
if (value == null)
_Headers = new Dictionary<string, string>();
else
_Headers = value;
}
}
/// <summary>
/// User-supplied content-type to include in the response.
/// </summary>
public string ContentType = String.Empty;
/// <summary>
/// The length of the supplied response data.
/// </summary>
public long ContentLength = 0;
/// <summary>
/// Indicates whether or not chunked transfer encoding should be indicated in the response.
/// </summary>
public bool ChunkedTransfer = false;
/// <summary>
/// Retrieve the response body sent using a Send() or SendAsync() method.
/// </summary>
[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;
}
}
/// <summary>
/// Retrieve the response body sent using a Send() or SendAsync() method.
/// </summary>
[JsonIgnore]
public byte[] DataAsBytes
{
get
{
if (_DataAsBytes != null)
return _DataAsBytes;
if (_Data != null && ContentLength > 0)
{
_DataAsBytes = ReadStreamFully(_Data);
return _DataAsBytes;
}
return null;
}
}
/// <summary>
/// Response data stream sent to the requestor.
/// </summary>
[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<string, string> _Headers = new Dictionary<string, string>();
private byte[] _DataAsBytes = null;
private MemoryStream _Data = null;
/// <summary>
/// Instantiate the object.
/// </summary>
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;
}
/// <summary>
/// Send headers and no data to the requestor and terminate the connection.
/// </summary>
/// <param name="token">Cancellation token useful for canceling the request.</param>
/// <returns>True if successful.</returns>
public async Task<bool> 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;
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="token">Cancellation token useful for canceling the request.</param>
/// <param name="contentLength">Content length.</param>
/// <returns>True if successful.</returns>
public async Task<bool> 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;
}
}
/// <summary>
/// Send headers and data to the requestor and terminate the connection.
/// </summary>
/// <param name="data">Data.</param>
/// <param name="token">Cancellation token useful for canceling the request.</param>
/// <returns>True if successful.</returns>
public async Task<bool> 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;
}
}
/// <summary>
/// Send headers and data to the requestor and terminate the connection.
/// </summary>
/// <param name="data">Data.</param>
/// <param name="token">Cancellation token useful for canceling the request.</param>
/// <returns>True if successful.</returns>
public async Task<bool> 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;
}
}
/// <summary>
/// Send headers and data to the requestor and terminate.
/// </summary>
/// <param name="contentLength">Number of bytes to send.</param>
/// <param name="stream">Stream containing the data.</param>
/// <param name="token">Cancellation token useful for canceling the request.</param>
/// <returns>True if successful.</returns>
public async Task<bool> 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;
}
}
/// <summary>
/// Send headers (if not already sent) and a chunk of data using chunked transfer-encoding, and keep the connection in-tact.
/// </summary>
/// <param name="chunk">Chunk of data.</param>
/// <param name="numBytes">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)).</param>
/// <param name="token">Cancellation token useful for canceling the request.</param>
/// <returns>True if successful.</returns>
public async Task<bool> 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;
}
/// <summary>
/// Send headers (if not already sent) and the final chunk of data using chunked transfer-encoding and terminate the connection.
/// </summary>
/// <param name="chunk">Chunk of data.</param>/// <param name="numBytes">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)).</param>
/// <param name="token">Cancellation token useful for canceling the request.</param>
/// <returns>True if successful.</returns>
public async Task<bool> 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;
}
}
/// <summary>
/// Convert the response data sent using a Send() method to the object type specified using JSON deserialization.
/// </summary>
/// <typeparam name="T">Type.</typeparam>
/// <returns>Object of type specified.</returns>
public T DataAsJsonObject<T>() where T : class
{
string json = DataAsString;
if (String.IsNullOrEmpty(json))
return null;
return SerializationHelper.DeserializeJson<T>(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<string, string> header in Headers)
{
if (String.IsNullOrEmpty(header.Key))
continue;
_Response.AddHeader(header.Key, header.Value);
}
}
if (_Settings.Headers != null)
{
foreach (KeyValuePair<string, string> 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;
}
}
}
}

View File

@ -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.
/// <summary>
/// MIME types and file extensions.
/// </summary>
public class MimeTypes
{
private static readonly IDictionary<string, string> data = new Dictionary<string, string>(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"},
};
/// <summary>
/// Instantiates the object.
/// </summary>
public MimeTypes()
{
}
/// <summary>
/// Retrieve MIME type from file extension.
/// </summary>
/// <param name="extension">File extension.</param>
/// <returns>String containing MIME type.</returns>
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";
}
}
}

View File

@ -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.
/// <summary>
/// Object extensions.
/// </summary>
public static class ObjectExtensions
{
/// <summary>
/// Return a JSON string of the object.
/// </summary>
/// <param name="obj">Object.</param>
/// <param name="pretty">Enable or disable pretty print.</param>
/// <returns>JSON string.</returns>
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;
}
}
}

View File

@ -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.
/// <summary>
/// Assign a method handler for when requests are received matching the supplied method and path containing parameters.
/// </summary>
public class ParameterRoute
{
/// <summary>
/// Globally-unique identifier.
/// </summary>
[JsonProperty(Order = -1)]
public string GUID { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// The HTTP method, i.e. GET, PUT, POST, DELETE, etc.
/// </summary>
[JsonProperty(Order = 0)]
public HttpMethod Method { get; set; } = HttpMethod.GET;
/// <summary>
/// The pattern against which the raw URL should be matched.
/// </summary>
[JsonProperty(Order = 1)]
public string Path { get; set; } = null;
/// <summary>
/// The handler for the parameter route.
/// </summary>
[JsonIgnore]
public Func<HttpContext, Task> Handler { get; set; } = null;
/// <summary>
/// User-supplied metadata.
/// </summary>
[JsonProperty(Order = 999)]
public object Metadata { get; set; } = null;
/// <summary>
/// Create a new route object.
/// </summary>
/// <param name="method">The HTTP method, i.e. GET, PUT, POST, DELETE, etc.</param>
/// <param name="path">The pattern against which the raw URL should be matched.</param>
/// <param name="handler">The method that should be called to handle the request.</param>
/// <param name="guid">Globally-unique identifier.</param>
/// <param name="metadata">User-supplied metadata.</param>
public ParameterRoute(HttpMethod method, string path, Func<HttpContext, Task> 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;
}
}
}

View File

@ -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.
/// <summary>
/// Attribute that is used to mark methods as a parameter route.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public sealed class ParameterRouteAttribute : Attribute
{
/// <summary>
/// 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'.
/// </summary>
public string Path = null;
/// <summary>
/// The HTTP method, i.e. GET, PUT, POST, DELETE, etc.
/// </summary>
public HttpMethod Method = HttpMethod.GET;
/// <summary>
/// Globally-unique identifier.
/// </summary>
public string GUID { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// User-supplied metadata.
/// </summary>
public object Metadata { get; set; } = null;
/// <summary>
/// Instantiate the object.
/// </summary>
/// <param name="method">The HTTP method, i.e. GET, PUT, POST, DELETE, etc.</param>
/// <param name="path">The path to match, i.e. /{version}/api/{id}.</param>
/// <param name="guid">Globally-unique identifier.</param>
/// <param name="metadata">User-supplied metadata.</param>
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;
}
}
}

View File

@ -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.
/// <summary>
/// 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.
/// </summary>
public class ParameterRouteManager
{
/// <summary>
/// Directly access the underlying URL matching library.
/// This is helpful in case you want to specify the matching behavior should multiple matches exist.
/// </summary>
public Matcher Matcher
{
get
{
return _Matcher;
}
}
private Matcher _Matcher = new Matcher();
private readonly object _Lock = new object();
private Dictionary<ParameterRoute, Func<HttpContext, Task>> _Routes = new Dictionary<ParameterRoute, Func<HttpContext, Task>>();
/// <summary>
/// Instantiate the object.
/// </summary>
public ParameterRouteManager()
{
}
/// <summary>
/// Add a route.
/// </summary>
/// <param name="method">The HTTP method.</param>
/// <param name="path">URL path, i.e. /path/to/resource.</param>
/// <param name="handler">Method to invoke.</param>
/// <param name="guid">Globally-unique identifier.</param>
/// <param name="metadata">User-supplied metadata.</param>
public void Add(HttpMethod method, string path, Func<HttpContext, Task> 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);
}
}
/// <summary>
/// Remove a route.
/// </summary>
/// <param name="method">The HTTP method.</param>
/// <param name="path">URL path.</param>
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<ParameterRoute> 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);
}
}
}
}
/// <summary>
/// Retrieve a parameter route.
/// </summary>
/// <param name="method">The HTTP method.</param>
/// <param name="path">URL path.</param>
/// <returns>ParameterRoute if the route exists, otherwise null.</returns>
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;
}
/// <summary>
/// Check if a content route exists.
/// </summary>
/// <param name="method">The HTTP method.</param>
/// <param name="path">URL path.</param>
/// <returns>True if exists.</returns>
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));
}
}
/// <summary>
/// Match a request method and URL to a handler method.
/// </summary>
/// <param name="method">The HTTP method.</param>
/// <param name="path">URL path.</param>
/// <param name="vals">Values extracted from the URL.</param>
/// <param name="pr">Matching route.</param>
/// <returns>True if match exists.</returns>
public Func<HttpContext, Task> Match(HttpMethod method, string path, out Dictionary<string, string> 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<ParameterRoute, Func<HttpContext, Task>> 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;
}
}
}

View File

@ -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.
/// <summary>
/// Request event arguments.
/// </summary>
public class RequestEventArgs : EventArgs
{
/// <summary>
/// IP address.
/// </summary>
public string Ip { get; private set; } = null;
/// <summary>
/// Port number.
/// </summary>
public int Port { get; private set; } = 0;
/// <summary>
/// HTTP method.
/// </summary>
public HttpMethod Method { get; private set; } = HttpMethod.GET;
/// <summary>
/// URL.
/// </summary>
public string Url { get; private set; } = null;
/// <summary>
/// Query found in the URL.
/// </summary>
public Dictionary<string, string> Query { get; private set; } = new Dictionary<string, string>();
/// <summary>
/// Request headers.
/// </summary>
public Dictionary<string, string> Headers { get; private set; } = new Dictionary<string, string>();
/// <summary>
/// Content length.
/// </summary>
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;
}
}
}

Some files were not shown because too many files have changed in this diff Show More