diff --git a/EonaCat.Network.Tester/Program.cs b/EonaCat.Network.Tester/Program.cs index 960a218..7b2454c 100644 --- a/EonaCat.Network.Tester/Program.cs +++ b/EonaCat.Network.Tester/Program.cs @@ -1,15 +1,15 @@ // This file is part of the EonaCat project(s) which is released under the Apache License. // See the LICENSE file or go to https://EonaCat.com/License for full license details. -using EonaCat.Network; using System.Reflection; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; +using EonaCat.WebSockets; internal class Program { - private static WSClient _client; - private static WSServer _server; + private static AsyncWebSocketClient _client; + private static AsyncWebSocketServer _server; private static async Task Main(string[] args) { @@ -30,7 +30,7 @@ internal class Program break; case "1": - await CreateServerAndClientAsync(); + await CreateServerAndClientAsync().ConfigureAwait(false); break; case "2": @@ -38,12 +38,13 @@ internal class Program { Console.Write("Enter message: "); var message = Console.ReadLine(); - _client.Send(message); + await _client.SendTextAsync(message).ConfigureAwait(false); } + break; case "3": - _client?.Close(); + _client?.CloseAsync().ConfigureAwait(false); return; default: @@ -66,64 +67,101 @@ internal class Program StartServer(serverUri, certificatePath, certificatePassword, requiredPassword); // Start the client in the main thread - _client = new WSClient(clientUri, new X509Certificate(certificatePath, certificatePassword)); - _client.OnConnect += (sender, e) => Console.WriteLine($"Connected to server"); - _client.OnMessageReceived += (sender, e) => Console.WriteLine($"Received message from server: {e.Data}"); - _client.OnDisconnect += (sender, e) => Console.WriteLine($"Disconnected from server: {e.Code} : {e.Reason}"); - _client.OnError += (sender, e) => Console.WriteLine($"Error: {sender}\n{e}"); - - _client.ConnectAsync(); - + var configuration = new AsyncWebSocketClientConfiguration(); + _client = new AsyncWebSocketClient(new Uri(clientUri), OnServerTextReceived, OnServerBinaryReceived, OnServerConnected, OnServerDisconnected, OnServerFragmentationStreamOpened, OnServerFragmentationStreamOpened, OnServerFragmentationStreamClosed, configuration); + await _client.ConnectAsync().ConfigureAwait(false); + //(sender, e) => Console.WriteLine($"Error: {sender}\n{e}"); Console.WriteLine("Connected to the server."); } + private static Task OnServerFragmentationStreamClosed(AsyncWebSocketClient arg1, byte[] arg2, int arg3, int arg4) + { + // Do nothing + return Task.CompletedTask; + } + + private static Task OnServerFragmentationStreamOpened(AsyncWebSocketClient arg1, byte[] arg2, int arg3, int arg4) + { + // Do nothing + return Task.CompletedTask; + } + + private static Task OnServerDisconnected(AsyncWebSocketClient arg) + { + Console.WriteLine("Disconnected from server."); + return Task.CompletedTask; + } + + private static Task OnServerConnected(AsyncWebSocketClient arg) + { + Console.WriteLine("Connected to server."); + return Task.CompletedTask; + } + + private static Task OnServerBinaryReceived(AsyncWebSocketClient arg1, byte[] bytes, int arg3, int arg4) + { + Console.WriteLine($"Received binary {bytes} {arg3} {arg4}"); + return Task.CompletedTask; + } + + private static Task OnServerTextReceived(AsyncWebSocketClient arg1, string data) + { + Console.WriteLine($"Received message from server: {data}"); + return Task.CompletedTask; + } + private static void CreateCertificate() { Console.Write("Enter hostname: (default: localhost) "); - string hostname = Console.ReadLine(); + var hostname = Console.ReadLine(); if (string.IsNullOrWhiteSpace(hostname)) { hostname = "localhost"; } - int days = 30; + var days = 30; Console.Write("Enter days until expiration: (default: 30 days) "); - if (int.TryParse(Console.ReadLine(), out int givenDays)) + if (int.TryParse(Console.ReadLine(), out var givenDays)) { days = givenDays; } Console.Write("Enter password, enter to skip: "); - string password = Console.ReadLine(); + var password = Console.ReadLine(); - RSA rsa = RSA.Create(); + var rsa = RSA.Create(); // Create a certificate request with the specified subject and key pair - CertificateRequest request = new CertificateRequest( + var request = new CertificateRequest( $"CN={hostname}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); // Create a self-signed certificate from the certificate request - X509Certificate2 certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddDays(days)); + var certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddDays(days)); // Export the certificate to a file with password - byte[] certBytes = string.IsNullOrEmpty(password) + var certBytes = string.IsNullOrEmpty(password) ? certificate.Export(X509ContentType.Pfx) : certificate.Export(X509ContentType.Pfx, password); File.WriteAllBytes($"{hostname}.pfx", certBytes); - Console.WriteLine($"Certificate for {hostname} created successfully and will expire on {certificate.NotAfter}."); + Console.WriteLine( + $"Certificate for {hostname} created successfully and will expire on {certificate.NotAfter}."); Console.WriteLine($"Path: {Path.Combine(AppContext.BaseDirectory, hostname)}.pfx"); } - private static void StartServer(string serverUri, string certificatePath, string certificatePassword, string requiredPassword) + private static void StartServer(string serverUri, string certificatePath, string certificatePassword, + string requiredPassword) { - _server = new WSServer(serverUri); - _server.SSL.Certificate = new X509Certificate2(certificatePath, certificatePassword); - _server.AddEndpoint("/Welcome"); - _server.Start(); + var test = new AsyncWebSocketServerModuleCatalog(); + //var module = new AsyncWebSocketServerModule + //test.RegisterModule(); + //_server = new AsyncWebSocketServer(serverUri); + //_server.SSL.Certificate = new X509Certificate2(certificatePath, certificatePassword); + //_server.AddEndpoint("/Welcome"); + //_server.Start(); } } \ No newline at end of file diff --git a/EonaCat.Network/Constants.cs b/EonaCat.Network/Constants.cs index aefcc4c..0523a1d 100644 --- a/EonaCat.Network/Constants.cs +++ b/EonaCat.Network/Constants.cs @@ -1,7 +1,6 @@ -namespace EonaCat.Network +namespace EonaCat.Network; + +internal class Constants { - internal class Constants - { - public static string Version { get; set; } = "1.1.4"; - } + public static string Version { get; set; } = "1.1.4"; } \ No newline at end of file diff --git a/EonaCat.Network/EonaCat.Network.csproj b/EonaCat.Network/EonaCat.Network.csproj index 97a170b..443f940 100644 --- a/EonaCat.Network/EonaCat.Network.csproj +++ b/EonaCat.Network/EonaCat.Network.csproj @@ -13,9 +13,9 @@ EonaCat, Network, .NET Standard, EonaCatHelpers, Jeroen, Saey, Protocol, Quic, UDP, TCP, Web, Server EonaCat Networking library with Quic, TCP, UDP, WebSockets and a Webserver - 1.1.5 - 1.1.5 - 1.1.5 + 1.1.6 + 1.1.6 + 1.1.6 icon.png README.md LICENSE diff --git a/EonaCat.Network/Helpers/NetworkHelper.cs b/EonaCat.Network/Helpers/NetworkHelper.cs index 38d651a..56bd82c 100644 --- a/EonaCat.Network/Helpers/NetworkHelper.cs +++ b/EonaCat.Network/Helpers/NetworkHelper.cs @@ -1,136 +1,137 @@ -using EonaCat.LogSystem; +using System; +using System.Net.Sockets; +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; -using System; -using System.Net.Sockets; -using System.Text; -namespace EonaCat.Network +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 { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + internal static Logging Logger = new(); + private static QuicServer _quicServer; - public static class NetworkHelper + //TODO: add udp and tcp example methods + + /// + /// Character bit encoding type web + /// + public static Encoding GlobalEncoding = Encoding.UTF8; + + /// + /// OnQuicClientConnected event + /// + public static event EventHandler OnQuicClientConnected; + + /// + /// OnQuicStreamOpened event + /// + public static event EventHandler OnQuicStreamOpened; + + /// + /// OnQuicStreamDataReceived event + /// + public static event EventHandler OnQuicStreamDataReceived; + + /// + /// Start a Quic server + /// + /// true, start successfully, false did not start successfully. + /// The ip bound to the server + /// The listening port. (default: 11000) + public static bool QuicStartServer(string ip, int port = 11000) { - internal static Logging Logger = new Logging(); - private static QuicServer _quicServer; - - /// - /// OnQuicClientConnected event - /// - public static event EventHandler OnQuicClientConnected; - - /// - /// OnQuicStreamOpened event - /// - public static event EventHandler OnQuicStreamOpened; - - /// - /// OnQuicStreamDataReceived event - /// - public static event EventHandler OnQuicStreamDataReceived; - - /// - /// Start a Quic server - /// - /// true, start successfully, false did not start successfully. - /// The ip bound to the server - /// The listening port. (default: 11000) - public static bool QuicStartServer(string ip, int port = 11000) - { - _quicServer = new QuicServer(ip, port); - _quicServer.OnClientConnected += ClientConnected; - _quicServer.Start(); - Logger.Info($"The Quic server has been successfully started on ip '{ip}' and port: {port}"); - return true; - } - - /// - /// Fired when Client is connected - /// - /// The new connection - private static void ClientConnected(QuicConnection connection) - { - OnQuicClientConnected?.Invoke(null, new QuicConnectionEventArgs { Connection = connection }); - connection.OnStreamOpened += StreamOpened; - } - - private static void StreamOpened(QuicStream stream) - { - OnQuicStreamOpened?.Invoke(null, new QuicStreamEventArgs { Stream = stream }); - stream.OnStreamDataReceived += StreamDataReceived; - } - - private static void StreamDataReceived(QuicStream stream, byte[] data) - { - OnQuicStreamDataReceived?.Invoke(null, new QuicStreamEventArgs { Stream = stream, Data = data }); - } - - /// - /// Start a Quic client - /// - /// - /// - /// - /// - public static QuicStream QuicStartClient(string ip, int port = 11000, StreamType streamType = StreamType.ClientBidirectional) - { - QuicClient client = new QuicClient(); - - // Connect to peer (Server) - QuicConnection connection = client.Connect(ip, port); - - // Create a data stream - return connection.CreateStream(streamType); - } - - /// - /// Stop the Quic server - /// - /// - public static bool QuicStopServer() - { - if (_quicServer != null) - { - _quicServer.Close(); - Logger.Info($"The Quic server has been successfully stopped"); - } - return true; - } - - //TODO: add udp and tcp example methods - - /// - /// Character bit encoding type web - /// - public static Encoding GlobalEncoding = Encoding.UTF8; - - private static void Main(string[] args) - { - var client = new UdpClient(); - - for (int i = 0; i < 5000; i++) - { - // TCP TEST - } - - for (int i = 0; i < 5000; i++) - { - // UDP TEST - } - Console.ReadLine(); - } + _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; } /// - /// IP type enumeration + /// Fired when Client is connected /// - public enum IPType : byte + /// The new connection + private static void ClientConnected(QuicConnection connection) { - IPv4, - IPv6 + 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 }); + } + + /// + /// Start a Quic client + /// + /// + /// + /// + /// + public static QuicStream QuicStartClient(string ip, int port = 11000, + StreamType streamType = StreamType.ClientBidirectional) + { + var client = new QuicClient(); + + // Connect to peer (Server) + var connection = client.Connect(ip, port); + + // Create a data stream + return connection.CreateStream(streamType); + } + + /// + /// Stop the Quic server + /// + /// + public static bool QuicStopServer() + { + if (_quicServer != null) + { + _quicServer.Close(); + Logger.Info("The Quic server has been successfully stopped"); + } + + return true; + } + + private static void Main(string[] args) + { + var client = new UdpClient(); + + for (var i = 0; i < 5000; i++) + { + // TCP TEST + } + + for (var i = 0; i < 5000; i++) + { + // UDP TEST + } + + Console.ReadLine(); + } +} + +/// +/// IP type enumeration +/// +public enum IPType : byte +{ + IPv4, + IPv6 } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Connections/ConnectionPool.cs b/EonaCat.Network/System/Quic/Connections/ConnectionPool.cs index c458f2c..690dcf3 100644 --- a/EonaCat.Network/System/Quic/Connections/ConnectionPool.cs +++ b/EonaCat.Network/System/Quic/Connections/ConnectionPool.cs @@ -1,74 +1,72 @@ -using EonaCat.Quic.Infrastructure; +using System.Collections.Generic; +using EonaCat.Quic.Infrastructure; using EonaCat.Quic.Infrastructure.Settings; using EonaCat.Quic.InternalInfrastructure; -using System.Collections.Generic; -namespace EonaCat.Quic.Connections +namespace EonaCat.Quic.Connections; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Since UDP is a stateless protocol, the ConnectionPool is used as a Conenction Manager to +/// route packets to the right "Connection". +/// +internal static class ConnectionPool { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + /// + /// Starting point for connection identifiers. + /// ConnectionId's are incremented sequentially by 1. + /// + private static readonly NumberSpace _ns = new(QuicSettings.MaximumConnectionIds); + + private static readonly Dictionary _pool = new(); + + private static readonly List _draining = new(); /// - /// Since UDP is a stateless protocol, the ConnectionPool is used as a Conenction Manager to - /// route packets to the right "Connection". + /// 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. /// - internal static class ConnectionPool + /// Connection Id + /// + public static bool AddConnection(ConnectionData connection, out ulong availableConnectionId) { - /// - /// Starting point for connection identifiers. - /// ConnectionId's are incremented sequentially by 1. - /// - private static readonly NumberSpace _ns = new NumberSpace(QuicSettings.MaximumConnectionIds); + availableConnectionId = 0; - private static readonly Dictionary _pool = new Dictionary(); - - private static readonly List _draining = new List(); - - /// - /// Adds a connection to the connection pool. - /// For now assume that the client connection id is valid, and just send it back. - /// Later this should change in a way that the server validates, and regenerates a connection Id. - /// - /// Connection Id - /// - public static bool AddConnection(ConnectionData connection, out ulong availableConnectionId) + if (_pool.ContainsKey(connection.ConnectionId.Value)) { - 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; + return false; } - public static void RemoveConnection(ulong id) + if (_pool.Count > QuicSettings.MaximumConnectionIds) { - if (_pool.ContainsKey(id)) - { - _pool.Remove(id); - } + return false; } - public static QuicConnection Find(ulong id) - { - if (_pool.ContainsKey(id) == false) - { - return null; - } + availableConnectionId = _ns.Get(); - return _pool[id]; + connection.PeerConnectionId = connection.ConnectionId; + _pool.Add(availableConnectionId, new QuicConnection(connection)); + + return true; + } + + public static void RemoveConnection(ulong id) + { + if (_pool.ContainsKey(id)) + { + _pool.Remove(id); } } + + public static QuicConnection Find(ulong id) + { + if (_pool.ContainsKey(id) == false) + { + return null; + } + + return _pool[id]; + } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Connections/ConnectionState.cs b/EonaCat.Network/System/Quic/Connections/ConnectionState.cs index 80aa483..7e4b7a4 100644 --- a/EonaCat.Network/System/Quic/Connections/ConnectionState.cs +++ b/EonaCat.Network/System/Quic/Connections/ConnectionState.cs @@ -1,13 +1,11 @@ -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. +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 - } +public enum ConnectionState +{ + Open, + Closing, + Closed, + Draining } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Connections/QuicConnection.cs b/EonaCat.Network/System/Quic/Connections/QuicConnection.cs index d2a1a7e..59afc0c 100644 --- a/EonaCat.Network/System/Quic/Connections/QuicConnection.cs +++ b/EonaCat.Network/System/Quic/Connections/QuicConnection.cs @@ -1,4 +1,5 @@ -using EonaCat.Quic.Constants; +using System.Collections.Generic; +using EonaCat.Quic.Constants; using EonaCat.Quic.Events; using EonaCat.Quic.Exceptions; using EonaCat.Quic.Helpers; @@ -9,315 +10,315 @@ using EonaCat.Quic.Infrastructure.Packets; using EonaCat.Quic.Infrastructure.Settings; using EonaCat.Quic.InternalInfrastructure; using EonaCat.Quic.Streams; -using System.Collections.Generic; -namespace EonaCat.Quic.Connections +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 { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private readonly NumberSpace _numberSpace = new(); + private readonly PacketWireTransfer _pwt; + private readonly Dictionary _streams; - public class QuicConnection + private ulong _currentTransferRate; + private string _lastError; + private ConnectionState _state; + + internal QuicConnection(ConnectionData connection) { - private readonly NumberSpace _numberSpace = new NumberSpace(); - private readonly PacketWireTransfer _pwt; + _currentTransferRate = 0; + _state = ConnectionState.Open; + _lastError = string.Empty; + _streams = new Dictionary(); + _pwt = connection.PWT; - private ulong _currentTransferRate; - private ConnectionState _state; - private string _lastError; - private readonly Dictionary _streams; + ConnectionId = connection.ConnectionId; + PeerConnectionId = connection.PeerConnectionId; + // Also creates a new number space + PacketCreator = new PacketCreator(ConnectionId, PeerConnectionId); + MaxData = QuicSettings.MaxData; + MaxStreams = QuicSettings.MaximumStreamId; + } - public IntegerParts ConnectionId { get; private set; } - public IntegerParts PeerConnectionId { get; private set; } + public IntegerParts ConnectionId { get; } + public IntegerParts PeerConnectionId { get; } - public PacketCreator PacketCreator { get; private set; } - public ulong MaxData { get; private set; } - public ulong MaxStreams { get; private set; } + public PacketCreator PacketCreator { get; } + public ulong MaxData { get; private set; } + public ulong MaxStreams { get; private set; } - public StreamOpenedEvent OnStreamOpened { get; set; } - public ConnectionClosedEvent OnConnectionClosed { get; set; } + public StreamOpenedEvent OnStreamOpened { get; set; } + public ConnectionClosedEvent OnConnectionClosed { get; set; } - /// - /// Creates a new stream for sending/receiving data. - /// - /// Type of the stream (Uni-Bidirectional) - /// A new stream instance or Null if the connection is terminated. - public QuicStream CreateStream(StreamType type) + /// + /// Creates a new stream for sending/receiving data. + /// + /// Type of the stream (Uni-Bidirectional) + /// A new stream instance or Null if the connection is terminated. + public QuicStream CreateStream(StreamType type) + { + var streamId = _numberSpace.Get(); + if (_state != ConnectionState.Open) { - uint streamId = _numberSpace.Get(); - if (_state != ConnectionState.Open) + return null; + } + + var stream = new QuicStream(this, new StreamId(streamId, type)); + _streams.Add(streamId, stream); + + return stream; + } + + public QuicStream ProcessFrames(List frames) + { + QuicStream stream = null; + + foreach (var frame in frames) + { + if (frame.Type == 0x01) { - return null; + OnRstStreamFrame(frame); } - QuicStream stream = new QuicStream(this, new StreamId(streamId, type)); - _streams.Add(streamId, stream); - - return stream; - } - - public QuicStream ProcessFrames(List frames) - { - QuicStream stream = null; - - foreach (Frame frame in frames) + if (frame.Type == 0x04) { - 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); - } + OnRstStreamFrame(frame); } - return stream; - } - - public void IncrementRate(int length) - { - _currentTransferRate += (uint)length; - } - - public bool MaximumReached() - { - if (_currentTransferRate >= MaxData) + if (frame.Type >= 0x08 && frame.Type <= 0x0f) { - return true; + stream = OnStreamFrame(frame); } - 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)) + if (frame.Type == 0x10) { - // Find and reset the stream - QuicStream stream = _streams[rsf.StreamId]; - stream.ResetStream(rsf); + OnMaxDataFrame(frame); + } - // Remove the stream from the connection - _streams.Remove(rsf.StreamId); + 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); } } - private QuicStream OnStreamFrame(Frame frame) + return stream; + } + + public void IncrementRate(int length) + { + _currentTransferRate += (uint)length; + } + + public bool MaximumReached() + { + if (_currentTransferRate >= MaxData) { - QuicStream stream; + return true; + } - StreamFrame sf = (StreamFrame)frame; - StreamId streamId = sf.StreamId; + return false; + } - if (_streams.ContainsKey(streamId.Id) == false) + private void OnConnectionCloseFrame(Frame frame) + { + var ccf = (ConnectionCloseFrame)frame; + _state = ConnectionState.Draining; + _lastError = ccf.ReasonPhrase; + + OnConnectionClosed?.Invoke(this); + } + + private void OnRstStreamFrame(Frame frame) + { + var rsf = (ResetStreamFrame)frame; + if (_streams.ContainsKey(rsf.StreamId)) + { + // Find and reset the stream + var stream = _streams[rsf.StreamId]; + stream.ResetStream(rsf); + + // Remove the stream from the connection + _streams.Remove(rsf.StreamId); + } + } + + private QuicStream OnStreamFrame(Frame frame) + { + QuicStream stream; + + var sf = (StreamFrame)frame; + StreamId streamId = sf.StreamId; + + if (_streams.ContainsKey(streamId.Id) == false) + { + stream = new QuicStream(this, streamId); + + if ((ulong)_streams.Count < MaxStreams) { - stream = new QuicStream(this, streamId); - - if ((ulong)_streams.Count < MaxStreams) - { - _streams.Add(streamId.Id, stream); - } - else - { - SendMaximumStreamReachedError(); - } - - OnStreamOpened?.Invoke(stream); + _streams.Add(streamId.Id, stream); } else { - stream = _streams[streamId.Id]; + SendMaximumStreamReachedError(); } - stream.ProcessData(sf); - - return stream; + OnStreamOpened?.Invoke(stream); + } + else + { + stream = _streams[streamId.Id]; } - private void OnMaxDataFrame(Frame frame) + stream.ProcessData(sf); + + return stream; + } + + private void OnMaxDataFrame(Frame frame) + { + var sf = (MaxDataFrame)frame; + if (sf.MaximumData.Value > MaxData) { - MaxDataFrame sf = (MaxDataFrame)frame; - if (sf.MaximumData.Value > MaxData) + MaxData = sf.MaximumData.Value; + } + } + + private void OnMaxStreamDataFrame(Frame frame) + { + var msdf = (MaxStreamDataFrame)frame; + StreamId streamId = msdf.StreamId; + if (_streams.ContainsKey(streamId.Id)) + { + // Find and set the new maximum stream data on the stream + var stream = _streams[streamId.Id]; + stream.SetMaximumStreamData(msdf.MaximumStreamData.Value); + } + } + + private void OnMaxStreamFrame(Frame frame) + { + var msf = (MaxStreamsFrame)frame; + if (msf.MaximumStreams > MaxStreams) + { + MaxStreams = msf.MaximumStreams.Value; + } + } + + private void OnDataBlockedFrame(Frame frame) + { + TerminateConnection(); + } + + public QuicStream OpenStream() + { + QuicStream stream = null; + + while (stream == null) + { + var packet = _pwt.ReadPacket(); + if (packet is ShortHeaderPacket shp) { - MaxData = sf.MaximumData.Value; + stream = ProcessFrames(shp.GetFrames()); } } - private void OnMaxStreamDataFrame(Frame frame) + return stream; + } + + /// + /// Client only! + /// + /// + internal void ReceivePacket() + { + var packet = _pwt.ReadPacket(); + + if (packet is ShortHeaderPacket shp) { - MaxStreamDataFrame msdf = (MaxStreamDataFrame)frame; - StreamId streamId = msdf.StreamId; - if (_streams.ContainsKey(streamId.Id)) + ProcessFrames(shp.GetFrames()); + } + + // If the connection has been closed + if (_state == ConnectionState.Draining) + { + if (string.IsNullOrWhiteSpace(_lastError)) { - // Find and set the new maximum stream data on the stream - QuicStream stream = _streams[streamId.Id]; - stream.SetMaximumStreamData(msdf.MaximumStreamData.Value); + _lastError = "Protocol error"; } - } - private void OnMaxStreamFrame(Frame frame) - { - MaxStreamsFrame msf = (MaxStreamsFrame)frame; - if (msf.MaximumStreams > MaxStreams) - { - MaxStreams = msf.MaximumStreams.Value; - } - } + TerminateConnection(); - private void OnDataBlockedFrame(Frame frame) + throw new ConnectionException(_lastError); + } + } + + internal bool SendData(Packet packet) + { + return _pwt.SendPacket(packet); + } + + internal void TerminateConnection() + { + _state = ConnectionState.Draining; + _streams.Clear(); + + ConnectionPool.RemoveConnection(ConnectionId); + } + + internal void SendMaximumStreamReachedError() + { + var packet = + PacketCreator.CreateConnectionClosePacket(ErrorCode.STREAM_LIMIT_ERROR, 0x00, + ErrorConstants.MaxNumberOfStreams); + Send(packet); + } + + /// + /// Used to send protocol packets to the peer. + /// + /// + /// + internal bool Send(Packet packet) + { + // Encode the packet + var 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(ErrorCode.FLOW_CONTROL_ERROR, 0x00, + ErrorConstants.MaxDataTransfer); + TerminateConnection(); } - internal QuicConnection(ConnectionData connection) + // Ignore empty packets + if (data == null || data.Length <= 0) { - _currentTransferRate = 0; - _state = ConnectionState.Open; - _lastError = string.Empty; - _streams = new Dictionary(); - _pwt = connection.PWT; - - ConnectionId = connection.ConnectionId; - PeerConnectionId = connection.PeerConnectionId; - // Also creates a new number space - PacketCreator = new PacketCreator(ConnectionId, PeerConnectionId); - MaxData = QuicSettings.MaxData; - MaxStreams = QuicSettings.MaximumStreamId; + return true; } - public QuicStream OpenStream() - { - QuicStream stream = null; + var result = _pwt.SendPacket(packet); - while (stream == null) - { - Packet packet = _pwt.ReadPacket(); - if (packet is ShortHeaderPacket shp) - { - stream = ProcessFrames(shp.GetFrames()); - } - } - - return stream; - } - - /// - /// Client only! - /// - /// - internal void ReceivePacket() - { - Packet packet = _pwt.ReadPacket(); - - if (packet is ShortHeaderPacket shp) - { - ProcessFrames(shp.GetFrames()); - } - - // If the connection has been closed - if (_state == ConnectionState.Draining) - { - if (string.IsNullOrWhiteSpace(_lastError)) - { - _lastError = "Protocol error"; - } - - TerminateConnection(); - - throw new ConnectionException(_lastError); - } - } - - internal bool SendData(Packet packet) - { - return _pwt.SendPacket(packet); - } - - internal void TerminateConnection() - { - _state = ConnectionState.Draining; - _streams.Clear(); - - ConnectionPool.RemoveConnection(this.ConnectionId); - } - - internal void SendMaximumStreamReachedError() - { - ShortHeaderPacket packet = PacketCreator.CreateConnectionClosePacket(Infrastructure.ErrorCode.STREAM_LIMIT_ERROR, 0x00, ErrorConstants.MaxNumberOfStreams); - Send(packet); - } - - /// - /// Used to send protocol packets to the peer. - /// - /// - /// - internal bool Send(Packet packet) - { - // Encode the packet - byte[] data = packet.Encode(); - - // Increment the connection transfer rate - IncrementRate(data.Length); - - // If the maximum transfer rate is reached, send FLOW_CONTROL_ERROR - if (MaximumReached()) - { - packet = PacketCreator.CreateConnectionClosePacket(Infrastructure.ErrorCode.FLOW_CONTROL_ERROR, 0x00, ErrorConstants.MaxDataTransfer); - - TerminateConnection(); - } - - // Ignore empty packets - if (data == null || data.Length <= 0) - { - return true; - } - - bool result = _pwt.SendPacket(packet); - - return result; - } + return result; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Constants/ErrorConstants.cs b/EonaCat.Network/System/Quic/Constants/ErrorConstants.cs index 33b2a02..b96618c 100644 --- a/EonaCat.Network/System/Quic/Constants/ErrorConstants.cs +++ b/EonaCat.Network/System/Quic/Constants/ErrorConstants.cs @@ -1,13 +1,11 @@ -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. +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."; - } +public class ErrorConstants +{ + public const string ServerTooBusy = "The server is too busy to process your request."; + public const string MaxDataTransfer = "Maximum data transfer reached."; + public const string MaxNumberOfStreams = "Maximum number of streams reached."; + public const string PMTUNotReached = "PMTU have not been reached."; } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Context/QuicStreamContext.cs b/EonaCat.Network/System/Quic/Context/QuicStreamContext.cs index d6c9ee0..41b2f73 100644 --- a/EonaCat.Network/System/Quic/Context/QuicStreamContext.cs +++ b/EonaCat.Network/System/Quic/Context/QuicStreamContext.cs @@ -1,77 +1,74 @@ using EonaCat.Quic.Streams; -namespace EonaCat.Quic.Context +namespace EonaCat.Quic.Context; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Wrapper to represent the stream. +/// +public class QuicStreamContext { - // This file is part of the EonaCat 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 constructor to prevent creating the context outside the scope of Quic. + /// + /// + internal QuicStreamContext(QuicStream stream) + { + Stream = stream; + StreamId = stream.StreamId; + } + ///// + ///// The connection's context. + ///// + //public QuicContext ConnectionContext { get; set; } /// - /// Wrapper to represent the stream. + /// Data received /// - public class QuicStreamContext + public byte[] Data { get; private set; } + + /// + /// Unique stream identifier + /// + public ulong StreamId { get; private set; } + + internal QuicStream Stream { get; set; } + + /// + /// Send data to the client. + /// + /// + /// + public bool Send(byte[] data) { - ///// - ///// The connection's context. - ///// - //public QuicContext ConnectionContext { get; set; } - - /// - /// Data received - /// - public byte[] Data { get; private set; } - - /// - /// Unique stream identifier - /// - public ulong StreamId { get; private set; } - - /// - /// Send data to the client. - /// - /// - /// - public bool Send(byte[] data) + if (Stream.CanSendData() == false) { - 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() + // Ignore empty packets + if (data == null || data.Length <= 0) { - // TODO: Close out the stream by sending appropriate packets to the peer + return true; } - internal QuicStream Stream { get; set; } + // Packet packet = ConnectionContext.Connection.PacketCreator.CreateDataPacket(StreamId, data); - /// - /// Internal constructor to prevent creating the context outside the scope of Quic. - /// - /// - internal QuicStreamContext(QuicStream stream) - { - Stream = stream; - StreamId = stream.StreamId; - } + // bool result = ConnectionContext.Send(packet); - internal void SetData(byte[] data) - { - Data = data; - } + //return result; + + return false; + } + + public void Close() + { + // TODO: Close out the stream by sending appropriate packets to the peer + } + + internal void SetData(byte[] data) + { + Data = data; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Events/DelegateDefinitions.cs b/EonaCat.Network/System/Quic/Events/DelegateDefinitions.cs index f27ada8..6276d6b 100644 --- a/EonaCat.Network/System/Quic/Events/DelegateDefinitions.cs +++ b/EonaCat.Network/System/Quic/Events/DelegateDefinitions.cs @@ -1,16 +1,14 @@ 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. +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 ClientConnectedEvent(QuicConnection connection); - public delegate void StreamOpenedEvent(QuicStream stream); +public delegate void StreamOpenedEvent(QuicStream stream); - public delegate void StreamDataReceivedEvent(QuicStream stream, byte[] data); +public delegate void StreamDataReceivedEvent(QuicStream stream, byte[] data); - public delegate void ConnectionClosedEvent(QuicConnection connection); -} \ No newline at end of file +public delegate void ConnectionClosedEvent(QuicConnection connection); \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Events/QuicEventArgs.cs b/EonaCat.Network/System/Quic/Events/QuicEventArgs.cs index eb806a5..bf46d65 100644 --- a/EonaCat.Network/System/Quic/Events/QuicEventArgs.cs +++ b/EonaCat.Network/System/Quic/Events/QuicEventArgs.cs @@ -1,19 +1,17 @@ using EonaCat.Quic.Connections; using EonaCat.Quic.Streams; -namespace EonaCat.Quic.Events +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 { - // This file is part of the EonaCat 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 QuicStream Stream { get; set; } + public byte[] Data { get; set; } +} - public class QuicStreamEventArgs - { - public QuicStream Stream { get; set; } - public byte[] Data { get; set; } - } - - public class QuicConnectionEventArgs - { - public QuicConnection Connection { get; set; } - } +public class QuicConnectionEventArgs +{ + public QuicConnection Connection { get; set; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Exceptions/ConnectionException.cs b/EonaCat.Network/System/Quic/Exceptions/ConnectionException.cs index b723049..adafe26 100644 --- a/EonaCat.Network/System/Quic/Exceptions/ConnectionException.cs +++ b/EonaCat.Network/System/Quic/Exceptions/ConnectionException.cs @@ -1,14 +1,12 @@ 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. +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 class ConnectionException : Exception +{ + public ConnectionException(string message) : base($"EonaCat Network: {message}") { - public ConnectionException(string message) : base($"EonaCat Network: {message}") - { - } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Exceptions/ServerNotStartedException.cs b/EonaCat.Network/System/Quic/Exceptions/ServerNotStartedException.cs index 16ab1af..df044f4 100644 --- a/EonaCat.Network/System/Quic/Exceptions/ServerNotStartedException.cs +++ b/EonaCat.Network/System/Quic/Exceptions/ServerNotStartedException.cs @@ -1,17 +1,16 @@ using System; -namespace EonaCat.Quic.Exceptions +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 { - // This file is part of the EonaCat 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() - { } + } - public ServerNotStartedException(string message) : base($"EonaCat Network: {message}") - { - } + public ServerNotStartedException(string message) : base($"EonaCat Network: {message}") + { } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Exceptions/StreamException.cs b/EonaCat.Network/System/Quic/Exceptions/StreamException.cs index a5205c5..3d92cc7 100644 --- a/EonaCat.Network/System/Quic/Exceptions/StreamException.cs +++ b/EonaCat.Network/System/Quic/Exceptions/StreamException.cs @@ -1,17 +1,16 @@ using System; -namespace EonaCat.Quic.Exceptions +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 { - // This file is part of the EonaCat 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() - { } + } - public StreamException(string message) : base($"EonaCat Network: {message}") - { - } + public StreamException(string message) : base($"EonaCat Network: {message}") + { } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Helpers/ByteArray.cs b/EonaCat.Network/System/Quic/Helpers/ByteArray.cs index ea82068..098f15c 100644 --- a/EonaCat.Network/System/Quic/Helpers/ByteArray.cs +++ b/EonaCat.Network/System/Quic/Helpers/ByteArray.cs @@ -1,99 +1,97 @@ using System; -namespace EonaCat.Quic.Helpers +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 { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private readonly byte[] _array; + private readonly int _length; - public class ByteArray + private int _offset; + + public ByteArray(byte[] array) { - private readonly byte[] _array; - private readonly int _length; + _array = array; + _length = array.Length; + _offset = 0; + } - private int _offset; + public byte ReadByte() + { + var result = _array[_offset++]; + return result; + } - public ByteArray(byte[] array) - { - _array = array; - _length = array.Length; - _offset = 0; - } + public byte PeekByte() + { + var result = _array[_offset]; + return result; + } - public byte ReadByte() - { - byte result = _array[_offset++]; - return result; - } + public byte[] ReadBytes(int count) + { + var bytes = new byte[count]; + Buffer.BlockCopy(_array, _offset, bytes, 0, count); - public byte PeekByte() - { - byte result = _array[_offset]; - return result; - } + _offset += count; - public byte[] ReadBytes(int count) - { - byte[] bytes = new byte[count]; - Buffer.BlockCopy(_array, _offset, bytes, 0, count); + return bytes; + } - _offset += count; + public byte[] ReadBytes(IntegerVar count) + { + return ReadBytes(count.Value); + } - return bytes; - } + public ushort ReadUInt16() + { + var bytes = ReadBytes(2); + var result = ByteHelpers.ToUInt16(bytes); - public byte[] ReadBytes(IntegerVar count) - { - return ReadBytes(count.Value); - } + return result; + } - public ushort ReadUInt16() - { - byte[] bytes = ReadBytes(2); - ushort result = ByteHelpers.ToUInt16(bytes); + public uint ReadUInt32() + { + var bytes = ReadBytes(4); + var result = ByteHelpers.ToUInt32(bytes); - return result; - } + return result; + } - public uint ReadUInt32() - { - byte[] bytes = ReadBytes(4); - uint result = ByteHelpers.ToUInt32(bytes); + public IntegerVar ReadIntegerVar() + { + // Set Token Length and Token + var initial = PeekByte(); + var size = IntegerVar.Size(initial); - return result; - } + var bytes = new byte[size]; + Buffer.BlockCopy(_array, _offset, bytes, 0, size); + _offset += size; - public IntegerVar ReadIntegerVar() - { - // Set Token Length and Token - byte initial = PeekByte(); - int size = IntegerVar.Size(initial); + return bytes; + } - byte[] bytes = new byte[size]; - Buffer.BlockCopy(_array, _offset, bytes, 0, size); - _offset += size; + public IntegerParts ReadGranularInteger(int size) + { + var data = ReadBytes(size); + IntegerParts result = data; - return bytes; - } + return result; + } - public IntegerParts ReadGranularInteger(int size) - { - byte[] data = ReadBytes(size); - IntegerParts result = data; + public StreamId ReadStreamId() + { + var streamId = ReadBytes(8); + StreamId result = streamId; - return result; - } + return result; + } - public StreamId ReadStreamId() - { - byte[] streamId = ReadBytes(8); - StreamId result = streamId; - - return result; - } - - public bool HasData() - { - return _offset < _length; - } + public bool HasData() + { + return _offset < _length; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Helpers/ByteHelpers.cs b/EonaCat.Network/System/Quic/Helpers/ByteHelpers.cs index 80012f5..4033a1f 100644 --- a/EonaCat.Network/System/Quic/Helpers/ByteHelpers.cs +++ b/EonaCat.Network/System/Quic/Helpers/ByteHelpers.cs @@ -1,94 +1,92 @@ using System; using System.Text; -namespace EonaCat.Quic.Helpers +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 { - // This file is part of the EonaCat 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(ulong integer) { - public static byte[] GetBytes(ulong integer) + var result = BitConverter.GetBytes(integer); + if (BitConverter.IsLittleEndian) { - byte[] result = BitConverter.GetBytes(integer); - if (BitConverter.IsLittleEndian) - { - Array.Reverse(result); - } - - return result; + Array.Reverse(result); } - public static byte[] GetBytes(uint integer) - { - byte[] result = BitConverter.GetBytes(integer); - if (BitConverter.IsLittleEndian) - { - Array.Reverse(result); - } + return result; + } - return result; + public static byte[] GetBytes(uint integer) + { + var result = BitConverter.GetBytes(integer); + if (BitConverter.IsLittleEndian) + { + Array.Reverse(result); } - public static byte[] GetBytes(ushort integer) - { - byte[] result = BitConverter.GetBytes(integer); - if (BitConverter.IsLittleEndian) - { - Array.Reverse(result); - } + return result; + } - return result; + public static byte[] GetBytes(ushort integer) + { + var result = BitConverter.GetBytes(integer); + if (BitConverter.IsLittleEndian) + { + Array.Reverse(result); } - public static byte[] GetBytes(string str) - { - byte[] result = Encoding.UTF8.GetBytes(str); + return result; + } - return result; + public static byte[] GetBytes(string str) + { + var result = Encoding.UTF8.GetBytes(str); + + return result; + } + + public static ulong ToUInt64(byte[] data) + { + if (BitConverter.IsLittleEndian) + { + Array.Reverse(data); } - public static ulong ToUInt64(byte[] data) + var result = BitConverter.ToUInt64(data, 0); + + return result; + } + + public static uint ToUInt32(byte[] data) + { + if (BitConverter.IsLittleEndian) { - if (BitConverter.IsLittleEndian) - { - Array.Reverse(data); - } - - ulong result = BitConverter.ToUInt64(data, 0); - - return result; + Array.Reverse(data); } - public static uint ToUInt32(byte[] data) + var result = BitConverter.ToUInt32(data, 0); + + return result; + } + + public static ushort ToUInt16(byte[] data) + { + if (BitConverter.IsLittleEndian) { - if (BitConverter.IsLittleEndian) - { - Array.Reverse(data); - } - - uint result = BitConverter.ToUInt32(data, 0); - - return result; + Array.Reverse(data); } - public static ushort ToUInt16(byte[] data) - { - if (BitConverter.IsLittleEndian) - { - Array.Reverse(data); - } + var result = BitConverter.ToUInt16(data, 0); - ushort result = BitConverter.ToUInt16(data, 0); + return result; + } - return result; - } + public static string GetString(byte[] str) + { + var result = Encoding.UTF8.GetString(str); - public static string GetString(byte[] str) - { - string result = Encoding.UTF8.GetString(str); - - return result; - } + return result; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Helpers/IntegerParts.cs b/EonaCat.Network/System/Quic/Helpers/IntegerParts.cs index 01f0d04..fe839f6 100644 --- a/EonaCat.Network/System/Quic/Helpers/IntegerParts.cs +++ b/EonaCat.Network/System/Quic/Helpers/IntegerParts.cs @@ -1,99 +1,97 @@ using System; -namespace EonaCat.Quic.Helpers +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 { - // This file is part of the EonaCat 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 const ulong MaxValue = 18446744073709551615; - public class IntegerParts + public IntegerParts(ulong integer) { - public const ulong MaxValue = 18446744073709551615; + Value = integer; + } - public ulong Value { get; } + public ulong Value { get; } - public byte Size => RequiredBytes(Value); + public byte Size => RequiredBytes(Value); - public IntegerParts(ulong integer) + public byte[] ToByteArray() + { + return Encode(Value); + } + + public static implicit operator byte[](IntegerParts integer) + { + return Encode(integer.Value); + } + + public static implicit operator IntegerParts(byte[] bytes) + { + return new IntegerParts(Decode(bytes)); + } + + public static implicit operator IntegerParts(ulong integer) + { + return new IntegerParts(integer); + } + + public static implicit operator ulong(IntegerParts integer) + { + return integer.Value; + } + + public static byte[] Encode(ulong integer) + { + var requiredBytes = RequiredBytes(integer); + var offset = 8 - requiredBytes; + + var uInt64Bytes = ByteHelpers.GetBytes(integer); + + var result = new byte[requiredBytes]; + Buffer.BlockCopy(uInt64Bytes, offset, result, 0, requiredBytes); + + return result; + } + + public static ulong Decode(byte[] bytes) + { + var i = 8 - bytes.Length; + var buffer = new byte[8]; + + Buffer.BlockCopy(bytes, 0, buffer, i, bytes.Length); + + var res = ByteHelpers.ToUInt64(buffer); + + return res; + } + + private static byte RequiredBytes(ulong integer) + { + byte result = 0; + + if (integer <= byte.MaxValue) /* 255 */ { - Value = integer; + result = 1; + } + else if (integer <= ushort.MaxValue) /* 65535 */ + { + result = 2; + } + else if (integer <= uint.MaxValue) /* 4294967295 */ + { + result = 4; + } + else if (integer <= ulong.MaxValue) /* 18446744073709551615 */ + { + result = 8; + } + else + { + throw new ArgumentOutOfRangeException("Value is larger than GranularInteger.MaxValue."); } - public byte[] ToByteArray() - { - return Encode(this.Value); - } - - public static implicit operator byte[](IntegerParts integer) - { - return Encode(integer.Value); - } - - public static implicit operator IntegerParts(byte[] bytes) - { - return new IntegerParts(Decode(bytes)); - } - - public static implicit operator IntegerParts(ulong integer) - { - return new IntegerParts(integer); - } - - public static implicit operator ulong(IntegerParts integer) - { - return integer.Value; - } - - public static byte[] Encode(ulong 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 ulong Decode(byte[] bytes) - { - int i = 8 - bytes.Length; - byte[] buffer = new byte[8]; - - Buffer.BlockCopy(bytes, 0, buffer, i, bytes.Length); - - ulong res = ByteHelpers.ToUInt64(buffer); - - return res; - } - - private static byte RequiredBytes(ulong integer) - { - byte result = 0; - - if (integer <= byte.MaxValue) /* 255 */ - { - result = 1; - } - else if (integer <= ushort.MaxValue) /* 65535 */ - { - result = 2; - } - else if (integer <= uint.MaxValue) /* 4294967295 */ - { - result = 4; - } - else if (integer <= ulong.MaxValue) /* 18446744073709551615 */ - { - result = 8; - } - else - { - throw new ArgumentOutOfRangeException("Value is larger than GranularInteger.MaxValue."); - } - - return result; - } + return result; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Helpers/StreamId.cs b/EonaCat.Network/System/Quic/Helpers/StreamId.cs index 53623b8..1caf8fc 100644 --- a/EonaCat.Network/System/Quic/Helpers/StreamId.cs +++ b/EonaCat.Network/System/Quic/Helpers/StreamId.cs @@ -1,69 +1,67 @@ -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. +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 +public enum StreamType +{ + ClientBidirectional = 0x0, + ServerBidirectional = 0x1, + ClientUnidirectional = 0x2, + ServerUnidirectional = 0x3 +} + +public class StreamId +{ + public StreamId(ulong id, StreamType type) { - ClientBidirectional = 0x0, - ServerBidirectional = 0x1, - ClientUnidirectional = 0x2, - ServerUnidirectional = 0x3 + Id = id; + Type = type; + IntegerValue = (id << 2) | (ulong)type; } - public class StreamId + public ulong Id { get; } + public ulong IntegerValue { get; } + public StreamType Type { get; } + + public static implicit operator byte[](StreamId id) { - public ulong Id { get; } - public ulong IntegerValue { get; } - public StreamType Type { get; private set; } + return Encode(id.Id, id.Type); + } - public StreamId(ulong id, StreamType type) - { - Id = id; - Type = type; - IntegerValue = id << 2 | (ulong)type; - } + public static implicit operator StreamId(byte[] data) + { + return Decode(data); + } - public static implicit operator byte[](StreamId id) - { - return Encode(id.Id, id.Type); - } + public static implicit operator ulong(StreamId streamId) + { + return streamId.Id; + } - public static implicit operator StreamId(byte[] data) - { - return Decode(data); - } + public static implicit operator StreamId(IntegerVar integer) + { + return Decode(ByteHelpers.GetBytes(integer.Value)); + } - public static implicit operator ulong(StreamId streamId) - { - return streamId.Id; - } + public static byte[] Encode(ulong id, StreamType type) + { + var identifier = (id << 2) | (ulong)type; - public static implicit operator StreamId(IntegerVar integer) - { - return Decode(ByteHelpers.GetBytes(integer.Value)); - } + var result = ByteHelpers.GetBytes(identifier); - public static byte[] Encode(ulong id, StreamType type) - { - ulong identifier = id << 2 | (ulong)type; + return result; + } - byte[] result = ByteHelpers.GetBytes(identifier); + public static StreamId Decode(byte[] data) + { + StreamId result; + var id = ByteHelpers.ToUInt64(data); + var identifier = id >> 2; + var type = 0x03 & id; + var streamType = (StreamType)type; - return result; - } + result = new StreamId(identifier, streamType); - public static StreamId Decode(byte[] data) - { - StreamId result; - ulong id = ByteHelpers.ToUInt64(data); - ulong identifier = id >> 2; - ulong type = 0x03 & id; - StreamType streamType = (StreamType)type; - - result = new StreamId(identifier, streamType); - - return result; - } + return result; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Helpers/VariableInteger.cs b/EonaCat.Network/System/Quic/Helpers/VariableInteger.cs index a1f979c..4940b03 100644 --- a/EonaCat.Network/System/Quic/Helpers/VariableInteger.cs +++ b/EonaCat.Network/System/Quic/Helpers/VariableInteger.cs @@ -1,106 +1,104 @@ using System; -namespace EonaCat.Quic.Helpers +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 { - // This file is part of the EonaCat 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 const ulong MaxValue = 4611686018427387903; - public class IntegerVar + public IntegerVar(ulong integer) { - public const ulong MaxValue = 4611686018427387903; + Value = integer; + } - public ulong Value { get; } + public ulong Value { get; } - public IntegerVar(ulong integer) + public static implicit operator byte[](IntegerVar integer) + { + return Encode(integer.Value); + } + + public static implicit operator IntegerVar(byte[] bytes) + { + return new IntegerVar(Decode(bytes)); + } + + public static implicit operator IntegerVar(ulong integer) + { + return new IntegerVar(integer); + } + + public static implicit operator ulong(IntegerVar integer) + { + return integer.Value; + } + + public static implicit operator IntegerVar(StreamId streamId) + { + return new IntegerVar(streamId.IntegerValue); + } + + public static int Size(byte firstByte) + { + var result = (int)Math.Pow(2, firstByte >> 6); + + return result; + } + + public byte[] ToByteArray() + { + return Encode(Value); + } + + public static byte[] Encode(ulong integer) + { + var requiredBytes = 0; + if (integer <= byte.MaxValue >> 2) /* 63 */ { - Value = integer; + requiredBytes = 1; + } + else if (integer <= ushort.MaxValue >> 2) /* 16383 */ + { + requiredBytes = 2; + } + else if (integer <= uint.MaxValue >> 2) /* 1073741823 */ + { + requiredBytes = 4; + } + else if (integer <= ulong.MaxValue >> 2) /* 4611686018427387903 */ + { + requiredBytes = 8; + } + else + { + throw new ArgumentOutOfRangeException("Value is larger than IntegerVar.MaxValue."); } - public static implicit operator byte[](IntegerVar integer) - { - return Encode(integer.Value); - } + var offset = 8 - requiredBytes; - public static implicit operator IntegerVar(byte[] bytes) - { - return new IntegerVar(Decode(bytes)); - } + var uInt64Bytes = ByteHelpers.GetBytes(integer); + var first = uInt64Bytes[offset]; + first = (byte)(first | ((requiredBytes / 2) << 6)); + uInt64Bytes[offset] = first; - public static implicit operator IntegerVar(ulong integer) - { - return new IntegerVar(integer); - } + var result = new byte[requiredBytes]; + Buffer.BlockCopy(uInt64Bytes, offset, result, 0, requiredBytes); - public static implicit operator ulong(IntegerVar integer) - { - return integer.Value; - } + return result; + } - public static implicit operator IntegerVar(StreamId streamId) - { - return new IntegerVar(streamId.IntegerValue); - } + public static ulong Decode(byte[] bytes) + { + var i = 8 - bytes.Length; + var buffer = new byte[8]; - public static int Size(byte firstByte) - { - int result = (int)Math.Pow(2, (firstByte >> 6)); + Buffer.BlockCopy(bytes, 0, buffer, i, bytes.Length); + buffer[i] = (byte)(buffer[i] & (255 >> 2)); - return result; - } + var res = ByteHelpers.ToUInt64(buffer); - public byte[] ToByteArray() - { - return Encode(this.Value); - } - - public static byte[] Encode(ulong integer) - { - int requiredBytes = 0; - if (integer <= byte.MaxValue >> 2) /* 63 */ - { - requiredBytes = 1; - } - else if (integer <= ushort.MaxValue >> 2) /* 16383 */ - { - requiredBytes = 2; - } - else if (integer <= uint.MaxValue >> 2) /* 1073741823 */ - { - requiredBytes = 4; - } - else if (integer <= ulong.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 ulong 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)); - - ulong res = ByteHelpers.ToUInt64(buffer); - - return res; - } + return res; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/ErrorCodes.cs b/EonaCat.Network/System/Quic/Infrastructure/ErrorCodes.cs index 5c42dbd..58da418 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/ErrorCodes.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/ErrorCodes.cs @@ -1,26 +1,24 @@ -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. +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 : ushort - { - 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 - } +public enum ErrorCode : ushort +{ + NO_ERROR = 0x0, + INTERNAL_ERROR = 0x1, + CONNECTION_REFUSED = 0x2, + FLOW_CONTROL_ERROR = 0x3, + STREAM_LIMIT_ERROR = 0x4, + STREAM_STATE_ERROR = 0x5, + FINAL_SIZE_ERROR = 0x6, + FRAME_ENCODING_ERROR = 0x7, + TRANSPORT_PARAMETER_ERROR = 0x8, + CONNECTION_ID_LIMIT_ERROR = 0x9, + PROTOCOL_VIOLATION = 0xA, + INVALID_TOKEN = 0xB, + APPLICATION_ERROR = 0xC, + CRYPTO_BUFFER_EXCEEDED = 0xD, + KEY_UPDATE_ERROR = 0xE, + AEAD_LIMIT_REACHED = 0xF, + CRYPTO_ERROR = 0x100 } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Exceptions/ProtocolException.cs b/EonaCat.Network/System/Quic/Infrastructure/Exceptions/ProtocolException.cs index 1f3a83d..f78819a 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Exceptions/ProtocolException.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Exceptions/ProtocolException.cs @@ -1,18 +1,16 @@ using System; -namespace EonaCat.Quic.Infrastructure.Exceptions +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 { - // This file is part of the EonaCat 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() - { - } + } - public ProtocolException(string message) : base($"EonaCat Network: {message}") - { - } + public ProtocolException(string message) : base($"EonaCat Network: {message}") + { } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/FrameParser.cs b/EonaCat.Network/System/Quic/Infrastructure/FrameParser.cs index 22f89de..59acb78 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/FrameParser.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/FrameParser.cs @@ -1,154 +1,152 @@ using EonaCat.Quic.Helpers; using EonaCat.Quic.Infrastructure.Frames; -namespace EonaCat.Quic.Infrastructure +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 { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private readonly ByteArray _array; - public class FrameParser + public FrameParser(ByteArray array) { - private readonly ByteArray _array; + _array = array; + } - public FrameParser(ByteArray array) + public Frame GetFrame() + { + Frame result; + var frameType = _array.PeekByte(); + switch (frameType) { - _array = array; + 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; } - public Frame GetFrame() - { - Frame result; - byte frameType = _array.PeekByte(); - switch (frameType) - { - case 0x00: - result = new PaddingFrame(); - break; + result?.Decode(_array); - 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; - } - - result?.Decode(_array); - - return result; - } + return result; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/AckFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/AckFrame.cs index 058fab6..3855e69 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/AckFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/AckFrame.cs @@ -1,23 +1,21 @@ -using EonaCat.Quic.Helpers; -using System; +using System; +using EonaCat.Quic.Helpers; -namespace EonaCat.Quic.Infrastructure.Frames +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 { - // This file is part of the EonaCat 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 override byte Type => 0x02; - public class AckFrame : Frame + public override void Decode(ByteArray array) { - public override byte Type => 0x02; + throw new NotImplementedException(); + } - public override void Decode(ByteArray array) - { - throw new NotImplementedException(); - } - - public override byte[] Encode() - { - throw new NotImplementedException(); - } + public override byte[] Encode() + { + throw new NotImplementedException(); } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/ConnectionCloseFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/ConnectionCloseFrame.cs index 244cca8..50838cb 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/ConnectionCloseFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/ConnectionCloseFrame.cs @@ -1,84 +1,82 @@ -using EonaCat.Quic.Helpers; -using System.Collections.Generic; +using System.Collections.Generic; +using EonaCat.Quic.Helpers; -namespace EonaCat.Quic.Infrastructure.Frames +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 { - // This file is part of the EonaCat 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 ConnectionCloseFrame() { - 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; } + ErrorCode = 0; + ReasonPhraseLength = new IntegerVar(0); + } - public ConnectionCloseFrame() + /// + /// 0x1d not yet supported (Application Protocol Error) + /// + public ConnectionCloseFrame(ErrorCode error, byte frameType, string reason) + { + ActualType = 0x1c; + + ErrorCode = (ulong)error; + FrameType = new IntegerVar(frameType); + if (!string.IsNullOrWhiteSpace(reason)) + { + ReasonPhraseLength = new IntegerVar((ulong)reason.Length); + } + else { - ErrorCode = 0; ReasonPhraseLength = new IntegerVar(0); } - /// - /// 0x1d not yet supported (Application Protocol Error) - /// - public ConnectionCloseFrame(ErrorCode error, byte frameType, string reason) + ReasonPhrase = reason; + } + + 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 override void Decode(ByteArray array) + { + ActualType = array.ReadByte(); + ErrorCode = array.ReadIntegerVar(); + if (ActualType == 0x1c) { - ActualType = 0x1c; - - ErrorCode = (ulong)error; - FrameType = new IntegerVar(frameType); - if (!string.IsNullOrWhiteSpace(reason)) - { - ReasonPhraseLength = new IntegerVar((ulong)reason.Length); - } - else - { - ReasonPhraseLength = new IntegerVar(0); - } - - ReasonPhrase = reason; + FrameType = array.ReadIntegerVar(); } - public override void Decode(ByteArray array) + ReasonPhraseLength = array.ReadIntegerVar(); + + var rp = array.ReadBytes((int)ReasonPhraseLength.Value); + ReasonPhrase = ByteHelpers.GetString(rp); + } + + public override byte[] Encode() + { + var result = new List { - 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); + ActualType + }; + result.AddRange(ErrorCode.ToByteArray()); + if (ActualType == 0x1c) + { + result.AddRange(FrameType.ToByteArray()); } - public override byte[] Encode() + if (string.IsNullOrWhiteSpace(ReasonPhrase) == false) { - List result = new List - { - ActualType - }; - result.AddRange(ErrorCode.ToByteArray()); - if (ActualType == 0x1c) - { - result.AddRange(FrameType.ToByteArray()); - } + byte[] rpl = new IntegerVar((ulong)ReasonPhrase.Length); + result.AddRange(rpl); - if (string.IsNullOrWhiteSpace(ReasonPhrase) == false) - { - byte[] rpl = new IntegerVar((ulong)ReasonPhrase.Length); - result.AddRange(rpl); - - byte[] reasonPhrase = ByteHelpers.GetBytes(ReasonPhrase); - result.AddRange(reasonPhrase); - } - - return result.ToArray(); + var reasonPhrase = ByteHelpers.GetBytes(ReasonPhrase); + result.AddRange(reasonPhrase); } + + return result.ToArray(); } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/CryptoFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/CryptoFrame.cs index 4235ef0..0545b41 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/CryptoFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/CryptoFrame.cs @@ -1,22 +1,21 @@ -using EonaCat.Quic.Helpers; -using System; +using System; +using EonaCat.Quic.Helpers; -namespace EonaCat.Quic.Infrastructure.Frames +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 { - // This file is part of the EonaCat 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) { - public override byte Type => 0x06; + throw new NotImplementedException(); + } - public override void Decode(ByteArray array) - { - throw new NotImplementedException(); - } - - public override byte[] Encode() - { - throw new NotImplementedException(); - } + public override byte[] Encode() + { + throw new NotImplementedException(); } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/DataBlockedFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/DataBlockedFrame.cs index c3917ff..bbe05b3 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/DataBlockedFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/DataBlockedFrame.cs @@ -1,39 +1,38 @@ -using EonaCat.Quic.Helpers; -using System.Collections.Generic; +using System.Collections.Generic; +using EonaCat.Quic.Helpers; -namespace EonaCat.Quic.Infrastructure.Frames +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 { - // This file is part of the EonaCat 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 DataBlockedFrame() { - public override byte Type => 0x14; - public IntegerVar MaximumData { get; set; } + } - public DataBlockedFrame() + public DataBlockedFrame(ulong dataLimit) + { + MaximumData = dataLimit; + } + + public override byte Type => 0x14; + public IntegerVar MaximumData { get; set; } + + public override void Decode(ByteArray array) + { + var type = array.ReadByte(); + MaximumData = array.ReadIntegerVar(); + } + + public override byte[] Encode() + { + var result = new List { - } + Type + }; + result.AddRange(MaximumData.ToByteArray()); - public DataBlockedFrame(ulong dataLimit) - { - MaximumData = dataLimit; - } - - public override void Decode(ByteArray array) - { - byte type = array.ReadByte(); - MaximumData = array.ReadIntegerVar(); - } - - public override byte[] Encode() - { - List result = new List - { - Type - }; - result.AddRange(MaximumData.ToByteArray()); - - return result.ToArray(); - } + return result.ToArray(); } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/Frame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/Frame.cs index 47a731b..a4d6b6f 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/Frame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/Frame.cs @@ -1,19 +1,17 @@ using EonaCat.Quic.Helpers; -namespace EonaCat.Quic.Infrastructure.Frames +namespace EonaCat.Quic.Infrastructure.Frames; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Data encapsulation unit for a Packet. +/// +public abstract class Frame { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + public abstract byte Type { get; } - /// - /// Data encapsulation unit for a Packet. - /// - public abstract class Frame - { - public abstract byte Type { get; } + public abstract byte[] Encode(); - public abstract byte[] Encode(); - - public abstract void Decode(ByteArray array); - } + public abstract void Decode(ByteArray array); } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxDataFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxDataFrame.cs index 645d684..2c4d9dd 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxDataFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxDataFrame.cs @@ -1,32 +1,31 @@ -using EonaCat.Quic.Helpers; -using System.Collections.Generic; +using System.Collections.Generic; +using EonaCat.Quic.Helpers; -namespace EonaCat.Quic.Infrastructure.Frames +namespace EonaCat.Quic.Infrastructure.Frames; + +public class MaxDataFrame : Frame { - 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) { - // 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. + array.ReadByte(); + MaximumData = array.ReadIntegerVar(); + } - public override byte Type => 0x10; - public IntegerVar MaximumData { get; set; } - - public override void Decode(ByteArray array) + public override byte[] Encode() + { + var result = new List { - array.ReadByte(); - MaximumData = array.ReadIntegerVar(); - } + Type + }; + result.AddRange(MaximumData.ToByteArray()); - public override byte[] Encode() - { - List result = new List - { - Type - }; - result.AddRange(MaximumData.ToByteArray()); - - return result.ToArray(); - } + return result.ToArray(); } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxStreamDataFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxStreamDataFrame.cs index 1fd5fb1..bcfc98f 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxStreamDataFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxStreamDataFrame.cs @@ -1,46 +1,44 @@ -using EonaCat.Quic.Helpers; -using System.Collections.Generic; +using System.Collections.Generic; +using EonaCat.Quic.Helpers; -namespace EonaCat.Quic.Infrastructure.Frames +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 { - // This file is part of the EonaCat 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 MaxStreamDataFrame() { - public override byte Type => 0x11; - public IntegerVar StreamId { get; set; } - public IntegerVar MaximumStreamData { get; set; } + } - public StreamId ConvertedStreamId { get; set; } + public MaxStreamDataFrame(ulong streamId, ulong maximumStreamData) + { + StreamId = streamId; + MaximumStreamData = maximumStreamData; + } - public MaxStreamDataFrame() + public override byte Type => 0x11; + public IntegerVar StreamId { get; set; } + public IntegerVar MaximumStreamData { get; set; } + + public StreamId ConvertedStreamId { get; set; } + + public override void Decode(ByteArray array) + { + var type = array.ReadByte(); + StreamId = array.ReadIntegerVar(); + MaximumStreamData = array.ReadIntegerVar(); + } + + public override byte[] Encode() + { + var result = new List { - } + Type + }; + result.AddRange(StreamId.ToByteArray()); + result.AddRange(MaximumStreamData.ToByteArray()); - public MaxStreamDataFrame(ulong streamId, ulong maximumStreamData) - { - StreamId = streamId; - MaximumStreamData = maximumStreamData; - } - - public override void Decode(ByteArray array) - { - byte type = array.ReadByte(); - StreamId = array.ReadIntegerVar(); - MaximumStreamData = array.ReadIntegerVar(); - } - - public override byte[] Encode() - { - List result = new List - { - Type - }; - result.AddRange(StreamId.ToByteArray()); - result.AddRange(MaximumStreamData.ToByteArray()); - - return result.ToArray(); - } + return result.ToArray(); } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxStreamsFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxStreamsFrame.cs index fc3be9a..b587f93 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxStreamsFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/MaxStreamsFrame.cs @@ -1,39 +1,38 @@ -using EonaCat.Quic.Helpers; -using System.Collections.Generic; +using System.Collections.Generic; +using EonaCat.Quic.Helpers; -namespace EonaCat.Quic.Infrastructure.Frames +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 { - // This file is part of the EonaCat 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 MaxStreamsFrame() { - public override byte Type => 0x12; - public IntegerVar MaximumStreams { get; set; } + } - public MaxStreamsFrame() + public MaxStreamsFrame(ulong maximumStreamId, StreamType appliesTo) + { + MaximumStreams = new IntegerVar(maximumStreamId); + } + + public override byte Type => 0x12; + public IntegerVar MaximumStreams { get; set; } + + public override void Decode(ByteArray array) + { + var type = array.ReadByte(); + MaximumStreams = array.ReadIntegerVar(); + } + + public override byte[] Encode() + { + var result = new List { - } + Type + }; + result.AddRange(MaximumStreams.ToByteArray()); - public MaxStreamsFrame(ulong maximumStreamId, StreamType appliesTo) - { - MaximumStreams = new IntegerVar(maximumStreamId); - } - - public override void Decode(ByteArray array) - { - byte type = array.ReadByte(); - MaximumStreams = array.ReadIntegerVar(); - } - - public override byte[] Encode() - { - List result = new List - { - Type - }; - result.AddRange(MaximumStreams.ToByteArray()); - - return result.ToArray(); - } + return result.ToArray(); } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/NewConnectionIdFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/NewConnectionIdFrame.cs index f5dae09..a49d83f 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/NewConnectionIdFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/NewConnectionIdFrame.cs @@ -1,23 +1,21 @@ -using EonaCat.Quic.Helpers; -using System; +using System; +using EonaCat.Quic.Helpers; -namespace EonaCat.Quic.Infrastructure.Frames +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 { - // This file is part of the EonaCat 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 override byte Type => 0x18; - public class NewConnectionIdFrame : Frame + public override void Decode(ByteArray array) { - public override byte Type => 0x18; + throw new NotImplementedException(); + } - public override void Decode(ByteArray array) - { - throw new NotImplementedException(); - } - - public override byte[] Encode() - { - throw new NotImplementedException(); - } + public override byte[] Encode() + { + throw new NotImplementedException(); } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/NewTokenFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/NewTokenFrame.cs index 6637993..5cbe8e5 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/NewTokenFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/NewTokenFrame.cs @@ -1,23 +1,21 @@ -using EonaCat.Quic.Helpers; -using System; +using System; +using EonaCat.Quic.Helpers; -namespace EonaCat.Quic.Infrastructure.Frames +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 { - // This file is part of the EonaCat 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 override byte Type => 0x07; - public class NewTokenFrame : Frame + public override void Decode(ByteArray array) { - public override byte Type => 0x07; + throw new NotImplementedException(); + } - public override void Decode(ByteArray array) - { - throw new NotImplementedException(); - } - - public override byte[] Encode() - { - throw new NotImplementedException(); - } + public override byte[] Encode() + { + throw new NotImplementedException(); } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/PaddingFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/PaddingFrame.cs index deb69d4..8a30120 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/PaddingFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/PaddingFrame.cs @@ -1,28 +1,26 @@ -using EonaCat.Quic.Helpers; -using System.Collections.Generic; +using System.Collections.Generic; +using EonaCat.Quic.Helpers; -namespace EonaCat.Quic.Infrastructure.Frames +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 { - // This file is part of the EonaCat 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 override byte Type => 0x00; - public class PaddingFrame : Frame + public override void Decode(ByteArray array) { - public override byte Type => 0x00; + var type = array.ReadByte(); + } - public override void Decode(ByteArray array) + public override byte[] Encode() + { + var data = new List { - byte type = array.ReadByte(); - } + Type + }; - public override byte[] Encode() - { - List data = new List - { - Type - }; - - return data.ToArray(); - } + return data.ToArray(); } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/PathChallengeFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/PathChallengeFrame.cs index 1326b47..90bd4c9 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/PathChallengeFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/PathChallengeFrame.cs @@ -1,23 +1,21 @@ -using EonaCat.Quic.Helpers; -using System; +using System; +using EonaCat.Quic.Helpers; -namespace EonaCat.Quic.Infrastructure.Frames +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 { - // This file is part of the EonaCat 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 override byte Type => 0x1a; - public class PathChallengeFrame : Frame + public override void Decode(ByteArray array) { - public override byte Type => 0x1a; + throw new NotImplementedException(); + } - public override void Decode(ByteArray array) - { - throw new NotImplementedException(); - } - - public override byte[] Encode() - { - throw new NotImplementedException(); - } + public override byte[] Encode() + { + throw new NotImplementedException(); } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/PathResponseFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/PathResponseFrame.cs index ece907d..6c11bf1 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/PathResponseFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/PathResponseFrame.cs @@ -1,23 +1,21 @@ -using EonaCat.Quic.Helpers; -using System; +using System; +using EonaCat.Quic.Helpers; -namespace EonaCat.Quic.Infrastructure.Frames +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 { - // This file is part of the EonaCat 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 override byte Type => 0x1b; - public class PathResponseFrame : Frame + public override void Decode(ByteArray array) { - public override byte Type => 0x1b; + throw new NotImplementedException(); + } - public override void Decode(ByteArray array) - { - throw new NotImplementedException(); - } - - public override byte[] Encode() - { - throw new NotImplementedException(); - } + public override byte[] Encode() + { + throw new NotImplementedException(); } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/PingFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/PingFrame.cs index 0800600..eb3c413 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/PingFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/PingFrame.cs @@ -1,28 +1,26 @@ -using EonaCat.Quic.Helpers; -using System.Collections.Generic; +using System.Collections.Generic; +using EonaCat.Quic.Helpers; -namespace EonaCat.Quic.Infrastructure.Frames +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 { - // This file is part of the EonaCat 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 override byte Type => 0x01; - public class PingFrame : Frame + public override void Decode(ByteArray array) { - public override byte Type => 0x01; + var type = array.ReadByte(); + } - public override void Decode(ByteArray array) + public override byte[] Encode() + { + var data = new List { - byte type = array.ReadByte(); - } + Type + }; - public override byte[] Encode() - { - List data = new List - { - Type - }; - - return data.ToArray(); - } + return data.ToArray(); } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/ResetStreamFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/ResetStreamFrame.cs index 6bee474..452bacf 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/ResetStreamFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/ResetStreamFrame.cs @@ -1,37 +1,35 @@ -using EonaCat.Quic.Helpers; -using System.Collections.Generic; +using System.Collections.Generic; +using EonaCat.Quic.Helpers; -namespace EonaCat.Quic.Infrastructure.Frames +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 { - // This file is part of the EonaCat 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 override byte Type => 0x04; + public IntegerVar StreamId { get; set; } + public IntegerVar ApplicationProtocolErrorCode { get; set; } + public IntegerVar FinalSize { get; set; } - public class ResetStreamFrame : Frame + public override void Decode(ByteArray array) { - public override byte Type => 0x04; - public IntegerVar StreamId { get; set; } - public IntegerVar ApplicationProtocolErrorCode { get; set; } - public IntegerVar FinalSize { get; set; } + var type = array.ReadByte(); + StreamId = array.ReadIntegerVar(); + ApplicationProtocolErrorCode = array.ReadIntegerVar(); + FinalSize = array.ReadIntegerVar(); + } - public override void Decode(ByteArray array) + public override byte[] Encode() + { + var result = new List { - byte type = array.ReadByte(); - StreamId = array.ReadIntegerVar(); - ApplicationProtocolErrorCode = array.ReadIntegerVar(); - FinalSize = array.ReadIntegerVar(); - } + Type + }; + result.AddRange(StreamId.ToByteArray()); + result.AddRange(ApplicationProtocolErrorCode.ToByteArray()); + result.AddRange(FinalSize.ToByteArray()); - public override byte[] Encode() - { - List result = new List - { - Type - }; - result.AddRange(StreamId.ToByteArray()); - result.AddRange(ApplicationProtocolErrorCode.ToByteArray()); - result.AddRange(FinalSize.ToByteArray()); - - return result.ToArray(); - } + return result.ToArray(); } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/RetireConnectionIdFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/RetireConnectionIdFrame.cs index 33be918..90d4546 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/RetireConnectionIdFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/RetireConnectionIdFrame.cs @@ -1,23 +1,21 @@ -using EonaCat.Quic.Helpers; -using System; +using System; +using EonaCat.Quic.Helpers; -namespace EonaCat.Quic.Infrastructure.Frames +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 { - // This file is part of the EonaCat 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 override byte Type => 0x19; - public class RetireConnectionIdFrame : Frame + public override void Decode(ByteArray array) { - public override byte Type => 0x19; + throw new NotImplementedException(); + } - public override void Decode(ByteArray array) - { - throw new NotImplementedException(); - } - - public override byte[] Encode() - { - throw new NotImplementedException(); - } + public override byte[] Encode() + { + throw new NotImplementedException(); } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/StopSendingFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/StopSendingFrame.cs index 9f2145a..7e8121f 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/StopSendingFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/StopSendingFrame.cs @@ -1,23 +1,21 @@ -using EonaCat.Quic.Helpers; -using System; +using System; +using EonaCat.Quic.Helpers; -namespace EonaCat.Quic.Infrastructure.Frames +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 { - // This file is part of the EonaCat 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 override byte Type => 0x05; - public class StopSendingFrame : Frame + public override void Decode(ByteArray array) { - public override byte Type => 0x05; + throw new NotImplementedException(); + } - public override void Decode(ByteArray array) - { - throw new NotImplementedException(); - } - - public override byte[] Encode() - { - throw new NotImplementedException(); - } + public override byte[] Encode() + { + throw new NotImplementedException(); } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamDataBlockedFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamDataBlockedFrame.cs index 9ee1e97..cf9a94c 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamDataBlockedFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamDataBlockedFrame.cs @@ -1,44 +1,42 @@ -using EonaCat.Quic.Helpers; -using System.Collections.Generic; +using System.Collections.Generic; +using EonaCat.Quic.Helpers; -namespace EonaCat.Quic.Infrastructure.Frames +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 { - // This file is part of the EonaCat 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 StreamDataBlockedFrame() { - public override byte Type => 0x15; - public IntegerVar StreamId { get; set; } - public IntegerVar MaximumStreamData { get; set; } + } - public StreamDataBlockedFrame() + public StreamDataBlockedFrame(ulong streamId, ulong streamDataLimit) + { + StreamId = streamId; + MaximumStreamData = streamDataLimit; + } + + public override byte Type => 0x15; + public IntegerVar StreamId { get; set; } + public IntegerVar MaximumStreamData { get; set; } + + public override void Decode(ByteArray array) + { + var type = array.ReadByte(); + StreamId = array.ReadIntegerVar(); + MaximumStreamData = array.ReadIntegerVar(); + } + + public override byte[] Encode() + { + var result = new List { - } + Type + }; + result.AddRange(StreamId.ToByteArray()); + result.AddRange(MaximumStreamData.ToByteArray()); - public StreamDataBlockedFrame(ulong streamId, ulong streamDataLimit) - { - StreamId = streamId; - MaximumStreamData = streamDataLimit; - } - - public override void Decode(ByteArray array) - { - byte type = array.ReadByte(); - StreamId = array.ReadIntegerVar(); - MaximumStreamData = array.ReadIntegerVar(); - } - - public override byte[] Encode() - { - List result = new List - { - Type - }; - result.AddRange(StreamId.ToByteArray()); - result.AddRange(MaximumStreamData.ToByteArray()); - - return result.ToArray(); - } + return result.ToArray(); } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamFrame.cs index 464f2ef..b3baebb 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamFrame.cs @@ -1,103 +1,101 @@ -using EonaCat.Quic.Helpers; -using System.Collections.Generic; +using System.Collections.Generic; +using EonaCat.Quic.Helpers; -namespace EonaCat.Quic.Infrastructure.Frames +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 { - // This file is part of the EonaCat 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 byte ActualType = 0x08; - public class StreamFrame : Frame + public StreamFrame() { - 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(ulong streamId, byte[] data, ulong offset, bool eos) + { + StreamId = streamId; + StreamData = data; + Offset = offset; + Length = (ulong)data.Length; + EndOfStream = eos; + } - public StreamFrame() + 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 override void Decode(ByteArray array) + { + var type = array.ReadByte(); + + var OFF_BIT = (byte)(type & 0x04); + var LEN_BIT = (byte)(type & 0x02); + var FIN_BIT = (byte)(type & 0x01); + + StreamId = array.ReadIntegerVar(); + if (OFF_BIT > 0) { + Offset = array.ReadIntegerVar(); } - public StreamFrame(ulong streamId, byte[] data, ulong offset, bool eos) + if (LEN_BIT > 0) { - StreamId = streamId; - StreamData = data; - Offset = offset; - Length = (ulong)data.Length; - EndOfStream = eos; + Length = array.ReadIntegerVar(); } - public override void Decode(ByteArray array) + if (FIN_BIT > 0) { - 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); + EndOfStream = true; } - public override byte[] Encode() + StreamData = array.ReadBytes((int)Length.Value); + } + + public override byte[] Encode() + { + if (Offset != null && Offset.Value > 0) { - if (Offset != null && Offset.Value > 0) - { - ActualType = (byte)(ActualType | 0x04); - } - - if (Length != null && Length.Value > 0) - { - ActualType = (byte)(ActualType | 0x02); - } - - if (EndOfStream == true) - { - ActualType = (byte)(ActualType | 0x01); - } - - byte OFF_BIT = (byte)(ActualType & 0x04); - byte LEN_BIT = (byte)(ActualType & 0x02); - byte FIN_BIT = (byte)(ActualType & 0x01); - - List result = new List - { - 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(); + ActualType = (byte)(ActualType | 0x04); } + + if (Length != null && Length.Value > 0) + { + ActualType = (byte)(ActualType | 0x02); + } + + if (EndOfStream) + { + ActualType = (byte)(ActualType | 0x01); + } + + var OFF_BIT = (byte)(ActualType & 0x04); + var LEN_BIT = (byte)(ActualType & 0x02); + var FIN_BIT = (byte)(ActualType & 0x01); + + var result = new List + { + ActualType + }; + byte[] streamId = StreamId; + result.AddRange(streamId); + + if (OFF_BIT > 0) + { + result.AddRange(Offset.ToByteArray()); + } + + if (LEN_BIT > 0) + { + result.AddRange(Length.ToByteArray()); + } + + result.AddRange(StreamData); + + return result.ToArray(); } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamsBlockedFrame.cs b/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamsBlockedFrame.cs index 767aaff..ed2d82c 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamsBlockedFrame.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Frames/StreamsBlockedFrame.cs @@ -1,23 +1,21 @@ -using EonaCat.Quic.Helpers; -using System; +using System; +using EonaCat.Quic.Helpers; -namespace EonaCat.Quic.Infrastructure.Frames +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 { - // This file is part of the EonaCat 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 override byte Type => 0x16; - public class StreamsBlockedFrame : Frame + public override void Decode(ByteArray array) { - public override byte Type => 0x16; + throw new NotImplementedException(); + } - public override void Decode(ByteArray array) - { - throw new NotImplementedException(); - } - - public override byte[] Encode() - { - throw new NotImplementedException(); - } + public override byte[] Encode() + { + throw new NotImplementedException(); } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/NumberSpace.cs b/EonaCat.Network/System/Quic/Infrastructure/NumberSpace.cs index 00f0a9d..623cf3d 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/NumberSpace.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/NumberSpace.cs @@ -1,36 +1,34 @@ -namespace EonaCat.Quic.Infrastructure +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 { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private readonly uint _max = uint.MaxValue; + private uint _n; - public class NumberSpace + public NumberSpace() { - private readonly uint _max = uint.MaxValue; - private uint _n = 0; + } - public NumberSpace() + public NumberSpace(uint max) + { + _max = max; + } + + public bool IsMax() + { + return _n == _max; + } + + public uint Get() + { + if (_n >= _max) { + return 0; } - public NumberSpace(uint max) - { - _max = max; - } - - public bool IsMax() - { - return _n == _max; - } - - public uint Get() - { - if (_n >= _max) - { - return 0; - } - - _n++; - return _n; - } + _n++; + return _n; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/PacketProcessing/InitialPacketCreator.cs b/EonaCat.Network/System/Quic/Infrastructure/PacketProcessing/InitialPacketCreator.cs index d5858b7..aa4b571 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/PacketProcessing/InitialPacketCreator.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/PacketProcessing/InitialPacketCreator.cs @@ -3,35 +3,30 @@ using EonaCat.Quic.Infrastructure.Frames; using EonaCat.Quic.Infrastructure.Packets; using EonaCat.Quic.Infrastructure.Settings; -namespace EonaCat.Quic.Infrastructure.PacketProcessing +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 { - // This file is part of the EonaCat 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) { - 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; + var 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; + var length = packet.Encode().Length; + var padding = QuicSettings.PMTU - length; - for (int i = 0; i < padding; i++) - { - packet.AttachFrame(new PaddingFrame()); - } + for (var i = 0; i < padding; i++) packet.AttachFrame(new PaddingFrame()); - return packet; - } + return packet; + } - public VersionNegotiationPacket CreateVersionNegotiationPacket() - { - return new VersionNegotiationPacket(); - } + public VersionNegotiationPacket CreateVersionNegotiationPacket() + { + return new VersionNegotiationPacket(); } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/PacketProcessing/PacketCreator.cs b/EonaCat.Network/System/Quic/Infrastructure/PacketProcessing/PacketCreator.cs index 5b073f8..e051cf0 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/PacketProcessing/PacketCreator.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/PacketProcessing/PacketCreator.cs @@ -2,53 +2,51 @@ using EonaCat.Quic.Infrastructure.Frames; using EonaCat.Quic.Infrastructure.Packets; -namespace EonaCat.Quic.Infrastructure.PacketProcessing +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 { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private readonly IntegerParts _connectionId; + private readonly NumberSpace _ns; + private readonly IntegerParts _peerConnectionId; - public class PacketCreator + public PacketCreator(IntegerParts connectionId, IntegerParts peerConnectionId) { - private readonly NumberSpace _ns; - private readonly IntegerParts _connectionId; - private readonly IntegerParts _peerConnectionId; + _ns = new NumberSpace(); - public PacketCreator(IntegerParts connectionId, IntegerParts peerConnectionId) - { - _ns = new NumberSpace(); + _connectionId = connectionId; + _peerConnectionId = peerConnectionId; + } - _connectionId = connectionId; - _peerConnectionId = peerConnectionId; - } + public ShortHeaderPacket CreateConnectionClosePacket(ErrorCode code, byte frameType, string reason) + { + var packet = new ShortHeaderPacket(_peerConnectionId.Size); + packet.PacketNumber = _ns.Get(); + packet.DestinationConnectionId = (byte)_peerConnectionId; + packet.AttachFrame(new ConnectionCloseFrame(code, frameType, reason)); - 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; + } - return packet; - } + public ShortHeaderPacket CreateDataPacket(ulong streamId, byte[] data, ulong offset, bool eos) + { + var packet = new ShortHeaderPacket(_peerConnectionId.Size); + packet.PacketNumber = _ns.Get(); + packet.DestinationConnectionId = (byte)_peerConnectionId; + packet.AttachFrame(new StreamFrame(streamId, data, offset, eos)); - public ShortHeaderPacket CreateDataPacket(ulong streamId, byte[] data, ulong 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; + } - return packet; - } + public InitialPacket CreateServerBusyPacket() + { + return new InitialPacket(0, 0); + } - public InitialPacket CreateServerBusyPacket() - { - return new InitialPacket(0, 0); - } - - public ShortHeaderPacket CreateShortHeaderPacket() - { - return new ShortHeaderPacket(0); - } + public ShortHeaderPacket CreateShortHeaderPacket() + { + return new ShortHeaderPacket(0); } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/PacketType.cs b/EonaCat.Network/System/Quic/Infrastructure/PacketType.cs index 941ac59..1dac2c9 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/PacketType.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/PacketType.cs @@ -1,13 +1,11 @@ -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. +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 : ushort - { - Initial = 0x0, - ZeroRTTProtected = 0x1, - Handshake = 0x2, - RetryPacket = 0x3 - } +public enum PacketType : ushort +{ + Initial = 0x0, + ZeroRTTProtected = 0x1, + Handshake = 0x2, + RetryPacket = 0x3 } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Packets/InitialPacket.cs b/EonaCat.Network/System/Quic/Infrastructure/Packets/InitialPacket.cs index af04a67..bb76cb9 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Packets/InitialPacket.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Packets/InitialPacket.cs @@ -1,103 +1,101 @@ -using EonaCat.Quic.Helpers; -using System.Collections.Generic; +using System.Collections.Generic; +using EonaCat.Quic.Helpers; -namespace EonaCat.Quic.Infrastructure.Packets +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 { - // This file is part of the EonaCat 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 InitialPacket() { - 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(IntegerParts destinationConnectionId, IntegerParts sourceConnectionId) + { + DestinationConnectionIdLength = destinationConnectionId.Size; + DestinationConnectionId = destinationConnectionId; - public InitialPacket() + SourceConnectionIdLength = sourceConnectionId.Size; + SourceConnectionId = sourceConnectionId; + } + + 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 override void Decode(byte[] packet) + { + var array = new ByteArray(packet); + var type = array.ReadByte(); + // Size of the packet PacketNumber is determined by the last 2 bits of the Type. + var pnSize = (type & 0x03) + 1; + + Version = array.ReadUInt32(); + + DestinationConnectionIdLength = array.ReadByte(); + if (DestinationConnectionIdLength > 0) { + DestinationConnectionId = array.ReadGranularInteger(DestinationConnectionIdLength); } - public InitialPacket(IntegerParts destinationConnectionId, IntegerParts sourceConnectionId) + SourceConnectionIdLength = array.ReadByte(); + if (SourceConnectionIdLength > 0) { - DestinationConnectionIdLength = destinationConnectionId.Size; - DestinationConnectionId = destinationConnectionId; - - SourceConnectionIdLength = sourceConnectionId.Size; - SourceConnectionId = sourceConnectionId; + SourceConnectionId = array.ReadGranularInteger(SourceConnectionIdLength); } - public override void Decode(byte[] packet) + TokenLength = array.ReadIntegerVar(); + if (TokenLength > 0) { - 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); + Token = array.ReadBytes(TokenLength); } - public override byte[] Encode() + Length = array.ReadIntegerVar(); + PacketNumber = array.ReadGranularInteger(pnSize); + + Length = Length - PacketNumber.Size; + + DecodeFrames(array); + } + + public override byte[] Encode() + { + var frames = EncodeFrames(); + + var result = new List { - byte[] frames = EncodeFrames(); + (byte)(Type | (PacketNumber.Size - 1)) + }; + result.AddRange(ByteHelpers.GetBytes(Version)); - List result = new List - { - (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 + (ulong)frames.Length); - - result.AddRange(tokenLength); - result.AddRange(length); - result.AddRange(PacketNumber.ToByteArray()); - result.AddRange(frames); - - return result.ToArray(); + 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 + (ulong)frames.Length); + + result.AddRange(tokenLength); + result.AddRange(length); + result.AddRange(PacketNumber.ToByteArray()); + result.AddRange(frames); + + return result.ToArray(); } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Packets/LongHeaderPacket.cs b/EonaCat.Network/System/Quic/Infrastructure/Packets/LongHeaderPacket.cs index 90fb4ed..f3c276e 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Packets/LongHeaderPacket.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Packets/LongHeaderPacket.cs @@ -1,99 +1,98 @@ -using EonaCat.Quic.Helpers; -using System.Collections.Generic; +using System.Collections.Generic; +using EonaCat.Quic.Helpers; -namespace EonaCat.Quic.Infrastructure.Packets +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 { - // This file is part of the EonaCat 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 LongHeaderPacket() { - 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 LongHeaderPacket(PacketType packetType, IntegerParts destinationConnectionId, + IntegerParts sourceConnectionId) + { + PacketType = packetType; + DestinationConnectionIdLength = destinationConnectionId.Size; + DestinationConnectionId = destinationConnectionId; - public PacketType PacketType { get; set; } + SourceConnectionIdLength = sourceConnectionId.Size; + SourceConnectionId = sourceConnectionId; + } - public LongHeaderPacket() + 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 override void Decode(byte[] packet) + { + var array = new ByteArray(packet); + + var type = array.ReadByte(); + PacketType = DecodeTypeFiled(type); + + Version = array.ReadUInt32(); + + DestinationConnectionIdLength = array.ReadByte(); + if (DestinationConnectionIdLength > 0) { + DestinationConnectionId = array.ReadGranularInteger(DestinationConnectionIdLength); } - public LongHeaderPacket(PacketType packetType, IntegerParts destinationConnectionId, IntegerParts sourceConnectionId) + SourceConnectionIdLength = array.ReadByte(); + if (SourceConnectionIdLength > 0) { - PacketType = packetType; - DestinationConnectionIdLength = destinationConnectionId.Size; - DestinationConnectionId = destinationConnectionId; - - SourceConnectionIdLength = sourceConnectionId.Size; - SourceConnectionId = sourceConnectionId; + SourceConnectionId = array.ReadGranularInteger(SourceConnectionIdLength); } - public override void Decode(byte[] packet) + DecodeFrames(array); + } + + public override byte[] Encode() + { + var frames = EncodeFrames(); + + var result = new List { - ByteArray array = new ByteArray(packet); + EncodeTypeField() + }; + result.AddRange(ByteHelpers.GetBytes(Version)); - 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); + result.Add(DestinationConnectionId.Size); + if (DestinationConnectionId.Size > 0) + { + result.AddRange(DestinationConnectionId.ToByteArray()); } - public override byte[] Encode() + result.Add(SourceConnectionId.Size); + if (SourceConnectionId.Size > 0) { - byte[] frames = EncodeFrames(); - - List result = new List - { - 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(); + result.AddRange(SourceConnectionId.ToByteArray()); } - private byte EncodeTypeField() - { - byte type = (byte)(Type | ((byte)PacketType << 4) & 0b0011_0000); + result.AddRange(frames); - return type; - } + return result.ToArray(); + } - private PacketType DecodeTypeFiled(byte type) - { - PacketType result = (PacketType)((type & 0b0011_0000) >> 4); + private byte EncodeTypeField() + { + var type = (byte)(Type | (((byte)PacketType << 4) & 0b0011_0000)); - return result; - } + return type; + } + + private PacketType DecodeTypeFiled(byte type) + { + var result = (PacketType)((type & 0b0011_0000) >> 4); + + return result; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Packets/Packet.cs b/EonaCat.Network/System/Quic/Infrastructure/Packets/Packet.cs index 6387c2d..e466561 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Packets/Packet.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Packets/Packet.cs @@ -1,69 +1,67 @@ -using EonaCat.Quic.Helpers; +using System.Collections.Generic; +using EonaCat.Quic.Helpers; using EonaCat.Quic.Infrastructure.Exceptions; using EonaCat.Quic.Infrastructure.Frames; using EonaCat.Quic.Infrastructure.Settings; -using System.Collections.Generic; -namespace EonaCat.Quic.Infrastructure.Packets +namespace EonaCat.Quic.Infrastructure.Packets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Base data transfer unit of QUIC Transport. +/// +public abstract class Packet { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + protected List _frames = new(); + public abstract byte Type { get; } - /// - /// Base data transfer unit of QUIC Transport. - /// - public abstract class Packet + public uint Version { get; set; } + + public abstract byte[] Encode(); + + public abstract void Decode(byte[] packet); + + public virtual void AttachFrame(Frame frame) { - protected List _frames = new List(); - public abstract byte Type { get; } + _frames.Add(frame); + } - public uint Version { get; set; } + public virtual List GetFrames() + { + return _frames; + } - public abstract byte[] Encode(); - - public abstract void Decode(byte[] packet); - - public virtual void AttachFrame(Frame frame) + public virtual void DecodeFrames(ByteArray array) + { + var factory = new FrameParser(array); + Frame result; + var frames = 0; + while (array.HasData() && frames <= QuicSettings.PMTU) { - _frames.Add(frame); - } - - public virtual List GetFrames() - { - return _frames; - } - - public virtual void DecodeFrames(ByteArray array) - { - FrameParser factory = new FrameParser(array); - Frame result; - int frames = 0; - while (array.HasData() && frames <= QuicSettings.PMTU) + result = factory.GetFrame(); + if (result != null) { - result = factory.GetFrame(); - if (result != null) - { - _frames.Add(result); - } - - frames++; + _frames.Add(result); } - if (array.HasData()) - { - throw new ProtocolException("Unexpected number of frames or possibly corrupted frame was sent."); - } + frames++; } - public virtual byte[] EncodeFrames() + if (array.HasData()) { - List result = new List(); - foreach (Frame frame in _frames) - { - result.AddRange(frame.Encode()); - } - - return result.ToArray(); + throw new ProtocolException("Unexpected number of frames or possibly corrupted frame was sent."); } } + + public virtual byte[] EncodeFrames() + { + var result = new List(); + foreach (var frame in _frames) + { + result.AddRange(frame.Encode()); + } + + return result.ToArray(); + } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Packets/ShortHeaderPacket.cs b/EonaCat.Network/System/Quic/Infrastructure/Packets/ShortHeaderPacket.cs index 161c409..c7bab1f 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Packets/ShortHeaderPacket.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Packets/ShortHeaderPacket.cs @@ -1,54 +1,53 @@ -using EonaCat.Quic.Helpers; -using System.Collections.Generic; +using System.Collections.Generic; +using EonaCat.Quic.Helpers; -namespace EonaCat.Quic.Infrastructure.Packets +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 { - // This file is part of the EonaCat 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 byte ActualType = 0b0100_0000; - public class ShortHeaderPacket : Packet + public ShortHeaderPacket(byte destinationConnectionIdLength) { - public byte ActualType = 0b0100_0000; - public override byte Type => 0b0100_0000; + DestinationConnectionIdLength = destinationConnectionIdLength; + } - public IntegerParts DestinationConnectionId { get; set; } - public IntegerParts PacketNumber { get; set; } + public override byte Type => 0b0100_0000; - // Field not transferred! Only the connection knows about the length of the ConnectionId - public byte DestinationConnectionIdLength { get; set; } + public IntegerParts DestinationConnectionId { get; set; } + public IntegerParts PacketNumber { get; set; } - public ShortHeaderPacket(byte destinationConnectionIdLength) + // Field not transferred! Only the connection knows about the length of the ConnectionId + public byte DestinationConnectionIdLength { get; set; } + + public override void Decode(byte[] packet) + { + var array = new ByteArray(packet); + var type = array.ReadByte(); + DestinationConnectionId = array.ReadGranularInteger(DestinationConnectionIdLength); + + var pnSize = (type & 0x03) + 1; + PacketNumber = array.ReadBytes(pnSize); + + DecodeFrames(array); + } + + public override byte[] Encode() + { + var frames = EncodeFrames(); + + var result = new List { - DestinationConnectionIdLength = destinationConnectionIdLength; - } + (byte)(Type | (PacketNumber.Size - 1)) + }; + result.AddRange(DestinationConnectionId.ToByteArray()); - public override void Decode(byte[] packet) - { - ByteArray array = new ByteArray(packet); - byte type = array.ReadByte(); - DestinationConnectionId = array.ReadGranularInteger(DestinationConnectionIdLength); + byte[] pnBytes = PacketNumber; + result.AddRange(pnBytes); + result.AddRange(frames); - int pnSize = (type & 0x03) + 1; - PacketNumber = array.ReadBytes(pnSize); - - DecodeFrames(array); - } - - public override byte[] Encode() - { - byte[] frames = EncodeFrames(); - - List result = new List - { - (byte)(Type | (PacketNumber.Size - 1)) - }; - result.AddRange(DestinationConnectionId.ToByteArray()); - - byte[] pnBytes = PacketNumber; - result.AddRange(pnBytes); - result.AddRange(frames); - - return result.ToArray(); - } + return result.ToArray(); } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Packets/Unpacker.cs b/EonaCat.Network/System/Quic/Infrastructure/Packets/Unpacker.cs index a4bff1c..9b88068 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Packets/Unpacker.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Packets/Unpacker.cs @@ -1,67 +1,65 @@ -namespace EonaCat.Quic.Infrastructure.Packets +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 { - // This file is part of the EonaCat 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) { - public Packet Unpack(byte[] data) + Packet result = null; + + var type = GetPacketType(data); + switch (type) { - Packet result = null; + case QuicPacketType.Initial: + result = new InitialPacket(); + break; - 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; + // Should be passed by the QuicConnection to the PacketWireTransfer -> Unpacker + case QuicPacketType.ShortHeader: + result = new ShortHeaderPacket(1); + break; } - public QuicPacketType GetPacketType(byte[] data) + if (result == null) { - if (data == null || data.Length <= 0) - { - return QuicPacketType.Broken; - } + return null; + } - byte type = data[0]; + result.Decode(data); - 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 result; + } + public QuicPacketType GetPacketType(byte[] data) + { + if (data == null || data.Length <= 0) + { return QuicPacketType.Broken; } + + var type = data[0]; + + if ((type & 0xC0) == 0xC0) + { + return QuicPacketType.Initial; + } + + if ((type & 0x40) == 0x40) + { + return QuicPacketType.ShortHeader; + } + + if ((type & 0x80) == 0x80) + { + return QuicPacketType.VersionNegotiation; + } + + if ((type & 0xE0) == 0xE0) + { + return QuicPacketType.LongHeader; + } + + return QuicPacketType.Broken; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Packets/VersionNegotiationPacket.cs b/EonaCat.Network/System/Quic/Infrastructure/Packets/VersionNegotiationPacket.cs index b5d9f71..67e22e4 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Packets/VersionNegotiationPacket.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Packets/VersionNegotiationPacket.cs @@ -1,22 +1,20 @@ using System; -namespace EonaCat.Quic.Infrastructure.Packets +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 { - // This file is part of the EonaCat 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 override byte Type => throw new NotImplementedException(); - public class VersionNegotiationPacket : Packet + public override void Decode(byte[] packet) { - public override byte Type => throw new NotImplementedException(); + throw new NotImplementedException(); + } - public override void Decode(byte[] packet) - { - throw new NotImplementedException(); - } - - public override byte[] Encode() - { - throw new NotImplementedException(); - } + public override byte[] Encode() + { + throw new NotImplementedException(); } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/QuicPacketType.cs b/EonaCat.Network/System/Quic/Infrastructure/QuicPacketType.cs index 5e05c5d..2427f7a 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/QuicPacketType.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/QuicPacketType.cs @@ -1,14 +1,12 @@ -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. +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 - } +public enum QuicPacketType +{ + Initial, + LongHeader, + ShortHeader, + VersionNegotiation, + Broken } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Settings/QuicSettings.cs b/EonaCat.Network/System/Quic/Infrastructure/Settings/QuicSettings.cs index 1f86979..70bfafd 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Settings/QuicSettings.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Settings/QuicSettings.cs @@ -1,55 +1,55 @@ -namespace EonaCat.Quic.Infrastructure.Settings +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 { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + /// + /// Path Maximum Transmission Unit. Indicates the mandatory initial packet capacity, and the maximum UDP packet + /// capacity. + /// + public const int PMTU = 1200; - public class QuicSettings - { - /// - /// Path Maximum Transmission Unit. Indicates the mandatory initial packet capacity, and the maximum UDP packet capacity. - /// - public const int PMTU = 1200; + /// + /// Does the server want the first connected client to decide it's initial connection id? + /// + public const bool CanAcceptInitialClientConnectionId = false; - /// - /// Does the server want the first connected client to decide it's initial connection id? - /// - public const bool CanAcceptInitialClientConnectionId = false; + /// + /// TBD. quic-transport 5.1. + /// + public const int MaximumConnectionIds = 8; - /// - /// TBD. quic-transport 5.1. - /// - public const int MaximumConnectionIds = 8; + /// + /// Maximum number of streams that connection can handle. + /// + public const int MaximumStreamId = 128; - /// - /// Maximum number of streams that connection can handle. - /// - public const int MaximumStreamId = 128; + /// + /// Maximum packets that can be transferred before any data transfer (loss of packets, packet resent, infinite ack + /// loop) + /// + public const int MaximumInitialPacketNumber = 100; - /// - /// Maximum packets that can be transferred before any data transfer (loss of packets, packet resent, infinite ack loop) - /// - public const int MaximumInitialPacketNumber = 100; + /// + /// Should the server buffer packets that came before the initial packet? + /// + public const bool ShouldBufferPacketsBeforeConnection = false; - /// - /// Should the server buffer packets that came before the initial packet? - /// - public const bool ShouldBufferPacketsBeforeConnection = false; + /// + /// Limit the maximum number of frames a packet can carry. + /// + public const int MaximumFramesPerPacket = 10; - /// - /// Limit the maximum number of frames a packet can carry. - /// - public const int MaximumFramesPerPacket = 10; + /// + /// Maximum data that can be transferred for a Connection. + /// Currently 10MB. + /// + public const int MaxData = 10 * 1000 * 1000; - /// - /// Maximum data that can be transferred for a Connection. - /// Currently 10MB. - /// - public const int MaxData = 10 * 1000 * 1000; - - /// - /// Maximum data that can be transferred for a Stream. - /// Currently 0.078125 MB, which is MaxData / MaximumStreamId - /// - public const int MaxStreamData = 78125; - } + /// + /// Maximum data that can be transferred for a Stream. + /// Currently 0.078125 MB, which is MaxData / MaximumStreamId + /// + public const int MaxStreamData = 78125; } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Infrastructure/Settings/QuicVersion.cs b/EonaCat.Network/System/Quic/Infrastructure/Settings/QuicVersion.cs index af7422d..7fc520f 100644 --- a/EonaCat.Network/System/Quic/Infrastructure/Settings/QuicVersion.cs +++ b/EonaCat.Network/System/Quic/Infrastructure/Settings/QuicVersion.cs @@ -1,14 +1,12 @@ using System.Collections.Generic; -namespace EonaCat.Quic.Infrastructure.Settings +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 { - // This file is part of the EonaCat 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 const int CurrentVersion = 16; - public class QuicVersion - { - public const int CurrentVersion = 16; - - public static readonly List SupportedVersions = new List() { 15, 16 }; - } + public static readonly List SupportedVersions = new() { 15, 16 }; } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/InternalInfrastructure/ConnectionData.cs b/EonaCat.Network/System/Quic/InternalInfrastructure/ConnectionData.cs index e9d9210..dc97e46 100644 --- a/EonaCat.Network/System/Quic/InternalInfrastructure/ConnectionData.cs +++ b/EonaCat.Network/System/Quic/InternalInfrastructure/ConnectionData.cs @@ -1,21 +1,19 @@ using EonaCat.Quic.Helpers; -namespace EonaCat.Quic.InternalInfrastructure +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 { - // This file is part of the EonaCat 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 ConnectionData(PacketWireTransfer pwt, IntegerParts connectionId, IntegerParts peerConnnectionId) { - 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; - } + PWT = pwt; + ConnectionId = connectionId; + PeerConnectionId = peerConnnectionId; } + + public PacketWireTransfer PWT { get; set; } + public IntegerParts ConnectionId { get; set; } + public IntegerParts PeerConnectionId { get; set; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/InternalInfrastructure/PacketWireTransfer.cs b/EonaCat.Network/System/Quic/InternalInfrastructure/PacketWireTransfer.cs index 8d7b4c4..42ba6c3 100644 --- a/EonaCat.Network/System/Quic/InternalInfrastructure/PacketWireTransfer.cs +++ b/EonaCat.Network/System/Quic/InternalInfrastructure/PacketWireTransfer.cs @@ -1,54 +1,52 @@ -using EonaCat.Quic.Exceptions; -using EonaCat.Quic.Infrastructure.Packets; -using System.Net; +using System.Net; using System.Net.Sockets; +using EonaCat.Quic.Exceptions; +using EonaCat.Quic.Infrastructure.Packets; -namespace EonaCat.Quic.InternalInfrastructure +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 { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private readonly UdpClient _client; - internal class PacketWireTransfer + private readonly Unpacker _unpacker; + private IPEndPoint _peerEndpoint; + + public PacketWireTransfer(UdpClient client, IPEndPoint peerEndpoint) { - private readonly UdpClient _client; - private IPEndPoint _peerEndpoint; + _client = client; + _peerEndpoint = peerEndpoint; - private readonly Unpacker _unpacker; + _unpacker = new Unpacker(); + } - public PacketWireTransfer(UdpClient client, IPEndPoint peerEndpoint) + public Packet ReadPacket() + { + // Await response for sucessfull connection creation by the server + var peerData = _client.Receive(ref _peerEndpoint); + if (peerData == null) { - _client = client; - _peerEndpoint = peerEndpoint; - - _unpacker = new Unpacker(); + throw new ConnectionException("Server did not respond properly."); } - 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."); - } + var packet = _unpacker.Unpack(peerData); - Packet packet = _unpacker.Unpack(peerData); + return packet; + } - return packet; - } + public bool SendPacket(Packet packet) + { + var data = packet.Encode(); - public bool SendPacket(Packet packet) - { - byte[] data = packet.Encode(); + var sent = _client.Send(data, data.Length, _peerEndpoint); - int sent = _client.Send(data, data.Length, _peerEndpoint); + return sent > 0; + } - return sent > 0; - } - - public IPEndPoint LastTransferEndpoint() - { - return _peerEndpoint; - } + public IPEndPoint LastTransferEndpoint() + { + return _peerEndpoint; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/QuicClient.cs b/EonaCat.Network/System/Quic/QuicClient.cs index 0aac34f..39b0df7 100644 --- a/EonaCat.Network/System/Quic/QuicClient.cs +++ b/EonaCat.Network/System/Quic/QuicClient.cs @@ -1,4 +1,8 @@ -using EonaCat.Quic.Connections; +using System; +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; @@ -6,113 +10,107 @@ using EonaCat.Quic.Infrastructure.PacketProcessing; using EonaCat.Quic.Infrastructure.Packets; using EonaCat.Quic.Infrastructure.Settings; using EonaCat.Quic.InternalInfrastructure; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Sockets; -namespace EonaCat.Quic +namespace EonaCat.Quic; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Quic Client. Used for sending and receiving data from a Quic Server. +/// +public class QuicClient : QuicTransport { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private readonly UdpClient _client; + private readonly InitialPacketCreator _packetCreator; + + private QuicConnection _connection; + + private ulong _maximumStreams = QuicSettings.MaximumStreamId; + private IPEndPoint _peerIp; + private PacketWireTransfer _pwt; + + public QuicClient() + { + _client = new UdpClient(); + _packetCreator = new InitialPacketCreator(); + } /// - /// Quic Client. Used for sending and receiving data from a Quic Server. + /// Connect to a remote server. /// - public class QuicClient : QuicTransport + /// Ip Address + /// Port + /// + public QuicConnection Connect(string ip, int port) { - private IPEndPoint _peerIp; - private readonly UdpClient _client; - - private QuicConnection _connection; - private readonly InitialPacketCreator _packetCreator; - - private ulong _maximumStreams = QuicSettings.MaximumStreamId; - private PacketWireTransfer _pwt; - - public QuicClient() + // Establish socket connection + var ipEntry = Uri.CheckHostName(ip); + IPAddress ipAddress; + if (ipEntry == UriHostNameType.Dns) { - _client = new UdpClient(); - _packetCreator = new InitialPacketCreator(); + ipAddress = Dns.GetHostEntry(ip).AddressList?.FirstOrDefault(); + } + else + { + ipAddress = IPAddress.Parse(ip); } - /// - /// Connect to a remote server. - /// - /// Ip Address - /// Port - /// - public QuicConnection Connect(string ip, int port) + _peerIp = new IPEndPoint(ipAddress, port); + + // Initialize packet reader + _pwt = new PacketWireTransfer(_client, _peerIp); + + // Start initial protocol process + var connectionPacket = _packetCreator.CreateInitialPacket(0, 0); + + // Send the initial packet + _pwt.SendPacket(connectionPacket); + + // Await response for sucessfull connection creation by the server + var packet = (InitialPacket)_pwt.ReadPacket(); + + HandleInitialFrames(packet); + EstablishConnection(packet.SourceConnectionId, packet.SourceConnectionId); + + return _connection; + } + + /// + /// Handles initial packet's frames. (In most cases protocol frames) + /// + /// + private void HandleInitialFrames(Packet packet) + { + var frames = packet.GetFrames(); + for (var i = frames.Count - 1; i > 0; i--) { - // Establish socket connection - var ipEntry = Uri.CheckHostName(ip); - IPAddress ipAddress; - if (ipEntry == UriHostNameType.Dns) + var frame = frames[i]; + if (frame is ConnectionCloseFrame ccf) { - ipAddress = Dns.GetHostEntry(ip).AddressList?.FirstOrDefault(); + throw new ConnectionException(ccf.ReasonPhrase); } - else + + if (frame is MaxStreamsFrame msf) { - ipAddress = IPAddress.Parse(ip); + _maximumStreams = msf.MaximumStreams.Value; } - _peerIp = new IPEndPoint(ipAddress, port); - // Initialize packet reader - _pwt = new PacketWireTransfer(_client, _peerIp); - - // Start initial protocol process - InitialPacket connectionPacket = _packetCreator.CreateInitialPacket(0, 0); - - // Send the initial packet - _pwt.SendPacket(connectionPacket); - - // Await response for sucessfull connection creation by the server - InitialPacket packet = (InitialPacket)_pwt.ReadPacket(); - - HandleInitialFrames(packet); - EstablishConnection(packet.SourceConnectionId, packet.SourceConnectionId); - - return _connection; - } - - /// - /// Handles initial packet's frames. (In most cases protocol frames) - /// - /// - private void HandleInitialFrames(Packet packet) - { - List frames = packet.GetFrames(); - for (int i = frames.Count - 1; i > 0; i--) + // Break out if the first Padding Frame has been reached + if (frame is PaddingFrame) { - 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; - } + break; } } - - /// - /// Create a new connection - /// - /// - /// - private void EstablishConnection(IntegerParts connectionId, IntegerParts peerConnectionId) - { - ConnectionData connection = new ConnectionData(_pwt, connectionId, peerConnectionId); - _connection = new QuicConnection(connection); - } + } + + /// + /// Create a new connection + /// + /// + /// + private void EstablishConnection(IntegerParts connectionId, IntegerParts peerConnectionId) + { + var connection = new ConnectionData(_pwt, connectionId, peerConnectionId); + _connection = new QuicConnection(connection); } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/QuicServer.cs b/EonaCat.Network/System/Quic/QuicServer.cs index 6747a94..2568265 100644 --- a/EonaCat.Network/System/Quic/QuicServer.cs +++ b/EonaCat.Network/System/Quic/QuicServer.cs @@ -1,4 +1,8 @@ -using EonaCat.Quic.Connections; +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; @@ -8,150 +12,146 @@ using EonaCat.Quic.Infrastructure.PacketProcessing; using EonaCat.Quic.Infrastructure.Packets; using EonaCat.Quic.Infrastructure.Settings; using EonaCat.Quic.InternalInfrastructure; -using System; -using System.Linq; -using System.Net; -using System.Net.Sockets; -namespace EonaCat.Quic +namespace EonaCat.Quic; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Quic Server - a Quic server that processes incoming connections and if possible sends back data on it's peers. +/// +public class QuicServer : QuicTransport { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private readonly string _hostname; + private readonly InitialPacketCreator _packetCreator; + + private readonly int _port; + private readonly Unpacker _unpacker; + + private UdpClient _client; + + private PacketWireTransfer _pwt; + private bool _started; /// - /// Quic Server - a Quic server that processes incoming connections and if possible sends back data on it's peers. + /// Create a new instance of QuicListener. /// - public class QuicServer : QuicTransport + /// The port that the server will listen on. + public QuicServer(string hostName, int port) { - private readonly Unpacker _unpacker; - private readonly InitialPacketCreator _packetCreator; + _started = false; + _port = port; + _hostname = hostName; - private PacketWireTransfer _pwt; + _unpacker = new Unpacker(); + _packetCreator = new InitialPacketCreator(); + } - private UdpClient _client; + public event ClientConnectedEvent OnClientConnected; - private readonly int _port; - private readonly string _hostname; - private bool _started; - - public event ClientConnectedEvent OnClientConnected; - - /// - /// Create a new instance of QuicListener. - /// - /// The port that the server will listen on. - public QuicServer(string hostName, int port) + /// + /// Starts the listener. + /// + public void Start() + { + var ipEntry = Uri.CheckHostName(_hostname); + IPAddress ipAddress; + if (ipEntry == UriHostNameType.Dns) { - _started = false; - _port = port; - _hostname = hostName; - - _unpacker = new Unpacker(); - _packetCreator = new InitialPacketCreator(); + ipAddress = Dns.GetHostEntry(_hostname).AddressList?.FirstOrDefault(); + } + else + { + ipAddress = IPAddress.Parse(_hostname); } - /// - /// Starts the listener. - /// - public void Start() + _client = new UdpClient(new IPEndPoint(ipAddress, _port)); + _started = true; + _pwt = new PacketWireTransfer(_client, null); + + while (true) { - var ipEntry = Uri.CheckHostName(_hostname); - IPAddress ipAddress; - if (ipEntry == UriHostNameType.Dns) + var packet = _pwt.ReadPacket(); + if (packet is InitialPacket) { - ipAddress = Dns.GetHostEntry(_hostname).AddressList?.FirstOrDefault(); - } - else - { - ipAddress = IPAddress.Parse(_hostname); + var connection = ProcessInitialPacket(packet, _pwt.LastTransferEndpoint()); + + OnClientConnected?.Invoke(connection); } - _client = new UdpClient(new IPEndPoint(ipAddress, _port)); - _started = true; - _pwt = new PacketWireTransfer(_client, null); - - while (true) + if (packet is ShortHeaderPacket) { - Packet packet = _pwt.ReadPacket(); - if (packet is InitialPacket) - { - QuicConnection connection = ProcessInitialPacket(packet, _pwt.LastTransferEndpoint()); - - OnClientConnected?.Invoke(connection); - } - - if (packet is ShortHeaderPacket) - { - ProcessShortHeaderPacket(packet); - } + ProcessShortHeaderPacket(packet); } } - - /// - /// Stops the listener. - /// - public void Close() - { - if (_started) - { - _client.Close(); - } - } - - /// - /// Processes incomming initial packet and creates or halts a connection. - /// - /// Initial Packet - /// Peer's endpoint - /// - private QuicConnection ProcessInitialPacket(Packet packet, IPEndPoint endPoint) - { - QuicConnection result = null; - 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 ulong 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; - } + } + + /// + /// Stops the listener. + /// + public void Close() + { + if (_started) + { + _client.Close(); + } + } + + /// + /// Processes incomming initial packet and creates or halts a connection. + /// + /// Initial Packet + /// Peer's endpoint + /// + private QuicConnection ProcessInitialPacket(Packet packet, IPEndPoint endPoint) + { + QuicConnection result = null; + 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)) + { + var vnp = _packetCreator.CreateVersionNegotiationPacket(); + data = vnp.Encode(); + + _client.Send(data, data.Length, endPoint); + return null; + } + + var cast = packet as InitialPacket; + var 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 var availableConnectionId)) + { + // 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(); + var dataSent = _client.Send(data, data.Length, endPoint); + if (dataSent > 0) + { + return result; + } + + return null; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/QuicTransport.cs b/EonaCat.Network/System/Quic/QuicTransport.cs index 2bd8d08..bd52371 100644 --- a/EonaCat.Network/System/Quic/QuicTransport.cs +++ b/EonaCat.Network/System/Quic/QuicTransport.cs @@ -1,30 +1,28 @@ using EonaCat.Quic.Connections; using EonaCat.Quic.Infrastructure.Packets; -namespace EonaCat.Quic +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 { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - - public class QuicTransport + /// + /// Processes short header packet, by distributing the frames towards connections. + /// + /// + protected void ProcessShortHeaderPacket(Packet packet) { - /// - /// Processes short header packet, by distributing the frames towards connections. - /// - /// - protected void ProcessShortHeaderPacket(Packet packet) + var shp = (ShortHeaderPacket)packet; + + var connection = ConnectionPool.Find(shp.DestinationConnectionId); + + // No suitable connection found. Discard the packet. + if (connection == null) { - 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()); + return; } + + connection.ProcessFrames(shp.GetFrames()); } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Streams/QuicStream.cs b/EonaCat.Network/System/Quic/Streams/QuicStream.cs index 7dfaa60..b0d08a3 100644 --- a/EonaCat.Network/System/Quic/Streams/QuicStream.cs +++ b/EonaCat.Network/System/Quic/Streams/QuicStream.cs @@ -1,217 +1,213 @@ -using EonaCat.Quic.Connections; +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; using EonaCat.Quic.Infrastructure.Frames; -using EonaCat.Quic.Infrastructure.Packets; using EonaCat.Quic.Infrastructure.Settings; -using System; -using System.Collections.Generic; -using System.Linq; -namespace EonaCat.Quic.Streams +namespace EonaCat.Quic.Streams; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Virtual multiplexing channel. +/// +public class QuicStream { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private readonly QuicConnection _connection; + private readonly SortedList _data = new(); + private ulong _currentTransferRate; + private ulong _maximumStreamData; + private ulong _sendOffset; + + public QuicStream(QuicConnection connection, StreamId streamId) + { + StreamId = streamId; + Type = streamId.Type; + + _maximumStreamData = QuicSettings.MaxStreamData; + _currentTransferRate = 0; + _sendOffset = 0; + + _connection = connection; + } + + public StreamState State { get; set; } + public StreamType Type { get; set; } + public StreamId StreamId { get; } + + public StreamDataReceivedEvent OnStreamDataReceived { get; set; } + + public byte[] Data => _data.SelectMany(v => v.Value).ToArray(); + + public bool Send(byte[] data) + { + if (Type == StreamType.ServerUnidirectional) + { + throw new StreamException("Cannot send data on unidirectional stream."); + } + + _connection.IncrementRate(data.Length); + + var numberOfPackets = data.Length / QuicSettings.PMTU + 1; + var leftoverCarry = data.Length % QuicSettings.PMTU; + + for (var i = 0; i < numberOfPackets; i++) + { + var eos = false; + var dataSize = QuicSettings.PMTU; + if (i == numberOfPackets - 1) + { + eos = true; + dataSize = leftoverCarry; + } + + var buffer = new byte[dataSize]; + Buffer.BlockCopy(data, (int)_sendOffset, buffer, 0, dataSize); + + var packet = _connection.PacketCreator.CreateDataPacket(StreamId.IntegerValue, buffer, _sendOffset, eos); + if (i == 0 && data.Length >= QuicSettings.MaxStreamData) + { + packet.AttachFrame(new MaxStreamDataFrame(StreamId.IntegerValue, (ulong)(data.Length + 1))); + } + + if (_connection.MaximumReached()) + { + packet.AttachFrame(new StreamDataBlockedFrame(StreamId.IntegerValue, (ulong)data.Length)); + } + + _sendOffset += (ulong)buffer.Length; + + _connection.SendData(packet); + } + + return true; + } /// - /// Virtual multiplexing channel. + /// Client only! /// - public class QuicStream + /// + public byte[] Receive() { - private readonly SortedList _data = new SortedList(); - private readonly QuicConnection _connection; - private ulong _maximumStreamData; - private ulong _currentTransferRate; - private ulong _sendOffset; - - public StreamState State { get; set; } - public StreamType Type { get; set; } - public StreamId StreamId { get; } - - public StreamDataReceivedEvent OnStreamDataReceived { get; set; } - - public byte[] Data => _data.SelectMany(v => v.Value).ToArray(); - - public QuicStream(QuicConnection connection, StreamId streamId) + if (Type == StreamType.ClientUnidirectional) { - StreamId = streamId; - Type = streamId.Type; - - _maximumStreamData = QuicSettings.MaxStreamData; - _currentTransferRate = 0; - _sendOffset = 0; - - _connection = connection; + throw new StreamException("Cannot receive data on unidirectional stream."); } - public bool Send(byte[] data) + 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(ulong maximumData) + { + _maximumStreamData = maximumData; + } + + public bool CanSendData() + { + if (Type == StreamType.ServerUnidirectional || Type == StreamType.ClientUnidirectional) { - 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, (int)_sendOffset, buffer, 0, dataSize); - - ShortHeaderPacket packet = _connection.PacketCreator.CreateDataPacket(this.StreamId.IntegerValue, buffer, _sendOffset, eos); - if (i == 0 && data.Length >= QuicSettings.MaxStreamData) - { - packet.AttachFrame(new MaxStreamDataFrame(this.StreamId.IntegerValue, (ulong)(data.Length + 1))); - } - - if (_connection.MaximumReached()) - { - packet.AttachFrame(new StreamDataBlockedFrame(StreamId.IntegerValue, (ulong)data.Length)); - } - - _sendOffset += (ulong)buffer.Length; - - _connection.SendData(packet); - } - - return true; - } - - /// - /// Client only! - /// - /// - public byte[] Receive() - { - if (Type == StreamType.ClientUnidirectional) - { - throw new StreamException("Cannot receive data on unidirectional stream."); - } - - while (!IsStreamFull() || State == StreamState.Receive) - { - _connection.ReceivePacket(); - } - - return Data; - } - - public void ResetStream(ResetStreamFrame frame) - { - // Reset the state - State = StreamState.ResetReceived; - // Clear data - _data.Clear(); - } - - public void SetMaximumStreamData(ulong 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.Receive || State == StreamState.SizeKnown) { - if (State == StreamState.DataReceived || State == StreamState.ResetReceived) + 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; + } + + var 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 += (ulong)data.Length; + + // Terminate connection if maximum stream data is reached + if (_currentTransferRate >= _maximumStreamData) + { + var errorPacket = _connection.PacketCreator.CreateConnectionClosePacket(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() + { + ulong length = 0; + + foreach (var kvp in _data) + { + if (kvp.Key > 0 && kvp.Key != length) { return false; } - return true; + length += (ulong)kvp.Value.Length; } - 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 += (ulong)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() - { - ulong length = 0; - - foreach (var kvp in _data) - { - if (kvp.Key > 0 && kvp.Key != length) - { - return false; - } - - length += (ulong)kvp.Value.Length; - } - - return true; - } + return true; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Quic/Streams/StreamState.cs b/EonaCat.Network/System/Quic/Streams/StreamState.cs index 68a6b4c..c5b4873 100644 --- a/EonaCat.Network/System/Quic/Streams/StreamState.cs +++ b/EonaCat.Network/System/Quic/Streams/StreamState.cs @@ -1,14 +1,12 @@ -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. +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 - } +public enum StreamState +{ + Receive, + SizeKnown, + DataReceived, + DataRead, + ResetReceived } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/RemoteInfo.cs b/EonaCat.Network/System/Sockets/RemoteInfo.cs index 6780127..8ab1fd6 100644 --- a/EonaCat.Network/System/Sockets/RemoteInfo.cs +++ b/EonaCat.Network/System/Sockets/RemoteInfo.cs @@ -1,23 +1,21 @@ 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. +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. - public class RemoteInfo - { - public bool IsTcp { get; set; } - public bool HasEndpoint => EndPoint != null; - public EndPoint EndPoint { get; set; } - public byte[] Data { get; set; } - public IPAddress Address => HasEndpoint ? ((IPEndPoint)EndPoint).Address : null; - public int Port => HasEndpoint ? ((IPEndPoint)EndPoint).Port : 0; - public Socket Socket { get; set; } - public bool IsIpv6 { get; set; } - public bool IsWebSocket { get; internal set; } - public string ClientId { get; internal set; } - public string ClientName { get; internal set; } - } +public class RemoteInfo +{ + public bool IsTcp { get; set; } + public bool HasEndpoint => EndPoint != null; + public EndPoint EndPoint { get; set; } + public byte[] Data { get; set; } + public IPAddress Address => HasEndpoint ? ((IPEndPoint)EndPoint).Address : null; + public int Port => HasEndpoint ? ((IPEndPoint)EndPoint).Port : 0; + public Socket Socket { get; set; } + public bool IsIPv6 { get; set; } + public bool IsWebSocket { get; internal set; } + public string ClientId { get; internal set; } + public string ClientName { get; internal set; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Tcp/SocketTcpClient.cs b/EonaCat.Network/System/Sockets/Tcp/SocketTcpClient.cs index d46a65f..8206a27 100644 --- a/EonaCat.Network/System/Sockets/Tcp/SocketTcpClient.cs +++ b/EonaCat.Network/System/Sockets/Tcp/SocketTcpClient.cs @@ -1,6 +1,10 @@ using System; +using System.IO; using System.Net; +using System.Net.Security; using System.Net.Sockets; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; namespace EonaCat.Network @@ -9,54 +13,53 @@ namespace EonaCat.Network { private const int BUFFER_SIZE = 4096; - /// - /// OnConnect event - /// + private Socket _socket; + private bool _isIPv6; + + public bool IsIPv6 => _isIPv6; + public event Action OnConnect; - - /// - /// OnReceive event - /// public event Action OnReceive; - - /// - /// OnDisconnect event - /// public event Action OnDisconnect; - - /// - /// OnError event - /// public event Action OnError; - private Socket socket; - - /// - /// Create TCP client - /// - /// - /// - /// - /// - public Task ConnectAsync(string ipAddress, int port) + public async Task ConnectAsync(string ipAddress, int port, bool useSsl = false, SslOptions sslOptions = null) { - if (!IPAddress.TryParse(ipAddress, out IPAddress ip)) + if (!IPAddress.TryParse(ipAddress, out var ip)) { - throw new Exception("EonaCat Network: Invalid ipAddress given"); + throw new ArgumentException("Invalid ipAddress given"); } - return CreateSocketTcpClientAsync(ip, port); + await CreateSocketTcpClientAsync(ip, port, useSsl, sslOptions); } - private async Task CreateSocketTcpClientAsync(IPAddress ipAddress, int port) + private async Task CreateSocketTcpClientAsync(IPAddress ipAddress, int port, bool useSsl, SslOptions sslOptions) { - IsIp6 = ipAddress.AddressFamily == AddressFamily.InterNetworkV6; - socket = new Socket(IsIp6 ? AddressFamily.InterNetworkV6 : AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + _isIPv6 = ipAddress.AddressFamily == AddressFamily.InterNetworkV6; + _socket = new Socket(_isIPv6 ? AddressFamily.InterNetworkV6 : AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); try { - await socket.ConnectAsync(ipAddress, port).ConfigureAwait(false); - OnConnect?.Invoke(new RemoteInfo { IsTcp = true, IsIpv6 = IsIp6 }); - _ = StartReceivingAsync(); + await _socket.ConnectAsync(ipAddress, port).ConfigureAwait(false); + OnConnect?.Invoke(new RemoteInfo { IsTcp = true, IsIPv6 = _isIPv6 }); + + if (useSsl) + { + var sslStream = new SslStream(new NetworkStream(_socket), false, ValidateRemoteCertificate, null); + try + { + await sslStream.AuthenticateAsClientAsync(ipAddress.ToString(), sslOptions.ClientCertificates, sslOptions.SslProtocol, sslOptions.CheckCertificateRevocation); + _ = StartReceivingSslAsync(sslStream); + } + catch (AuthenticationException ex) + { + OnError?.Invoke(ex, $"AuthenticationException: {ex.Message}"); + Disconnect(); + } + } + else + { + _ = StartReceivingAsync(); + } } catch (SocketException ex) { @@ -65,26 +68,24 @@ namespace EonaCat.Network } } - public bool IsIp6 { get; set; } - private async Task StartReceivingAsync() { - byte[] buffer = new byte[BUFFER_SIZE]; // Increased buffer size for better performance - while (socket.Connected) + var buffer = new byte[BUFFER_SIZE]; + while (_socket.Connected) { try { - int received = await socket.ReceiveAsync(new ArraySegment(buffer), SocketFlags.None).ConfigureAwait(false); + var received = await _socket.ReceiveAsync(new ArraySegment(buffer), SocketFlags.None).ConfigureAwait(false); if (received > 0) { - byte[] data = new byte[received]; + var data = new byte[received]; Buffer.BlockCopy(buffer, 0, data, 0, received); OnReceive?.Invoke(new RemoteInfo { IsTcp = true, Data = data, - EndPoint = socket.RemoteEndPoint, - IsIpv6 = socket.AddressFamily == AddressFamily.InterNetworkV6 + EndPoint = _socket.RemoteEndPoint, + IsIPv6 = _isIPv6 }); } else @@ -98,25 +99,73 @@ namespace EonaCat.Network break; } } - OnDisconnect?.Invoke(new RemoteInfo { IsTcp = true, EndPoint = socket.RemoteEndPoint, IsIpv6 = socket.AddressFamily == AddressFamily.InterNetworkV6 }); + + OnDisconnect?.Invoke(new RemoteInfo + { + IsTcp = true, + EndPoint = _socket.RemoteEndPoint, + IsIPv6 = _isIPv6 + }); } - /// - /// Send data - /// - /// - /// - public Task SendAsync(byte[] data) + private async Task StartReceivingSslAsync(SslStream sslStream) { - return socket.SendAsync(new ArraySegment(data), SocketFlags.None); + var buffer = new byte[BUFFER_SIZE]; + while (_socket.Connected) + { + try + { + var received = await sslStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + if (received > 0) + { + var data = new byte[received]; + Buffer.BlockCopy(buffer, 0, data, 0, received); + OnReceive?.Invoke(new RemoteInfo + { + IsTcp = true, + Data = data, + EndPoint = _socket.RemoteEndPoint, + IsIPv6 = _isIPv6 + }); + } + else + { + break; + } + } + catch (IOException ex) + { + OnError?.Invoke(ex, $"IOException: {ex.Message}"); + break; + } + catch (AuthenticationException ex) + { + OnError?.Invoke(ex, $"AuthenticationException: {ex.Message}"); + break; + } + } + + OnDisconnect?.Invoke(new RemoteInfo + { + IsTcp = true, + EndPoint = _socket.RemoteEndPoint, + IsIPv6 = _isIPv6 + }); + } + + public async Task SendAsync(byte[] data) + { + await _socket.SendAsync(new ArraySegment(data), SocketFlags.None).ConfigureAwait(false); } - /// - /// Disconnect - /// public void Disconnect() { - socket.Close(); + _socket.Close(); + } + + private bool ValidateRemoteCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + { + return sslPolicyErrors == SslPolicyErrors.None; } } -} \ No newline at end of file +} diff --git a/EonaCat.Network/System/Sockets/Tcp/SocketTcpServer.cs b/EonaCat.Network/System/Sockets/Tcp/SocketTcpServer.cs index 767cda4..c6244eb 100644 --- a/EonaCat.Network/System/Sockets/Tcp/SocketTcpServer.cs +++ b/EonaCat.Network/System/Sockets/Tcp/SocketTcpServer.cs @@ -1,6 +1,10 @@ using System; +using System.IO; using System.Net; +using System.Net.Security; using System.Net.Sockets; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; @@ -10,61 +14,33 @@ namespace EonaCat.Network { private const int BUFFER_SIZE = 4096; - /// - /// OnConnect event - /// - public event Action OnConnect; - - /// - /// OnReceive event - /// - public event Action OnReceive; - - /// - /// OnSend event - /// - public event Action OnSend; - - /// - /// OnDisconnect event - /// - public event Action OnDisconnect; - - /// - /// OnError event - /// - public event Action OnError; - private readonly TcpListener _listener; - private CancellationTokenSource _cancellationTokenSource; + private readonly X509Certificate2 _certificate; + private readonly SslOptions _sslOptions; private Task _acceptTask; + private CancellationTokenSource _cancellationTokenSource; - /// - /// Create TCP server - /// - /// - /// - public SocketTcpServer(IPAddress ipAddress, int port) + public SocketTcpServer(IPAddress ipAddress, int port, X509Certificate2 certificate = null, SslOptions sslOptions = null) { _listener = new TcpListener(ipAddress, port); + _certificate = certificate; + _sslOptions = sslOptions; } - /// - /// Create TCP server - /// - /// - /// - public SocketTcpServer(string ipAddress, int port) + public SocketTcpServer(string ipAddress, int port, X509Certificate2 certificate = null, SslOptions sslOptions = null) { - IPAddress address = IPAddress.Parse(ipAddress); + var address = IPAddress.Parse(ipAddress); _listener = new TcpListener(address, port); + _certificate = certificate; + _sslOptions = sslOptions; } - /// - /// Start TCP server - /// - /// - /// + public event Action OnConnect; + public event Action OnReceive; + public event Action OnSend; + public event Action OnDisconnect; + public event Action OnError; + public Task StartAsync(CancellationToken cancellationToken = default) { _listener.Start(); @@ -79,8 +55,8 @@ namespace EonaCat.Network { try { - var socket = await _listener.AcceptSocketAsync().ConfigureAwait(false); - _ = HandleConnectionAsync(socket, cancellationToken); + var tcpClient = await _listener.AcceptTcpClientAsync().ConfigureAwait(false); + _ = HandleConnectionAsync(tcpClient, cancellationToken); } catch (SocketException ex) { @@ -89,69 +65,76 @@ namespace EonaCat.Network } } - private async Task HandleConnectionAsync(Socket socket, CancellationToken cancellationToken) + private async Task HandleConnectionAsync(TcpClient tcpClient, CancellationToken cancellationToken) { - OnConnect?.Invoke(new RemoteInfo + var remoteEndpoint = tcpClient.Client.RemoteEndPoint; + using (tcpClient) { - Socket = socket, - IsTcp = true, - IsIpv6 = socket.AddressFamily == AddressFamily.InterNetworkV6, - EndPoint = socket.RemoteEndPoint - }); - - byte[] buffer = new byte[BUFFER_SIZE]; - while (!cancellationToken.IsCancellationRequested) - { - try + OnConnect?.Invoke(new RemoteInfo { - int received = await ReceiveAsync(socket, buffer, cancellationToken).ConfigureAwait(false); - if (received > 0) + Socket = tcpClient.Client, + IsTcp = true, + IsIPv6 = remoteEndpoint.AddressFamily == AddressFamily.InterNetworkV6, + EndPoint = remoteEndpoint + }); + + Stream stream = tcpClient.GetStream(); + if (_certificate != null) + { + var sslStream = new SslStream(stream, false, ValidateRemoteCertificate, null); + try { - byte[] data = new byte[received]; - Buffer.BlockCopy(buffer, 0, data, 0, received); - OnReceive?.Invoke(new RemoteInfo - { - Socket = socket, - IsTcp = true, - IsIpv6 = socket.AddressFamily == AddressFamily.InterNetworkV6, - EndPoint = socket.RemoteEndPoint, - Data = data - }); + await sslStream.AuthenticateAsServerAsync(_certificate, _sslOptions.ClientCertificateRequired, _sslOptions.SslProtocol, _sslOptions.CheckCertificateRevocation); + stream = sslStream; } - else + catch (AuthenticationException ex) { - break; + OnError?.Invoke(ex, $"AuthenticationException: {ex.Message}"); + return; } } - catch (SocketException ex) + + var buffer = new byte[BUFFER_SIZE]; + while (!cancellationToken.IsCancellationRequested) { - OnError?.Invoke(ex, $"SocketException: {ex.Message}"); - break; + try + { + var received = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + if (received > 0) + { + var data = new byte[received]; + Buffer.BlockCopy(buffer, 0, data, 0, received); + OnReceive?.Invoke(new RemoteInfo + { + Socket = tcpClient.Client, + IsTcp = true, + IsIPv6 = remoteEndpoint.AddressFamily == AddressFamily.InterNetworkV6, + EndPoint = remoteEndpoint, + Data = data + }); + } + else + { + break; + } + } + catch (IOException ex) + { + OnError?.Invoke(ex, $"IOException: {ex.Message}"); + break; + } } } OnDisconnect?.Invoke(new RemoteInfo { - Socket = socket, + Socket = tcpClient.Client, IsTcp = true, - EndPoint = socket.RemoteEndPoint, - IsIpv6 = socket.AddressFamily == AddressFamily.InterNetworkV6 + EndPoint = remoteEndpoint, + IsIPv6 = remoteEndpoint.AddressFamily == AddressFamily.InterNetworkV6 }); } - private Task ReceiveAsync(Socket socket, byte[] buffer, CancellationToken cancellationToken) - { - return Task.Factory.FromAsync(socket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, null, socket), - socket.EndReceive); - } - - - /// - /// Send data to socket - /// - /// - /// - /// public async Task SendToAsync(Socket socket, byte[] data) { await socket.SendAsync(new ArraySegment(data), SocketFlags.None).ConfigureAwait(false); @@ -160,15 +143,11 @@ namespace EonaCat.Network Socket = socket, IsTcp = true, EndPoint = socket.RemoteEndPoint, - IsIpv6 = socket.AddressFamily == AddressFamily.InterNetworkV6, + IsIPv6 = socket.AddressFamily == AddressFamily.InterNetworkV6, Data = data }); } - /// - /// Stop TCP server - /// - /// public async Task StopAsync() { _cancellationTokenSource.Cancel(); @@ -179,8 +158,19 @@ namespace EonaCat.Network { await _acceptTask.ConfigureAwait(false); } - catch (AggregateException) { } + catch (AggregateException) + { + } } } + + private bool ValidateRemoteCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + { + if (_sslOptions != null && _sslOptions.CertificateValidationCallback != null) + { + return _sslOptions.CertificateValidationCallback(sender, certificate, chain, sslPolicyErrors); + } + return sslPolicyErrors == SslPolicyErrors.None; + } } -} \ No newline at end of file +} diff --git a/EonaCat.Network/System/Sockets/Tcp/SslOptions.cs b/EonaCat.Network/System/Sockets/Tcp/SslOptions.cs new file mode 100644 index 0000000..93b8406 --- /dev/null +++ b/EonaCat.Network/System/Sockets/Tcp/SslOptions.cs @@ -0,0 +1,14 @@ +using System.Net.Security; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; + +namespace EonaCat.Network; + +public class SslOptions +{ + public SslProtocols SslProtocol { get; set; } = SslProtocols.Tls12; + public bool CheckCertificateRevocation { get; set; } = true; + public X509CertificateCollection ClientCertificates { get; set; } + public RemoteCertificateValidationCallback CertificateValidationCallback { get; set; } + public bool ClientCertificateRequired { get; set; } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Udp/SocketUdpClient.cs b/EonaCat.Network/System/Sockets/Udp/SocketUdpClient.cs index b2b6949..129d3f6 100644 --- a/EonaCat.Network/System/Sockets/Udp/SocketUdpClient.cs +++ b/EonaCat.Network/System/Sockets/Udp/SocketUdpClient.cs @@ -4,112 +4,110 @@ using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; -namespace EonaCat.Network +namespace EonaCat.Network; + +public class SocketUdpClient { - public class SocketUdpClient + private UdpClient _udpClient; + + /// + /// Create UDP client + /// + /// + /// + /// + public SocketUdpClient(IPAddress ipAddress, int port, CancellationToken cancellationToken = default) { - /// - /// OnConnect event - /// - public event Action OnConnect; + CreateUdpClient(ipAddress, port, cancellationToken); + } - /// - /// OnReceive event - /// - public event Action OnReceive; + public bool IsMulticastGroupEnabled { get; set; } - /// - /// OnDisconnect event - /// - public event Action OnDisconnect; + public bool IsIp6 { get; private set; } - /// - /// OnSend event - /// - public event Action OnSend; + /// + /// OnConnect event + /// + public event Action OnConnect; - /// - /// OnError event - /// - public event Action OnError; + /// + /// OnReceive event + /// + public event Action OnReceive; - private UdpClient _udpClient; + /// + /// OnDisconnect event + /// + public event Action OnDisconnect; - /// - /// Create UDP client - /// - /// - /// - /// - public SocketUdpClient(IPAddress ipAddress, int port, CancellationToken cancellationToken = default) + /// + /// OnSend event + /// + public event Action OnSend; + + /// + /// OnError event + /// + public event Action OnError; + + private void CreateUdpClient(IPAddress ipAddress, int port, CancellationToken cancellationToken = default) + { + IsIp6 = ipAddress.AddressFamily == AddressFamily.InterNetworkV6; + _udpClient = new UdpClient(ipAddress.AddressFamily); + + if (IsIp6) { - CreateUdpClient(ipAddress, port, cancellationToken); + _udpClient.Client.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, false); } - private void CreateUdpClient(IPAddress ipAddress, int port, CancellationToken cancellationToken = default) + _udpClient.Client.Bind(new IPEndPoint(IsIp6 ? IPAddress.IPv6Any : IPAddress.Any, 0)); + + if (IsMulticastGroupEnabled) { - IsIp6 = ipAddress.AddressFamily == AddressFamily.InterNetworkV6; - _udpClient = new UdpClient(ipAddress.AddressFamily); - - if (IsIp6) - { - _udpClient.Client.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, false); - } - - _udpClient.Client.Bind(new IPEndPoint(IsIp6 ? IPAddress.IPv6Any : IPAddress.Any, 0)); - - if (IsMulticastGroupEnabled) - { - _udpClient.JoinMulticastGroup(ipAddress); - } - - OnConnect?.Invoke(_udpClient.Client.RemoteEndPoint); - _ = StartReceivingAsync(cancellationToken); + _udpClient.JoinMulticastGroup(ipAddress); } - public bool IsMulticastGroupEnabled { get; set; } + OnConnect?.Invoke(_udpClient.Client.RemoteEndPoint); + _ = StartReceivingAsync(cancellationToken); + } - public bool IsIp6 { get; private set; } - - private async Task StartReceivingAsync(CancellationToken cancellationToken) - { - while (!cancellationToken.IsCancellationRequested) + private async Task StartReceivingAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + try { - try - { - var result = await _udpClient.ReceiveAsync().ConfigureAwait(false); - OnReceive?.Invoke(result.Buffer); - } - catch (SocketException ex) - { - OnError?.Invoke(ex, $"SocketException: {ex.Message}"); - break; - } + var result = await _udpClient.ReceiveAsync().ConfigureAwait(false); + OnReceive?.Invoke(result.Buffer); } - OnDisconnect?.Invoke(_udpClient.Client.RemoteEndPoint); - } - - /// - /// Send data to endPoint - /// - /// - /// - /// - public async Task SendTo(EndPoint endPoint, byte[] data) - { - if (endPoint is IPEndPoint ipEndPoint) + catch (SocketException ex) { - await _udpClient.SendAsync(data, data.Length, ipEndPoint).ConfigureAwait(false); - OnSend?.Invoke(endPoint, data); + OnError?.Invoke(ex, $"SocketException: {ex.Message}"); + break; } - } - /// - /// Disconnect UDP client - /// - public void Disconnect() + OnDisconnect?.Invoke(_udpClient.Client.RemoteEndPoint); + } + + /// + /// Send data to endPoint + /// + /// + /// + /// + public async Task SendTo(EndPoint endPoint, byte[] data) + { + if (endPoint is IPEndPoint ipEndPoint) { - _udpClient.Close(); + await _udpClient.SendAsync(data, data.Length, ipEndPoint).ConfigureAwait(false); + OnSend?.Invoke(endPoint, data); } } + + /// + /// Disconnect UDP client + /// + public void Disconnect() + { + _udpClient.Close(); + } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/Udp/SocketUdpServer.cs b/EonaCat.Network/System/Sockets/Udp/SocketUdpServer.cs index d0c55ed..bd9d403 100644 --- a/EonaCat.Network/System/Sockets/Udp/SocketUdpServer.cs +++ b/EonaCat.Network/System/Sockets/Udp/SocketUdpServer.cs @@ -6,146 +6,144 @@ using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; -namespace EonaCat.Network +namespace EonaCat.Network; + +public class SocketUdpServer { - public class SocketUdpServer + private UdpClient udpClient; + + /// + /// Create UDP server + /// + /// + /// + public SocketUdpServer(IPAddress ipAddress, int port) { - /// - /// OnReceive event - /// - public event Action OnReceive; + CreateUdpServer(ipAddress, port); + } - /// - /// OnSend event - /// - public event Action OnSend; - - /// - /// OnError event - /// - public event Action OnError; - - /// - /// OnDisconnect event - /// - public event Action OnDisconnect; - - private UdpClient udpClient; - - /// - /// Determines if the UDP server is IpV6 - /// - public bool IsIp6 { get; private set; } - - /// - /// Create UDP server - /// - /// - /// - public SocketUdpServer(IPAddress ipAddress, int port) + /// + /// Create UDP server + /// + /// + /// + /// + public SocketUdpServer(string ipAddress, int port) + { + if (!IPAddress.TryParse(ipAddress, out var ip)) { - CreateUdpServer(ipAddress, port); + throw new Exception("EonaCat Network: Invalid ipAddress given"); } - /// - /// Create UDP server - /// - /// - /// - /// - public SocketUdpServer(string ipAddress, int port) - { - if (!IPAddress.TryParse(ipAddress, out IPAddress ip)) - { - throw new Exception("EonaCat Network: Invalid ipAddress given"); - } + CreateUdpServer(ip, port); + } - CreateUdpServer(ip, port); + /// + /// Determines if the UDP server is IpV6 + /// + public bool IsIp6 { get; private set; } + + /// + /// OnReceive event + /// + public event Action OnReceive; + + /// + /// OnSend event + /// + public event Action OnSend; + + /// + /// OnError event + /// + public event Action OnError; + + /// + /// OnDisconnect event + /// + public event Action OnDisconnect; + + private static void SetConnectionReset(Socket socket) + { + if (socket.ProtocolType != ProtocolType.Udp && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return; } - private static void SetConnectionReset(Socket socket) + // Disable ICMP packet shutdown (forcibly closed) + const int SioUdpConnReset = -1744830452; + + byte[] inValue = { 0, 0, 0, 0 }; + socket.IOControl(SioUdpConnReset, inValue, null); + } + + private void CreateUdpServer(IPAddress ipAddress, int port) + { + IsIp6 = ipAddress.AddressFamily == AddressFamily.InterNetworkV6; + udpClient = new UdpClient(ipAddress.AddressFamily); + SetConnectionReset(udpClient.Client); + + if (IsIp6) { - if (socket.ProtocolType != ProtocolType.Udp && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return; - } - - // Disable ICMP packet shutdown (forcibly closed) - const int SioUdpConnReset = -1744830452; - - byte[] inValue = { 0, 0, 0, 0 }; - socket.IOControl(SioUdpConnReset, inValue, null); + udpClient.Client.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, false); } - private void CreateUdpServer(IPAddress ipAddress, int port) - { - IsIp6 = ipAddress.AddressFamily == AddressFamily.InterNetworkV6; - udpClient = new UdpClient(ipAddress.AddressFamily); - SetConnectionReset(udpClient.Client); + udpClient.Client.Bind(new IPEndPoint(ipAddress, port)); + } - if (IsIp6) + /// + /// Start UDP server + /// + /// + /// + public async Task StartAsync(CancellationToken cancellationToken = default) + { + while (!cancellationToken.IsCancellationRequested) + try { - udpClient.Client.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, false); - } - - udpClient.Client.Bind(new IPEndPoint(ipAddress, port)); - } - - /// - /// Start UDP server - /// - /// - /// - public async Task StartAsync(CancellationToken cancellationToken = default) - { - while (!cancellationToken.IsCancellationRequested) - { - try + var result = await udpClient.ReceiveAsync().ConfigureAwait(false); + OnReceive?.Invoke(new RemoteInfo { - var result = await udpClient.ReceiveAsync().ConfigureAwait(false); - OnReceive?.Invoke(new RemoteInfo() - { - EndPoint = result.RemoteEndPoint, - IsIpv6 = result.RemoteEndPoint.AddressFamily == AddressFamily.InterNetworkV6, - Data = result.Buffer - }); - } - catch (SocketException ex) - { - OnError?.Invoke(ex, $"SocketException: {ex.Message}"); - } + EndPoint = result.RemoteEndPoint, + IsIPv6 = result.RemoteEndPoint.AddressFamily == AddressFamily.InterNetworkV6, + Data = result.Buffer + }); } - } - - /// - /// Send data to endPoint - /// - /// - /// - /// - public async Task SendToAsync(EndPoint endPoint, byte[] data) - { - if (endPoint is IPEndPoint ipEndPoint) + catch (SocketException ex) { - await udpClient.SendAsync(data, data.Length, ipEndPoint).ConfigureAwait(false); - OnSend?.Invoke(new RemoteInfo() { EndPoint = endPoint, Data = data, IsIpv6 = endPoint.AddressFamily == AddressFamily.InterNetworkV6 }); + OnError?.Invoke(ex, $"SocketException: {ex.Message}"); } - } + } - /// - /// Send data to all clients - /// - /// - /// - public async Task SendToAllAsync(byte[] data) + /// + /// Send data to endPoint + /// + /// + /// + /// + public async Task SendToAsync(EndPoint endPoint, byte[] data) + { + if (endPoint is IPEndPoint ipEndPoint) { - // get all connected clients - var ipProperties = IPGlobalProperties.GetIPGlobalProperties(); - var endPoints = ipProperties.GetActiveUdpListeners(); - foreach (var endPoint in endPoints) - { - await udpClient.SendAsync(data, data.Length, endPoint).ConfigureAwait(false); - } + await udpClient.SendAsync(data, data.Length, ipEndPoint).ConfigureAwait(false); + OnSend?.Invoke(new RemoteInfo + { EndPoint = endPoint, Data = data, IsIPv6 = endPoint.AddressFamily == AddressFamily.InterNetworkV6 }); + } + } + + /// + /// Send data to all clients + /// + /// + /// + public async Task SendToAllAsync(byte[] data) + { + // get all connected clients + var ipProperties = IPGlobalProperties.GetIPGlobalProperties(); + var endPoints = ipProperties.GetActiveUdpListeners(); + foreach (var endPoint in endPoints) + { + await udpClient.SendAsync(data, data.Length, endPoint).ConfigureAwait(false); } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Buffer/BufferValidator.cs b/EonaCat.Network/System/Sockets/WebSockets/Buffer/BufferValidator.cs index 9cf5723..41f654f 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Buffer/BufferValidator.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Buffer/BufferValidator.cs @@ -1,49 +1,59 @@ using System; -namespace EonaCat.WebSockets.Buffer +namespace EonaCat.WebSockets.Buffer; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public class BufferValidator { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - - public class BufferValidator + public static void ValidateBuffer(byte[] buffer, int offset, int count, + string bufferParameterName = null, + string offsetParameterName = null, + string countParameterName = null) { - public static void ValidateBuffer(byte[] buffer, int offset, int count, - string bufferParameterName = null, - string offsetParameterName = null, - string countParameterName = null) + if (buffer == null) { - if (buffer == null) - { - throw new ArgumentNullException(!string.IsNullOrEmpty(bufferParameterName) ? bufferParameterName : "buffer"); - } - - if (offset < 0 || offset > buffer.Length) - { - throw new ArgumentOutOfRangeException(!string.IsNullOrEmpty(offsetParameterName) ? offsetParameterName : "offset"); - } - - if (count < 0 || count > (buffer.Length - offset)) - { - throw new ArgumentOutOfRangeException(!string.IsNullOrEmpty(countParameterName) ? countParameterName : "count"); - } + throw new ArgumentNullException(!string.IsNullOrEmpty(bufferParameterName) + ? bufferParameterName + : "buffer"); } - public static void ValidateArraySegment(ArraySegment arraySegment, string arraySegmentParameterName = null) + if (offset < 0 || offset > buffer.Length) { - if (arraySegment.Array == null) - { - throw new ArgumentNullException((!string.IsNullOrEmpty(arraySegmentParameterName) ? arraySegmentParameterName : "arraySegment") + ".Array"); - } + throw new ArgumentOutOfRangeException(!string.IsNullOrEmpty(offsetParameterName) + ? offsetParameterName + : "offset"); + } - if (arraySegment.Offset < 0 || arraySegment.Offset > arraySegment.Array.Length) - { - throw new ArgumentOutOfRangeException((!string.IsNullOrEmpty(arraySegmentParameterName) ? arraySegmentParameterName : "arraySegment") + ".Offset"); - } - - if (arraySegment.Count < 0 || arraySegment.Count > (arraySegment.Array.Length - arraySegment.Offset)) - { - throw new ArgumentOutOfRangeException((!string.IsNullOrEmpty(arraySegmentParameterName) ? arraySegmentParameterName : "arraySegment") + ".Count"); - } + if (count < 0 || count > buffer.Length - offset) + { + throw new ArgumentOutOfRangeException(!string.IsNullOrEmpty(countParameterName) + ? countParameterName + : "count"); } } -} + + public static void ValidateArraySegment(ArraySegment arraySegment, string arraySegmentParameterName = null) + { + if (arraySegment.Array == null) + { + throw new ArgumentNullException((!string.IsNullOrEmpty(arraySegmentParameterName) + ? arraySegmentParameterName + : "arraySegment") + ".Array"); + } + + if (arraySegment.Offset < 0 || arraySegment.Offset > arraySegment.Array.Length) + { + throw new ArgumentOutOfRangeException((!string.IsNullOrEmpty(arraySegmentParameterName) + ? arraySegmentParameterName + : "arraySegment") + ".Offset"); + } + + if (arraySegment.Count < 0 || arraySegment.Count > arraySegment.Array.Length - arraySegment.Offset) + { + throw new ArgumentOutOfRangeException((!string.IsNullOrEmpty(arraySegmentParameterName) + ? arraySegmentParameterName + : "arraySegment") + ".Count"); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Buffer/ISegmentBufferManager.cs b/EonaCat.Network/System/Sockets/WebSockets/Buffer/ISegmentBufferManager.cs index a5db27d..10df0d5 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Buffer/ISegmentBufferManager.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Buffer/ISegmentBufferManager.cs @@ -1,17 +1,15 @@ using System; using System.Collections.Generic; -namespace EonaCat.WebSockets.Buffer -{ - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. +namespace EonaCat.WebSockets.Buffer; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. - public interface ISegmentBufferManager - { - ArraySegment BorrowBuffer(); - IEnumerable> BorrowBuffers(int count); - void ReturnBuffer(ArraySegment buffer); - void ReturnBuffers(IEnumerable> buffers); - void ReturnBuffers(params ArraySegment[] buffers); - } -} +public interface ISegmentBufferManager +{ + ArraySegment BorrowBuffer(); + IEnumerable> BorrowBuffers(int count); + void ReturnBuffer(ArraySegment buffer); + void ReturnBuffers(IEnumerable> buffers); + void ReturnBuffers(params ArraySegment[] buffers); +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Buffer/SegmentBufferDeflector.cs b/EonaCat.Network/System/Sockets/WebSockets/Buffer/SegmentBufferDeflector.cs index 43be208..9d5b9f3 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Buffer/SegmentBufferDeflector.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Buffer/SegmentBufferDeflector.cs @@ -1,93 +1,97 @@ using System; -namespace EonaCat.WebSockets.Buffer +namespace EonaCat.WebSockets.Buffer; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public class SegmentBufferDeflector { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - - public class SegmentBufferDeflector + public static void AppendBuffer( + ISegmentBufferManager bufferManager, + ref ArraySegment receiveBuffer, + int receiveCount, + ref ArraySegment sessionBuffer, + ref int sessionBufferCount) { - public static void AppendBuffer( - ISegmentBufferManager bufferManager, - ref ArraySegment receiveBuffer, - int receiveCount, - ref ArraySegment sessionBuffer, - ref int sessionBufferCount) + if (sessionBuffer.Count < sessionBufferCount + receiveCount) { - if (sessionBuffer.Count < (sessionBufferCount + receiveCount)) + var autoExpandedBuffer = bufferManager.BorrowBuffer(); + if (autoExpandedBuffer.Count < (sessionBufferCount + receiveCount) * 2) { - ArraySegment autoExpandedBuffer = bufferManager.BorrowBuffer(); - if (autoExpandedBuffer.Count < (sessionBufferCount + receiveCount) * 2) - { - bufferManager.ReturnBuffer(autoExpandedBuffer); - autoExpandedBuffer = new ArraySegment(new byte[(sessionBufferCount + receiveCount) * 2]); - } - - Array.Copy(sessionBuffer.Array, sessionBuffer.Offset, autoExpandedBuffer.Array, autoExpandedBuffer.Offset, sessionBufferCount); - - var discardBuffer = sessionBuffer; - sessionBuffer = autoExpandedBuffer; - bufferManager.ReturnBuffer(discardBuffer); + bufferManager.ReturnBuffer(autoExpandedBuffer); + autoExpandedBuffer = new ArraySegment(new byte[(sessionBufferCount + receiveCount) * 2]); } - Array.Copy(receiveBuffer.Array, receiveBuffer.Offset, sessionBuffer.Array, sessionBuffer.Offset + sessionBufferCount, receiveCount); - sessionBufferCount = sessionBufferCount + receiveCount; + Array.Copy(sessionBuffer.Array, sessionBuffer.Offset, autoExpandedBuffer.Array, autoExpandedBuffer.Offset, + sessionBufferCount); + + var discardBuffer = sessionBuffer; + sessionBuffer = autoExpandedBuffer; + bufferManager.ReturnBuffer(discardBuffer); } - public static void ShiftBuffer( - ISegmentBufferManager bufferManager, - int shiftStart, - ref ArraySegment sessionBuffer, - ref int sessionBufferCount) + Array.Copy(receiveBuffer.Array, receiveBuffer.Offset, sessionBuffer.Array, + sessionBuffer.Offset + sessionBufferCount, receiveCount); + sessionBufferCount = sessionBufferCount + receiveCount; + } + + public static void ShiftBuffer( + ISegmentBufferManager bufferManager, + int shiftStart, + ref ArraySegment sessionBuffer, + ref int sessionBufferCount) + { + if (sessionBufferCount - shiftStart < shiftStart) { - if ((sessionBufferCount - shiftStart) < shiftStart) + Array.Copy(sessionBuffer.Array, sessionBuffer.Offset + shiftStart, sessionBuffer.Array, + sessionBuffer.Offset, sessionBufferCount - shiftStart); + sessionBufferCount = sessionBufferCount - shiftStart; + } + else + { + var copyBuffer = bufferManager.BorrowBuffer(); + if (copyBuffer.Count < sessionBufferCount - shiftStart) { - Array.Copy(sessionBuffer.Array, sessionBuffer.Offset + shiftStart, sessionBuffer.Array, sessionBuffer.Offset, sessionBufferCount - shiftStart); - sessionBufferCount = sessionBufferCount - shiftStart; - } - else - { - ArraySegment copyBuffer = bufferManager.BorrowBuffer(); - if (copyBuffer.Count < (sessionBufferCount - shiftStart)) - { - bufferManager.ReturnBuffer(copyBuffer); - copyBuffer = new ArraySegment(new byte[sessionBufferCount - shiftStart]); - } - - Array.Copy(sessionBuffer.Array, sessionBuffer.Offset + shiftStart, copyBuffer.Array, copyBuffer.Offset, sessionBufferCount - shiftStart); - Array.Copy(copyBuffer.Array, copyBuffer.Offset, sessionBuffer.Array, sessionBuffer.Offset, sessionBufferCount - shiftStart); - sessionBufferCount = sessionBufferCount - shiftStart; - bufferManager.ReturnBuffer(copyBuffer); + copyBuffer = new ArraySegment(new byte[sessionBufferCount - shiftStart]); } - } - public static void ReplaceBuffer( - ISegmentBufferManager bufferManager, - ref ArraySegment receiveBuffer, - ref int receiveBufferOffset, - int receiveCount) - { - if ((receiveBufferOffset + receiveCount) < receiveBuffer.Count) - { - receiveBufferOffset = receiveBufferOffset + receiveCount; - } - else - { - ArraySegment autoExpandedBuffer = bufferManager.BorrowBuffer(); - if (autoExpandedBuffer.Count < (receiveBufferOffset + receiveCount) * 2) - { - bufferManager.ReturnBuffer(autoExpandedBuffer); - autoExpandedBuffer = new ArraySegment(new byte[(receiveBufferOffset + receiveCount) * 2]); - } + Array.Copy(sessionBuffer.Array, sessionBuffer.Offset + shiftStart, copyBuffer.Array, copyBuffer.Offset, + sessionBufferCount - shiftStart); + Array.Copy(copyBuffer.Array, copyBuffer.Offset, sessionBuffer.Array, sessionBuffer.Offset, + sessionBufferCount - shiftStart); + sessionBufferCount = sessionBufferCount - shiftStart; - Array.Copy(receiveBuffer.Array, receiveBuffer.Offset, autoExpandedBuffer.Array, autoExpandedBuffer.Offset, receiveBufferOffset + receiveCount); - receiveBufferOffset = receiveBufferOffset + receiveCount; - - var discardBuffer = receiveBuffer; - receiveBuffer = autoExpandedBuffer; - bufferManager.ReturnBuffer(discardBuffer); - } + bufferManager.ReturnBuffer(copyBuffer); } } -} + + public static void ReplaceBuffer( + ISegmentBufferManager bufferManager, + ref ArraySegment receiveBuffer, + ref int receiveBufferOffset, + int receiveCount) + { + if (receiveBufferOffset + receiveCount < receiveBuffer.Count) + { + receiveBufferOffset = receiveBufferOffset + receiveCount; + } + else + { + var autoExpandedBuffer = bufferManager.BorrowBuffer(); + if (autoExpandedBuffer.Count < (receiveBufferOffset + receiveCount) * 2) + { + bufferManager.ReturnBuffer(autoExpandedBuffer); + autoExpandedBuffer = new ArraySegment(new byte[(receiveBufferOffset + receiveCount) * 2]); + } + + Array.Copy(receiveBuffer.Array, receiveBuffer.Offset, autoExpandedBuffer.Array, autoExpandedBuffer.Offset, + receiveBufferOffset + receiveCount); + receiveBufferOffset = receiveBufferOffset + receiveCount; + + var discardBuffer = receiveBuffer; + receiveBuffer = autoExpandedBuffer; + bufferManager.ReturnBuffer(discardBuffer); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Buffer/SegmentBufferManager.cs b/EonaCat.Network/System/Sockets/WebSockets/Buffer/SegmentBufferManager.cs index cab78bb..05c45f0 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Buffer/SegmentBufferManager.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Buffer/SegmentBufferManager.cs @@ -3,166 +3,148 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -namespace EonaCat.WebSockets.Buffer +namespace EonaCat.WebSockets.Buffer; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// A manager to handle buffers for the socket connections. +/// +/// +/// When used in an async call a buffer is pinned. Large numbers of pinned buffers +/// cause problem with the GC (in particular it causes heap fragmentation). +/// This class maintains a set of large segments and gives clients pieces of these +/// segments that they can use for their buffers. The alternative to this would be to +/// create many small arrays which it then maintained. This methodology should be slightly +/// better than the many small array methodology because in creating only a few very +/// large objects it will force these objects to be placed on the LOH. Since the +/// objects are on the LOH they are at this time not subject to compacting which would +/// require an update of all GC roots as would be the case with lots of smaller arrays +/// that were in the normal heap. +/// +public class SegmentBufferManager : ISegmentBufferManager { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private const int TrialsCount = 100; + + private static SegmentBufferManager _defaultBufferManager; + private readonly bool _allowedToCreateMemory; + + private readonly ConcurrentStack> _buffers = new(); + private readonly object _creatingNewSegmentLock = new(); + + private readonly List _segments; + private readonly int _segmentSize; + + public SegmentBufferManager(int segmentChunks, int chunkSize) + : this(segmentChunks, chunkSize, 1) + { + } + + public SegmentBufferManager(int segmentChunks, int chunkSize, int initialSegments) + : this(segmentChunks, chunkSize, initialSegments, true) + { + } /// - /// A manager to handle buffers for the socket connections. + /// Constructs a new object /// - /// - /// When used in an async call a buffer is pinned. Large numbers of pinned buffers - /// cause problem with the GC (in particular it causes heap fragmentation). - /// This class maintains a set of large segments and gives clients pieces of these - /// segments that they can use for their buffers. The alternative to this would be to - /// create many small arrays which it then maintained. This methodology should be slightly - /// better than the many small array methodology because in creating only a few very - /// large objects it will force these objects to be placed on the LOH. Since the - /// objects are on the LOH they are at this time not subject to compacting which would - /// require an update of all GC roots as would be the case with lots of smaller arrays - /// that were in the normal heap. - /// - public class SegmentBufferManager : ISegmentBufferManager + /// The number of chunks to create per segment + /// The size of a chunk in bytes + /// The initial number of segments to create + /// If false when empty and checkout is called an exception will be thrown + public SegmentBufferManager(int segmentChunks, int chunkSize, int initialSegments, bool allowedToCreateMemory) { - private const int TrialsCount = 100; - - private static SegmentBufferManager _defaultBufferManager; - - private readonly int _segmentChunks; - private readonly int _chunkSize; - private readonly int _segmentSize; - private readonly bool _allowedToCreateMemory; - - private readonly ConcurrentStack> _buffers = new ConcurrentStack>(); - - private readonly List _segments; - private readonly object _creatingNewSegmentLock = new object(); - - public static SegmentBufferManager Default + if (segmentChunks <= 0) { - get - { - // default to 1024 1kb buffers if people don't want to manage it on their own; - if (_defaultBufferManager == null) - { - _defaultBufferManager = new SegmentBufferManager(1024, 1024, 1); - } - - return _defaultBufferManager; - } + throw new ArgumentException("segmentChunks"); } - public static void SetDefaultBufferManager(SegmentBufferManager manager) + if (chunkSize <= 0) { - if (manager == null) + throw new ArgumentException("chunkSize"); + } + + if (initialSegments < 0) + { + throw new ArgumentException("initialSegments"); + } + + SegmentChunksCount = segmentChunks; + ChunkSize = chunkSize; + _segmentSize = SegmentChunksCount * ChunkSize; + + _segments = new List(); + + _allowedToCreateMemory = true; + for (var i = 0; i < initialSegments; i++) CreateNewSegment(true); + _allowedToCreateMemory = allowedToCreateMemory; + } + + public static SegmentBufferManager Default + { + get + { + // default to 1024 1kb buffers if people don't want to manage it on their own; + if (_defaultBufferManager == null) { - throw new ArgumentNullException("manager"); + _defaultBufferManager = new SegmentBufferManager(1024, 1024, 1); } - _defaultBufferManager = manager; + return _defaultBufferManager; } + } - public int ChunkSize + public int ChunkSize { get; } + + public int SegmentsCount => _segments.Count; + + public int SegmentChunksCount { get; } + + public int AvailableBuffers => _buffers.Count; + + public int TotalBufferSize => _segments.Count * _segmentSize; + + public ArraySegment BorrowBuffer() + { + var trial = 0; + while (trial < TrialsCount) { - get { return _chunkSize; } - } - - public int SegmentsCount - { - get { return _segments.Count; } - } - - public int SegmentChunksCount - { - get { return _segmentChunks; } - } - - public int AvailableBuffers - { - get { return _buffers.Count; } - } - - public int TotalBufferSize - { - get { return _segments.Count * _segmentSize; } - } - - public SegmentBufferManager(int segmentChunks, int chunkSize) - : this(segmentChunks, chunkSize, 1) { } - - public SegmentBufferManager(int segmentChunks, int chunkSize, int initialSegments) - : this(segmentChunks, chunkSize, initialSegments, true) { } - - /// - /// Constructs a new object - /// - /// The number of chunks to create per segment - /// The size of a chunk in bytes - /// The initial number of segments to create - /// If false when empty and checkout is called an exception will be thrown - public SegmentBufferManager(int segmentChunks, int chunkSize, int initialSegments, bool allowedToCreateMemory) - { - if (segmentChunks <= 0) + ArraySegment result; + if (_buffers.TryPop(out result)) { - throw new ArgumentException("segmentChunks"); + return result; } - if (chunkSize <= 0) - { - throw new ArgumentException("chunkSize"); - } - - if (initialSegments < 0) - { - throw new ArgumentException("initialSegments"); - } - - _segmentChunks = segmentChunks; - _chunkSize = chunkSize; - _segmentSize = _segmentChunks * _chunkSize; - - _segments = new List(); - - _allowedToCreateMemory = true; - for (int i = 0; i < initialSegments; i++) - { - CreateNewSegment(true); - } - _allowedToCreateMemory = allowedToCreateMemory; + CreateNewSegment(false); + trial++; } - private void CreateNewSegment(bool forceCreation) + throw new UnableToAllocateBufferException(); + } + + public IEnumerable> BorrowBuffers(int count) + { + var result = new ArraySegment[count]; + var trial = 0; + var totalReceived = 0; + + try { - if (!_allowedToCreateMemory) - { - throw new UnableToCreateMemoryException(); - } - - lock (_creatingNewSegmentLock) - { - if (!forceCreation && _buffers.Count > _segmentChunks / 2) - { - return; - } - - var bytes = new byte[_segmentSize]; - _segments.Add(bytes); - for (int i = 0; i < _segmentChunks; i++) - { - var chunk = new ArraySegment(bytes, i * _chunkSize, _chunkSize); - _buffers.Push(chunk); - } - } - } - - public ArraySegment BorrowBuffer() - { - int trial = 0; while (trial < TrialsCount) { - ArraySegment result; - if (_buffers.TryPop(out result)) + ArraySegment piece; + while (totalReceived < count) + { + if (!_buffers.TryPop(out piece)) + { + break; + } + + result[totalReceived] = piece; + ++totalReceived; + } + + if (totalReceived == count) { return result; } @@ -170,104 +152,106 @@ namespace EonaCat.WebSockets.Buffer CreateNewSegment(false); trial++; } + throw new UnableToAllocateBufferException(); } - - public IEnumerable> BorrowBuffers(int count) + catch { - var result = new ArraySegment[count]; - var trial = 0; - var totalReceived = 0; - - try + if (totalReceived > 0) { - while (trial < TrialsCount) - { - ArraySegment piece; - while (totalReceived < count) - { - if (!_buffers.TryPop(out piece)) - { - break; - } - - result[totalReceived] = piece; - ++totalReceived; - } - if (totalReceived == count) - { - return result; - } - - CreateNewSegment(false); - trial++; - } - throw new UnableToAllocateBufferException(); - } - catch - { - if (totalReceived > 0) - { - ReturnBuffers(result.Take(totalReceived)); - } - - throw; - } - } - - public void ReturnBuffer(ArraySegment buffer) - { - if (ValidateBuffer(buffer)) - { - _buffers.Push(buffer); - } - } - - public void ReturnBuffers(IEnumerable> buffers) - { - if (buffers == null) - { - throw new ArgumentNullException("buffers"); + ReturnBuffers(result.Take(totalReceived)); } - foreach (var buf in buffers) - { - if (ValidateBuffer(buf)) - { - _buffers.Push(buf); - } - } - } - - public void ReturnBuffers(params ArraySegment[] buffers) - { - if (buffers == null) - { - throw new ArgumentNullException("buffers"); - } - - foreach (var buf in buffers) - { - if (ValidateBuffer(buf)) - { - _buffers.Push(buf); - } - } - } - - private bool ValidateBuffer(ArraySegment buffer) - { - if (buffer.Array == null || buffer.Count == 0 || buffer.Array.Length < buffer.Offset + buffer.Count) - { - return false; - } - - if (buffer.Count != _chunkSize) - { - return false; - } - - return true; + throw; } } -} + + public void ReturnBuffer(ArraySegment buffer) + { + if (ValidateBuffer(buffer)) + { + _buffers.Push(buffer); + } + } + + public void ReturnBuffers(IEnumerable> buffers) + { + if (buffers == null) + { + throw new ArgumentNullException("buffers"); + } + + foreach (var buf in buffers) + { + if (ValidateBuffer(buf)) + { + _buffers.Push(buf); + } + } + } + + public void ReturnBuffers(params ArraySegment[] buffers) + { + if (buffers == null) + { + throw new ArgumentNullException("buffers"); + } + + foreach (var buf in buffers) + { + if (ValidateBuffer(buf)) + { + _buffers.Push(buf); + } + } + } + + public static void SetDefaultBufferManager(SegmentBufferManager manager) + { + if (manager == null) + { + throw new ArgumentNullException("manager"); + } + + _defaultBufferManager = manager; + } + + private void CreateNewSegment(bool forceCreation) + { + if (!_allowedToCreateMemory) + { + throw new UnableToCreateMemoryException(); + } + + lock (_creatingNewSegmentLock) + { + if (!forceCreation && _buffers.Count > SegmentChunksCount / 2) + { + return; + } + + var bytes = new byte[_segmentSize]; + _segments.Add(bytes); + for (var i = 0; i < SegmentChunksCount; i++) + { + var chunk = new ArraySegment(bytes, i * ChunkSize, ChunkSize); + _buffers.Push(chunk); + } + } + } + + private bool ValidateBuffer(ArraySegment buffer) + { + if (buffer.Array == null || buffer.Count == 0 || buffer.Array.Length < buffer.Offset + buffer.Count) + { + return false; + } + + if (buffer.Count != ChunkSize) + { + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Buffer/UnableToAllocateBufferException.cs b/EonaCat.Network/System/Sockets/WebSockets/Buffer/UnableToAllocateBufferException.cs index 5f32b5e..1cce022 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Buffer/UnableToAllocateBufferException.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Buffer/UnableToAllocateBufferException.cs @@ -1,16 +1,14 @@ using System; -namespace EonaCat.WebSockets.Buffer -{ - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. +namespace EonaCat.WebSockets.Buffer; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. - [Serializable] - public class UnableToAllocateBufferException : Exception +[Serializable] +public class UnableToAllocateBufferException : Exception +{ + public UnableToAllocateBufferException() + : base("Cannot allocate buffer after few trials.") { - public UnableToAllocateBufferException() - : base("Cannot allocate buffer after few trials.") - { - } } -} +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Buffer/UnableToCreateMemoryException .cs b/EonaCat.Network/System/Sockets/WebSockets/Buffer/UnableToCreateMemoryException .cs index 7f6d0f2..71e4951 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Buffer/UnableToCreateMemoryException .cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Buffer/UnableToCreateMemoryException .cs @@ -1,16 +1,14 @@ using System; -namespace EonaCat.WebSockets.Buffer -{ - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. +namespace EonaCat.WebSockets.Buffer; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. - [Serializable] - public class UnableToCreateMemoryException : Exception +[Serializable] +public class UnableToCreateMemoryException : Exception +{ + public UnableToCreateMemoryException() + : base("All buffers were in use and acquiring more memory has been disabled.") { - public UnableToCreateMemoryException() - : base("All buffers were in use and acquiring more memory has been disabled.") - { - } } -} +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Client/AsyncWebSocketClient.cs b/EonaCat.Network/System/Sockets/WebSockets/Client/AsyncWebSocketClient.cs index 4c5b8cf..6dabf00 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Client/AsyncWebSocketClient.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Client/AsyncWebSocketClient.cs @@ -1,1332 +1,1311 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net; -using System.Net.NetworkInformation; using System.Net.Security; using System.Net.Sockets; -using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; using System.Threading.Tasks; -using EonaCat.WebSockets.Buffer; using EonaCat.Logger.Extensions; using EonaCat.Network; +using EonaCat.WebSockets.Buffer; using EonaCat.WebSockets.Extensions; using EonaCat.WebSockets.SubProtocols; -namespace EonaCat.WebSockets +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public sealed class AsyncWebSocketClient : IDisposable { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private const int _none = 0; + private const int _connecting = 1; + private const int _connected = 2; + private const int _closing = 3; + private const int _closed = 5; + private readonly AsyncWebSocketClientConfiguration _configuration; + private readonly IAsyncWebSocketClientMessageDispatcher _dispatcher; + private readonly IFrameBuilder _frameBuilder = new WebSocketFrameBuilder(); - public sealed class AsyncWebSocketClient : IDisposable + private readonly SemaphoreSlim _keepAliveLocker = new(1, 1); + private readonly IPEndPoint _remoteEndPoint; + + private readonly bool _sslEnabled; + private Timer _closingTimeoutTimer; + private Timer _keepAliveTimeoutTimer; + private KeepAliveTracker _keepAliveTracker; + private ArraySegment _receiveBuffer; + private int _receiveBufferOffset; + private string _secWebSocketKey; + + private int _state; + private Stream _stream; + + private TcpClient _tcpClient; + + public AsyncWebSocketClient(Uri uri, IAsyncWebSocketClientMessageDispatcher dispatcher, + AsyncWebSocketClientConfiguration configuration = null) { - #region Fields - - private TcpClient _tcpClient; - private readonly IAsyncWebSocketClientMessageDispatcher _dispatcher; - private readonly AsyncWebSocketClientConfiguration _configuration; - private readonly IFrameBuilder _frameBuilder = new WebSocketFrameBuilder(); - private IPEndPoint _remoteEndPoint; - private Stream _stream; - private ArraySegment _receiveBuffer = default(ArraySegment); - private int _receiveBufferOffset = 0; - - private readonly Uri _uri; - private bool _sslEnabled = false; - private string _secWebSocketKey; - - private int _state; - private const int _none = 0; - private const int _connecting = 1; - private const int _connected = 2; - private const int _closing = 3; - private const int _closed = 5; - - private readonly SemaphoreSlim _keepAliveLocker = new SemaphoreSlim(1, 1); - private KeepAliveTracker _keepAliveTracker; - private Timer _keepAliveTimeoutTimer; - private Timer _closingTimeoutTimer; - - #endregion - - #region Constructors - - public AsyncWebSocketClient(Uri uri, IAsyncWebSocketClientMessageDispatcher dispatcher, AsyncWebSocketClientConfiguration configuration = null) + if (uri == null) { - if (uri == null) + throw new ArgumentNullException("uri"); + } + + if (dispatcher == null) + { + throw new ArgumentNullException("dispatcher"); + } + + if (!Consts.WebSocketSchemes.Contains(uri.Scheme.ToLowerInvariant())) + { + throw new NotSupportedException( + $"Not support the specified scheme [{uri.Scheme}]."); + } + + Uri = uri; + _remoteEndPoint = ResolveRemoteEndPoint(Uri); + _dispatcher = dispatcher; + _configuration = configuration ?? new AsyncWebSocketClientConfiguration(); + _sslEnabled = uri.Scheme.ToLowerInvariant() == "wss"; + + if (_configuration.BufferManager == null) + { + throw new InvalidProgramException("The buffer manager in configuration cannot be null."); + } + } + + public AsyncWebSocketClient(Uri uri, + Func onServerTextReceived = null, + Func onServerBinaryReceived = null, + Func onServerConnected = null, + Func onServerDisconnected = null, + AsyncWebSocketClientConfiguration configuration = null) + : this(uri, + new InternalAsyncWebSocketClientMessageDispatcherImplementation( + onServerTextReceived, onServerBinaryReceived, onServerConnected, onServerDisconnected), + configuration) + { + } + + public AsyncWebSocketClient(Uri uri, + Func onServerTextReceived = null, + Func onServerBinaryReceived = null, + Func onServerConnected = null, + Func onServerDisconnected = null, + Func onServerFragmentationStreamOpened = null, + Func onServerFragmentationStreamContinued = null, + Func onServerFragmentationStreamClosed = null, + AsyncWebSocketClientConfiguration configuration = null) + : this(uri, + new InternalAsyncWebSocketClientMessageDispatcherImplementation( + onServerTextReceived, onServerBinaryReceived, onServerConnected, onServerDisconnected, + onServerFragmentationStreamOpened, onServerFragmentationStreamContinued, + onServerFragmentationStreamClosed), + configuration) + { + } + + private bool Connected => _tcpClient != null && _tcpClient.Client.Connected; + public IPEndPoint RemoteEndPoint => Connected ? (IPEndPoint)_tcpClient.Client.RemoteEndPoint : _remoteEndPoint; + public IPEndPoint LocalEndPoint => Connected ? (IPEndPoint)_tcpClient.Client.LocalEndPoint : null; + + public Uri Uri { get; } + + public TimeSpan ConnectTimeout => _configuration.ConnectTimeout; + public TimeSpan CloseTimeout => _configuration.CloseTimeout; + public TimeSpan KeepAliveInterval => _configuration.KeepAliveInterval; + public TimeSpan KeepAliveTimeout => _configuration.KeepAliveTimeout; + + public IDictionary EnabledExtensions => _configuration.EnabledExtensions; + + public IDictionary EnabledSubProtocols => + _configuration.EnabledSubProtocols; + + public IEnumerable OfferedExtensions => _configuration.OfferedExtensions; + + public IEnumerable RequestedSubProtocols => + _configuration.RequestedSubProtocols; + + public WebSocketState State + { + get + { + switch (_state) { - throw new ArgumentNullException("uri"); + case _none: + return WebSocketState.None; + case _connecting: + return WebSocketState.Connecting; + case _connected: + return WebSocketState.Open; + case _closing: + return WebSocketState.Closing; + case _closed: + return WebSocketState.Closed; + default: + return WebSocketState.Closed; + } + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + internal void AgreeExtensions(IEnumerable extensions) + { + if (extensions == null) + { + throw new ArgumentNullException("extensions"); + } + + // If a server gives an invalid response, such as accepting a PMCE that + // the client did not offer, the client MUST _Fail the WebSocket Connection_. + if (OfferedExtensions == null + || !OfferedExtensions.Any() + || EnabledExtensions == null + || !EnabledExtensions.Any()) + { + throw new WebSocketHandshakeException( + $"Negotiate extension with remote [{RemoteEndPoint}] failed due to no extension enabled."); + } + + // Note that the order of extensions is significant. Any interactions + // between multiple extensions MAY be defined in the documents defining + // the extensions. In the absence of such definitions, the + // interpretation is that the header fields listed by the client in its + // request represent a preference of the header fields it wishes to use, + // with the first options listed being most preferable. The extensions + // listed by the server in response represent the extensions actually in + // use for the connection. Should the extensions modify the data and/or + // framing, the order of operations on the data should be assumed to be + // the same as the order in which the extensions are listed in the + // server's response in the opening handshake. + // For example, if there are two extensions "foo" and "bar" and if the + // header field |Sec-WebSocket-Extensions| sent by the server has the + // value "foo, bar", then operations on the data will be made as + // bar(foo(data)), be those changes to the data itself (such as + // compression) or changes to the framing that may "stack". + var agreedExtensions = new SortedList(); + var suggestedExtensions = string.Join(",", extensions).Split(',') + .Select(p => p.TrimStart().TrimEnd()).Where(p => !string.IsNullOrWhiteSpace(p)); + + var order = 0; + foreach (var extension in suggestedExtensions) + { + order++; + + var offeredExtensionName = extension.Split(';').First(); + + // Extensions not listed by the client MUST NOT be listed. + if (!EnabledExtensions.ContainsKey(offeredExtensionName)) + { + throw new WebSocketHandshakeException(string.Format( + "Negotiate extension with remote [{0}] failed due to un-enabled extensions [{1}].", + RemoteEndPoint, offeredExtensionName)); } - if (dispatcher == null) + var extensionNegotiator = EnabledExtensions[offeredExtensionName]; + + string invalidParameter; + IWebSocketExtension negotiatedExtension; + if (!extensionNegotiator.NegotiateAsClient(extension, out invalidParameter, out negotiatedExtension) + || !string.IsNullOrEmpty(invalidParameter) + || negotiatedExtension == null) { - throw new ArgumentNullException("dispatcher"); + throw new WebSocketHandshakeException(string.Format( + "Negotiate extension with remote [{0}] failed due to extension [{1}] has invalid parameter [{2}].", + RemoteEndPoint, extension, invalidParameter)); } - if (!Consts.WebSocketSchemes.Contains(uri.Scheme.ToLowerInvariant())) - { - throw new NotSupportedException( - string.Format("Not support the specified scheme [{0}].", uri.Scheme)); - } + agreedExtensions.Add(order, negotiatedExtension); + } - _uri = uri; - _remoteEndPoint = ResolveRemoteEndPoint(_uri); - _dispatcher = dispatcher; - _configuration = configuration ?? new AsyncWebSocketClientConfiguration(); - _sslEnabled = uri.Scheme.ToLowerInvariant() == "wss"; - - if (_configuration.BufferManager == null) + // If a server gives an invalid response, such as accepting a PMCE that + // the client did not offer, the client MUST _Fail the WebSocket Connection_. + foreach (var extension in agreedExtensions.Values) + { + if (!OfferedExtensions.Any(x => x.ExtensionNegotiationOffer.StartsWith(extension.Name))) { - throw new InvalidProgramException("The buffer manager in configuration cannot be null."); + throw new WebSocketHandshakeException(string.Format( + "Negotiate extension with remote [{0}] failed due to extension [{1}] not be offered.", + RemoteEndPoint, extension.Name)); } } - public AsyncWebSocketClient(Uri uri, - Func onServerTextReceived = null, - Func onServerBinaryReceived = null, - Func onServerConnected = null, - Func onServerDisconnected = null, - AsyncWebSocketClientConfiguration configuration = null) - : this(uri, - new InternalAsyncWebSocketClientMessageDispatcherImplementation( - onServerTextReceived, onServerBinaryReceived, onServerConnected, onServerDisconnected), - configuration) + // A server MUST NOT accept a PMCE extension negotiation offer together + // with another extension if the PMCE will conflict with the extension + // on their use of the RSV1 bit. A client that received a response + // accepting a PMCE extension negotiation offer together with such an + // extension MUST _Fail the WebSocket Connection_. + var isRsv1BitOccupied = false; + var isRsv2BitOccupied = false; + var isRsv3BitOccupied = false; + foreach (var extension in agreedExtensions.Values) { - } - - public AsyncWebSocketClient(Uri uri, - Func onServerTextReceived = null, - Func onServerBinaryReceived = null, - Func onServerConnected = null, - Func onServerDisconnected = null, - Func onServerFragmentationStreamOpened = null, - Func onServerFragmentationStreamContinued = null, - Func onServerFragmentationStreamClosed = null, - AsyncWebSocketClientConfiguration configuration = null) - : this(uri, - new InternalAsyncWebSocketClientMessageDispatcherImplementation( - onServerTextReceived, onServerBinaryReceived, onServerConnected, onServerDisconnected, - onServerFragmentationStreamOpened, onServerFragmentationStreamContinued, onServerFragmentationStreamClosed), - configuration) - { - } - - private IPEndPoint ResolveRemoteEndPoint(Uri uri) - { - var host = uri.Host; - var port = uri.Port > 0 ? uri.Port : uri.Scheme.ToLowerInvariant() == "wss" ? 443 : 80; - - IPAddress ipAddress; - if (IPAddress.TryParse(host, out ipAddress)) + if ((isRsv1BitOccupied && extension.Rsv1BitOccupied) + || (isRsv2BitOccupied && extension.Rsv2BitOccupied) + || (isRsv3BitOccupied && extension.Rsv3BitOccupied)) { - return new IPEndPoint(ipAddress, port); - } - else - { - if (host.ToLowerInvariant() == "localhost") - { - return new IPEndPoint(IPAddress.Parse(@"127.0.0.1"), port); - } - else - { - IPAddress[] addresses = Dns.GetHostAddresses(host); - if (addresses.Length > 0) - { - return new IPEndPoint(addresses[0], port); - } - else - { - throw new InvalidOperationException( - string.Format("Cannot resolve host [{0}] by DNS.", host)); - } - } + throw new WebSocketHandshakeException( + $"Negotiate extension with remote [{RemoteEndPoint}] failed due to conflict bit occupied."); } + + isRsv1BitOccupied = isRsv1BitOccupied | extension.Rsv1BitOccupied; + isRsv2BitOccupied = isRsv2BitOccupied | extension.Rsv2BitOccupied; + isRsv3BitOccupied = isRsv3BitOccupied | extension.Rsv3BitOccupied; } - #endregion + _frameBuilder.NegotiatedExtensions = agreedExtensions; + } - #region Properties - - private bool Connected { get { return _tcpClient != null && _tcpClient.Client.Connected; } } - public IPEndPoint RemoteEndPoint { get { return Connected ? (IPEndPoint)_tcpClient.Client.RemoteEndPoint : _remoteEndPoint; } } - public IPEndPoint LocalEndPoint { get { return Connected ? (IPEndPoint)_tcpClient.Client.LocalEndPoint : null; } } - - public Uri Uri { get { return _uri; } } - - public TimeSpan ConnectTimeout { get { return _configuration.ConnectTimeout; } } - public TimeSpan CloseTimeout { get { return _configuration.CloseTimeout; } } - public TimeSpan KeepAliveInterval { get { return _configuration.KeepAliveInterval; } } - public TimeSpan KeepAliveTimeout { get { return _configuration.KeepAliveTimeout; } } - - public IDictionary EnabledExtensions { get { return _configuration.EnabledExtensions; } } - public IDictionary EnabledSubProtocols { get { return _configuration.EnabledSubProtocols; } } - public IEnumerable OfferedExtensions { get { return _configuration.OfferedExtensions; } } - public IEnumerable RequestedSubProtocols { get { return _configuration.RequestedSubProtocols; } } - - public WebSocketState State + internal void UseSubProtocol(string protocol) + { + if (string.IsNullOrWhiteSpace(protocol)) { - get - { - switch (_state) - { - case _none: - return WebSocketState.None; - case _connecting: - return WebSocketState.Connecting; - case _connected: - return WebSocketState.Open; - case _closing: - return WebSocketState.Closing; - case _closed: - return WebSocketState.Closed; - default: - return WebSocketState.Closed; - } - } + throw new ArgumentNullException("protocol"); } - public override string ToString() + if (RequestedSubProtocols == null + || !RequestedSubProtocols.Any() + || EnabledSubProtocols == null + || !EnabledSubProtocols.Any()) { - return string.Format("RemoteEndPoint[{0}], LocalEndPoint[{1}]", - this.RemoteEndPoint, this.LocalEndPoint); + throw new WebSocketHandshakeException(string.Format( + "Negotiate sub-protocol with remote [{0}] failed due to sub-protocol [{1}] is not enabled.", + RemoteEndPoint, protocol)); } - #endregion + var requestedSubProtocols = string.Join(",", RequestedSubProtocols.Select(s => s.RequestedSubProtocol)) + .Split(',').Select(p => p.TrimStart().TrimEnd()).Where(p => !string.IsNullOrWhiteSpace(p)); - #region Connect - - public async Task Connect() + if (!requestedSubProtocols.Contains(protocol)) { - int origin = Interlocked.Exchange(ref _state, _connecting); - if (!(origin == _none || origin == _closed)) + throw new WebSocketHandshakeException(string.Format( + "Negotiate sub-protocol with remote [{0}] failed due to sub-protocol [{1}] has not been requested.", + RemoteEndPoint, protocol)); + } + + // format : name.version.parameter + var segements = protocol.Split('.') + .Select(p => p.TrimStart().TrimEnd()).Where(p => !string.IsNullOrWhiteSpace(p)) + .ToArray(); + var protocolName = segements[0]; + var protocolVersion = segements.Length > 1 ? segements[1] : null; + var protocolParameter = segements.Length > 2 ? segements[2] : null; + + if (!EnabledSubProtocols.ContainsKey(protocolName)) + { + throw new WebSocketHandshakeException(string.Format( + "Negotiate sub-protocol with remote [{0}] failed due to sub-protocol [{1}] is not enabled.", + RemoteEndPoint, protocolName)); + } + + var subProtocolNegotiator = EnabledSubProtocols[protocolName]; + + string invalidParameter; + IWebSocketSubProtocol negotiatedSubProtocol; + if (!subProtocolNegotiator.NegotiateAsClient(protocolName, protocolVersion, protocolParameter, + out invalidParameter, out negotiatedSubProtocol) + || !string.IsNullOrEmpty(invalidParameter) + || negotiatedSubProtocol == null) + { + throw new WebSocketHandshakeException(string.Format( + "Negotiate sub-protocol with remote [{0}] failed due to sub-protocol [{1}] has invalid parameter [{2}].", + RemoteEndPoint, protocol, invalidParameter)); + } + } + + private IPEndPoint ResolveRemoteEndPoint(Uri uri) + { + var host = uri.Host; + var port = uri.Port > 0 ? uri.Port : uri.Scheme.ToLowerInvariant() == "wss" ? 443 : 80; + + IPAddress ipAddress; + if (IPAddress.TryParse(host, out ipAddress)) + { + return new IPEndPoint(ipAddress, port); + } + + if (host.ToLowerInvariant() == "localhost") + { + return new IPEndPoint(IPAddress.Parse(@"127.0.0.1"), port); + } + + var addresses = Dns.GetHostAddresses(host); + if (addresses.Length > 0) + { + return new IPEndPoint(addresses[0], port); + } + + throw new InvalidOperationException( + $"Cannot resolve host [{host}] by DNS."); + } + + public override string ToString() + { + return string.Format("RemoteEndPoint[{0}], LocalEndPoint[{1}]", + RemoteEndPoint, LocalEndPoint); + } + + public async Task ConnectAsync() + { + var origin = Interlocked.Exchange(ref _state, _connecting); + if (!(origin == _none || origin == _closed)) + { + await InternalClose(false); + throw new InvalidOperationException("This websocket client is in invalid state when connecting."); + } + + try + { + Clean(); // forcefully clean all things + ResetKeepAlive(); + + _tcpClient = new TcpClient(_remoteEndPoint.Address.AddressFamily); + + var awaiter = _tcpClient.ConnectAsync(_remoteEndPoint.Address, _remoteEndPoint.Port); + if (!awaiter.Wait(ConnectTimeout)) { await InternalClose(false); - throw new InvalidOperationException("This websocket client is in invalid state when connecting."); + throw new TimeoutException($"Connect to [{_remoteEndPoint}] timeout [{ConnectTimeout}]."); } + ConfigureClient(); + var negotiator = NegotiateStream(_tcpClient.GetStream()); + if (!negotiator.Wait(ConnectTimeout)) + { + await InternalClose(false); + throw new TimeoutException( + $"Negotiate SSL/TSL with remote [{RemoteEndPoint}] timeout [{ConnectTimeout}]."); + } + + _stream = negotiator.Result; + + _receiveBuffer = _configuration.BufferManager.BorrowBuffer(); + _receiveBufferOffset = 0; + + var handshaker = OpenHandshake(); + if (!handshaker.Wait(ConnectTimeout)) + { + await CloseAsync(WebSocketCloseCode.ProtocolError, "Opening handshake timeout."); + throw new TimeoutException($"Handshake with remote [{RemoteEndPoint}] timeout [{ConnectTimeout}]."); + } + + if (!handshaker.Result) + { + await CloseAsync(WebSocketCloseCode.ProtocolError, "Opening handshake failed."); + throw new WebSocketException($"Handshake with remote [{RemoteEndPoint}] failed."); + } + + if (Interlocked.CompareExchange(ref _state, _connected, _connecting) != _connecting) + { + await InternalClose(false); + throw new InvalidOperationException("This websocket client is in invalid state when connected."); + } + + NetworkHelper.Logger.Debug( + $"Connected to server [{RemoteEndPoint}] with dispatcher [{_dispatcher.GetType().Name}] on [{DateTime.UtcNow.ToString(@"yyyy-MM-dd HH:mm:ss.fffffff")}]."); + + var isErrorOccurredInUserSide = false; try { - Clean(); // forcefully clean all things - ResetKeepAlive(); - - _tcpClient = new TcpClient(_remoteEndPoint.Address.AddressFamily); - - var awaiter = _tcpClient.ConnectAsync(_remoteEndPoint.Address, _remoteEndPoint.Port); - if (!awaiter.Wait(ConnectTimeout)) - { - await InternalClose(false); - throw new TimeoutException(string.Format( - "Connect to [{0}] timeout [{1}].", _remoteEndPoint, ConnectTimeout)); - } - - ConfigureClient(); - var negotiator = NegotiateStream(_tcpClient.GetStream()); - if (!negotiator.Wait(ConnectTimeout)) - { - await InternalClose(false); - throw new TimeoutException(string.Format( - "Negotiate SSL/TSL with remote [{0}] timeout [{1}].", RemoteEndPoint, ConnectTimeout)); - } - _stream = negotiator.Result; - - _receiveBuffer = _configuration.BufferManager.BorrowBuffer(); - _receiveBufferOffset = 0; - - var handshaker = OpenHandshake(); - if (!handshaker.Wait(ConnectTimeout)) - { - await Close(WebSocketCloseCode.ProtocolError, "Opening handshake timeout."); - throw new TimeoutException(string.Format( - "Handshake with remote [{0}] timeout [{1}].", RemoteEndPoint, ConnectTimeout)); - } - if (!handshaker.Result) - { - await Close(WebSocketCloseCode.ProtocolError, "Opening handshake failed."); - throw new WebSocketException(string.Format( - "Handshake with remote [{0}] failed.", RemoteEndPoint)); - } - - if (Interlocked.CompareExchange(ref _state, _connected, _connecting) != _connecting) - { - await InternalClose(false); - throw new InvalidOperationException("This websocket client is in invalid state when connected."); - } - - NetworkHelper.Logger.Debug($"Connected to server [{this.RemoteEndPoint}] with dispatcher [{_dispatcher.GetType().Name}] on [{DateTime.UtcNow.ToString(@"yyyy-MM-dd HH:mm:ss.fffffff")}]."); - - bool isErrorOccurredInUserSide = false; - try - { - await _dispatcher.OnServerConnected(this); - } - catch (Exception ex) - { - isErrorOccurredInUserSide = true; - await HandleUserSideError(ex); - } - - if (!isErrorOccurredInUserSide) - { - Task.Factory.StartNew(async () => - { - _keepAliveTracker.StartTimer(); - await Process(); - }, - TaskCreationOptions.LongRunning) - .Forget(); - } - else - { - await InternalClose(true); // user side handle tcp connection error occurred - } + await _dispatcher.OnServerConnected(this); } catch (Exception ex) { - NetworkHelper.Logger.Error(ex.FormatExceptionToMessage()); - throw; - } - } - - private void ConfigureClient() - { - _tcpClient.ReceiveBufferSize = _configuration.ReceiveBufferSize; - _tcpClient.SendBufferSize = _configuration.SendBufferSize; - _tcpClient.ReceiveTimeout = (int)_configuration.ReceiveTimeout.TotalMilliseconds; - _tcpClient.SendTimeout = (int)_configuration.SendTimeout.TotalMilliseconds; - _tcpClient.NoDelay = _configuration.NoDelay; - _tcpClient.LingerState = _configuration.LingerState; - } - - private async Task NegotiateStream(Stream stream) - { - if (!_sslEnabled) - { - return stream; + isErrorOccurredInUserSide = true; + await HandleUserSideError(ex); } - var validateRemoteCertificate = new RemoteCertificateValidationCallback( - (object sender, - X509Certificate certificate, - X509Chain chain, - SslPolicyErrors sslPolicyErrors) - => - { - if (sslPolicyErrors == SslPolicyErrors.None) - { - return true; - } - - if (_configuration.SslPolicyErrorsBypassed) - { - return true; - } - else - { - NetworkHelper.Logger.Error($"Error occurred when validating remote certificate: [{this.RemoteEndPoint}], [{sslPolicyErrors}]."); - } - - return false; - }); - - var sslStream = new SslStream( - stream, - false, - validateRemoteCertificate, - null, - _configuration.SslEncryptionPolicy); - - if (_configuration.SslClientCertificates == null || _configuration.SslClientCertificates.Count == 0) + if (!isErrorOccurredInUserSide) { - await sslStream.AuthenticateAsClientAsync( // No client certificates are used in the authentication. The certificate revocation list is not checked during authentication. - _configuration.SslTargetHost); // The name of the server that will share this SslStream. The value specified for targetHost must match the name on the server's certificate. + Task.Factory.StartNew(async () => + { + _keepAliveTracker.StartTimer(); + await Process(); + }, + TaskCreationOptions.LongRunning) + .Forget(); } else { - await sslStream.AuthenticateAsClientAsync( - _configuration.SslTargetHost, // The name of the server that will share this SslStream. The value specified for targetHost must match the name on the server's certificate. - _configuration.SslClientCertificates, // The X509CertificateCollection that contains client certificates. - _configuration.SslEnabledProtocols, // The SslProtocols value that represents the protocol used for authentication. - _configuration.SslCheckCertificateRevocation); // A Boolean value that specifies whether the certificate revocation list is checked during authentication. + await InternalClose(true); // user side handle tcp connection error occurred } + } + catch (Exception ex) + { + NetworkHelper.Logger.Error(ex.FormatExceptionToMessage()); + throw; + } + } - // When authentication succeeds, you must check the IsEncrypted and IsSigned properties - // to determine what security services are used by the SslStream. - // Check the IsMutuallyAuthenticated property to determine whether mutual authentication occurred. - NetworkHelper.Logger.Debug(string.Format( - "Ssl Stream: SslProtocol[{0}], IsServer[{1}], IsAuthenticated[{2}], IsEncrypted[{3}], IsSigned[{4}], IsMutuallyAuthenticated[{5}], " - + "HashAlgorithm[{6}], HashStrength[{7}], KeyExchangeAlgorithm[{8}], KeyExchangeStrength[{9}], CipherAlgorithm[{10}], CipherStrength[{11}].", - sslStream.SslProtocol, - sslStream.IsServer, - sslStream.IsAuthenticated, - sslStream.IsEncrypted, - sslStream.IsSigned, - sslStream.IsMutuallyAuthenticated, - sslStream.HashAlgorithm, - sslStream.HashStrength, - sslStream.KeyExchangeAlgorithm, - sslStream.KeyExchangeStrength, - sslStream.CipherAlgorithm, - sslStream.CipherStrength)); + private void ConfigureClient() + { + _tcpClient.ReceiveBufferSize = _configuration.ReceiveBufferSize; + _tcpClient.SendBufferSize = _configuration.SendBufferSize; + _tcpClient.ReceiveTimeout = (int)_configuration.ReceiveTimeout.TotalMilliseconds; + _tcpClient.SendTimeout = (int)_configuration.SendTimeout.TotalMilliseconds; + _tcpClient.NoDelay = _configuration.NoDelay; + _tcpClient.LingerState = _configuration.LingerState; + } - return sslStream; + private async Task NegotiateStream(Stream stream) + { + if (!_sslEnabled) + { + return stream; } - private async Task OpenHandshake() - { - bool handshakeResult = false; - - try + var validateRemoteCertificate = new RemoteCertificateValidationCallback( + (sender, certificate, chain, sslPolicyErrors) + => { - var requestBuffer = WebSocketClientHandshaker.CreateOpenningHandshakeRequest(this, out _secWebSocketKey); - await _stream.WriteAsync(requestBuffer, 0, requestBuffer.Length); - - int terminatorIndex = -1; - while (!WebSocketHelpers.FindHttpMessageTerminator(_receiveBuffer.Array, _receiveBuffer.Offset, _receiveBufferOffset, out terminatorIndex)) + if (sslPolicyErrors == SslPolicyErrors.None) { - int receiveCount = await _stream.ReadAsync( - _receiveBuffer.Array, - _receiveBuffer.Offset + _receiveBufferOffset, - _receiveBuffer.Count - _receiveBufferOffset); - if (receiveCount == 0) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to receive zero bytes.", RemoteEndPoint)); - } - - SegmentBufferDeflector.ReplaceBuffer(_configuration.BufferManager, ref _receiveBuffer, ref _receiveBufferOffset, receiveCount); - - if (_receiveBufferOffset > 2048) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to receive weird stream.", RemoteEndPoint)); - } + return true; } - handshakeResult = WebSocketClientHandshaker.VerifyOpenningHandshakeResponse( - this, - _receiveBuffer.Array, - _receiveBuffer.Offset, - terminatorIndex + Consts.HttpMessageTerminator.Length, - _secWebSocketKey); - - SegmentBufferDeflector.ShiftBuffer( - _configuration.BufferManager, - terminatorIndex + Consts.HttpMessageTerminator.Length, - ref _receiveBuffer, - ref _receiveBufferOffset); - } - catch (WebSocketHandshakeException ex) - { - NetworkHelper.Logger.Error(ex.FormatExceptionToMessage()); - handshakeResult = false; - } - catch (Exception) - { - handshakeResult = false; - throw; - } - - return handshakeResult; - } - - private void ResetKeepAlive() - { - _keepAliveTracker = KeepAliveTracker.Create(KeepAliveInterval, new TimerCallback((s) => OnKeepAlive())); - _keepAliveTimeoutTimer = new Timer(new TimerCallback((s) => OnKeepAliveTimeout()), null, Timeout.Infinite, Timeout.Infinite); - _closingTimeoutTimer = new Timer(new TimerCallback((s) => OnCloseTimeout()), null, Timeout.Infinite, Timeout.Infinite); - } - - #endregion - - #region Process - - private async Task Process() - { - try - { - Header frameHeader; - byte[] payload; - int payloadOffset; - int payloadCount; - int consumedLength = 0; - - while (State == WebSocketState.Open || State == WebSocketState.Closing) + if (_configuration.SslPolicyErrorsBypassed) { - int receiveCount = await _stream.ReadAsync( - _receiveBuffer.Array, - _receiveBuffer.Offset + _receiveBufferOffset, - _receiveBuffer.Count - _receiveBufferOffset); - if (receiveCount == 0) - { - break; - } + return true; + } - _keepAliveTracker.OnDataReceived(); - SegmentBufferDeflector.ReplaceBuffer(_configuration.BufferManager, ref _receiveBuffer, ref _receiveBufferOffset, receiveCount); - consumedLength = 0; + NetworkHelper.Logger.Error( + $"Error occurred when validating remote certificate: [{RemoteEndPoint}], [{sslPolicyErrors}]."); - while (true) - { - frameHeader = null; - payload = null; - payloadOffset = 0; - payloadCount = 0; + return false; + }); - if (_frameBuilder.TryDecodeFrameHeader( + var sslStream = new SslStream( + stream, + false, + validateRemoteCertificate, + null, + _configuration.SslEncryptionPolicy); + + if (_configuration.SslClientCertificates == null || _configuration.SslClientCertificates.Count == 0) + { + await + sslStream + .AuthenticateAsClientAsync( // No client certificates are used in the authentication. The certificate revocation list is not checked during authentication. + _configuration + .SslTargetHost); // The name of the server that will share this SslStream. The value specified for targetHost must match the name on the server's certificate. + } + else + { + await sslStream.AuthenticateAsClientAsync( + _configuration + .SslTargetHost, // The name of the server that will share this SslStream. The value specified for targetHost must match the name on the server's certificate. + _configuration + .SslClientCertificates, // The X509CertificateCollection that contains client certificates. + _configuration + .SslEnabledProtocols, // The SslProtocols value that represents the protocol used for authentication. + _configuration + .SslCheckCertificateRevocation); // A Boolean value that specifies whether the certificate revocation list is checked during authentication. + } + + // When authentication succeeds, you must check the IsEncrypted and IsSigned properties + // to determine what security services are used by the SslStream. + // Check the IsMutuallyAuthenticated property to determine whether mutual authentication occurred. + NetworkHelper.Logger.Debug(string.Format( + "Ssl Stream: SslProtocol[{0}], IsServer[{1}], IsAuthenticated[{2}], IsEncrypted[{3}], IsSigned[{4}], IsMutuallyAuthenticated[{5}], " + + "HashAlgorithm[{6}], HashStrength[{7}], KeyExchangeAlgorithm[{8}], KeyExchangeStrength[{9}], CipherAlgorithm[{10}], CipherStrength[{11}].", + sslStream.SslProtocol, + sslStream.IsServer, + sslStream.IsAuthenticated, + sslStream.IsEncrypted, + sslStream.IsSigned, + sslStream.IsMutuallyAuthenticated, + sslStream.HashAlgorithm, + sslStream.HashStrength, + sslStream.KeyExchangeAlgorithm, + sslStream.KeyExchangeStrength, + sslStream.CipherAlgorithm, + sslStream.CipherStrength)); + + return sslStream; + } + + private async Task OpenHandshake() + { + var handshakeResult = false; + + try + { + var requestBuffer = WebSocketClientHandshaker.CreateOpenningHandshakeRequest(this, out _secWebSocketKey); + await _stream.WriteAsync(requestBuffer, 0, requestBuffer.Length); + + var terminatorIndex = -1; + while (!WebSocketHelpers.FindHttpMessageTerminator(_receiveBuffer.Array, _receiveBuffer.Offset, + _receiveBufferOffset, out terminatorIndex)) + { + var receiveCount = await _stream.ReadAsync( + _receiveBuffer.Array, + _receiveBuffer.Offset + _receiveBufferOffset, + _receiveBuffer.Count - _receiveBufferOffset); + if (receiveCount == 0) + { + throw new WebSocketHandshakeException( + $"Handshake with remote [{RemoteEndPoint}] failed due to receive zero bytes."); + } + + SegmentBufferDeflector.ReplaceBuffer(_configuration.BufferManager, ref _receiveBuffer, + ref _receiveBufferOffset, receiveCount); + + if (_receiveBufferOffset > 2048) + { + throw new WebSocketHandshakeException( + $"Handshake with remote [{RemoteEndPoint}] failed due to receive weird stream."); + } + } + + handshakeResult = WebSocketClientHandshaker.VerifyOpenningHandshakeResponse( + this, + _receiveBuffer.Array, + _receiveBuffer.Offset, + terminatorIndex + Consts.HttpMessageTerminator.Length, + _secWebSocketKey); + + SegmentBufferDeflector.ShiftBuffer( + _configuration.BufferManager, + terminatorIndex + Consts.HttpMessageTerminator.Length, + ref _receiveBuffer, + ref _receiveBufferOffset); + } + catch (WebSocketHandshakeException ex) + { + NetworkHelper.Logger.Error(ex.FormatExceptionToMessage()); + handshakeResult = false; + } + catch (Exception) + { + handshakeResult = false; + throw; + } + + return handshakeResult; + } + + private void ResetKeepAlive() + { + _keepAliveTracker = KeepAliveTracker.Create(KeepAliveInterval, s => OnKeepAlive()); + _keepAliveTimeoutTimer = new Timer(s => OnKeepAliveTimeout(), null, Timeout.Infinite, Timeout.Infinite); + _closingTimeoutTimer = new Timer(s => OnCloseTimeout(), null, Timeout.Infinite, Timeout.Infinite); + } + + private async Task Process() + { + try + { + Header frameHeader; + byte[] payload; + int payloadOffset; + int payloadCount; + var consumedLength = 0; + + while (State == WebSocketState.Open || State == WebSocketState.Closing) + { + var receiveCount = await _stream.ReadAsync( + _receiveBuffer.Array, + _receiveBuffer.Offset + _receiveBufferOffset, + _receiveBuffer.Count - _receiveBufferOffset); + if (receiveCount == 0) + { + break; + } + + _keepAliveTracker.OnDataReceived(); + SegmentBufferDeflector.ReplaceBuffer(_configuration.BufferManager, ref _receiveBuffer, + ref _receiveBufferOffset, receiveCount); + consumedLength = 0; + + while (true) + { + frameHeader = null; + payload = null; + payloadOffset = 0; + payloadCount = 0; + + if (_frameBuilder.TryDecodeFrameHeader( _receiveBuffer.Array, _receiveBuffer.Offset + consumedLength, _receiveBufferOffset - consumedLength, out frameHeader) - && frameHeader.Length + frameHeader.PayloadLength <= _receiveBufferOffset - consumedLength) - { - try - { - if (frameHeader.IsMasked) - { - await Close(WebSocketCloseCode.ProtocolError, "A client MUST close a connection if it detects a masked frame."); - throw new WebSocketException(string.Format( - "Client received masked frame [{0}] from remote [{1}].", frameHeader.OpCode, RemoteEndPoint)); - } - - _frameBuilder.DecodePayload( - _receiveBuffer.Array, - _receiveBuffer.Offset + consumedLength, - frameHeader, - out payload, out payloadOffset, out payloadCount); - - switch (frameHeader.OpCode) - { - case OpCode.Continuation: - await HandleContinuationFrame(frameHeader, payload, payloadOffset, payloadCount); - break; - case OpCode.Text: - await HandleTextFrame(frameHeader, payload, payloadOffset, payloadCount); - break; - case OpCode.Binary: - await HandleBinaryFrame(frameHeader, payload, payloadOffset, payloadCount); - break; - case OpCode.Close: - await HandleCloseFrame(frameHeader, payload, payloadOffset, payloadCount); - break; - case OpCode.Ping: - await HandlePingFrame(frameHeader, payload, payloadOffset, payloadCount); - break; - case OpCode.Pong: - await HandlePongFrame(frameHeader, payload, payloadOffset, payloadCount); - break; - default: - { - // Incoming data MUST always be validated by both clients and servers. - // If, at any time, an endpoint is faced with data that it does not - // understand or that violates some criteria by which the endpoint - // determines safety of input, or when the endpoint sees an opening - // handshake that does not correspond to the values it is expecting - // (e.g., incorrect path or origin in the client request), the endpoint - // MAY drop the TCP connection. If the invalid data was received after - // a successful WebSocket handshake, the endpoint SHOULD send a Close - // frame with an appropriate status code (Section 7.4) before proceeding - // to _Close the WebSocket Connection_. Use of a Close frame with an - // appropriate status code can help in diagnosing the problem. If the - // invalid data is sent during the WebSocket handshake, the server - // SHOULD return an appropriate HTTP [RFC2616] status code. - await Close(WebSocketCloseCode.InvalidMessageType); - throw new NotSupportedException( - string.Format("Not support received opcode [{0}].", (byte)frameHeader.OpCode)); - } - } - } - catch (Exception ex) - { - NetworkHelper.Logger.Error(ex.FormatExceptionToMessage()); - throw; - } - finally - { - consumedLength += frameHeader.Length + frameHeader.PayloadLength; - } - } - else - { - break; - } - } - - if (_receiveBuffer != null && _receiveBuffer.Array != null) + && frameHeader.Length + frameHeader.PayloadLength <= _receiveBufferOffset - consumedLength) { - SegmentBufferDeflector.ShiftBuffer(_configuration.BufferManager, consumedLength, ref _receiveBuffer, ref _receiveBufferOffset); - } - } - } - catch (ObjectDisposedException) - { - // looking forward to a graceful quit from the ReadAsync but the inside EndRead will raise the ObjectDisposedException, - // so a gracefully close for the socket should be a Shutdown, but we cannot avoid the Close triggers this happen. - } - catch (Exception ex) - { - await HandleReceiveOperationException(ex); - } - finally - { - await InternalClose(true); // read async buffer returned, remote notifies closed - } - } - - private async Task HandleContinuationFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) - { - if (!frameHeader.IsFIN) - { - try - { - await _dispatcher.OnServerFragmentationStreamContinued(this, payload, payloadOffset, payloadCount); - } - catch (Exception ex) - { - await HandleUserSideError(ex); - } - } - else - { - try - { - await _dispatcher.OnServerFragmentationStreamClosed(this, payload, payloadOffset, payloadCount); - } - catch (Exception ex) - { - await HandleUserSideError(ex); - } - } - } - - private async Task HandleTextFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) - { - if (frameHeader.IsFIN) - { - try - { - var text = Encoding.UTF8.GetString(payload, payloadOffset, payloadCount); - await _dispatcher.OnServerTextReceived(this, text); - } - catch (Exception ex) - { - await HandleUserSideError(ex); - } - } - else - { - try - { - await _dispatcher.OnServerFragmentationStreamOpened(this, payload, payloadOffset, payloadCount); - } - catch (Exception ex) - { - await HandleUserSideError(ex); - } - } - } - - private async Task HandleBinaryFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) - { - if (frameHeader.IsFIN) - { - try - { - await _dispatcher.OnServerBinaryReceived(this, payload, payloadOffset, payloadCount); - } - catch (Exception ex) - { - await HandleUserSideError(ex); - } - } - else - { - try - { - await _dispatcher.OnServerFragmentationStreamOpened(this, payload, payloadOffset, payloadCount); - } - catch (Exception ex) - { - await HandleUserSideError(ex); - } - } - } - - private async Task HandleCloseFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) - { - if (!frameHeader.IsFIN) - { - throw new WebSocketException(string.Format( - "Client received unfinished frame [{0}] from remote [{1}].", frameHeader.OpCode, RemoteEndPoint)); - } - - if (payloadCount > 1) - { - var statusCode = payload[payloadOffset + 0] * 256 + payload[payloadOffset + 1]; - var closeCode = (WebSocketCloseCode)statusCode; - var closeReason = string.Empty; - - if (payloadCount > 2) - { - closeReason = Encoding.UTF8.GetString(payload, payloadOffset + 2, payloadCount - 2); - } -#if DEBUG - NetworkHelper.Logger.Debug($"Receive server side close frame [{closeCode}] [{closeReason}]."); -#endif - // If an endpoint receives a Close frame and did not previously send a - // Close frame, the endpoint MUST send a Close frame in response. (When - // sending a Close frame in response, the endpoint typically echos the - // status code it received.) It SHOULD do so as soon as practical. - await Close(closeCode, closeReason); - } - else - { -#if DEBUG - NetworkHelper.Logger.Debug($"Receive server side close frame but no status code."); -#endif - await Close(WebSocketCloseCode.InvalidPayloadData); - } - } - - private async Task HandlePingFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) - { - if (!frameHeader.IsFIN) - { - throw new WebSocketException(string.Format( - "Client received unfinished frame [{0}] from remote [{1}].", frameHeader.OpCode, RemoteEndPoint)); - } - - // Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in - // response, unless it already received a Close frame. It SHOULD - // respond with Pong frame as soon as is practical. Pong frames are - // discussed in Section 5.5.3. - // - // An endpoint MAY send a Ping frame any time after the connection is - // established and before the connection is closed. - // - // A Ping frame may serve either as a keep-alive or as a means to - // verify that the remote endpoint is still responsive. - var ping = Encoding.UTF8.GetString(payload, payloadOffset, payloadCount); -#if DEBUG - NetworkHelper.Logger.Debug($"Receive server side ping frame [{ping}]."); -#endif - if (State == WebSocketState.Open) - { - // A Pong frame sent in response to a Ping frame must have identical - // "Application data" as found in the message body of the Ping frame being replied to. - var pong = new PongFrame(ping).ToArray(_frameBuilder); - await SendFrame(pong); -#if DEBUG - NetworkHelper.Logger.Debug($"Send client side pong frame [{ping}]."); -#endif - } - } - - private async Task HandlePongFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) - { - if (!frameHeader.IsFIN) - { - throw new WebSocketException(string.Format( - "Client received unfinished frame [{0}] from remote [{1}].", frameHeader.OpCode, RemoteEndPoint)); - } - - // If an endpoint receives a Ping frame and has not yet sent Pong - // frame(s) in response to previous Ping frame(s), the endpoint MAY - // elect to send a Pong frame for only the most recently processed Ping frame. - // - // A Pong frame MAY be sent unsolicited. This serves as a - // unidirectional heartbeat. A response to an unsolicited Pong frame is not expected. - var pong = Encoding.UTF8.GetString(payload, payloadOffset, payloadCount); - StopKeepAliveTimeoutTimer(); -#if DEBUG - NetworkHelper.Logger.Debug($"Receive server side pong frame [{pong}]."); -#endif - await Task.CompletedTask; - } - - #endregion - - #region Close - - public async Task Close(WebSocketCloseCode closeCode) - { - await Close(closeCode, null); - } - - public async Task Close(WebSocketCloseCode closeCode, string closeReason) - { - if (State == WebSocketState.Closed || State == WebSocketState.None) - { - return; - } - - var priorState = Interlocked.Exchange(ref _state, _closing); - switch (priorState) - { - case _connected: - { - var closingHandshake = new CloseFrame(closeCode, closeReason).ToArray(_frameBuilder); try { - StartClosingTimer(); -#if DEBUG - NetworkHelper.Logger.Debug($"Send client side close frame [{closeCode}] [{closeReason}]."); -#endif - var awaiter = _stream.WriteAsync(closingHandshake, 0, closingHandshake.Length); - if (!awaiter.Wait(ConnectTimeout)) + if (frameHeader.IsMasked) { - await InternalClose(true); - throw new TimeoutException(string.Format( - "Closing handshake with [{0}] timeout [{1}].", _remoteEndPoint, ConnectTimeout)); + await CloseAsync(WebSocketCloseCode.ProtocolError, + "A client MUST close a connection if it detects a masked frame."); + throw new WebSocketException(string.Format( + "Client received masked frame [{0}] from remote [{1}].", frameHeader.OpCode, + RemoteEndPoint)); + } + + _frameBuilder.DecodePayload( + _receiveBuffer.Array, + _receiveBuffer.Offset + consumedLength, + frameHeader, + out payload, out payloadOffset, out payloadCount); + + switch (frameHeader.OpCode) + { + case OpCode.Continuation: + await HandleContinuationFrame(frameHeader, payload, payloadOffset, payloadCount); + break; + case OpCode.Text: + await HandleTextFrame(frameHeader, payload, payloadOffset, payloadCount); + break; + case OpCode.Binary: + await HandleBinaryFrame(frameHeader, payload, payloadOffset, payloadCount); + break; + case OpCode.Close: + await HandleCloseFrame(frameHeader, payload, payloadOffset, payloadCount); + break; + case OpCode.Ping: + await HandlePingFrame(frameHeader, payload, payloadOffset, payloadCount); + break; + case OpCode.Pong: + await HandlePongFrame(frameHeader, payload, payloadOffset, payloadCount); + break; + default: + { + // Incoming data MUST always be validated by both clients and servers. + // If, at any time, an endpoint is faced with data that it does not + // understand or that violates some criteria by which the endpoint + // determines safety of input, or when the endpoint sees an opening + // handshake that does not correspond to the values it is expecting + // (e.g., incorrect path or origin in the client request), the endpoint + // MAY drop the TCP connection. If the invalid data was received after + // a successful WebSocket handshake, the endpoint SHOULD send a Close + // frame with an appropriate status code (Section 7.4) before proceeding + // to _Close the WebSocket Connection_. Use of a Close frame with an + // appropriate status code can help in diagnosing the problem. If the + // invalid data is sent during the WebSocket handshake, the server + // SHOULD return an appropriate HTTP [RFC2616] status code. + await CloseAsync(WebSocketCloseCode.InvalidMessageType); + throw new NotSupportedException( + $"Not support received opcode [{(byte)frameHeader.OpCode}]."); + } } } catch (Exception ex) { - await HandleSendOperationException(ex); + NetworkHelper.Logger.Error(ex.FormatExceptionToMessage()); + throw; + } + finally + { + consumedLength += frameHeader.Length + frameHeader.PayloadLength; } - return; } - case _connecting: - case _closing: + else { - await InternalClose(true); - return; + break; } - case _closed: - case _none: - default: - return; - } - } - - private async Task InternalClose(bool shallNotifyUserSide) - { - if (Interlocked.Exchange(ref _state, _closed) == _closed) - { - return; - } - - Shutdown(); - - if (shallNotifyUserSide) - { - NetworkHelper.Logger.Debug(string.Format("Disconnected from server [{0}] with dispatcher [{1}] on [{2}].", - this.RemoteEndPoint, - _dispatcher.GetType().Name, - DateTime.UtcNow.ToString(@"yyyy-MM-dd HH:mm:ss.fffffff"))); - - try - { - await _dispatcher.OnServerDisconnected(this); } - catch (Exception ex) + + if (_receiveBuffer != null && _receiveBuffer.Array != null) { - await HandleUserSideError(ex); + SegmentBufferDeflector.ShiftBuffer(_configuration.BufferManager, consumedLength, ref _receiveBuffer, + ref _receiveBufferOffset); } } - - Clean(); } - - public void Shutdown() + catch (ObjectDisposedException) { - // The correct way to shut down the connection (especially if you are in a full-duplex conversation) - // is to call socket.Shutdown(SocketShutdown.Send) and give the remote party some time to close - // their send channel. This ensures that you receive any pending data instead of slamming the - // connection shut. ObjectDisposedException should never be part of the normal application flow. - if (_tcpClient != null && _tcpClient.Connected) - { - _tcpClient.Client.Shutdown(SocketShutdown.Send); - } + // looking forward to a graceful quit from the ReadAsync but the inside EndRead will raise the ObjectDisposedException, + // so a gracefully close for the socket should be a Shutdown, but we cannot avoid the Close triggers this happen. } + catch (Exception ex) + { + await HandleReceiveOperationException(ex); + } + finally + { + await InternalClose(true); // read async buffer returned, remote notifies closed + } + } - private void Clean() + private async Task HandleContinuationFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) + { + if (!frameHeader.IsFIN) { try { - try - { - if (_keepAliveTracker != null) - { - _keepAliveTracker.StopTimer(); - _keepAliveTracker.Dispose(); - } - } - catch { } - try - { - if (_keepAliveTimeoutTimer != null) - { - _keepAliveTimeoutTimer.Change(Timeout.Infinite, Timeout.Infinite); - _keepAliveTimeoutTimer.Dispose(); - } - } - catch { } - try - { - if (_closingTimeoutTimer != null) - { - _closingTimeoutTimer.Change(Timeout.Infinite, Timeout.Infinite); - _closingTimeoutTimer.Dispose(); - } - } - catch { } - try - { - if (_stream != null) - { - _stream.Dispose(); - } - } - catch { } - try - { - if (_tcpClient != null) - { - _tcpClient.Dispose(); - } - } - catch { } - } - catch { } - finally - { - _keepAliveTracker = null; - _keepAliveTimeoutTimer = null; - _closingTimeoutTimer = null; - _stream = null; - _tcpClient = null; - } - - if (_receiveBuffer != default(ArraySegment)) - { - _configuration.BufferManager.ReturnBuffer(_receiveBuffer); - } - - _receiveBuffer = default(ArraySegment); - _receiveBufferOffset = 0; - } - - public async Task Abort() - { - await InternalClose(true); - } - - private void StartClosingTimer() - { - // In abnormal cases (such as not having received a TCP Close - // from the server after a reasonable amount of time) a client MAY initiate the TCP Close. - _closingTimeoutTimer.Change((int)CloseTimeout.TotalMilliseconds, Timeout.Infinite); - } - - private async void OnCloseTimeout() - { - // After both sending and receiving a Close message, an endpoint - // considers the WebSocket connection closed and MUST close the - // underlying TCP connection. The server MUST close the underlying TCP - // connection immediately; the client SHOULD wait for the server to - // close the connection but MAY close the connection at any time after - // sending and receiving a Close message, e.g., if it has not received a - // TCP Close from the server in a reasonable time period. - NetworkHelper.Logger.Warn($"Closing timer timeout [{CloseTimeout}] then close automatically."); - await InternalClose(true); - } - - #endregion - - #region Exception Handler - - private async Task HandleSendOperationException(Exception ex) - { - if (IsSocketTimeOut(ex)) - { - await CloseIfShould(ex); - throw new WebSocketException(ex.Message, new TimeoutException(ex.Message, ex)); - } - - await CloseIfShould(ex); - throw new WebSocketException(ex.Message, ex); - } - - private async Task HandleReceiveOperationException(Exception ex) - { - if (IsSocketTimeOut(ex)) - { - await CloseIfShould(ex); - throw new WebSocketException(ex.Message, new TimeoutException(ex.Message, ex)); - } - - await CloseIfShould(ex); - throw new WebSocketException(ex.Message, ex); - } - - private bool IsSocketTimeOut(Exception ex) - { - return ex is IOException - && ex.InnerException != null - && ex.InnerException is SocketException - && (ex.InnerException as SocketException).SocketErrorCode == SocketError.TimedOut; - } - - private async Task CloseIfShould(Exception ex) - { - if (ex is ObjectDisposedException - || ex is InvalidOperationException - || ex is SocketException - || ex is IOException - || ex is NullReferenceException // buffer array operation - || ex is ArgumentException // buffer array operation - ) - { - NetworkHelper.Logger.Error(ex.FormatExceptionToMessage()); - - await InternalClose(false); // intend to close the session - - return true; - } - - return false; - } - - private async Task HandleUserSideError(Exception ex) - { - NetworkHelper.Logger.Error($"Client [{this}] error occurred in user side [{ex.Message}].{Environment.NewLine}{ex.FormatExceptionToMessage()}"); - await Task.CompletedTask; - } - - #endregion - - #region Send - - public async Task SendTextAsync(string text) - { - await SendFrame(new TextFrame(text).ToArray(_frameBuilder)); - } - - public async Task SendBinaryAsync(byte[] data) - { - await SendBinaryAsync(data, 0, data.Length); - } - - public async Task SendBinaryAsync(byte[] data, int offset, int count) - { - await SendFrame(new BinaryFrame(data, offset, count).ToArray(_frameBuilder)); - } - - public async Task SendBinaryAsync(ArraySegment segment) - { - await SendFrame(new BinaryFrame(segment).ToArray(_frameBuilder)); - } - - public async Task SendStreamAsync(Stream stream) - { - if (stream == null) - { - throw new ArgumentNullException("stream"); - } - - int fragmentLength = _configuration.ReasonableFragmentSize; - var buffer = new byte[fragmentLength]; - int readCount = 0; - - readCount = await stream.ReadAsync(buffer, 0, fragmentLength); - if (readCount == 0) - { - return; - } - - await SendFrame(new BinaryFragmentationFrame(OpCode.Binary, buffer, 0, readCount, isFin: false).ToArray(_frameBuilder)); - - while (true) - { - readCount = await stream.ReadAsync(buffer, 0, fragmentLength); - if (readCount != 0) - { - await SendFrame(new BinaryFragmentationFrame(OpCode.Continuation, buffer, 0, readCount, isFin: false).ToArray(_frameBuilder)); - } - else - { - await SendFrame(new BinaryFragmentationFrame(OpCode.Continuation, buffer, 0, 0, isFin: true).ToArray(_frameBuilder)); - break; - } - } - } - - private async Task SendFrame(byte[] frame) - { - if (frame == null) - { - throw new ArgumentNullException("frame"); - } - if (State != WebSocketState.Open) - { - throw new InvalidOperationException("This websocket client has not connected to server."); - } - - try - { - await _stream.WriteAsync(frame, 0, frame.Length); - _keepAliveTracker.OnDataSent(); + await _dispatcher.OnServerFragmentationStreamContinued(this, payload, payloadOffset, payloadCount); } catch (Exception ex) { - await HandleSendOperationException(ex); + await HandleUserSideError(ex); + } + } + else + { + try + { + await _dispatcher.OnServerFragmentationStreamClosed(this, payload, payloadOffset, payloadCount); + } + catch (Exception ex) + { + await HandleUserSideError(ex); + } + } + } + + private async Task HandleTextFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) + { + if (frameHeader.IsFIN) + { + try + { + var text = Encoding.UTF8.GetString(payload, payloadOffset, payloadCount); + await _dispatcher.OnServerTextReceived(this, text); + } + catch (Exception ex) + { + await HandleUserSideError(ex); + } + } + else + { + try + { + await _dispatcher.OnServerFragmentationStreamOpened(this, payload, payloadOffset, payloadCount); + } + catch (Exception ex) + { + await HandleUserSideError(ex); + } + } + } + + private async Task HandleBinaryFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) + { + if (frameHeader.IsFIN) + { + try + { + await _dispatcher.OnServerBinaryReceived(this, payload, payloadOffset, payloadCount); + } + catch (Exception ex) + { + await HandleUserSideError(ex); + } + } + else + { + try + { + await _dispatcher.OnServerFragmentationStreamOpened(this, payload, payloadOffset, payloadCount); + } + catch (Exception ex) + { + await HandleUserSideError(ex); + } + } + } + + private async Task HandleCloseFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) + { + if (!frameHeader.IsFIN) + { + throw new WebSocketException( + $"Client received unfinished frame [{frameHeader.OpCode}] from remote [{RemoteEndPoint}]."); + } + + if (payloadCount > 1) + { + var statusCode = payload[payloadOffset + 0] * 256 + payload[payloadOffset + 1]; + var closeCode = (WebSocketCloseCode)statusCode; + var closeReason = string.Empty; + + if (payloadCount > 2) + { + closeReason = Encoding.UTF8.GetString(payload, payloadOffset + 2, payloadCount - 2); + } +#if DEBUG + NetworkHelper.Logger.Debug($"Receive server side close frame [{closeCode}] [{closeReason}]."); +#endif + // If an endpoint receives a Close frame and did not previously send a + // Close frame, the endpoint MUST send a Close frame in response. (When + // sending a Close frame in response, the endpoint typically echos the + // status code it received.) It SHOULD do so as soon as practical. + await CloseAsync(closeCode, closeReason); + } + else + { +#if DEBUG + NetworkHelper.Logger.Debug($"Receive server side close frame but no status code."); +#endif + await CloseAsync(WebSocketCloseCode.InvalidPayloadData); + } + } + + private async Task HandlePingFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) + { + if (!frameHeader.IsFIN) + { + throw new WebSocketException( + $"Client received unfinished frame [{frameHeader.OpCode}] from remote [{RemoteEndPoint}]."); + } + + // Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in + // response, unless it already received a Close frame. It SHOULD + // respond with Pong frame as soon as is practical. Pong frames are + // discussed in Section 5.5.3. + // + // An endpoint MAY send a Ping frame any time after the connection is + // established and before the connection is closed. + // + // A Ping frame may serve either as a keep-alive or as a means to + // verify that the remote endpoint is still responsive. + var ping = Encoding.UTF8.GetString(payload, payloadOffset, payloadCount); +#if DEBUG + NetworkHelper.Logger.Debug($"Receive server side ping frame [{ping}]."); +#endif + if (State == WebSocketState.Open) + { + // A Pong frame sent in response to a Ping frame must have identical + // "Application data" as found in the message body of the Ping frame being replied to. + var pong = new PongFrame(ping).ToArray(_frameBuilder); + await SendFrame(pong); +#if DEBUG + NetworkHelper.Logger.Debug($"Send client side pong frame [{ping}]."); +#endif + } + } + + private async Task HandlePongFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) + { + if (!frameHeader.IsFIN) + { + throw new WebSocketException( + $"Client received unfinished frame [{frameHeader.OpCode}] from remote [{RemoteEndPoint}]."); + } + + // If an endpoint receives a Ping frame and has not yet sent Pong + // frame(s) in response to previous Ping frame(s), the endpoint MAY + // elect to send a Pong frame for only the most recently processed Ping frame. + // + // A Pong frame MAY be sent unsolicited. This serves as a + // unidirectional heartbeat. A response to an unsolicited Pong frame is not expected. + var pong = Encoding.UTF8.GetString(payload, payloadOffset, payloadCount); + StopKeepAliveTimeoutTimer(); +#if DEBUG + NetworkHelper.Logger.Debug($"Receive server side pong frame [{pong}]."); +#endif + await Task.CompletedTask; + } + + public async Task CloseAsync(WebSocketCloseCode closeCode = WebSocketCloseCode.NormalClosure) + { + await CloseAsync(closeCode, null); + } + + public async Task CloseAsync(WebSocketCloseCode closeCode, string closeReason) + { + if (State == WebSocketState.Closed || State == WebSocketState.None) + { + return; + } + + var priorState = Interlocked.Exchange(ref _state, _closing); + switch (priorState) + { + case _connected: + { + var closingHandshake = new CloseFrame(closeCode, closeReason).ToArray(_frameBuilder); + try + { + StartClosingTimer(); +#if DEBUG + NetworkHelper.Logger.Debug($"Send client side close frame [{closeCode}] [{closeReason}]."); +#endif + var awaiter = _stream.WriteAsync(closingHandshake, 0, closingHandshake.Length); + if (!awaiter.Wait(ConnectTimeout)) + { + await InternalClose(true); + throw new TimeoutException( + $"Closing handshake with [{_remoteEndPoint}] timeout [{ConnectTimeout}]."); + } + } + catch (Exception ex) + { + await HandleSendOperationException(ex); + } + + return; + } + case _connecting: + case _closing: + { + await InternalClose(true); + return; + } + case _closed: + case _none: + default: + return; + } + } + + private async Task InternalClose(bool shallNotifyUserSide) + { + if (Interlocked.Exchange(ref _state, _closed) == _closed) + { + return; + } + + Shutdown(); + + if (shallNotifyUserSide) + { + NetworkHelper.Logger.Debug(string.Format("Disconnected from server [{0}] with dispatcher [{1}] on [{2}].", + RemoteEndPoint, + _dispatcher.GetType().Name, + DateTime.UtcNow.ToString(@"yyyy-MM-dd HH:mm:ss.fffffff"))); + + try + { + await _dispatcher.OnServerDisconnected(this); + } + catch (Exception ex) + { + await HandleUserSideError(ex); } } - #endregion + Clean(); + } - #region Keep Alive - - private void StartKeepAliveTimeoutTimer() + public void Shutdown() + { + // The correct way to shut down the connection (especially if you are in a full-duplex conversation) + // is to call socket.Shutdown(SocketShutdown.Send) and give the remote party some time to close + // their send channel. This ensures that you receive any pending data instead of slamming the + // connection shut. ObjectDisposedException should never be part of the normal application flow. + if (_tcpClient != null && _tcpClient.Connected) { - _keepAliveTimeoutTimer.Change((int)KeepAliveTimeout.TotalMilliseconds, Timeout.Infinite); + _tcpClient.Client.Shutdown(SocketShutdown.Send); } + } - private void StopKeepAliveTimeoutTimer() + private void Clean() + { + try { - _keepAliveTimeoutTimer.Change(Timeout.Infinite, Timeout.Infinite); - } - - private async void OnKeepAliveTimeout() - { - NetworkHelper.Logger.Warn($"Keep-alive timer timeout [{KeepAliveTimeout}]."); - await Close(WebSocketCloseCode.AbnormalClosure, "Keep-Alive Timeout"); - } - - private async void OnKeepAlive() - { - if (await _keepAliveLocker.WaitAsync(0)) + try { - try + if (_keepAliveTracker != null) { - if (State != WebSocketState.Open) - { - return; - } + _keepAliveTracker.StopTimer(); + _keepAliveTracker.Dispose(); + } + } + catch + { + } - if (_keepAliveTracker.ShouldSendKeepAlive()) - { - var keepAliveFrame = new PingFrame().ToArray(_frameBuilder); - await SendFrame(keepAliveFrame); - StartKeepAliveTimeoutTimer(); + try + { + if (_keepAliveTimeoutTimer != null) + { + _keepAliveTimeoutTimer.Change(Timeout.Infinite, Timeout.Infinite); + _keepAliveTimeoutTimer.Dispose(); + } + } + catch + { + } + + try + { + if (_closingTimeoutTimer != null) + { + _closingTimeoutTimer.Change(Timeout.Infinite, Timeout.Infinite); + _closingTimeoutTimer.Dispose(); + } + } + catch + { + } + + try + { + if (_stream != null) + { + _stream.Dispose(); + } + } + catch + { + } + + try + { + if (_tcpClient != null) + { + _tcpClient.Dispose(); + } + } + catch + { + } + } + catch + { + } + finally + { + _keepAliveTracker = null; + _keepAliveTimeoutTimer = null; + _closingTimeoutTimer = null; + _stream = null; + _tcpClient = null; + } + + if (_receiveBuffer != default) + { + _configuration.BufferManager.ReturnBuffer(_receiveBuffer); + } + + _receiveBuffer = default; + _receiveBufferOffset = 0; + } + + public async Task Abort() + { + await InternalClose(true); + } + + private void StartClosingTimer() + { + // In abnormal cases (such as not having received a TCP Close + // from the server after a reasonable amount of time) a client MAY initiate the TCP Close. + _closingTimeoutTimer.Change((int)CloseTimeout.TotalMilliseconds, Timeout.Infinite); + } + + private async void OnCloseTimeout() + { + // After both sending and receiving a Close message, an endpoint + // considers the WebSocket connection closed and MUST close the + // underlying TCP connection. The server MUST close the underlying TCP + // connection immediately; the client SHOULD wait for the server to + // close the connection but MAY close the connection at any time after + // sending and receiving a Close message, e.g., if it has not received a + // TCP Close from the server in a reasonable time period. + NetworkHelper.Logger.Warn($"Closing timer timeout [{CloseTimeout}] then close automatically."); + await InternalClose(true); + } + + private async Task HandleSendOperationException(Exception ex) + { + if (IsSocketTimeOut(ex)) + { + await CloseIfShould(ex); + throw new WebSocketException(ex.Message, new TimeoutException(ex.Message, ex)); + } + + await CloseIfShould(ex); + throw new WebSocketException(ex.Message, ex); + } + + private async Task HandleReceiveOperationException(Exception ex) + { + if (IsSocketTimeOut(ex)) + { + await CloseIfShould(ex); + throw new WebSocketException(ex.Message, new TimeoutException(ex.Message, ex)); + } + + await CloseIfShould(ex); + throw new WebSocketException(ex.Message, ex); + } + + private bool IsSocketTimeOut(Exception ex) + { + return ex is IOException + && ex.InnerException != null + && ex.InnerException is SocketException + && (ex.InnerException as SocketException).SocketErrorCode == SocketError.TimedOut; + } + + private async Task CloseIfShould(Exception ex) + { + if (ex is ObjectDisposedException + || ex is InvalidOperationException + || ex is SocketException + || ex is IOException + || ex is NullReferenceException // buffer array operation + || ex is ArgumentException // buffer array operation + ) + { + NetworkHelper.Logger.Error(ex.FormatExceptionToMessage()); + + await InternalClose(false); // intend to close the session + + return true; + } + + return false; + } + + private async Task HandleUserSideError(Exception ex) + { + NetworkHelper.Logger.Error( + $"Client [{this}] error occurred in user side [{ex.Message}].{Environment.NewLine}{ex.FormatExceptionToMessage()}"); + await Task.CompletedTask; + } + + public async Task SendTextAsync(string text) + { + await SendFrame(new TextFrame(text).ToArray(_frameBuilder)); + } + + public async Task SendBinaryAsync(byte[] data) + { + await SendBinaryAsync(data, 0, data.Length); + } + + public async Task SendBinaryAsync(byte[] data, int offset, int count) + { + await SendFrame(new BinaryFrame(data, offset, count).ToArray(_frameBuilder)); + } + + public async Task SendBinaryAsync(ArraySegment segment) + { + await SendFrame(new BinaryFrame(segment).ToArray(_frameBuilder)); + } + + public async Task SendStreamAsync(Stream stream) + { + if (stream == null) + { + throw new ArgumentNullException("stream"); + } + + var fragmentLength = _configuration.ReasonableFragmentSize; + var buffer = new byte[fragmentLength]; + var readCount = 0; + + readCount = await stream.ReadAsync(buffer, 0, fragmentLength); + if (readCount == 0) + { + return; + } + + await SendFrame(new BinaryFragmentationFrame(OpCode.Binary, buffer, 0, readCount).ToArray(_frameBuilder)); + + while (true) + { + readCount = await stream.ReadAsync(buffer, 0, fragmentLength); + if (readCount != 0) + { + await SendFrame( + new BinaryFragmentationFrame(OpCode.Continuation, buffer, 0, readCount).ToArray(_frameBuilder)); + } + else + { + await SendFrame( + new BinaryFragmentationFrame(OpCode.Continuation, buffer, 0, 0, true).ToArray(_frameBuilder)); + break; + } + } + } + + private async Task SendFrame(byte[] frame) + { + if (frame == null) + { + throw new ArgumentNullException("frame"); + } + + if (State != WebSocketState.Open) + { + throw new InvalidOperationException("This websocket client has not connected to server."); + } + + try + { + await _stream.WriteAsync(frame, 0, frame.Length); + _keepAliveTracker.OnDataSent(); + } + catch (Exception ex) + { + await HandleSendOperationException(ex); + } + } + + private void StartKeepAliveTimeoutTimer() + { + _keepAliveTimeoutTimer.Change((int)KeepAliveTimeout.TotalMilliseconds, Timeout.Infinite); + } + + private void StopKeepAliveTimeoutTimer() + { + _keepAliveTimeoutTimer.Change(Timeout.Infinite, Timeout.Infinite); + } + + private async void OnKeepAliveTimeout() + { + NetworkHelper.Logger.Warn($"Keep-alive timer timeout [{KeepAliveTimeout}]."); + await CloseAsync(WebSocketCloseCode.AbnormalClosure, "Keep-Alive Timeout"); + } + + private async void OnKeepAlive() + { + if (await _keepAliveLocker.WaitAsync(0)) + { + try + { + if (State != WebSocketState.Open) + { + return; + } + + if (_keepAliveTracker.ShouldSendKeepAlive()) + { + var keepAliveFrame = new PingFrame().ToArray(_frameBuilder); + await SendFrame(keepAliveFrame); + StartKeepAliveTimeoutTimer(); #if DEBUG NetworkHelper.Logger.Debug($"Send client side ping frame [{string.Empty}]."); #endif - _keepAliveTracker.ResetTimer(); - } - } - catch (Exception ex) - { - NetworkHelper.Logger.Error(ex.FormatExceptionToMessage()); - await Close(WebSocketCloseCode.EndpointUnavailable); - } - finally - { - _keepAliveLocker.Release(); + _keepAliveTracker.ResetTimer(); } } + catch (Exception ex) + { + NetworkHelper.Logger.Error(ex.FormatExceptionToMessage()); + await CloseAsync(WebSocketCloseCode.EndpointUnavailable); + } + finally + { + _keepAliveLocker.Release(); + } } - - #endregion - - #region Extensions - - internal void AgreeExtensions(IEnumerable extensions) - { - if (extensions == null) - { - throw new ArgumentNullException("extensions"); - } - - // If a server gives an invalid response, such as accepting a PMCE that - // the client did not offer, the client MUST _Fail the WebSocket Connection_. - if (this.OfferedExtensions == null - || !this.OfferedExtensions.Any() - || this.EnabledExtensions == null - || !this.EnabledExtensions.Any()) - { - throw new WebSocketHandshakeException(string.Format( - "Negotiate extension with remote [{0}] failed due to no extension enabled.", this.RemoteEndPoint)); - } - - // Note that the order of extensions is significant. Any interactions - // between multiple extensions MAY be defined in the documents defining - // the extensions. In the absence of such definitions, the - // interpretation is that the header fields listed by the client in its - // request represent a preference of the header fields it wishes to use, - // with the first options listed being most preferable. The extensions - // listed by the server in response represent the extensions actually in - // use for the connection. Should the extensions modify the data and/or - // framing, the order of operations on the data should be assumed to be - // the same as the order in which the extensions are listed in the - // server's response in the opening handshake. - // For example, if there are two extensions "foo" and "bar" and if the - // header field |Sec-WebSocket-Extensions| sent by the server has the - // value "foo, bar", then operations on the data will be made as - // bar(foo(data)), be those changes to the data itself (such as - // compression) or changes to the framing that may "stack". - var agreedExtensions = new SortedList(); - var suggestedExtensions = string.Join(",", extensions).Split(',') - .Select(p => p.TrimStart().TrimEnd()).Where(p => !string.IsNullOrWhiteSpace(p)); - - int order = 0; - foreach (var extension in suggestedExtensions) - { - order++; - - var offeredExtensionName = extension.Split(';').First(); - - // Extensions not listed by the client MUST NOT be listed. - if (!this.EnabledExtensions.ContainsKey(offeredExtensionName)) - { - throw new WebSocketHandshakeException(string.Format( - "Negotiate extension with remote [{0}] failed due to un-enabled extensions [{1}].", - this.RemoteEndPoint, offeredExtensionName)); - } - - var extensionNegotiator = this.EnabledExtensions[offeredExtensionName]; - - string invalidParameter; - IWebSocketExtension negotiatedExtension; - if (!extensionNegotiator.NegotiateAsClient(extension, out invalidParameter, out negotiatedExtension) - || !string.IsNullOrEmpty(invalidParameter) - || negotiatedExtension == null) - { - throw new WebSocketHandshakeException(string.Format( - "Negotiate extension with remote [{0}] failed due to extension [{1}] has invalid parameter [{2}].", - this.RemoteEndPoint, extension, invalidParameter)); - } - - agreedExtensions.Add(order, negotiatedExtension); - } - - // If a server gives an invalid response, such as accepting a PMCE that - // the client did not offer, the client MUST _Fail the WebSocket Connection_. - foreach (var extension in agreedExtensions.Values) - { - if (!this.OfferedExtensions.Any(x => x.ExtensionNegotiationOffer.StartsWith(extension.Name))) - { - throw new WebSocketHandshakeException(string.Format( - "Negotiate extension with remote [{0}] failed due to extension [{1}] not be offered.", - this.RemoteEndPoint, extension.Name)); - } - } - - // A server MUST NOT accept a PMCE extension negotiation offer together - // with another extension if the PMCE will conflict with the extension - // on their use of the RSV1 bit. A client that received a response - // accepting a PMCE extension negotiation offer together with such an - // extension MUST _Fail the WebSocket Connection_. - bool isRsv1BitOccupied = false; - bool isRsv2BitOccupied = false; - bool isRsv3BitOccupied = false; - foreach (var extension in agreedExtensions.Values) - { - if ((isRsv1BitOccupied && extension.Rsv1BitOccupied) - || (isRsv2BitOccupied && extension.Rsv2BitOccupied) - || (isRsv3BitOccupied && extension.Rsv3BitOccupied)) - { - throw new WebSocketHandshakeException(string.Format( - "Negotiate extension with remote [{0}] failed due to conflict bit occupied.", this.RemoteEndPoint)); - } - - isRsv1BitOccupied = isRsv1BitOccupied | extension.Rsv1BitOccupied; - isRsv2BitOccupied = isRsv2BitOccupied | extension.Rsv2BitOccupied; - isRsv3BitOccupied = isRsv3BitOccupied | extension.Rsv3BitOccupied; - } - - _frameBuilder.NegotiatedExtensions = agreedExtensions; - } - - #endregion - - #region Sub-Protocols - - internal void UseSubProtocol(string protocol) - { - if (string.IsNullOrWhiteSpace(protocol)) - { - throw new ArgumentNullException("protocol"); - } - - if (this.RequestedSubProtocols == null - || !this.RequestedSubProtocols.Any() - || this.EnabledSubProtocols == null - || !this.EnabledSubProtocols.Any()) - { - throw new WebSocketHandshakeException(string.Format( - "Negotiate sub-protocol with remote [{0}] failed due to sub-protocol [{1}] is not enabled.", - this.RemoteEndPoint, protocol)); - } - - var requestedSubProtocols = string.Join(",", this.RequestedSubProtocols.Select(s => s.RequestedSubProtocol)) - .Split(',').Select(p => p.TrimStart().TrimEnd()).Where(p => !string.IsNullOrWhiteSpace(p)); - - if (!requestedSubProtocols.Contains(protocol)) - { - throw new WebSocketHandshakeException(string.Format( - "Negotiate sub-protocol with remote [{0}] failed due to sub-protocol [{1}] has not been requested.", - this.RemoteEndPoint, protocol)); - } - - // format : name.version.parameter - var segements = protocol.Split('.') - .Select(p => p.TrimStart().TrimEnd()).Where(p => !string.IsNullOrWhiteSpace(p)) - .ToArray(); - string protocolName = segements[0]; - string protocolVersion = segements.Length > 1 ? segements[1] : null; - string protocolParameter = segements.Length > 2 ? segements[2] : null; - - if (!this.EnabledSubProtocols.ContainsKey(protocolName)) - { - throw new WebSocketHandshakeException(string.Format( - "Negotiate sub-protocol with remote [{0}] failed due to sub-protocol [{1}] is not enabled.", - this.RemoteEndPoint, protocolName)); - } - - var subProtocolNegotiator = this.EnabledSubProtocols[protocolName]; - - string invalidParameter; - IWebSocketSubProtocol negotiatedSubProtocol; - if (!subProtocolNegotiator.NegotiateAsClient(protocolName, protocolVersion, protocolParameter, out invalidParameter, out negotiatedSubProtocol) - || !string.IsNullOrEmpty(invalidParameter) - || negotiatedSubProtocol == null) - { - throw new WebSocketHandshakeException(string.Format( - "Negotiate sub-protocol with remote [{0}] failed due to sub-protocol [{1}] has invalid parameter [{2}].", - this.RemoteEndPoint, protocol, invalidParameter)); - } - } - - #endregion - - #region IDisposable Members - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - private void Dispose(bool disposing) - { - if (disposing) - { - try - { - InternalClose(false).Wait(); // disposing - } - catch (Exception ex) - { - NetworkHelper.Logger.Error(ex.FormatExceptionToMessage()); - } - } - } - - #endregion } -} + + private void Dispose(bool disposing) + { + if (disposing) + { + try + { + InternalClose(false).Wait(); // disposing + } + catch (Exception ex) + { + NetworkHelper.Logger.Error(ex.FormatExceptionToMessage()); + } + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Client/AsyncWebSocketClientConfiguration.cs b/EonaCat.Network/System/Sockets/WebSockets/Client/AsyncWebSocketClientConfiguration.cs index 6fcb09c..f0aa2df 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Client/AsyncWebSocketClientConfiguration.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Client/AsyncWebSocketClientConfiguration.cs @@ -8,74 +8,72 @@ using EonaCat.WebSockets.Buffer; using EonaCat.WebSockets.Extensions; using EonaCat.WebSockets.SubProtocols; -namespace EonaCat.WebSockets +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public sealed class AsyncWebSocketClientConfiguration { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - - public sealed class AsyncWebSocketClientConfiguration + public AsyncWebSocketClientConfiguration() { - public AsyncWebSocketClientConfiguration() + BufferManager = new SegmentBufferManager(100, 8192, 1, true); + ReceiveBufferSize = 8192; + SendBufferSize = 8192; + ReceiveTimeout = TimeSpan.Zero; + SendTimeout = TimeSpan.Zero; + NoDelay = true; + LingerState = new LingerOption(false, 0); // The socket will linger for x seconds after Socket.Close is called. + + SslTargetHost = null; + SslClientCertificates = new X509CertificateCollection(); + SslEncryptionPolicy = EncryptionPolicy.RequireEncryption; + SslEnabledProtocols = SslProtocols.Ssl3 | SslProtocols.Tls; + SslCheckCertificateRevocation = false; + SslPolicyErrorsBypassed = false; + + ConnectTimeout = TimeSpan.FromSeconds(10); + CloseTimeout = TimeSpan.FromSeconds(5); + KeepAliveInterval = TimeSpan.FromSeconds(30); + KeepAliveTimeout = TimeSpan.FromSeconds(5); + ReasonableFragmentSize = 4096; + + EnabledExtensions = new Dictionary { - BufferManager = new SegmentBufferManager(100, 8192, 1, true); - ReceiveBufferSize = 8192; - SendBufferSize = 8192; - ReceiveTimeout = TimeSpan.Zero; - SendTimeout = TimeSpan.Zero; - NoDelay = true; - LingerState = new LingerOption(false, 0); // The socket will linger for x seconds after Socket.Close is called. + { PerMessageCompressionExtension.RegisteredToken, new PerMessageCompressionExtensionNegotiator() } + }; + EnabledSubProtocols = new Dictionary(); - SslTargetHost = null; - SslClientCertificates = new X509CertificateCollection(); - SslEncryptionPolicy = EncryptionPolicy.RequireEncryption; - SslEnabledProtocols = SslProtocols.Ssl3 | SslProtocols.Tls; - SslCheckCertificateRevocation = false; - SslPolicyErrorsBypassed = false; - - ConnectTimeout = TimeSpan.FromSeconds(10); - CloseTimeout = TimeSpan.FromSeconds(5); - KeepAliveInterval = TimeSpan.FromSeconds(30); - KeepAliveTimeout = TimeSpan.FromSeconds(5); - ReasonableFragmentSize = 4096; - - EnabledExtensions = new Dictionary() - { - { PerMessageCompressionExtension.RegisteredToken, new PerMessageCompressionExtensionNegotiator() }, - }; - EnabledSubProtocols = new Dictionary(); - - OfferedExtensions = new List() - { - new WebSocketExtensionOfferDescription(PerMessageCompressionExtension.RegisteredToken), - }; - RequestedSubProtocols = new List(); - } - - public ISegmentBufferManager BufferManager { get; set; } - public int ReceiveBufferSize { get; set; } - public int SendBufferSize { get; set; } - public TimeSpan ReceiveTimeout { get; set; } - public TimeSpan SendTimeout { get; set; } - public bool NoDelay { get; set; } - public LingerOption LingerState { get; set; } - - public string SslTargetHost { get; set; } - public X509CertificateCollection SslClientCertificates { get; set; } - public EncryptionPolicy SslEncryptionPolicy { get; set; } - public SslProtocols SslEnabledProtocols { get; set; } - public bool SslCheckCertificateRevocation { get; set; } - public bool SslPolicyErrorsBypassed { get; set; } - - public TimeSpan ConnectTimeout { get; set; } - public TimeSpan CloseTimeout { get; set; } - public TimeSpan KeepAliveInterval { get; set; } - public TimeSpan KeepAliveTimeout { get; set; } - public int ReasonableFragmentSize { get; set; } - - public Dictionary EnabledExtensions { get; set; } - public Dictionary EnabledSubProtocols { get; set; } - - public List OfferedExtensions { get; set; } - public List RequestedSubProtocols { get; set; } + OfferedExtensions = new List + { + new(PerMessageCompressionExtension.RegisteredToken) + }; + RequestedSubProtocols = new List(); } -} + + public ISegmentBufferManager BufferManager { get; set; } + public int ReceiveBufferSize { get; set; } + public int SendBufferSize { get; set; } + public TimeSpan ReceiveTimeout { get; set; } + public TimeSpan SendTimeout { get; set; } + public bool NoDelay { get; set; } + public LingerOption LingerState { get; set; } + + public string SslTargetHost { get; set; } + public X509CertificateCollection SslClientCertificates { get; set; } + public EncryptionPolicy SslEncryptionPolicy { get; set; } + public SslProtocols SslEnabledProtocols { get; set; } + public bool SslCheckCertificateRevocation { get; set; } + public bool SslPolicyErrorsBypassed { get; set; } + + public TimeSpan ConnectTimeout { get; set; } + public TimeSpan CloseTimeout { get; set; } + public TimeSpan KeepAliveInterval { get; set; } + public TimeSpan KeepAliveTimeout { get; set; } + public int ReasonableFragmentSize { get; set; } + + public Dictionary EnabledExtensions { get; set; } + public Dictionary EnabledSubProtocols { get; set; } + + public List OfferedExtensions { get; set; } + public List RequestedSubProtocols { get; set; } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Client/IAsyncWebSocketClientMessageDispatcher.cs b/EonaCat.Network/System/Sockets/WebSockets/Client/IAsyncWebSocketClientMessageDispatcher.cs index 32c380a..bd811ba 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Client/IAsyncWebSocketClientMessageDispatcher.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Client/IAsyncWebSocketClientMessageDispatcher.cs @@ -1,19 +1,17 @@ using System.Threading.Tasks; -namespace EonaCat.WebSockets +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public interface IAsyncWebSocketClientMessageDispatcher { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + Task OnServerConnected(AsyncWebSocketClient client); + Task OnServerTextReceived(AsyncWebSocketClient client, string text); + Task OnServerBinaryReceived(AsyncWebSocketClient client, byte[] data, int offset, int count); + Task OnServerDisconnected(AsyncWebSocketClient client); - public interface IAsyncWebSocketClientMessageDispatcher - { - Task OnServerConnected(AsyncWebSocketClient client); - Task OnServerTextReceived(AsyncWebSocketClient client, string text); - Task OnServerBinaryReceived(AsyncWebSocketClient client, byte[] data, int offset, int count); - Task OnServerDisconnected(AsyncWebSocketClient client); - - Task OnServerFragmentationStreamOpened(AsyncWebSocketClient client, byte[] data, int offset, int count); - Task OnServerFragmentationStreamContinued(AsyncWebSocketClient client, byte[] data, int offset, int count); - Task OnServerFragmentationStreamClosed(AsyncWebSocketClient client, byte[] data, int offset, int count); - } -} + Task OnServerFragmentationStreamOpened(AsyncWebSocketClient client, byte[] data, int offset, int count); + Task OnServerFragmentationStreamContinued(AsyncWebSocketClient client, byte[] data, int offset, int count); + Task OnServerFragmentationStreamClosed(AsyncWebSocketClient client, byte[] data, int offset, int count); +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Client/InternalAsyncWebSocketClientMessageDispatcherImplementation.cs b/EonaCat.Network/System/Sockets/WebSockets/Client/InternalAsyncWebSocketClientMessageDispatcherImplementation.cs index 9f57d80..a4de0c7 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Client/InternalAsyncWebSocketClientMessageDispatcherImplementation.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Client/InternalAsyncWebSocketClientMessageDispatcherImplementation.cs @@ -1,113 +1,112 @@ using System; using System.Threading.Tasks; -namespace EonaCat.WebSockets +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +internal class InternalAsyncWebSocketClientMessageDispatcherImplementation : IAsyncWebSocketClientMessageDispatcher { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private readonly Func _onServerBinaryReceived; + private readonly Func _onServerConnected; + private readonly Func _onServerDisconnected; + private readonly Func _onServerFragmentationStreamClosed; + private readonly Func _onServerFragmentationStreamContinued; - internal class InternalAsyncWebSocketClientMessageDispatcherImplementation : IAsyncWebSocketClientMessageDispatcher + private readonly Func _onServerFragmentationStreamOpened; + private readonly Func _onServerTextReceived; + + public InternalAsyncWebSocketClientMessageDispatcherImplementation() { - private Func _onServerTextReceived; - private Func _onServerBinaryReceived; - private Func _onServerConnected; - private Func _onServerDisconnected; + } - private Func _onServerFragmentationStreamOpened; - private Func _onServerFragmentationStreamContinued; - private Func _onServerFragmentationStreamClosed; + public InternalAsyncWebSocketClientMessageDispatcherImplementation( + Func onServerTextReceived, + Func onServerDataReceived, + Func onServerConnected, + Func onServerDisconnected) + : this() + { + _onServerTextReceived = onServerTextReceived; + _onServerBinaryReceived = onServerDataReceived; + _onServerConnected = onServerConnected; + _onServerDisconnected = onServerDisconnected; + } - public InternalAsyncWebSocketClientMessageDispatcherImplementation() + public InternalAsyncWebSocketClientMessageDispatcherImplementation( + Func onServerTextReceived, + Func onServerDataReceived, + Func onServerConnected, + Func onServerDisconnected, + Func onServerFragmentationStreamOpened, + Func onServerFragmentationStreamContinued, + Func onServerFragmentationStreamClosed) + : this() + { + _onServerTextReceived = onServerTextReceived; + _onServerBinaryReceived = onServerDataReceived; + _onServerConnected = onServerConnected; + _onServerDisconnected = onServerDisconnected; + + _onServerFragmentationStreamOpened = onServerFragmentationStreamOpened; + _onServerFragmentationStreamContinued = onServerFragmentationStreamContinued; + _onServerFragmentationStreamClosed = onServerFragmentationStreamClosed; + } + + public async Task OnServerConnected(AsyncWebSocketClient client) + { + if (_onServerConnected != null) { - } - - public InternalAsyncWebSocketClientMessageDispatcherImplementation( - Func onServerTextReceived, - Func onServerDataReceived, - Func onServerConnected, - Func onServerDisconnected) - : this() - { - _onServerTextReceived = onServerTextReceived; - _onServerBinaryReceived = onServerDataReceived; - _onServerConnected = onServerConnected; - _onServerDisconnected = onServerDisconnected; - } - - public InternalAsyncWebSocketClientMessageDispatcherImplementation( - Func onServerTextReceived, - Func onServerDataReceived, - Func onServerConnected, - Func onServerDisconnected, - Func onServerFragmentationStreamOpened, - Func onServerFragmentationStreamContinued, - Func onServerFragmentationStreamClosed) - : this() - { - _onServerTextReceived = onServerTextReceived; - _onServerBinaryReceived = onServerDataReceived; - _onServerConnected = onServerConnected; - _onServerDisconnected = onServerDisconnected; - - _onServerFragmentationStreamOpened = onServerFragmentationStreamOpened; - _onServerFragmentationStreamContinued = onServerFragmentationStreamContinued; - _onServerFragmentationStreamClosed = onServerFragmentationStreamClosed; - } - - public async Task OnServerConnected(AsyncWebSocketClient client) - { - if (_onServerConnected != null) - { - await _onServerConnected(client); - } - } - - public async Task OnServerTextReceived(AsyncWebSocketClient client, string text) - { - if (_onServerTextReceived != null) - { - await _onServerTextReceived(client, text); - } - } - - public async Task OnServerBinaryReceived(AsyncWebSocketClient client, byte[] data, int offset, int count) - { - if (_onServerBinaryReceived != null) - { - await _onServerBinaryReceived(client, data, offset, count); - } - } - - public async Task OnServerDisconnected(AsyncWebSocketClient client) - { - if (_onServerDisconnected != null) - { - await _onServerDisconnected(client); - } - } - - public async Task OnServerFragmentationStreamOpened(AsyncWebSocketClient client, byte[] data, int offset, int count) - { - if (_onServerFragmentationStreamOpened != null) - { - await _onServerFragmentationStreamOpened(client, data, offset, count); - } - } - - public async Task OnServerFragmentationStreamContinued(AsyncWebSocketClient client, byte[] data, int offset, int count) - { - if (_onServerFragmentationStreamContinued != null) - { - await _onServerFragmentationStreamContinued(client, data, offset, count); - } - } - - public async Task OnServerFragmentationStreamClosed(AsyncWebSocketClient client, byte[] data, int offset, int count) - { - if (_onServerFragmentationStreamClosed != null) - { - await _onServerFragmentationStreamClosed(client, data, offset, count); - } + await _onServerConnected(client); } } -} + + public async Task OnServerTextReceived(AsyncWebSocketClient client, string text) + { + if (_onServerTextReceived != null) + { + await _onServerTextReceived(client, text); + } + } + + public async Task OnServerBinaryReceived(AsyncWebSocketClient client, byte[] data, int offset, int count) + { + if (_onServerBinaryReceived != null) + { + await _onServerBinaryReceived(client, data, offset, count); + } + } + + public async Task OnServerDisconnected(AsyncWebSocketClient client) + { + if (_onServerDisconnected != null) + { + await _onServerDisconnected(client); + } + } + + public async Task OnServerFragmentationStreamOpened(AsyncWebSocketClient client, byte[] data, int offset, int count) + { + if (_onServerFragmentationStreamOpened != null) + { + await _onServerFragmentationStreamOpened(client, data, offset, count); + } + } + + public async Task OnServerFragmentationStreamContinued(AsyncWebSocketClient client, byte[] data, int offset, + int count) + { + if (_onServerFragmentationStreamContinued != null) + { + await _onServerFragmentationStreamContinued(client, data, offset, count); + } + } + + public async Task OnServerFragmentationStreamClosed(AsyncWebSocketClient client, byte[] data, int offset, int count) + { + if (_onServerFragmentationStreamClosed != null) + { + await _onServerFragmentationStreamClosed(client, data, offset, count); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Client/WebSocketClientHandshaker.cs b/EonaCat.Network/System/Sockets/WebSockets/Client/WebSocketClientHandshaker.cs index 0a41d9b..54326e1 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Client/WebSocketClientHandshaker.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Client/WebSocketClientHandshaker.cs @@ -4,391 +4,396 @@ using System.Linq; using System.Net; using System.Security.Cryptography; using System.Text; -using EonaCat.WebSockets.Buffer; using EonaCat.Logger.Extensions; -using EonaCat.Logger.Managers; using EonaCat.Network; +using EonaCat.WebSockets.Buffer; -namespace EonaCat.WebSockets +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +internal sealed class WebSocketClientHandshaker { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private static readonly char[] _headerLineSplitter = { '\r', '\n' }; - internal sealed class WebSocketClientHandshaker + internal static byte[] CreateOpenningHandshakeRequest(AsyncWebSocketClient client, out string secWebSocketKey) { - private static readonly char[] _headerLineSplitter = new char[] { '\r', '\n' }; + var sb = new StringBuilder(); - internal static byte[] CreateOpenningHandshakeRequest(AsyncWebSocketClient client, out string secWebSocketKey) + // The handshake MUST be a valid HTTP request as specified by [RFC2616]. + // The method of the request MUST be GET, and the HTTP version MUST be at least 1.1. + // For example, if the WebSocket URI is "ws://example.com/chat", + // the first line sent should be "GET /chat HTTP/1.1". + sb.AppendFormatWithCrCf("GET {0} HTTP/{1}", + !string.IsNullOrEmpty(client.Uri.PathAndQuery) ? client.Uri.PathAndQuery : "/", + Consts.HttpVersion); + + // The request MUST contain a |Host| header field whose value + // contains /host/ plus optionally ":" followed by /port/ (when not + // using the default port). + sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.Host, client.Uri.Host); + + // The request MUST contain an |Upgrade| header field whose value + // MUST include the "websocket" keyword. + sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.Upgrade, + Consts.WebSocketUpgradeToken); + + // The request MUST contain a |Connection| header field whose value + // MUST include the "Upgrade" token. + sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.Connection, + Consts.WebSocketConnectionToken); + + // The request MUST include a header field with the name + // |Sec-WebSocket-Key|. The value of this header field MUST be a + // nonce consisting of a randomly selected 16-byte value that has + // been base64-encoded (see Section 4 of [RFC4648]). The nonce + // MUST be selected randomly for each connection. + secWebSocketKey = Convert.ToBase64String(Encoding.ASCII.GetBytes(Guid.NewGuid().ToString().Substring(0, 16))); + sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.SecWebSocketKey, secWebSocketKey); + + // The request MUST include a header field with the name + // |Sec-WebSocket-Version|. The value of this header field MUST be 13. + sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.SecWebSocketVersion, + Consts.WebSocketVersion); + + // The request MAY include a header field with the name + // |Sec-WebSocket-Extensions|. If present, this value indicates + // the protocol-level extension(s) the client wishes to speak. The + // interpretation and format of this header field is described in Section 9.1. + if (client.OfferedExtensions != null && client.OfferedExtensions.Any()) { - var sb = new StringBuilder(); - - // The handshake MUST be a valid HTTP request as specified by [RFC2616]. - // The method of the request MUST be GET, and the HTTP version MUST be at least 1.1. - // For example, if the WebSocket URI is "ws://example.com/chat", - // the first line sent should be "GET /chat HTTP/1.1". - sb.AppendFormatWithCrCf("GET {0} HTTP/{1}", - !string.IsNullOrEmpty(client.Uri.PathAndQuery) ? client.Uri.PathAndQuery : "/", - Consts.HttpVersion); - - // The request MUST contain a |Host| header field whose value - // contains /host/ plus optionally ":" followed by /port/ (when not - // using the default port). - sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.Host, client.Uri.Host); - - // The request MUST contain an |Upgrade| header field whose value - // MUST include the "websocket" keyword. - sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.Upgrade, Consts.WebSocketUpgradeToken); - - // The request MUST contain a |Connection| header field whose value - // MUST include the "Upgrade" token. - sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.Connection, Consts.WebSocketConnectionToken); - - // The request MUST include a header field with the name - // |Sec-WebSocket-Key|. The value of this header field MUST be a - // nonce consisting of a randomly selected 16-byte value that has - // been base64-encoded (see Section 4 of [RFC4648]). The nonce - // MUST be selected randomly for each connection. - secWebSocketKey = Convert.ToBase64String(Encoding.ASCII.GetBytes(Guid.NewGuid().ToString().Substring(0, 16))); - sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.SecWebSocketKey, secWebSocketKey); - - // The request MUST include a header field with the name - // |Sec-WebSocket-Version|. The value of this header field MUST be 13. - sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.SecWebSocketVersion, Consts.WebSocketVersion); - - // The request MAY include a header field with the name - // |Sec-WebSocket-Extensions|. If present, this value indicates - // the protocol-level extension(s) the client wishes to speak. The - // interpretation and format of this header field is described in Section 9.1. - if (client.OfferedExtensions != null && client.OfferedExtensions.Any()) + foreach (var extension in client.OfferedExtensions) { - foreach (var extension in client.OfferedExtensions) - { - sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.SecWebSocketExtensions, extension.ExtensionNegotiationOffer); - } + sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.SecWebSocketExtensions, + extension.ExtensionNegotiationOffer); } + } - // The request MAY include a header field with the name - // |Sec-WebSocket-Protocol|. If present, this value indicates one - // or more comma-separated subprotocol the client wishes to speak, - // ordered by preference. The elements that comprise this value - // MUST be non-empty strings with characters in the range U+0021 to - // U+007E not including separator characters as defined in - // [RFC2616] and MUST all be unique strings. The ABNF for the - // value of this header field is 1#token, where the definitions of - // constructs and rules are as given in [RFC2616]. - if (client.RequestedSubProtocols != null && client.RequestedSubProtocols.Any()) + // The request MAY include a header field with the name + // |Sec-WebSocket-Protocol|. If present, this value indicates one + // or more comma-separated subprotocol the client wishes to speak, + // ordered by preference. The elements that comprise this value + // MUST be non-empty strings with characters in the range U+0021 to + // U+007E not including separator characters as defined in + // [RFC2616] and MUST all be unique strings. The ABNF for the + // value of this header field is 1#token, where the definitions of + // constructs and rules are as given in [RFC2616]. + if (client.RequestedSubProtocols != null && client.RequestedSubProtocols.Any()) + { + foreach (var description in client.RequestedSubProtocols) { - foreach (var description in client.RequestedSubProtocols) - { - sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.SecWebSocketProtocol, description.RequestedSubProtocol); - } + sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.SecWebSocketProtocol, + description.RequestedSubProtocol); } + } - // The request MUST include a header field with the name |Origin| - // [RFC6454] if the request is coming from a browser client. If - // the connection is from a non-browser client, the request MAY - // include this header field if the semantics of that client match - // the use-case described here for browser clients. The value of - // this header field is the ASCII serialization of origin of the - // context in which the code establishing the connection is - // running. See [RFC6454] for the details of how this header field - // value is constructed. + // The request MUST include a header field with the name |Origin| + // [RFC6454] if the request is coming from a browser client. If + // the connection is from a non-browser client, the request MAY + // include this header field if the semantics of that client match + // the use-case described here for browser clients. The value of + // this header field is the ASCII serialization of origin of the + // context in which the code establishing the connection is + // running. See [RFC6454] for the details of how this header field + // value is constructed. - // The request MAY include any other header fields, for example, - // cookies [RFC6265] and/or authentication-related header fields - // such as the |Authorization| header field [RFC2616], which are - // processed according to documents that define them. + // The request MAY include any other header fields, for example, + // cookies [RFC6265] and/or authentication-related header fields + // such as the |Authorization| header field [RFC2616], which are + // processed according to documents that define them. - sb.AppendWithCrCf(); + sb.AppendWithCrCf(); - // GET /chat HTTP/1.1 - // Host: server.example.com - // Upgrade: websocket - // Connection: Upgrade - // Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== - // Sec-WebSocket-Protocol: chat, superchat - // Sec-WebSocket-Version: 13 - // Origin: http://example.com - var request = sb.ToString(); + // GET /chat HTTP/1.1 + // Host: server.example.com + // Upgrade: websocket + // Connection: Upgrade + // Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== + // Sec-WebSocket-Protocol: chat, superchat + // Sec-WebSocket-Version: 13 + // Origin: http://example.com + var request = sb.ToString(); #if DEBUG NetworkHelper.Logger.Debug($"{client.RemoteEndPoint}{Environment.NewLine}{request}"); #endif - return Encoding.UTF8.GetBytes(request); + return Encoding.UTF8.GetBytes(request); + } + + internal static bool VerifyOpenningHandshakeResponse(AsyncWebSocketClient client, byte[] buffer, int offset, + int count, string secWebSocketKey) + { + BufferValidator.ValidateBuffer(buffer, offset, count, "buffer"); + if (string.IsNullOrEmpty(secWebSocketKey)) + { + throw new ArgumentNullException("secWebSocketKey"); } - internal static bool VerifyOpenningHandshakeResponse(AsyncWebSocketClient client, byte[] buffer, int offset, int count, string secWebSocketKey) - { - BufferValidator.ValidateBuffer(buffer, offset, count, "buffer"); - if (string.IsNullOrEmpty(secWebSocketKey)) - { - throw new ArgumentNullException("secWebSocketKey"); - } - - var response = Encoding.UTF8.GetString(buffer, offset, count); + var response = Encoding.UTF8.GetString(buffer, offset, count); #if DEBUG NetworkHelper.Logger.Debug($"{client.RemoteEndPoint}{Environment.NewLine}{response}"); #endif - try + try + { + // HTTP/1.1 101 Switching Protocols + // Upgrade: websocket + // Connection: Upgrade + // Sec-WebSocket-Accept: 1tGBmA9p0DQDgmFll6P0/UcVS/E= + // Sec-WebSocket-Protocol: chat + Dictionary headers; + List extensions; + List protocols; + ParseOpenningHandshakeResponseHeaders(response, out headers, out extensions, out protocols); + if (headers == null) { - // HTTP/1.1 101 Switching Protocols - // Upgrade: websocket - // Connection: Upgrade - // Sec-WebSocket-Accept: 1tGBmA9p0DQDgmFll6P0/UcVS/E= - // Sec-WebSocket-Protocol: chat - Dictionary headers; - List extensions; - List protocols; - ParseOpenningHandshakeResponseHeaders(response, out headers, out extensions, out protocols); - if (headers == null) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to invalid headers.", client.RemoteEndPoint)); - } - - // If the status code received from the server is not 101, the - // client handles the response per HTTP [RFC2616] procedures. In - // particular, the client might perform authentication if it - // receives a 401 status code; the server might redirect the client - // using a 3xx status code (but clients are not required to follow them), etc. - if (!headers.ContainsKey(Consts.HttpStatusCodeName)) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to lack of status code.", client.RemoteEndPoint)); - } - - if (!headers.ContainsKey(Consts.HttpStatusCodeDescription)) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to lack of status description.", client.RemoteEndPoint)); - } - - if (headers[Consts.HttpStatusCodeName] == ((int)HttpStatusCode.BadRequest).ToString()) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to bad request [{1}].", - client.RemoteEndPoint, headers[Consts.HttpStatusCodeName])); - } - - if (headers[Consts.HttpStatusCodeName] != ((int)HttpStatusCode.SwitchingProtocols).ToString()) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to expected 101 Switching Protocols but received [{1}].", - client.RemoteEndPoint, headers[Consts.HttpStatusCodeName])); - } - - // If the response lacks an |Upgrade| header field or the |Upgrade| - // header field contains a value that is not an ASCII case- - // insensitive match for the value "websocket", the client MUST - // _Fail the WebSocket Connection_. - if (!headers.ContainsKey(HttpKnownHeaderNames.Connection)) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to lack of connection header item.", client.RemoteEndPoint)); - } - - if (headers[HttpKnownHeaderNames.Connection].ToLowerInvariant() != Consts.WebSocketConnectionToken.ToLowerInvariant()) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to invalid connection header item value [{1}].", - client.RemoteEndPoint, headers[HttpKnownHeaderNames.Connection])); - } - - // If the response lacks a |Connection| header field or the - // |Connection| header field doesn't contain a token that is an - // ASCII case-insensitive match for the value "Upgrade", the client - // MUST _Fail the WebSocket Connection_. - if (!headers.ContainsKey(HttpKnownHeaderNames.Upgrade)) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to lack of upgrade header item.", client.RemoteEndPoint)); - } - - if (headers[HttpKnownHeaderNames.Upgrade].ToLowerInvariant() != Consts.WebSocketUpgradeToken.ToLowerInvariant()) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to invalid upgrade header item value [{1}].", - client.RemoteEndPoint, headers[HttpKnownHeaderNames.Upgrade])); - } - - // If the response lacks a |Sec-WebSocket-Accept| header field or - // the |Sec-WebSocket-Accept| contains a value other than the - // base64-encoded SHA-1 of the concatenation of the |Sec-WebSocket- - // Key| (as a string, not base64-decoded) with the string "258EAFA5- - // E914-47DA-95CA-C5AB0DC85B11" but ignoring any leading and - // trailing whitespace, the client MUST _Fail the WebSocket Connection_. - if (!headers.ContainsKey(HttpKnownHeaderNames.SecWebSocketAccept)) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to lack of Sec-WebSocket-Accept header item.", client.RemoteEndPoint)); - } - - string challenge = GetSecWebSocketAcceptString(secWebSocketKey); - if (!headers[HttpKnownHeaderNames.SecWebSocketAccept].Equals(challenge, StringComparison.OrdinalIgnoreCase)) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to invalid Sec-WebSocket-Accept header item value [{1}].", - client.RemoteEndPoint, headers[HttpKnownHeaderNames.SecWebSocketAccept])); - } - - // If the response includes a |Sec-WebSocket-Extensions| header - // field and this header field indicates the use of an extension - // that was not present in the client's handshake (the server has - // indicated an extension not requested by the client), the client - // MUST _Fail the WebSocket Connection_. - if (extensions != null) - { - foreach (var extension in extensions) - { - // The empty string is not the same as the null value for these - // purposes and is not a legal value for this field. - if (string.IsNullOrWhiteSpace(extension)) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to empty extension.", client.RemoteEndPoint)); - } - } - - client.AgreeExtensions(extensions); - } - - // If the response includes a |Sec-WebSocket-Protocol| header field - // and this header field indicates the use of a subprotocol that was - // not present in the client's handshake (the server has indicated a - // subprotocol not requested by the client), the client MUST _Fail - // the WebSocket Connection_. - if (protocols != null) - { - if (!protocols.Any()) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to empty sub-protocol.", client.RemoteEndPoint)); - } - - if (protocols.Count > 1) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to suggest to use multiple sub-protocols.", client.RemoteEndPoint)); - } - - foreach (var protocol in protocols) - { - // The empty string is not the same as the null value for these - // purposes and is not a legal value for this field. - if (string.IsNullOrWhiteSpace(protocol)) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to empty sub-protocol.", client.RemoteEndPoint)); - } - } - - var suggestedProtocols = protocols.First().Split(',') - .Select(p => p.TrimStart().TrimEnd()).Where(p => !string.IsNullOrWhiteSpace(p)); - - if (!suggestedProtocols.Any()) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to invalid sub-protocol.", client.RemoteEndPoint)); - } - - if (suggestedProtocols.Count() > 1) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to suggest to use multiple sub-protocols.", client.RemoteEndPoint)); - } - - // The value chosen MUST be derived - // from the client's handshake, specifically by selecting one of - // the values from the |Sec-WebSocket-Protocol| field that the - // server is willing to use for this connection (if any). - client.UseSubProtocol(suggestedProtocols.First()); - } - } - catch (Exception ex) - { - NetworkHelper.Logger.Error($"{client.RemoteEndPoint}{Environment.NewLine}{ex.FormatExceptionToMessage()}"); - throw; + throw new WebSocketHandshakeException( + $"Handshake with remote [{client.RemoteEndPoint}] failed due to invalid headers."); } - return true; + // If the status code received from the server is not 101, the + // client handles the response per HTTP [RFC2616] procedures. In + // particular, the client might perform authentication if it + // receives a 401 status code; the server might redirect the client + // using a 3xx status code (but clients are not required to follow them), etc. + if (!headers.ContainsKey(Consts.HttpStatusCodeName)) + { + throw new WebSocketHandshakeException( + $"Handshake with remote [{client.RemoteEndPoint}] failed due to lack of status code."); + } + + if (!headers.ContainsKey(Consts.HttpStatusCodeDescription)) + { + throw new WebSocketHandshakeException( + $"Handshake with remote [{client.RemoteEndPoint}] failed due to lack of status description."); + } + + if (headers[Consts.HttpStatusCodeName] == ((int)HttpStatusCode.BadRequest).ToString()) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to bad request [{1}].", + client.RemoteEndPoint, headers[Consts.HttpStatusCodeName])); + } + + if (headers[Consts.HttpStatusCodeName] != ((int)HttpStatusCode.SwitchingProtocols).ToString()) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to expected 101 Switching Protocols but received [{1}].", + client.RemoteEndPoint, headers[Consts.HttpStatusCodeName])); + } + + // If the response lacks an |Upgrade| header field or the |Upgrade| + // header field contains a value that is not an ASCII case- + // insensitive match for the value "websocket", the client MUST + // _Fail the WebSocket Connection_. + if (!headers.ContainsKey(HttpKnownHeaderNames.Connection)) + { + throw new WebSocketHandshakeException( + $"Handshake with remote [{client.RemoteEndPoint}] failed due to lack of connection header item."); + } + + if (headers[HttpKnownHeaderNames.Connection].ToLowerInvariant() != + Consts.WebSocketConnectionToken.ToLowerInvariant()) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to invalid connection header item value [{1}].", + client.RemoteEndPoint, headers[HttpKnownHeaderNames.Connection])); + } + + // If the response lacks a |Connection| header field or the + // |Connection| header field doesn't contain a token that is an + // ASCII case-insensitive match for the value "Upgrade", the client + // MUST _Fail the WebSocket Connection_. + if (!headers.ContainsKey(HttpKnownHeaderNames.Upgrade)) + { + throw new WebSocketHandshakeException( + $"Handshake with remote [{client.RemoteEndPoint}] failed due to lack of upgrade header item."); + } + + if (headers[HttpKnownHeaderNames.Upgrade].ToLowerInvariant() != + Consts.WebSocketUpgradeToken.ToLowerInvariant()) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to invalid upgrade header item value [{1}].", + client.RemoteEndPoint, headers[HttpKnownHeaderNames.Upgrade])); + } + + // If the response lacks a |Sec-WebSocket-Accept| header field or + // the |Sec-WebSocket-Accept| contains a value other than the + // base64-encoded SHA-1 of the concatenation of the |Sec-WebSocket- + // Key| (as a string, not base64-decoded) with the string "258EAFA5- + // E914-47DA-95CA-C5AB0DC85B11" but ignoring any leading and + // trailing whitespace, the client MUST _Fail the WebSocket Connection_. + if (!headers.ContainsKey(HttpKnownHeaderNames.SecWebSocketAccept)) + { + throw new WebSocketHandshakeException( + $"Handshake with remote [{client.RemoteEndPoint}] failed due to lack of Sec-WebSocket-Accept header item."); + } + + var challenge = GetSecWebSocketAcceptString(secWebSocketKey); + if (!headers[HttpKnownHeaderNames.SecWebSocketAccept].Equals(challenge, StringComparison.OrdinalIgnoreCase)) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to invalid Sec-WebSocket-Accept header item value [{1}].", + client.RemoteEndPoint, headers[HttpKnownHeaderNames.SecWebSocketAccept])); + } + + // If the response includes a |Sec-WebSocket-Extensions| header + // field and this header field indicates the use of an extension + // that was not present in the client's handshake (the server has + // indicated an extension not requested by the client), the client + // MUST _Fail the WebSocket Connection_. + if (extensions != null) + { + foreach (var extension in extensions) + // The empty string is not the same as the null value for these + // purposes and is not a legal value for this field. + { + if (string.IsNullOrWhiteSpace(extension)) + { + throw new WebSocketHandshakeException( + $"Handshake with remote [{client.RemoteEndPoint}] failed due to empty extension."); + } + } + + client.AgreeExtensions(extensions); + } + + // If the response includes a |Sec-WebSocket-Protocol| header field + // and this header field indicates the use of a subprotocol that was + // not present in the client's handshake (the server has indicated a + // subprotocol not requested by the client), the client MUST _Fail + // the WebSocket Connection_. + if (protocols != null) + { + if (!protocols.Any()) + { + throw new WebSocketHandshakeException( + $"Handshake with remote [{client.RemoteEndPoint}] failed due to empty sub-protocol."); + } + + if (protocols.Count > 1) + { + throw new WebSocketHandshakeException( + $"Handshake with remote [{client.RemoteEndPoint}] failed due to suggest to use multiple sub-protocols."); + } + + foreach (var protocol in protocols) + // The empty string is not the same as the null value for these + // purposes and is not a legal value for this field. + { + if (string.IsNullOrWhiteSpace(protocol)) + { + throw new WebSocketHandshakeException( + $"Handshake with remote [{client.RemoteEndPoint}] failed due to empty sub-protocol."); + } + } + + var suggestedProtocols = protocols.First().Split(',') + .Select(p => p.TrimStart().TrimEnd()).Where(p => !string.IsNullOrWhiteSpace(p)); + + if (!suggestedProtocols.Any()) + { + throw new WebSocketHandshakeException( + $"Handshake with remote [{client.RemoteEndPoint}] failed due to invalid sub-protocol."); + } + + if (suggestedProtocols.Count() > 1) + { + throw new WebSocketHandshakeException( + $"Handshake with remote [{client.RemoteEndPoint}] failed due to suggest to use multiple sub-protocols."); + } + + // The value chosen MUST be derived + // from the client's handshake, specifically by selecting one of + // the values from the |Sec-WebSocket-Protocol| field that the + // server is willing to use for this connection (if any). + client.UseSubProtocol(suggestedProtocols.First()); + } + } + catch (Exception ex) + { + NetworkHelper.Logger.Error($"{client.RemoteEndPoint}{Environment.NewLine}{ex.FormatExceptionToMessage()}"); + throw; } - private static void ParseOpenningHandshakeResponseHeaders(string response, - out Dictionary headers, - out List extensions, - out List protocols) + return true; + } + + private static void ParseOpenningHandshakeResponseHeaders(string response, + out Dictionary headers, + out List extensions, + out List protocols) + { + headers = new Dictionary(); + + // The |Sec-WebSocket-Extensions| header field MAY appear multiple times + // in an HTTP request (which is logically the same as a single + // |Sec-WebSocket-Extensions| header field that contains all values. + // However, the |Sec-WebSocket-Extensions| header field MUST NOT appear + // more than once in an HTTP response. + extensions = null; + // The |Sec-WebSocket-Protocol| header field MAY appear multiple times + // in an HTTP request (which is logically the same as a single + // |Sec-WebSocket-Protocol| header field that contains all values). + // However, the |Sec-WebSocket-Protocol| header field MUST NOT appear + // more than once in an HTTP response. + protocols = null; + + var lines = response.Split(_headerLineSplitter).Where(l => l.Length > 0); + foreach (var line in lines) + // HTTP/1.1 101 Switching Protocols + // HTTP/1.1 400 Bad Request { - headers = new Dictionary(); - - // The |Sec-WebSocket-Extensions| header field MAY appear multiple times - // in an HTTP request (which is logically the same as a single - // |Sec-WebSocket-Extensions| header field that contains all values. - // However, the |Sec-WebSocket-Extensions| header field MUST NOT appear - // more than once in an HTTP response. - extensions = null; - // The |Sec-WebSocket-Protocol| header field MAY appear multiple times - // in an HTTP request (which is logically the same as a single - // |Sec-WebSocket-Protocol| header field that contains all values). - // However, the |Sec-WebSocket-Protocol| header field MUST NOT appear - // more than once in an HTTP response. - protocols = null; - - var lines = response.Split(_headerLineSplitter).Where(l => l.Length > 0); - foreach (var line in lines) + if (line.StartsWith(@"HTTP/")) { - // HTTP/1.1 101 Switching Protocols - // HTTP/1.1 400 Bad Request - if (line.StartsWith(@"HTTP/")) + var segements = line.Split(' '); + if (segements.Length > 1) { - var segements = line.Split(' '); - if (segements.Length > 1) - { - headers.Add(Consts.HttpStatusCodeName, segements[1]); + headers.Add(Consts.HttpStatusCodeName, segements[1]); - if (segements.Length > 2) - { - headers.Add(Consts.HttpStatusCodeDescription, segements[2]); - } + if (segements.Length > 2) + { + headers.Add(Consts.HttpStatusCodeDescription, segements[2]); } } - else + } + else + { + foreach (var key in HttpKnownHeaderNames.All) { - foreach (var key in HttpKnownHeaderNames.All) + if (line.StartsWith(key + ":")) { - if (line.StartsWith(key + ":")) + var index = line.IndexOf(':'); + if (index != -1) { - var index = line.IndexOf(':'); - if (index != -1) + var value = line.Substring(index + 1); + + if (key == HttpKnownHeaderNames.SecWebSocketExtensions) { - var value = line.Substring(index + 1); - - if (key == HttpKnownHeaderNames.SecWebSocketExtensions) + if (extensions == null) { - if (extensions == null) - { - extensions = new List(); - } - - extensions.Add(value.Trim()); + extensions = new List(); } - else if (key == HttpKnownHeaderNames.SecWebSocketProtocol) - { - if (protocols == null) - { - protocols = new List(); - } - protocols.Add(value.Trim()); + extensions.Add(value.Trim()); + } + else if (key == HttpKnownHeaderNames.SecWebSocketProtocol) + { + if (protocols == null) + { + protocols = new List(); + } + + protocols.Add(value.Trim()); + } + else + { + if (headers.ContainsKey(key)) + { + headers[key] = string.Join(",", headers[key], value.Trim()); } else { - if (headers.ContainsKey(key)) - { - headers[key] = string.Join(",", headers[key], value.Trim()); - } - else - { - headers.Add(key, value.Trim()); - } + headers.Add(key, value.Trim()); } } } @@ -396,19 +401,19 @@ namespace EonaCat.WebSockets } } } - - private static string GetSecWebSocketAcceptString(string secWebSocketKey) - { - string retVal; - - using (SHA1 sha1 = SHA1.Create()) - { - string acceptString = string.Concat(secWebSocketKey, Consts.SecWebSocketKeyGuid); - byte[] toHash = Encoding.UTF8.GetBytes(acceptString); - retVal = Convert.ToBase64String(sha1.ComputeHash(toHash)); - } - - return retVal; - } } -} + + private static string GetSecWebSocketAcceptString(string secWebSocketKey) + { + string retVal; + + using (var sha1 = SHA1.Create()) + { + var acceptString = string.Concat(secWebSocketKey, Consts.SecWebSocketKeyGuid); + var toHash = Encoding.UTF8.GetBytes(acceptString); + retVal = Convert.ToBase64String(sha1.ComputeHash(toHash)); + } + + return retVal; + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Exceptions/WebSocketException.cs b/EonaCat.Network/System/Sockets/WebSockets/Exceptions/WebSocketException.cs index b70e425..f19d9ed 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Exceptions/WebSocketException.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Exceptions/WebSocketException.cs @@ -1,21 +1,19 @@ using System; -namespace EonaCat.WebSockets +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +[Serializable] +public class WebSocketException : Exception { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - - [Serializable] - public class WebSocketException : Exception + public WebSocketException(string message) + : base(message) { - public WebSocketException(string message) - : base(message) - { - } - - public WebSocketException(string message, Exception innerException) - : base(message, innerException) - { - } } -} + + public WebSocketException(string message, Exception innerException) + : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Exceptions/WebSocketHandshakeException.cs b/EonaCat.Network/System/Sockets/WebSockets/Exceptions/WebSocketHandshakeException.cs index 05cd39c..b069480 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Exceptions/WebSocketHandshakeException.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Exceptions/WebSocketHandshakeException.cs @@ -1,21 +1,19 @@ using System; -namespace EonaCat.WebSockets +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +[Serializable] +public sealed class WebSocketHandshakeException : WebSocketException { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - - [Serializable] - public sealed class WebSocketHandshakeException : WebSocketException + public WebSocketHandshakeException(string message) + : base(message) { - public WebSocketHandshakeException(string message) - : base(message) - { - } - - public WebSocketHandshakeException(string message, Exception innerException) - : base(message, innerException) - { - } } -} + + public WebSocketHandshakeException(string message, Exception innerException) + : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Extensions/IWebSocketExtension.cs b/EonaCat.Network/System/Sockets/WebSockets/Extensions/IWebSocketExtension.cs index d62ca75..9a11cb4 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Extensions/IWebSocketExtension.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Extensions/IWebSocketExtension.cs @@ -1,21 +1,19 @@ -namespace EonaCat.WebSockets.Extensions +namespace EonaCat.WebSockets.Extensions; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public interface IWebSocketExtension { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + string Name { get; } - public interface IWebSocketExtension - { - string Name { get; } + bool Rsv1BitOccupied { get; } + bool Rsv2BitOccupied { get; } + bool Rsv3BitOccupied { get; } - bool Rsv1BitOccupied { get; } - bool Rsv2BitOccupied { get; } - bool Rsv3BitOccupied { get; } + string GetAgreedOffer(); - string GetAgreedOffer(); + byte[] BuildExtensionData(byte[] payload, int offset, int count); - byte[] BuildExtensionData(byte[] payload, int offset, int count); - - byte[] ProcessIncomingMessagePayload(byte[] payload, int offset, int count); - byte[] ProcessOutgoingMessagePayload(byte[] payload, int offset, int count); - } -} + byte[] ProcessIncomingMessagePayload(byte[] payload, int offset, int count); + byte[] ProcessOutgoingMessagePayload(byte[] payload, int offset, int count); +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Extensions/IWebSocketExtensionNegotiator.cs b/EonaCat.Network/System/Sockets/WebSockets/Extensions/IWebSocketExtensionNegotiator.cs index 9a079ee..744534a 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Extensions/IWebSocketExtensionNegotiator.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Extensions/IWebSocketExtensionNegotiator.cs @@ -1,11 +1,9 @@ -namespace EonaCat.WebSockets.Extensions -{ - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. +namespace EonaCat.WebSockets.Extensions; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. - public interface IWebSocketExtensionNegotiator - { - bool NegotiateAsServer(string offer, out string invalidParameter, out IWebSocketExtension negotiatedExtension); - bool NegotiateAsClient(string offer, out string invalidParameter, out IWebSocketExtension negotiatedExtension); - } -} +public interface IWebSocketExtensionNegotiator +{ + bool NegotiateAsServer(string offer, out string invalidParameter, out IWebSocketExtension negotiatedExtension); + bool NegotiateAsClient(string offer, out string invalidParameter, out IWebSocketExtension negotiatedExtension); +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/AbsentableValueParameter.cs b/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/AbsentableValueParameter.cs index f96d2c5..36ad8fa 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/AbsentableValueParameter.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/AbsentableValueParameter.cs @@ -1,34 +1,27 @@ using System; -namespace EonaCat.WebSockets.Extensions +namespace EonaCat.WebSockets.Extensions; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public class AbsentableValueParameter : ExtensionParameter { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - - public class AbsentableValueParameter : ExtensionParameter + public AbsentableValueParameter(string name, Func valueValidator, T defaultValue) + : base(name) { - public AbsentableValueParameter(string name, Func valueValidator, T defaultValue) - : base(name) + if (valueValidator == null) { - if (valueValidator == null) - { - throw new ArgumentNullException("valueValidator"); - } - - this.ValueValidator = valueValidator; - this.DefaultValue = defaultValue; + throw new ArgumentNullException("valueValidator"); } - public override ExtensionParameterType ParameterType - { - get - { - return ExtensionParameterType.Single | ExtensionParameterType.Valuable; - } - } - - public Func ValueValidator { get; private set; } - - public T DefaultValue { get; private set; } + ValueValidator = valueValidator; + DefaultValue = defaultValue; } -} + + public override ExtensionParameterType ParameterType => + ExtensionParameterType.Single | ExtensionParameterType.Valuable; + + public Func ValueValidator { get; private set; } + + public T DefaultValue { get; private set; } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/AgreedExtensionParameter.cs b/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/AgreedExtensionParameter.cs index 2f3effd..bdc5561 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/AgreedExtensionParameter.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/AgreedExtensionParameter.cs @@ -1,28 +1,26 @@ using System; -namespace EonaCat.WebSockets.Extensions +namespace EonaCat.WebSockets.Extensions; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public abstract class AgreedExtensionParameter { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - - public abstract class AgreedExtensionParameter + public AgreedExtensionParameter(string name) { - public AgreedExtensionParameter(string name) + if (string.IsNullOrWhiteSpace(name)) { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentNullException("name"); - } - - this.Name = name; + throw new ArgumentNullException("name"); } - public string Name { get; private set; } - public abstract ExtensionParameterType ParameterType { get; } - - public override string ToString() - { - return string.Format("{0}", this.Name); - } + Name = name; } -} + + public string Name { get; } + public abstract ExtensionParameterType ParameterType { get; } + + public override string ToString() + { + return $"{Name}"; + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/AgreedSingleParameter.cs b/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/AgreedSingleParameter.cs index 87a3b9e..b749315 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/AgreedSingleParameter.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/AgreedSingleParameter.cs @@ -1,21 +1,13 @@ -namespace EonaCat.WebSockets.Extensions +namespace EonaCat.WebSockets.Extensions; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public class AgreedSingleParameter : AgreedExtensionParameter { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - - public class AgreedSingleParameter : AgreedExtensionParameter + public AgreedSingleParameter(string name) + : base(name) { - public AgreedSingleParameter(string name) - : base(name) - { - } - - public override ExtensionParameterType ParameterType - { - get - { - return ExtensionParameterType.Single; - } - } } -} + + public override ExtensionParameterType ParameterType => ExtensionParameterType.Single; +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/AgreedValuableParameter.cs b/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/AgreedValuableParameter.cs index 313733b..e4e7800 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/AgreedValuableParameter.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/AgreedValuableParameter.cs @@ -1,29 +1,21 @@ -namespace EonaCat.WebSockets.Extensions +namespace EonaCat.WebSockets.Extensions; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public class AgreedValuableParameter : AgreedExtensionParameter { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - - public class AgreedValuableParameter : AgreedExtensionParameter + public AgreedValuableParameter(string name, T value) + : base(name) { - public AgreedValuableParameter(string name, T @value) - : base(name) - { - this.Value = @value; - } - - public override ExtensionParameterType ParameterType - { - get - { - return ExtensionParameterType.Valuable; - } - } - - public T Value { get; private set; } - - public override string ToString() - { - return string.Format("{0}={1}", this.Name, this.Value); - } + Value = value; } -} + + public override ExtensionParameterType ParameterType => ExtensionParameterType.Valuable; + + public T Value { get; } + + public override string ToString() + { + return $"{Name}={Value}"; + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/ExtensionParameter.cs b/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/ExtensionParameter.cs index 6b24153..13d6532 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/ExtensionParameter.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/ExtensionParameter.cs @@ -1,28 +1,26 @@ using System; -namespace EonaCat.WebSockets.Extensions +namespace EonaCat.WebSockets.Extensions; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public abstract class ExtensionParameter { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - - public abstract class ExtensionParameter + public ExtensionParameter(string name) { - public ExtensionParameter(string name) + if (string.IsNullOrWhiteSpace(name)) { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentNullException("name"); - } - - this.Name = name; + throw new ArgumentNullException("name"); } - public string Name { get; private set; } - public abstract ExtensionParameterType ParameterType { get; } - - public override string ToString() - { - return string.Format("{0}", this.Name); - } + Name = name; } -} + + public string Name { get; } + public abstract ExtensionParameterType ParameterType { get; } + + public override string ToString() + { + return $"{Name}"; + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/ExtensionParameterType.cs b/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/ExtensionParameterType.cs index 6c8d0ed..aaef104 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/ExtensionParameterType.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/ExtensionParameterType.cs @@ -1,14 +1,12 @@ using System; -namespace EonaCat.WebSockets.Extensions -{ - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. +namespace EonaCat.WebSockets.Extensions; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. - [Flags] - public enum ExtensionParameterType : byte - { - Single = 0x1, - Valuable = 0x2, - } -} +[Flags] +public enum ExtensionParameterType : byte +{ + Single = 0x1, + Valuable = 0x2 +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/SingleParameter.cs b/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/SingleParameter.cs index 24087b3..959dc26 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/SingleParameter.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/SingleParameter.cs @@ -1,21 +1,13 @@ -namespace EonaCat.WebSockets.Extensions +namespace EonaCat.WebSockets.Extensions; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public class SingleParameter : ExtensionParameter { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - - public class SingleParameter : ExtensionParameter + public SingleParameter(string name) + : base(name) { - public SingleParameter(string name) - : base(name) - { - } - - public override ExtensionParameterType ParameterType - { - get - { - return ExtensionParameterType.Single; - } - } } -} + + public override ExtensionParameterType ParameterType => ExtensionParameterType.Single; +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/ValuableParameter.cs b/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/ValuableParameter.cs index 644fb5a..58992ef 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/ValuableParameter.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Extensions/Parameters/ValuableParameter.cs @@ -1,31 +1,23 @@ using System; -namespace EonaCat.WebSockets.Extensions +namespace EonaCat.WebSockets.Extensions; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public class ValuableParameter : ExtensionParameter { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - - public class ValuableParameter : ExtensionParameter + public ValuableParameter(string name, Func valueValidator) + : base(name) { - public ValuableParameter(string name, Func valueValidator) - : base(name) + if (valueValidator == null) { - if (valueValidator == null) - { - throw new ArgumentNullException("valueValidator"); - } - - this.ValueValidator = valueValidator; + throw new ArgumentNullException("valueValidator"); } - public override ExtensionParameterType ParameterType - { - get - { - return ExtensionParameterType.Valuable; - } - } - - public Func ValueValidator { get; private set; } + ValueValidator = valueValidator; } -} + + public override ExtensionParameterType ParameterType => ExtensionParameterType.Valuable; + + public Func ValueValidator { get; private set; } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Extensions/PerMessageExtensions/PMCE/DeflateCompression.cs b/EonaCat.Network/System/Sockets/WebSockets/Extensions/PerMessageExtensions/PMCE/DeflateCompression.cs index fda5ba9..e630953 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Extensions/PerMessageExtensions/PMCE/DeflateCompression.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Extensions/PerMessageExtensions/PMCE/DeflateCompression.cs @@ -4,78 +4,75 @@ using System.IO; using System.IO.Compression; using EonaCat.WebSockets.Buffer; -namespace EonaCat.WebSockets.Extensions +namespace EonaCat.WebSockets.Extensions; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public class DeflateCompression { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private readonly ISegmentBufferManager _bufferAllocator; - public class DeflateCompression + public DeflateCompression(ISegmentBufferManager bufferAllocator) { - private readonly ISegmentBufferManager _bufferAllocator; - - public DeflateCompression(ISegmentBufferManager bufferAllocator) + if (bufferAllocator == null) { - if (bufferAllocator == null) + throw new ArgumentNullException("bufferAllocator"); + } + + _bufferAllocator = bufferAllocator; + } + + public byte[] Compress(byte[] raw) + { + return Compress(raw, 0, raw.Length); + } + + [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")] + public byte[] Compress(byte[] raw, int offset, int count) + { + using (var memory = new MemoryStream()) + { + using (var deflate = new DeflateStream(memory, CompressionMode.Compress, true)) { - throw new ArgumentNullException("bufferAllocator"); + deflate.Write(raw, offset, count); } - _bufferAllocator = bufferAllocator; + return memory.ToArray(); } + } - public byte[] Compress(byte[] raw) - { - return Compress(raw, 0, raw.Length); - } + public byte[] Decompress(byte[] raw) + { + return Decompress(raw, 0, raw.Length); + } - [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")] - public byte[] Compress(byte[] raw, int offset, int count) + [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")] + public byte[] Decompress(byte[] raw, int offset, int count) + { + var buffer = _bufferAllocator.BorrowBuffer(); + + try { + using (var input = new MemoryStream(raw, offset, count)) + using (var deflate = new DeflateStream(input, CompressionMode.Decompress, true)) using (var memory = new MemoryStream()) { - using (var deflate = new DeflateStream(memory, CompressionMode.Compress, leaveOpen: true)) + var readCount = 0; + do { - deflate.Write(raw, offset, count); - } + readCount = deflate.Read(buffer.Array, buffer.Offset, buffer.Count); + if (readCount > 0) + { + memory.Write(buffer.Array, buffer.Offset, readCount); + } + } while (readCount > 0); return memory.ToArray(); } } - - public byte[] Decompress(byte[] raw) + finally { - return Decompress(raw, 0, raw.Length); - } - - [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")] - public byte[] Decompress(byte[] raw, int offset, int count) - { - var buffer = _bufferAllocator.BorrowBuffer(); - - try - { - using (var input = new MemoryStream(raw, offset, count)) - using (var deflate = new DeflateStream(input, CompressionMode.Decompress, leaveOpen: true)) - using (var memory = new MemoryStream()) - { - int readCount = 0; - do - { - readCount = deflate.Read(buffer.Array, buffer.Offset, buffer.Count); - if (readCount > 0) - { - memory.Write(buffer.Array, buffer.Offset, readCount); - } - } - while (readCount > 0); - - return memory.ToArray(); - } - } - finally - { - _bufferAllocator.ReturnBuffer(buffer); - } + _bufferAllocator.ReturnBuffer(buffer); } } -} +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Extensions/PerMessageExtensions/PMCE/PerMessageCompressionExtension.cs b/EonaCat.Network/System/Sockets/WebSockets/Extensions/PerMessageExtensions/PMCE/PerMessageCompressionExtension.cs index 201dbbe..7134a5f 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Extensions/PerMessageExtensions/PMCE/PerMessageCompressionExtension.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Extensions/PerMessageExtensions/PMCE/PerMessageCompressionExtension.cs @@ -3,95 +3,93 @@ using System.Linq; using System.Text; using EonaCat.WebSockets.Buffer; -namespace EonaCat.WebSockets.Extensions +namespace EonaCat.WebSockets.Extensions; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public sealed class PerMessageCompressionExtension : IWebSocketExtension { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + // Any extension-token used MUST be a registered token (see + // Section 11.4). The parameters supplied with any given extension MUST + // be defined for that extension. Note that the client is only offering + // to use any advertised extensions and MUST NOT use them unless the + // server indicates that it wishes to use the extension. + public static readonly string RegisteredToken = @"permessage-deflate"; + private readonly SortedList _agreedParameters; - public sealed class PerMessageCompressionExtension : IWebSocketExtension + private readonly DeflateCompression _deflater; + + public PerMessageCompressionExtension() { - // Any extension-token used MUST be a registered token (see - // Section 11.4). The parameters supplied with any given extension MUST - // be defined for that extension. Note that the client is only offering - // to use any advertised extensions and MUST NOT use them unless the - // server indicates that it wishes to use the extension. - public static readonly string RegisteredToken = @"permessage-deflate"; - - private readonly DeflateCompression _deflater; - private SortedList _agreedParameters; - - public PerMessageCompressionExtension() - { - var bufferAllocator = new SegmentBufferManager(100, 8192, 1, true); - _deflater = new DeflateCompression(bufferAllocator); - } - - public PerMessageCompressionExtension(SortedList agreedParameters) - : this() - { - _agreedParameters = agreedParameters; - } - - public string Name { get { return RegisteredToken; } } - - // PMCEs use the RSV1 bit of the WebSocket frame header to indicate whether a - // message is compressed or not so that an endpoint can choose not to - // compress messages with incompressible contents. - public bool Rsv1BitOccupied { get { return true; } } - public bool Rsv2BitOccupied { get { return false; } } - public bool Rsv3BitOccupied { get { return false; } } - - public string GetAgreedOffer() - { - var sb = new StringBuilder(); - - sb.Append(this.Name); - - if (_agreedParameters != null && _agreedParameters.Any()) - { - foreach (var parameter in _agreedParameters.Values) - { - sb.Append("; "); - sb.Append(parameter.ToString()); - } - } - - return sb.ToString(); - } - - public byte[] BuildExtensionData(byte[] payload, int offset, int count) - { - // Payload data: (x+y) bytes - // - // The "Payload data" is defined as "Extension data" concatenated - // with "Application data". - // - // Extension data: x bytes - // - // The "Extension data" is 0 bytes unless an extension has been - // negotiated. Any extension MUST specify the length of the - // "Extension data", or how that length may be calculated, and how - // the extension use MUST be negotiated during the opening handshake. - // If present, the "Extension data" is included in the total payload - // length. - // - // Application data: y bytes - // - // Arbitrary "Application data", taking up the remainder of the frame - // after any "Extension data". The length of the "Application data" - // is equal to the payload length minus the length of the "Extension - // data". - return null; // PMCE doesn't have an extension data definition. - } - - public byte[] ProcessIncomingMessagePayload(byte[] payload, int offset, int count) - { - return _deflater.Decompress(payload, offset, count); - } - - public byte[] ProcessOutgoingMessagePayload(byte[] payload, int offset, int count) - { - return _deflater.Compress(payload, offset, count); - } + var bufferAllocator = new SegmentBufferManager(100, 8192, 1, true); + _deflater = new DeflateCompression(bufferAllocator); } -} + + public PerMessageCompressionExtension(SortedList agreedParameters) + : this() + { + _agreedParameters = agreedParameters; + } + + public string Name => RegisteredToken; + + // PMCEs use the RSV1 bit of the WebSocket frame header to indicate whether a + // message is compressed or not so that an endpoint can choose not to + // compress messages with incompressible contents. + public bool Rsv1BitOccupied => true; + public bool Rsv2BitOccupied => false; + public bool Rsv3BitOccupied => false; + + public string GetAgreedOffer() + { + var sb = new StringBuilder(); + + sb.Append(Name); + + if (_agreedParameters != null && _agreedParameters.Any()) + { + foreach (var parameter in _agreedParameters.Values) + { + sb.Append("; "); + sb.Append(parameter); + } + } + + return sb.ToString(); + } + + public byte[] BuildExtensionData(byte[] payload, int offset, int count) + { + // Payload data: (x+y) bytes + // + // The "Payload data" is defined as "Extension data" concatenated + // with "Application data". + // + // Extension data: x bytes + // + // The "Extension data" is 0 bytes unless an extension has been + // negotiated. Any extension MUST specify the length of the + // "Extension data", or how that length may be calculated, and how + // the extension use MUST be negotiated during the opening handshake. + // If present, the "Extension data" is included in the total payload + // length. + // + // Application data: y bytes + // + // Arbitrary "Application data", taking up the remainder of the frame + // after any "Extension data". The length of the "Application data" + // is equal to the payload length minus the length of the "Extension + // data". + return null; // PMCE doesn't have an extension data definition. + } + + public byte[] ProcessIncomingMessagePayload(byte[] payload, int offset, int count) + { + return _deflater.Decompress(payload, offset, count); + } + + public byte[] ProcessOutgoingMessagePayload(byte[] payload, int offset, int count) + { + return _deflater.Compress(payload, offset, count); + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Extensions/PerMessageExtensions/PMCE/PerMessageCompressionExtensionNegotiator.cs b/EonaCat.Network/System/Sockets/WebSockets/Extensions/PerMessageExtensions/PMCE/PerMessageCompressionExtensionNegotiator.cs index 3721a30..2843d30 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Extensions/PerMessageExtensions/PMCE/PerMessageCompressionExtensionNegotiator.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Extensions/PerMessageExtensions/PMCE/PerMessageCompressionExtensionNegotiator.cs @@ -1,135 +1,138 @@ using System; using System.Collections.Generic; -namespace EonaCat.WebSockets.Extensions +namespace EonaCat.WebSockets.Extensions; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public sealed class PerMessageCompressionExtensionNegotiator : IWebSocketExtensionNegotiator { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private static readonly char[] TrimableChars = { ' ', ';', '\r', '\n' }; - public sealed class PerMessageCompressionExtensionNegotiator : IWebSocketExtensionNegotiator + public bool NegotiateAsServer(string offer, out string invalidParameter, + out IWebSocketExtension negotiatedExtension) { - private static readonly char[] TrimableChars = new char[] { ' ', ';', '\r', '\n' }; + return Negotiate(offer, AgreeAsServer, out invalidParameter, out negotiatedExtension); + } - public bool NegotiateAsServer(string offer, out string invalidParameter, out IWebSocketExtension negotiatedExtension) + public bool NegotiateAsClient(string offer, out string invalidParameter, + out IWebSocketExtension negotiatedExtension) + { + return Negotiate(offer, AgreeAsClient, out invalidParameter, out negotiatedExtension); + } + + private bool Negotiate(string offer, Func agree, out string invalidParameter, + out IWebSocketExtension negotiatedExtension) + { + invalidParameter = null; + negotiatedExtension = null; + + if (string.IsNullOrWhiteSpace(offer)) { - return Negotiate(offer, AgreeAsServer, out invalidParameter, out negotiatedExtension); + invalidParameter = offer; + return false; } - public bool NegotiateAsClient(string offer, out string invalidParameter, out IWebSocketExtension negotiatedExtension) + var segements = offer.Replace('\r', ' ').Replace('\n', ' ').TrimStart(TrimableChars).TrimEnd(TrimableChars) + .Split(';'); + + var offeredExtensionName = segements[0].TrimStart(TrimableChars).TrimEnd(TrimableChars); + if (string.IsNullOrEmpty(offeredExtensionName)) { - return Negotiate(offer, AgreeAsClient, out invalidParameter, out negotiatedExtension); + invalidParameter = offer; + return false; } - private bool Negotiate(string offer, Func agree, out string invalidParameter, out IWebSocketExtension negotiatedExtension) + if (string.Compare(offeredExtensionName, PerMessageCompressionExtension.RegisteredToken, + StringComparison.OrdinalIgnoreCase) != 0) { - invalidParameter = null; - negotiatedExtension = null; + invalidParameter = offeredExtensionName; + return false; + } - if (string.IsNullOrWhiteSpace(offer)) - { - invalidParameter = offer; - return false; - } - - var segements = offer.Replace('\r', ' ').Replace('\n', ' ').TrimStart(TrimableChars).TrimEnd(TrimableChars).Split(';'); - - var offeredExtensionName = segements[0].TrimStart(TrimableChars).TrimEnd(TrimableChars); - if (string.IsNullOrEmpty(offeredExtensionName)) - { - invalidParameter = offer; - return false; - } - - if (string.Compare(offeredExtensionName, PerMessageCompressionExtension.RegisteredToken, StringComparison.OrdinalIgnoreCase) != 0) - { - invalidParameter = offeredExtensionName; - return false; - } - - if (segements.Length == 1) - { - negotiatedExtension = new PerMessageCompressionExtension(); - return true; - } - - // This set of elements MAY include multiple PMCEs with the same extension - // name to offer the possibility to use the same algorithm with - // different configuration parameters. - for (int i = 1; i < segements.Length; i++) - { - var offeredParameter = segements[i]; - if (!PerMessageCompressionExtensionParameters.ValidateParameter(offeredParameter)) - { - invalidParameter = offeredParameter; - return false; - } - } - - // The order of elements is important as it specifies the client's preference. - // An element preceding another element has higher preference. It is recommended - // that a server accepts PMCEs with higher preference if the server supports them. - var agreedSet = new SortedList(); - - for (int i = 1; i < segements.Length; i++) - { - var offeredParameter = segements[i]; - var agreeingParameter = PerMessageCompressionExtensionParameters.ResolveParameter(offeredParameter); - if (agree(agreeingParameter)) - { - agreedSet.Add(i, agreeingParameter); - } - } - - negotiatedExtension = new PerMessageCompressionExtension(agreedSet); + if (segements.Length == 1) + { + negotiatedExtension = new PerMessageCompressionExtension(); return true; } - private bool AgreeAsServer(AgreedExtensionParameter parameter) + // This set of elements MAY include multiple PMCEs with the same extension + // name to offer the possibility to use the same algorithm with + // different configuration parameters. + for (var i = 1; i < segements.Length; i++) { - if (parameter == null) + var offeredParameter = segements[i]; + if (!PerMessageCompressionExtensionParameters.ValidateParameter(offeredParameter)) { + invalidParameter = offeredParameter; return false; } - - switch (parameter.Name) - { - case PerMessageCompressionExtensionParameters.ServerNoContextTakeOverParameterName: - case PerMessageCompressionExtensionParameters.ClientNoContextTakeOverParameterName: - { - return false; - } - case PerMessageCompressionExtensionParameters.ServerMaxWindowBitsParameterName: - case PerMessageCompressionExtensionParameters.ClientMaxWindowBitsParameterName: - { - return false; - } - default: - throw new NotSupportedException("Invalid parameter name."); - } } - private bool AgreeAsClient(AgreedExtensionParameter parameter) + // The order of elements is important as it specifies the client's preference. + // An element preceding another element has higher preference. It is recommended + // that a server accepts PMCEs with higher preference if the server supports them. + var agreedSet = new SortedList(); + + for (var i = 1; i < segements.Length; i++) { - if (parameter == null) + var offeredParameter = segements[i]; + var agreeingParameter = PerMessageCompressionExtensionParameters.ResolveParameter(offeredParameter); + if (agree(agreeingParameter)) + { + agreedSet.Add(i, agreeingParameter); + } + } + + negotiatedExtension = new PerMessageCompressionExtension(agreedSet); + return true; + } + + private bool AgreeAsServer(AgreedExtensionParameter parameter) + { + if (parameter == null) + { + return false; + } + + switch (parameter.Name) + { + case PerMessageCompressionExtensionParameters.ServerNoContextTakeOverParameterName: + case PerMessageCompressionExtensionParameters.ClientNoContextTakeOverParameterName: { return false; } - - switch (parameter.Name) + case PerMessageCompressionExtensionParameters.ServerMaxWindowBitsParameterName: + case PerMessageCompressionExtensionParameters.ClientMaxWindowBitsParameterName: { - case PerMessageCompressionExtensionParameters.ServerNoContextTakeOverParameterName: - case PerMessageCompressionExtensionParameters.ClientNoContextTakeOverParameterName: - { - return false; - } - case PerMessageCompressionExtensionParameters.ServerMaxWindowBitsParameterName: - case PerMessageCompressionExtensionParameters.ClientMaxWindowBitsParameterName: - { - return false; - } - default: - throw new NotSupportedException("Invalid parameter name."); + return false; } + default: + throw new NotSupportedException("Invalid parameter name."); } } -} + + private bool AgreeAsClient(AgreedExtensionParameter parameter) + { + if (parameter == null) + { + return false; + } + + switch (parameter.Name) + { + case PerMessageCompressionExtensionParameters.ServerNoContextTakeOverParameterName: + case PerMessageCompressionExtensionParameters.ClientNoContextTakeOverParameterName: + { + return false; + } + case PerMessageCompressionExtensionParameters.ServerMaxWindowBitsParameterName: + case PerMessageCompressionExtensionParameters.ClientMaxWindowBitsParameterName: + { + return false; + } + default: + throw new NotSupportedException("Invalid parameter name."); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Extensions/PerMessageExtensions/PMCE/PerMessageCompressionExtensionParameters.cs b/EonaCat.Network/System/Sockets/WebSockets/Extensions/PerMessageExtensions/PMCE/PerMessageCompressionExtensionParameters.cs index 2eb1da0..86dd8b3 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Extensions/PerMessageExtensions/PMCE/PerMessageCompressionExtensionParameters.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Extensions/PerMessageExtensions/PMCE/PerMessageCompressionExtensionParameters.cs @@ -2,202 +2,205 @@ using System.Collections.Generic; using System.Linq; -namespace EonaCat.WebSockets.Extensions +namespace EonaCat.WebSockets.Extensions; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public sealed class PerMessageCompressionExtensionParameters { - // This file is part of the EonaCat 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 const string ServerNoContextTakeOverParameterName = @"server_no_context_takeover"; + public const string ClientNoContextTakeOverParameterName = @"client_no_context_takeover"; + public const string ServerMaxWindowBitsParameterName = @"server_max_window_bits"; + public const string ClientMaxWindowBitsParameterName = @"client_max_window_bits"; - public sealed class PerMessageCompressionExtensionParameters + public static readonly SingleParameter ServerNoContextTakeOver = new(ServerNoContextTakeOverParameterName); + public static readonly SingleParameter ClientNoContextTakeOver = new(ClientNoContextTakeOverParameterName); + + public static readonly AbsentableValueParameter ServerMaxWindowBits = + new(ServerMaxWindowBitsParameterName, ValidateServerMaxWindowBitsParameterValue, 15); + + public static readonly AbsentableValueParameter ClientMaxWindowBits = + new(ClientMaxWindowBitsParameterName, ValidateClientMaxWindowBitsParameterValue, 15); + + public static readonly IEnumerable AllAvailableParameters = new List { - public const string ServerNoContextTakeOverParameterName = @"server_no_context_takeover"; - public const string ClientNoContextTakeOverParameterName = @"client_no_context_takeover"; - public const string ServerMaxWindowBitsParameterName = @"server_max_window_bits"; - public const string ClientMaxWindowBitsParameterName = @"client_max_window_bits"; + ServerNoContextTakeOver, + ClientNoContextTakeOver, + ServerMaxWindowBits, + ClientMaxWindowBits + }; - public static readonly SingleParameter ServerNoContextTakeOver = new SingleParameter(ServerNoContextTakeOverParameterName); - public static readonly SingleParameter ClientNoContextTakeOver = new SingleParameter(ClientNoContextTakeOverParameterName); - public static readonly AbsentableValueParameter ServerMaxWindowBits = new AbsentableValueParameter(ServerMaxWindowBitsParameterName, ValidateServerMaxWindowBitsParameterValue, 15); - public static readonly AbsentableValueParameter ClientMaxWindowBits = new AbsentableValueParameter(ClientMaxWindowBitsParameterName, ValidateClientMaxWindowBitsParameterValue, 15); + public static readonly IEnumerable AllAvailableParameterNames = AllAvailableParameters.Select(p => p.Name); - public static readonly IEnumerable AllAvailableParameters = new List() + private static bool ValidateServerMaxWindowBitsParameterValue(string value) + { + // A client MAY include the "server_max_window_bits" extension parameter + // in an extension negotiation offer. This parameter has a decimal + // integer value without leading zeroes between 8 to 15, inclusive, + // indicating the base-2 logarithm of the LZ77 sliding window size, and + // MUST conform to the ABNF below. + // server-max-window-bits = 1*DIGIT + + if (string.IsNullOrWhiteSpace(value)) { - ServerNoContextTakeOver, - ClientNoContextTakeOver, - ServerMaxWindowBits, - ClientMaxWindowBits, - }; + return false; + } - public static readonly IEnumerable AllAvailableParameterNames = AllAvailableParameters.Select(p => p.Name); - - private static bool ValidateServerMaxWindowBitsParameterValue(string @value) + var paramValue = -1; + if (int.TryParse(value, out paramValue)) { - // A client MAY include the "server_max_window_bits" extension parameter - // in an extension negotiation offer. This parameter has a decimal - // integer value without leading zeroes between 8 to 15, inclusive, - // indicating the base-2 logarithm of the LZ77 sliding window size, and - // MUST conform to the ABNF below. - // server-max-window-bits = 1*DIGIT - - if (string.IsNullOrWhiteSpace(@value)) + if (8 <= paramValue && paramValue <= 15) { - return false; + return true; } + } - int paramValue = -1; - if (int.TryParse(@value, out paramValue)) + return false; + } + + private static bool ValidateClientMaxWindowBitsParameterValue(string value) + { + // A client MAY include the "client_max_window_bits" extension parameter + // in an extension negotiation offer. This parameter has no value or a + // decimal integer value without leading zeroes between 8 to 15 + // inclusive indicating the base-2 logarithm of the LZ77 sliding window + // size. If a value is specified for this parameter, the value MUST + // conform to the ABNF below. + // client-max-window-bits = 1*DIGIT + + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + + var paramValue = -1; + if (int.TryParse(value, out paramValue)) + { + if (8 <= paramValue && paramValue <= 15) { - if (8 <= paramValue && paramValue <= 15) + return true; + } + } + + return false; + } + + public static bool ValidateParameter(string parameter) + { + if (string.IsNullOrWhiteSpace(parameter)) + { + return false; + } + + var keyValuePair = parameter.TrimStart().TrimEnd().Split('='); + var inputParameterName = keyValuePair[0].TrimStart().TrimEnd(); + ExtensionParameter matchedParameter = null; + + foreach (var param in AllAvailableParameters) + { + if (string.Compare(inputParameterName, param.Name, StringComparison.OrdinalIgnoreCase) == 0) + { + matchedParameter = param; + break; + } + } + + if (matchedParameter == null) + { + return false; + } + + switch (matchedParameter.ParameterType) + { + case ExtensionParameterType.Single: + { + if (keyValuePair.Length == 1) { return true; } } - - return false; - } - - private static bool ValidateClientMaxWindowBitsParameterValue(string @value) - { - // A client MAY include the "client_max_window_bits" extension parameter - // in an extension negotiation offer. This parameter has no value or a - // decimal integer value without leading zeroes between 8 to 15 - // inclusive indicating the base-2 logarithm of the LZ77 sliding window - // size. If a value is specified for this parameter, the value MUST - // conform to the ABNF below. - // client-max-window-bits = 1*DIGIT - - if (string.IsNullOrWhiteSpace(@value)) + break; + case ExtensionParameterType.Valuable: { - return false; - } + if (keyValuePair.Length != 2) + { + return false; + } - int paramValue = -1; - if (int.TryParse(@value, out paramValue)) - { - if (8 <= paramValue && paramValue <= 15) + var inputParameterValue = keyValuePair[1].TrimStart().TrimEnd(); + if (((ValuableParameter)matchedParameter).ValueValidator.Invoke(inputParameterValue)) { return true; } } - - return false; - } - - public static bool ValidateParameter(string parameter) - { - if (string.IsNullOrWhiteSpace(parameter)) + break; + case ExtensionParameterType.Single | ExtensionParameterType.Valuable: { - return false; - } - - var keyValuePair = parameter.TrimStart().TrimEnd().Split('='); - var inputParameterName = keyValuePair[0].TrimStart().TrimEnd(); - ExtensionParameter matchedParameter = null; - - foreach (var @param in AllAvailableParameters) - { - if (string.Compare(inputParameterName, @param.Name, StringComparison.OrdinalIgnoreCase) == 0) + if (keyValuePair.Length == 1) { - matchedParameter = @param; - break; + return true; + } + + if (keyValuePair.Length > 2) + { + return false; + } + + var inputParameterValue = keyValuePair[1].TrimStart().TrimEnd(); + if (((AbsentableValueParameter)matchedParameter).ValueValidator.Invoke(inputParameterValue)) + { + return true; } } - - if (matchedParameter == null) - { - return false; - } - - switch (matchedParameter.ParameterType) - { - case ExtensionParameterType.Single: - { - if (keyValuePair.Length == 1) - { - return true; - } - } - break; - case ExtensionParameterType.Valuable: - { - if (keyValuePair.Length != 2) - { - return false; - } - - var inputParameterValue = keyValuePair[1].TrimStart().TrimEnd(); - if (((ValuableParameter)matchedParameter).ValueValidator.Invoke(inputParameterValue)) - { - return true; - } - } - break; - case ExtensionParameterType.Single | ExtensionParameterType.Valuable: - { - if (keyValuePair.Length == 1) - { - return true; - } - - if (keyValuePair.Length > 2) - { - return false; - } - - var inputParameterValue = keyValuePair[1].TrimStart().TrimEnd(); - if (((AbsentableValueParameter)matchedParameter).ValueValidator.Invoke(inputParameterValue)) - { - return true; - } - } - break; - default: - throw new NotSupportedException("Invalid parameter type."); - } - - return false; + break; + default: + throw new NotSupportedException("Invalid parameter type."); } - public static AgreedExtensionParameter ResolveParameter(string parameter) + return false; + } + + public static AgreedExtensionParameter ResolveParameter(string parameter) + { + if (!ValidateParameter(parameter)) { - if (!ValidateParameter(parameter)) + return null; + } + + var keyValuePair = parameter.TrimStart().TrimEnd().Split('='); + var inputParameterName = keyValuePair[0].TrimStart().TrimEnd(); + ExtensionParameter matchedParameter = null; + + foreach (var param in AllAvailableParameters) + { + if (string.Compare(inputParameterName, param.Name, StringComparison.OrdinalIgnoreCase) == 0) { - return null; + matchedParameter = param; + break; } + } - var keyValuePair = parameter.TrimStart().TrimEnd().Split('='); - var inputParameterName = keyValuePair[0].TrimStart().TrimEnd(); - ExtensionParameter matchedParameter = null; - - foreach (var @param in AllAvailableParameters) + switch (matchedParameter.Name) + { + case ServerNoContextTakeOverParameterName: + case ClientNoContextTakeOverParameterName: { - if (string.Compare(inputParameterName, @param.Name, StringComparison.OrdinalIgnoreCase) == 0) + return new AgreedSingleParameter(matchedParameter.Name); + } + case ServerMaxWindowBitsParameterName: + case ClientMaxWindowBitsParameterName: + { + if (keyValuePair.Length == 1) { - matchedParameter = @param; - break; + return new AgreedValuableParameter(matchedParameter.Name, + ((AbsentableValueParameter)matchedParameter).DefaultValue); } - } - switch (matchedParameter.Name) - { - case ServerNoContextTakeOverParameterName: - case ClientNoContextTakeOverParameterName: - { - return new AgreedSingleParameter(matchedParameter.Name); - } - case ServerMaxWindowBitsParameterName: - case ClientMaxWindowBitsParameterName: - { - if (keyValuePair.Length == 1) - { - return new AgreedValuableParameter(matchedParameter.Name, ((AbsentableValueParameter)matchedParameter).DefaultValue); - } - - var inputParameterValue = keyValuePair[1].TrimStart().TrimEnd(); - return new AgreedValuableParameter(matchedParameter.Name, byte.Parse(inputParameterValue)); - } - default: - throw new NotSupportedException("Invalid parameter type."); + var inputParameterValue = keyValuePair[1].TrimStart().TrimEnd(); + return new AgreedValuableParameter(matchedParameter.Name, byte.Parse(inputParameterValue)); } + default: + throw new NotSupportedException("Invalid parameter type."); } } -} +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Extensions/WebSocketExtensionOfferDescription.cs b/EonaCat.Network/System/Sockets/WebSockets/Extensions/WebSocketExtensionOfferDescription.cs index 96f3f99..a44b680 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Extensions/WebSocketExtensionOfferDescription.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Extensions/WebSocketExtensionOfferDescription.cs @@ -1,22 +1,20 @@ using System; -namespace EonaCat.WebSockets.Extensions +namespace EonaCat.WebSockets.Extensions; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public sealed class WebSocketExtensionOfferDescription { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - - public sealed class WebSocketExtensionOfferDescription + public WebSocketExtensionOfferDescription(string offer) { - public WebSocketExtensionOfferDescription(string offer) + if (string.IsNullOrWhiteSpace(offer)) { - if (string.IsNullOrWhiteSpace(offer)) - { - throw new ArgumentNullException("offer"); - } - - this.ExtensionNegotiationOffer = offer; + throw new ArgumentNullException("offer"); } - public string ExtensionNegotiationOffer { get; private set; } + ExtensionNegotiationOffer = offer; } -} + + public string ExtensionNegotiationOffer { get; private set; } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Framing/BinaryFragmentationFrame.cs b/EonaCat.Network/System/Sockets/WebSockets/Framing/BinaryFragmentationFrame.cs index cf3b473..be1683e 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Framing/BinaryFragmentationFrame.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Framing/BinaryFragmentationFrame.cs @@ -1,46 +1,40 @@ using System; using EonaCat.WebSockets.Buffer; -namespace EonaCat.WebSockets +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public sealed class BinaryFragmentationFrame : Frame { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - - public sealed class BinaryFragmentationFrame : Frame + public BinaryFragmentationFrame(OpCode opCode, byte[] data, int offset, int count, bool isFin = false, + bool isMasked = true) { - private OpCode _opCode; + BufferValidator.ValidateBuffer(data, offset, count, "data"); - public BinaryFragmentationFrame(OpCode opCode, byte[] data, int offset, int count, bool isFin = false, bool isMasked = true) - { - BufferValidator.ValidateBuffer(data, offset, count, "data"); - - _opCode = opCode; - this.Data = data; - this.Offset = offset; - this.Count = count; - this.IsFin = isFin; - this.IsMasked = isMasked; - } - - public byte[] Data { get; private set; } - public int Offset { get; private set; } - public int Count { get; private set; } - public bool IsFin { get; private set; } - public bool IsMasked { get; private set; } - - public override OpCode OpCode - { - get { return _opCode; } - } - - public byte[] ToArray(IFrameBuilder builder) - { - if (builder == null) - { - throw new ArgumentNullException("builder"); - } - - return builder.EncodeFrame(this); - } + OpCode = opCode; + Data = data; + Offset = offset; + Count = count; + IsFin = isFin; + IsMasked = isMasked; } -} + + public byte[] Data { get; private set; } + public int Offset { get; private set; } + public int Count { get; private set; } + public bool IsFin { get; private set; } + public bool IsMasked { get; private set; } + + public override OpCode OpCode { get; } + + public byte[] ToArray(IFrameBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException("builder"); + } + + return builder.EncodeFrame(this); + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Framing/BinaryFrame.cs b/EonaCat.Network/System/Sockets/WebSockets/Framing/BinaryFrame.cs index 97badef..d96c8fd 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Framing/BinaryFrame.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Framing/BinaryFrame.cs @@ -1,51 +1,46 @@ using System; using EonaCat.WebSockets.Buffer; -namespace EonaCat.WebSockets +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public sealed class BinaryFrame : DataFrame { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - - public sealed class BinaryFrame : DataFrame + public BinaryFrame(ArraySegment segment, bool isMasked = true) { - public BinaryFrame(ArraySegment segment, bool isMasked = true) - { - BufferValidator.ValidateArraySegment(segment, "segment"); + BufferValidator.ValidateArraySegment(segment, "segment"); - this.Data = segment.Array; - this.Offset = segment.Offset; - this.Count = segment.Count; - this.IsMasked = isMasked; - } - - public BinaryFrame(byte[] data, int offset, int count, bool isMasked = true) - { - BufferValidator.ValidateBuffer(data, offset, count, "data"); - - this.Data = data; - this.Offset = offset; - this.Count = count; - this.IsMasked = isMasked; - } - - public byte[] Data { get; private set; } - public int Offset { get; private set; } - public int Count { get; private set; } - public bool IsMasked { get; private set; } - - public override OpCode OpCode - { - get { return OpCode.Binary; } - } - - public byte[] ToArray(IFrameBuilder builder) - { - if (builder == null) - { - throw new ArgumentNullException("builder"); - } - - return builder.EncodeFrame(this); - } + Data = segment.Array; + Offset = segment.Offset; + Count = segment.Count; + IsMasked = isMasked; } -} + + public BinaryFrame(byte[] data, int offset, int count, bool isMasked = true) + { + BufferValidator.ValidateBuffer(data, offset, count, "data"); + + Data = data; + Offset = offset; + Count = count; + IsMasked = isMasked; + } + + public byte[] Data { get; private set; } + public int Offset { get; private set; } + public int Count { get; private set; } + public bool IsMasked { get; private set; } + + public override OpCode OpCode => OpCode.Binary; + + public byte[] ToArray(IFrameBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException("builder"); + } + + return builder.EncodeFrame(this); + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Framing/Builder/IFrameBuilder.cs b/EonaCat.Network/System/Sockets/WebSockets/Framing/Builder/IFrameBuilder.cs index b12e326..2ffe436 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Framing/Builder/IFrameBuilder.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Framing/Builder/IFrameBuilder.cs @@ -1,23 +1,23 @@ using System.Collections.Generic; using EonaCat.WebSockets.Extensions; -namespace EonaCat.WebSockets +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public interface IFrameBuilder { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + SortedList NegotiatedExtensions { get; set; } - public interface IFrameBuilder - { - SortedList NegotiatedExtensions { get; set; } + byte[] EncodeFrame(PingFrame frame); + byte[] EncodeFrame(PongFrame frame); + byte[] EncodeFrame(CloseFrame frame); + byte[] EncodeFrame(TextFrame frame); + byte[] EncodeFrame(BinaryFrame frame); + byte[] EncodeFrame(BinaryFragmentationFrame frame); - byte[] EncodeFrame(PingFrame frame); - byte[] EncodeFrame(PongFrame frame); - byte[] EncodeFrame(CloseFrame frame); - byte[] EncodeFrame(TextFrame frame); - byte[] EncodeFrame(BinaryFrame frame); - byte[] EncodeFrame(BinaryFragmentationFrame frame); + bool TryDecodeFrameHeader(byte[] buffer, int offset, int count, out Header frameHeader); - bool TryDecodeFrameHeader(byte[] buffer, int offset, int count, out Header frameHeader); - void DecodePayload(byte[] buffer, int offset, Header frameHeader, out byte[] payload, out int payloadOffset, out int payloadCount); - } -} + void DecodePayload(byte[] buffer, int offset, Header frameHeader, out byte[] payload, out int payloadOffset, + out int payloadCount); +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Framing/Builder/WebSocketFrameBuilder.cs b/EonaCat.Network/System/Sockets/WebSockets/Framing/Builder/WebSocketFrameBuilder.cs index 3e91277..bd7e47a 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Framing/Builder/WebSocketFrameBuilder.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Framing/Builder/WebSocketFrameBuilder.cs @@ -4,434 +4,419 @@ using System.Linq; using System.Text; using EonaCat.WebSockets.Extensions; -namespace EonaCat.WebSockets +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +// http://tools.ietf.org/html/rfc6455 +// This wire format for the data transfer part is described by the ABNF +// [RFC5234] given in detail in this section. (Note that, unlike in +// other sections of this document, the ABNF in this section is +// operating on groups of bits. The length of each group of bits is +// indicated in a comment. When encoded on the wire, the most +// significant bit is the leftmost in the ABNF). A high-level overview +// of the framing is given in the following figure. In a case of +// conflict between the figure below and the ABNF specified later in +// this section, the figure is authoritative. +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-------+-+-------------+-------------------------------+ +// |F|R|R|R| opcode|M| Payload len | Extended payload length | +// |I|S|S|S| (4) |A| (7) | (16/64) | +// |N|V|V|V| |S| | (if payload len==126/127) | +// | |1|2|3| |K| | | +// +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + +// | Extended payload length continued, if payload len == 127 | +// + - - - - - - - - - - - - - - - +-------------------------------+ +// | |Masking-key, if MASK set to 1 | +// +-------------------------------+-------------------------------+ +// | Masking-key (continued) | Payload Data | +// +-------------------------------- - - - - - - - - - - - - - - - + +// : Payload Data continued ... : +// + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +// | Payload Data continued ... | +// +---------------------------------------------------------------+ +public class WebSocketFrameBuilder : IFrameBuilder { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private static readonly byte[] EmptyArray = new byte[0]; + private static readonly Random _rng = new(DateTime.UtcNow.Millisecond); + private static readonly int MaskingKeyLength = 4; - // http://tools.ietf.org/html/rfc6455 - // This wire format for the data transfer part is described by the ABNF - // [RFC5234] given in detail in this section. (Note that, unlike in - // other sections of this document, the ABNF in this section is - // operating on groups of bits. The length of each group of bits is - // indicated in a comment. When encoded on the wire, the most - // significant bit is the leftmost in the ABNF). A high-level overview - // of the framing is given in the following figure. In a case of - // conflict between the figure below and the ABNF specified later in - // this section, the figure is authoritative. - // 0 1 2 3 - // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - // +-+-+-+-+-------+-+-------------+-------------------------------+ - // |F|R|R|R| opcode|M| Payload len | Extended payload length | - // |I|S|S|S| (4) |A| (7) | (16/64) | - // |N|V|V|V| |S| | (if payload len==126/127) | - // | |1|2|3| |K| | | - // +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + - // | Extended payload length continued, if payload len == 127 | - // + - - - - - - - - - - - - - - - +-------------------------------+ - // | |Masking-key, if MASK set to 1 | - // +-------------------------------+-------------------------------+ - // | Masking-key (continued) | Payload Data | - // +-------------------------------- - - - - - - - - - - - - - - - + - // : Payload Data continued ... : - // + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - // | Payload Data continued ... | - // +---------------------------------------------------------------+ - public class WebSocketFrameBuilder : IFrameBuilder + public SortedList NegotiatedExtensions { get; set; } + + public byte[] EncodeFrame(PingFrame frame) { - private static readonly byte[] EmptyArray = new byte[0]; - private static readonly Random _rng = new Random(DateTime.UtcNow.Millisecond); - private static readonly int MaskingKeyLength = 4; - - public WebSocketFrameBuilder() + if (!string.IsNullOrEmpty(frame.Data)) { - } - - public SortedList NegotiatedExtensions { get; set; } - - public byte[] EncodeFrame(PingFrame frame) - { - if (!string.IsNullOrEmpty(frame.Data)) - { - var data = Encoding.UTF8.GetBytes(frame.Data); - if (data.Length > 125) - { - throw new WebSocketException("All control frames must have a payload length of 125 bytes or less."); - } - - return Encode(frame.OpCode, data, 0, data.Length, isMasked: frame.IsMasked); - } - else - { - return Encode(frame.OpCode, EmptyArray, 0, 0, isMasked: frame.IsMasked); - } - } - - public byte[] EncodeFrame(PongFrame frame) - { - if (!string.IsNullOrEmpty(frame.Data)) - { - var data = Encoding.UTF8.GetBytes(frame.Data); - if (data.Length > 125) - { - throw new WebSocketException("All control frames must have a payload length of 125 bytes or less."); - } - - return Encode(frame.OpCode, data, 0, data.Length, isMasked: frame.IsMasked); - } - else - { - return Encode(frame.OpCode, EmptyArray, 0, 0, isMasked: frame.IsMasked); - } - } - - public byte[] EncodeFrame(CloseFrame frame) - { - // The Close frame MAY contain a body (the "Application data" portion of - // the frame) that indicates a reason for closing, such as an endpoint - // shutting down, an endpoint having received a frame too large, or an - // endpoint having received a frame that does not conform to the format - // expected by the endpoint. If there is a body, the first two bytes of - // the body MUST be a 2-byte unsigned integer (in network byte order) - // representing a status code with value /code/ defined in Section 7.4. - // Following the 2-byte integer, the body MAY contain UTF-8-encoded data - // with value /reason/, the interpretation of which is not defined by - // this specification. This data is not necessarily human readable but - // may be useful for debugging or passing information relevant to the - // script that opened the connection. As the data is not guaranteed to - // be human readable, clients MUST NOT show it to end users. - int payloadLength = (string.IsNullOrEmpty(frame.CloseReason) ? 0 : Encoding.UTF8.GetMaxByteCount(frame.CloseReason.Length)) + 2; - if (payloadLength > 125) + var data = Encoding.UTF8.GetBytes(frame.Data); + if (data.Length > 125) { throw new WebSocketException("All control frames must have a payload length of 125 bytes or less."); } - byte[] payload = new byte[payloadLength]; + return Encode(frame.OpCode, data, 0, data.Length, frame.IsMasked); + } - int higherByte = (int)frame.CloseCode / 256; - int lowerByte = (int)frame.CloseCode % 256; + return Encode(frame.OpCode, EmptyArray, 0, 0, frame.IsMasked); + } - payload[0] = (byte)higherByte; - payload[1] = (byte)lowerByte; - - if (!string.IsNullOrEmpty(frame.CloseReason)) + public byte[] EncodeFrame(PongFrame frame) + { + if (!string.IsNullOrEmpty(frame.Data)) + { + var data = Encoding.UTF8.GetBytes(frame.Data); + if (data.Length > 125) { - int count = Encoding.UTF8.GetBytes(frame.CloseReason, 0, frame.CloseReason.Length, payload, 2); - return Encode(frame.OpCode, payload, 0, 2 + count, isMasked: frame.IsMasked); + throw new WebSocketException("All control frames must have a payload length of 125 bytes or less."); + } + + return Encode(frame.OpCode, data, 0, data.Length, frame.IsMasked); + } + + return Encode(frame.OpCode, EmptyArray, 0, 0, frame.IsMasked); + } + + public byte[] EncodeFrame(CloseFrame frame) + { + // The Close frame MAY contain a body (the "Application data" portion of + // the frame) that indicates a reason for closing, such as an endpoint + // shutting down, an endpoint having received a frame too large, or an + // endpoint having received a frame that does not conform to the format + // expected by the endpoint. If there is a body, the first two bytes of + // the body MUST be a 2-byte unsigned integer (in network byte order) + // representing a status code with value /code/ defined in Section 7.4. + // Following the 2-byte integer, the body MAY contain UTF-8-encoded data + // with value /reason/, the interpretation of which is not defined by + // this specification. This data is not necessarily human readable but + // may be useful for debugging or passing information relevant to the + // script that opened the connection. As the data is not guaranteed to + // be human readable, clients MUST NOT show it to end users. + var payloadLength = (string.IsNullOrEmpty(frame.CloseReason) + ? 0 + : Encoding.UTF8.GetMaxByteCount(frame.CloseReason.Length)) + 2; + if (payloadLength > 125) + { + throw new WebSocketException("All control frames must have a payload length of 125 bytes or less."); + } + + var payload = new byte[payloadLength]; + + var higherByte = (int)frame.CloseCode / 256; + var lowerByte = (int)frame.CloseCode % 256; + + payload[0] = (byte)higherByte; + payload[1] = (byte)lowerByte; + + if (!string.IsNullOrEmpty(frame.CloseReason)) + { + var count = Encoding.UTF8.GetBytes(frame.CloseReason, 0, frame.CloseReason.Length, payload, 2); + return Encode(frame.OpCode, payload, 0, 2 + count, frame.IsMasked); + } + + return Encode(frame.OpCode, payload, 0, payload.Length, frame.IsMasked); + } + + public byte[] EncodeFrame(TextFrame frame) + { + if (!string.IsNullOrEmpty(frame.Text)) + { + var data = Encoding.UTF8.GetBytes(frame.Text); + return Encode(frame.OpCode, data, 0, data.Length, frame.IsMasked); + } + + return Encode(frame.OpCode, EmptyArray, 0, 0, frame.IsMasked); + } + + public byte[] EncodeFrame(BinaryFrame frame) + { + return Encode(frame.OpCode, frame.Data, frame.Offset, frame.Count, frame.IsMasked); + } + + public byte[] EncodeFrame(BinaryFragmentationFrame frame) + { + return Encode(frame.OpCode, frame.Data, frame.Offset, frame.Count, frame.IsMasked, frame.IsFin); + } + + public bool TryDecodeFrameHeader(byte[] buffer, int offset, int count, out Header frameHeader) + { + frameHeader = DecodeFrameHeader(buffer, offset, count); + return frameHeader != null; + } + + public void DecodePayload(byte[] buffer, int offset, Header frameHeader, out byte[] payload, out int payloadOffset, + out int payloadCount) + { + payload = buffer; + payloadOffset = offset + frameHeader.Length; + payloadCount = frameHeader.PayloadLength; + + if (frameHeader.IsMasked) + { + payload = new byte[payloadCount]; + + for (var i = 0; i < payloadCount; i++) + payload[i] = (byte)(buffer[payloadOffset + i] ^ + buffer[offset + frameHeader.MaskingKeyOffset + i % MaskingKeyLength]); + + payloadOffset = 0; + payloadCount = payload.Length; + } + + // Payload data: (x+y) bytes + // Extension data: x bytes + // Application data: y bytes + // The "Extension data" is 0 bytes unless an extension has been + // negotiated. Any extension MUST specify the length of the + // "Extension data", or how that length may be calculated, and how + // the extension use MUST be negotiated during the opening handshake. + // If present, the "Extension data" is included in the total payload length. + if (NegotiatedExtensions != null) + { + byte[] bakedBuffer = null; + foreach (var extension in NegotiatedExtensions.Reverse().Select(e => e.Value)) + { + if (bakedBuffer == null) + { + bakedBuffer = extension.ProcessIncomingMessagePayload(payload, payloadOffset, payloadCount); + } + else + { + bakedBuffer = extension.ProcessIncomingMessagePayload(bakedBuffer, 0, bakedBuffer.Length); + } + } + + payload = bakedBuffer; + payloadOffset = 0; + payloadCount = payload.Length; + } + } + + private byte[] Encode(OpCode opCode, byte[] payload, int offset, int count, bool isMasked = true, bool isFin = true) + { + // Payload data: (x+y) bytes + // Extension data: x bytes + // Application data: y bytes + // The "Extension data" is 0 bytes unless an extension has been + // negotiated. Any extension MUST specify the length of the + // "Extension data", or how that length may be calculated, and how + // the extension use MUST be negotiated during the opening handshake. + // If present, the "Extension data" is included in the total payload length. + if (NegotiatedExtensions != null) + { + byte[] bakedBuffer = null; + foreach (var extension in NegotiatedExtensions.Values) + { + if (bakedBuffer == null) + { + bakedBuffer = extension.ProcessOutgoingMessagePayload(payload, offset, count); + } + else + { + bakedBuffer = extension.ProcessOutgoingMessagePayload(bakedBuffer, 0, bakedBuffer.Length); + } + } + + payload = bakedBuffer; + offset = 0; + count = payload.Length; + } + + byte[] fragment; + + // Payload length: 7 bits, 7+16 bits, or 7+64 bits. + // The length of the "Payload data", in bytes: if 0-125, that is the + // payload length. If 126, the following 2 bytes interpreted as a + // 16-bit unsigned integer are the payload length. If 127, the + // following 8 bytes interpreted as a 64-bit unsigned integer (the + // most significant bit MUST be 0) are the payload length. + if (count < 126) + { + fragment = new byte[2 + (isMasked ? MaskingKeyLength : 0) + count]; + fragment[1] = (byte)count; + } + else if (count < 65536) + { + fragment = new byte[2 + 2 + (isMasked ? MaskingKeyLength : 0) + count]; + fragment[1] = 126; + fragment[2] = (byte)(count / 256); + fragment[3] = (byte)(count % 256); + } + else + { + fragment = new byte[2 + 8 + (isMasked ? MaskingKeyLength : 0) + count]; + fragment[1] = 127; + + var left = count; + for (var i = 9; i > 1; i--) + { + fragment[i] = (byte)(left % 256); + left = left / 256; + + if (left == 0) + { + break; + } + } + } + + // FIN: 1 bit + // Indicates that this is the final fragment in a message. The first + // fragment MAY also be the final fragment. + if (isFin) + { + fragment[0] = 0x80; + } + + // RSV1, RSV2, RSV3: 1 bit each + // MUST be 0 unless an extension is negotiated that defines meanings + // for non-zero values. If a nonzero value is received and none of + // the negotiated extensions defines the meaning of such a nonzero + // value, the receiving endpoint MUST _Fail the WebSocket + // Connection_. + if (NegotiatedExtensions != null) + { + foreach (var extension in NegotiatedExtensions.Values) + { + if (extension.Rsv1BitOccupied) + { + fragment[0] = (byte)(fragment[0] | 0x40); + } + + if (extension.Rsv2BitOccupied) + { + fragment[0] = (byte)(fragment[0] | 0x20); + } + + if (extension.Rsv3BitOccupied) + { + fragment[0] = (byte)(fragment[0] | 0x10); + } + } + } + + // Opcode: 4 bits + // Defines the interpretation of the "Payload data". If an unknown + // opcode is received, the receiving endpoint MUST _Fail the + // WebSocket Connection_. The following values are defined. + fragment[0] = (byte)(fragment[0] | (byte)opCode); + + // Mask: 1 bit + // Defines whether the "Payload data" is masked. If set to 1, a + // masking key is present in masking-key, and this is used to unmask + // the "Payload data" as per Section 5.3. All frames sent from + // client to server have this bit set to 1. + if (isMasked) + { + fragment[1] = (byte)(fragment[1] | 0x80); + } + + // Masking-key: 0 or 4 bytes + // All frames sent from the client to the server are masked by a + // 32-bit value that is contained within the frame. + // The masking key is a 32-bit value chosen at random by the client. + // When preparing a masked frame, the client MUST pick a fresh masking + // key from the set of allowed 32-bit values. The masking key needs to + // be unpredictable; thus, the masking key MUST be derived from a strong + // source of entropy, and the masking key for a given frame MUST NOT + // make it simple for a server/proxy to predict the masking key for a + // subsequent frame. The unpredictability of the masking key is + // essential to prevent authors of malicious applications from selecting + // the bytes that appear on the wire. RFC 4086 [RFC4086] discusses what + // entails a suitable source of entropy for security-sensitive applications. + if (isMasked) + { + var maskingKeyIndex = fragment.Length - (MaskingKeyLength + count); + for (var i = maskingKeyIndex; i < maskingKeyIndex + MaskingKeyLength; i++) + fragment[i] = (byte)_rng.Next(0, 255); + + if (count > 0) + { + var payloadIndex = fragment.Length - count; + for (var i = 0; i < count; i++) + fragment[payloadIndex + i] = + (byte)(payload[offset + i] ^ fragment[maskingKeyIndex + i % MaskingKeyLength]); + } + } + else + { + if (count > 0) + { + var payloadIndex = fragment.Length - count; + Array.Copy(payload, offset, fragment, payloadIndex, count); + } + } + + return fragment; + } + + private Header DecodeFrameHeader(byte[] buffer, int offset, int count) + { + if (count < 2) + { + return null; + } + + // parse fixed header + var header = new Header + { + IsFIN = (buffer[offset + 0] & 0x80) == 0x80, + IsRSV1 = (buffer[offset + 0] & 0x40) == 0x40, + IsRSV2 = (buffer[offset + 0] & 0x20) == 0x20, + IsRSV3 = (buffer[offset + 0] & 0x10) == 0x10, + OpCode = (OpCode)(buffer[offset + 0] & 0x0f), + IsMasked = (buffer[offset + 1] & 0x80) == 0x80, + PayloadLength = buffer[offset + 1] & 0x7f, + Length = 2 + }; + + // parse extended payload length + if (header.PayloadLength >= 126) + { + if (header.PayloadLength == 126) + { + header.Length += 2; } else { - return Encode(frame.OpCode, payload, 0, payload.Length, isMasked: frame.IsMasked); - } - } - - public byte[] EncodeFrame(TextFrame frame) - { - if (!string.IsNullOrEmpty(frame.Text)) - { - var data = Encoding.UTF8.GetBytes(frame.Text); - return Encode(frame.OpCode, data, 0, data.Length, isMasked: frame.IsMasked); - } - else - { - return Encode(frame.OpCode, EmptyArray, 0, 0, isMasked: frame.IsMasked); - } - } - - public byte[] EncodeFrame(BinaryFrame frame) - { - return Encode(frame.OpCode, frame.Data, frame.Offset, frame.Count, isMasked: frame.IsMasked); - } - - public byte[] EncodeFrame(BinaryFragmentationFrame frame) - { - return Encode(frame.OpCode, frame.Data, frame.Offset, frame.Count, isMasked: frame.IsMasked, isFin: frame.IsFin); - } - - private byte[] Encode(OpCode opCode, byte[] payload, int offset, int count, bool isMasked = true, bool isFin = true) - { - // Payload data: (x+y) bytes - // Extension data: x bytes - // Application data: y bytes - // The "Extension data" is 0 bytes unless an extension has been - // negotiated. Any extension MUST specify the length of the - // "Extension data", or how that length may be calculated, and how - // the extension use MUST be negotiated during the opening handshake. - // If present, the "Extension data" is included in the total payload length. - if (this.NegotiatedExtensions != null) - { - byte[] bakedBuffer = null; - foreach (var extension in this.NegotiatedExtensions.Values) - { - if (bakedBuffer == null) - { - bakedBuffer = extension.ProcessOutgoingMessagePayload(payload, offset, count); - } - else - { - bakedBuffer = extension.ProcessOutgoingMessagePayload(bakedBuffer, 0, bakedBuffer.Length); - } - } - - payload = bakedBuffer; - offset = 0; - count = payload.Length; + header.Length += 8; } - byte[] fragment; - - // Payload length: 7 bits, 7+16 bits, or 7+64 bits. - // The length of the "Payload data", in bytes: if 0-125, that is the - // payload length. If 126, the following 2 bytes interpreted as a - // 16-bit unsigned integer are the payload length. If 127, the - // following 8 bytes interpreted as a 64-bit unsigned integer (the - // most significant bit MUST be 0) are the payload length. - if (count < 126) - { - fragment = new byte[2 + (isMasked ? MaskingKeyLength : 0) + count]; - fragment[1] = (byte)count; - } - else if (count < 65536) - { - fragment = new byte[2 + 2 + (isMasked ? MaskingKeyLength : 0) + count]; - fragment[1] = (byte)126; - fragment[2] = (byte)(count / 256); - fragment[3] = (byte)(count % 256); - } - else - { - fragment = new byte[2 + 8 + (isMasked ? MaskingKeyLength : 0) + count]; - fragment[1] = (byte)127; - - int left = count; - for (int i = 9; i > 1; i--) - { - fragment[i] = (byte)(left % 256); - left = left / 256; - - if (left == 0) - { - break; - } - } - } - - // FIN: 1 bit - // Indicates that this is the final fragment in a message. The first - // fragment MAY also be the final fragment. - if (isFin) - { - fragment[0] = 0x80; - } - - // RSV1, RSV2, RSV3: 1 bit each - // MUST be 0 unless an extension is negotiated that defines meanings - // for non-zero values. If a nonzero value is received and none of - // the negotiated extensions defines the meaning of such a nonzero - // value, the receiving endpoint MUST _Fail the WebSocket - // Connection_. - if (this.NegotiatedExtensions != null) - { - foreach (var extension in this.NegotiatedExtensions.Values) - { - if (extension.Rsv1BitOccupied) - { - fragment[0] = (byte)(fragment[0] | 0x40); - } - - if (extension.Rsv2BitOccupied) - { - fragment[0] = (byte)(fragment[0] | 0x20); - } - - if (extension.Rsv3BitOccupied) - { - fragment[0] = (byte)(fragment[0] | 0x10); - } - } - } - - // Opcode: 4 bits - // Defines the interpretation of the "Payload data". If an unknown - // opcode is received, the receiving endpoint MUST _Fail the - // WebSocket Connection_. The following values are defined. - fragment[0] = (byte)(fragment[0] | (byte)opCode); - - // Mask: 1 bit - // Defines whether the "Payload data" is masked. If set to 1, a - // masking key is present in masking-key, and this is used to unmask - // the "Payload data" as per Section 5.3. All frames sent from - // client to server have this bit set to 1. - if (isMasked) - { - fragment[1] = (byte)(fragment[1] | 0x80); - } - - // Masking-key: 0 or 4 bytes - // All frames sent from the client to the server are masked by a - // 32-bit value that is contained within the frame. - // The masking key is a 32-bit value chosen at random by the client. - // When preparing a masked frame, the client MUST pick a fresh masking - // key from the set of allowed 32-bit values. The masking key needs to - // be unpredictable; thus, the masking key MUST be derived from a strong - // source of entropy, and the masking key for a given frame MUST NOT - // make it simple for a server/proxy to predict the masking key for a - // subsequent frame. The unpredictability of the masking key is - // essential to prevent authors of malicious applications from selecting - // the bytes that appear on the wire. RFC 4086 [RFC4086] discusses what - // entails a suitable source of entropy for security-sensitive applications. - if (isMasked) - { - int maskingKeyIndex = fragment.Length - (MaskingKeyLength + count); - for (var i = maskingKeyIndex; i < maskingKeyIndex + MaskingKeyLength; i++) - { - fragment[i] = (byte)_rng.Next(0, 255); - } - - if (count > 0) - { - int payloadIndex = fragment.Length - count; - for (var i = 0; i < count; i++) - { - fragment[payloadIndex + i] = (byte)(payload[offset + i] ^ fragment[maskingKeyIndex + i % MaskingKeyLength]); - } - } - } - else - { - if (count > 0) - { - int payloadIndex = fragment.Length - count; - Array.Copy(payload, offset, fragment, payloadIndex, count); - } - } - - return fragment; - } - - public bool TryDecodeFrameHeader(byte[] buffer, int offset, int count, out Header frameHeader) - { - frameHeader = DecodeFrameHeader(buffer, offset, count); - return frameHeader != null; - } - - private Header DecodeFrameHeader(byte[] buffer, int offset, int count) - { - if (count < 2) + if (count < header.Length) { return null; } - // parse fixed header - var header = new Header() + if (header.PayloadLength == 126) { - IsFIN = ((buffer[offset + 0] & 0x80) == 0x80), - IsRSV1 = ((buffer[offset + 0] & 0x40) == 0x40), - IsRSV2 = ((buffer[offset + 0] & 0x20) == 0x20), - IsRSV3 = ((buffer[offset + 0] & 0x10) == 0x10), - OpCode = (OpCode)(buffer[offset + 0] & 0x0f), - IsMasked = ((buffer[offset + 1] & 0x80) == 0x80), - PayloadLength = (buffer[offset + 1] & 0x7f), - Length = 2, - }; - - // parse extended payload length - if (header.PayloadLength >= 126) - { - if (header.PayloadLength == 126) - { - header.Length += 2; - } - else - { - header.Length += 8; - } - - if (count < header.Length) - { - return null; - } - - if (header.PayloadLength == 126) - { - header.PayloadLength = buffer[offset + 2] * 256 + buffer[offset + 3]; - } - else - { - int totalLength = 0; - int level = 1; - - for (int i = 7; i >= 0; i--) - { - totalLength += buffer[offset + i + 2] * level; - level *= 256; - } - - header.PayloadLength = totalLength; - } + header.PayloadLength = buffer[offset + 2] * 256 + buffer[offset + 3]; } - - // parse masking key - if (header.IsMasked) + else { - if (count < header.Length + MaskingKeyLength) + var totalLength = 0; + var level = 1; + + for (var i = 7; i >= 0; i--) { - return null; + totalLength += buffer[offset + i + 2] * level; + level *= 256; } - header.MaskingKeyOffset = header.Length; - header.Length += MaskingKeyLength; + header.PayloadLength = totalLength; } - - return header; } - public void DecodePayload(byte[] buffer, int offset, Header frameHeader, out byte[] payload, out int payloadOffset, out int payloadCount) + // parse masking key + if (header.IsMasked) { - payload = buffer; - payloadOffset = offset + frameHeader.Length; - payloadCount = frameHeader.PayloadLength; - - if (frameHeader.IsMasked) + if (count < header.Length + MaskingKeyLength) { - payload = new byte[payloadCount]; - - for (var i = 0; i < payloadCount; i++) - { - payload[i] = (byte)(buffer[payloadOffset + i] ^ buffer[offset + frameHeader.MaskingKeyOffset + i % MaskingKeyLength]); - } - - payloadOffset = 0; - payloadCount = payload.Length; + return null; } - // Payload data: (x+y) bytes - // Extension data: x bytes - // Application data: y bytes - // The "Extension data" is 0 bytes unless an extension has been - // negotiated. Any extension MUST specify the length of the - // "Extension data", or how that length may be calculated, and how - // the extension use MUST be negotiated during the opening handshake. - // If present, the "Extension data" is included in the total payload length. - if (this.NegotiatedExtensions != null) - { - byte[] bakedBuffer = null; - foreach (var extension in this.NegotiatedExtensions.Reverse().Select(e => e.Value)) - { - if (bakedBuffer == null) - { - bakedBuffer = extension.ProcessIncomingMessagePayload(payload, payloadOffset, payloadCount); - } - else - { - bakedBuffer = extension.ProcessIncomingMessagePayload(bakedBuffer, 0, bakedBuffer.Length); - } - } - - payload = bakedBuffer; - payloadOffset = 0; - payloadCount = payload.Length; - } + header.MaskingKeyOffset = header.Length; + header.Length += MaskingKeyLength; } + + return header; } -} +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Framing/CloseFrame.cs b/EonaCat.Network/System/Sockets/WebSockets/Framing/CloseFrame.cs index 8fa9172..31222a0 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Framing/CloseFrame.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Framing/CloseFrame.cs @@ -1,41 +1,36 @@ using System; -namespace EonaCat.WebSockets +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public sealed class CloseFrame : ControlFrame { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - - public sealed class CloseFrame : ControlFrame + public CloseFrame(bool isMasked = true) { - public CloseFrame(bool isMasked = true) - { - this.IsMasked = isMasked; - } - - public CloseFrame(WebSocketCloseCode closeCode, string closeReason, bool isMasked = true) - : this(isMasked) - { - this.CloseCode = closeCode; - this.CloseReason = closeReason; - } - - public WebSocketCloseCode CloseCode { get; private set; } - public string CloseReason { get; private set; } - public bool IsMasked { get; private set; } - - public override OpCode OpCode - { - get { return OpCode.Close; } - } - - public byte[] ToArray(IFrameBuilder builder) - { - if (builder == null) - { - throw new ArgumentNullException("builder"); - } - - return builder.EncodeFrame(this); - } + IsMasked = isMasked; } -} + + public CloseFrame(WebSocketCloseCode closeCode, string closeReason, bool isMasked = true) + : this(isMasked) + { + CloseCode = closeCode; + CloseReason = closeReason; + } + + public WebSocketCloseCode CloseCode { get; private set; } + public string CloseReason { get; private set; } + public bool IsMasked { get; private set; } + + public override OpCode OpCode => OpCode.Close; + + public byte[] ToArray(IFrameBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException("builder"); + } + + return builder.EncodeFrame(this); + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Framing/ControlFrame.cs b/EonaCat.Network/System/Sockets/WebSockets/Framing/ControlFrame.cs index 82a40e3..7460fdc 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Framing/ControlFrame.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Framing/ControlFrame.cs @@ -1,9 +1,7 @@ -namespace EonaCat.WebSockets -{ - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. - public abstract class ControlFrame : Frame - { - } -} +public abstract class ControlFrame : Frame +{ +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Framing/DataFrame.cs b/EonaCat.Network/System/Sockets/WebSockets/Framing/DataFrame.cs index 9824b89..9561959 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Framing/DataFrame.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Framing/DataFrame.cs @@ -1,9 +1,7 @@ -namespace EonaCat.WebSockets -{ - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. - public abstract class DataFrame : Frame - { - } -} +public abstract class DataFrame : Frame +{ +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Framing/Frame.cs b/EonaCat.Network/System/Sockets/WebSockets/Framing/Frame.cs index ee10fc9..f0d43e0 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Framing/Frame.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Framing/Frame.cs @@ -1,15 +1,13 @@ -namespace EonaCat.WebSockets +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public abstract class Frame { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + public abstract OpCode OpCode { get; } - public abstract class Frame + public override string ToString() { - public abstract OpCode OpCode { get; } - - public override string ToString() - { - return string.Format("OpName[{0}], OpCode[{1}]", OpCode, (byte)OpCode); - } + return $"OpName[{OpCode}], OpCode[{(byte)OpCode}]"; } -} +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Framing/Header/Header.cs b/EonaCat.Network/System/Sockets/WebSockets/Framing/Header/Header.cs index 58eb08b..9edbc3a 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Framing/Header/Header.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Framing/Header/Header.cs @@ -1,32 +1,32 @@ -namespace EonaCat.WebSockets +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public class FixedHeader { - // This file is part of the EonaCat 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 bool IsFIN { get; set; } + public bool IsRSV1 { get; set; } + public bool IsRSV2 { get; set; } + public bool IsRSV3 { get; set; } + public OpCode OpCode { get; set; } + public bool IsMasked { get; set; } - public class FixedHeader + public override string ToString() { - public bool IsFIN { get; set; } - public bool IsRSV1 { get; set; } - public bool IsRSV2 { get; set; } - public bool IsRSV3 { get; set; } - public OpCode OpCode { get; set; } - public bool IsMasked { get; set; } - - public override string ToString() - { - return $"IsFIN[{IsFIN}], IsRSV1[{IsRSV1}], IsRSV2[{IsRSV2}], IsRSV3[{IsRSV3}], OpCode[{OpCode}], IsMasked[{IsMasked}]"; - } - } - - public class Header : FixedHeader - { - public int PayloadLength { get; set; } - public int MaskingKeyOffset { get; set; } - public int Length { get; set; } - - public override string ToString() - { - return $"IsFIN[{IsFIN}], IsRSV1[{IsRSV1}], IsRSV2[{IsRSV2}], IsRSV3[{IsRSV3}], OpCode[{OpCode}], IsMasked[{IsMasked}], PayloadLength[{PayloadLength}], MaskingKeyOffset[{MaskingKeyOffset}], Length[{Length}]"; - } + return + $"IsFIN[{IsFIN}], IsRSV1[{IsRSV1}], IsRSV2[{IsRSV2}], IsRSV3[{IsRSV3}], OpCode[{OpCode}], IsMasked[{IsMasked}]"; } } + +public class Header : FixedHeader +{ + public int PayloadLength { get; set; } + public int MaskingKeyOffset { get; set; } + public int Length { get; set; } + + public override string ToString() + { + return + $"IsFIN[{IsFIN}], IsRSV1[{IsRSV1}], IsRSV2[{IsRSV2}], IsRSV3[{IsRSV3}], OpCode[{OpCode}], IsMasked[{IsMasked}], PayloadLength[{PayloadLength}], MaskingKeyOffset[{MaskingKeyOffset}], Length[{Length}]"; + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Framing/OpCode.cs b/EonaCat.Network/System/Sockets/WebSockets/Framing/OpCode.cs index dacdc48..a75c3c1 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Framing/OpCode.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Framing/OpCode.cs @@ -1,27 +1,25 @@ -namespace EonaCat.WebSockets -{ - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. - // https://www.iana.org/assignments/websocket/websocket.xhtml - // The opcode denotes the frame type of the WebSocket frame. - // The opcode is an integer number between 0 and 15, inclusive. - // Opcode Meaning Reference - // 0 Continuation Frame [RFC6455] - // 1 Text Frame [RFC6455] - // 2 Binary Frame [RFC6455] - // 3-7 Unassigned - // 8 Connection Close Frame [RFC6455] - // 9 Ping Frame [RFC6455] - // 10 Pong Frame [RFC6455] - // 11-15 Unassigned - public enum OpCode : byte - { - Continuation = 0, - Text = 1, - Binary = 2, - Close = 8, - Ping = 9, - Pong = 10, - } -} +// https://www.iana.org/assignments/websocket/websocket.xhtml +// The opcode denotes the frame type of the WebSocket frame. +// The opcode is an integer number between 0 and 15, inclusive. +// Opcode Meaning Reference +// 0 Continuation Frame [RFC6455] +// 1 Text Frame [RFC6455] +// 2 Binary Frame [RFC6455] +// 3-7 Unassigned +// 8 Connection Close Frame [RFC6455] +// 9 Ping Frame [RFC6455] +// 10 Pong Frame [RFC6455] +// 11-15 Unassigned +public enum OpCode : byte +{ + Continuation = 0, + Text = 1, + Binary = 2, + Close = 8, + Ping = 9, + Pong = 10 +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Framing/PingFrame.cs b/EonaCat.Network/System/Sockets/WebSockets/Framing/PingFrame.cs index 47e7a04..14cdfc3 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Framing/PingFrame.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Framing/PingFrame.cs @@ -1,39 +1,34 @@ using System; -namespace EonaCat.WebSockets +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public sealed class PingFrame : ControlFrame { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - - public sealed class PingFrame : ControlFrame + public PingFrame(bool isMasked = true) { - public PingFrame(bool isMasked = true) - { - this.IsMasked = isMasked; - } - - public PingFrame(string data, bool isMasked = true) - : this(isMasked) - { - this.Data = data; - } - - public string Data { get; private set; } - public bool IsMasked { get; private set; } - - public override OpCode OpCode - { - get { return OpCode.Ping; } - } - - public byte[] ToArray(IFrameBuilder builder) - { - if (builder == null) - { - throw new ArgumentNullException("builder"); - } - - return builder.EncodeFrame(this); - } + IsMasked = isMasked; } -} + + public PingFrame(string data, bool isMasked = true) + : this(isMasked) + { + Data = data; + } + + public string Data { get; private set; } + public bool IsMasked { get; private set; } + + public override OpCode OpCode => OpCode.Ping; + + public byte[] ToArray(IFrameBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException("builder"); + } + + return builder.EncodeFrame(this); + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Framing/PongFrame.cs b/EonaCat.Network/System/Sockets/WebSockets/Framing/PongFrame.cs index 60c071b..25963f9 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Framing/PongFrame.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Framing/PongFrame.cs @@ -1,39 +1,34 @@ using System; -namespace EonaCat.WebSockets +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public sealed class PongFrame : ControlFrame { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - - public sealed class PongFrame : ControlFrame + public PongFrame(bool isMasked = true) { - public PongFrame(bool isMasked = true) - { - this.IsMasked = isMasked; - } - - public PongFrame(string data, bool isMasked = true) - : this(isMasked) - { - this.Data = data; - } - - public string Data { get; private set; } - public bool IsMasked { get; private set; } - - public override OpCode OpCode - { - get { return OpCode.Pong; } - } - - public byte[] ToArray(IFrameBuilder builder) - { - if (builder == null) - { - throw new ArgumentNullException("builder"); - } - - return builder.EncodeFrame(this); - } + IsMasked = isMasked; } -} + + public PongFrame(string data, bool isMasked = true) + : this(isMasked) + { + Data = data; + } + + public string Data { get; private set; } + public bool IsMasked { get; private set; } + + public override OpCode OpCode => OpCode.Pong; + + public byte[] ToArray(IFrameBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException("builder"); + } + + return builder.EncodeFrame(this); + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Framing/TextFrame.cs b/EonaCat.Network/System/Sockets/WebSockets/Framing/TextFrame.cs index c7e07b7..9e976ce 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Framing/TextFrame.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Framing/TextFrame.cs @@ -1,39 +1,34 @@ using System; -namespace EonaCat.WebSockets +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public sealed class TextFrame : DataFrame { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - - public sealed class TextFrame : DataFrame + public TextFrame(string text, bool isMasked = true) { - public TextFrame(string text, bool isMasked = true) + if (string.IsNullOrEmpty(text)) { - if (string.IsNullOrEmpty(text)) - { - throw new ArgumentNullException("text"); - } - - this.Text = text; - this.IsMasked = isMasked; + throw new ArgumentNullException("text"); } - public string Text { get; private set; } - public bool IsMasked { get; private set; } - - public override OpCode OpCode - { - get { return OpCode.Text; } - } - - public byte[] ToArray(IFrameBuilder builder) - { - if (builder == null) - { - throw new ArgumentNullException("builder"); - } - - return builder.EncodeFrame(this); - } + Text = text; + IsMasked = isMasked; } -} + + public string Text { get; private set; } + public bool IsMasked { get; private set; } + + public override OpCode OpCode => OpCode.Text; + + public byte[] ToArray(IFrameBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException("builder"); + } + + return builder.EncodeFrame(this); + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Helpers/Consts.cs b/EonaCat.Network/System/Sockets/WebSockets/Helpers/Consts.cs index 7927def..871519d 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Helpers/Consts.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Helpers/Consts.cs @@ -1,45 +1,42 @@ using System.Text; -namespace EonaCat.WebSockets +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +internal sealed class Consts { - // This file is part of the EonaCat 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 const string HttpHeaderLineFormat = "{0}: {1}"; - internal sealed class Consts - { - internal static readonly byte[] HttpMessageTerminator = Encoding.UTF8.GetBytes("\r\n\r\n"); + internal const string HttpStatusCodeName = "HttpStatusCode"; + internal const string HttpStatusCodeDescription = "HttpStatusCodeDescription"; + internal const string HttpGetMethodName = "GET"; + internal const string HttpVersionName = "HTTP"; + internal const string HttpVersion = "1.1"; - internal static readonly string[] WebSocketSchemes = new string[] { "ws", "wss" }; + internal const string SecWebSocketKeyGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - internal const string HttpHeaderLineFormat = "{0}: {1}"; + internal const string WebSocketUpgradeToken = "websocket"; + internal const string WebSocketConnectionToken = "Upgrade"; - internal const string HttpStatusCodeName = "HttpStatusCode"; - internal const string HttpStatusCodeDescription = "HttpStatusCodeDescription"; - internal const string HttpGetMethodName = "GET"; - internal const string HttpVersionName = "HTTP"; - internal const string HttpVersion = "1.1"; + // https://www.iana.org/assignments/websocket/websocket.xhtml#version-number + // Version Number Reference Status + // 0 [draft-ietf-hybi-thewebsocketprotocol-00] Interim + // 1 [draft-ietf-hybi-thewebsocketprotocol-01] Interim + // 2 [draft-ietf-hybi-thewebsocketprotocol-02] Interim + // 3 [draft-ietf-hybi-thewebsocketprotocol-03] Interim + // 4 [draft-ietf-hybi-thewebsocketprotocol-04] Interim + // 5 [draft-ietf-hybi-thewebsocketprotocol-05] Interim + // 6 [draft-ietf-hybi-thewebsocketprotocol-06] Interim + // 7 [draft-ietf-hybi-thewebsocketprotocol-07] Interim + // 8 [draft-ietf-hybi-thewebsocketprotocol-08] Interim + // 9 [Reserved] + // 10 [Reserved] + // 11 [Reserved] + // 12 [Reserved] + // 13 [RFC6455] Standard + internal const string WebSocketVersion = "13"; + internal static readonly byte[] HttpMessageTerminator = Encoding.UTF8.GetBytes("\r\n\r\n"); - internal const string SecWebSocketKeyGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - - internal const string WebSocketUpgradeToken = "websocket"; - internal const string WebSocketConnectionToken = "Upgrade"; - - // https://www.iana.org/assignments/websocket/websocket.xhtml#version-number - // Version Number Reference Status - // 0 [draft-ietf-hybi-thewebsocketprotocol-00] Interim - // 1 [draft-ietf-hybi-thewebsocketprotocol-01] Interim - // 2 [draft-ietf-hybi-thewebsocketprotocol-02] Interim - // 3 [draft-ietf-hybi-thewebsocketprotocol-03] Interim - // 4 [draft-ietf-hybi-thewebsocketprotocol-04] Interim - // 5 [draft-ietf-hybi-thewebsocketprotocol-05] Interim - // 6 [draft-ietf-hybi-thewebsocketprotocol-06] Interim - // 7 [draft-ietf-hybi-thewebsocketprotocol-07] Interim - // 8 [draft-ietf-hybi-thewebsocketprotocol-08] Interim - // 9 [Reserved] - // 10 [Reserved] - // 11 [Reserved] - // 12 [Reserved] - // 13 [RFC6455] Standard - internal const string WebSocketVersion = "13"; - } -} + internal static readonly string[] WebSocketSchemes = { "ws", "wss" }; +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Helpers/HttpKnownHeaderNames.cs b/EonaCat.Network/System/Sockets/WebSockets/Helpers/HttpKnownHeaderNames.cs index c354841..4f5b63d 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Helpers/HttpKnownHeaderNames.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Helpers/HttpKnownHeaderNames.cs @@ -1,144 +1,142 @@ using System.Collections.Generic; using System.Collections.ObjectModel; -namespace EonaCat.WebSockets +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +internal static class HttpKnownHeaderNames { - // This file is part of the EonaCat 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 const string CacheControl = "Cache-Control"; + public const string Connection = "Connection"; + public const string Date = "Date"; + public const string KeepAlive = "Keep-Alive"; + public const string Pragma = "Pragma"; + public const string ProxyConnection = "Proxy-Connection"; + public const string Trailer = "Trailer"; + public const string TransferEncoding = "Transfer-Encoding"; + public const string Upgrade = "Upgrade"; + public const string Via = "Via"; + public const string Warning = "Warning"; + public const string ContentLength = "Content-Length"; + public const string ContentType = "Content-Type"; + public const string ContentDisposition = "Content-Disposition"; + public const string ContentEncoding = "Content-Encoding"; + public const string ContentLanguage = "Content-Language"; + public const string ContentLocation = "Content-Location"; + public const string ContentRange = "Content-Range"; + public const string Expires = "Expires"; + public const string LastModified = "Last-Modified"; + public const string Age = "Age"; + public const string Location = "Location"; + public const string ProxyAuthenticate = "Proxy-Authenticate"; + public const string RetryAfter = "Retry-After"; + public const string Server = "Server"; + public const string SetCookie = "Set-Cookie"; + public const string SetCookie2 = "Set-Cookie2"; + public const string Vary = "Vary"; + public const string WWWAuthenticate = "WWW-Authenticate"; + public const string Accept = "Accept"; + public const string AcceptCharset = "Accept-Charset"; + public const string AcceptEncoding = "Accept-Encoding"; + public const string AcceptLanguage = "Accept-Language"; + public const string Authorization = "Authorization"; + public const string Cookie = "Cookie"; + public const string Cookie2 = "Cookie2"; + public const string Expect = "Expect"; + public const string From = "From"; + public const string Host = "Host"; + public const string IfMatch = "If-Match"; + public const string IfModifiedSince = "If-Modified-Since"; + public const string IfNoneMatch = "If-None-Match"; + public const string IfRange = "If-Range"; + public const string IfUnmodifiedSince = "If-Unmodified-Since"; + public const string MaxForwards = "Max-Forwards"; + public const string ProxyAuthorization = "Proxy-Authorization"; + public const string Referer = "Referer"; + public const string Range = "Range"; + public const string UserAgent = "User-Agent"; + public const string ContentMD5 = "Content-MD5"; + public const string ETag = "ETag"; + public const string TE = "TE"; + public const string Allow = "Allow"; + public const string AcceptRanges = "Accept-Ranges"; + public const string P3P = "P3P"; + public const string XPoweredBy = "X-Powered-By"; + public const string XAspNetVersion = "X-AspNet-Version"; + public const string SecWebSocketKey = "Sec-WebSocket-Key"; + public const string SecWebSocketExtensions = "Sec-WebSocket-Extensions"; + public const string SecWebSocketAccept = "Sec-WebSocket-Accept"; + public const string Origin = "Origin"; + public const string SecWebSocketProtocol = "Sec-WebSocket-Protocol"; + public const string SecWebSocketVersion = "Sec-WebSocket-Version"; - internal static class HttpKnownHeaderNames - { - public const string CacheControl = "Cache-Control"; - public const string Connection = "Connection"; - public const string Date = "Date"; - public const string KeepAlive = "Keep-Alive"; - public const string Pragma = "Pragma"; - public const string ProxyConnection = "Proxy-Connection"; - public const string Trailer = "Trailer"; - public const string TransferEncoding = "Transfer-Encoding"; - public const string Upgrade = "Upgrade"; - public const string Via = "Via"; - public const string Warning = "Warning"; - public const string ContentLength = "Content-Length"; - public const string ContentType = "Content-Type"; - public const string ContentDisposition = "Content-Disposition"; - public const string ContentEncoding = "Content-Encoding"; - public const string ContentLanguage = "Content-Language"; - public const string ContentLocation = "Content-Location"; - public const string ContentRange = "Content-Range"; - public const string Expires = "Expires"; - public const string LastModified = "Last-Modified"; - public const string Age = "Age"; - public const string Location = "Location"; - public const string ProxyAuthenticate = "Proxy-Authenticate"; - public const string RetryAfter = "Retry-After"; - public const string Server = "Server"; - public const string SetCookie = "Set-Cookie"; - public const string SetCookie2 = "Set-Cookie2"; - public const string Vary = "Vary"; - public const string WWWAuthenticate = "WWW-Authenticate"; - public const string Accept = "Accept"; - public const string AcceptCharset = "Accept-Charset"; - public const string AcceptEncoding = "Accept-Encoding"; - public const string AcceptLanguage = "Accept-Language"; - public const string Authorization = "Authorization"; - public const string Cookie = "Cookie"; - public const string Cookie2 = "Cookie2"; - public const string Expect = "Expect"; - public const string From = "From"; - public const string Host = "Host"; - public const string IfMatch = "If-Match"; - public const string IfModifiedSince = "If-Modified-Since"; - public const string IfNoneMatch = "If-None-Match"; - public const string IfRange = "If-Range"; - public const string IfUnmodifiedSince = "If-Unmodified-Since"; - public const string MaxForwards = "Max-Forwards"; - public const string ProxyAuthorization = "Proxy-Authorization"; - public const string Referer = "Referer"; - public const string Range = "Range"; - public const string UserAgent = "User-Agent"; - public const string ContentMD5 = "Content-MD5"; - public const string ETag = "ETag"; - public const string TE = "TE"; - public const string Allow = "Allow"; - public const string AcceptRanges = "Accept-Ranges"; - public const string P3P = "P3P"; - public const string XPoweredBy = "X-Powered-By"; - public const string XAspNetVersion = "X-AspNet-Version"; - public const string SecWebSocketKey = "Sec-WebSocket-Key"; - public const string SecWebSocketExtensions = "Sec-WebSocket-Extensions"; - public const string SecWebSocketAccept = "Sec-WebSocket-Accept"; - public const string Origin = "Origin"; - public const string SecWebSocketProtocol = "Sec-WebSocket-Protocol"; - public const string SecWebSocketVersion = "Sec-WebSocket-Version"; - - public static readonly IReadOnlyCollection All = - new ReadOnlyCollection( - new List() - { - CacheControl , - Connection , - Date, - KeepAlive , - Pragma, - ProxyConnection , - Trailer , - TransferEncoding, - Upgrade , - Via , - Warning , - ContentLength , - ContentType , - ContentDisposition, - ContentEncoding , - ContentLanguage , - ContentLocation, - ContentRange , - Expires , - LastModified , - Age , - Location , - ProxyAuthenticate , - RetryAfter , - Server , - SetCookie , - SetCookie2, - Vary, - WWWAuthenticate, - Accept, - AcceptCharset, - AcceptEncoding, - AcceptLanguage, - Authorization, - Cookie, - Cookie2, - Expect, - From, - Host, - IfMatch, - IfModifiedSince, - IfNoneMatch, - IfRange, - IfUnmodifiedSince, - MaxForwards, - ProxyAuthorization, - Referer, - Range, - UserAgent, - ContentMD5, - ETag, - TE, - Allow, - AcceptRanges, - P3P, - XPoweredBy, - XAspNetVersion, - SecWebSocketKey, - SecWebSocketExtensions, - SecWebSocketAccept, - Origin, - SecWebSocketProtocol, - SecWebSocketVersion, - }); - } -} + public static readonly IReadOnlyCollection All = + new ReadOnlyCollection( + new List + { + CacheControl, + Connection, + Date, + KeepAlive, + Pragma, + ProxyConnection, + Trailer, + TransferEncoding, + Upgrade, + Via, + Warning, + ContentLength, + ContentType, + ContentDisposition, + ContentEncoding, + ContentLanguage, + ContentLocation, + ContentRange, + Expires, + LastModified, + Age, + Location, + ProxyAuthenticate, + RetryAfter, + Server, + SetCookie, + SetCookie2, + Vary, + WWWAuthenticate, + Accept, + AcceptCharset, + AcceptEncoding, + AcceptLanguage, + Authorization, + Cookie, + Cookie2, + Expect, + From, + Host, + IfMatch, + IfModifiedSince, + IfNoneMatch, + IfRange, + IfUnmodifiedSince, + MaxForwards, + ProxyAuthorization, + Referer, + Range, + UserAgent, + ContentMD5, + ETag, + TE, + Allow, + AcceptRanges, + P3P, + XPoweredBy, + XAspNetVersion, + SecWebSocketKey, + SecWebSocketExtensions, + SecWebSocketAccept, + Origin, + SecWebSocketProtocol, + SecWebSocketVersion + }); +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Helpers/KeepAliveTracker.cs b/EonaCat.Network/System/Sockets/WebSockets/Helpers/KeepAliveTracker.cs index 2b6799c..ec6ebc5 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Helpers/KeepAliveTracker.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Helpers/KeepAliveTracker.cs @@ -2,171 +2,170 @@ using System.Diagnostics; using System.Threading; -namespace EonaCat.WebSockets +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +internal abstract class KeepAliveTracker : IDisposable { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + public abstract void Dispose(); + public abstract void OnDataReceived(); + public abstract void OnDataSent(); + public abstract void StartTimer(); + public abstract void StopTimer(); + public abstract void ResetTimer(); + public abstract bool ShouldSendKeepAlive(); - internal abstract class KeepAliveTracker : IDisposable + public static KeepAliveTracker Create(TimeSpan keepAliveInterval, TimerCallback keepAliveCallback) { - public abstract void OnDataReceived(); - public abstract void OnDataSent(); - public abstract void StartTimer(); - public abstract void StopTimer(); - public abstract void ResetTimer(); - public abstract bool ShouldSendKeepAlive(); - public abstract void Dispose(); - - public static KeepAliveTracker Create(TimeSpan keepAliveInterval, TimerCallback keepAliveCallback) + if ((int)keepAliveInterval.TotalMilliseconds > 0) { - if ((int)keepAliveInterval.TotalMilliseconds > 0) - { - return new DefaultKeepAliveTracker(keepAliveInterval, keepAliveCallback); - } - - return new DisabledKeepAliveTracker(); + return new DefaultKeepAliveTracker(keepAliveInterval, keepAliveCallback); } - private class DisabledKeepAliveTracker : KeepAliveTracker + return new DisabledKeepAliveTracker(); + } + + private class DisabledKeepAliveTracker : KeepAliveTracker + { + public override void OnDataReceived() { - public override void OnDataReceived() - { - } - - public override void OnDataSent() - { - } - - public override void StartTimer() - { - } - - public override void StopTimer() - { - } - - public override void ResetTimer() - { - } - - public override bool ShouldSendKeepAlive() - { - return false; - } - - public override void Dispose() - { - } } - private class DefaultKeepAliveTracker : KeepAliveTracker + public override void OnDataSent() { - private readonly TimerCallback _keepAliveTimerElapsedCallback; - private readonly TimeSpan _keepAliveInterval; - private readonly Stopwatch _lastSendActivity; - private readonly Stopwatch _lastReceiveActivity; - private Timer _keepAliveTimer; + } - public DefaultKeepAliveTracker(TimeSpan keepAliveInterval, TimerCallback keepAliveCallback) - { - _keepAliveInterval = keepAliveInterval; - _keepAliveTimerElapsedCallback = keepAliveCallback; - _lastSendActivity = new Stopwatch(); - _lastReceiveActivity = new Stopwatch(); - } + public override void StartTimer() + { + } - public override void OnDataReceived() - { - _lastReceiveActivity.Restart(); - } + public override void StopTimer() + { + } - public override void OnDataSent() - { - _lastSendActivity.Restart(); - } + public override void ResetTimer() + { + } - public override void StartTimer() - { - int keepAliveIntervalMilliseconds = (int)_keepAliveInterval.TotalMilliseconds; + public override bool ShouldSendKeepAlive() + { + return false; + } - if (ExecutionContext.IsFlowSuppressed()) - { - _keepAliveTimer = new Timer(_keepAliveTimerElapsedCallback, null, Timeout.Infinite, Timeout.Infinite); - _keepAliveTimer.Change(keepAliveIntervalMilliseconds, Timeout.Infinite); - } - else - { - using (ExecutionContext.SuppressFlow()) - { - _keepAliveTimer = new Timer(_keepAliveTimerElapsedCallback, null, Timeout.Infinite, Timeout.Infinite); - _keepAliveTimer.Change(keepAliveIntervalMilliseconds, Timeout.Infinite); - } - } - } - - public override void StopTimer() - { - if (_keepAliveTimer != null) - { - _keepAliveTimer.Change(Timeout.Infinite, Timeout.Infinite); - } - } - - public override void ResetTimer() - { - ResetTimer((int)_keepAliveInterval.TotalMilliseconds); - } - - public override bool ShouldSendKeepAlive() - { - TimeSpan idleTime = GetIdleTime(); - if (idleTime >= _keepAliveInterval) - { - return true; - } - - ResetTimer((int)(_keepAliveInterval - idleTime).TotalMilliseconds); - return false; - } - - public override void Dispose() - { - if (_keepAliveTimer != null) - { - _keepAliveTimer.Dispose(); - } - } - - private void ResetTimer(int dueInMilliseconds) - { - if (_keepAliveTimer != null) - { - _keepAliveTimer.Change(dueInMilliseconds, Timeout.Infinite); - } - } - - private TimeSpan GetIdleTime() - { - TimeSpan sinceLastSendActivity = GetTimeElapsed(_lastSendActivity); - TimeSpan sinceLastReceiveActivity = GetTimeElapsed(_lastReceiveActivity); - - if (sinceLastReceiveActivity < sinceLastSendActivity) - { - return sinceLastReceiveActivity; - } - - return sinceLastSendActivity; - } - - private TimeSpan GetTimeElapsed(Stopwatch watch) - { - if (watch.IsRunning) - { - return watch.Elapsed; - } - - return _keepAliveInterval; - } + public override void Dispose() + { } } -} + + private class DefaultKeepAliveTracker : KeepAliveTracker + { + private readonly TimeSpan _keepAliveInterval; + private readonly TimerCallback _keepAliveTimerElapsedCallback; + private readonly Stopwatch _lastReceiveActivity; + private readonly Stopwatch _lastSendActivity; + private Timer _keepAliveTimer; + + public DefaultKeepAliveTracker(TimeSpan keepAliveInterval, TimerCallback keepAliveCallback) + { + _keepAliveInterval = keepAliveInterval; + _keepAliveTimerElapsedCallback = keepAliveCallback; + _lastSendActivity = new Stopwatch(); + _lastReceiveActivity = new Stopwatch(); + } + + public override void OnDataReceived() + { + _lastReceiveActivity.Restart(); + } + + public override void OnDataSent() + { + _lastSendActivity.Restart(); + } + + public override void StartTimer() + { + var keepAliveIntervalMilliseconds = (int)_keepAliveInterval.TotalMilliseconds; + + if (ExecutionContext.IsFlowSuppressed()) + { + _keepAliveTimer = new Timer(_keepAliveTimerElapsedCallback, null, Timeout.Infinite, Timeout.Infinite); + _keepAliveTimer.Change(keepAliveIntervalMilliseconds, Timeout.Infinite); + } + else + { + using (ExecutionContext.SuppressFlow()) + { + _keepAliveTimer = new Timer(_keepAliveTimerElapsedCallback, null, Timeout.Infinite, + Timeout.Infinite); + _keepAliveTimer.Change(keepAliveIntervalMilliseconds, Timeout.Infinite); + } + } + } + + public override void StopTimer() + { + if (_keepAliveTimer != null) + { + _keepAliveTimer.Change(Timeout.Infinite, Timeout.Infinite); + } + } + + public override void ResetTimer() + { + ResetTimer((int)_keepAliveInterval.TotalMilliseconds); + } + + public override bool ShouldSendKeepAlive() + { + var idleTime = GetIdleTime(); + if (idleTime >= _keepAliveInterval) + { + return true; + } + + ResetTimer((int)(_keepAliveInterval - idleTime).TotalMilliseconds); + return false; + } + + public override void Dispose() + { + if (_keepAliveTimer != null) + { + _keepAliveTimer.Dispose(); + } + } + + private void ResetTimer(int dueInMilliseconds) + { + if (_keepAliveTimer != null) + { + _keepAliveTimer.Change(dueInMilliseconds, Timeout.Infinite); + } + } + + private TimeSpan GetIdleTime() + { + var sinceLastSendActivity = GetTimeElapsed(_lastSendActivity); + var sinceLastReceiveActivity = GetTimeElapsed(_lastReceiveActivity); + + if (sinceLastReceiveActivity < sinceLastSendActivity) + { + return sinceLastReceiveActivity; + } + + return sinceLastSendActivity; + } + + private TimeSpan GetTimeElapsed(Stopwatch watch) + { + if (watch.IsRunning) + { + return watch.Elapsed; + } + + return _keepAliveInterval; + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Helpers/StringBuilderExtensions.cs b/EonaCat.Network/System/Sockets/WebSockets/Helpers/StringBuilderExtensions.cs index 35d9085..6ec592a 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Helpers/StringBuilderExtensions.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Helpers/StringBuilderExtensions.cs @@ -1,35 +1,33 @@ using System.Text; -namespace EonaCat.WebSockets +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +internal static class StringBuilderExtensions { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private static readonly char[] _crcf = { '\r', '\n' }; - internal static class StringBuilderExtensions + public static void AppendFormatWithCrCf(this StringBuilder builder, string format, object arg) { - private static readonly char[] _crcf = new char[] { '\r', '\n' }; - - public static void AppendFormatWithCrCf(this StringBuilder builder, string format, object arg) - { - builder.AppendFormat(format, arg); - builder.Append(_crcf); - } - - public static void AppendFormatWithCrCf(this StringBuilder builder, string format, params object[] args) - { - builder.AppendFormat(format, args); - builder.Append(_crcf); - } - - public static void AppendWithCrCf(this StringBuilder builder, string text) - { - builder.Append(text); - builder.Append(_crcf); - } - - public static void AppendWithCrCf(this StringBuilder builder) - { - builder.Append(_crcf); - } + builder.AppendFormat(format, arg); + builder.Append(_crcf); } -} + + public static void AppendFormatWithCrCf(this StringBuilder builder, string format, params object[] args) + { + builder.AppendFormat(format, args); + builder.Append(_crcf); + } + + public static void AppendWithCrCf(this StringBuilder builder, string text) + { + builder.Append(text); + builder.Append(_crcf); + } + + public static void AppendWithCrCf(this StringBuilder builder) + { + builder.Append(_crcf); + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Helpers/TplExtensions.cs b/EonaCat.Network/System/Sockets/WebSockets/Helpers/TplExtensions.cs index 91c5327..b489658 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Helpers/TplExtensions.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Helpers/TplExtensions.cs @@ -1,14 +1,12 @@ using System.Threading.Tasks; -namespace EonaCat.WebSockets -{ - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. - internal static class TplExtensions +internal static class TplExtensions +{ + public static void Forget(this Task task) { - public static void Forget(this Task task) - { - } } -} +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Helpers/WebSocketHelpers.cs b/EonaCat.Network/System/Sockets/WebSockets/Helpers/WebSocketHelpers.cs index 200e4e9..f5028c2 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Helpers/WebSocketHelpers.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Helpers/WebSocketHelpers.cs @@ -1,77 +1,71 @@ using System; using System.Globalization; -namespace EonaCat.WebSockets +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +internal sealed class WebSocketHelpers { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - - internal sealed class WebSocketHelpers + internal static bool FindHttpMessageTerminator(byte[] buffer, int offset, int count, out int index) { - internal static bool FindHttpMessageTerminator(byte[] buffer, int offset, int count, out int index) - { - index = -1; + index = -1; - for (int i = 0; i < count; i++) + for (var i = 0; i < count; i++) + if (i + Consts.HttpMessageTerminator.Length <= count) { - if (i + Consts.HttpMessageTerminator.Length <= count) - { - bool matched = true; - for (int j = 0; j < Consts.HttpMessageTerminator.Length; j++) + var matched = true; + for (var j = 0; j < Consts.HttpMessageTerminator.Length; j++) + if (buffer[offset + i + j] != Consts.HttpMessageTerminator[j]) { - if (buffer[offset + i + j] != Consts.HttpMessageTerminator[j]) - { - matched = false; - break; - } + matched = false; + break; } - if (matched) - { - index = i; - return true; - } - } - else + if (matched) { - break; + index = i; + return true; } } - - return false; - } - - internal static bool ValidateSubprotocol(string subProtocol) - { - if (string.IsNullOrWhiteSpace(subProtocol)) + else { - throw new ArgumentNullException("subProtocol"); + break; } - string separators = "()<>@,;:\\\"/[]?={} "; - - char[] chars = subProtocol.ToCharArray(); - string invalidChar = null; - int i = 0; - while (i < chars.Length) - { - char ch = chars[i]; - if (ch < 0x21 || ch > 0x7e) - { - invalidChar = string.Format(CultureInfo.InvariantCulture, "[{0}]", (int)ch); - break; - } - - if (!char.IsLetterOrDigit(ch) && separators.IndexOf(ch) >= 0) - { - invalidChar = ch.ToString(); - break; - } - - i++; - } - - return invalidChar == null; - } + return false; } -} + + internal static bool ValidateSubprotocol(string subProtocol) + { + if (string.IsNullOrWhiteSpace(subProtocol)) + { + throw new ArgumentNullException("subProtocol"); + } + + var separators = "()<>@,;:\\\"/[]?={} "; + + var chars = subProtocol.ToCharArray(); + string invalidChar = null; + var i = 0; + while (i < chars.Length) + { + var ch = chars[i]; + if (ch < 0x21 || ch > 0x7e) + { + invalidChar = string.Format(CultureInfo.InvariantCulture, "[{0}]", (int)ch); + break; + } + + if (!char.IsLetterOrDigit(ch) && separators.IndexOf(ch) >= 0) + { + invalidChar = ch.ToString(); + break; + } + + i++; + } + + return invalidChar == null; + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Server/AsyncWebSocketServer.cs b/EonaCat.Network/System/Sockets/WebSockets/Server/AsyncWebSocketServer.cs index eddbd84..ec4e9fa 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Server/AsyncWebSocketServer.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Server/AsyncWebSocketServer.cs @@ -6,341 +6,309 @@ using System.Net; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; -using EonaCat.WebSockets.Buffer; using EonaCat.Logger.Extensions; using EonaCat.Network; -namespace EonaCat.WebSockets +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public sealed class AsyncWebSocketServer { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private const int _none = 0; + private const int _listening = 1; + private const int _disposed = 5; + private readonly AsyncWebSocketServerModuleCatalog _catalog; + private readonly AsyncWebSocketServerConfiguration _configuration; + private readonly ConcurrentDictionary _sessions = new(); + private TcpListener _listener; + private AsyncWebSocketRouteResolver _routeResolver; - public sealed class AsyncWebSocketServer + private int _state; + + public AsyncWebSocketServer(int listenedPort, AsyncWebSocketServerModuleCatalog catalog, + AsyncWebSocketServerConfiguration configuration = null) + : this(IPAddress.Any, listenedPort, catalog, configuration) { - #region Fields + } - private TcpListener _listener; - private readonly ConcurrentDictionary _sessions = new ConcurrentDictionary(); - private readonly AsyncWebSocketServerModuleCatalog _catalog; - private readonly AsyncWebSocketServerConfiguration _configuration; - private AsyncWebSocketRouteResolver _routeResolver; + public AsyncWebSocketServer(IPAddress listenedAddress, int listenedPort, AsyncWebSocketServerModuleCatalog catalog, + AsyncWebSocketServerConfiguration configuration = null) + : this(new IPEndPoint(listenedAddress, listenedPort), catalog, configuration) + { + } - private int _state; - private const int _none = 0; - private const int _listening = 1; - private const int _disposed = 5; - - #endregion - - #region Constructors - - public AsyncWebSocketServer(int listenedPort, AsyncWebSocketServerModuleCatalog catalog, AsyncWebSocketServerConfiguration configuration = null) - : this(IPAddress.Any, listenedPort, catalog, configuration) + public AsyncWebSocketServer(IPEndPoint listenedEndPoint, AsyncWebSocketServerModuleCatalog catalog, + AsyncWebSocketServerConfiguration configuration = null) + { + if (listenedEndPoint == null) { + throw new ArgumentNullException("listenedEndPoint"); } - public AsyncWebSocketServer(IPAddress listenedAddress, int listenedPort, AsyncWebSocketServerModuleCatalog catalog, AsyncWebSocketServerConfiguration configuration = null) - : this(new IPEndPoint(listenedAddress, listenedPort), catalog, configuration) + if (catalog == null) { + throw new ArgumentNullException("catalog"); } - public AsyncWebSocketServer(IPEndPoint listenedEndPoint, AsyncWebSocketServerModuleCatalog catalog, AsyncWebSocketServerConfiguration configuration = null) + ListenedEndPoint = listenedEndPoint; + _catalog = catalog; + _configuration = configuration ?? new AsyncWebSocketServerConfiguration(); + + if (_configuration.BufferManager == null) { - if (listenedEndPoint == null) - { - throw new ArgumentNullException("listenedEndPoint"); - } - - if (catalog == null) - { - throw new ArgumentNullException("catalog"); - } - - this.ListenedEndPoint = listenedEndPoint; - _catalog = catalog; - _configuration = configuration ?? new AsyncWebSocketServerConfiguration(); - - if (_configuration.BufferManager == null) - { - throw new InvalidProgramException("The buffer manager in configuration cannot be null."); - } - - Initialize(); + throw new InvalidProgramException("The buffer manager in configuration cannot be null."); } - private void Initialize() + Initialize(); + } + + public IPEndPoint ListenedEndPoint { get; } + public bool IsListening => _state == _listening; + public int SessionCount => _sessions.Count; + + public IEnumerable EnabledExtensions => + _configuration.EnabledExtensions != null ? _configuration.EnabledExtensions.Keys : null; + + public IEnumerable EnabledSubProtocols => + _configuration.EnabledSubProtocols != null ? _configuration.EnabledSubProtocols.Keys : null; + + private void Initialize() + { + _routeResolver = new AsyncWebSocketRouteResolver(_catalog); + } + + + public void Listen() + { + var origin = Interlocked.CompareExchange(ref _state, _listening, _none); + if (origin == _disposed) { - _routeResolver = new AsyncWebSocketRouteResolver(_catalog); + throw new ObjectDisposedException(GetType().FullName); } - #endregion - - #region Properties - - public IPEndPoint ListenedEndPoint { get; private set; } - public bool IsListening { get { return _state == _listening; } } - public int SessionCount { get { return _sessions.Count; } } - - public IEnumerable EnabledExtensions + if (origin != _none) { - get { return _configuration.EnabledExtensions != null ? _configuration.EnabledExtensions.Keys : null; } - } - public IEnumerable EnabledSubProtocols - { - get { return _configuration.EnabledSubProtocols != null ? _configuration.EnabledSubProtocols.Keys : null; } + throw new InvalidOperationException("This websocket server has already started."); } - #endregion - - #region Server - - public void Listen() + try { - int origin = Interlocked.CompareExchange(ref _state, _listening, _none); - if (origin == _disposed) - { - throw new ObjectDisposedException(GetType().FullName); - } - else if (origin != _none) - { - throw new InvalidOperationException("This websocket server has already started."); - } + _listener = new TcpListener(ListenedEndPoint); + ConfigureListener(); - try - { - _listener = new TcpListener(this.ListenedEndPoint); - ConfigureListener(); + _listener.Start(_configuration.PendingConnectionBacklog); - _listener.Start(_configuration.PendingConnectionBacklog); - - Task.Factory.StartNew(async () => - { - await Accept(); - }, - TaskCreationOptions.LongRunning) + Task.Factory.StartNew(async () => { await Accept(); }, + TaskCreationOptions.LongRunning) .Forget(); - } - catch (Exception ex) when (!ShouldThrow(ex)) { } + } + catch (Exception ex) when (!ShouldThrow(ex)) + { + } + } + + public void Shutdown() + { + if (Interlocked.Exchange(ref _state, _disposed) == _disposed) + { + return; } - public void Shutdown() + try { - if (Interlocked.Exchange(ref _state, _disposed) == _disposed) - { - return; - } + _listener.Stop(); + _listener = null; - try - { - _listener.Stop(); - _listener = null; - - Task.Factory.StartNew(async () => - { - try + Task.Factory.StartNew(async () => { - foreach (var session in _sessions.Values) + try + { + foreach (var session in _sessions.Values) + { + await session.Close(WebSocketCloseCode.NormalClosure); + } + } + catch (Exception ex) when (!ShouldThrow(ex)) { - await session.Close(WebSocketCloseCode.NormalClosure); } - } - catch (Exception ex) when (!ShouldThrow(ex)) { } - }, - TaskCreationOptions.PreferFairness) - .Wait(); - } - catch (Exception ex) when (!ShouldThrow(ex)) { } - } - - private void ConfigureListener() - { - _listener.AllowNatTraversal(_configuration.AllowNatTraversal); - } - - public bool Pending() - { - if (!IsListening) - { - throw new InvalidOperationException("The websocket server is not active."); - } - - // determine if there are pending connection requests. - return _listener.Pending(); - } - - private async Task Accept() - { - try - { - while (IsListening) - { - var tcpClient = await _listener.AcceptTcpClientAsync(); - Task.Factory.StartNew(async () => - { - await Process(tcpClient); }, TaskCreationOptions.PreferFairness) + .Wait(); + } + catch (Exception ex) when (!ShouldThrow(ex)) + { + } + } + + private void ConfigureListener() + { + _listener.AllowNatTraversal(_configuration.AllowNatTraversal); + } + + public bool Pending() + { + if (!IsListening) + { + throw new InvalidOperationException("The websocket server is not active."); + } + + // determine if there are pending connection requests. + return _listener.Pending(); + } + + private async Task Accept() + { + try + { + while (IsListening) + { + var tcpClient = await _listener.AcceptTcpClientAsync(); + Task.Factory.StartNew(async () => { await Process(tcpClient); }, + TaskCreationOptions.PreferFairness) .Forget(); - } } - catch (Exception ex) when (!ShouldThrow(ex)) { } + } + catch (Exception ex) when (!ShouldThrow(ex)) + { + } + catch (Exception ex) + { + NetworkHelper.Logger.Error(ex.FormatExceptionToMessage()); + } + } + + private async Task Process(TcpClient acceptedTcpClient) + { + var session = new AsyncWebSocketSession(acceptedTcpClient, _configuration, _configuration.BufferManager, + _routeResolver, this); + + if (_sessions.TryAdd(session.SessionKey, session)) + { + NetworkHelper.Logger.Debug($"New session [{session}]."); + try + { + await session.Start(); + } catch (Exception ex) + when (ex is TimeoutException || ex is WebSocketException) { NetworkHelper.Logger.Error(ex.FormatExceptionToMessage()); } - } - - private async Task Process(TcpClient acceptedTcpClient) - { - var session = new AsyncWebSocketSession(acceptedTcpClient, _configuration, _configuration.BufferManager, _routeResolver, this); - - if (_sessions.TryAdd(session.SessionKey, session)) + finally { - NetworkHelper.Logger.Debug($"New session [{session}]."); - try + if (_sessions.TryRemove(session.SessionKey, out var throwAway)) { - await session.Start(); - } - catch (Exception ex) - when (ex is TimeoutException || ex is WebSocketException) - { - NetworkHelper.Logger.Error(ex.FormatExceptionToMessage()); - } - finally - { - AsyncWebSocketSession throwAway; - if (_sessions.TryRemove(session.SessionKey, out throwAway)) - { - NetworkHelper.Logger.Debug($"Close session [{throwAway}]."); - } + NetworkHelper.Logger.Debug($"Close session [{throwAway}]."); } } } - - private bool ShouldThrow(Exception ex) - { - if (ex is ObjectDisposedException - || ex is InvalidOperationException - || ex is SocketException - || ex is IOException) - { - return false; - } - return true; - } - - #endregion - - #region Send - - public async Task SendTextToAsync(string sessionKey, string text) - { - AsyncWebSocketSession sessionFound; - if (_sessions.TryGetValue(sessionKey, out sessionFound)) - { - await sessionFound.SendTextAsync(text); - } - else - { - NetworkHelper.Logger.Warn(string.Format("Cannot find session [{0}].", sessionKey)); - } - } - - public async Task SendTextToAsync(AsyncWebSocketSession session, string text) - { - AsyncWebSocketSession sessionFound; - if (_sessions.TryGetValue(session.SessionKey, out sessionFound)) - { - await sessionFound.SendTextAsync(text); - } - else - { - NetworkHelper.Logger.Warn($"Send text data but cannot find session [{session}]."); - } - } - - public async Task SendBinaryToAsync(string sessionKey, byte[] data) - { - await SendBinaryToAsync(sessionKey, data, 0, data.Length); - } - - public async Task SendBinaryToAsync(string sessionKey, byte[] data, int offset, int count) - { - AsyncWebSocketSession sessionFound; - if (_sessions.TryGetValue(sessionKey, out sessionFound)) - { - await sessionFound.SendBinaryAsync(data, offset, count); - } - else - { - NetworkHelper.Logger.Warn($"Cannot find session [{sessionKey}]."); - } - } - - public async Task SendBinaryToAsync(AsyncWebSocketSession session, byte[] data) - { - await SendBinaryToAsync(session, data, 0, data.Length); - } - - public async Task SendBinaryToAsync(AsyncWebSocketSession session, byte[] data, int offset, int count) - { - AsyncWebSocketSession sessionFound; - if (_sessions.TryGetValue(session.SessionKey, out sessionFound)) - { - await sessionFound.SendBinaryAsync(data, offset, count); - } - else - { - NetworkHelper.Logger.Warn($"Send binary data but cannot find session [{session}]."); - } - } - - public async Task BroadcastTextAsync(string text) - { - foreach (var session in _sessions.Values) - { - await session.SendTextAsync(text); - } - } - - public async Task BroadcastBinaryAsync(byte[] data) - { - await BroadcastBinaryAsync(data, 0, data.Length); - } - - public async Task BroadcastBinaryAsync(byte[] data, int offset, int count) - { - foreach (var session in _sessions.Values) - { - await session.SendBinaryAsync(data, offset, count); - } - } - - #endregion - - #region Session - - public bool HasSession(string sessionKey) - { - return _sessions.ContainsKey(sessionKey); - } - - public AsyncWebSocketSession GetSession(string sessionKey) - { - AsyncWebSocketSession session = null; - _sessions.TryGetValue(sessionKey, out session); - return session; - } - - public async Task CloseSession(string sessionKey) - { - AsyncWebSocketSession session = null; - if (_sessions.TryGetValue(sessionKey, out session)) - { - await session.Close(WebSocketCloseCode.NormalClosure); - } - } - - #endregion } -} + + private static bool ShouldThrow(Exception ex) + { + return ex is not ObjectDisposedException + && ex is not InvalidOperationException + && ex is not SocketException + && ex is not IOException; + } + + public async Task SendTextToAsync(string sessionKey, string text) + { + if (_sessions.TryGetValue(sessionKey, out var sessionFound)) + { + await sessionFound.SendTextAsync(text); + } + else + { + NetworkHelper.Logger.Warn($"Cannot find session [{sessionKey}]."); + } + } + + public async Task SendTextToAsync(AsyncWebSocketSession session, string text) + { + AsyncWebSocketSession sessionFound; + if (_sessions.TryGetValue(session.SessionKey, out sessionFound)) + { + await sessionFound.SendTextAsync(text); + } + else + { + NetworkHelper.Logger.Warn($"Send text data but cannot find session [{session}]."); + } + } + + public async Task SendBinaryToAsync(string sessionKey, byte[] data) + { + await SendBinaryToAsync(sessionKey, data, 0, data.Length); + } + + public async Task SendBinaryToAsync(string sessionKey, byte[] data, int offset, int count) + { + if (_sessions.TryGetValue(sessionKey, out var sessionFound)) + { + await sessionFound.SendBinaryAsync(data, offset, count); + } + else + { + NetworkHelper.Logger.Warn($"Cannot find session [{sessionKey}]."); + } + } + + public async Task SendBinaryToAsync(AsyncWebSocketSession session, byte[] data) + { + await SendBinaryToAsync(session, data, 0, data.Length); + } + + public async Task SendBinaryToAsync(AsyncWebSocketSession session, byte[] data, int offset, int count) + { + if (_sessions.TryGetValue(session.SessionKey, out var sessionFound)) + { + await sessionFound.SendBinaryAsync(data, offset, count); + } + else + { + NetworkHelper.Logger.Warn($"Send binary data but cannot find session [{session}]."); + } + } + + public async Task BroadcastTextAsync(string text) + { + foreach (var session in _sessions.Values) + { + await session.SendTextAsync(text); + } + } + + public async Task BroadcastBinaryAsync(byte[] data) + { + await BroadcastBinaryAsync(data, 0, data.Length); + } + + public async Task BroadcastBinaryAsync(byte[] data, int offset, int count) + { + foreach (var session in _sessions.Values) + { + await session.SendBinaryAsync(data, offset, count); + } + } + + public bool HasSession(string sessionKey) + { + return _sessions.ContainsKey(sessionKey); + } + + public AsyncWebSocketSession GetSession(string sessionKey) + { + _sessions.TryGetValue(sessionKey, out var session); + return session; + } + + public async Task CloseSession(string sessionKey) + { + if (_sessions.TryGetValue(sessionKey, out var session)) + { + await session.Close(WebSocketCloseCode.NormalClosure); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Server/AsyncWebSocketServerConfiguration.cs b/EonaCat.Network/System/Sockets/WebSockets/Server/AsyncWebSocketServerConfiguration.cs index 19c1f15..067847e 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Server/AsyncWebSocketServerConfiguration.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Server/AsyncWebSocketServerConfiguration.cs @@ -8,73 +8,71 @@ using EonaCat.WebSockets.Buffer; using EonaCat.WebSockets.Extensions; using EonaCat.WebSockets.SubProtocols; -namespace EonaCat.WebSockets +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public sealed class AsyncWebSocketServerConfiguration { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - - public sealed class AsyncWebSocketServerConfiguration + public AsyncWebSocketServerConfiguration() { - public AsyncWebSocketServerConfiguration() + BufferManager = new SegmentBufferManager(1024, 8192, 1, true); + ReceiveBufferSize = 8192; + SendBufferSize = 8192; + ReceiveTimeout = TimeSpan.Zero; + SendTimeout = TimeSpan.Zero; + NoDelay = true; + LingerState = new LingerOption(false, 0); // The socket will linger for x seconds after Socket.Close is called. + + PendingConnectionBacklog = 200; + AllowNatTraversal = true; + + SslEnabled = false; + SslServerCertificate = null; + SslEncryptionPolicy = EncryptionPolicy.RequireEncryption; + SslEnabledProtocols = SslProtocols.Ssl3 | SslProtocols.Tls; + SslClientCertificateRequired = true; + SslCheckCertificateRevocation = false; + SslPolicyErrorsBypassed = false; + + ConnectTimeout = TimeSpan.FromSeconds(10); + CloseTimeout = TimeSpan.FromSeconds(5); + KeepAliveInterval = TimeSpan.FromSeconds(60); + KeepAliveTimeout = TimeSpan.FromSeconds(15); + ReasonableFragmentSize = 4096; + + EnabledExtensions = new Dictionary { - BufferManager = new SegmentBufferManager(1024, 8192, 1, true); - ReceiveBufferSize = 8192; - SendBufferSize = 8192; - ReceiveTimeout = TimeSpan.Zero; - SendTimeout = TimeSpan.Zero; - NoDelay = true; - LingerState = new LingerOption(false, 0); // The socket will linger for x seconds after Socket.Close is called. - - PendingConnectionBacklog = 200; - AllowNatTraversal = true; - - SslEnabled = false; - SslServerCertificate = null; - SslEncryptionPolicy = EncryptionPolicy.RequireEncryption; - SslEnabledProtocols = SslProtocols.Ssl3 | SslProtocols.Tls; - SslClientCertificateRequired = true; - SslCheckCertificateRevocation = false; - SslPolicyErrorsBypassed = false; - - ConnectTimeout = TimeSpan.FromSeconds(10); - CloseTimeout = TimeSpan.FromSeconds(5); - KeepAliveInterval = TimeSpan.FromSeconds(60); - KeepAliveTimeout = TimeSpan.FromSeconds(15); - ReasonableFragmentSize = 4096; - - EnabledExtensions = new Dictionary() - { - { PerMessageCompressionExtension.RegisteredToken, new PerMessageCompressionExtensionNegotiator() }, - }; - EnabledSubProtocols = new Dictionary(); - } - - public ISegmentBufferManager BufferManager { get; set; } - public int ReceiveBufferSize { get; set; } - public int SendBufferSize { get; set; } - public TimeSpan ReceiveTimeout { get; set; } - public TimeSpan SendTimeout { get; set; } - public bool NoDelay { get; set; } - public LingerOption LingerState { get; set; } - - public int PendingConnectionBacklog { get; set; } - public bool AllowNatTraversal { get; set; } - - public bool SslEnabled { get; set; } - public X509Certificate2 SslServerCertificate { get; set; } - public EncryptionPolicy SslEncryptionPolicy { get; set; } - public SslProtocols SslEnabledProtocols { get; set; } - public bool SslClientCertificateRequired { get; set; } - public bool SslCheckCertificateRevocation { get; set; } - public bool SslPolicyErrorsBypassed { get; set; } - - public TimeSpan ConnectTimeout { get; set; } - public TimeSpan CloseTimeout { get; set; } - public TimeSpan KeepAliveInterval { get; set; } - public TimeSpan KeepAliveTimeout { get; set; } - public int ReasonableFragmentSize { get; set; } - - public Dictionary EnabledExtensions { get; set; } - public Dictionary EnabledSubProtocols { get; set; } + { PerMessageCompressionExtension.RegisteredToken, new PerMessageCompressionExtensionNegotiator() } + }; + EnabledSubProtocols = new Dictionary(); } -} + + public ISegmentBufferManager BufferManager { get; set; } + public int ReceiveBufferSize { get; set; } + public int SendBufferSize { get; set; } + public TimeSpan ReceiveTimeout { get; set; } + public TimeSpan SendTimeout { get; set; } + public bool NoDelay { get; set; } + public LingerOption LingerState { get; set; } + + public int PendingConnectionBacklog { get; set; } + public bool AllowNatTraversal { get; set; } + + public bool SslEnabled { get; set; } + public X509Certificate2 SslServerCertificate { get; set; } + public EncryptionPolicy SslEncryptionPolicy { get; set; } + public SslProtocols SslEnabledProtocols { get; set; } + public bool SslClientCertificateRequired { get; set; } + public bool SslCheckCertificateRevocation { get; set; } + public bool SslPolicyErrorsBypassed { get; set; } + + public TimeSpan ConnectTimeout { get; set; } + public TimeSpan CloseTimeout { get; set; } + public TimeSpan KeepAliveInterval { get; set; } + public TimeSpan KeepAliveTimeout { get; set; } + public int ReasonableFragmentSize { get; set; } + + public Dictionary EnabledExtensions { get; set; } + public Dictionary EnabledSubProtocols { get; set; } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Server/AsyncWebSocketSession.cs b/EonaCat.Network/System/Sockets/WebSockets/Server/AsyncWebSocketSession.cs index 477672f..951d3f0 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Server/AsyncWebSocketSession.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Server/AsyncWebSocketSession.cs @@ -4,1215 +4,1205 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net; -using System.Net.NetworkInformation; using System.Net.Security; using System.Net.Sockets; -using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; using System.Threading.Tasks; -using EonaCat.WebSockets.Buffer; using EonaCat.Logger.Extensions; using EonaCat.Network; +using EonaCat.WebSockets.Buffer; using EonaCat.WebSockets.Extensions; using EonaCat.WebSockets.SubProtocols; -namespace EonaCat.WebSockets +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public sealed class AsyncWebSocketSession : IDisposable { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private const int _none = 0; + private const int _connecting = 1; + private const int _connected = 2; + private const int _closing = 3; + private const int _disposed = 5; + private readonly ISegmentBufferManager _bufferManager; + private readonly AsyncWebSocketServerConfiguration _configuration; + private readonly IFrameBuilder _frameBuilder = new WebSocketFrameBuilder(); - public sealed class AsyncWebSocketSession : IDisposable + private readonly SemaphoreSlim _keepAliveLocker = new(1, 1); + private readonly IPEndPoint _localEndPoint; + private readonly IPEndPoint _remoteEndPoint; + private readonly AsyncWebSocketRouteResolver _routeResolver; + private Timer _closingTimeoutTimer; + private Timer _keepAliveTimeoutTimer; + private KeepAliveTracker _keepAliveTracker; + private AsyncWebSocketServerModule _module; + private ArraySegment _receiveBuffer; + private int _receiveBufferOffset; + + private int _state; + private Stream _stream; + + private TcpClient _tcpClient; + + public AsyncWebSocketSession( + TcpClient tcpClient, + AsyncWebSocketServerConfiguration configuration, + ISegmentBufferManager bufferManager, + AsyncWebSocketRouteResolver routeResolver, + AsyncWebSocketServer server) { - #region Fields - - private TcpClient _tcpClient; - private readonly AsyncWebSocketServerConfiguration _configuration; - private readonly ISegmentBufferManager _bufferManager; - private readonly AsyncWebSocketRouteResolver _routeResolver; - private AsyncWebSocketServerModule _module; - private readonly AsyncWebSocketServer _server; - private readonly IFrameBuilder _frameBuilder = new WebSocketFrameBuilder(); - private readonly string _sessionKey; - private Stream _stream; - private ArraySegment _receiveBuffer = default(ArraySegment); - private int _receiveBufferOffset = 0; - private IPEndPoint _remoteEndPoint; - private IPEndPoint _localEndPoint; - - private int _state; - private const int _none = 0; - private const int _connecting = 1; - private const int _connected = 2; - private const int _closing = 3; - private const int _disposed = 5; - - private readonly SemaphoreSlim _keepAliveLocker = new SemaphoreSlim(1, 1); - private KeepAliveTracker _keepAliveTracker; - private Timer _keepAliveTimeoutTimer; - private Timer _closingTimeoutTimer; - - #endregion - - #region Constructors - - public AsyncWebSocketSession( - TcpClient tcpClient, - AsyncWebSocketServerConfiguration configuration, - ISegmentBufferManager bufferManager, - AsyncWebSocketRouteResolver routeResolver, - AsyncWebSocketServer server) + if (tcpClient == null) { - if (tcpClient == null) - { - throw new ArgumentNullException("tcpClient"); - } - - if (configuration == null) - { - throw new ArgumentNullException("configuration"); - } - - if (bufferManager == null) - { - throw new ArgumentNullException("bufferManager"); - } - - if (routeResolver == null) - { - throw new ArgumentNullException("routeResolver"); - } - - if (server == null) - { - throw new ArgumentNullException("server"); - } - - _tcpClient = tcpClient; - _configuration = configuration; - _bufferManager = bufferManager; - _routeResolver = routeResolver; - _server = server; - - _sessionKey = Guid.NewGuid().ToString(); - this.StartTime = DateTime.UtcNow; - - _remoteEndPoint = (_tcpClient != null && _tcpClient.Client.Connected) ? - (IPEndPoint)_tcpClient.Client.RemoteEndPoint : null; - _localEndPoint = (_tcpClient != null && _tcpClient.Client.Connected) ? - (IPEndPoint)_tcpClient.Client.LocalEndPoint : null; + throw new ArgumentNullException("tcpClient"); } - #endregion - - #region Properties - - public string SessionKey { get { return _sessionKey; } } - public DateTime StartTime { get; private set; } - - private bool Connected { get { return _tcpClient != null && _tcpClient.Client.Connected; } } - public IPEndPoint RemoteEndPoint { get { return Connected ? (IPEndPoint)_tcpClient.Client.RemoteEndPoint : _remoteEndPoint; } } - public IPEndPoint LocalEndPoint { get { return Connected ? (IPEndPoint)_tcpClient.Client.LocalEndPoint : _localEndPoint; } } - - public AsyncWebSocketServer Server { get { return _server; } } - - public TimeSpan ConnectTimeout { get { return _configuration.ConnectTimeout; } } - public TimeSpan CloseTimeout { get { return _configuration.CloseTimeout; } } - public TimeSpan KeepAliveInterval { get { return _configuration.KeepAliveInterval; } } - public TimeSpan KeepAliveTimeout { get { return _configuration.KeepAliveTimeout; } } - - public IDictionary EnabledExtensions { get { return _configuration.EnabledExtensions; } } - public IDictionary EnabledSubProtocols { get { return _configuration.EnabledSubProtocols; } } - public SortedList NegotiatedExtensions { get { return _frameBuilder.NegotiatedExtensions; } } - public IWebSocketSubProtocol NegotiatedSubProtocol { get; private set; } - - public WebSocketState State + if (configuration == null) { - get - { - switch (_state) - { - case _none: - return WebSocketState.None; - case _connecting: - return WebSocketState.Connecting; - case _connected: - return WebSocketState.Open; - case _closing: - return WebSocketState.Closing; - case _disposed: - return WebSocketState.Closed; - default: - return WebSocketState.Closed; - } - } + throw new ArgumentNullException("configuration"); } - public override string ToString() + if (bufferManager == null) { - return string.Format("SessionKey[{0}], RemoteEndPoint[{1}], LocalEndPoint[{2}]", - this.SessionKey, this.RemoteEndPoint, this.LocalEndPoint); + throw new ArgumentNullException("bufferManager"); } - #endregion - - #region Start - - internal async Task Start() + if (routeResolver == null) { - int origin = Interlocked.CompareExchange(ref _state, _connecting, _none); - if (origin == _disposed) + throw new ArgumentNullException("routeResolver"); + } + + if (server == null) + { + throw new ArgumentNullException("server"); + } + + _tcpClient = tcpClient; + _configuration = configuration; + _bufferManager = bufferManager; + _routeResolver = routeResolver; + Server = server; + + SessionKey = Guid.NewGuid().ToString(); + StartTime = DateTime.UtcNow; + + _remoteEndPoint = _tcpClient != null && _tcpClient.Client.Connected + ? (IPEndPoint)_tcpClient.Client.RemoteEndPoint + : null; + _localEndPoint = _tcpClient != null && _tcpClient.Client.Connected + ? (IPEndPoint)_tcpClient.Client.LocalEndPoint + : null; + } + + public string SessionKey { get; } + + public DateTime StartTime { get; } + + private bool Connected => _tcpClient != null && _tcpClient.Client.Connected; + public IPEndPoint RemoteEndPoint => Connected ? (IPEndPoint)_tcpClient.Client.RemoteEndPoint : _remoteEndPoint; + public IPEndPoint LocalEndPoint => Connected ? (IPEndPoint)_tcpClient.Client.LocalEndPoint : _localEndPoint; + + public AsyncWebSocketServer Server { get; } + + public TimeSpan ConnectTimeout => _configuration.ConnectTimeout; + public TimeSpan CloseTimeout => _configuration.CloseTimeout; + public TimeSpan KeepAliveInterval => _configuration.KeepAliveInterval; + public TimeSpan KeepAliveTimeout => _configuration.KeepAliveTimeout; + + public IDictionary EnabledExtensions => _configuration.EnabledExtensions; + + public IDictionary EnabledSubProtocols => + _configuration.EnabledSubProtocols; + + public SortedList NegotiatedExtensions => _frameBuilder.NegotiatedExtensions; + public IWebSocketSubProtocol NegotiatedSubProtocol { get; } + + public WebSocketState State + { + get + { + switch (_state) { - throw new ObjectDisposedException("This websocket session has been disposed when connecting."); + case _none: + return WebSocketState.None; + case _connecting: + return WebSocketState.Connecting; + case _connected: + return WebSocketState.Open; + case _closing: + return WebSocketState.Closing; + case _disposed: + return WebSocketState.Closed; + default: + return WebSocketState.Closed; } - else if (origin != _none) + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + internal void AgreeExtensions(IEnumerable extensions) + { + if (extensions == null) + { + throw new ArgumentNullException("extensions"); + } + + // no extension configured, but client offered, so just ignore them. + if (EnabledExtensions == null || !EnabledExtensions.Any()) + { + return; + } + + // Note that the order of extensions is significant. Any interactions + // between multiple extensions MAY be defined in the documents defining + // the extensions. In the absence of such definitions, the + // interpretation is that the header fields listed by the client in its + // request represent a preference of the header fields it wishes to use, + // with the first options listed being most preferable. The extensions + // listed by the server in response represent the extensions actually in + // use for the connection. Should the extensions modify the data and/or + // framing, the order of operations on the data should be assumed to be + // the same as the order in which the extensions are listed in the + // server's response in the opening handshake. + // For example, if there are two extensions "foo" and "bar" and if the + // header field |Sec-WebSocket-Extensions| sent by the server has the + // value "foo, bar", then operations on the data will be made as + // bar(foo(data)), be those changes to the data itself (such as + // compression) or changes to the framing that may "stack". + var agreedExtensions = new SortedList(); + var offeredExtensions = string.Join(",", extensions).Split(',') + .Select(p => p.TrimStart().TrimEnd()).Where(p => !string.IsNullOrWhiteSpace(p)); + + var order = 0; + foreach (var extension in offeredExtensions) + { + order++; + + var offeredExtensionName = extension.Split(';').First(); + if (!EnabledExtensions.ContainsKey(offeredExtensionName)) { - throw new InvalidOperationException("This websocket session is in invalid state when connecting."); + continue; } + var extensionNegotiator = EnabledExtensions[offeredExtensionName]; + + string invalidParameter; + IWebSocketExtension negotiatedExtension; + if (!extensionNegotiator.NegotiateAsServer(extension, out invalidParameter, out negotiatedExtension) + || !string.IsNullOrEmpty(invalidParameter) + || negotiatedExtension == null) + { + throw new WebSocketHandshakeException(string.Format( + "Negotiate extension with remote [{0}] failed due to extension [{1}] has invalid parameter [{2}].", + RemoteEndPoint, extension, invalidParameter)); + } + + agreedExtensions.Add(order, negotiatedExtension); + } + + // A server MUST NOT accept a PMCE extension negotiation offer together + // with another extension if the PMCE will conflict with the extension + // on their use of the RSV1 bit. A client that received a response + // accepting a PMCE extension negotiation offer together with such an + // extension MUST _Fail the WebSocket Connection_. + var isRsv1BitOccupied = false; + var isRsv2BitOccupied = false; + var isRsv3BitOccupied = false; + foreach (var extension in agreedExtensions.Values) + { + if ((isRsv1BitOccupied && extension.Rsv1BitOccupied) + || (isRsv2BitOccupied && extension.Rsv2BitOccupied) + || (isRsv3BitOccupied && extension.Rsv3BitOccupied)) + { + throw new WebSocketHandshakeException( + $"Negotiate extension with remote [{RemoteEndPoint}] failed due to conflict bit occupied."); + } + + isRsv1BitOccupied = isRsv1BitOccupied | extension.Rsv1BitOccupied; + isRsv2BitOccupied = isRsv2BitOccupied | extension.Rsv2BitOccupied; + isRsv3BitOccupied = isRsv3BitOccupied | extension.Rsv3BitOccupied; + } + + _frameBuilder.NegotiatedExtensions = agreedExtensions; + } + + internal void AgreeSubProtocols(string protocols) + { + if (string.IsNullOrWhiteSpace(protocols)) + { + throw new ArgumentNullException("protocols"); + } + } + + public override string ToString() + { + return string.Format("SessionKey[{0}], RemoteEndPoint[{1}], LocalEndPoint[{2}]", + SessionKey, RemoteEndPoint, LocalEndPoint); + } + + internal async Task Start() + { + var origin = Interlocked.CompareExchange(ref _state, _connecting, _none); + if (origin == _disposed) + { + throw new ObjectDisposedException("This websocket session has been disposed when connecting."); + } + + if (origin != _none) + { + throw new InvalidOperationException("This websocket session is in invalid state when connecting."); + } + + try + { + ResetKeepAlive(); + ConfigureClient(); + + var negotiator = NegotiateStream(_tcpClient.GetStream()); + if (!negotiator.Wait(ConnectTimeout)) + { + await Close(WebSocketCloseCode.TlsHandshakeFailed, "SSL/TLS handshake timeout."); + throw new TimeoutException( + $"Negotiate SSL/TSL with remote [{RemoteEndPoint}] timeout [{ConnectTimeout}]."); + } + + _stream = negotiator.Result; + + _receiveBuffer = _bufferManager.BorrowBuffer(); + _receiveBufferOffset = 0; + + var handshaker = OpenHandshake(); + if (!handshaker.Wait(ConnectTimeout)) + { + throw new TimeoutException($"Handshake with remote [{RemoteEndPoint}] timeout [{ConnectTimeout}]."); + } + + if (!handshaker.Result) + { + var responseBuffer = WebSocketServerHandshaker.CreateOpenningHandshakeBadRequestResponse(this); + await _stream.WriteAsync(responseBuffer, 0, responseBuffer.Length); + + throw new WebSocketException($"Handshake with remote [{RemoteEndPoint}] failed."); + } + + if (Interlocked.CompareExchange(ref _state, _connected, _connecting) != _connecting) + { + await InternalClose(false); // connected with wrong state + throw new ObjectDisposedException("This websocket session has been disposed after connected."); + } + + NetworkHelper.Logger.Debug( + $"Session started for [{RemoteEndPoint}] on [{StartTime.ToString(@"yyyy-MM-dd HH:mm:ss.fffffff")}] in module [{_module.GetType().Name}] with session count [{Server.SessionCount}]."); + + var isErrorOccurredInUserSide = false; try { - ResetKeepAlive(); - ConfigureClient(); - - var negotiator = NegotiateStream(_tcpClient.GetStream()); - if (!negotiator.Wait(ConnectTimeout)) - { - await Close(WebSocketCloseCode.TlsHandshakeFailed, "SSL/TLS handshake timeout."); - throw new TimeoutException(string.Format( - "Negotiate SSL/TSL with remote [{0}] timeout [{1}].", this.RemoteEndPoint, ConnectTimeout)); - } - _stream = negotiator.Result; - - _receiveBuffer = _bufferManager.BorrowBuffer(); - _receiveBufferOffset = 0; - - var handshaker = OpenHandshake(); - if (!handshaker.Wait(ConnectTimeout)) - { - throw new TimeoutException(string.Format( - "Handshake with remote [{0}] timeout [{1}].", this.RemoteEndPoint, ConnectTimeout)); - } - if (!handshaker.Result) - { - var responseBuffer = WebSocketServerHandshaker.CreateOpenningHandshakeBadRequestResponse(this); - await _stream.WriteAsync(responseBuffer, 0, responseBuffer.Length); - - throw new WebSocketException(string.Format( - "Handshake with remote [{0}] failed.", this.RemoteEndPoint)); - } - - if (Interlocked.CompareExchange(ref _state, _connected, _connecting) != _connecting) - { - await InternalClose(false); // connected with wrong state - throw new ObjectDisposedException("This websocket session has been disposed after connected."); - } - - NetworkHelper.Logger.Debug($"Session started for [{this.RemoteEndPoint}] on [{this.StartTime.ToString(@"yyyy-MM-dd HH:mm:ss.fffffff")}] in module [{_module.GetType().Name}] with session count [{this.Server.SessionCount}]."); - - bool isErrorOccurredInUserSide = false; - try - { - await _module.OnSessionStarted(this); - } - catch (Exception ex) - { - isErrorOccurredInUserSide = true; - await HandleUserSideError(ex); - } - - if (!isErrorOccurredInUserSide) - { - _keepAliveTracker.StartTimer(); - await Process(); - } - else - { - await InternalClose(true); // user side handle tcp connection error occurred - } + await _module.OnSessionStarted(this); } - catch (Exception ex) when (ex is TimeoutException || ex is WebSocketException) + catch (Exception ex) { - NetworkHelper.Logger.Error($"Session [{this}] exception occurred, [{ex.Message}].{Environment.NewLine}{ex.FormatExceptionToMessage()}"); - await InternalClose(true); // handle tcp connection error occurred - throw; - } - } - - private void ConfigureClient() - { - _tcpClient.ReceiveBufferSize = _configuration.ReceiveBufferSize; - _tcpClient.SendBufferSize = _configuration.SendBufferSize; - _tcpClient.ReceiveTimeout = (int)_configuration.ReceiveTimeout.TotalMilliseconds; - _tcpClient.SendTimeout = (int)_configuration.SendTimeout.TotalMilliseconds; - _tcpClient.NoDelay = _configuration.NoDelay; - _tcpClient.LingerState = _configuration.LingerState; - } - - private async Task NegotiateStream(Stream stream) - { - if (!_configuration.SslEnabled) - { - return stream; + isErrorOccurredInUserSide = true; + await HandleUserSideError(ex); } - var validateRemoteCertificate = new RemoteCertificateValidationCallback( - (object sender, - X509Certificate certificate, - X509Chain chain, - SslPolicyErrors sslPolicyErrors) - => - { - if (sslPolicyErrors == SslPolicyErrors.None) - { - return true; - } - - if (_configuration.SslPolicyErrorsBypassed) - { - return true; - } - else - { - NetworkHelper.Logger.Error($"Session [{this}] error occurred when validating remote certificate: [{this.RemoteEndPoint}], [{sslPolicyErrors}]."); - } - - return false; - }); - - var sslStream = new SslStream( - stream, - false, - validateRemoteCertificate, - null, - _configuration.SslEncryptionPolicy); - - if (!_configuration.SslClientCertificateRequired) + if (!isErrorOccurredInUserSide) { - await sslStream.AuthenticateAsServerAsync( - _configuration.SslServerCertificate); // The X509Certificate used to authenticate the server. + _keepAliveTracker.StartTimer(); + await Process(); } else { - await sslStream.AuthenticateAsServerAsync( - _configuration.SslServerCertificate, // The X509Certificate used to authenticate the server. - _configuration.SslClientCertificateRequired, // A Boolean value that specifies whether the client must supply a certificate for authentication. - _configuration.SslEnabledProtocols, // The SslProtocols value that represents the protocol used for authentication. - _configuration.SslCheckCertificateRevocation); // A Boolean value that specifies whether the certificate revocation list is checked during authentication. + await InternalClose(true); // user side handle tcp connection error occurred } + } + catch (Exception ex) when (ex is TimeoutException || ex is WebSocketException) + { + NetworkHelper.Logger.Error( + $"Session [{this}] exception occurred, [{ex.Message}].{Environment.NewLine}{ex.FormatExceptionToMessage()}"); + await InternalClose(true); // handle tcp connection error occurred + throw; + } + } - // When authentication succeeds, you must check the IsEncrypted and IsSigned properties - // to determine what security services are used by the SslStream. - // Check the IsMutuallyAuthenticated property to determine whether mutual authentication occurred. - NetworkHelper.Logger.Debug(string.Format( - "Ssl Stream: SslProtocol[{0}], IsServer[{1}], IsAuthenticated[{2}], IsEncrypted[{3}], IsSigned[{4}], IsMutuallyAuthenticated[{5}], " - + "HashAlgorithm[{6}], HashStrength[{7}], KeyExchangeAlgorithm[{8}], KeyExchangeStrength[{9}], CipherAlgorithm[{10}], CipherStrength[{11}].", - sslStream.SslProtocol, - sslStream.IsServer, - sslStream.IsAuthenticated, - sslStream.IsEncrypted, - sslStream.IsSigned, - sslStream.IsMutuallyAuthenticated, - sslStream.HashAlgorithm, - sslStream.HashStrength, - sslStream.KeyExchangeAlgorithm, - sslStream.KeyExchangeStrength, - sslStream.CipherAlgorithm, - sslStream.CipherStrength)); + private void ConfigureClient() + { + _tcpClient.ReceiveBufferSize = _configuration.ReceiveBufferSize; + _tcpClient.SendBufferSize = _configuration.SendBufferSize; + _tcpClient.ReceiveTimeout = (int)_configuration.ReceiveTimeout.TotalMilliseconds; + _tcpClient.SendTimeout = (int)_configuration.SendTimeout.TotalMilliseconds; + _tcpClient.NoDelay = _configuration.NoDelay; + _tcpClient.LingerState = _configuration.LingerState; + } - return sslStream; + private async Task NegotiateStream(Stream stream) + { + if (!_configuration.SslEnabled) + { + return stream; } - private async Task OpenHandshake() - { - bool handshakeResult = false; - - try + var validateRemoteCertificate = new RemoteCertificateValidationCallback( + (sender, certificate, chain, sslPolicyErrors) + => { - int terminatorIndex = -1; - while (!WebSocketHelpers.FindHttpMessageTerminator(_receiveBuffer.Array, _receiveBuffer.Offset, _receiveBufferOffset, out terminatorIndex)) + if (sslPolicyErrors == SslPolicyErrors.None) { - int receiveCount = await _stream.ReadAsync( - _receiveBuffer.Array, - _receiveBuffer.Offset + _receiveBufferOffset, - _receiveBuffer.Count - _receiveBufferOffset); - if (receiveCount == 0) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to receive zero bytes.", RemoteEndPoint)); - } - - SegmentBufferDeflector.ReplaceBuffer(_bufferManager, ref _receiveBuffer, ref _receiveBufferOffset, receiveCount); - - if (_receiveBufferOffset > 2048) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to receive weird stream.", RemoteEndPoint)); - } + return true; } - string secWebSocketKey = string.Empty; - string path = string.Empty; - string query = string.Empty; - handshakeResult = WebSocketServerHandshaker.HandleOpenningHandshakeRequest( - this, + if (_configuration.SslPolicyErrorsBypassed) + { + return true; + } + + NetworkHelper.Logger.Error( + $"Session [{this}] error occurred when validating remote certificate: [{RemoteEndPoint}], [{sslPolicyErrors}]."); + + return false; + }); + + var sslStream = new SslStream( + stream, + false, + validateRemoteCertificate, + null, + _configuration.SslEncryptionPolicy); + + if (!_configuration.SslClientCertificateRequired) + { + await sslStream.AuthenticateAsServerAsync( + _configuration.SslServerCertificate); // The X509Certificate used to authenticate the server. + } + else + { + await sslStream.AuthenticateAsServerAsync( + _configuration.SslServerCertificate, // The X509Certificate used to authenticate the server. + _configuration + .SslClientCertificateRequired, // A Boolean value that specifies whether the client must supply a certificate for authentication. + _configuration + .SslEnabledProtocols, // The SslProtocols value that represents the protocol used for authentication. + _configuration + .SslCheckCertificateRevocation); // A Boolean value that specifies whether the certificate revocation list is checked during authentication. + } + + // When authentication succeeds, you must check the IsEncrypted and IsSigned properties + // to determine what security services are used by the SslStream. + // Check the IsMutuallyAuthenticated property to determine whether mutual authentication occurred. + NetworkHelper.Logger.Debug(string.Format( + "Ssl Stream: SslProtocol[{0}], IsServer[{1}], IsAuthenticated[{2}], IsEncrypted[{3}], IsSigned[{4}], IsMutuallyAuthenticated[{5}], " + + "HashAlgorithm[{6}], HashStrength[{7}], KeyExchangeAlgorithm[{8}], KeyExchangeStrength[{9}], CipherAlgorithm[{10}], CipherStrength[{11}].", + sslStream.SslProtocol, + sslStream.IsServer, + sslStream.IsAuthenticated, + sslStream.IsEncrypted, + sslStream.IsSigned, + sslStream.IsMutuallyAuthenticated, + sslStream.HashAlgorithm, + sslStream.HashStrength, + sslStream.KeyExchangeAlgorithm, + sslStream.KeyExchangeStrength, + sslStream.CipherAlgorithm, + sslStream.CipherStrength)); + + return sslStream; + } + + private async Task OpenHandshake() + { + var handshakeResult = false; + + try + { + var terminatorIndex = -1; + while (!WebSocketHelpers.FindHttpMessageTerminator(_receiveBuffer.Array, _receiveBuffer.Offset, + _receiveBufferOffset, out terminatorIndex)) + { + var receiveCount = await _stream.ReadAsync( _receiveBuffer.Array, - _receiveBuffer.Offset, - terminatorIndex + Consts.HttpMessageTerminator.Length, - out secWebSocketKey, out path, out query); - - _module = _routeResolver.Resolve(path, query); - if (_module == null) + _receiveBuffer.Offset + _receiveBufferOffset, + _receiveBuffer.Count - _receiveBufferOffset); + if (receiveCount == 0) { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to cannot identify the resource name [{1}{2}].", RemoteEndPoint, path, query)); + throw new WebSocketHandshakeException( + $"Handshake with remote [{RemoteEndPoint}] failed due to receive zero bytes."); } - if (handshakeResult) + SegmentBufferDeflector.ReplaceBuffer(_bufferManager, ref _receiveBuffer, ref _receiveBufferOffset, + receiveCount); + + if (_receiveBufferOffset > 2048) { - var responseBuffer = WebSocketServerHandshaker.CreateOpenningHandshakeResponse(this, secWebSocketKey); - await _stream.WriteAsync(responseBuffer, 0, responseBuffer.Length); + throw new WebSocketHandshakeException( + $"Handshake with remote [{RemoteEndPoint}] failed due to receive weird stream."); + } + } + + var secWebSocketKey = string.Empty; + var path = string.Empty; + var query = string.Empty; + handshakeResult = WebSocketServerHandshaker.HandleOpenningHandshakeRequest( + this, + _receiveBuffer.Array, + _receiveBuffer.Offset, + terminatorIndex + Consts.HttpMessageTerminator.Length, + out secWebSocketKey, out path, out query); + + _module = _routeResolver.Resolve(path, query); + if (_module == null) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to cannot identify the resource name [{1}{2}].", + RemoteEndPoint, path, query)); + } + + if (handshakeResult) + { + var responseBuffer = WebSocketServerHandshaker.CreateOpenningHandshakeResponse(this, secWebSocketKey); + await _stream.WriteAsync(responseBuffer, 0, responseBuffer.Length); + } + + SegmentBufferDeflector.ShiftBuffer( + _bufferManager, + terminatorIndex + Consts.HttpMessageTerminator.Length, + ref _receiveBuffer, + ref _receiveBufferOffset); + } + catch (WebSocketHandshakeException ex) + { + NetworkHelper.Logger.Error( + $"Session [{this}] exception occurred, [{ex.Message}].{Environment.NewLine}{ex.FormatExceptionToMessage()}"); + handshakeResult = false; + } + catch (Exception) + { + handshakeResult = false; + throw; + } + + return handshakeResult; + } + + private void ResetKeepAlive() + { + _keepAliveTracker = KeepAliveTracker.Create(KeepAliveInterval, s => OnKeepAlive()); + _keepAliveTimeoutTimer = new Timer(s => OnKeepAliveTimeout(), null, Timeout.Infinite, Timeout.Infinite); + _closingTimeoutTimer = new Timer(s => OnCloseTimeout(), null, Timeout.Infinite, Timeout.Infinite); + } + + private async Task Process() + { + try + { + Header frameHeader; + byte[] payload; + int payloadOffset; + int payloadCount; + var consumedLength = 0; + + while (State == WebSocketState.Open || State == WebSocketState.Closing) + { + var receiveCount = await _stream.ReadAsync( + _receiveBuffer.Array, + _receiveBuffer.Offset + _receiveBufferOffset, + _receiveBuffer.Count - _receiveBufferOffset); + if (receiveCount == 0) + { + break; } - SegmentBufferDeflector.ShiftBuffer( - _bufferManager, - terminatorIndex + Consts.HttpMessageTerminator.Length, - ref _receiveBuffer, - ref _receiveBufferOffset); - } - catch (WebSocketHandshakeException ex) - { - NetworkHelper.Logger.Error($"Session [{this}] exception occurred, [{ex.Message}].{Environment.NewLine}{ex.FormatExceptionToMessage()}"); - handshakeResult = false; - } - catch (Exception) - { - handshakeResult = false; - throw; - } + _keepAliveTracker.OnDataReceived(); + SegmentBufferDeflector.ReplaceBuffer(_bufferManager, ref _receiveBuffer, ref _receiveBufferOffset, + receiveCount); + consumedLength = 0; - return handshakeResult; - } - - private void ResetKeepAlive() - { - _keepAliveTracker = KeepAliveTracker.Create(KeepAliveInterval, new TimerCallback((s) => OnKeepAlive())); - _keepAliveTimeoutTimer = new Timer(new TimerCallback((s) => OnKeepAliveTimeout()), null, Timeout.Infinite, Timeout.Infinite); - _closingTimeoutTimer = new Timer(new TimerCallback((s) => OnCloseTimeout()), null, Timeout.Infinite, Timeout.Infinite); - } - - #endregion - - #region Process - - private async Task Process() - { - try - { - Header frameHeader; - byte[] payload; - int payloadOffset; - int payloadCount; - int consumedLength = 0; - - while (State == WebSocketState.Open || State == WebSocketState.Closing) + while (true) { - int receiveCount = await _stream.ReadAsync( - _receiveBuffer.Array, - _receiveBuffer.Offset + _receiveBufferOffset, - _receiveBuffer.Count - _receiveBufferOffset); - if (receiveCount == 0) - { - break; - } + frameHeader = null; + payload = null; + payloadOffset = 0; + payloadCount = 0; - _keepAliveTracker.OnDataReceived(); - SegmentBufferDeflector.ReplaceBuffer(_bufferManager, ref _receiveBuffer, ref _receiveBufferOffset, receiveCount); - consumedLength = 0; - - while (true) - { - frameHeader = null; - payload = null; - payloadOffset = 0; - payloadCount = 0; - - if (_frameBuilder.TryDecodeFrameHeader( + if (_frameBuilder.TryDecodeFrameHeader( _receiveBuffer.Array, _receiveBuffer.Offset + consumedLength, _receiveBufferOffset - consumedLength, out frameHeader) - && frameHeader.Length + frameHeader.PayloadLength <= _receiveBufferOffset - consumedLength) - { - try - { - if (!frameHeader.IsMasked) - { - await Close(WebSocketCloseCode.ProtocolError, "A server MUST close the connection upon receiving a frame that is not masked."); - throw new WebSocketException(string.Format( - "Server received unmasked frame [{0}] from remote [{1}].", frameHeader.OpCode, RemoteEndPoint)); - } - - _frameBuilder.DecodePayload( - _receiveBuffer.Array, - _receiveBuffer.Offset + consumedLength, - frameHeader, - out payload, out payloadOffset, out payloadCount); - - switch (frameHeader.OpCode) - { - case OpCode.Continuation: - await HandleContinuationFrame(frameHeader, payload, payloadOffset, payloadCount); - break; - case OpCode.Text: - await HandleTextFrame(frameHeader, payload, payloadOffset, payloadCount); - break; - case OpCode.Binary: - await HandleBinaryFrame(frameHeader, payload, payloadOffset, payloadCount); - break; - case OpCode.Close: - await HandleCloseFrame(frameHeader, payload, payloadOffset, payloadCount); - break; - case OpCode.Ping: - await HandlePingFrame(frameHeader, payload, payloadOffset, payloadCount); - break; - case OpCode.Pong: - await HandlePongFrame(frameHeader, payload, payloadOffset, payloadCount); - break; - default: - { - // Incoming data MUST always be validated by both clients and servers. - // If, at any time, an endpoint is faced with data that it does not - // understand or that violates some criteria by which the endpoint - // determines safety of input, or when the endpoint sees an opening - // handshake that does not correspond to the values it is expecting - // (e.g., incorrect path or origin in the client request), the endpoint - // MAY drop the TCP connection. If the invalid data was received after - // a successful WebSocket handshake, the endpoint SHOULD send a Close - // frame with an appropriate status code (Section 7.4) before proceeding - // to _Close the WebSocket Connection_. Use of a Close frame with an - // appropriate status code can help in diagnosing the problem. If the - // invalid data is sent during the WebSocket handshake, the server - // SHOULD return an appropriate HTTP [RFC2616] status code. - await Close(WebSocketCloseCode.InvalidMessageType); - throw new NotSupportedException( - string.Format("Not support received opcode [{0}].", (byte)frameHeader.OpCode)); - } - } - } - catch (Exception ex) - { - NetworkHelper.Logger.Error($"Session [{this}] exception occurred, [{ex.Message}].{Environment.NewLine}{ex.FormatExceptionToMessage()}"); - throw; - } - finally - { - consumedLength += frameHeader.Length + frameHeader.PayloadLength; - } - } - else - { - break; - } - } - - if (_receiveBuffer != null && _receiveBuffer.Array != null) + && frameHeader.Length + frameHeader.PayloadLength <= _receiveBufferOffset - consumedLength) { - SegmentBufferDeflector.ShiftBuffer(_bufferManager, consumedLength, ref _receiveBuffer, ref _receiveBufferOffset); - } - } - } - catch (ObjectDisposedException) - { - // looking forward to a graceful quit from the ReadAsync but the inside EndRead will raise the ObjectDisposedException, - // so a gracefully close for the socket should be a Shutdown, but we cannot avoid the Close triggers this happen. - } - catch (Exception ex) - { - await HandleReceiveOperationException(ex); - } - finally - { - await InternalClose(true); // read async buffer returned, remote notifies closed - } - } - - private async Task HandleContinuationFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) - { - if (!frameHeader.IsFIN) - { - try - { - await _module.OnSessionFragmentationStreamContinued(this, payload, payloadOffset, payloadCount); - } - catch (Exception ex) - { - await HandleUserSideError(ex); - } - } - else - { - try - { - await _module.OnSessionFragmentationStreamClosed(this, payload, payloadOffset, payloadCount); - } - catch (Exception ex) - { - await HandleUserSideError(ex); - } - } - } - - private async Task HandleTextFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) - { - if (frameHeader.IsFIN) - { - try - { - var text = Encoding.UTF8.GetString(payload, payloadOffset, payloadCount); - await _module.OnSessionTextReceived(this, text); - } - catch (Exception ex) - { - await HandleUserSideError(ex); - } - } - else - { - try - { - await _module.OnSessionFragmentationStreamOpened(this, payload, payloadOffset, payloadCount); - } - catch (Exception ex) - { - await HandleUserSideError(ex); - } - } - } - - private async Task HandleBinaryFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) - { - if (frameHeader.IsFIN) - { - try - { - await _module.OnSessionBinaryReceived(this, payload, payloadOffset, payloadCount); - } - catch (Exception ex) - { - await HandleUserSideError(ex); - } - } - else - { - try - { - await _module.OnSessionFragmentationStreamOpened(this, payload, payloadOffset, payloadCount); - } - catch (Exception ex) - { - await HandleUserSideError(ex); - } - } - } - - private async Task HandleCloseFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) - { - if (!frameHeader.IsFIN) - { - throw new WebSocketException(string.Format( - "Server received unfinished frame [{0}] from remote [{1}].", frameHeader.OpCode, RemoteEndPoint)); - } - - if (payloadCount > 1) - { - var statusCode = payload[payloadOffset + 0] * 256 + payload[payloadOffset + 1]; - var closeCode = (WebSocketCloseCode)statusCode; - var closeReason = string.Empty; - - if (payloadCount > 2) - { - closeReason = Encoding.UTF8.GetString(payload, payloadOffset + 2, payloadCount - 2); - } -#if DEBUG - NetworkHelper.Logger.Debug($"Session [{this}] received client side close frame [{closeCode}] [{closeReason}]."); -#endif - // If an endpoint receives a Close frame and did not previously send a - // Close frame, the endpoint MUST send a Close frame in response. (When - // sending a Close frame in response, the endpoint typically echos the - // status code it received.) It SHOULD do so as soon as practical. - await Close(closeCode, closeReason); - } - else - { -#if DEBUG - NetworkHelper.Logger.Debug($"Session [{this}] received client side close frame but no status code."); -#endif - await Close(WebSocketCloseCode.InvalidPayloadData); - } - } - - private async Task HandlePingFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) - { - if (!frameHeader.IsFIN) - { - throw new WebSocketException(string.Format( - "Server received unfinished frame [{0}] from remote [{1}].", frameHeader.OpCode, RemoteEndPoint)); - } - - // Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in - // response, unless it already received a Close frame. It SHOULD - // respond with Pong frame as soon as is practical. Pong frames are - // discussed in Section 5.5.3. - // - // An endpoint MAY send a Ping frame any time after the connection is - // established and before the connection is closed. - // - // A Ping frame may serve either as a keep-alive or as a means to - // verify that the remote endpoint is still responsive. - var ping = Encoding.UTF8.GetString(payload, payloadOffset, payloadCount); -#if DEBUG - NetworkHelper.Logger.Debug($"Session [{this}] received client side ping frame [{ping}]."); -#endif - if (State == WebSocketState.Open) - { - // A Pong frame sent in response to a Ping frame must have identical - // "Application data" as found in the message body of the Ping frame being replied to. - var pong = new PongFrame(ping, false).ToArray(_frameBuilder); - await SendFrame(pong); -#if DEBUG - NetworkHelper.Logger.Debug($"Session [{this}] sends server side pong frame [{ping}]."); -#endif - } - } - - private async Task HandlePongFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) - { - if (!frameHeader.IsFIN) - { - throw new WebSocketException(string.Format( - "Server received unfinished frame [{0}] from remote [{1}].", frameHeader.OpCode, RemoteEndPoint)); - } - - // If an endpoint receives a Ping frame and has not yet sent Pong - // frame(s) in response to previous Ping frame(s), the endpoint MAY - // elect to send a Pong frame for only the most recently processed Ping frame. - // - // A Pong frame MAY be sent unsolicited. This serves as a - // unidirectional heartbeat. A response to an unsolicited Pong frame is not expected. - var pong = Encoding.UTF8.GetString(payload, payloadOffset, payloadCount); - StopKeepAliveTimeoutTimer(); -#if DEBUG - NetworkHelper.Logger.Debug($"Session [{this}] received client side pong frame [{pong}]."); -#endif - await Task.CompletedTask; - } - - #endregion - - #region Close - - public async Task Close(WebSocketCloseCode closeCode) - { - await Close(closeCode, null); - } - - public async Task Close(WebSocketCloseCode closeCode, string closeReason) - { - if (State == WebSocketState.Closed || State == WebSocketState.None) - { - return; - } - - var priorState = Interlocked.Exchange(ref _state, _closing); - switch (priorState) - { - case _connected: - { - var closingHandshake = new CloseFrame(closeCode, closeReason, false).ToArray(_frameBuilder); try { - await _stream.WriteAsync(closingHandshake, 0, closingHandshake.Length); - StartClosingTimer(); -#if DEBUG - NetworkHelper.Logger.Debug($"Session [{this}] sends server side close frame [{closeCode}] [{closeReason}]."); -#endif + if (!frameHeader.IsMasked) + { + await Close(WebSocketCloseCode.ProtocolError, + "A server MUST close the connection upon receiving a frame that is not masked."); + throw new WebSocketException(string.Format( + "Server received unmasked frame [{0}] from remote [{1}].", frameHeader.OpCode, + RemoteEndPoint)); + } + + _frameBuilder.DecodePayload( + _receiveBuffer.Array, + _receiveBuffer.Offset + consumedLength, + frameHeader, + out payload, out payloadOffset, out payloadCount); + + switch (frameHeader.OpCode) + { + case OpCode.Continuation: + await HandleContinuationFrame(frameHeader, payload, payloadOffset, payloadCount); + break; + case OpCode.Text: + await HandleTextFrame(frameHeader, payload, payloadOffset, payloadCount); + break; + case OpCode.Binary: + await HandleBinaryFrame(frameHeader, payload, payloadOffset, payloadCount); + break; + case OpCode.Close: + await HandleCloseFrame(frameHeader, payload, payloadOffset, payloadCount); + break; + case OpCode.Ping: + await HandlePingFrame(frameHeader, payload, payloadOffset, payloadCount); + break; + case OpCode.Pong: + await HandlePongFrame(frameHeader, payload, payloadOffset, payloadCount); + break; + default: + { + // Incoming data MUST always be validated by both clients and servers. + // If, at any time, an endpoint is faced with data that it does not + // understand or that violates some criteria by which the endpoint + // determines safety of input, or when the endpoint sees an opening + // handshake that does not correspond to the values it is expecting + // (e.g., incorrect path or origin in the client request), the endpoint + // MAY drop the TCP connection. If the invalid data was received after + // a successful WebSocket handshake, the endpoint SHOULD send a Close + // frame with an appropriate status code (Section 7.4) before proceeding + // to _Close the WebSocket Connection_. Use of a Close frame with an + // appropriate status code can help in diagnosing the problem. If the + // invalid data is sent during the WebSocket handshake, the server + // SHOULD return an appropriate HTTP [RFC2616] status code. + await Close(WebSocketCloseCode.InvalidMessageType); + throw new NotSupportedException( + $"Not support received opcode [{(byte)frameHeader.OpCode}]."); + } + } } catch (Exception ex) { - await HandleSendOperationException(ex); + NetworkHelper.Logger.Error( + $"Session [{this}] exception occurred, [{ex.Message}].{Environment.NewLine}{ex.FormatExceptionToMessage()}"); + throw; + } + finally + { + consumedLength += frameHeader.Length + frameHeader.PayloadLength; } - return; } - case _connecting: - case _closing: + else { - await InternalClose(true); // closing - return; + break; } - case _disposed: - case _none: - default: - return; - } - } - - private async Task InternalClose(bool shallNotifyUserSide) - { - if (Interlocked.Exchange(ref _state, _disposed) == _disposed) - { - return; - } - - Shutdown(); - - if (shallNotifyUserSide) - { - NetworkHelper.Logger.Debug($"Session closed for [{this.RemoteEndPoint}] on [{DateTime.UtcNow.ToString(@"yyyy-MM-dd HH:mm:ss.fffffff")}] in dispatcher [{_module.GetType().Name}] with session count [{this.Server.SessionCount - 1}]."); - - try - { - await _module.OnSessionClosed(this); } - catch (Exception ex) + + if (_receiveBuffer != null && _receiveBuffer.Array != null) { - await HandleUserSideError(ex); + SegmentBufferDeflector.ShiftBuffer(_bufferManager, consumedLength, ref _receiveBuffer, + ref _receiveBufferOffset); } } - - Clean(); } - - public void Shutdown() + catch (ObjectDisposedException) { - // The correct way to shut down the connection (especially if you are in a full-duplex conversation) - // is to call socket.Shutdown(SocketShutdown.Send) and give the remote party some time to close - // their send channel. This ensures that you receive any pending data instead of slamming the - // connection shut. ObjectDisposedException should never be part of the normal application flow. - if (_tcpClient != null && _tcpClient.Connected) - { - _tcpClient.Client.Shutdown(SocketShutdown.Send); - } + // looking forward to a graceful quit from the ReadAsync but the inside EndRead will raise the ObjectDisposedException, + // so a gracefully close for the socket should be a Shutdown, but we cannot avoid the Close triggers this happen. } + catch (Exception ex) + { + await HandleReceiveOperationException(ex); + } + finally + { + await InternalClose(true); // read async buffer returned, remote notifies closed + } + } - private void Clean() + private async Task HandleContinuationFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) + { + if (!frameHeader.IsFIN) { try { - try - { - if (_keepAliveTracker != null) - { - _keepAliveTracker.StopTimer(); - _keepAliveTracker.Dispose(); - } - } - catch { } - try - { - if (_keepAliveTimeoutTimer != null) - { - _keepAliveTimeoutTimer.Change(Timeout.Infinite, Timeout.Infinite); - _keepAliveTimeoutTimer.Dispose(); - } - } - catch { } - try - { - if (_closingTimeoutTimer != null) - { - _closingTimeoutTimer.Change(Timeout.Infinite, Timeout.Infinite); - _closingTimeoutTimer.Dispose(); - } - } - catch { } - try - { - if (_stream != null) - { - _stream.Dispose(); - } - } - catch { } - try - { - if (_tcpClient != null) - { - _tcpClient.Dispose(); - } - } - catch { } - } - catch { } - finally - { - _keepAliveTracker = null; - _keepAliveTimeoutTimer = null; - _closingTimeoutTimer = null; - _stream = null; - _tcpClient = null; - } - - if (_receiveBuffer != default(ArraySegment)) - { - _bufferManager.ReturnBuffer(_receiveBuffer); - } - - _receiveBuffer = default(ArraySegment); - _receiveBufferOffset = 0; - } - - public async Task Abort() - { - await InternalClose(true); // abort - } - - private void StartClosingTimer() - { - // In abnormal cases (such as not having received a TCP Close - // from the server after a reasonable amount of time) a client MAY initiate the TCP Close. - _closingTimeoutTimer.Change((int)CloseTimeout.TotalMilliseconds, Timeout.Infinite); - } - - private async void OnCloseTimeout() - { - // After both sending and receiving a Close message, an endpoint - // considers the WebSocket connection closed and MUST close the - // underlying TCP connection. The server MUST close the underlying TCP - // connection immediately; the client SHOULD wait for the server to - // close the connection but MAY close the connection at any time after - // sending and receiving a Close message, e.g., if it has not received a - // TCP Close from the server in a reasonable time period. - NetworkHelper.Logger.Warn($"Session [{this}] closing timer timeout [{CloseTimeout}] then close automatically."); - await InternalClose(true); // close timeout - } - - #endregion - - #region Exception Handler - - private async Task HandleSendOperationException(Exception ex) - { - if (IsSocketTimeOut(ex)) - { - await CloseIfShould(ex); - throw new WebSocketException(ex.Message, new TimeoutException(ex.Message, ex)); - } - - await CloseIfShould(ex); - throw new WebSocketException(ex.Message, ex); - } - - private async Task HandleReceiveOperationException(Exception ex) - { - if (IsSocketTimeOut(ex)) - { - await CloseIfShould(ex); - throw new WebSocketException(ex.Message, new TimeoutException(ex.Message, ex)); - } - - await CloseIfShould(ex); - throw new WebSocketException(ex.Message, ex); - } - - private bool IsSocketTimeOut(Exception ex) - { - return ex is IOException - && ex.InnerException != null - && ex.InnerException is SocketException - && (ex.InnerException as SocketException).SocketErrorCode == SocketError.TimedOut; - } - - private async Task CloseIfShould(Exception ex) - { - if (ex is ObjectDisposedException - || ex is InvalidOperationException - || ex is SocketException - || ex is IOException - || ex is NullReferenceException // buffer array operation - || ex is ArgumentException // buffer array operation - ) - { - NetworkHelper.Logger.Error(ex.FormatExceptionToMessage()); - - await InternalClose(true); // catch specified exception then intend to close the session - - return true; - } - - return false; - } - - private async Task HandleUserSideError(Exception ex) - { - NetworkHelper.Logger.Error($"Session [{this}] error occurred in user side [{ex.Message}].{Environment.NewLine}{ex.FormatExceptionToMessage()}"); - await Task.CompletedTask; - } - - #endregion - - #region Send - - public async Task SendTextAsync(string text) - { - await SendFrame(new TextFrame(text, false).ToArray(_frameBuilder)); - } - - public async Task SendBinaryAsync(byte[] data) - { - await SendBinaryAsync(data, 0, data.Length); - } - - public async Task SendBinaryAsync(byte[] data, int offset, int count) - { - await SendFrame(new BinaryFrame(data, offset, count, false).ToArray(_frameBuilder)); - } - - public async Task SendBinaryAsync(ArraySegment segment) - { - await SendFrame(new BinaryFrame(segment, false).ToArray(_frameBuilder)); - } - - public async Task SendStreamAsync(Stream stream) - { - if (stream == null) - { - throw new ArgumentNullException("stream"); - } - - int fragmentLength = _configuration.ReasonableFragmentSize; - var buffer = new byte[fragmentLength]; - int readCount = 0; - - readCount = await stream.ReadAsync(buffer, 0, fragmentLength); - if (readCount == 0) - { - return; - } - - await SendFrame(new BinaryFragmentationFrame(OpCode.Binary, buffer, 0, readCount, isFin: false, isMasked: false).ToArray(_frameBuilder)); - - while (true) - { - readCount = await stream.ReadAsync(buffer, 0, fragmentLength); - if (readCount != 0) - { - await SendFrame(new BinaryFragmentationFrame(OpCode.Continuation, buffer, 0, readCount, isFin: false, isMasked: false).ToArray(_frameBuilder)); - } - else - { - await SendFrame(new BinaryFragmentationFrame(OpCode.Continuation, buffer, 0, 0, isFin: true, isMasked: false).ToArray(_frameBuilder)); - break; - } - } - } - - private async Task SendFrame(byte[] frame) - { - if (frame == null) - { - throw new ArgumentNullException("frame"); - } - if (State != WebSocketState.Open) - { - throw new InvalidOperationException("This websocket session has not connected."); - } - - try - { - await _stream.WriteAsync(frame, 0, frame.Length); - _keepAliveTracker.OnDataSent(); + await _module.OnSessionFragmentationStreamContinued(this, payload, payloadOffset, payloadCount); } catch (Exception ex) { - await HandleSendOperationException(ex); + await HandleUserSideError(ex); + } + } + else + { + try + { + await _module.OnSessionFragmentationStreamClosed(this, payload, payloadOffset, payloadCount); + } + catch (Exception ex) + { + await HandleUserSideError(ex); + } + } + } + + private async Task HandleTextFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) + { + if (frameHeader.IsFIN) + { + try + { + var text = Encoding.UTF8.GetString(payload, payloadOffset, payloadCount); + await _module.OnSessionTextReceived(this, text); + } + catch (Exception ex) + { + await HandleUserSideError(ex); + } + } + else + { + try + { + await _module.OnSessionFragmentationStreamOpened(this, payload, payloadOffset, payloadCount); + } + catch (Exception ex) + { + await HandleUserSideError(ex); + } + } + } + + private async Task HandleBinaryFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) + { + if (frameHeader.IsFIN) + { + try + { + await _module.OnSessionBinaryReceived(this, payload, payloadOffset, payloadCount); + } + catch (Exception ex) + { + await HandleUserSideError(ex); + } + } + else + { + try + { + await _module.OnSessionFragmentationStreamOpened(this, payload, payloadOffset, payloadCount); + } + catch (Exception ex) + { + await HandleUserSideError(ex); + } + } + } + + private async Task HandleCloseFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) + { + if (!frameHeader.IsFIN) + { + throw new WebSocketException( + $"Server received unfinished frame [{frameHeader.OpCode}] from remote [{RemoteEndPoint}]."); + } + + if (payloadCount > 1) + { + var statusCode = payload[payloadOffset + 0] * 256 + payload[payloadOffset + 1]; + var closeCode = (WebSocketCloseCode)statusCode; + var closeReason = string.Empty; + + if (payloadCount > 2) + { + closeReason = Encoding.UTF8.GetString(payload, payloadOffset + 2, payloadCount - 2); + } +#if DEBUG + NetworkHelper.Logger.Debug($"Session [{this}] received client side close frame [{closeCode}] [{closeReason}]."); +#endif + // If an endpoint receives a Close frame and did not previously send a + // Close frame, the endpoint MUST send a Close frame in response. (When + // sending a Close frame in response, the endpoint typically echos the + // status code it received.) It SHOULD do so as soon as practical. + await Close(closeCode, closeReason); + } + else + { +#if DEBUG + NetworkHelper.Logger.Debug($"Session [{this}] received client side close frame but no status code."); +#endif + await Close(WebSocketCloseCode.InvalidPayloadData); + } + } + + private async Task HandlePingFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) + { + if (!frameHeader.IsFIN) + { + throw new WebSocketException( + $"Server received unfinished frame [{frameHeader.OpCode}] from remote [{RemoteEndPoint}]."); + } + + // Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in + // response, unless it already received a Close frame. It SHOULD + // respond with Pong frame as soon as is practical. Pong frames are + // discussed in Section 5.5.3. + // + // An endpoint MAY send a Ping frame any time after the connection is + // established and before the connection is closed. + // + // A Ping frame may serve either as a keep-alive or as a means to + // verify that the remote endpoint is still responsive. + var ping = Encoding.UTF8.GetString(payload, payloadOffset, payloadCount); +#if DEBUG + NetworkHelper.Logger.Debug($"Session [{this}] received client side ping frame [{ping}]."); +#endif + if (State == WebSocketState.Open) + { + // A Pong frame sent in response to a Ping frame must have identical + // "Application data" as found in the message body of the Ping frame being replied to. + var pong = new PongFrame(ping, false).ToArray(_frameBuilder); + await SendFrame(pong); +#if DEBUG + NetworkHelper.Logger.Debug($"Session [{this}] sends server side pong frame [{ping}]."); +#endif + } + } + + private async Task HandlePongFrame(Header frameHeader, byte[] payload, int payloadOffset, int payloadCount) + { + if (!frameHeader.IsFIN) + { + throw new WebSocketException( + $"Server received unfinished frame [{frameHeader.OpCode}] from remote [{RemoteEndPoint}]."); + } + + // If an endpoint receives a Ping frame and has not yet sent Pong + // frame(s) in response to previous Ping frame(s), the endpoint MAY + // elect to send a Pong frame for only the most recently processed Ping frame. + // + // A Pong frame MAY be sent unsolicited. This serves as a + // unidirectional heartbeat. A response to an unsolicited Pong frame is not expected. + var pong = Encoding.UTF8.GetString(payload, payloadOffset, payloadCount); + StopKeepAliveTimeoutTimer(); +#if DEBUG + NetworkHelper.Logger.Debug($"Session [{this}] received client side pong frame [{pong}]."); +#endif + await Task.CompletedTask; + } + + public async Task Close(WebSocketCloseCode closeCode) + { + await Close(closeCode, null); + } + + public async Task Close(WebSocketCloseCode closeCode, string closeReason) + { + if (State == WebSocketState.Closed || State == WebSocketState.None) + { + return; + } + + var priorState = Interlocked.Exchange(ref _state, _closing); + switch (priorState) + { + case _connected: + { + var closingHandshake = new CloseFrame(closeCode, closeReason, false).ToArray(_frameBuilder); + try + { + await _stream.WriteAsync(closingHandshake, 0, closingHandshake.Length); + StartClosingTimer(); +#if DEBUG + NetworkHelper.Logger.Debug($"Session [{this}] sends server side close frame [{closeCode}] [{closeReason}]."); +#endif + } + catch (Exception ex) + { + await HandleSendOperationException(ex); + } + + return; + } + case _connecting: + case _closing: + { + await InternalClose(true); // closing + return; + } + case _disposed: + case _none: + default: + return; + } + } + + private async Task InternalClose(bool shallNotifyUserSide) + { + if (Interlocked.Exchange(ref _state, _disposed) == _disposed) + { + return; + } + + Shutdown(); + + if (shallNotifyUserSide) + { + NetworkHelper.Logger.Debug( + $"Session closed for [{RemoteEndPoint}] on [{DateTime.UtcNow.ToString(@"yyyy-MM-dd HH:mm:ss.fffffff")}] in dispatcher [{_module.GetType().Name}] with session count [{Server.SessionCount - 1}]."); + + try + { + await _module.OnSessionClosed(this); + } + catch (Exception ex) + { + await HandleUserSideError(ex); } } - #endregion + Clean(); + } - #region Keep Alive - - private void StartKeepAliveTimeoutTimer() + public void Shutdown() + { + // The correct way to shut down the connection (especially if you are in a full-duplex conversation) + // is to call socket.Shutdown(SocketShutdown.Send) and give the remote party some time to close + // their send channel. This ensures that you receive any pending data instead of slamming the + // connection shut. ObjectDisposedException should never be part of the normal application flow. + if (_tcpClient != null && _tcpClient.Connected) { - _keepAliveTimeoutTimer.Change((int)KeepAliveTimeout.TotalMilliseconds, Timeout.Infinite); + _tcpClient.Client.Shutdown(SocketShutdown.Send); } + } - private void StopKeepAliveTimeoutTimer() + private void Clean() + { + try { - _keepAliveTimeoutTimer.Change(Timeout.Infinite, Timeout.Infinite); - } - - private async void OnKeepAliveTimeout() - { - NetworkHelper.Logger.Warn($"Session [{this}] keep-alive timer timeout [{KeepAliveTimeout}]."); - await Close(WebSocketCloseCode.AbnormalClosure, "Keep-Alive Timeout"); - } - - private async void OnKeepAlive() - { - if (await _keepAliveLocker.WaitAsync(0)) + try { - try + if (_keepAliveTracker != null) { - if (State != WebSocketState.Open) - { - return; - } + _keepAliveTracker.StopTimer(); + _keepAliveTracker.Dispose(); + } + } + catch + { + } - if (_keepAliveTracker.ShouldSendKeepAlive()) - { - var keepAliveFrame = new PingFrame(false).ToArray(_frameBuilder); - await SendFrame(keepAliveFrame); - StartKeepAliveTimeoutTimer(); + try + { + if (_keepAliveTimeoutTimer != null) + { + _keepAliveTimeoutTimer.Change(Timeout.Infinite, Timeout.Infinite); + _keepAliveTimeoutTimer.Dispose(); + } + } + catch + { + } + + try + { + if (_closingTimeoutTimer != null) + { + _closingTimeoutTimer.Change(Timeout.Infinite, Timeout.Infinite); + _closingTimeoutTimer.Dispose(); + } + } + catch + { + } + + try + { + if (_stream != null) + { + _stream.Dispose(); + } + } + catch + { + } + + try + { + if (_tcpClient != null) + { + _tcpClient.Dispose(); + } + } + catch + { + } + } + catch + { + } + finally + { + _keepAliveTracker = null; + _keepAliveTimeoutTimer = null; + _closingTimeoutTimer = null; + _stream = null; + _tcpClient = null; + } + + if (_receiveBuffer != default) + { + _bufferManager.ReturnBuffer(_receiveBuffer); + } + + _receiveBuffer = default; + _receiveBufferOffset = 0; + } + + public async Task Abort() + { + await InternalClose(true); // abort + } + + private void StartClosingTimer() + { + // In abnormal cases (such as not having received a TCP Close + // from the server after a reasonable amount of time) a client MAY initiate the TCP Close. + _closingTimeoutTimer.Change((int)CloseTimeout.TotalMilliseconds, Timeout.Infinite); + } + + private async void OnCloseTimeout() + { + // After both sending and receiving a Close message, an endpoint + // considers the WebSocket connection closed and MUST close the + // underlying TCP connection. The server MUST close the underlying TCP + // connection immediately; the client SHOULD wait for the server to + // close the connection but MAY close the connection at any time after + // sending and receiving a Close message, e.g., if it has not received a + // TCP Close from the server in a reasonable time period. + NetworkHelper.Logger.Warn($"Session [{this}] closing timer timeout [{CloseTimeout}] then close automatically."); + await InternalClose(true); // close timeout + } + + private async Task HandleSendOperationException(Exception ex) + { + if (IsSocketTimeOut(ex)) + { + await CloseIfShould(ex); + throw new WebSocketException(ex.Message, new TimeoutException(ex.Message, ex)); + } + + await CloseIfShould(ex); + throw new WebSocketException(ex.Message, ex); + } + + private async Task HandleReceiveOperationException(Exception ex) + { + if (IsSocketTimeOut(ex)) + { + await CloseIfShould(ex); + throw new WebSocketException(ex.Message, new TimeoutException(ex.Message, ex)); + } + + await CloseIfShould(ex); + throw new WebSocketException(ex.Message, ex); + } + + private bool IsSocketTimeOut(Exception ex) + { + return ex is IOException + && ex.InnerException != null + && ex.InnerException is SocketException + && (ex.InnerException as SocketException).SocketErrorCode == SocketError.TimedOut; + } + + private async Task CloseIfShould(Exception ex) + { + if (ex is ObjectDisposedException + || ex is InvalidOperationException + || ex is SocketException + || ex is IOException + || ex is NullReferenceException // buffer array operation + || ex is ArgumentException // buffer array operation + ) + { + NetworkHelper.Logger.Error(ex.FormatExceptionToMessage()); + + await InternalClose(true); // catch specified exception then intend to close the session + + return true; + } + + return false; + } + + private async Task HandleUserSideError(Exception ex) + { + NetworkHelper.Logger.Error( + $"Session [{this}] error occurred in user side [{ex.Message}].{Environment.NewLine}{ex.FormatExceptionToMessage()}"); + await Task.CompletedTask; + } + + public async Task SendTextAsync(string text) + { + await SendFrame(new TextFrame(text, false).ToArray(_frameBuilder)); + } + + public async Task SendBinaryAsync(byte[] data) + { + await SendBinaryAsync(data, 0, data.Length); + } + + public async Task SendBinaryAsync(byte[] data, int offset, int count) + { + await SendFrame(new BinaryFrame(data, offset, count, false).ToArray(_frameBuilder)); + } + + public async Task SendBinaryAsync(ArraySegment segment) + { + await SendFrame(new BinaryFrame(segment, false).ToArray(_frameBuilder)); + } + + public async Task SendStreamAsync(Stream stream) + { + if (stream == null) + { + throw new ArgumentNullException("stream"); + } + + var fragmentLength = _configuration.ReasonableFragmentSize; + var buffer = new byte[fragmentLength]; + var readCount = 0; + + readCount = await stream.ReadAsync(buffer, 0, fragmentLength); + if (readCount == 0) + { + return; + } + + await SendFrame( + new BinaryFragmentationFrame(OpCode.Binary, buffer, 0, readCount, false, false).ToArray(_frameBuilder)); + + while (true) + { + readCount = await stream.ReadAsync(buffer, 0, fragmentLength); + if (readCount != 0) + { + await SendFrame(new BinaryFragmentationFrame(OpCode.Continuation, buffer, 0, readCount, false, false) + .ToArray(_frameBuilder)); + } + else + { + await SendFrame( + new BinaryFragmentationFrame(OpCode.Continuation, buffer, 0, 0, true, false) + .ToArray(_frameBuilder)); + break; + } + } + } + + private async Task SendFrame(byte[] frame) + { + if (frame == null) + { + throw new ArgumentNullException("frame"); + } + + if (State != WebSocketState.Open) + { + throw new InvalidOperationException("This websocket session has not connected."); + } + + try + { + await _stream.WriteAsync(frame, 0, frame.Length); + _keepAliveTracker.OnDataSent(); + } + catch (Exception ex) + { + await HandleSendOperationException(ex); + } + } + + private void StartKeepAliveTimeoutTimer() + { + _keepAliveTimeoutTimer.Change((int)KeepAliveTimeout.TotalMilliseconds, Timeout.Infinite); + } + + private void StopKeepAliveTimeoutTimer() + { + _keepAliveTimeoutTimer.Change(Timeout.Infinite, Timeout.Infinite); + } + + private async void OnKeepAliveTimeout() + { + NetworkHelper.Logger.Warn($"Session [{this}] keep-alive timer timeout [{KeepAliveTimeout}]."); + await Close(WebSocketCloseCode.AbnormalClosure, "Keep-Alive Timeout"); + } + + private async void OnKeepAlive() + { + if (await _keepAliveLocker.WaitAsync(0)) + { + try + { + if (State != WebSocketState.Open) + { + return; + } + + if (_keepAliveTracker.ShouldSendKeepAlive()) + { + var keepAliveFrame = new PingFrame(false).ToArray(_frameBuilder); + await SendFrame(keepAliveFrame); + StartKeepAliveTimeoutTimer(); #if DEBUG NetworkHelper.Logger.Debug($"Session [{this}] sends server side ping frame [{string.Empty}]."); #endif - _keepAliveTracker.ResetTimer(); - } - } - catch (Exception ex) - { - NetworkHelper.Logger.Error($"Session [{this}] error occurred in user side [{ex.Message}].{Environment.NewLine}{ex.FormatExceptionToMessage()}"); - await Close(WebSocketCloseCode.EndpointUnavailable); - } - finally - { - _keepAliveLocker.Release(); + _keepAliveTracker.ResetTimer(); } } + catch (Exception ex) + { + NetworkHelper.Logger.Error( + $"Session [{this}] error occurred in user side [{ex.Message}].{Environment.NewLine}{ex.FormatExceptionToMessage()}"); + await Close(WebSocketCloseCode.EndpointUnavailable); + } + finally + { + _keepAliveLocker.Release(); + } } - - #endregion - - #region Extensions - - internal void AgreeExtensions(IEnumerable extensions) - { - if (extensions == null) - { - throw new ArgumentNullException("extensions"); - } - - // no extension configured, but client offered, so just ignore them. - if (this.EnabledExtensions == null || !this.EnabledExtensions.Any()) - { - return; - } - - // Note that the order of extensions is significant. Any interactions - // between multiple extensions MAY be defined in the documents defining - // the extensions. In the absence of such definitions, the - // interpretation is that the header fields listed by the client in its - // request represent a preference of the header fields it wishes to use, - // with the first options listed being most preferable. The extensions - // listed by the server in response represent the extensions actually in - // use for the connection. Should the extensions modify the data and/or - // framing, the order of operations on the data should be assumed to be - // the same as the order in which the extensions are listed in the - // server's response in the opening handshake. - // For example, if there are two extensions "foo" and "bar" and if the - // header field |Sec-WebSocket-Extensions| sent by the server has the - // value "foo, bar", then operations on the data will be made as - // bar(foo(data)), be those changes to the data itself (such as - // compression) or changes to the framing that may "stack". - var agreedExtensions = new SortedList(); - var offeredExtensions = string.Join(",", extensions).Split(',') - .Select(p => p.TrimStart().TrimEnd()).Where(p => !string.IsNullOrWhiteSpace(p)); - - int order = 0; - foreach (var extension in offeredExtensions) - { - order++; - - var offeredExtensionName = extension.Split(';').First(); - if (!this.EnabledExtensions.ContainsKey(offeredExtensionName)) - { - continue; - } - - var extensionNegotiator = this.EnabledExtensions[offeredExtensionName]; - - string invalidParameter; - IWebSocketExtension negotiatedExtension; - if (!extensionNegotiator.NegotiateAsServer(extension, out invalidParameter, out negotiatedExtension) - || !string.IsNullOrEmpty(invalidParameter) - || negotiatedExtension == null) - { - throw new WebSocketHandshakeException(string.Format( - "Negotiate extension with remote [{0}] failed due to extension [{1}] has invalid parameter [{2}].", - this.RemoteEndPoint, extension, invalidParameter)); - } - - agreedExtensions.Add(order, negotiatedExtension); - } - - // A server MUST NOT accept a PMCE extension negotiation offer together - // with another extension if the PMCE will conflict with the extension - // on their use of the RSV1 bit. A client that received a response - // accepting a PMCE extension negotiation offer together with such an - // extension MUST _Fail the WebSocket Connection_. - bool isRsv1BitOccupied = false; - bool isRsv2BitOccupied = false; - bool isRsv3BitOccupied = false; - foreach (var extension in agreedExtensions.Values) - { - if ((isRsv1BitOccupied && extension.Rsv1BitOccupied) - || (isRsv2BitOccupied && extension.Rsv2BitOccupied) - || (isRsv3BitOccupied && extension.Rsv3BitOccupied)) - { - throw new WebSocketHandshakeException(string.Format( - "Negotiate extension with remote [{0}] failed due to conflict bit occupied.", this.RemoteEndPoint)); - } - - isRsv1BitOccupied = isRsv1BitOccupied | extension.Rsv1BitOccupied; - isRsv2BitOccupied = isRsv2BitOccupied | extension.Rsv2BitOccupied; - isRsv3BitOccupied = isRsv3BitOccupied | extension.Rsv3BitOccupied; - } - - _frameBuilder.NegotiatedExtensions = agreedExtensions; - } - - #endregion - - #region Sub-Protocols - - internal void AgreeSubProtocols(string protocols) - { - if (string.IsNullOrWhiteSpace(protocols)) - { - throw new ArgumentNullException("protocols"); - } - } - - #endregion - - #region IDisposable Members - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - [SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_keepAliveTimeoutTimer")] - [SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_keepAliveLocker")] - [SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_closingTimeoutTimer")] - private void Dispose(bool disposing) - { - if (disposing) - { - try - { - InternalClose(false).Wait(); // disposing - } - catch (Exception ex) - { - NetworkHelper.Logger.Error(ex.FormatExceptionToMessage()); - } - } - } - - #endregion } -} + + [SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", + MessageId = "_keepAliveTimeoutTimer")] + [SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_keepAliveLocker")] + [SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_closingTimeoutTimer")] + private void Dispose(bool disposing) + { + if (disposing) + { + try + { + InternalClose(false).Wait(); // disposing + } + catch (Exception ex) + { + NetworkHelper.Logger.Error(ex.FormatExceptionToMessage()); + } + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Server/Module/AsyncWebSocketRouteResolver.cs b/EonaCat.Network/System/Sockets/WebSockets/Server/Module/AsyncWebSocketRouteResolver.cs index 169e70e..3810d61 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Server/Module/AsyncWebSocketRouteResolver.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Server/Module/AsyncWebSocketRouteResolver.cs @@ -1,34 +1,32 @@ using System; using System.Linq; -namespace EonaCat.WebSockets +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public class AsyncWebSocketRouteResolver { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private readonly AsyncWebSocketServerModuleCatalog _moduleCatalog; - public class AsyncWebSocketRouteResolver + public AsyncWebSocketRouteResolver(AsyncWebSocketServerModuleCatalog moduleCatalog) { - private AsyncWebSocketServerModuleCatalog _moduleCatalog; - - public AsyncWebSocketRouteResolver(AsyncWebSocketServerModuleCatalog moduleCatalog) + if (moduleCatalog == null) { - if (moduleCatalog == null) - { - throw new ArgumentNullException("moduleCatalog"); - } - - _moduleCatalog = moduleCatalog; + throw new ArgumentNullException("moduleCatalog"); } - public AsyncWebSocketServerModule Resolve(string path, string query) - { - var modules = _moduleCatalog.GetAllModules(); - return modules.FirstOrDefault(m => - string.Compare( - m.ModulePath.Trim().TrimStart('/').TrimEnd('/').ToLowerInvariant(), - path.Trim().TrimStart('/').TrimEnd('/').ToLowerInvariant(), - StringComparison.OrdinalIgnoreCase - ) == 0); - } + _moduleCatalog = moduleCatalog; } -} + + public AsyncWebSocketServerModule Resolve(string path, string query) + { + var modules = _moduleCatalog.GetAllModules(); + return modules.FirstOrDefault(m => + string.Compare( + m.ModulePath.Trim().TrimStart('/').TrimEnd('/').ToLowerInvariant(), + path.Trim().TrimStart('/').TrimEnd('/').ToLowerInvariant(), + StringComparison.OrdinalIgnoreCase + ) == 0); + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Server/Module/AsyncWebSocketServerModule.cs b/EonaCat.Network/System/Sockets/WebSockets/Server/Module/AsyncWebSocketServerModule.cs index ed145b4..81ad57c 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Server/Module/AsyncWebSocketServerModule.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Server/Module/AsyncWebSocketServerModule.cs @@ -3,117 +3,106 @@ using System.Diagnostics; using System.Text.RegularExpressions; using System.Threading.Tasks; -namespace EonaCat.WebSockets +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public abstract class AsyncWebSocketServerModule : IAsyncWebSocketServerMessageDispatcher { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private static readonly Regex ModuleNameExpression = new(@"(?[\w]+)Module$", RegexOptions.Compiled); - public abstract class AsyncWebSocketServerModule : IAsyncWebSocketServerMessageDispatcher + private readonly ConcurrentDictionary _sessions = new(); + + protected AsyncWebSocketServerModule() + : this(string.Empty) { - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private static readonly Regex ModuleNameExpression = new Regex(@"(?[\w]+)Module$", RegexOptions.Compiled); - - private ConcurrentDictionary _sessions = new ConcurrentDictionary(); - - protected AsyncWebSocketServerModule() - : this(string.Empty) - { - } - - protected AsyncWebSocketServerModule(string modulePath) - { - this.ModulePath = modulePath; - this.ModuleName = GetModuleName(); - } - - private string GetModuleName() - { - var typeName = this.GetType().Name; - var nameMatch = ModuleNameExpression.Match(typeName); - - if (nameMatch.Success) - { - return nameMatch.Groups["name"].Value; - } - - return typeName; - } - - public string ModuleName { get; protected set; } - - public string ModulePath { get; protected set; } - - public int SessionCount { get { return _sessions.Count; } } - - #region Dispatcher - - public virtual async Task OnSessionStarted(AsyncWebSocketSession session) - { - _sessions.TryAdd(session.SessionKey, session); - await Task.CompletedTask; - } - - public virtual async Task OnSessionTextReceived(AsyncWebSocketSession session, string text) - { - await Task.CompletedTask; - } - - public virtual async Task OnSessionBinaryReceived(AsyncWebSocketSession session, byte[] data, int offset, int count) - { - await Task.CompletedTask; - } - - public virtual async Task OnSessionClosed(AsyncWebSocketSession session) - { - AsyncWebSocketSession throwAway; - _sessions.TryRemove(session.SessionKey, out throwAway); - await Task.CompletedTask; - } - - #endregion - - #region Fragmentation - - public virtual async Task OnSessionFragmentationStreamOpened(AsyncWebSocketSession session, byte[] data, int offset, int count) - { - await Task.CompletedTask; - } - - public virtual async Task OnSessionFragmentationStreamContinued(AsyncWebSocketSession session, byte[] data, int offset, int count) - { - await Task.CompletedTask; - } - - public virtual async Task OnSessionFragmentationStreamClosed(AsyncWebSocketSession session, byte[] data, int offset, int count) - { - await Task.CompletedTask; - } - - #endregion - - #region Send - - public async Task Broadcast(string text) - { - foreach (var session in _sessions.Values) - { - await session.SendTextAsync(text); - } - } - - public async Task Broadcast(byte[] binary) - { - await Broadcast(binary, 0, binary.Length); - } - - public async Task Broadcast(byte[] binary, int offset, int count) - { - foreach (var session in _sessions.Values) - { - await session.SendBinaryAsync(binary, offset, count); - } - } - - #endregion } -} + + protected AsyncWebSocketServerModule(string modulePath) + { + ModulePath = modulePath; + ModuleName = GetModuleName(); + } + + public string ModuleName { get; protected set; } + + public string ModulePath { get; protected set; } + + public int SessionCount => _sessions.Count; + + public virtual async Task OnSessionStarted(AsyncWebSocketSession session) + { + _sessions.TryAdd(session.SessionKey, session); + await Task.CompletedTask; + } + + public virtual async Task OnSessionTextReceived(AsyncWebSocketSession session, string text) + { + await Task.CompletedTask; + } + + public virtual async Task OnSessionBinaryReceived(AsyncWebSocketSession session, byte[] data, int offset, int count) + { + await Task.CompletedTask; + } + + public virtual async Task OnSessionClosed(AsyncWebSocketSession session) + { + AsyncWebSocketSession throwAway; + _sessions.TryRemove(session.SessionKey, out throwAway); + await Task.CompletedTask; + } + + public virtual async Task OnSessionFragmentationStreamOpened(AsyncWebSocketSession session, byte[] data, int offset, + int count) + { + await Task.CompletedTask; + } + + public virtual async Task OnSessionFragmentationStreamContinued(AsyncWebSocketSession session, byte[] data, + int offset, int count) + { + await Task.CompletedTask; + } + + public virtual async Task OnSessionFragmentationStreamClosed(AsyncWebSocketSession session, byte[] data, int offset, + int count) + { + await Task.CompletedTask; + } + + private string GetModuleName() + { + var typeName = GetType().Name; + var nameMatch = ModuleNameExpression.Match(typeName); + + if (nameMatch.Success) + { + return nameMatch.Groups["name"].Value; + } + + return typeName; + } + + public async Task Broadcast(string text) + { + foreach (var session in _sessions.Values) + { + await session.SendTextAsync(text); + } + } + + public async Task Broadcast(byte[] binary) + { + await Broadcast(binary, 0, binary.Length); + } + + public async Task Broadcast(byte[] binary, int offset, int count) + { + foreach (var session in _sessions.Values) + { + await session.SendBinaryAsync(binary, offset, count); + } + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Server/Module/AsyncWebSocketServerModuleCatalog.cs b/EonaCat.Network/System/Sockets/WebSockets/Server/Module/AsyncWebSocketServerModuleCatalog.cs index 10ff3a3..7c5f4d9 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Server/Module/AsyncWebSocketServerModuleCatalog.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Server/Module/AsyncWebSocketServerModuleCatalog.cs @@ -1,28 +1,26 @@ using System; using System.Collections.Generic; -namespace EonaCat.WebSockets +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public class AsyncWebSocketServerModuleCatalog { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private readonly Dictionary _modules = new(); - public class AsyncWebSocketServerModuleCatalog + public IEnumerable GetAllModules() { - private Dictionary _modules = new Dictionary(); - - public IEnumerable GetAllModules() - { - return _modules.Values; - } - - public AsyncWebSocketServerModule GetModule(Type moduleType) - { - return _modules[moduleType.FullName]; - } - - public void RegisterModule(AsyncWebSocketServerModule module) - { - _modules.Add(module.GetType().FullName, module); - } + return _modules.Values; } -} + + public AsyncWebSocketServerModule GetModule(Type moduleType) + { + return _modules[moduleType.FullName]; + } + + public void RegisterModule(AsyncWebSocketServerModule module) + { + _modules.Add(module.GetType().FullName, module); + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Server/Module/IAsyncWebSocketServerMessageDispatcher.cs b/EonaCat.Network/System/Sockets/WebSockets/Server/Module/IAsyncWebSocketServerMessageDispatcher.cs index 70b9849..f75d78a 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Server/Module/IAsyncWebSocketServerMessageDispatcher.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Server/Module/IAsyncWebSocketServerMessageDispatcher.cs @@ -1,19 +1,17 @@ using System.Threading.Tasks; -namespace EonaCat.WebSockets +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public interface IAsyncWebSocketServerMessageDispatcher { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + Task OnSessionStarted(AsyncWebSocketSession session); + Task OnSessionTextReceived(AsyncWebSocketSession session, string text); + Task OnSessionBinaryReceived(AsyncWebSocketSession session, byte[] data, int offset, int count); + Task OnSessionClosed(AsyncWebSocketSession session); - public interface IAsyncWebSocketServerMessageDispatcher - { - Task OnSessionStarted(AsyncWebSocketSession session); - Task OnSessionTextReceived(AsyncWebSocketSession session, string text); - Task OnSessionBinaryReceived(AsyncWebSocketSession session, byte[] data, int offset, int count); - Task OnSessionClosed(AsyncWebSocketSession session); - - Task OnSessionFragmentationStreamOpened(AsyncWebSocketSession session, byte[] data, int offset, int count); - Task OnSessionFragmentationStreamContinued(AsyncWebSocketSession session, byte[] data, int offset, int count); - Task OnSessionFragmentationStreamClosed(AsyncWebSocketSession session, byte[] data, int offset, int count); - } -} + Task OnSessionFragmentationStreamOpened(AsyncWebSocketSession session, byte[] data, int offset, int count); + Task OnSessionFragmentationStreamContinued(AsyncWebSocketSession session, byte[] data, int offset, int count); + Task OnSessionFragmentationStreamClosed(AsyncWebSocketSession session, byte[] data, int offset, int count); +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/Server/WebSocketServerHandshaker.cs b/EonaCat.Network/System/Sockets/WebSockets/Server/WebSocketServerHandshaker.cs index ede6e14..1ac8976 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/Server/WebSocketServerHandshaker.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/Server/WebSocketServerHandshaker.cs @@ -5,435 +5,448 @@ using System.Net; using System.Net.Sockets; using System.Security.Cryptography; using System.Text; -using EonaCat.WebSockets.Buffer; using EonaCat.Logger.Extensions; using EonaCat.Network; +using EonaCat.WebSockets.Buffer; -namespace EonaCat.WebSockets +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +internal sealed class WebSocketServerHandshaker { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private static readonly char[] _headerLineSplitter = { '\r', '\n' }; - internal sealed class WebSocketServerHandshaker + internal static bool HandleOpenningHandshakeRequest(AsyncWebSocketSession session, byte[] buffer, int offset, + int count, + out string secWebSocketKey, + out string path, + out string query) { - private static readonly char[] _headerLineSplitter = new char[] { '\r', '\n' }; + BufferValidator.ValidateBuffer(buffer, offset, count, "buffer"); - internal static bool HandleOpenningHandshakeRequest(AsyncWebSocketSession session, byte[] buffer, int offset, int count, - out string secWebSocketKey, - out string path, - out string query) - { - BufferValidator.ValidateBuffer(buffer, offset, count, "buffer"); - - var request = Encoding.UTF8.GetString(buffer, offset, count); + var request = Encoding.UTF8.GetString(buffer, offset, count); #if DEBUG NetworkHelper.Logger.Debug($"[{session.RemoteEndPoint}]{Environment.NewLine}{request}"); #endif - try + try + { + // GET /chat HTTP/1.1 + // Host: server.example.com + // Upgrade: websocket + // Connection: Upgrade + // Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== + // Origin: http://example.com + // Sec-WebSocket-Protocol: chat, superchat + // Sec-WebSocket-Version: 13 + Dictionary headers; + List extensions; + List protocols; + ParseOpenningHandshakeRequestHeaders(request, out headers, out extensions, out protocols); + if (headers == null) { - // GET /chat HTTP/1.1 - // Host: server.example.com - // Upgrade: websocket - // Connection: Upgrade - // Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== - // Origin: http://example.com - // Sec-WebSocket-Protocol: chat, superchat - // Sec-WebSocket-Version: 13 - Dictionary headers; - List extensions; - List protocols; - ParseOpenningHandshakeRequestHeaders(request, out headers, out extensions, out protocols); - if (headers == null) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to invalid headers.", session.RemoteEndPoint)); - } + throw new WebSocketHandshakeException( + $"Handshake with remote [{session.RemoteEndPoint}] failed due to invalid headers."); + } - // An HTTP/1.1 or higher GET request, including a "Request-URI" - // [RFC2616] that should be interpreted as a /resource name/ - // defined in Section 3 (or an absolute HTTP/HTTPS URI containing the /resource name/). - // A |Host| header field containing the server's authority. - if (!headers.ContainsKey(Consts.HttpGetMethodName)) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to lack of get method.", session.RemoteEndPoint)); - } + // An HTTP/1.1 or higher GET request, including a "Request-URI" + // [RFC2616] that should be interpreted as a /resource name/ + // defined in Section 3 (or an absolute HTTP/HTTPS URI containing the /resource name/). + // A |Host| header field containing the server's authority. + if (!headers.ContainsKey(Consts.HttpGetMethodName)) + { + throw new WebSocketHandshakeException( + $"Handshake with remote [{session.RemoteEndPoint}] failed due to lack of get method."); + } - if (!headers.ContainsKey(HttpKnownHeaderNames.Host)) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to lack of host authority.", session.RemoteEndPoint)); - } + if (!headers.ContainsKey(HttpKnownHeaderNames.Host)) + { + throw new WebSocketHandshakeException( + $"Handshake with remote [{session.RemoteEndPoint}] failed due to lack of host authority."); + } - string uriString = string.Empty; - var host = headers[HttpKnownHeaderNames.Host]; - IPAddress hostIpAddress; - if (IPAddress.TryParse(host, out hostIpAddress)) + var uriString = string.Empty; + var host = headers[HttpKnownHeaderNames.Host]; + IPAddress hostIpAddress; + if (IPAddress.TryParse(host, out hostIpAddress)) + { + if (hostIpAddress.AddressFamily == AddressFamily.InterNetworkV6) { - if (hostIpAddress.AddressFamily == AddressFamily.InterNetworkV6) - { - uriString = string.Format("ws://{0}{1}", string.Format("[{0}]", host), headers[Consts.HttpGetMethodName]); - } - else - { - uriString = string.Format("ws://{0}{1}", host, headers[Consts.HttpGetMethodName]); - } + uriString = string.Format("ws://{0}{1}", $"[{host}]", + headers[Consts.HttpGetMethodName]); } else { - uriString = string.Format("ws://{0}{1}", host, headers[Consts.HttpGetMethodName]); + uriString = $"ws://{host}{headers[Consts.HttpGetMethodName]}"; } - Uri requestUri = null; - if (!Uri.TryCreate(uriString, UriKind.RelativeOrAbsolute, out requestUri)) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to invalid requested resource name.", session.RemoteEndPoint)); - } - path = requestUri.AbsolutePath; - query = requestUri.Query; - - // A |Connection| header field that includes the token "Upgrade", - // treated as an ASCII case-insensitive value. - if (!headers.ContainsKey(HttpKnownHeaderNames.Connection)) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to lack of connection header item.", session.RemoteEndPoint)); - } - - if (headers[HttpKnownHeaderNames.Connection].ToLowerInvariant() != Consts.WebSocketConnectionToken.ToLowerInvariant()) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to invalid connection header item value [{1}].", - session.RemoteEndPoint, headers[HttpKnownHeaderNames.Connection])); - } - - // An |Upgrade| header field containing the value "websocket", - // treated as an ASCII case-insensitive value. - if (!headers.ContainsKey(HttpKnownHeaderNames.Upgrade)) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to lack of upgrade header item.", session.RemoteEndPoint)); - } - - if (headers[HttpKnownHeaderNames.Upgrade].ToLowerInvariant() != Consts.WebSocketUpgradeToken.ToLowerInvariant()) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to invalid upgrade header item value [{1}].", - session.RemoteEndPoint, headers[HttpKnownHeaderNames.Upgrade])); - } - - // A |Sec-WebSocket-Key| header field with a base64-encoded (see - // Section 4 of [RFC4648]) value that, when decoded, is 16 bytes in length. - if (!headers.ContainsKey(HttpKnownHeaderNames.SecWebSocketKey)) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to lack of Sec-WebSocket-Key header item.", session.RemoteEndPoint)); - } - - if (string.IsNullOrWhiteSpace(headers[HttpKnownHeaderNames.SecWebSocketKey])) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to invalid Sec-WebSocket-Key header item value [{1}].", - session.RemoteEndPoint, headers[HttpKnownHeaderNames.SecWebSocketKey])); - } - - secWebSocketKey = headers[HttpKnownHeaderNames.SecWebSocketKey]; - - // A |Sec-WebSocket-Version| header field, with a value of 13. - if (!headers.ContainsKey(HttpKnownHeaderNames.SecWebSocketVersion)) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to lack of Sec-WebSocket-Version header item.", session.RemoteEndPoint)); - } - - if (headers[HttpKnownHeaderNames.SecWebSocketVersion].ToLowerInvariant() != Consts.WebSocketVersion.ToLowerInvariant()) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to invalid Sec-WebSocket-Version header item value [{1}].", - session.RemoteEndPoint, headers[HttpKnownHeaderNames.SecWebSocketVersion])); - } - - // Optionally, a |Sec-WebSocket-Extensions| header field, with a - // list of values indicating which extensions the client would like - // to speak. The interpretation of this header field is discussed in Section 9.1. - if (extensions != null) - { - if (!extensions.Any()) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to empty extension.", session.RemoteEndPoint)); - } - - foreach (var extension in extensions) - { - // The empty string is not the same as the null value for these - // purposes and is not a legal value for this field. - if (string.IsNullOrWhiteSpace(extension)) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to empty extension.", session.RemoteEndPoint)); - } - } - - session.AgreeExtensions(extensions); - } - - // Optionally, a |Sec-WebSocket-Protocol| header field, with a list - // of values indicating which protocols the client would like to - // speak, ordered by preference. - if (protocols != null) - { - if (!protocols.Any()) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to empty sub-protocol.", session.RemoteEndPoint)); - } - - foreach (var protocol in protocols) - { - // The empty string is not the same as the null value for these - // purposes and is not a legal value for this field. - if (string.IsNullOrWhiteSpace(protocol)) - { - throw new WebSocketHandshakeException(string.Format( - "Handshake with remote [{0}] failed due to empty sub-protocol.", session.RemoteEndPoint)); - } - } - - session.AgreeSubProtocols(string.Join(",", protocols)); - } - - // Optionally, an |Origin| header field. This header field is sent - // by all browser clients. A connection attempt lacking this - // header field SHOULD NOT be interpreted as coming from a browser client. - // - // Servers that are not intended to process input from any web page but - // only for certain sites SHOULD verify the |Origin| field is an origin - // they expect. If the origin indicated is unacceptable to the server, - // then it SHOULD respond to the WebSocket handshake with a reply - // containing HTTP 403 Forbidden status code. - // - // The |Origin| header field protects from the attack cases when the - // untrusted party is typically the author of a JavaScript application - // that is executing in the context of the trusted client. The client - // itself can contact the server and, via the mechanism of the |Origin| - // header field, determine whether to extend those communication - // privileges to the JavaScript application. The intent is not to - // prevent non-browsers from establishing connections but rather to - // ensure that trusted browsers under the control of potentially - // malicious JavaScript cannot fake a WebSocket handshake. - - // Optionally, other header fields, such as those used to send - // cookies or request authentication to a server. Unknown header - // fields are ignored, as per [RFC2616]. } - catch (Exception ex) + else { - NetworkHelper.Logger.Error($"{session}{Environment.NewLine}{request}{Environment.NewLine}{ex.FormatExceptionToMessage()}"); - throw; + uriString = $"ws://{host}{headers[Consts.HttpGetMethodName]}"; } - return true; - } + Uri requestUri = null; + if (!Uri.TryCreate(uriString, UriKind.RelativeOrAbsolute, out requestUri)) + { + throw new WebSocketHandshakeException( + $"Handshake with remote [{session.RemoteEndPoint}] failed due to invalid requested resource name."); + } - internal static byte[] CreateOpenningHandshakeResponse(AsyncWebSocketSession session, string secWebSocketKey) - { - var sb = new StringBuilder(); + path = requestUri.AbsolutePath; + query = requestUri.Query; - // A Status-Line with a 101 response code as per RFC 2616 - // [RFC2616]. Such a response could look like "HTTP/1.1 101 Switching Protocols". - sb.AppendFormatWithCrCf("HTTP/{0} {1} {2}", - Consts.HttpVersion, - (int)HttpStatusCode.SwitchingProtocols, - @"Switching Protocols"); + // A |Connection| header field that includes the token "Upgrade", + // treated as an ASCII case-insensitive value. + if (!headers.ContainsKey(HttpKnownHeaderNames.Connection)) + { + throw new WebSocketHandshakeException( + $"Handshake with remote [{session.RemoteEndPoint}] failed due to lack of connection header item."); + } - // An |Upgrade| header field with value "websocket" as per RFC2616 [RFC2616]. - sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.Upgrade, Consts.WebSocketUpgradeToken); + if (headers[HttpKnownHeaderNames.Connection].ToLowerInvariant() != + Consts.WebSocketConnectionToken.ToLowerInvariant()) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to invalid connection header item value [{1}].", + session.RemoteEndPoint, headers[HttpKnownHeaderNames.Connection])); + } - // A |Connection| header field with value "Upgrade". - sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.Connection, Consts.WebSocketConnectionToken); + // An |Upgrade| header field containing the value "websocket", + // treated as an ASCII case-insensitive value. + if (!headers.ContainsKey(HttpKnownHeaderNames.Upgrade)) + { + throw new WebSocketHandshakeException( + $"Handshake with remote [{session.RemoteEndPoint}] failed due to lack of upgrade header item."); + } - // A |Sec-WebSocket-Accept| header field. The value of this - // header field is constructed by concatenating /key/, defined - // above in step 4 in Section 4.2.2, with the string "258EAFA5- - // E914-47DA-95CA-C5AB0DC85B11", taking the SHA-1 hash of this - // concatenated value to obtain a 20-byte value and base64- - // encoding (see Section 4 of [RFC4648]) this 20-byte hash. - var secWebSocketAccept = GetSecWebSocketAcceptString(secWebSocketKey); - sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.SecWebSocketAccept, secWebSocketAccept); + if (headers[HttpKnownHeaderNames.Upgrade].ToLowerInvariant() != + Consts.WebSocketUpgradeToken.ToLowerInvariant()) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to invalid upgrade header item value [{1}].", + session.RemoteEndPoint, headers[HttpKnownHeaderNames.Upgrade])); + } + + // A |Sec-WebSocket-Key| header field with a base64-encoded (see + // Section 4 of [RFC4648]) value that, when decoded, is 16 bytes in length. + if (!headers.ContainsKey(HttpKnownHeaderNames.SecWebSocketKey)) + { + throw new WebSocketHandshakeException( + $"Handshake with remote [{session.RemoteEndPoint}] failed due to lack of Sec-WebSocket-Key header item."); + } + + if (string.IsNullOrWhiteSpace(headers[HttpKnownHeaderNames.SecWebSocketKey])) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to invalid Sec-WebSocket-Key header item value [{1}].", + session.RemoteEndPoint, headers[HttpKnownHeaderNames.SecWebSocketKey])); + } + + secWebSocketKey = headers[HttpKnownHeaderNames.SecWebSocketKey]; + + // A |Sec-WebSocket-Version| header field, with a value of 13. + if (!headers.ContainsKey(HttpKnownHeaderNames.SecWebSocketVersion)) + { + throw new WebSocketHandshakeException( + $"Handshake with remote [{session.RemoteEndPoint}] failed due to lack of Sec-WebSocket-Version header item."); + } + + if (headers[HttpKnownHeaderNames.SecWebSocketVersion].ToLowerInvariant() != + Consts.WebSocketVersion.ToLowerInvariant()) + { + throw new WebSocketHandshakeException(string.Format( + "Handshake with remote [{0}] failed due to invalid Sec-WebSocket-Version header item value [{1}].", + session.RemoteEndPoint, headers[HttpKnownHeaderNames.SecWebSocketVersion])); + } // Optionally, a |Sec-WebSocket-Extensions| header field, with a - // value /extensions/ as defined in step 4 in Section 4.2.2. If - // multiple extensions are to be used, they can all be listed in - // a single |Sec-WebSocket-Extensions| header field or split - // between multiple instances of the |Sec-WebSocket-Extensions| header field. - // A server accepts one or more extensions by including a - // |Sec-WebSocket-Extensions| header field containing one or more - // extensions that were requested by the client. The interpretation of - // any extension parameters, and what constitutes a valid response by a - // server to a requested set of parameters by a client, will be defined - // by each such extension. - if (session.NegotiatedExtensions != null && session.NegotiatedExtensions.Any()) + // list of values indicating which extensions the client would like + // to speak. The interpretation of this header field is discussed in Section 9.1. + if (extensions != null) { - foreach (var extension in session.NegotiatedExtensions.Values) + if (!extensions.Any()) { - var offer = extension.GetAgreedOffer(); - sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.SecWebSocketExtensions, offer); + throw new WebSocketHandshakeException( + $"Handshake with remote [{session.RemoteEndPoint}] failed due to empty extension."); } + + foreach (var extension in extensions) + // The empty string is not the same as the null value for these + // purposes and is not a legal value for this field. + { + if (string.IsNullOrWhiteSpace(extension)) + { + throw new WebSocketHandshakeException( + $"Handshake with remote [{session.RemoteEndPoint}] failed due to empty extension."); + } + } + + session.AgreeExtensions(extensions); } - /** - // Optionally, a |Sec-WebSocket-Protocol| header field, with a - // value /subprotocol/ as defined in step 4 in Section 4.2.2. - // - // The client can request that the server use a specific subprotocol by - // including the |Sec-WebSocket-Protocol| field in its handshake. If it - // is specified, the server needs to include the same field and one of - // the selected subprotocol values in its response for the connection to - // be established. - // - // These subprotocol names should be registered as per Section 11.5. To - // avoid potential collisions, it is recommended to use names that - // contain the ASCII version of the domain name of the subprotocol's - // originator. For example, if Example Corporation were to create a - // Chat subprotocol to be implemented by many servers around the Web, - // they could name it "chat.example.com". If the Example Organization - // called their competing subprotocol "chat.example.org", then the two - // subprotocols could be implemented by servers simultaneously, with the - // server dynamically selecting which subprotocol to use based on the - // value sent by the client. - // - // Subprotocols can be versioned in backward-incompatible ways by - // changing the subprotocol name, e.g., going from - // "bookings.example.net" to "v2.bookings.example.net". These - // subprotocols would be considered completely separate by WebSocket - // clients. Backward-compatible versioning can be implemented by - // reusing the same subprotocol string but carefully designing the - // actual subprotocol to support this kind of extensibility. - */ - - sb.AppendWithCrCf(); - - // HTTP/1.1 101 Switching Protocols - // Upgrade: websocket - // Connection: Upgrade - // Sec-WebSocket-Accept: 1tGBmA9p0DQDgmFll6P0/UcVS/E= - // Sec-WebSocket-Protocol: chat - var response = sb.ToString(); -#if DEBUG - NetworkHelper.Logger.Debug($"[{session.RemoteEndPoint}]{Environment.NewLine}{response}"); -#endif - return Encoding.UTF8.GetBytes(response); - } - - internal static byte[] CreateOpenningHandshakeBadRequestResponse(AsyncWebSocketSession session) - { - var sb = new StringBuilder(); - - // HTTP/1.1 400 Bad Request - sb.AppendFormatWithCrCf("HTTP/{0} {1} {2}", - Consts.HttpVersion, - (int)HttpStatusCode.BadRequest, - @"Bad Request"); - - // Upgrade: websocket - sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.Upgrade, Consts.WebSocketUpgradeToken); - - // Connection: Upgrade - sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.Connection, Consts.WebSocketConnectionToken); - - // Sec-WebSocket-Version: 13 - sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.SecWebSocketVersion, Consts.WebSocketVersion); - - sb.AppendWithCrCf(); - - var response = sb.ToString(); -#if DEBUG - NetworkHelper.Logger.Debug($"[{session.RemoteEndPoint}]{Environment.NewLine}{response}"); -#endif - return Encoding.UTF8.GetBytes(response); - } - - private static void ParseOpenningHandshakeRequestHeaders(string request, - out Dictionary headers, - out List extensions, - out List protocols) - { - headers = new Dictionary(); - - // The |Sec-WebSocket-Extensions| header field MAY appear multiple times - // in an HTTP request (which is logically the same as a single - // |Sec-WebSocket-Extensions| header field that contains all values. - // However, the |Sec-WebSocket-Extensions| header field MUST NOT appear - // more than once in an HTTP response. - extensions = null; - // The |Sec-WebSocket-Protocol| header field MAY appear multiple times - // in an HTTP request (which is logically the same as a single - // |Sec-WebSocket-Protocol| header field that contains all values). - // However, the |Sec-WebSocket-Protocol| header field MUST NOT appear - // more than once in an HTTP response. - protocols = null; - - var lines = request.Split(_headerLineSplitter).Where(l => l.Length > 0); - foreach (var line in lines) + // Optionally, a |Sec-WebSocket-Protocol| header field, with a list + // of values indicating which protocols the client would like to + // speak, ordered by preference. + if (protocols != null) { - // GET /chat HTTP/1.1 - if (line.StartsWith(Consts.HttpGetMethodName)) + if (!protocols.Any()) { - var segements = line.Split(' '); - if (segements.Length > 1) - { - headers.Add(Consts.HttpGetMethodName, segements[1]); + throw new WebSocketHandshakeException( + $"Handshake with remote [{session.RemoteEndPoint}] failed due to empty sub-protocol."); + } - if (segements.Length > 2) + foreach (var protocol in protocols) + // The empty string is not the same as the null value for these + // purposes and is not a legal value for this field. + { + if (string.IsNullOrWhiteSpace(protocol)) + { + throw new WebSocketHandshakeException( + $"Handshake with remote [{session.RemoteEndPoint}] failed due to empty sub-protocol."); + } + } + + session.AgreeSubProtocols(string.Join(",", protocols)); + } + + // Optionally, an |Origin| header field. This header field is sent + // by all browser clients. A connection attempt lacking this + // header field SHOULD NOT be interpreted as coming from a browser client. + // + // Servers that are not intended to process input from any web page but + // only for certain sites SHOULD verify the |Origin| field is an origin + // they expect. If the origin indicated is unacceptable to the server, + // then it SHOULD respond to the WebSocket handshake with a reply + // containing HTTP 403 Forbidden status code. + // + // The |Origin| header field protects from the attack cases when the + // untrusted party is typically the author of a JavaScript application + // that is executing in the context of the trusted client. The client + // itself can contact the server and, via the mechanism of the |Origin| + // header field, determine whether to extend those communication + // privileges to the JavaScript application. The intent is not to + // prevent non-browsers from establishing connections but rather to + // ensure that trusted browsers under the control of potentially + // malicious JavaScript cannot fake a WebSocket handshake. + + // Optionally, other header fields, such as those used to send + // cookies or request authentication to a server. Unknown header + // fields are ignored, as per [RFC2616]. + } + catch (Exception ex) + { + NetworkHelper.Logger.Error( + $"{session}{Environment.NewLine}{request}{Environment.NewLine}{ex.FormatExceptionToMessage()}"); + throw; + } + + return true; + } + + internal static byte[] CreateOpenningHandshakeResponse(AsyncWebSocketSession session, string secWebSocketKey) + { + var sb = new StringBuilder(); + + // A Status-Line with a 101 response code as per RFC 2616 + // [RFC2616]. Such a response could look like "HTTP/1.1 101 Switching Protocols". + sb.AppendFormatWithCrCf("HTTP/{0} {1} {2}", + Consts.HttpVersion, + (int)HttpStatusCode.SwitchingProtocols, + @"Switching Protocols"); + + // An |Upgrade| header field with value "websocket" as per RFC2616 [RFC2616]. + sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.Upgrade, + Consts.WebSocketUpgradeToken); + + // A |Connection| header field with value "Upgrade". + sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.Connection, + Consts.WebSocketConnectionToken); + + // A |Sec-WebSocket-Accept| header field. The value of this + // header field is constructed by concatenating /key/, defined + // above in step 4 in Section 4.2.2, with the string "258EAFA5- + // E914-47DA-95CA-C5AB0DC85B11", taking the SHA-1 hash of this + // concatenated value to obtain a 20-byte value and base64- + // encoding (see Section 4 of [RFC4648]) this 20-byte hash. + var secWebSocketAccept = GetSecWebSocketAcceptString(secWebSocketKey); + sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.SecWebSocketAccept, + secWebSocketAccept); + + // Optionally, a |Sec-WebSocket-Extensions| header field, with a + // value /extensions/ as defined in step 4 in Section 4.2.2. If + // multiple extensions are to be used, they can all be listed in + // a single |Sec-WebSocket-Extensions| header field or split + // between multiple instances of the |Sec-WebSocket-Extensions| header field. + // A server accepts one or more extensions by including a + // |Sec-WebSocket-Extensions| header field containing one or more + // extensions that were requested by the client. The interpretation of + // any extension parameters, and what constitutes a valid response by a + // server to a requested set of parameters by a client, will be defined + // by each such extension. + if (session.NegotiatedExtensions != null && session.NegotiatedExtensions.Any()) + { + foreach (var extension in session.NegotiatedExtensions.Values) + { + var offer = extension.GetAgreedOffer(); + sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.SecWebSocketExtensions, + offer); + } + } + + /** + // Optionally, a |Sec-WebSocket-Protocol| header field, with a + // value /subprotocol/ as defined in step 4 in Section 4.2.2. + // + // The client can request that the server use a specific subprotocol by + // including the |Sec-WebSocket-Protocol| field in its handshake. If it + // is specified, the server needs to include the same field and one of + // the selected subprotocol values in its response for the connection to + // be established. + // + // These subprotocol names should be registered as per Section 11.5. To + // avoid potential collisions, it is recommended to use names that + // contain the ASCII version of the domain name of the subprotocol's + // originator. For example, if Example Corporation were to create a + // Chat subprotocol to be implemented by many servers around the Web, + // they could name it "chat.example.com". If the Example Organization + // called their competing subprotocol "chat.example.org", then the two + // subprotocols could be implemented by servers simultaneously, with the + // server dynamically selecting which subprotocol to use based on the + // value sent by the client. + // + // Subprotocols can be versioned in backward-incompatible ways by + // changing the subprotocol name, e.g., going from + // "bookings.example.net" to "v2.bookings.example.net". These + // subprotocols would be considered completely separate by WebSocket + // clients. Backward-compatible versioning can be implemented by + // reusing the same subprotocol string but carefully designing the + // actual subprotocol to support this kind of extensibility. + */ + + sb.AppendWithCrCf(); + + // HTTP/1.1 101 Switching Protocols + // Upgrade: websocket + // Connection: Upgrade + // Sec-WebSocket-Accept: 1tGBmA9p0DQDgmFll6P0/UcVS/E= + // Sec-WebSocket-Protocol: chat + var response = sb.ToString(); +#if DEBUG + NetworkHelper.Logger.Debug($"[{session.RemoteEndPoint}]{Environment.NewLine}{response}"); +#endif + return Encoding.UTF8.GetBytes(response); + } + + internal static byte[] CreateOpenningHandshakeBadRequestResponse(AsyncWebSocketSession session) + { + var sb = new StringBuilder(); + + // HTTP/1.1 400 Bad Request + sb.AppendFormatWithCrCf("HTTP/{0} {1} {2}", + Consts.HttpVersion, + (int)HttpStatusCode.BadRequest, + @"Bad Request"); + + // Upgrade: websocket + sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.Upgrade, + Consts.WebSocketUpgradeToken); + + // Connection: Upgrade + sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.Connection, + Consts.WebSocketConnectionToken); + + // Sec-WebSocket-Version: 13 + sb.AppendFormatWithCrCf(Consts.HttpHeaderLineFormat, HttpKnownHeaderNames.SecWebSocketVersion, + Consts.WebSocketVersion); + + sb.AppendWithCrCf(); + + var response = sb.ToString(); +#if DEBUG + NetworkHelper.Logger.Debug($"[{session.RemoteEndPoint}]{Environment.NewLine}{response}"); +#endif + return Encoding.UTF8.GetBytes(response); + } + + private static void ParseOpenningHandshakeRequestHeaders(string request, + out Dictionary headers, + out List extensions, + out List protocols) + { + headers = new Dictionary(); + + // The |Sec-WebSocket-Extensions| header field MAY appear multiple times + // in an HTTP request (which is logically the same as a single + // |Sec-WebSocket-Extensions| header field that contains all values. + // However, the |Sec-WebSocket-Extensions| header field MUST NOT appear + // more than once in an HTTP response. + extensions = null; + // The |Sec-WebSocket-Protocol| header field MAY appear multiple times + // in an HTTP request (which is logically the same as a single + // |Sec-WebSocket-Protocol| header field that contains all values). + // However, the |Sec-WebSocket-Protocol| header field MUST NOT appear + // more than once in an HTTP response. + protocols = null; + + var lines = request.Split(_headerLineSplitter).Where(l => l.Length > 0); + foreach (var line in lines) + // GET /chat HTTP/1.1 + { + if (line.StartsWith(Consts.HttpGetMethodName)) + { + var segements = line.Split(' '); + if (segements.Length > 1) + { + headers.Add(Consts.HttpGetMethodName, segements[1]); + + if (segements.Length > 2) + { + var versions = segements[2].Split('/'); + if (versions.Length > 1) { - var versions = segements[2].Split('/'); - if (versions.Length > 1) - { - headers.Add(Consts.HttpVersionName, versions[1]); - } + headers.Add(Consts.HttpVersionName, versions[1]); } } } - else + } + else + { + foreach (var key in HttpKnownHeaderNames.All) { - foreach (var key in HttpKnownHeaderNames.All) + if (line.StartsWith(key + ":")) { - if (line.StartsWith(key + ":")) + var index = line.IndexOf(':'); + if (index != -1) { - var index = line.IndexOf(':'); - if (index != -1) + var value = line.Substring(index + 1); + + if (key == HttpKnownHeaderNames.SecWebSocketExtensions) { - var value = line.Substring(index + 1); - - if (key == HttpKnownHeaderNames.SecWebSocketExtensions) + if (extensions == null) { - if (extensions == null) - { - extensions = new List(); - } - - extensions.Add(value.Trim()); + extensions = new List(); } - else if (key == HttpKnownHeaderNames.SecWebSocketProtocol) - { - if (protocols == null) - { - protocols = new List(); - } - protocols.Add(value.Trim()); + extensions.Add(value.Trim()); + } + else if (key == HttpKnownHeaderNames.SecWebSocketProtocol) + { + if (protocols == null) + { + protocols = new List(); + } + + protocols.Add(value.Trim()); + } + else + { + if (headers.ContainsKey(key)) + { + headers[key] = string.Join(",", headers[key], value.Trim()); } else { - if (headers.ContainsKey(key)) - { - headers[key] = string.Join(",", headers[key], value.Trim()); - } - else - { - headers.Add(key, value.Trim()); - } + headers.Add(key, value.Trim()); } } } @@ -441,19 +454,19 @@ namespace EonaCat.WebSockets } } } - - private static string GetSecWebSocketAcceptString(string secWebSocketKey) - { - string retVal; - - using (SHA1 sha1 = SHA1.Create()) - { - string acceptString = string.Concat(secWebSocketKey, Consts.SecWebSocketKeyGuid); - byte[] toHash = Encoding.UTF8.GetBytes(acceptString); - retVal = Convert.ToBase64String(sha1.ComputeHash(toHash)); - } - - return retVal; - } } -} + + private static string GetSecWebSocketAcceptString(string secWebSocketKey) + { + string retVal; + + using (var sha1 = SHA1.Create()) + { + var acceptString = string.Concat(secWebSocketKey, Consts.SecWebSocketKeyGuid); + var toHash = Encoding.UTF8.GetBytes(acceptString); + retVal = Convert.ToBase64String(sha1.ComputeHash(toHash)); + } + + return retVal; + } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/SubProtocols/IWebSocketSubProtocol.cs b/EonaCat.Network/System/Sockets/WebSockets/SubProtocols/IWebSocketSubProtocol.cs index cb55be8..2e0d658 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/SubProtocols/IWebSocketSubProtocol.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/SubProtocols/IWebSocketSubProtocol.cs @@ -1,9 +1,7 @@ -namespace EonaCat.WebSockets.SubProtocols -{ - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. +namespace EonaCat.WebSockets.SubProtocols; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. - public interface IWebSocketSubProtocol - { - } -} +public interface IWebSocketSubProtocol +{ +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/SubProtocols/IWebSocketSubProtocolNegotiator.cs b/EonaCat.Network/System/Sockets/WebSockets/SubProtocols/IWebSocketSubProtocolNegotiator.cs index 454e467..147886e 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/SubProtocols/IWebSocketSubProtocolNegotiator.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/SubProtocols/IWebSocketSubProtocolNegotiator.cs @@ -1,11 +1,12 @@ -namespace EonaCat.WebSockets.SubProtocols -{ - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. +namespace EonaCat.WebSockets.SubProtocols; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. - public interface IWebSocketSubProtocolNegotiator - { - bool NegotiateAsClient(string protocolName, string protocolVersion, string protocolParameter, out string invalidParameter, out IWebSocketSubProtocol negotiatedSubProtocol); - bool NegotiateAsServer(string protocolName, string protocolVersion, string protocolParameter, out string invalidParameter, out IWebSocketSubProtocol negotiatedSubProtocol); - } -} +public interface IWebSocketSubProtocolNegotiator +{ + bool NegotiateAsClient(string protocolName, string protocolVersion, string protocolParameter, + out string invalidParameter, out IWebSocketSubProtocol negotiatedSubProtocol); + + bool NegotiateAsServer(string protocolName, string protocolVersion, string protocolParameter, + out string invalidParameter, out IWebSocketSubProtocol negotiatedSubProtocol); +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/SubProtocols/WebSocketSubProtocolRequestDescription.cs b/EonaCat.Network/System/Sockets/WebSockets/SubProtocols/WebSocketSubProtocolRequestDescription.cs index 9ca0279..bde4786 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/SubProtocols/WebSocketSubProtocolRequestDescription.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/SubProtocols/WebSocketSubProtocolRequestDescription.cs @@ -1,22 +1,20 @@ using System; -namespace EonaCat.WebSockets.SubProtocols +namespace EonaCat.WebSockets.SubProtocols; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public sealed class WebSocketSubProtocolRequestDescription { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - - public sealed class WebSocketSubProtocolRequestDescription + public WebSocketSubProtocolRequestDescription(string requestedSubProtocol) { - public WebSocketSubProtocolRequestDescription(string requestedSubProtocol) + if (string.IsNullOrWhiteSpace(requestedSubProtocol)) { - if (string.IsNullOrWhiteSpace(requestedSubProtocol)) - { - throw new ArgumentNullException("requestedSubProtocol"); - } - - this.RequestedSubProtocol = requestedSubProtocol; + throw new ArgumentNullException("requestedSubProtocol"); } - public string RequestedSubProtocol { get; private set; } + RequestedSubProtocol = requestedSubProtocol; } -} + + public string RequestedSubProtocol { get; private set; } +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/WebSocketCloseCode.cs b/EonaCat.Network/System/Sockets/WebSockets/WebSocketCloseCode.cs index f886386..fccfda7 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/WebSocketCloseCode.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/WebSocketCloseCode.cs @@ -1,29 +1,27 @@ -namespace EonaCat.WebSockets -{ - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. - // 0 - 999 Status codes in the range 0-999 are not used. - // 1000 - 1999 Status codes in the range 1000-1999 are reserved for definition by this protocol. - // 2000 - 2999 Status codes in the range 2000-2999 are reserved for use by extensions. - // 3000 - 3999 Status codes in the range 3000-3999 MAY be used by libraries and frameworks. The - // interpretation of these codes is undefined by this protocol. End applications MUST - // NOT use status codes in this range. - // 4000 - 4999 Status codes in the range 4000-4999 MAY be used by application code. The interpretation - // of these codes is undefined by this protocol. - public enum WebSocketCloseCode - { - NormalClosure = 1000, - EndpointUnavailable = 1001, - ProtocolError = 1002, - InvalidMessageType = 1003, - Empty = 1005, - AbnormalClosure = 1006, // 1006 is reserved and should never be used by user - InvalidPayloadData = 1007, - PolicyViolation = 1008, - MessageTooBig = 1009, - MandatoryExtension = 1010, - InternalServerError = 1011, - TlsHandshakeFailed = 1015, // 1015 is reserved and should never be used by user - } -} +// 0 - 999 Status codes in the range 0-999 are not used. +// 1000 - 1999 Status codes in the range 1000-1999 are reserved for definition by this protocol. +// 2000 - 2999 Status codes in the range 2000-2999 are reserved for use by extensions. +// 3000 - 3999 Status codes in the range 3000-3999 MAY be used by libraries and frameworks. The +// interpretation of these codes is undefined by this protocol. End applications MUST +// NOT use status codes in this range. +// 4000 - 4999 Status codes in the range 4000-4999 MAY be used by application code. The interpretation +// of these codes is undefined by this protocol. +public enum WebSocketCloseCode +{ + NormalClosure = 1000, + EndpointUnavailable = 1001, + ProtocolError = 1002, + InvalidMessageType = 1003, + Empty = 1005, + AbnormalClosure = 1006, // 1006 is reserved and should never be used by user + InvalidPayloadData = 1007, + PolicyViolation = 1008, + MessageTooBig = 1009, + MandatoryExtension = 1010, + InternalServerError = 1011, + TlsHandshakeFailed = 1015 // 1015 is reserved and should never be used by user +} \ No newline at end of file diff --git a/EonaCat.Network/System/Sockets/WebSockets/WebSocketState.cs b/EonaCat.Network/System/Sockets/WebSockets/WebSocketState.cs index e8591b0..220603e 100644 --- a/EonaCat.Network/System/Sockets/WebSockets/WebSocketState.cs +++ b/EonaCat.Network/System/Sockets/WebSockets/WebSocketState.cs @@ -1,14 +1,12 @@ -namespace EonaCat.WebSockets -{ - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. +namespace EonaCat.WebSockets; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. - public enum WebSocketState - { - None = 0, - Connecting = 1, - Open = 2, - Closing = 3, - Closed = 5, - } -} +public enum WebSocketState +{ + None = 0, + Connecting = 1, + Open = 2, + Closing = 3, + Closed = 5 +} \ No newline at end of file diff --git a/EonaCat.Network/System/Tools/CertificateInfoHelper.cs b/EonaCat.Network/System/Tools/CertificateInfoHelper.cs index a432a38..a396c28 100644 --- a/EonaCat.Network/System/Tools/CertificateInfoHelper.cs +++ b/EonaCat.Network/System/Tools/CertificateInfoHelper.cs @@ -2,68 +2,67 @@ using System.Security.Cryptography.X509Certificates; using System.Text; -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public class CertificateInfoHelper { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - - public class CertificateInfoHelper + public static string GetCertificatesInformation(IEnumerable certificates) { - public static string GetCertificatesInformation(IEnumerable certificates) - { - StringBuilder stringBuilder = new StringBuilder(); + var stringBuilder = new StringBuilder(); - foreach (var certificate in certificates) + foreach (var certificate in certificates) + { + if (certificate != null) { - if (certificate != null) - { - stringBuilder.AppendLine(GetCertificateInformation(certificate)); - } + stringBuilder.AppendLine(GetCertificateInformation(certificate)); } - return stringBuilder.ToString(); } - public static string GetCertificateInformation(X509Certificate2 certificate) + return stringBuilder.ToString(); + } + + public static string GetCertificateInformation(X509Certificate2 certificate) + { + var stringBuilder = new StringBuilder(); + + stringBuilder.AppendLine("Certificate Information:"); + stringBuilder.AppendLine($"Subject: {certificate.Subject}"); + stringBuilder.AppendLine($"Issuer: {certificate.Issuer}"); + stringBuilder.AppendLine($"Serial Number: {certificate.SerialNumber}"); + stringBuilder.AppendLine($"Thumbprint: {certificate.Thumbprint}"); + stringBuilder.AppendLine($"Valid From: {certificate.NotBefore}"); + stringBuilder.AppendLine($"Valid Until: {certificate.NotAfter}"); + stringBuilder.AppendLine($"Has Private Key: {certificate.HasPrivateKey}"); + stringBuilder.AppendLine(); + + stringBuilder.AppendLine("Public Key Information:"); + stringBuilder.AppendLine($"Algorithm: {certificate.PublicKey.Key.KeyExchangeAlgorithm}"); + stringBuilder.AppendLine($"Key Size: {certificate.PublicKey.Key.KeySize}"); + stringBuilder.AppendLine(); + + stringBuilder.AppendLine("Certificate Extensions:"); + foreach (var extension in certificate.Extensions) { - StringBuilder stringBuilder = new StringBuilder(); - - stringBuilder.AppendLine("Certificate Information:"); - stringBuilder.AppendLine($"Subject: {certificate.Subject}"); - stringBuilder.AppendLine($"Issuer: {certificate.Issuer}"); - stringBuilder.AppendLine($"Serial Number: {certificate.SerialNumber}"); - stringBuilder.AppendLine($"Thumbprint: {certificate.Thumbprint}"); - stringBuilder.AppendLine($"Valid From: {certificate.NotBefore}"); - stringBuilder.AppendLine($"Valid Until: {certificate.NotAfter}"); - stringBuilder.AppendLine($"Has Private Key: {certificate.HasPrivateKey}"); - stringBuilder.AppendLine(); - - stringBuilder.AppendLine("Public Key Information:"); - stringBuilder.AppendLine($"Algorithm: {certificate.PublicKey.Key.KeyExchangeAlgorithm}"); - stringBuilder.AppendLine($"Key Size: {certificate.PublicKey.Key.KeySize}"); - stringBuilder.AppendLine(); - - stringBuilder.AppendLine("Certificate Extensions:"); - foreach (X509Extension extension in certificate.Extensions) - { - stringBuilder.AppendLine($" {extension.Oid.FriendlyName}: {extension.Format(true)}"); - } - - return stringBuilder.ToString(); + stringBuilder.AppendLine($" {extension.Oid.FriendlyName}: {extension.Format(true)}"); } - public static string GetSubject(X509Certificate2 certificate) - { - return certificate.Subject; - } + return stringBuilder.ToString(); + } - public static string GetIssuer(X509Certificate2 certificate) - { - return certificate.Issuer; - } + public static string GetSubject(X509Certificate2 certificate) + { + return certificate.Subject; + } - public static string GetValidityPeriod(X509Certificate2 certificate) - { - return $"Valid From: {certificate.NotBefore}, Valid Until: {certificate.NotAfter}"; - } + public static string GetIssuer(X509Certificate2 certificate) + { + return certificate.Issuer; + } + + public static string GetValidityPeriod(X509Certificate2 certificate) + { + return $"Valid From: {certificate.NotBefore}, Valid Until: {certificate.NotAfter}"; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Tools/FileReader.cs b/EonaCat.Network/System/Tools/FileReader.cs index a9cdfb6..5d2a6f0 100644 --- a/EonaCat.Network/System/Tools/FileReader.cs +++ b/EonaCat.Network/System/Tools/FileReader.cs @@ -2,82 +2,80 @@ using System.IO; using System.Text; -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public class FileReader { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private readonly StreamReader streamReader; - public class FileReader + public FileReader(string filePath) : this(filePath, NetworkHelper.GlobalEncoding) { - /// - /// Reads a configuration file (varName=varValue format #beginning with comments) - /// - /// Parameter list. - /// File path. - public static Dictionary ReadFile(string filePath) + } + + public FileReader(string filePath, Encoding codingType) + { + streamReader = new StreamReader(filePath, codingType); + } + + /// + /// Reads a configuration file (varName=varValue format #beginning with comments) + /// + /// Parameter list. + /// File path. + public static Dictionary ReadFile(string filePath) + { + var handler = new FileReader(filePath); + + var result = handler.Read(); + + handler = null; + + return result; + } + + // return parameters + public Dictionary Read() + { + var result = new Dictionary(); + + string line; + + while ((line = streamReader.ReadLine()) != null) { - FileReader handler = new FileReader(filePath); - - Dictionary result = handler.Read(); - - handler = null; - - return result; - } - - private readonly StreamReader streamReader; - - public FileReader(string filePath) : this(filePath, NetworkHelper.GlobalEncoding) - { - } - - public FileReader(string filePath, Encoding codingType) - { - streamReader = new StreamReader(filePath, codingType); - } - - // return parameters - public Dictionary Read() - { - Dictionary result = new Dictionary(); - - string line; - - while ((line = streamReader.ReadLine()) != null) + if (line.Length > 0) { - if (line.Length > 0) + if (line.Substring(0, 1) == @"#") { - if (line.Substring(0, 1) == @"#") - { - line = ""; + 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; + continue; // annotation line } } + else // empty line + { + continue; + } - streamReader.Close(); + var strPair = line.Split('='); - return result; + line = ""; + + if (strPair.Length == 2) + { + result.Add( + strPair[0].ToUpper().Replace("\"", "").Replace("\'", "").TrimStart().TrimEnd(), + strPair[1].Replace("\"", "").Replace("\'", "").TrimStart().TrimEnd()); + } + else + { + continue; + } } + + streamReader.Close(); + + return result; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Tools/FileWriter.cs b/EonaCat.Network/System/Tools/FileWriter.cs index 494f5cc..d9bb155 100644 --- a/EonaCat.Network/System/Tools/FileWriter.cs +++ b/EonaCat.Network/System/Tools/FileWriter.cs @@ -1,57 +1,55 @@ using System.IO; -namespace EonaCat.Network +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 { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private readonly FileStream fileStream; + private readonly StreamWriter streamWriter; - public class FileWriter + public FileWriter(string filePath) { - /// - /// Write a file - /// - /// File address - /// Write content. - public static void WriteFile(string filePath, string content) + fileStream = new FileStream(filePath, FileMode.OpenOrCreate); + + streamWriter = new StreamWriter(fileStream); + } + + /// + /// Write a file + /// + /// File address + /// Write content. + public static void WriteFile(string filePath, string content) + { + var handler = new FileWriter(filePath); + + handler.Write(content, false); + + handler.Finished(); + + handler = null; + } + + public void Write(string content, bool isLine) + { + if (isLine) { - FileWriter handler = new FileWriter(filePath); - - handler.Write(content, false); - - handler.Finished(); - - handler = null; + streamWriter.WriteLine(content); + } + else + { + streamWriter.Write(content); } - private readonly FileStream fileStream; - private readonly StreamWriter streamWriter; + streamWriter.Flush(); + } - public FileWriter(string filePath) - { - fileStream = new FileStream(filePath, FileMode.OpenOrCreate); + public void Finished() + { + streamWriter.Close(); - 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(); - } + fileStream.Close(); } } \ No newline at end of file diff --git a/EonaCat.Network/System/Tools/Helpers.cs b/EonaCat.Network/System/Tools/Helpers.cs index 145087c..af12e40 100644 --- a/EonaCat.Network/System/Tools/Helpers.cs +++ b/EonaCat.Network/System/Tools/Helpers.cs @@ -6,170 +6,172 @@ using System.Net.Sockets; using System.Text.RegularExpressions; using System.Threading; -namespace EonaCat.Network +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 { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private static List scanThreads; + private static Action ConnectedEvent; - public class Helpers + private Helpers() { - private Helpers() - { } + } - /// - /// Port scan - /// - /// the c prefix of IP such as "192.168.0." - /// Start of segment D such as 1. - /// The end of section D such as 255. - /// Detected port. - /// return of the detection result Pass in the IPAndPort terminal class object and the boolean status of whether it is turned on. - public static void IPv4ScanPort(string IPPrefix, int DStart, int DEnd, int port, Action ConnectEvent) + /// + /// Port scan + /// + /// the c prefix of IP such as "192.168.0." + /// Start of segment D such as 1. + /// The end of section D such as 255. + /// Detected port. + /// + /// return of the detection result Pass in the IPAndPort terminal class object and the boolean + /// status of whether it is turned on. + /// + public static void IPv4ScanPort(string IPPrefix, int DStart, int DEnd, int port, + Action ConnectEvent) + { + // Configure CallBack Event + ConnectedEvent = ConnectEvent; + + // Check + if (!(IPv4Verify(IPPrefix + DStart) && IPv4Verify(IPPrefix + DEnd))) { - // Configure CallBack Event - ConnectedEvent = ConnectEvent; + throw new Exception("EonaCat Network: Wrong Scan Parameters"); + } - // Check - if (!(IPv4Verify(IPPrefix + DStart) && IPv4Verify(IPPrefix + DEnd))) + if (DStart > DEnd) + { + var temp = DEnd; + + DEnd = DStart; + + DStart = temp; + } + + // Init + scanThreads = new List(); + + // Scan + for (var i = DStart; i <= DEnd; i++) + { + var ip = IPPrefix + i; + + scanThreads.Add(new Thread(ScanOne)); + + if (scanThreads.Any()) { - throw new Exception("EonaCat Network: Wrong Scan Parameters"); - } - - if (DStart > DEnd) - { - int temp = DEnd; - - DEnd = DStart; - - DStart = temp; - } - - // Init - scanThreads = new List(); - - // Scan - for (int i = DStart; i <= DEnd; i++) - { - string ip = IPPrefix + i; - - scanThreads.Add(new Thread(new ParameterizedThreadStart(ScanOne))); - - if (scanThreads.Any()) - { - scanThreads[scanThreads.Count - 1].Start(new IPAndPort(ip, port)); - } + scanThreads[scanThreads.Count - 1].Start(new IPAndPort(ip, port)); } + } + } + /// + /// Stop all port scanning threads + /// + public static void StopPortScan() + { + if (scanThreads == null) + { return; } - /// - /// Stop all port scanning threads - /// - public static void StopPortScan() + if (scanThreads.Count == 0) { - if (scanThreads == null) + return; + } + + foreach (var t in scanThreads) + { + if (t == null) { - return; + continue; } - if (scanThreads.Count == 0) - { - return; - } + t.Abort(); + } - foreach (Thread t in scanThreads) + scanThreads.Clear(); + } + + private static void ScanOne(object _para) + { + var para = (IPAndPort)_para; + + if (para.port == 0 || para.ip == null || para.ip == string.Empty) + { + NetworkHelper.Logger.Error("Wrong IP or Port"); + return; + } + + var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + + try + { + NetworkHelper.Logger.Debug($"try to connect {para.ip}: ar {para.port}"); + socket.Connect(para.ip, para.port); + } + catch (Exception e) + { + NetworkHelper.Logger.Error(string.Format(" {0}: at {1} is close ,error message :{2}", para.ip, para.port, + e.Message)); + } + finally + { + if (ConnectedEvent != null) { - if (t == null) + if (socket.Connected) { - continue; + ConnectedEvent(para, true); } - - t.Abort(); - } - - scanThreads.Clear(); - } - - private static List scanThreads; - private static Action ConnectedEvent; - - private static void ScanOne(object _para) - { - IPAndPort para = (IPAndPort)_para; - - if (para.port == 0 || para.ip == null || para.ip == string.Empty) - { - NetworkHelper.Logger.Error("Wrong IP or Port"); - return; - } - - Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - - try - { - NetworkHelper.Logger.Debug(string.Format("try to connect {0}: ar {1}", para.ip, para.port)); - socket.Connect(para.ip, para.port); - } - catch (Exception e) - { - NetworkHelper.Logger.Error(string.Format(" {0}: at {1} is close ,error message :{2}", para.ip, para.port, e.Message)); - } - finally - { - if (ConnectedEvent != null) + else { - if (socket.Connected) - { - ConnectedEvent(para, true); - } - else - { - ConnectedEvent(para, false); - } + ConnectedEvent(para, false); } - - socket.Close(); - socket = null; - } - } - - public static bool IPv4Verify(string IP) - { - return Regex.IsMatch(IP, @"^((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)$"); - } - - /// - /// Get the internal network IP - /// - /// The local ip. - public static string[] GetLocalIP() - { - string name = Dns.GetHostName(); - - List result = new List(); - - IPAddress[] iPs = Dns.GetHostAddresses(name); - - foreach (IPAddress ip in iPs) - { - result.Add(ip.ToString()); } - return result.ToArray(); + socket.Close(); + socket = null; } } - public class IPAndPort + public static bool IPv4Verify(string IP) { - public 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?)$"); + } - public int port; + /// + /// Get the internal network IP + /// + /// The local ip. + public static string[] GetLocalIP() + { + var name = Dns.GetHostName(); - public IPAndPort(string _ip, int _port) + var result = new List(); + + var iPs = Dns.GetHostAddresses(name); + + foreach (var ip in iPs) { - this.ip = _ip; - this.port = _port; + result.Add(ip.ToString()); } + + return result.ToArray(); + } +} + +public class IPAndPort +{ + public string ip; + + public int port; + + public IPAndPort(string _ip, int _port) + { + ip = _ip; + port = _port; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Tools/RegEx.cs b/EonaCat.Network/System/Tools/RegEx.cs index 5625fe7..bcbfc1c 100644 --- a/EonaCat.Network/System/Tools/RegEx.cs +++ b/EonaCat.Network/System/Tools/RegEx.cs @@ -1,106 +1,106 @@ using System.Collections.Generic; using System.Text.RegularExpressions; -namespace EonaCat.Network +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 { - // This file is part of the EonaCat 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 + private RegEx() { - /// - /// Match everything from the source - /// - /// matched data. - /// source text. - /// matches the prefix. - /// match suffix. - public static List FindAll(string sourceData, string regexPrefix, string regexPostFix) + } + + /// + /// Match everything from the source + /// + /// matched data. + /// source text. + /// matches the prefix. + /// match suffix. + public static List FindAll(string sourceData, string regexPrefix, string regexPostFix) + { + return FindAll(sourceData, regexPrefix, regexPostFix, false, true); + } + + /// + /// Match everything from the source. + /// + /// matched data. + /// source text. + /// Regular expression. + /// If set to true Ignore case. + public static List FindAll(string sourceData, string regexPattern, bool ignoreCase) + { + var result = new List(); + + MatchCollection matches; + + if (ignoreCase) { - return FindAll(sourceData, regexPrefix, regexPostFix, false, true); + matches = Regex.Matches(sourceData, regexPattern, RegexOptions.IgnoreCase); + } + else + { + matches = Regex.Matches(sourceData, regexPattern); } - /// - /// Match everything from the source. - /// - /// matched data. - /// source text. - /// Regular expression. - /// If set to true Ignore case. - public static List FindAll(string sourceData, string regexPattern, bool ignoreCase) + foreach (Match matchItem in matches) { - List result = new List(); - - MatchCollection matches; - - if (ignoreCase) - { - matches = Regex.Matches(sourceData, regexPattern, RegexOptions.IgnoreCase); - } - else - { - matches = Regex.Matches(sourceData, regexPattern); - } - - foreach (Match matchItem in matches) - { - result.Add(matchItem.Value); - } - - return result; + result.Add(matchItem.Value); } - /// - /// Match everything from the source - /// - /// matched data. - /// source text. - /// match prefix. - /// match suffix. - /// If set to true only extract numbers. - public static List FindAll(string sourceData, string regexPrefix, string regexPostFix, bool OnlyDigit) + return result; + } + + /// + /// Match everything from the source + /// + /// matched data. + /// source text. + /// match prefix. + /// match suffix. + /// If set to true only extract numbers. + public static List FindAll(string sourceData, string regexPrefix, string regexPostFix, bool OnlyDigit) + { + return FindAll(sourceData, regexPrefix, regexPostFix, OnlyDigit, true); + } + + /// + /// Match everything from the source + /// + /// matched data. + /// source text. + /// match prefix. + /// match suffix. + /// If set to true only extract numbers. + /// If set to true Ignore case. + public static List FindAll(string sourceData, string regexPreFix, string regexPostFix, bool OnlyDigit, + bool ignoreCase) + { + var result = new List(); + + MatchCollection matches; + + if (ignoreCase) { - return FindAll(sourceData, regexPrefix, regexPostFix, OnlyDigit, true); + matches = Regex.Matches(sourceData, + regexPreFix + (OnlyDigit ? @"(\d*?)" : @"(.*?)") + regexPostFix, + RegexOptions.IgnoreCase); + } + else + { + matches = Regex.Matches(sourceData, + regexPreFix + (OnlyDigit ? @"(\d*?)" : @"(.*?)") + regexPostFix); } - /// - /// Match everything from the source - /// - /// matched data. - /// source text. - /// match prefix. - /// match suffix. - /// If set to true only extract numbers. - /// If set to true Ignore case. - public static List FindAll(string sourceData, string regexPreFix, string regexPostFix, bool OnlyDigit, bool ignoreCase) + foreach (Match matchItem in matches) { - List result = new List(); - - MatchCollection matches; - - if (ignoreCase) - { - matches = Regex.Matches(sourceData, - regexPreFix + (OnlyDigit ? @"(\d*?)" : @"(.*?)") + regexPostFix, - RegexOptions.IgnoreCase); - } - else - { - matches = Regex.Matches(sourceData, - regexPreFix + (OnlyDigit ? @"(\d*?)" : @"(.*?)") + regexPostFix); - } - - foreach (Match matchItem in matches) - { - result.Add(matchItem.Value - .Replace(regexPreFix, "") - .Replace(regexPostFix, "")); - } - - return result; + result.Add(matchItem.Value + .Replace(regexPreFix, "") + .Replace(regexPostFix, "")); } - private RegEx() - { } + return result; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/AccessControlManager.cs b/EonaCat.Network/System/Web/AccessControlManager.cs index 312860d..60a3c0b 100644 --- a/EonaCat.Network/System/Web/AccessControlManager.cs +++ b/EonaCat.Network/System/Web/AccessControlManager.cs @@ -1,72 +1,70 @@ -using IpMatcher; -using System; +using System; +using IpMatcher; -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Access control manager. Dictates which connections are permitted or denied. +/// +public class AccessControlManager { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + /// + /// Matcher to match denied addresses. + /// + public Matcher DenyList = new(); /// - /// Access control manager. Dictates which connections are permitted or denied. + /// Access control mode, either DefaultPermit or DefaultDeny. + /// DefaultPermit: allow everything, except for those explicitly denied. + /// DefaultDeny: deny everything, except for those explicitly permitted. /// - public class AccessControlManager + public AccessControlMode Mode = AccessControlMode.DefaultPermit; + + /// + /// Matcher to match permitted addresses. + /// + public Matcher PermitList = new(); + + /// + /// Instantiate the object. + /// + /// Access control mode. + public AccessControlManager(AccessControlMode mode) { - /// - /// Matcher to match denied addresses. - /// - public Matcher DenyList = new Matcher(); + Mode = mode; + } - /// - /// Matcher to match permitted addresses. - /// - public Matcher PermitList = new Matcher(); - - /// - /// Access control mode, either DefaultPermit or DefaultDeny. - /// DefaultPermit: allow everything, except for those explicitly denied. - /// DefaultDeny: deny everything, except for those explicitly permitted. - /// - public AccessControlMode Mode = AccessControlMode.DefaultPermit; - - /// - /// Instantiate the object. - /// - /// Access control mode. - public AccessControlManager(AccessControlMode mode) + /// + /// Permit or deny a request based on IP address. + /// When operating in 'default deny', only specified entries are permitted. + /// When operating in 'default permit', everything is allowed unless explicitly denied. + /// + /// The IP address to evaluate. + /// True if permitted. + public bool Permit(string ip) + { + if (string.IsNullOrEmpty(ip)) { - Mode = mode; + throw new ArgumentNullException(nameof(ip)); } - /// - /// Permit or deny a request based on IP address. - /// When operating in 'default deny', only specified entries are permitted. - /// When operating in 'default permit', everything is allowed unless explicitly denied. - /// - /// The IP address to evaluate. - /// True if permitted. - public bool Permit(string ip) + switch (Mode) { - if (string.IsNullOrEmpty(ip)) - { - throw new ArgumentNullException(nameof(ip)); - } + case AccessControlMode.DefaultDeny: + return PermitList.MatchExists(ip); - switch (Mode) - { - case AccessControlMode.DefaultDeny: - return PermitList.MatchExists(ip); + case AccessControlMode.DefaultPermit: + if (DenyList.MatchExists(ip)) + { + return false; + } - case AccessControlMode.DefaultPermit: - if (DenyList.MatchExists(ip)) - { - return false; - } + return true; - return true; - - default: - throw new ArgumentException("Unknown access control mode: " + Mode.ToString()); - } + default: + throw new ArgumentException("Unknown access control mode: " + Mode); } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/AccessControlMode.cs b/EonaCat.Network/System/Web/AccessControlMode.cs index c706115..90ecb6d 100644 --- a/EonaCat.Network/System/Web/AccessControlMode.cs +++ b/EonaCat.Network/System/Web/AccessControlMode.cs @@ -1,27 +1,24 @@ -using EonaCat.Json.Converters; -using System.Runtime.Serialization; +using System.Runtime.Serialization; +using EonaCat.Json; +using EonaCat.Json.Converters; -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Access control mode of operation. +/// +[Converter(typeof(StringEnumConverter))] +public enum AccessControlMode { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + /// + /// Permit requests from any endpoint by default. + /// + [EnumMember(Value = "DefaultPermit")] DefaultPermit, /// - /// Access control mode of operation. + /// Deny requests from any endpoint by default. /// - [Json.Converter(typeof(StringEnumConverter))] - public enum AccessControlMode - { - /// - /// Permit requests from any endpoint by default. - /// - [EnumMember(Value = "DefaultPermit")] - DefaultPermit, - - /// - /// Deny requests from any endpoint by default. - /// - [EnumMember(Value = "DefaultDeny")] - DefaultDeny - } + [EnumMember(Value = "DefaultDeny")] DefaultDeny } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/Attributes/DynamicRouteAttribute.cs b/EonaCat.Network/System/Web/Attributes/DynamicRouteAttribute.cs index 4f682be..e61b936 100644 --- a/EonaCat.Network/System/Web/Attributes/DynamicRouteAttribute.cs +++ b/EonaCat.Network/System/Web/Attributes/DynamicRouteAttribute.cs @@ -1,58 +1,56 @@ using System; using System.Text.RegularExpressions; -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Attribute that is used to mark methods as a dynamic route. +/// +[AttributeUsage(AttributeTargets.Method)] +public sealed class DynamicRouteAttribute : Attribute { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + /// + /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + /// + public HttpMethod Method = HttpMethod.GET; /// - /// Attribute that is used to mark methods as a dynamic route. + /// The pattern against which the raw URL should be matched. Must be convertible to a regular expression. /// - [AttributeUsage(AttributeTargets.Method)] - public sealed class DynamicRouteAttribute : Attribute + public Regex Path; + + /// + /// Instantiate the object. + /// + /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + /// The regular expression pattern against which the raw URL should be matched. + /// Globally-unique identifier. + /// User-supplied metadata. + public DynamicRouteAttribute(HttpMethod method, string path, string guid = null, object metadata = null) { - /// - /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. - /// - public HttpMethod Method = HttpMethod.GET; + Path = new Regex(path); + Method = method; - /// - /// The pattern against which the raw URL should be matched. Must be convertible to a regular expression. - /// - public Regex Path = null; - - /// - /// Globally-unique identifier. - /// - public string GUID { get; set; } = Guid.NewGuid().ToString(); - - /// - /// User-supplied metadata. - /// - public object Metadata { get; set; } = null; - - /// - /// Instantiate the object. - /// - /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. - /// The regular expression pattern against which the raw URL should be matched. - /// Globally-unique identifier. - /// User-supplied metadata. - public DynamicRouteAttribute(HttpMethod method, string path, string guid = null, object metadata = null) + if (!string.IsNullOrEmpty(guid)) { - Path = new Regex(path); - Method = method; + GUID = guid; + } - if (!string.IsNullOrEmpty(guid)) - { - GUID = guid; - } - - if (metadata != null) - { - Metadata = metadata; - } + if (metadata != null) + { + Metadata = metadata; } } + + /// + /// Globally-unique identifier. + /// + public string GUID { get; set; } = Guid.NewGuid().ToString(); + + /// + /// User-supplied metadata. + /// + public object Metadata { get; set; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/Attributes/ParameterRouteAttribute.cs b/EonaCat.Network/System/Web/Attributes/ParameterRouteAttribute.cs index 57a2ddf..40b54b9 100644 --- a/EonaCat.Network/System/Web/Attributes/ParameterRouteAttribute.cs +++ b/EonaCat.Network/System/Web/Attributes/ParameterRouteAttribute.cs @@ -1,58 +1,56 @@ using System; -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Attribute that is used to mark methods as a parameter route. +/// +[AttributeUsage(AttributeTargets.Method)] +public sealed class ParameterRouteAttribute : Attribute { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + /// + /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + /// + public HttpMethod Method = HttpMethod.GET; /// - /// Attribute that is used to mark methods as a parameter route. + /// The path to match, i.e. /{version}/api/{id}. + /// If a match is found, the Dictionary found in HttpRequest.Url.Parameters will contain keys for 'version' and 'id'. /// - [AttributeUsage(AttributeTargets.Method)] - public sealed class ParameterRouteAttribute : Attribute + public string Path; + + /// + /// Instantiate the object. + /// + /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + /// The path to match, i.e. /{version}/api/{id}. + /// Globally-unique identifier. + /// User-supplied metadata. + public ParameterRouteAttribute(HttpMethod method, string path, string guid = null, object metadata = null) { - /// - /// The path to match, i.e. /{version}/api/{id}. - /// If a match is found, the Dictionary found in HttpRequest.Url.Parameters will contain keys for 'version' and 'id'. - /// - public string Path = null; + Path = path; + Method = method; - /// - /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. - /// - public HttpMethod Method = HttpMethod.GET; - - /// - /// Globally-unique identifier. - /// - public string GUID { get; set; } = Guid.NewGuid().ToString(); - - /// - /// User-supplied metadata. - /// - public object Metadata { get; set; } = null; - - /// - /// Instantiate the object. - /// - /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. - /// The path to match, i.e. /{version}/api/{id}. - /// Globally-unique identifier. - /// User-supplied metadata. - public ParameterRouteAttribute(HttpMethod method, string path, string guid = null, object metadata = null) + if (!string.IsNullOrEmpty(guid)) { - Path = path; - Method = method; + GUID = guid; + } - if (!string.IsNullOrEmpty(guid)) - { - GUID = guid; - } - - if (metadata != null) - { - Metadata = metadata; - } + if (metadata != null) + { + Metadata = metadata; } } + + /// + /// Globally-unique identifier. + /// + public string GUID { get; set; } = Guid.NewGuid().ToString(); + + /// + /// User-supplied metadata. + /// + public object Metadata { get; set; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/Attributes/StaticRouteAttribute.cs b/EonaCat.Network/System/Web/Attributes/StaticRouteAttribute.cs index ee9f57f..e992328 100644 --- a/EonaCat.Network/System/Web/Attributes/StaticRouteAttribute.cs +++ b/EonaCat.Network/System/Web/Attributes/StaticRouteAttribute.cs @@ -1,57 +1,55 @@ using System; -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Attribute that is used to mark methods as a static route. +/// +[AttributeUsage(AttributeTargets.Method)] +public sealed class StaticRouteAttribute : Attribute { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + /// + /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + /// + public HttpMethod Method = HttpMethod.GET; /// - /// Attribute that is used to mark methods as a static route. + /// The raw URL, i.e. /foo/bar/. Be sure this begins and ends with '/'. /// - [AttributeUsage(AttributeTargets.Method)] - public sealed class StaticRouteAttribute : Attribute + public string Path; + + /// + /// Instantiate the object. + /// + /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + /// The raw URL, i.e. /foo/bar/. Be sure this begins and ends with '/'. + /// Globally-unique identifier. + /// User-supplied metadata. + public StaticRouteAttribute(HttpMethod method, string path, string guid = null, object metadata = null) { - /// - /// The raw URL, i.e. /foo/bar/. Be sure this begins and ends with '/'. - /// - public string Path = null; + Path = path; + Method = method; - /// - /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. - /// - public HttpMethod Method = HttpMethod.GET; - - /// - /// Globally-unique identifier. - /// - public string GUID { get; set; } = Guid.NewGuid().ToString(); - - /// - /// User-supplied metadata. - /// - public object Metadata { get; set; } = null; - - /// - /// Instantiate the object. - /// - /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. - /// The raw URL, i.e. /foo/bar/. Be sure this begins and ends with '/'. - /// Globally-unique identifier. - /// User-supplied metadata. - public StaticRouteAttribute(HttpMethod method, string path, string guid = null, object metadata = null) + if (!string.IsNullOrEmpty(guid)) { - Path = path; - Method = method; + GUID = guid; + } - if (!string.IsNullOrEmpty(guid)) - { - GUID = guid; - } - - if (metadata != null) - { - Metadata = metadata; - } + if (metadata != null) + { + Metadata = metadata; } } + + /// + /// Globally-unique identifier. + /// + public string GUID { get; set; } = Guid.NewGuid().ToString(); + + /// + /// User-supplied metadata. + /// + public object Metadata { get; set; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/Chunk.cs b/EonaCat.Network/System/Web/Chunk.cs index 36160ef..a560b38 100644 --- a/EonaCat.Network/System/Web/Chunk.cs +++ b/EonaCat.Network/System/Web/Chunk.cs @@ -1,35 +1,33 @@ -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// A chunk of data, used when reading from a request where the Transfer-Encoding header includes 'chunked'. +/// +public class Chunk { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + /// + /// Data. + /// + public byte[] Data = null; /// - /// A chunk of data, used when reading from a request where the Transfer-Encoding header includes 'chunked'. + /// Indicates whether or not this is the final chunk, i.e. the chunk length received was zero. /// - public class Chunk + public bool IsFinalChunk = false; + + /// + /// Length of the data. + /// + public int Length = 0; + + /// + /// Any additional metadata that appears on the length line after the length hex value and semicolon. + /// + public string Metadata = null; + + internal Chunk() { - /// - /// Length of the data. - /// - public int Length = 0; - - /// - /// Data. - /// - public byte[] Data = null; - - /// - /// Any additional metadata that appears on the length line after the length hex value and semicolon. - /// - public string Metadata = null; - - /// - /// Indicates whether or not this is the final chunk, i.e. the chunk length received was zero. - /// - public bool IsFinalChunk = false; - - internal Chunk() - { - } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/ContentRoute.cs b/EonaCat.Network/System/Web/ContentRoute.cs index 168b3e9..3674413 100644 --- a/EonaCat.Network/System/Web/ContentRoute.cs +++ b/EonaCat.Network/System/Web/ContentRoute.cs @@ -1,66 +1,68 @@ -using EonaCat.Json; -using System; +using System; +using EonaCat.Json; -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Assign a method handler for when requests are received matching the supplied method and path. +/// +public class ContentRoute { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - /// - /// Assign a method handler for when requests are received matching the supplied method and path. + /// Create a new route object. /// - public class ContentRoute + /// The pattern against which the raw URL should be matched. + /// + /// Indicates whether or not the path specifies a directory. If so, any matching URL will be + /// handled by the specified handler. + /// + /// Globally-unique identifier. + /// User-supplied metadata. + public ContentRoute(string path, bool isDirectory, string guid = null, object metadata = null) { - /// - /// Globally-unique identifier. - /// - [JsonProperty(Order = -1)] - public string GUID { get; set; } = Guid.NewGuid().ToString(); - - /// - /// The pattern against which the raw URL should be matched. - /// - [JsonProperty(Order = 0)] - public string Path { get; set; } = null; - - /// - /// Indicates whether or not the path specifies a directory. If so, any matching URL will be handled by the specified handler. - /// - [JsonProperty(Order = 1)] - public bool IsDirectory { get; set; } = false; - - /// - /// User-supplied metadata. - /// - [JsonProperty(Order = 999)] - public object Metadata { get; set; } = null; - - /// - /// Create a new route object. - /// - /// The pattern against which the raw URL should be matched. - /// Indicates whether or not the path specifies a directory. If so, any matching URL will be handled by the specified handler. - /// Globally-unique identifier. - /// User-supplied metadata. - public ContentRoute(string path, bool isDirectory, string guid = null, object metadata = null) + if (string.IsNullOrEmpty(path)) { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } + throw new ArgumentNullException(nameof(path)); + } - Path = path.ToLower(); - IsDirectory = isDirectory; + Path = path.ToLower(); + IsDirectory = isDirectory; - if (!string.IsNullOrEmpty(guid)) - { - GUID = guid; - } + if (!string.IsNullOrEmpty(guid)) + { + GUID = guid; + } - if (metadata != null) - { - Metadata = metadata; - } + if (metadata != null) + { + Metadata = metadata; } } + + /// + /// Globally-unique identifier. + /// + [JsonProperty(Order = -1)] + public string GUID { get; set; } = Guid.NewGuid().ToString(); + + /// + /// The pattern against which the raw URL should be matched. + /// + [JsonProperty(Order = 0)] + public string Path { get; set; } + + /// + /// Indicates whether or not the path specifies a directory. If so, any matching URL will be handled by the specified + /// handler. + /// + [JsonProperty(Order = 1)] + public bool IsDirectory { get; set; } + + /// + /// User-supplied metadata. + /// + [JsonProperty(Order = 999)] + public object Metadata { get; set; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/ContentRouteManager.cs b/EonaCat.Network/System/Web/ContentRouteManager.cs index 4875d94..463eb07 100644 --- a/EonaCat.Network/System/Web/ContentRouteManager.cs +++ b/EonaCat.Network/System/Web/ContentRouteManager.cs @@ -2,259 +2,254 @@ using System.Collections.Generic; using System.IO; -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Content route manager. Content routes are used for GET and HEAD requests to specific files or entire directories. +/// +public class ContentRouteManager { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private readonly object _Lock = new(); + + private readonly List _Routes = new(); + private string _BaseDirectory = AppDomain.CurrentDomain.BaseDirectory; /// - /// Content route manager. Content routes are used for GET and HEAD requests to specific files or entire directories. + /// Instantiate the object. /// - public class ContentRouteManager + public ContentRouteManager() { - /// - /// Base directory for files and directories accessible via content routes. - /// - public string BaseDirectory + } + + /// + /// Base directory for files and directories accessible via content routes. + /// + public string BaseDirectory + { + get => _BaseDirectory; + set { - get + if (string.IsNullOrEmpty(value)) { - return _BaseDirectory; + _BaseDirectory = AppDomain.CurrentDomain.BaseDirectory; } - set + else { - if (string.IsNullOrEmpty(value)) + if (!Directory.Exists(value)) { - _BaseDirectory = AppDomain.CurrentDomain.BaseDirectory; + throw new DirectoryNotFoundException("EonaCat Network: The requested directory '" + value + + "' was not found or not accessible."); } - else - { - if (!Directory.Exists(value)) - { - throw new DirectoryNotFoundException("EonaCat Network: The requested directory '" + value + "' was not found or not accessible."); - } - _BaseDirectory = value; - } + _BaseDirectory = value; } } + } - private readonly List _Routes = new List(); - private readonly object _Lock = new object(); - private string _BaseDirectory = AppDomain.CurrentDomain.BaseDirectory; - - /// - /// Instantiate the object. - /// - public ContentRouteManager() + /// + /// Add a route. + /// + /// URL path, i.e. /path/to/resource. + /// True if the path represents a directory. + /// Globally-unique identifier. + /// User-supplied metadata. + public void Add(string path, bool isDirectory, string guid = null, object metadata = null) + { + if (string.IsNullOrEmpty(path)) { + throw new ArgumentNullException(nameof(path)); } - /// - /// Add a route. - /// - /// URL path, i.e. /path/to/resource. - /// True if the path represents a directory. - /// Globally-unique identifier. - /// User-supplied metadata. - public void Add(string path, bool isDirectory, string guid = null, object metadata = null) - { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } + Add(new ContentRoute(path, isDirectory, guid, metadata)); + } - Add(new ContentRoute(path, isDirectory, guid, metadata)); + /// + /// Remove a route. + /// + /// URL path. + public void Remove(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException(nameof(path)); } - /// - /// Remove a route. - /// - /// URL path. - public void Remove(string path) + var r = Get(path); + if (r == null) { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } - - ContentRoute r = Get(path); - if (r == null) - { - return; - } - - lock (_Lock) - { - _Routes.Remove(r); - } - return; } - /// - /// Retrieve a content route. - /// - /// URL path. - /// ContentRoute if the route exists, otherwise null. - public ContentRoute Get(string path) + lock (_Lock) { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } + _Routes.Remove(r); + } + } - path = path.ToLower(); - if (!path.StartsWith("/")) - { - path = "/" + path; - } + /// + /// Retrieve a content route. + /// + /// URL path. + /// ContentRoute if the route exists, otherwise null. + public ContentRoute Get(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException(nameof(path)); + } - if (!path.EndsWith("/")) - { - path = path + "/"; - } + path = path.ToLower(); + if (!path.StartsWith("/")) + { + path = "/" + path; + } - lock (_Lock) + if (!path.EndsWith("/")) + { + path = path + "/"; + } + + lock (_Lock) + { + foreach (var curr in _Routes) { - foreach (ContentRoute curr in _Routes) + if (curr.IsDirectory) { - if (curr.IsDirectory) + if (path.StartsWith(curr.Path.ToLower())) { - if (path.StartsWith(curr.Path.ToLower())) - { - return curr; - } - } - else - { - if (path.Equals(curr.Path.ToLower())) - { - return curr; - } + return curr; } } + else + { + if (path.Equals(curr.Path.ToLower())) + { + return curr; + } + } + } - return null; + return null; + } + } + + /// + /// Check if a content route exists. + /// + /// URL path. + /// True if exists. + public bool Exists(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException(nameof(path)); + } + + path = path.ToLower(); + if (!path.StartsWith("/")) + { + path = "/" + path; + } + + lock (_Lock) + { + foreach (var curr in _Routes) + { + if (curr.IsDirectory) + { + if (path.StartsWith(curr.Path.ToLower())) + { + return true; + } + } + else + { + if (path.Equals(curr.Path.ToLower())) + { + return true; + } + } } } - /// - /// Check if a content route exists. - /// - /// URL path. - /// True if exists. - public bool Exists(string path) + return false; + } + + /// + /// Retrieve a content route. + /// + /// URL path. + /// Matching route. + /// True if a match exists. + public bool Match(string path, out ContentRoute route) + { + route = null; + if (string.IsNullOrEmpty(path)) { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } + throw new ArgumentNullException(nameof(path)); + } - path = path.ToLower(); - if (!path.StartsWith("/")) - { - path = "/" + path; - } + path = path.ToLower(); + var dirPath = path; + if (!dirPath.EndsWith("/")) + { + dirPath = dirPath + "/"; + } - lock (_Lock) + lock (_Lock) + { + foreach (var curr in _Routes) { - foreach (ContentRoute curr in _Routes) + if (curr.IsDirectory) { - if (curr.IsDirectory) + if (dirPath.StartsWith(curr.Path.ToLower())) { - if (path.StartsWith(curr.Path.ToLower())) - { - return true; - } + route = curr; + return true; } - else + } + else + { + if (path.Equals(curr.Path.ToLower())) { - if (path.Equals(curr.Path.ToLower())) - { - return true; - } + route = curr; + return true; } } } return false; } + } - /// - /// Retrieve a content route. - /// - /// URL path. - /// Matching route. - /// True if a match exists. - public bool Match(string path, out ContentRoute route) + private void Add(ContentRoute route) + { + if (route == null) { - 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; - } + throw new ArgumentNullException(nameof(route)); } - private void Add(ContentRoute route) + route.Path = route.Path.ToLower(); + if (!route.Path.StartsWith("/")) { - if (route == null) - { - throw new ArgumentNullException(nameof(route)); - } + route.Path = "/" + route.Path; + } - route.Path = route.Path.ToLower(); - if (!route.Path.StartsWith("/")) - { - route.Path = "/" + route.Path; - } + if (route.IsDirectory && !route.Path.EndsWith("/")) + { + route.Path = route.Path + "/"; + } - if (route.IsDirectory && !route.Path.EndsWith("/")) - { - route.Path = route.Path + "/"; - } + if (Exists(route.Path)) + { + return; + } - if (Exists(route.Path)) - { - return; - } - - lock (_Lock) - { - _Routes.Add(route); - } + lock (_Lock) + { + _Routes.Add(route); } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/ContentRouteProcessor.cs b/EonaCat.Network/System/Web/ContentRouteProcessor.cs index 80deb6c..359799c 100644 --- a/EonaCat.Network/System/Web/ContentRouteProcessor.cs +++ b/EonaCat.Network/System/Web/ContentRouteProcessor.cs @@ -3,150 +3,146 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Content route handler. Handles GET and HEAD requests to content routes for files and directories. +/// +public class ContentRouteHandler { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private readonly ContentRouteManager _Routes; /// - /// Content route handler. Handles GET and HEAD requests to content routes for files and directories. + /// The FileAccess value to use when accessing files within a content route via a FileStream. Default is + /// FileAccess.Read. /// - public class ContentRouteHandler + public FileAccess ContentFileAccess = FileAccess.Read; + + /// + /// The FileMode value to use when accessing files within a content route via a FileStream. Default is FileMode.Open. + /// + public FileMode ContentFileMode = FileMode.Open; + + /// + /// The FileShare value to use when accessing files within a content route via a FileStream. Default is + /// FileShare.Read. + /// + public FileShare ContentFileShare = FileShare.Read; + + internal ContentRouteHandler(ContentRouteManager routes) { - /// - /// The FileMode value to use when accessing files within a content route via a FileStream. Default is FileMode.Open. - /// - public FileMode ContentFileMode = FileMode.Open; - - /// - /// The FileAccess value to use when accessing files within a content route via a FileStream. Default is FileAccess.Read. - /// - public FileAccess ContentFileAccess = FileAccess.Read; - - /// - /// The FileShare value to use when accessing files within a content route via a FileStream. Default is FileShare.Read. - /// - public FileShare ContentFileShare = FileShare.Read; - - private readonly ContentRouteManager _Routes; - - internal ContentRouteHandler(ContentRouteManager routes) + if (routes == null) { - if (routes == null) - { - throw new ArgumentNullException(nameof(routes)); - } - - _Routes = routes; + throw new ArgumentNullException(nameof(routes)); } - internal async Task Process(HttpContext context, CancellationToken token) + _Routes = routes; + } + + internal async Task Process(HttpContext context, CancellationToken token) + { + if (context == null) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (context.Request == null) - { - throw new ArgumentNullException(nameof(context.Request)); - } - - if (context.Response == null) - { - throw new ArgumentNullException(nameof(context.Response)); - } - - if (context.Request.Method != HttpMethod.GET - && context.Request.Method != HttpMethod.HEAD) - { - Set500Response(context); - await context.Response.Send(token).ConfigureAwait(false); - return; - } - - string filePath = context.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(context); - await context.Response.Send(token).ConfigureAwait(false); - return; - } - - FileInfo fi = new FileInfo(filePath); - long contentLength = fi.Length; - - if (context.Request.Method == HttpMethod.GET) - { - FileStream fs = new FileStream(filePath, ContentFileMode, ContentFileAccess, ContentFileShare); - context.Response.StatusCode = 200; - context.Response.ContentLength = contentLength; - context.Response.ContentType = GetContentType(filePath); - await context.Response.Send(contentLength, fs, token).ConfigureAwait(false); - return; - } - else if (context.Request.Method == HttpMethod.HEAD) - { - context.Response.StatusCode = 200; - context.Response.ContentLength = contentLength; - context.Response.ContentType = GetContentType(filePath); - await context.Response.Send(contentLength, token).ConfigureAwait(false); - return; - } - else - { - Set500Response(context); - await context.Response.Send(token).ConfigureAwait(false); - return; - } + throw new ArgumentNullException(nameof(context)); } - private string GetContentType(string path) + if (context.Request == null) { - if (string.IsNullOrEmpty(path)) - { - return "application/octet-stream"; - } + throw new ArgumentNullException(nameof(context.Request)); + } - int idx = path.LastIndexOf("."); - if (idx >= 0) - { - return MimeTypes.GetFromExtension(path.Substring(idx)); - } + if (context.Response == null) + { + throw new ArgumentNullException(nameof(context.Response)); + } + if (context.Request.Method != HttpMethod.GET + && context.Request.Method != HttpMethod.HEAD) + { + Set500Response(context); + await context.Response.Send(token).ConfigureAwait(false); + return; + } + + var filePath = context.Request.Url.RawWithoutQuery; + if (!string.IsNullOrEmpty(filePath)) + { + while (filePath.StartsWith("/")) + filePath = filePath.Substring(1); + } + + var baseDirectory = _Routes.BaseDirectory; + baseDirectory = baseDirectory.Replace("\\", "/"); + if (!baseDirectory.EndsWith("/")) + { + baseDirectory += "/"; + } + + filePath = baseDirectory + filePath; + filePath = filePath.Replace("+", " ").Replace("%20", " "); + + var contentType = GetContentType(filePath); + + if (!File.Exists(filePath)) + { + Set404Response(context); + await context.Response.Send(token).ConfigureAwait(false); + return; + } + + var fi = new FileInfo(filePath); + var contentLength = fi.Length; + + if (context.Request.Method == HttpMethod.GET) + { + var fs = new FileStream(filePath, ContentFileMode, ContentFileAccess, ContentFileShare); + context.Response.StatusCode = 200; + context.Response.ContentLength = contentLength; + context.Response.ContentType = GetContentType(filePath); + await context.Response.Send(contentLength, fs, token).ConfigureAwait(false); + return; + } + + if (context.Request.Method == HttpMethod.HEAD) + { + context.Response.StatusCode = 200; + context.Response.ContentLength = contentLength; + context.Response.ContentType = GetContentType(filePath); + await context.Response.Send(contentLength, token).ConfigureAwait(false); + return; + } + + Set500Response(context); + await context.Response.Send(token).ConfigureAwait(false); + } + + private string GetContentType(string path) + { + if (string.IsNullOrEmpty(path)) + { return "application/octet-stream"; } - private void Set404Response(HttpContext context) + var idx = path.LastIndexOf("."); + if (idx >= 0) { - context.Response.StatusCode = 404; - context.Response.ContentLength = 0; + return MimeTypes.GetFromExtension(path.Substring(idx)); } - private void Set500Response(HttpContext context) - { - context.Response.StatusCode = 500; - context.Response.ContentLength = 0; - } + return "application/octet-stream"; + } + + private void Set404Response(HttpContext context) + { + context.Response.StatusCode = 404; + context.Response.ContentLength = 0; + } + + private void Set500Response(HttpContext context) + { + context.Response.StatusCode = 500; + context.Response.ContentLength = 0; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/DynamicRoute.cs b/EonaCat.Network/System/Web/DynamicRoute.cs index ee82a2c..07a7f80 100644 --- a/EonaCat.Network/System/Web/DynamicRoute.cs +++ b/EonaCat.Network/System/Web/DynamicRoute.cs @@ -1,81 +1,80 @@ -using EonaCat.Json; -using System; +using System; using System.Text.RegularExpressions; using System.Threading.Tasks; +using EonaCat.Json; -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Assign a method handler for when requests are received matching the supplied method and path regex. +/// +public class DynamicRoute { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - /// - /// Assign a method handler for when requests are received matching the supplied method and path regex. + /// Create a new route object. /// - public class DynamicRoute + /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + /// The pattern against which the raw URL should be matched. + /// The method that should be called to handle the request. + /// Globally-unique identifier. + /// User-supplied metadata. + public DynamicRoute(HttpMethod method, Regex path, Func handler, string guid = null, + object metadata = null) { - /// - /// Globally-unique identifier. - /// - [JsonProperty(Order = -1)] - public string GUID { get; set; } = Guid.NewGuid().ToString(); - - /// - /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. - /// - [JsonProperty(Order = 0)] - public HttpMethod Method { get; set; } = HttpMethod.GET; - - /// - /// The pattern against which the raw URL should be matched. - /// - [JsonProperty(Order = 1)] - public Regex Path { get; set; } = null; - - /// - /// The handler for the dynamic route. - /// - [JsonIgnore] - public Func Handler { get; set; } = null; - - /// - /// User-supplied metadata. - /// - [JsonProperty(Order = 999)] - public object Metadata { get; set; } = null; - - /// - /// Create a new route object. - /// - /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. - /// The pattern against which the raw URL should be matched. - /// The method that should be called to handle the request. - /// Globally-unique identifier. - /// User-supplied metadata. - public DynamicRoute(HttpMethod method, Regex path, Func handler, string guid = null, object metadata = null) + if (path == null) { - if (path == null) - { - throw new ArgumentNullException(nameof(path)); - } + throw new ArgumentNullException(nameof(path)); + } - if (handler == null) - { - throw new ArgumentNullException(nameof(handler)); - } + if (handler == null) + { + throw new ArgumentNullException(nameof(handler)); + } - Method = method; - Path = path; - Handler = handler; + Method = method; + Path = path; + Handler = handler; - if (!string.IsNullOrEmpty(guid)) - { - GUID = guid; - } + if (!string.IsNullOrEmpty(guid)) + { + GUID = guid; + } - if (metadata != null) - { - Metadata = metadata; - } + if (metadata != null) + { + Metadata = metadata; } } + + /// + /// Globally-unique identifier. + /// + [JsonProperty(Order = -1)] + public string GUID { get; set; } = Guid.NewGuid().ToString(); + + /// + /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + /// + [JsonProperty(Order = 0)] + public HttpMethod Method { get; set; } = HttpMethod.GET; + + /// + /// The pattern against which the raw URL should be matched. + /// + [JsonProperty(Order = 1)] + public Regex Path { get; set; } + + /// + /// The handler for the dynamic route. + /// + [JsonIgnore] + public Func Handler { get; set; } + + /// + /// User-supplied metadata. + /// + [JsonProperty(Order = 999)] + public object Metadata { get; set; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/DynamicRouteManager.cs b/EonaCat.Network/System/Web/DynamicRouteManager.cs index e338fef..a18ae24 100644 --- a/EonaCat.Network/System/Web/DynamicRouteManager.cs +++ b/EonaCat.Network/System/Web/DynamicRouteManager.cs @@ -1,165 +1,163 @@ -using RegexMatcher; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; +using RegexMatcher; -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Dynamic route manager. Dynamic routes are used for requests using any HTTP method to any path that can be matched +/// by regular expression. +/// +public class DynamicRouteManager { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private readonly object _Lock = new(); + private readonly Dictionary> _Routes = new(); /// - /// Dynamic route manager. Dynamic routes are used for requests using any HTTP method to any path that can be matched by regular expression. + /// Instantiate the object. /// - public class DynamicRouteManager + public DynamicRouteManager() { - /// - /// Directly access the underlying regular expression matching library. - /// This is helpful in case you want to specify the matching behavior should multiple matches exist. - /// - public Matcher Matcher { get; } = new Matcher(); + Matcher.MatchPreference = MatchPreferenceType.LongestFirst; + } - private readonly object _Lock = new object(); - private readonly Dictionary> _Routes = new Dictionary>(); + /// + /// Directly access the underlying regular expression matching library. + /// This is helpful in case you want to specify the matching behavior should multiple matches exist. + /// + public Matcher Matcher { get; } = new(); - /// - /// Instantiate the object. - /// - public DynamicRouteManager() + /// + /// Add a route. + /// + /// The HTTP method. + /// URL path, i.e. /path/to/resource. + /// Method to invoke. + /// Globally-unique identifier. + /// User-supplied metadata. + public void Add(HttpMethod method, Regex path, Func handler, string guid = null, + object metadata = null) + { + if (path == null) { - Matcher.MatchPreference = MatchPreferenceType.LongestFirst; + throw new ArgumentNullException(nameof(path)); } - /// - /// Add a route. - /// - /// The HTTP method. - /// URL path, i.e. /path/to/resource. - /// Method to invoke. - /// Globally-unique identifier. - /// User-supplied metadata. - public void Add(HttpMethod method, Regex path, Func handler, string guid = null, object metadata = null) + if (handler == 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); - } + throw new ArgumentNullException(nameof(handler)); } - /// - /// Remove a route. - /// - /// The HTTP method. - /// URL path. - public void Remove(HttpMethod method, Regex path) + lock (_Lock) { - if (path == null) - { - throw new ArgumentNullException(nameof(path)); - } + var dr = new DynamicRoute(method, path, handler); - lock (_Lock) - { - Matcher.Remove( - new Regex(BuildConsolidatedRegex(method, path))); + Matcher.Add( + new Regex(BuildConsolidatedRegex(method, path)), + dr); - if (_Routes.Any(r => r.Key.Method == method && r.Key.Path.Equals(path))) - { - List removeList = _Routes.Where(r => r.Key.Method == method && r.Key.Path.Equals(path)) - .Select(r => r.Key) - .ToList(); - - foreach (DynamicRoute remove in removeList) - { - _Routes.Remove(remove); - } - } - } - } - - /// - /// Check if a content route exists. - /// - /// The HTTP method. - /// URL path. - /// True if exists. - public bool Exists(HttpMethod method, Regex path) - { - if (path == null) - { - throw new ArgumentNullException(nameof(path)); - } - - lock (_Lock) - { - return _Routes.Any(r => r.Key.Method == method && r.Key.Path.Equals(path)); - } - } - - /// - /// Match a request method and URL to a handler method. - /// - /// The HTTP method. - /// URL path. - /// Matching route. - /// Method to invoke. - public Func Match(HttpMethod method, string rawUrl, out DynamicRoute dr) - { - dr = null; - if (string.IsNullOrEmpty(rawUrl)) - { - throw new ArgumentNullException(nameof(rawUrl)); - } - - if (Matcher.Match( - BuildConsolidatedRegex(method, rawUrl), - out object 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; + _Routes.Add(new DynamicRoute(method, path, handler, guid, metadata), handler); } } + + /// + /// Remove a route. + /// + /// The HTTP method. + /// URL path. + public void Remove(HttpMethod method, Regex path) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + lock (_Lock) + { + Matcher.Remove( + new Regex(BuildConsolidatedRegex(method, path))); + + if (_Routes.Any(r => r.Key.Method == method && r.Key.Path.Equals(path))) + { + var removeList = _Routes.Where(r => r.Key.Method == method && r.Key.Path.Equals(path)) + .Select(r => r.Key) + .ToList(); + + foreach (var remove in removeList) + { + _Routes.Remove(remove); + } + } + } + } + + /// + /// Check if a content route exists. + /// + /// The HTTP method. + /// URL path. + /// True if exists. + public bool Exists(HttpMethod method, Regex path) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + lock (_Lock) + { + return _Routes.Any(r => r.Key.Method == method && r.Key.Path.Equals(path)); + } + } + + /// + /// Match a request method and URL to a handler method. + /// + /// The HTTP method. + /// URL path. + /// Matching route. + /// Method to invoke. + public Func Match(HttpMethod method, string rawUrl, out DynamicRoute dr) + { + dr = null; + if (string.IsNullOrEmpty(rawUrl)) + { + throw new ArgumentNullException(nameof(rawUrl)); + } + + if (Matcher.Match( + BuildConsolidatedRegex(method, rawUrl), + out var val)) + { + if (val == null) + { + return null; + } + + lock (_Lock) + { + dr = (DynamicRoute)val; + return dr.Handler; + } + } + + return null; + } + + private string BuildConsolidatedRegex(HttpMethod method, string rawUrl) + { + rawUrl = rawUrl.Replace("^", ""); + return method + " " + rawUrl; + } + + private string BuildConsolidatedRegex(HttpMethod method, Regex path) + { + var pathString = path.ToString().Replace("^", ""); + return method + " " + pathString; + } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/EonaCatWebserver.cs b/EonaCat.Network/System/Web/EonaCatWebserver.cs index 3bd519d..e392c24 100644 --- a/EonaCat.Network/System/Web/EonaCatWebserver.cs +++ b/EonaCat.Network/System/Web/EonaCatWebserver.cs @@ -6,597 +6,615 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// EonaCat Webserver. +/// +public class WebServer : IDisposable { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private readonly Assembly _Assembly = Assembly.GetCallingAssembly(); + + private readonly string _Header = "[EonaCat] "; + private Task _AcceptConnections; + private HttpListener _HttpListener = new(); + private int _RequestCount; + private EonaCatWebserverRoutes _Routes = new(); + private EonaCatWebserverSettings _Settings = new(); + private CancellationToken _Token; + + private CancellationTokenSource _TokenSource = new(); /// - /// EonaCat Webserver. + /// Creates a new instance of the EonaCat webserver. + /// If you do not provide a settings object, default settings will be used, which will cause EonaCat Webserver to + /// listen on http://127.0.0.1:8000, and send events to the console. /// - public class WebServer : IDisposable + /// EonaCat webserver settings. + /// + /// Method used when a request is received and no matching routes are found. Commonly used as + /// the 404 handler when routes are used. + /// + public WebServer(EonaCatWebserverSettings settings = null, Func defaultRoute = null) { - /// - /// Indicates whether or not the server is listening. - /// - public bool IsListening => (_HttpListener != null) && _HttpListener.IsListening; - - /// - /// Number of requests being serviced currently. - /// - public int RequestCount => _RequestCount; - - /// - /// EonaCat webserver settings. - /// - public EonaCatWebserverSettings Settings + if (settings == null) { - get - { - return _Settings; - } - set - { - if (value == null) - { - _Settings = new EonaCatWebserverSettings(); - } - else - { - _Settings = value; - } - } + settings = new EonaCatWebserverSettings(); + settings.Prefixes.Add("http://127.0.0.1:8000/"); + Events.Logger = Console.WriteLine; } - /// - /// EonaCat webserver routes. - /// - public EonaCatWebserverRoutes Routes + _Settings = settings; + _Routes.Default = defaultRoute; + } + + /// + /// Creates a new instance of the EonaCat webserver. + /// + /// Hostname or IP address on which to listen. + /// TCP port on which to listen. + /// Specify whether or not SSL should be used (HTTPS). + /// + /// Method used when a request is received and no matching routes are found. Commonly used as + /// the 404 handler when routes are used. + /// + public WebServer(string hostname, int port, bool ssl = false, Func defaultRoute = null) + { + if (string.IsNullOrEmpty(hostname)) { - get - { - return _Routes; - } - set - { - if (value == null) - { - _Routes = new EonaCatWebserverRoutes(); - } - else - { - _Routes = value; - } - } + hostname = "localhost"; } - /// - /// EonaCat webserver statistics. - /// - public EonaCatWebserverStatistics Statistics { get; private set; } = new EonaCatWebserverStatistics(); - - /// - /// Set specific actions/callbacks to use when events are raised. - /// - public EonaCatWebserverEvents Events { get; private set; } = new EonaCatWebserverEvents(); - - /// - /// Default pages served by the EonaCat webserver. - /// - public EonaCatWebserverPages Pages { get; private set; } = new EonaCatWebserverPages(); - - private readonly string _Header = "[EonaCat] "; - private readonly Assembly _Assembly = Assembly.GetCallingAssembly(); - private EonaCatWebserverSettings _Settings = new EonaCatWebserverSettings(); - private EonaCatWebserverRoutes _Routes = new EonaCatWebserverRoutes(); - private System.Net.HttpListener _HttpListener = new System.Net.HttpListener(); - private int _RequestCount = 0; - - private CancellationTokenSource _TokenSource = new CancellationTokenSource(); - private CancellationToken _Token; - private Task _AcceptConnections = null; - - /// - /// Creates a new instance of the EonaCat webserver. - /// If you do not provide a settings object, default settings will be used, which will cause EonaCat Webserver to listen on http://127.0.0.1:8000, and send events to the console. - /// - /// EonaCat webserver settings. - /// Method used when a request is received and no matching routes are found. Commonly used as the 404 handler when routes are used. - public WebServer(EonaCatWebserverSettings settings = null, Func defaultRoute = null) + if (port < 1) { - if (settings == null) - { - settings = new EonaCatWebserverSettings(); - settings.Prefixes.Add("http://127.0.0.1:8000/"); - Events.Logger = Console.WriteLine; - } - - _Settings = settings; - _Routes.Default = defaultRoute; + throw new ArgumentOutOfRangeException(nameof(port)); } - /// - /// Creates a new instance of the EonaCat webserver. - /// - /// Hostname or IP address on which to listen. - /// TCP port on which to listen. - /// Specify whether or not SSL should be used (HTTPS). - /// Method used when a request is received and no matching routes are found. Commonly used as the 404 handler when routes are used. - public WebServer(string hostname, int port, bool ssl = false, Func defaultRoute = null) + _Settings = new EonaCatWebserverSettings(hostname, port, ssl); + _Routes.Default = defaultRoute; + } + + /// + /// Creates a new instance of the EonaCat webserver. + /// + /// + /// Hostnames or IP addresses on which to listen. Note: multiple listener endpoints are not + /// supported on all platforms. + /// + /// TCP port on which to listen. + /// Specify whether or not SSL should be used (HTTPS). + /// + /// Method used when a request is received and no matching routes are found. Commonly used as + /// the 404 handler when routes are used. + /// + public WebServer(List hostnames, int port, bool ssl = false, Func defaultRoute = null) + { + if (hostnames == null || hostnames.Count < 1) { - if (string.IsNullOrEmpty(hostname)) - { - hostname = "localhost"; - } - - if (port < 1) - { - throw new ArgumentOutOfRangeException(nameof(port)); - } - - _Settings = new EonaCatWebserverSettings(hostname, port, ssl); - _Routes.Default = defaultRoute; + hostnames = new List { "localhost" }; } - /// - /// Creates a new instance of the EonaCat webserver. - /// - /// Hostnames or IP addresses on which to listen. Note: multiple listener endpoints are not supported on all platforms. - /// TCP port on which to listen. - /// Specify whether or not SSL should be used (HTTPS). - /// Method used when a request is received and no matching routes are found. Commonly used as the 404 handler when routes are used. - public WebServer(List hostnames, int port, bool ssl = false, Func defaultRoute = null) + if (port < 1) { - if (hostnames == null || hostnames.Count < 1) - { - hostnames = new List { "localhost" }; - } - - if (port < 1) - { - throw new ArgumentOutOfRangeException(nameof(port)); - } - - _Settings = new EonaCatWebserverSettings(hostnames, port, ssl); - _Routes.Default = defaultRoute; + throw new ArgumentOutOfRangeException(nameof(port)); } - /// - /// Tear down the server and dispose of background workers. - /// Do not use this object after disposal. - /// - public void Dispose() + _Settings = new EonaCatWebserverSettings(hostnames, port, ssl); + _Routes.Default = defaultRoute; + } + + /// + /// Indicates whether or not the server is listening. + /// + public bool IsListening => _HttpListener != null && _HttpListener.IsListening; + + /// + /// Number of requests being serviced currently. + /// + public int RequestCount => _RequestCount; + + /// + /// EonaCat webserver settings. + /// + public EonaCatWebserverSettings Settings + { + get => _Settings; + set { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Start accepting new connections. - /// - /// Cancellation token useful for canceling the server. - public void Start(CancellationToken token = default) - { - if (_HttpListener != null && _HttpListener.IsListening) + if (value == null) { - throw new InvalidOperationException("EonaCat Webserver is already listening."); - } - - _HttpListener = new System.Net.HttpListener(); - - LoadRoutes(); - Statistics = new EonaCatWebserverStatistics(); - - _TokenSource = CancellationTokenSource.CreateLinkedTokenSource(token); - _Token = token; - - foreach (string prefix in _Settings.Prefixes) - { - _HttpListener.Prefixes.Add(prefix); - } - - _HttpListener.Start(); - _AcceptConnections = Task.Run(() => AcceptConnections(_Token), _Token); - } - - /// - /// Start accepting new connections. - /// - /// Cancellation token useful for canceling the server. - /// Task. - public Task StartAsync(CancellationToken token = default) - { - if (_HttpListener != null && _HttpListener.IsListening) - { - throw new InvalidOperationException("EonaCat Webserver is already listening."); - } - - _HttpListener = new System.Net.HttpListener(); - - LoadRoutes(); - Statistics = new EonaCatWebserverStatistics(); - - _TokenSource = CancellationTokenSource.CreateLinkedTokenSource(token); - _Token = token; - - foreach (string prefix in _Settings.Prefixes) - { - _HttpListener.Prefixes.Add(prefix); - } - - _HttpListener.Start(); - _AcceptConnections = Task.Run(() => AcceptConnections(_Token), _Token); - return _AcceptConnections; - } - - /// - /// Stop accepting new connections. - /// - public void Stop() - { - if (!_HttpListener.IsListening) - { - throw new InvalidOperationException("EonaCat Webserver is already stopped."); - } - - if (_HttpListener != null && _HttpListener.IsListening) - { - _HttpListener.Stop(); - } - - if (_TokenSource != null && !_TokenSource.IsCancellationRequested) - { - _TokenSource.Cancel(); - } - } - - /// - /// Tear down the server and dispose of background workers. - /// Do not use this object after disposal. - /// - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - if (_HttpListener != null && _HttpListener.IsListening) - { - Stop(); - - _HttpListener.Close(); - } - - Events.HandleServerDisposing(this, EventArgs.Empty); - - _HttpListener = null; - _Settings = null; - _Routes = null; - _TokenSource = null; - _AcceptConnections = null; - - Events = null; - Statistics = null; - } - } - - private void LoadRoutes() - { - var staticRoutes = _Assembly - .GetTypes() // Get all classes from assembly - .SelectMany(x => x.GetMethods()) // Get all methods from assembly - .Where(IsStaticRoute); // Only select methods that are valid routes - - var parameterRoutes = _Assembly - .GetTypes() // Get all classes from assembly - .SelectMany(x => x.GetMethods()) // Get all methods from assembly - .Where(IsParameterRoute); // Only select methods that are valid routes - - var dynamicRoutes = _Assembly - .GetTypes() // Get all classes from assembly - .SelectMany(x => x.GetMethods()) // Get all methods from assembly - .Where(IsDynamicRoute); // Only select methods that are valid routes - - foreach (var staticRoute in staticRoutes) - { - var attribute = staticRoute.GetCustomAttributes().OfType().First(); - if (!_Routes.Static.Exists(attribute.Method, attribute.Path)) - { - Events.Logger?.Invoke(_Header + "adding static route " + attribute.Method.ToString() + " " + attribute.Path); - _Routes.Static.Add(attribute.Method, attribute.Path, ToRouteMethod(staticRoute), attribute.GUID, attribute.Metadata); - } - } - - foreach (var parameterRoute in parameterRoutes) - { - var attribute = parameterRoute.GetCustomAttributes().OfType().First(); - if (!_Routes.Parameter.Exists(attribute.Method, attribute.Path)) - { - Events.Logger?.Invoke(_Header + "adding parameter route " + attribute.Method.ToString() + " " + attribute.Path); - _Routes.Parameter.Add(attribute.Method, attribute.Path, ToRouteMethod(parameterRoute), attribute.GUID, attribute.Metadata); - } - } - - foreach (var dynamicRoute in dynamicRoutes) - { - var attribute = dynamicRoute.GetCustomAttributes().OfType().First(); - if (!_Routes.Dynamic.Exists(attribute.Method, attribute.Path)) - { - Events.Logger?.Invoke(_Header + "adding dynamic route " + attribute.Method.ToString() + " " + attribute.Path); - _Routes.Dynamic.Add(attribute.Method, attribute.Path, ToRouteMethod(dynamicRoute), attribute.GUID, attribute.Metadata); - } - } - } - - private bool IsStaticRoute(MethodInfo method) - { - return method.GetCustomAttributes().OfType().Any() - && method.ReturnType == typeof(Task) - && method.GetParameters().Length == 1 - && method.GetParameters().First().ParameterType == typeof(HttpContext); - } - - private bool IsParameterRoute(MethodInfo method) - { - return method.GetCustomAttributes().OfType().Any() - && method.ReturnType == typeof(Task) - && method.GetParameters().Length == 1 - && method.GetParameters().First().ParameterType == typeof(HttpContext); - } - - private bool IsDynamicRoute(MethodInfo method) - { - return method.GetCustomAttributes().OfType().Any() - && method.ReturnType == typeof(Task) - && method.GetParameters().Length == 1 - && method.GetParameters().First().ParameterType == typeof(HttpContext); - } - - private Func ToRouteMethod(MethodInfo method) - { - if (method.IsStatic) - { - return (Func)Delegate.CreateDelegate(typeof(Func), method); + _Settings = new EonaCatWebserverSettings(); } else { - object instance = Activator.CreateInstance(method.DeclaringType ?? throw new Exception("EonaCat Network: Declaring class is null")); - return (Func)Delegate.CreateDelegate(typeof(Func), instance, method); - } - } - - private async Task AcceptConnections(CancellationToken token) - { - try - { - while (_HttpListener.IsListening) - { - if (_RequestCount >= _Settings.IO.MaxRequests) - { - await Task.Delay(100, token).ConfigureAwait(false); - continue; - } - - System.Net.HttpListenerContext listenercontext = await _HttpListener.GetContextAsync().ConfigureAwait(false); - Interlocked.Increment(ref _RequestCount); - HttpContext context = null; - - Task unawaited = Task.Run(async () => - { - DateTime startTime = DateTime.Now; - - try - { - Events.HandleConnectionReceived(this, new ConnectionEventArgs( - listenercontext.Request.RemoteEndPoint.Address.ToString(), - listenercontext.Request.RemoteEndPoint.Port)); - - context = new HttpContext(listenercontext, _Settings, Events); - - Events.HandleRequestReceived(this, new RequestEventArgs(context)); - - if (_Settings.Debug.Requests) - { - Events.Logger?.Invoke( - _Header + context.Request.Source.IpAddress + ":" + context.Request.Source.Port + " " + - context.Request.Method.ToString() + " " + context.Request.Url.RawWithoutQuery); - } - - Statistics.IncrementRequestCounter(context.Request.Method); - Statistics.IncrementReceivedPayloadBytes(context.Request.ContentLength); - - if (!_Settings.AccessControl.Permit(context.Request.Source.IpAddress)) - { - Events.HandleRequestDenied(this, new RequestEventArgs(context)); - - if (_Settings.Debug.AccessControl) - { - Events.Logger?.Invoke(_Header + context.Request.Source.IpAddress + ":" + context.Request.Source.Port + " denied due to access control"); - } - - listenercontext.Response.StatusCode = 403; - listenercontext.Response.Close(); - return; - } - - if (context.Request.Method == HttpMethod.OPTIONS) - { - if (_Routes.Preflight != null) - { - if (_Settings.Debug.Routing) - { - Events.Logger?.Invoke( - _Header + "preflight route for " + context.Request.Source.IpAddress + ":" + context.Request.Source.Port + " " + - context.Request.Method.ToString() + " " + context.Request.Url.RawWithoutQuery); - } - - await _Routes.Preflight(context).ConfigureAwait(false); - return; - } - } - - bool terminate = false; - if (_Routes.PreRouting != null) - { - terminate = await _Routes.PreRouting(context).ConfigureAwait(false); - if (terminate) - { - if (_Settings.Debug.Routing) - { - Events.Logger?.Invoke( - _Header + "prerouting terminated connection for " + context.Request.Source.IpAddress + ":" + context.Request.Source.Port + " " + - context.Request.Method.ToString() + " " + context.Request.Url.RawWithoutQuery); - } - - return; - } - } - - if (context.Request.Method == HttpMethod.GET || context.Request.Method == HttpMethod.HEAD) - { - if (_Routes.Content.Match(context.Request.Url.RawWithoutQuery, out ContentRoute cr)) - { - if (_Settings.Debug.Routing) - { - Events.Logger?.Invoke( - _Header + "content route for " + context.Request.Source.IpAddress + ":" + context.Request.Source.Port + " " + - context.Request.Method.ToString() + " " + context.Request.Url.RawWithoutQuery); - } - - context.RouteType = RouteTypeEnum.Content; - context.Route = cr; - await _Routes.ContentHandler.Process(context, token).ConfigureAwait(false); - return; - } - } - - Func handler = _Routes.Static.Match(context.Request.Method, context.Request.Url.RawWithoutQuery, out StaticRoute sr); - if (handler != null) - { - if (_Settings.Debug.Routing) - { - Events.Logger?.Invoke( - _Header + "static route for " + context.Request.Source.IpAddress + ":" + context.Request.Source.Port + " " + - context.Request.Method.ToString() + " " + context.Request.Url.RawWithoutQuery); - } - - context.RouteType = RouteTypeEnum.Static; - context.Route = sr; - await handler(context).ConfigureAwait(false); - return; - } - - handler = _Routes.Parameter.Match(context.Request.Method, context.Request.Url.RawWithoutQuery, out Dictionary parameters, out ParameterRoute pr); - if (handler != null) - { - context.Request.Url.Parameters = new Dictionary(parameters); - - if (_Settings.Debug.Routing) - { - Events.Logger?.Invoke( - _Header + "parameter route for " + context.Request.Source.IpAddress + ":" + context.Request.Source.Port + " " + - context.Request.Method.ToString() + " " + context.Request.Url.RawWithoutQuery); - } - - context.RouteType = RouteTypeEnum.Parameter; - context.Route = pr; - await handler(context).ConfigureAwait(false); - return; - } - - handler = _Routes.Dynamic.Match(context.Request.Method, context.Request.Url.RawWithoutQuery, out DynamicRoute dr); - if (handler != null) - { - if (_Settings.Debug.Routing) - { - Events.Logger?.Invoke( - _Header + "dynamic route for " + context.Request.Source.IpAddress + ":" + context.Request.Source.Port + " " + - context.Request.Method.ToString() + " " + context.Request.Url.RawWithoutQuery); - } - - context.RouteType = RouteTypeEnum.Dynamic; - context.Route = dr; - await handler(context).ConfigureAwait(false); - return; - } - - if (_Routes.Default != null) - { - if (_Settings.Debug.Routing) - { - Events.Logger?.Invoke( - _Header + "default route for " + context.Request.Source.IpAddress + ":" + context.Request.Source.Port + " " + - context.Request.Method.ToString() + " " + context.Request.Url.RawWithoutQuery); - } - - context.RouteType = RouteTypeEnum.Default; - await _Routes.Default(context).ConfigureAwait(false); - return; - } - else - { - if (_Settings.Debug.Routing) - { - Events.Logger?.Invoke( - _Header + "default route not found for " + context.Request.Source.IpAddress + ":" + context.Request.Source.Port + " " + - context.Request.Method.ToString() + " " + context.Request.Url.RawWithoutQuery); - } - - context.Response.StatusCode = 404; - context.Response.ContentType = Pages.Default404Page.ContentType; - await context.Response.Send(Pages.Default404Page.Content).ConfigureAwait(false); - return; - } - } - catch (Exception eInner) - { - context.Response.StatusCode = 500; - context.Response.ContentType = Pages.Default500Page.ContentType; - await context.Response.Send(Pages.Default500Page.Content).ConfigureAwait(false); - Events.HandleExceptionEncountered(this, new ExceptionEventArgs(context, eInner)); - } - finally - { - Interlocked.Decrement(ref _RequestCount); - - if (context != null && context.Response != null && context.Response.ResponseSent) - { - Events.HandleResponseSent(this, new ResponseEventArgs(context, TotalMsFrom(startTime))); - Statistics.IncrementSentPayloadBytes(context.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; + _Settings = value; } } } + + /// + /// EonaCat webserver routes. + /// + public EonaCatWebserverRoutes Routes + { + get => _Routes; + set + { + if (value == null) + { + _Routes = new EonaCatWebserverRoutes(); + } + else + { + _Routes = value; + } + } + } + + /// + /// EonaCat webserver statistics. + /// + public EonaCatWebserverStatistics Statistics { get; private set; } = new(); + + /// + /// Set specific actions/callbacks to use when events are raised. + /// + public EonaCatWebserverEvents Events { get; private set; } = new(); + + /// + /// Default pages served by the EonaCat webserver. + /// + public EonaCatWebserverPages Pages { get; } = new(); + + /// + /// Tear down the server and dispose of background workers. + /// Do not use this object after disposal. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Start accepting new connections. + /// + /// Cancellation token useful for canceling the server. + public void Start(CancellationToken token = default) + { + if (_HttpListener != null && _HttpListener.IsListening) + { + throw new InvalidOperationException("EonaCat Webserver is already listening."); + } + + _HttpListener = new HttpListener(); + + LoadRoutes(); + Statistics = new EonaCatWebserverStatistics(); + + _TokenSource = CancellationTokenSource.CreateLinkedTokenSource(token); + _Token = token; + + foreach (var prefix in _Settings.Prefixes) + { + _HttpListener.Prefixes.Add(prefix); + } + + _HttpListener.Start(); + _AcceptConnections = Task.Run(() => AcceptConnections(_Token), _Token); + } + + /// + /// Start accepting new connections. + /// + /// Cancellation token useful for canceling the server. + /// Task. + public Task StartAsync(CancellationToken token = default) + { + if (_HttpListener != null && _HttpListener.IsListening) + { + throw new InvalidOperationException("EonaCat Webserver is already listening."); + } + + _HttpListener = new HttpListener(); + + LoadRoutes(); + Statistics = new EonaCatWebserverStatistics(); + + _TokenSource = CancellationTokenSource.CreateLinkedTokenSource(token); + _Token = token; + + foreach (var prefix in _Settings.Prefixes) + { + _HttpListener.Prefixes.Add(prefix); + } + + _HttpListener.Start(); + _AcceptConnections = Task.Run(() => AcceptConnections(_Token), _Token); + return _AcceptConnections; + } + + /// + /// Stop accepting new connections. + /// + public void Stop() + { + if (!_HttpListener.IsListening) + { + throw new InvalidOperationException("EonaCat Webserver is already stopped."); + } + + if (_HttpListener != null && _HttpListener.IsListening) + { + _HttpListener.Stop(); + } + + if (_TokenSource != null && !_TokenSource.IsCancellationRequested) + { + _TokenSource.Cancel(); + } + } + + /// + /// Tear down the server and dispose of background workers. + /// Do not use this object after disposal. + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (_HttpListener != null && _HttpListener.IsListening) + { + Stop(); + + _HttpListener.Close(); + } + + Events.HandleServerDisposing(this, EventArgs.Empty); + + _HttpListener = null; + _Settings = null; + _Routes = null; + _TokenSource = null; + _AcceptConnections = null; + + Events = null; + Statistics = null; + } + } + + private void LoadRoutes() + { + var staticRoutes = _Assembly + .GetTypes() // Get all classes from assembly + .SelectMany(x => x.GetMethods()) // Get all methods from assembly + .Where(IsStaticRoute); // Only select methods that are valid routes + + var parameterRoutes = _Assembly + .GetTypes() // Get all classes from assembly + .SelectMany(x => x.GetMethods()) // Get all methods from assembly + .Where(IsParameterRoute); // Only select methods that are valid routes + + var dynamicRoutes = _Assembly + .GetTypes() // Get all classes from assembly + .SelectMany(x => x.GetMethods()) // Get all methods from assembly + .Where(IsDynamicRoute); // Only select methods that are valid routes + + foreach (var staticRoute in staticRoutes) + { + var attribute = staticRoute.GetCustomAttributes().OfType().First(); + if (!_Routes.Static.Exists(attribute.Method, attribute.Path)) + { + Events.Logger?.Invoke(_Header + "adding static route " + attribute.Method + " " + attribute.Path); + _Routes.Static.Add(attribute.Method, attribute.Path, ToRouteMethod(staticRoute), attribute.GUID, + attribute.Metadata); + } + } + + foreach (var parameterRoute in parameterRoutes) + { + var attribute = parameterRoute.GetCustomAttributes().OfType().First(); + if (!_Routes.Parameter.Exists(attribute.Method, attribute.Path)) + { + Events.Logger?.Invoke(_Header + "adding parameter route " + attribute.Method + " " + attribute.Path); + _Routes.Parameter.Add(attribute.Method, attribute.Path, ToRouteMethod(parameterRoute), attribute.GUID, + attribute.Metadata); + } + } + + foreach (var dynamicRoute in dynamicRoutes) + { + var attribute = dynamicRoute.GetCustomAttributes().OfType().First(); + if (!_Routes.Dynamic.Exists(attribute.Method, attribute.Path)) + { + Events.Logger?.Invoke(_Header + "adding dynamic route " + attribute.Method + " " + attribute.Path); + _Routes.Dynamic.Add(attribute.Method, attribute.Path, ToRouteMethod(dynamicRoute), attribute.GUID, + attribute.Metadata); + } + } + } + + private bool IsStaticRoute(MethodInfo method) + { + return method.GetCustomAttributes().OfType().Any() + && method.ReturnType == typeof(Task) + && method.GetParameters().Length == 1 + && method.GetParameters().First().ParameterType == typeof(HttpContext); + } + + private bool IsParameterRoute(MethodInfo method) + { + return method.GetCustomAttributes().OfType().Any() + && method.ReturnType == typeof(Task) + && method.GetParameters().Length == 1 + && method.GetParameters().First().ParameterType == typeof(HttpContext); + } + + private bool IsDynamicRoute(MethodInfo method) + { + return method.GetCustomAttributes().OfType().Any() + && method.ReturnType == typeof(Task) + && method.GetParameters().Length == 1 + && method.GetParameters().First().ParameterType == typeof(HttpContext); + } + + private Func ToRouteMethod(MethodInfo method) + { + if (method.IsStatic) + { + return (Func)Delegate.CreateDelegate(typeof(Func), method); + } + + var instance = Activator.CreateInstance(method.DeclaringType ?? + throw new Exception("EonaCat Network: Declaring class is null")); + return (Func)Delegate.CreateDelegate(typeof(Func), instance, method); + } + + private async Task AcceptConnections(CancellationToken token) + { + try + { + while (_HttpListener.IsListening) + { + if (_RequestCount >= _Settings.IO.MaxRequests) + { + await Task.Delay(100, token).ConfigureAwait(false); + continue; + } + + var listenercontext = await _HttpListener.GetContextAsync().ConfigureAwait(false); + Interlocked.Increment(ref _RequestCount); + HttpContext context = null; + + var unawaited = Task.Run(async () => + { + var startTime = DateTime.Now; + + try + { + Events.HandleConnectionReceived(this, new ConnectionEventArgs( + listenercontext.Request.RemoteEndPoint.Address.ToString(), + listenercontext.Request.RemoteEndPoint.Port)); + + context = new HttpContext(listenercontext, _Settings, Events); + + Events.HandleRequestReceived(this, new RequestEventArgs(context)); + + if (_Settings.Debug.Requests) + { + Events.Logger?.Invoke( + _Header + context.Request.Source.IpAddress + ":" + context.Request.Source.Port + " " + + context.Request.Method + " " + context.Request.Url.RawWithoutQuery); + } + + Statistics.IncrementRequestCounter(context.Request.Method); + Statistics.IncrementReceivedPayloadBytes(context.Request.ContentLength); + + if (!_Settings.AccessControl.Permit(context.Request.Source.IpAddress)) + { + Events.HandleRequestDenied(this, new RequestEventArgs(context)); + + if (_Settings.Debug.AccessControl) + { + Events.Logger?.Invoke(_Header + context.Request.Source.IpAddress + ":" + + context.Request.Source.Port + " denied due to access control"); + } + + listenercontext.Response.StatusCode = 403; + listenercontext.Response.Close(); + return; + } + + if (context.Request.Method == HttpMethod.OPTIONS) + { + if (_Routes.Preflight != null) + { + if (_Settings.Debug.Routing) + { + Events.Logger?.Invoke( + _Header + "preflight route for " + context.Request.Source.IpAddress + ":" + + context.Request.Source.Port + " " + + context.Request.Method + " " + context.Request.Url.RawWithoutQuery); + } + + await _Routes.Preflight(context).ConfigureAwait(false); + return; + } + } + + var terminate = false; + if (_Routes.PreRouting != null) + { + terminate = await _Routes.PreRouting(context).ConfigureAwait(false); + if (terminate) + { + if (_Settings.Debug.Routing) + { + Events.Logger?.Invoke( + _Header + "prerouting terminated connection for " + + context.Request.Source.IpAddress + ":" + context.Request.Source.Port + " " + + context.Request.Method + " " + context.Request.Url.RawWithoutQuery); + } + + return; + } + } + + if (context.Request.Method == HttpMethod.GET || context.Request.Method == HttpMethod.HEAD) + { + if (_Routes.Content.Match(context.Request.Url.RawWithoutQuery, out var cr)) + { + if (_Settings.Debug.Routing) + { + Events.Logger?.Invoke( + _Header + "content route for " + context.Request.Source.IpAddress + ":" + + context.Request.Source.Port + " " + + context.Request.Method + " " + context.Request.Url.RawWithoutQuery); + } + + context.RouteType = RouteTypeEnum.Content; + context.Route = cr; + await _Routes.ContentHandler.Process(context, token).ConfigureAwait(false); + return; + } + } + + var handler = _Routes.Static.Match(context.Request.Method, context.Request.Url.RawWithoutQuery, + out var sr); + if (handler != null) + { + if (_Settings.Debug.Routing) + { + Events.Logger?.Invoke( + _Header + "static route for " + context.Request.Source.IpAddress + ":" + + context.Request.Source.Port + " " + + context.Request.Method + " " + context.Request.Url.RawWithoutQuery); + } + + context.RouteType = RouteTypeEnum.Static; + context.Route = sr; + await handler(context).ConfigureAwait(false); + return; + } + + handler = _Routes.Parameter.Match(context.Request.Method, context.Request.Url.RawWithoutQuery, + out var parameters, out var pr); + if (handler != null) + { + context.Request.Url.Parameters = new Dictionary(parameters); + + if (_Settings.Debug.Routing) + { + Events.Logger?.Invoke( + _Header + "parameter route for " + context.Request.Source.IpAddress + ":" + + context.Request.Source.Port + " " + + context.Request.Method + " " + context.Request.Url.RawWithoutQuery); + } + + context.RouteType = RouteTypeEnum.Parameter; + context.Route = pr; + await handler(context).ConfigureAwait(false); + return; + } + + handler = _Routes.Dynamic.Match(context.Request.Method, context.Request.Url.RawWithoutQuery, + out var dr); + if (handler != null) + { + if (_Settings.Debug.Routing) + { + Events.Logger?.Invoke( + _Header + "dynamic route for " + context.Request.Source.IpAddress + ":" + + context.Request.Source.Port + " " + + context.Request.Method + " " + context.Request.Url.RawWithoutQuery); + } + + context.RouteType = RouteTypeEnum.Dynamic; + context.Route = dr; + await handler(context).ConfigureAwait(false); + return; + } + + if (_Routes.Default != null) + { + if (_Settings.Debug.Routing) + { + Events.Logger?.Invoke( + _Header + "default route for " + context.Request.Source.IpAddress + ":" + + context.Request.Source.Port + " " + + context.Request.Method + " " + context.Request.Url.RawWithoutQuery); + } + + context.RouteType = RouteTypeEnum.Default; + await _Routes.Default(context).ConfigureAwait(false); + } + else + { + if (_Settings.Debug.Routing) + { + Events.Logger?.Invoke( + _Header + "default route not found for " + context.Request.Source.IpAddress + ":" + + context.Request.Source.Port + " " + + context.Request.Method + " " + context.Request.Url.RawWithoutQuery); + } + + context.Response.StatusCode = 404; + context.Response.ContentType = Pages.Default404Page.ContentType; + await context.Response.Send(Pages.Default404Page.Content).ConfigureAwait(false); + } + } + catch (Exception eInner) + { + context.Response.StatusCode = 500; + context.Response.ContentType = Pages.Default500Page.ContentType; + await context.Response.Send(Pages.Default500Page.Content).ConfigureAwait(false); + Events.HandleExceptionEncountered(this, new ExceptionEventArgs(context, eInner)); + } + finally + { + Interlocked.Decrement(ref _RequestCount); + + if (context != null && context.Response != null && context.Response.ResponseSent) + { + Events.HandleResponseSent(this, new ResponseEventArgs(context, TotalMsFrom(startTime))); + Statistics.IncrementSentPayloadBytes(context.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 + { + var endTime = DateTime.Now; + var totalTime = endTime - startTime; + return totalTime.TotalMilliseconds; + } + catch (Exception) + { + return -1; + } + } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/EonaCatWebserver.xml b/EonaCat.Network/System/Web/EonaCatWebserver.xml index da1009e..034cc26 100644 --- a/EonaCat.Network/System/Web/EonaCatWebserver.xml +++ b/EonaCat.Network/System/Web/EonaCatWebserver.xml @@ -1,1811 +1,1834 @@ + - - EonaCatWebServer - - - - - Access control manager. Dictates which connections are permitted or denied. - - - - - Matcher to match denied addresses. - - - - - Matcher to match permitted addresses. - - - - - Access control mode, either DefaultPermit or DefaultDeny. - DefaultPermit: allow everything, except for those explicitly denied. - DefaultDeny: deny everything, except for those explicitly permitted. - - - - - Instantiate the object. - - Access control mode. - - - - Permit or deny a request based on IP address. - When operating in 'default deny', only specified entries are permitted. - When operating in 'default permit', everything is allowed unless explicitly denied. - - The IP address to evaluate. - True if permitted. - - - - Access control mode of operation. - - - - - Permit requests from any endpoint by default. - - - - - Deny requests from any endpoint by default. - - - - - A chunk of data, used when reading from a request where the Transfer-Encoding header includes 'chunked'. - - - - - Length of the data. - - - - - Data. - - - - - Any additional metadata that appears on the length line after the length hex value and semicolon. - - - - - Indicates whether or not this is the final chunk, i.e. the chunk length received was zero. - - - - - Connection event arguments. - - - - - Requestor IP address. - - - - - Request TCP port. - - - - - Connection event arguments. - - Requestor IP address. - Request TCP port. - - - - Assign a method handler for when requests are received matching the supplied method and path. - - - - - Globally-unique identifier. - - - - - The pattern against which the raw URL should be matched. - - - - - Indicates whether or not the path specifies a directory. If so, any matching URL will be handled by the specified handler. - - - - - User-supplied metadata. - - - - - Create a new route object. - - The pattern against which the raw URL should be matched. - Indicates whether or not the path specifies a directory. If so, any matching URL will be handled by the specified handler. - Globally-unique identifier. - User-supplied metadata. - - - - Content route manager. Content routes are used for GET and HEAD requests to specific files or entire directories. - - - - - Base directory for files and directories accessible via content routes. - - - - - Instantiate the object. - - - - - Add a route. - - URL path, i.e. /path/to/resource. - True if the path represents a directory. - Globally-unique identifier. - User-supplied metadata. - - - - Remove a route. - - URL path. - - - - Retrieve a content route. - - URL path. - ContentRoute if the route exists, otherwise null. - - - - Check if a content route exists. - - URL path. - True if exists. - - - - Retrieve a content route. - - URL path. - Matching route. - True if a match exists. - - - - Content route handler. Handles GET and HEAD requests to content routes for files and directories. - - - - - The FileMode value to use when accessing files within a content route via a FileStream. Default is FileMode.Open. - - - - - The FileAccess value to use when accessing files within a content route via a FileStream. Default is FileAccess.Read. - - - - - The FileShare value to use when accessing files within a content route via a FileStream. Default is FileShare.Read. - - - - - Assign a method handler for when requests are received matching the supplied method and path regex. - - - - - Globally-unique identifier. - - - - - The HTTP method, i.e. GET, PUT, POST, DELETE, etc. - - - - - The pattern against which the raw URL should be matched. - - - - - The handler for the dynamic route. - - - - - User-supplied metadata. - - - - - Create a new route object. - - The HTTP method, i.e. GET, PUT, POST, DELETE, etc. - The pattern against which the raw URL should be matched. - The method that should be called to handle the request. - Globally-unique identifier. - User-supplied metadata. - - - - Attribute that is used to mark methods as a dynamic route. - - - - - The HTTP method, i.e. GET, PUT, POST, DELETE, etc. - - - - - The pattern against which the raw URL should be matched. Must be convertible to a regular expression. - - - - - Globally-unique identifier. - - - - - User-supplied metadata. - - - - - Instantiate the object. - - The HTTP method, i.e. GET, PUT, POST, DELETE, etc. - The regular expression pattern against which the raw URL should be matched. - Globally-unique identifier. - User-supplied metadata. - - - - Dynamic route manager. Dynamic routes are used for requests using any HTTP method to any path that can be matched by regular expression. - - - - - Directly access the underlying regular expression matching library. - This is helpful in case you want to specify the matching behavior should multiple matches exist. - - - - - Instantiate the object. - - - - - Add a route. - - The HTTP method. - URL path, i.e. /path/to/resource. - Method to invoke. - Globally-unique identifier. - User-supplied metadata. - - - - Remove a route. - - The HTTP method. - URL path. - - - - Check if a content route exists. - - The HTTP method. - URL path. - True if exists. - - - - Match a request method and URL to a handler method. - - The HTTP method. - URL path. - Matching route. - Method to invoke. - - - - Exception event arguments. - - - - - IP address. - - - - - Port number. - - - - - HTTP method. - - - - - URL. - - - - - Request query. - - - - - Request headers. - - - - - Content length. - - - - - Response status. - - - - - Response headers. - - - - - Response content length. - - - - - Exception. - - - - - JSON string of the Exception. - - - - - HTTP context including both request and response. - - - - - The HTTP request that was received. - - - - - Type of route. - - - - - Matched route. - - - - - The HTTP response that will be sent. This object is preconstructed on your behalf and can be modified directly. - - - - - User-supplied metadata. - - - - - Instantiate the object. - - - - - HTTP methods, i.e. GET, PUT, POST, DELETE, etc. - - - - - HTTP GET. - - - - - HTTP HEAD. - - - - - HTTP PUT. - - - - - HTTP POST. - - - - - HTTP DELETE. - - - - - HTTP PATCH. - - - - - HTTP CONNECT. - - - - - HTTP OPTIONS. - - - - - HTTP TRACE. - - - - - HTTP request. - - - - - UTC timestamp from when the request was received. - - - - - Thread ID on which the request exists. - - - - - The protocol and version. - - - - - Source (requestor) IP and port information. - - - - - Destination IP and port information. - - - - - The HTTP method used in the request. - - - - - URL details. - - - - - Query details. - - - - - The headers found in the request. - - - - - Specifies whether or not the client requested HTTP keepalives. - - - - - Indicates whether or not chunked transfer encoding was detected. - - - - - Indicates whether or not the payload has been gzip compressed. - - - - - Indicates whether or not the payload has been deflate compressed. - - - - - The useragent specified in the request. - - - - - The content type as specified by the requestor (client). - - - - - The number of bytes in the request body. - - - - - The stream from which to read the request body sent by the requestor (client). - - - - - Retrieve the request body as a byte array. This will fully read the stream. - - - - - Retrieve the request body as a string. This will fully read the stream. - - - - - The original HttpListenerContext from which the HttpRequest was constructed. - - - - - HTTP request. - - - - - HTTP request. - Instantiate the object using an HttpListenerContext. - - HttpListenerContext. - - - - Retrieve a specified header value from either the headers or the querystring (case insensitive). - - - - - - - Determine if a header exists. - - Header key. - Specify whether a case sensitive search should be used. - True if exists. - - - - Determine if a querystring entry exists. - - Querystring key. - Specify whether a case sensitive search should be used. - True if exists. - - - - For chunked transfer-encoded requests, read the next chunk. - It is strongly recommended that you use the ChunkedTransfer parameter before invoking this method. - - Cancellation token useful for canceling the request. - Chunk. - - - - Read the data stream fully and convert the data to the object type specified using JSON deserialization. - Note: if you use this method, you will not be able to read from the data stream afterward. - - Type. - Object of type specified. - - - - Source details. - - - - - IP address of the requestor. - - - - - TCP port from which the request originated on the requestor. - - - - - Source details. - - - - - Source details. - - IP address of the requestor. - TCP port from which the request originated on the requestor. - - - - Destination details. - - - - - IP address to which the request was made. - - - - - TCP port on which the request was received. - - - - - Hostname to which the request was directed. - - - - - Hostname elements. - - - - - Destination details. - - - - - Source details. - - IP address to which the request was made. - TCP port on which the request was received. - Hostname. - - - - URL details. - - - - - Full URL. - - - - - Raw URL with query. - - - - - Raw URL without query. - - - - - Raw URL elements. - - - - - Parameters found within the URL, if using parameter routes. - - - - - URL details. - - - - - URL details. - - Full URL. - Raw URL. - - - - Query details. - - - - - Querystring, excluding the leading '?'. - - - - - Query elements. - - - - - Query details. - - - - - Query details. - - Full URL. - - - - HTTP response. - - - - - The HTTP status code to return to the requestor (client). - - - - - The HTTP status description to return to the requestor (client). - - - - - User-supplied headers to include in the response. - - - - - User-supplied content-type to include in the response. - - - - - The length of the supplied response data. - - - - - Indicates whether or not chunked transfer encoding should be indicated in the response. - - - - - Retrieve the response body sent using a Send() or SendAsync() method. - - - - - Retrieve the response body sent using a Send() or SendAsync() method. - - - - - Response data stream sent to the requestor. - - - - - Instantiate the object. - - - - - Send headers and no data to the requestor and terminate the connection. - - Cancellation token useful for canceling the request. - True if successful. - - - - Send headers with a specified content length and no data to the requestor and terminate the connection. Useful for HEAD requests where the content length must be set. - - Cancellation token useful for canceling the request. - Content length. - True if successful. - - - - Send headers and data to the requestor and terminate the connection. - - Data. - Cancellation token useful for canceling the request. - True if successful. - - - - Send headers and data to the requestor and terminate the connection. - - Data. - Cancellation token useful for canceling the request. - True if successful. - - - - Send headers and data to the requestor and terminate. - - Number of bytes to send. - Stream containing the data. - Cancellation token useful for canceling the request. - True if successful. - - - - Send headers (if not already sent) and a chunk of data using chunked transfer-encoding, and keep the connection in-tact. - - Chunk of data. - Number of bytes to send from the chunk, i.e. the actual data size (for example, return value of FileStream.ReadAsync(buffer, 0, buffer.Length)). - Cancellation token useful for canceling the request. - True if successful. - - - - Send headers (if not already sent) and the final chunk of data using chunked transfer-encoding and terminate the connection. - - Chunk of data./// Number of bytes to send from the chunk, i.e. the actual data size (for example, return value of FileStream.ReadAsync(buffer, 0, buffer.Length)). - Cancellation token useful for canceling the request. - True if successful. - - - - Convert the response data sent using a Send() method to the object type specified using JSON deserialization. - - Type. - Object of type specified. - - - - MIME types and file extensions. - - - - - Instantiates the object. - - - - - Retrieve MIME type from file extension. - - File extension. - String containing MIME type. - - - - Object extensions. - - - - - Return a JSON string of the object. - - Object. - Enable or disable pretty print. - JSON string. - - - - Assign a method handler for when requests are received matching the supplied method and path containing parameters. - - - - - Globally-unique identifier. - - - - - The HTTP method, i.e. GET, PUT, POST, DELETE, etc. - - - - - The pattern against which the raw URL should be matched. - - - - - The handler for the parameter route. - - - - - User-supplied metadata. - - - - - Create a new route object. - - The HTTP method, i.e. GET, PUT, POST, DELETE, etc. - The pattern against which the raw URL should be matched. - The method that should be called to handle the request. - Globally-unique identifier. - User-supplied metadata. - - - - Attribute that is used to mark methods as a parameter route. - - - - - The path to match, i.e. /{version}/api/{id}. - If a match is found, the Dictionary found in HttpRequest.Url.Parameters will contain keys for 'version' and 'id'. - - - - - The HTTP method, i.e. GET, PUT, POST, DELETE, etc. - - - - - Globally-unique identifier. - - - - - User-supplied metadata. - - - - - Instantiate the object. - - The HTTP method, i.e. GET, PUT, POST, DELETE, etc. - The path to match, i.e. /{version}/api/{id}. - Globally-unique identifier. - User-supplied metadata. - - - - Parameter route manager. Parameter routes are used for requests using any HTTP method to any path where parameters are defined in the URL. - For example, /{version}/api. - For a matching URL, the HttpRequest.Url.Parameters will contain a key called 'version' with the value found in the URL. - - - - - Directly access the underlying URL matching library. - This is helpful in case you want to specify the matching behavior should multiple matches exist. - - - - - Instantiate the object. - - - - - Add a route. - - The HTTP method. - URL path, i.e. /path/to/resource. - Method to invoke. - Globally-unique identifier. - User-supplied metadata. - - - - Remove a route. - - The HTTP method. - URL path. - - - - Retrieve a parameter route. - - The HTTP method. - URL path. - ParameterRoute if the route exists, otherwise null. - - - - Check if a content route exists. - - The HTTP method. - URL path. - True if exists. - - - - Match a request method and URL to a handler method. - - The HTTP method. - URL path. - Values extracted from the URL. - Matching route. - True if match exists. - - - - Request event arguments. - - - - - IP address. - - - - - Port number. - - - - - HTTP method. - - - - - URL. - - - - - Query found in the URL. - - - - - Request headers. - - - - - Content length. - - - - - Response event arguments. - - - - - IP address. - - - - - Port number. - - - - - HTTP method. - - - - - URL. - - - - - Request query. - - - - - Request headers. - - - - - Content length. - - - - - Response status. - - - - - Response headers. - - - - - Response content length. - - - - - Total time in processing the request and sending the response, in milliseconds. - - - - - Route type. - - - - - Default route. - - - - - Content route. - - - - - Static route. - - - - - Parameter route. - - - - - Dynamic route. - - - - - Assign a method handler for when requests are received matching the supplied method and path. - - - - - Globally-unique identifier. - - - - - The HTTP method, i.e. GET, PUT, POST, DELETE, etc. - - - - - The raw URL, i.e. /foo/bar/. Be sure this begins and ends with '/'. - - - - - The handler for the static route. - - - - - User-supplied metadata. - - - - - Create a new route object. - - The HTTP method, i.e. GET, PUT, POST, DELETE, etc. - The raw URL, i.e. /foo/bar/. Be sure this begins and ends with '/'. - The method that should be called to handle the request. - Globally-unique identifier. - User-supplied metadata. - - - - Attribute that is used to mark methods as a static route. - - - - - The raw URL, i.e. /foo/bar/. Be sure this begins and ends with '/'. - - - - - The HTTP method, i.e. GET, PUT, POST, DELETE, etc. - - - - - Globally-unique identifier. - - - - - User-supplied metadata. - - - - - Instantiate the object. - - The HTTP method, i.e. GET, PUT, POST, DELETE, etc. - The raw URL, i.e. /foo/bar/. Be sure this begins and ends with '/'. - Globally-unique identifier. - User-supplied metadata. - - - - Static route manager. Static routes are used for requests using any HTTP method to a specific path. - - - - - Instantiate the object. - - - - - Add a route. - - The HTTP method. - URL path, i.e. /path/to/resource. - Method to invoke. - Globally-unique identifier. - User-supplied metadata. - - - - Remove a route. - - The HTTP method. - URL path. - - - - Retrieve a static route. - - The HTTP method. - URL path. - StaticRoute if the route exists, otherwise null. - - - - Check if a static route exists. - - The HTTP method. - URL path. - True if exists. - - - - Match a request method and URL to a handler method. - - The HTTP method. - URL path. - Matching route. - Method to invoke. - - - + + EonaCatWebServer + + + + + Access control manager. Dictates which connections are permitted or denied. + + + + + Matcher to match denied addresses. + + + + + Matcher to match permitted addresses. + + + + + Access control mode, either DefaultPermit or DefaultDeny. + DefaultPermit: allow everything, except for those explicitly denied. + DefaultDeny: deny everything, except for those explicitly permitted. + + + + + Instantiate the object. + + Access control mode. + + + + Permit or deny a request based on IP address. + When operating in 'default deny', only specified entries are permitted. + When operating in 'default permit', everything is allowed unless explicitly denied. + + The IP address to evaluate. + True if permitted. + + + + Access control mode of operation. + + + + + Permit requests from any endpoint by default. + + + + + Deny requests from any endpoint by default. + + + + + A chunk of data, used when reading from a request where the Transfer-Encoding header includes 'chunked'. + + + + + Length of the data. + + + + + Data. + + + + + Any additional metadata that appears on the length line after the length hex value and semicolon. + + + + + Indicates whether or not this is the final chunk, i.e. the chunk length received was zero. + + + + + Connection event arguments. + + + + + Requestor IP address. + + + + + Request TCP port. + + + + + Connection event arguments. + + Requestor IP address. + Request TCP port. + + + + Assign a method handler for when requests are received matching the supplied method and path. + + + + + Globally-unique identifier. + + + + + The pattern against which the raw URL should be matched. + + + + + Indicates whether or not the path specifies a directory. If so, any matching URL will be handled by the specified handler. + + + + + User-supplied metadata. + + + + + Create a new route object. + + The pattern against which the raw URL should be matched. + Indicates whether or not the path specifies a directory. If so, any matching URL will be handled by the specified handler. + Globally-unique identifier. + User-supplied metadata. + + + + Content route manager. Content routes are used for GET and HEAD requests to specific files or entire directories. + + + + + Base directory for files and directories accessible via content routes. + + + + + Instantiate the object. + + + + + Add a route. + + URL path, i.e. /path/to/resource. + True if the path represents a directory. + Globally-unique identifier. + User-supplied metadata. + + + + Remove a route. + + URL path. + + + + Retrieve a content route. + + URL path. + ContentRoute if the route exists, otherwise null. + + + + Check if a content route exists. + + URL path. + True if exists. + + + + Retrieve a content route. + + URL path. + Matching route. + True if a match exists. + + + + Content route handler. Handles GET and HEAD requests to content routes for files and directories. + + + + + The FileMode value to use when accessing files within a content route via a FileStream. Default is FileMode.Open. + + + + + The FileAccess value to use when accessing files within a content route via a FileStream. Default is FileAccess.Read. + + + + + The FileShare value to use when accessing files within a content route via a FileStream. Default is FileShare.Read. + + + + + Assign a method handler for when requests are received matching the supplied method and path regex. + + + + + Globally-unique identifier. + + + + + The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + + + + + The pattern against which the raw URL should be matched. + + + + + The handler for the dynamic route. + + + + + User-supplied metadata. + + + + + Create a new route object. + + The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + The pattern against which the raw URL should be matched. + The method that should be called to handle the request. + Globally-unique identifier. + User-supplied metadata. + + + + Attribute that is used to mark methods as a dynamic route. + + + + + The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + + + + + The pattern against which the raw URL should be matched. Must be convertible to a regular expression. + + + + + Globally-unique identifier. + + + + + User-supplied metadata. + + + + + Instantiate the object. + + The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + The regular expression pattern against which the raw URL should be matched. + Globally-unique identifier. + User-supplied metadata. + + + + Dynamic route manager. Dynamic routes are used for requests using any HTTP method to any path that can be matched by regular expression. + + + + + Directly access the underlying regular expression matching library. + This is helpful in case you want to specify the matching behavior should multiple matches exist. + + + + + Instantiate the object. + + + + + Add a route. + + The HTTP method. + URL path, i.e. /path/to/resource. + Method to invoke. + Globally-unique identifier. + User-supplied metadata. + + + + Remove a route. + + The HTTP method. + URL path. + + + + Check if a content route exists. + + The HTTP method. + URL path. + True if exists. + + + + Match a request method and URL to a handler method. + + The HTTP method. + URL path. + Matching route. + Method to invoke. + + + + Exception event arguments. + + + + + IP address. + + + + + Port number. + + + + + HTTP method. + + + + + URL. + + + + + Request query. + + + + + Request headers. + + + + + Content length. + + + + + Response status. + + + + + Response headers. + + + + + Response content length. + + + + + Exception. + + + + + JSON string of the Exception. + + + + + HTTP context including both request and response. + + + + + The HTTP request that was received. + + + + + Type of route. + + + + + Matched route. + + + + + The HTTP response that will be sent. This object is preconstructed on your behalf and can be modified directly. + + + + + User-supplied metadata. + + + + + Instantiate the object. + + + + + HTTP methods, i.e. GET, PUT, POST, DELETE, etc. + + + + + HTTP GET. + + + + + HTTP HEAD. + + + + + HTTP PUT. + + + + + HTTP POST. + + + + + HTTP DELETE. + + + + + HTTP PATCH. + + + + + HTTP CONNECT. + + + + + HTTP OPTIONS. + + + + + HTTP TRACE. + + + + + HTTP request. + + + + + UTC timestamp from when the request was received. + + + + + Thread ID on which the request exists. + + + + + The protocol and version. + + + + + Source (requestor) IP and port information. + + + + + Destination IP and port information. + + + + + The HTTP method used in the request. + + + + + URL details. + + + + + Query details. + + + + + The headers found in the request. + + + + + Specifies whether or not the client requested HTTP keepalives. + + + + + Indicates whether or not chunked transfer encoding was detected. + + + + + Indicates whether or not the payload has been gzip compressed. + + + + + Indicates whether or not the payload has been deflate compressed. + + + + + The useragent specified in the request. + + + + + The content type as specified by the requestor (client). + + + + + The number of bytes in the request body. + + + + + The stream from which to read the request body sent by the requestor (client). + + + + + Retrieve the request body as a byte array. This will fully read the stream. + + + + + Retrieve the request body as a string. This will fully read the stream. + + + + + The original HttpListenerContext from which the HttpRequest was constructed. + + + + + HTTP request. + + + + + HTTP request. + Instantiate the object using an HttpListenerContext. + + HttpListenerContext. + + + + Retrieve a specified header value from either the headers or the querystring (case insensitive). + + + + + + + Determine if a header exists. + + Header key. + Specify whether a case sensitive search should be used. + True if exists. + + + + Determine if a querystring entry exists. + + Querystring key. + Specify whether a case sensitive search should be used. + True if exists. + + + + For chunked transfer-encoded requests, read the next chunk. + It is strongly recommended that you use the ChunkedTransfer parameter before invoking this method. + + Cancellation token useful for canceling the request. + Chunk. + + + + Read the data stream fully and convert the data to the object type specified using JSON deserialization. + Note: if you use this method, you will not be able to read from the data stream afterward. + + Type. + Object of type specified. + + + + Source details. + + + + + IP address of the requestor. + + + + + TCP port from which the request originated on the requestor. + + + + + Source details. + + + + + Source details. + + IP address of the requestor. + TCP port from which the request originated on the requestor. + + + + Destination details. + + + + + IP address to which the request was made. + + + + + TCP port on which the request was received. + + + + + Hostname to which the request was directed. + + + + + Hostname elements. + + + + + Destination details. + + + + + Source details. + + IP address to which the request was made. + TCP port on which the request was received. + Hostname. + + + + URL details. + + + + + Full URL. + + + + + Raw URL with query. + + + + + Raw URL without query. + + + + + Raw URL elements. + + + + + Parameters found within the URL, if using parameter routes. + + + + + URL details. + + + + + URL details. + + Full URL. + Raw URL. + + + + Query details. + + + + + Querystring, excluding the leading '?'. + + + + + Query elements. + + + + + Query details. + + + + + Query details. + + Full URL. + + + + HTTP response. + + + + + The HTTP status code to return to the requestor (client). + + + + + The HTTP status description to return to the requestor (client). + + + + + User-supplied headers to include in the response. + + + + + User-supplied content-type to include in the response. + + + + + The length of the supplied response data. + + + + + Indicates whether or not chunked transfer encoding should be indicated in the response. + + + + + Retrieve the response body sent using a Send() or SendAsync() method. + + + + + Retrieve the response body sent using a Send() or SendAsync() method. + + + + + Response data stream sent to the requestor. + + + + + Instantiate the object. + + + + + Send headers and no data to the requestor and terminate the connection. + + Cancellation token useful for canceling the request. + True if successful. + + + + Send headers with a specified content length and no data to the requestor and terminate the connection. Useful for HEAD requests where the content length must be set. + + Cancellation token useful for canceling the request. + Content length. + True if successful. + + + + Send headers and data to the requestor and terminate the connection. + + Data. + Cancellation token useful for canceling the request. + True if successful. + + + + Send headers and data to the requestor and terminate the connection. + + Data. + Cancellation token useful for canceling the request. + True if successful. + + + + Send headers and data to the requestor and terminate. + + Number of bytes to send. + Stream containing the data. + Cancellation token useful for canceling the request. + True if successful. + + + + Send headers (if not already sent) and a chunk of data using chunked transfer-encoding, and keep the connection in-tact. + + Chunk of data. + Number of bytes to send from the chunk, i.e. the actual data size (for example, return value of FileStream.ReadAsync(buffer, 0, buffer.Length)). + Cancellation token useful for canceling the request. + True if successful. + + + + Send headers (if not already sent) and the final chunk of data using chunked transfer-encoding and terminate the connection. + + Chunk of data./// + Number of bytes to send from the chunk, i.e. the actual data size (for example, return value of FileStream.ReadAsync(buffer, 0, buffer.Length)). + Cancellation token useful for canceling the request. + True if successful. + + + + Convert the response data sent using a Send() method to the object type specified using JSON deserialization. + + Type. + Object of type specified. + + + + MIME types and file extensions. + + + + + Instantiates the object. + + + + + Retrieve MIME type from file extension. + + File extension. + String containing MIME type. + + + + Object extensions. + + + + + Return a JSON string of the object. + + Object. + Enable or disable pretty print. + JSON string. + + + + Assign a method handler for when requests are received matching the supplied method and path containing parameters. + + + + + Globally-unique identifier. + + + + + The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + + + + + The pattern against which the raw URL should be matched. + + + + + The handler for the parameter route. + + + + + User-supplied metadata. + + + + + Create a new route object. + + The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + The pattern against which the raw URL should be matched. + The method that should be called to handle the request. + Globally-unique identifier. + User-supplied metadata. + + + + Attribute that is used to mark methods as a parameter route. + + + + + The path to match, i.e. /{version}/api/{id}. + If a match is found, the Dictionary found in HttpRequest.Url.Parameters will contain keys for 'version' and 'id'. + + + + + The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + + + + + Globally-unique identifier. + + + + + User-supplied metadata. + + + + + Instantiate the object. + + The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + The path to match, i.e. /{version}/api/{id}. + Globally-unique identifier. + User-supplied metadata. + + + + Parameter route manager. Parameter routes are used for requests using any HTTP method to any path where parameters are defined in the URL. + For example, /{version}/api. + For a matching URL, the HttpRequest.Url.Parameters will contain a key called 'version' with the value found in the URL. + + + + + Directly access the underlying URL matching library. + This is helpful in case you want to specify the matching behavior should multiple matches exist. + + + + + Instantiate the object. + + + + + Add a route. + + The HTTP method. + URL path, i.e. /path/to/resource. + Method to invoke. + Globally-unique identifier. + User-supplied metadata. + + + + Remove a route. + + The HTTP method. + URL path. + + + + Retrieve a parameter route. + + The HTTP method. + URL path. + ParameterRoute if the route exists, otherwise null. + + + + Check if a content route exists. + + The HTTP method. + URL path. + True if exists. + + + + Match a request method and URL to a handler method. + + The HTTP method. + URL path. + Values extracted from the URL. + Matching route. + True if match exists. + + + + Request event arguments. + + + + + IP address. + + + + + Port number. + + + + + HTTP method. + + + + + URL. + + + + + Query found in the URL. + + + + + Request headers. + + + + + Content length. + + + + + Response event arguments. + + + + + IP address. + + + + + Port number. + + + + + HTTP method. + + + + + URL. + + + + + Request query. + + + + + Request headers. + + + + + Content length. + + + + + Response status. + + + + + Response headers. + + + + + Response content length. + + + + + Total time in processing the request and sending the response, in milliseconds. + + + + + Route type. + + + + + Default route. + + + + + Content route. + + + + + Static route. + + + + + Parameter route. + + + + + Dynamic route. + + + + + Assign a method handler for when requests are received matching the supplied method and path. + + + + + Globally-unique identifier. + + + + + The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + + + + + The raw URL, i.e. /foo/bar/. Be sure this begins and ends with '/'. + + + + + The handler for the static route. + + + + + User-supplied metadata. + + + + + Create a new route object. + + The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + The raw URL, i.e. /foo/bar/. Be sure this begins and ends with '/'. + The method that should be called to handle the request. + Globally-unique identifier. + User-supplied metadata. + + + + Attribute that is used to mark methods as a static route. + + + + + The raw URL, i.e. /foo/bar/. Be sure this begins and ends with '/'. + + + + + The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + + + + + Globally-unique identifier. + + + + + User-supplied metadata. + + + + + Instantiate the object. + + The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + The raw URL, i.e. /foo/bar/. Be sure this begins and ends with '/'. + Globally-unique identifier. + User-supplied metadata. + + + + Static route manager. Static routes are used for requests using any HTTP method to a specific path. + + + + + Instantiate the object. + + + + + Add a route. + + The HTTP method. + URL path, i.e. /path/to/resource. + Method to invoke. + Globally-unique identifier. + User-supplied metadata. + + + + Remove a route. + + The HTTP method. + URL path. + + + + Retrieve a static route. + + The HTTP method. + URL path. + StaticRoute if the route exists, otherwise null. + + + + Check if a static route exists. + + The HTTP method. + URL path. + True if exists. + + + + Match a request method and URL to a handler method. + + The HTTP method. + URL path. + Matching route. + Method to invoke. + + + EonaCat webserver. - - - - Indicates whether or not the server is listening. - - - - - Number of requests being serviced currently. - - - - + + + + Indicates whether or not the server is listening. + + + + + Number of requests being serviced currently. + + + + EonaCat webserver settings. - - - + + + EonaCat webserver routes. - - - + + + EonaCat webserver statistics. - - - - Set specific actions/callbacks to use when events are raised. - - - - + + + + Set specific actions/callbacks to use when events are raised. + + + + Default pages served by the EonaCat webserver. - - - + + + Creates a new instance of the EonaCat webserver. If you do not provide a settings object, default settings will be used, which will cause the EonaCat Webserver to listen on http://127.0.0.1:8000, and send events to the console. - EonaCat webserver settings. - Method used when a request is received and no matching routes are found. Commonly used as the 404 handler when routes are used. - - - + EonaCat webserver settings. + Method used when a request is received and no matching routes are found. Commonly used as the 404 handler when routes are used. + + + Creates a new instance of the EonaCat webserver. - Hostname or IP address on which to listen. - TCP port on which to listen. - Specify whether or not SSL should be used (HTTPS). - Method used when a request is received and no matching routes are found. Commonly used as the 404 handler when routes are used. - - - + Hostname or IP address on which to listen. + TCP port on which to listen. + Specify whether or not SSL should be used (HTTPS). + Method used when a request is received and no matching routes are found. Commonly used as the 404 handler when routes are used. + + + Creates a new instance of the EonaCat webserver. - Hostnames or IP addresses on which to listen. Note: multiple listener endpoints are not supported on all platforms. - TCP port on which to listen. - Specify whether or not SSL should be used (HTTPS). - Method used when a request is received and no matching routes are found. Commonly used as the 404 handler when routes are used. - - - - Tear down the server and dispose of background workers. - Do not use this object after disposal. - - - - - Start accepting new connections. - - Cancellation token useful for canceling the server. - - - - Start accepting new connections. - - Cancellation token useful for canceling the server. - Task. - - - - Stop accepting new connections. - - - - - Tear down the server and dispose of background workers. - Do not use this object after disposal. - - - - - Callbacks/actions to use when various events are encountered. - - - - - Method to use for sending log messages. - - - - - Event to fire when a connection is received. - - - - - Event to fire when a request is received. - - - - - Event to fire when a request is denied due to access control. - - - - - Event to fire when a requestor disconnected unexpectedly. - - - - - Event to fire when a response is sent. - - - - - Event to fire when an exception is encountered. - - - - - Event to fire when the server is started. - - - - - Event to fire when the server is stopped. - - - - - Event to fire when the server is being disposed. - - - - - Instantiate the object. - - - - + Hostnames or IP addresses on which to listen. Note: multiple listener endpoints are not supported on all platforms. + TCP port on which to listen. + Specify whether or not SSL should be used (HTTPS). + Method used when a request is received and no matching routes are found. Commonly used as the 404 handler when routes are used. + + + + Tear down the server and dispose of background workers. + Do not use this object after disposal. + + + + + Start accepting new connections. + + Cancellation token useful for canceling the server. + + + + Start accepting new connections. + + Cancellation token useful for canceling the server. + Task. + + + + Stop accepting new connections. + + + + + Tear down the server and dispose of background workers. + Do not use this object after disposal. + + + + + Callbacks/actions to use when various events are encountered. + + + + + Method to use for sending log messages. + + + + + Event to fire when a connection is received. + + + + + Event to fire when a request is received. + + + + + Event to fire when a request is denied due to access control. + + + + + Event to fire when a requestor disconnected unexpectedly. + + + + + Event to fire when a response is sent. + + + + + Event to fire when an exception is encountered. + + + + + Event to fire when the server is started. + + + + + Event to fire when the server is stopped. + + + + + Event to fire when the server is being disposed. + + + + + Instantiate the object. + + + + Default pages served by the EonaCat webserver. - - - - Page displayed when sending a 404 due to a lack of a route. - - - - - Page displayed when sending a 500 due to an exception is unhandled within your routes. - - - - + + + + Page displayed when sending a 404 due to a lack of a route. + + + + + Page displayed when sending a 500 due to an exception is unhandled within your routes. + + + + Default pages served by the EonaCat webserver. - - - + + + Page served by the EonaCat webserver. - - - - Content type. - - - - - Content. - - - - + + + + Content type. + + + + + Content. + + + + Page served by the EonaCat webserver. - Content type. - Content. - - - + Content type. + Content. + + + EonaCat webserver routes. - - - - Function to call when a preflight (OPTIONS) request is received. - Often used to handle CORS. - Leave null to use the default OPTIONS handler. - - - - - Function to call prior to routing. - Return 'true' if the connection should be terminated. - Return 'false' to allow the connection to continue routing. - - - - - Content routes; i.e. routes to specific files or folders for GET and HEAD requests. - - - - - Handler for content route requests. - - - - - Static routes; i.e. routes with explicit matching and any HTTP method. - - - - - Parameter routes; i.e. routes with parameters embedded in the URL, such as /{version}/api/{id}. - - - - - Dynamic routes; i.e. routes with regex matching and any HTTP method. - - - - - Default route; used when no other routes match. - - - - - Instantiate the object using default settings. - - - - - Instantiate the object using default settings and the specified default route. - - - - + + + + Function to call when a preflight (OPTIONS) request is received. + Often used to handle CORS. + Leave null to use the default OPTIONS handler. + + + + + Function to call prior to routing. + Return 'true' if the connection should be terminated. + Return 'false' to allow the connection to continue routing. + + + + + Content routes; i.e. routes to specific files or folders for GET and HEAD requests. + + + + + Handler for content route requests. + + + + + Static routes; i.e. routes with explicit matching and any HTTP method. + + + + + Parameter routes; i.e. routes with parameters embedded in the URL, such as /{version}/api/{id}. + + + + + Dynamic routes; i.e. routes with regex matching and any HTTP method. + + + + + Default route; used when no other routes match. + + + + + Instantiate the object using default settings. + + + + + Instantiate the object using default settings and the specified default route. + + + + EonaCat webserver settings. - - - - Prefixes on which to listen. - - - - - Input-output settings. - - - - - SSL settings. - - - - - Headers that will be added to every response unless previously set. - - - - - Access control manager, i.e. default mode of operation, permit list, and deny list. - - - - - Debug logging settings. - Be sure to set Events.Logger in order to receive debug messages. - - - - + + + + Prefixes on which to listen. + + + + + Input-output settings. + + + + + SSL settings. + + + + + Headers that will be added to every response unless previously set. + + + + + Access control manager, i.e. default mode of operation, permit list, and deny list. + + + + + Debug logging settings. + Be sure to set Events.Logger in order to receive debug messages. + + + + EonaCat webserver settings. - - - + + + EonaCat webserver settings. - The hostname on which to listen. - The port on which to listen. - Enable or disable SSL. - - - + The hostname on which to listen. + The port on which to listen. + Enable or disable SSL. + + + EonaCat webserver settings. - The hostnames on which to listen. - The port on which to listen. - Enable or disable SSL. - - - - Input-output settings. - - - - - Buffer size to use when interacting with streams. - - - - - Maximum number of concurrent requests. - - - - - Input-output settings. - - - - - SSL settings. - - - - - Enable or disable SSL. - - - - - Require mutual authentication. - - - - - Accept invalid certificates including self-signed and those that are unable to be verified. - - - - - SSL settings. - - - - - Headers that will be added to every response unless previously set. - - - - - Access-Control-Allow-Origin header. - - - - - Access-Control-Allow-Methods header. - - - - - Access-Control-Allow-Headers header. - - - - - Access-Control-Expose-Headers header. - - - - - Accept header. - - - - - Accept-Language header. - - - - - Accept-Charset header. - - - - - Connection header. - - - - - Host header. - - - - - Headers that will be added to every response unless previously set. - - - - - Debug logging settings. - Be sure to set Events.Logger in order to receive debug messages. - - - - - Enable or disable debug logging of access control. - - - - - Enable or disable debug logging of routing. - - - - - Enable or disable debug logging of requests. - - - - - Enable or disable debug logging of responses. - - - - - Debug logging settings. - Be sure to set Events.Logger in order to receive debug messages. - - - - + The hostnames on which to listen. + The port on which to listen. + Enable or disable SSL. + + + + Input-output settings. + + + + + Buffer size to use when interacting with streams. + + + + + Maximum number of concurrent requests. + + + + + Input-output settings. + + + + + SSL settings. + + + + + Enable or disable SSL. + + + + + Require mutual authentication. + + + + + Accept invalid certificates including self-signed and those that are unable to be verified. + + + + + SSL settings. + + + + + Headers that will be added to every response unless previously set. + + + + + Access-Control-Allow-Origin header. + + + + + Access-Control-Allow-Methods header. + + + + + Access-Control-Allow-Headers header. + + + + + Access-Control-Expose-Headers header. + + + + + Accept header. + + + + + Accept-Language header. + + + + + Accept-Charset header. + + + + + Connection header. + + + + + Host header. + + + + + Headers that will be added to every response unless previously set. + + + + + Debug logging settings. + Be sure to set Events.Logger in order to receive debug messages. + + + + + Enable or disable debug logging of access control. + + + + + Enable or disable debug logging of routing. + + + + + Enable or disable debug logging of requests. + + + + + Enable or disable debug logging of responses. + + + + + Debug logging settings. + Be sure to set Events.Logger in order to receive debug messages. + + + + EonaCat webserver statistics. - - - - The time at which the client or server was started. - - - - - The amount of time which the client or server has been up. - - - - - The number of payload bytes received (incoming request body). - - - - - The number of payload bytes sent (outgoing request body). - - - - - Initialize the statistics object. - - - - - Human-readable version of the object. - - String. - - - - Reset statistics other than StartTime and UpTime. - - - - - Retrieve the number of requests received using a specific HTTP method. - - HTTP method. - Number of requests received using this method. - - + + + + The time at which the client or server was started. + + + + + The amount of time which the client or server has been up. + + + + + The number of payload bytes received (incoming request body). + + + + + The number of payload bytes sent (outgoing request body). + + + + + Initialize the statistics object. + + + + + Human-readable version of the object. + + String. + + + + Reset statistics other than StartTime and UpTime. + + + + + Retrieve the number of requests received using a specific HTTP method. + + HTTP method. + Number of requests received using this method. + + \ No newline at end of file diff --git a/EonaCat.Network/System/Web/EonaCatWebserverEvents.cs b/EonaCat.Network/System/Web/EonaCatWebserverEvents.cs index aa0d4c0..e09ba8d 100644 --- a/EonaCat.Network/System/Web/EonaCatWebserverEvents.cs +++ b/EonaCat.Network/System/Web/EonaCatWebserverEvents.cs @@ -1,136 +1,125 @@ using System; -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Callbacks/actions to use when various events are encountered. +/// +public class EonaCatWebserverEvents { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + /// + /// Method to use for sending log messages. + /// + public Action Logger = null; /// - /// Callbacks/actions to use when various events are encountered. + /// Instantiate the object. /// - public class EonaCatWebserverEvents + public EonaCatWebserverEvents() { - /// - /// Method to use for sending log messages. - /// - public Action Logger = null; + } - /// - /// Event to fire when a connection is received. - /// - public event EventHandler ConnectionReceived = delegate - { }; + /// + /// Event to fire when a connection is received. + /// + public event EventHandler ConnectionReceived = delegate { }; - /// - /// Event to fire when a request is received. - /// - public event EventHandler RequestReceived = delegate - { }; + /// + /// Event to fire when a request is received. + /// + public event EventHandler RequestReceived = delegate { }; - /// - /// Event to fire when a request is denied due to access control. - /// - public event EventHandler RequestDenied = delegate - { }; + /// + /// Event to fire when a request is denied due to access control. + /// + public event EventHandler RequestDenied = delegate { }; - /// - /// Event to fire when a requestor disconnected unexpectedly. - /// - public event EventHandler RequestorDisconnected = delegate - { }; + /// + /// Event to fire when a requestor disconnected unexpectedly. + /// + public event EventHandler RequestorDisconnected = delegate { }; - /// - /// Event to fire when a response is sent. - /// - public event EventHandler ResponseSent = delegate - { }; + /// + /// Event to fire when a response is sent. + /// + public event EventHandler ResponseSent = delegate { }; - /// - /// Event to fire when an exception is encountered. - /// - public event EventHandler ExceptionEncountered = delegate - { }; + /// + /// Event to fire when an exception is encountered. + /// + public event EventHandler ExceptionEncountered = delegate { }; - /// - /// Event to fire when the server is started. - /// - public event EventHandler ServerStarted = delegate - { }; + /// + /// Event to fire when the server is started. + /// + public event EventHandler ServerStarted = delegate { }; - /// - /// Event to fire when the server is stopped. - /// - public event EventHandler ServerStopped = delegate - { }; + /// + /// Event to fire when the server is stopped. + /// + public event EventHandler ServerStopped = delegate { }; - /// - /// Event to fire when the server is being disposed. - /// - public event EventHandler ServerDisposing = delegate - { }; + /// + /// Event to fire when the server is being disposed. + /// + public event EventHandler ServerDisposing = delegate { }; - /// - /// Instantiate the object. - /// - public EonaCatWebserverEvents() + internal void HandleConnectionReceived(object sender, ConnectionEventArgs args) + { + WrappedEventHandler(() => ConnectionReceived?.Invoke(sender, args), "ConnectionReceived", sender); + } + + internal void HandleRequestReceived(object sender, RequestEventArgs args) + { + WrappedEventHandler(() => RequestReceived?.Invoke(sender, args), "RequestReceived", sender); + } + + internal void HandleRequestDenied(object sender, RequestEventArgs args) + { + WrappedEventHandler(() => RequestDenied?.Invoke(sender, args), "RequestDenied", sender); + } + + internal void HandleResponseSent(object sender, ResponseEventArgs args) + { + WrappedEventHandler(() => ResponseSent?.Invoke(sender, args), "ResponseSent", sender); + } + + internal void HandleExceptionEncountered(object sender, ExceptionEventArgs args) + { + WrappedEventHandler(() => ExceptionEncountered?.Invoke(sender, args), "ExceptionEncountered", sender); + } + + internal void HandleServerStarted(object sender, EventArgs args) + { + WrappedEventHandler(() => ServerStarted?.Invoke(sender, args), "ServerStarted", sender); + } + + internal void HandleServerStopped(object sender, EventArgs args) + { + WrappedEventHandler(() => ServerStopped?.Invoke(sender, args), "ServerStopped", sender); + } + + internal void HandleServerDisposing(object sender, EventArgs args) + { + WrappedEventHandler(() => ServerDisposing?.Invoke(sender, args), "ServerDisposing", sender); + } + + private void WrappedEventHandler(Action action, string handler, object sender) + { + if (action == null) { + return; } - internal void HandleConnectionReceived(object sender, ConnectionEventArgs args) + try { - WrappedEventHandler(() => ConnectionReceived?.Invoke(sender, args), "ConnectionReceived", sender); + action.Invoke(); } - - internal void HandleRequestReceived(object sender, RequestEventArgs args) + catch (Exception e) { - 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)); - } + Logger?.Invoke("Event handler exception in " + handler + ": " + Environment.NewLine + e.ToJson(true)); } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/EonaCatWebserverPages.cs b/EonaCat.Network/System/Web/EonaCatWebserverPages.cs index 4651d20..38c805f 100644 --- a/EonaCat.Network/System/Web/EonaCatWebserverPages.cs +++ b/EonaCat.Network/System/Web/EonaCatWebserverPages.cs @@ -1,100 +1,92 @@ using System; -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Default pages served by the EonaCat webserver. +/// +public class EonaCatWebserverPages { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private Page _Default404Page = new("text/plain", "Not found"); + private Page _Default500Page = new("text/plain", "Internal server error"); /// - /// Default pages served by the EonaCat webserver. + /// Default pages served by the EonaCat webserver. /// - public class EonaCatWebserverPages + public EonaCatWebserverPages() { - /// - /// Page displayed when sending a 404 due to a lack of a route. - /// - public Page Default404Page + } + + /// + /// Page displayed when sending a 404 due to a lack of a route. + /// + public Page Default404Page + { + get => _Default404Page; + set { - get + if (value == null) { - return _Default404Page; + throw new ArgumentNullException(nameof(Default404Page)); } - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(Default404Page)); - } - _Default404Page = value; - } - } - - /// - /// Page displayed when sending a 500 due to an exception is unhandled within your routes. - /// - public Page Default500Page - { - get - { - return _Default500Page; - } - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(Default500Page)); - } - - _Default500Page = value; - } - } - - private Page _Default404Page = new Page("text/plain", "Not found"); - private Page _Default500Page = new Page("text/plain", "Internal server error"); - - /// - /// Default pages served by the EonaCat webserver. - /// - public EonaCatWebserverPages() - { - } - - /// - /// Page served by the EonaCat webserver. - /// - public class Page - { - /// - /// Content type. - /// - public string ContentType { get; private set; } = null; - - /// - /// Content. - /// - public string Content { get; private set; } = null; - - /// - /// Page served by the EonaCat webserver. - /// - /// Content type. - /// Content. - public Page(string contentType, string content) - { - if (string.IsNullOrEmpty(contentType)) - { - throw new ArgumentNullException(nameof(contentType)); - } - - if (string.IsNullOrEmpty(content)) - { - throw new ArgumentNullException(nameof(content)); - } - - ContentType = contentType; - Content = content; - } + _Default404Page = value; } } + + /// + /// Page displayed when sending a 500 due to an exception is unhandled within your routes. + /// + public Page Default500Page + { + get => _Default500Page; + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(Default500Page)); + } + + _Default500Page = value; + } + } + + /// + /// Page served by the EonaCat webserver. + /// + public class Page + { + /// + /// Page served by the EonaCat webserver. + /// + /// Content type. + /// Content. + public Page(string contentType, string content) + { + if (string.IsNullOrEmpty(contentType)) + { + throw new ArgumentNullException(nameof(contentType)); + } + + if (string.IsNullOrEmpty(content)) + { + throw new ArgumentNullException(nameof(content)); + } + + ContentType = contentType; + Content = content; + } + + /// + /// Content type. + /// + public string ContentType { get; private set; } + + /// + /// Content. + /// + public string Content { get; private set; } + } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/EonaCatWebserverRoutes.cs b/EonaCat.Network/System/Web/EonaCatWebserverRoutes.cs index 358fc4f..5379509 100644 --- a/EonaCat.Network/System/Web/EonaCatWebserverRoutes.cs +++ b/EonaCat.Network/System/Web/EonaCatWebserverRoutes.cs @@ -1,245 +1,224 @@ using System; -using System.Collections.Generic; using System.Threading.Tasks; -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// EonaCat webserver routes. +/// +public class EonaCatWebserverRoutes { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private readonly EonaCatWebserverSettings _Settings = new(); + private ContentRouteManager _Content = new(); + private ContentRouteHandler _ContentHandler; + private DynamicRouteManager _Dynamic = new(); + private ParameterRouteManager _Parameter = new(); + private Func _Preflight; + + private StaticRouteManager _Static = new(); /// - /// EonaCat webserver routes. + /// Function to call prior to routing. + /// Return 'true' if the connection should be terminated. + /// Return 'false' to allow the connection to continue routing. /// - public class EonaCatWebserverRoutes + public Func> PreRouting = null; + + /// + /// Instantiate the object using default settings. + /// + public EonaCatWebserverRoutes() { - /// - /// Function to call when a preflight (OPTIONS) request is received. - /// Often used to handle CORS. - /// Leave null to use the default OPTIONS handler. - /// - public Func Preflight + _Preflight = PreflightInternal; + _ContentHandler = new ContentRouteHandler(_Content); + } + + /// + /// Instantiate the object using default settings and the specified default route. + /// + public EonaCatWebserverRoutes(EonaCatWebserverSettings settings, Func defaultRoute) + { + settings ??= new EonaCatWebserverSettings(); + + if (defaultRoute == null) { - get - { - return _Preflight; - } - set - { - if (value == null) - { - _Preflight = PreflightInternal; - } - else - { - _Preflight = value; - } - } + throw new ArgumentNullException(nameof(defaultRoute)); } - /// - /// Function to call prior to routing. - /// Return 'true' if the connection should be terminated. - /// Return 'false' to allow the connection to continue routing. - /// - public Func> PreRouting = null; + _Settings = settings; + _Preflight = PreflightInternal; + Default = defaultRoute; + _ContentHandler = new ContentRouteHandler(_Content); + } - /// - /// Content routes; i.e. routes to specific files or folders for GET and HEAD requests. - /// - public ContentRouteManager Content + /// + /// Function to call when a preflight (OPTIONS) request is received. + /// Often used to handle CORS. + /// Leave null to use the default OPTIONS handler. + /// + public Func Preflight + { + get => _Preflight; + set { - get + if (value == null) { - return _Content; + _Preflight = PreflightInternal; } - set + else { - if (value == null) - { - throw new ArgumentNullException(nameof(Content)); - } - - _Content = value; + _Preflight = value; } } - - /// - /// Handler for content route requests. - /// - public ContentRouteHandler ContentHandler - { - get - { - return _ContentHandler; - } - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(ContentHandler)); - } - - _ContentHandler = value; - } - } - - /// - /// Static routes; i.e. routes with explicit matching and any HTTP method. - /// - public StaticRouteManager Static - { - get - { - return _Static; - } - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(Static)); - } - - _Static = value; - } - } - - /// - /// Parameter routes; i.e. routes with parameters embedded in the URL, such as /{version}/api/{id}. - /// - public ParameterRouteManager Parameter - { - get - { - return _Parameter; - } - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(Parameter)); - } - - _Parameter = value; - } - } - - /// - /// Dynamic routes; i.e. routes with regex matching and any HTTP method. - /// - public DynamicRouteManager Dynamic - { - get - { - return _Dynamic; - } - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(Dynamic)); - } - - _Dynamic = value; - } - } - - /// - /// Default route; used when no other routes match. - /// - public Func Default { get; set; } = null; - - private readonly EonaCatWebserverSettings _Settings = new EonaCatWebserverSettings(); - private ContentRouteManager _Content = new ContentRouteManager(); - private ContentRouteHandler _ContentHandler = null; - - private StaticRouteManager _Static = new StaticRouteManager(); - private ParameterRouteManager _Parameter = new ParameterRouteManager(); - private DynamicRouteManager _Dynamic = new DynamicRouteManager(); - private Func _Preflight = null; - - /// - /// Instantiate the object using default settings. - /// - public EonaCatWebserverRoutes() - { - _Preflight = PreflightInternal; - _ContentHandler = new ContentRouteHandler(_Content); - } - - /// - /// Instantiate the object using default settings and the specified default route. - /// - public EonaCatWebserverRoutes(EonaCatWebserverSettings settings, Func defaultRoute) - { - 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 context) - { - context.Response.StatusCode = 200; - - string[] requestedHeaders = null; - if (context.Request.Headers != null) - { - foreach (KeyValuePair curr in context.Request.Headers) - { - if (string.IsNullOrEmpty(curr.Key)) - { - continue; - } - - if (string.IsNullOrEmpty(curr.Value)) - { - continue; - } - - if (string.Compare(curr.Key.ToLower(), "access-control-request-headers") == 0) - { - requestedHeaders = curr.Value.Split(','); - break; - } - } - } - - string headers = ""; - - if (requestedHeaders != null) - { - int addedCount = 0; - foreach (string curr in requestedHeaders) - { - if (string.IsNullOrEmpty(curr)) - { - continue; - } - - if (addedCount > 0) - { - headers += ", "; - } - - headers += ", " + curr; - addedCount++; - } - } - - foreach (KeyValuePair header in _Settings.Headers) - { - context.Response.Headers.Add(header.Key, header.Value); - } - - context.Response.ContentLength = 0; - await context.Response.Send().ConfigureAwait(false); - } + } + + /// + /// Content routes; i.e. routes to specific files or folders for GET and HEAD requests. + /// + public ContentRouteManager Content + { + get => _Content; + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(Content)); + } + + _Content = value; + } + } + + /// + /// Handler for content route requests. + /// + public ContentRouteHandler ContentHandler + { + get => _ContentHandler; + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(ContentHandler)); + } + + _ContentHandler = value; + } + } + + /// + /// Static routes; i.e. routes with explicit matching and any HTTP method. + /// + public StaticRouteManager Static + { + get => _Static; + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(Static)); + } + + _Static = value; + } + } + + /// + /// Parameter routes; i.e. routes with parameters embedded in the URL, such as /{version}/api/{id}. + /// + public ParameterRouteManager Parameter + { + get => _Parameter; + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(Parameter)); + } + + _Parameter = value; + } + } + + /// + /// Dynamic routes; i.e. routes with regex matching and any HTTP method. + /// + public DynamicRouteManager Dynamic + { + get => _Dynamic; + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(Dynamic)); + } + + _Dynamic = value; + } + } + + /// + /// Default route; used when no other routes match. + /// + public Func Default { get; set; } + + private async Task PreflightInternal(HttpContext context) + { + context.Response.StatusCode = 200; + + string[] requestedHeaders = null; + if (context.Request.Headers != null) + { + foreach (var curr in context.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; + } + } + } + + var headers = ""; + + if (requestedHeaders != null) + { + var addedCount = 0; + foreach (var curr in requestedHeaders) + { + if (string.IsNullOrEmpty(curr)) + { + continue; + } + + if (addedCount > 0) + { + headers += ", "; + } + + headers += ", " + curr; + addedCount++; + } + } + + foreach (var header in _Settings.Headers) + { + context.Response.Headers.Add(header.Key, header.Value); + } + + context.Response.ContentLength = 0; + await context.Response.Send().ConfigureAwait(false); } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/EonaCatWebserverSettings.cs b/EonaCat.Network/System/Web/EonaCatWebserverSettings.cs index 15a9625..0bf7208 100644 --- a/EonaCat.Network/System/Web/EonaCatWebserverSettings.cs +++ b/EonaCat.Network/System/Web/EonaCatWebserverSettings.cs @@ -1,185 +1,91 @@ using System; using System.Collections.Generic; -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// EonaCat webserver settings. +/// +public class EonaCatWebserverSettings { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private AccessControlManager _AccessControl = new(AccessControlMode.DefaultPermit); + private DebugSettings _Debug = new(); + + private Dictionary _Headers = new() + { + { "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" } + }; + + private IOSettings _IO = new(); + + private List _Prefixes = new(); + private SslSettings _Ssl = new(); /// - /// EonaCat webserver settings. + /// EonaCat webserver settings. /// - public class EonaCatWebserverSettings + public EonaCatWebserverSettings() { - /// - /// Prefixes on which to listen. - /// - public List Prefixes + } + + /// + /// EonaCat webserver settings. + /// + /// The hostname on which to listen. + /// The port on which to listen. + /// Enable or disable SSL. + public EonaCatWebserverSettings(string hostname, int port, bool ssl = false) + { + if (string.IsNullOrEmpty(hostname)) { - 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; - } + hostname = "localhost"; } - /// - /// Input-output settings. - /// - public IOSettings IO + if (port < 0) { - get - { - return _IO; - } - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(IO)); - } - - _IO = value; - } + throw new ArgumentOutOfRangeException(nameof(port)); } - /// - /// SSL settings. - /// - public SslSettings Ssl + var prefix = "http"; + if (ssl) { - get - { - return _Ssl; - } - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(Ssl)); - } - - _Ssl = value; - } + prefix += "s://" + hostname + ":" + port + "/"; + } + else + { + prefix += "://" + hostname + ":" + port + "/"; } - /// - /// Headers that will be added to every response unless previously set. - /// - public Dictionary Headers - { - get - { - return _Headers; - } - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(Headers)); - } + _Prefixes.Add(prefix); + _Ssl.Enable = ssl; + } - _Headers = value; - } + /// + /// EonaCat webserver settings. + /// + /// The hostnames on which to listen. + /// The port on which to listen. + /// Enable or disable SSL. + public EonaCatWebserverSettings(List hostnames, int port, bool ssl = false) + { + hostnames ??= new List { "localhost" }; + + if (port < 0) + { + throw new ArgumentOutOfRangeException(nameof(port)); } - /// - /// Access control manager, i.e. default mode of operation, permit list, and deny list. - /// - public AccessControlManager AccessControl + foreach (var hostname in hostnames) { - get - { - return _AccessControl; - } - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(AccessControl)); - } - - _AccessControl = value; - } - } - - /// - /// Debug logging settings. - /// Be sure to set Events.Logger in order to receive debug messages. - /// - public DebugSettings Debug - { - get - { - return _Debug; - } - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(Debug)); - } - - _Debug = value; - } - } - - private List _Prefixes = new List(); - private IOSettings _IO = new IOSettings(); - private SslSettings _Ssl = new SslSettings(); - private AccessControlManager _AccessControl = new AccessControlManager(AccessControlMode.DefaultPermit); - private DebugSettings _Debug = new DebugSettings(); - - private Dictionary _Headers = new Dictionary - { - { "Access-Control-Allow-Origin", "*" }, - { "Access-Control-Allow-Methods", "OPTIONS, HEAD, GET, PUT, POST, DELETE" }, - { "Access-Control-Allow-Headers", "*" }, - { "Accept", "*/*" }, - { "Accept-Language", "en-US, en" }, - { "Accept-Charset", "ISO-8859-1, utf-8" }, - { "Connection", "close" } - }; - - /// - /// EonaCat webserver settings. - /// - public EonaCatWebserverSettings() - { - } - - /// - /// EonaCat webserver settings. - /// - /// The hostname on which to listen. - /// The port on which to listen. - /// Enable or disable SSL. - public EonaCatWebserverSettings(string hostname, int port, bool ssl = false) - { - if (string.IsNullOrEmpty(hostname)) - { - hostname = "localhost"; - } - - if (port < 0) - { - throw new ArgumentOutOfRangeException(nameof(port)); - } - - string prefix = "http"; + var prefix = "http"; if (ssl) { prefix += "s://" + hostname + ":" + port + "/"; @@ -190,217 +96,288 @@ namespace EonaCat.Network } _Prefixes.Add(prefix); - _Ssl.Enable = ssl; + } + + _Ssl.Enable = ssl; + } + + /// + /// Prefixes on which to listen. + /// + public List Prefixes + { + get => _Prefixes; + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(Prefixes)); + } + + if (value.Count < 1) + { + throw new ArgumentException("At least one prefix must be specified."); + } + + _Prefixes = value; + } + } + + /// + /// Input-output settings. + /// + public IOSettings IO + { + get => _IO; + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(IO)); + } + + _IO = value; + } + } + + /// + /// SSL settings. + /// + public SslSettings Ssl + { + get => _Ssl; + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(Ssl)); + } + + _Ssl = value; + } + } + + /// + /// Headers that will be added to every response unless previously set. + /// + public Dictionary Headers + { + get => _Headers; + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(Headers)); + } + + _Headers = value; + } + } + + /// + /// Access control manager, i.e. default mode of operation, permit list, and deny list. + /// + public AccessControlManager AccessControl + { + get => _AccessControl; + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(AccessControl)); + } + + _AccessControl = value; + } + } + + /// + /// Debug logging settings. + /// Be sure to set Events.Logger in order to receive debug messages. + /// + public DebugSettings Debug + { + get => _Debug; + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(Debug)); + } + + _Debug = value; + } + } + + /// + /// Input-output settings. + /// + public class IOSettings + { + private int _MaxRequests = 1024; + + private int _StreamBufferSize = 65536; + + /// + /// Input-output settings. + /// + public IOSettings() + { } /// - /// EonaCat webserver settings. + /// Buffer size to use when interacting with streams. /// - /// The hostnames on which to listen. - /// The port on which to listen. - /// Enable or disable SSL. - public EonaCatWebserverSettings(List hostnames, int port, bool ssl = false) + public int StreamBufferSize { - hostnames ??= new List { "localhost" }; - - if (port < 0) + get => _StreamBufferSize; + set { - throw new ArgumentOutOfRangeException(nameof(port)); - } - - foreach (string hostname in hostnames) - { - string prefix = "http"; - if (ssl) + if (value < 1) { - prefix += "s://" + hostname + ":" + port + "/"; - } - else - { - prefix += "://" + hostname + ":" + port + "/"; + throw new ArgumentOutOfRangeException(nameof(StreamBufferSize)); } - _Prefixes.Add(prefix); - } - - _Ssl.Enable = ssl; - } - - /// - /// Input-output settings. - /// - public class IOSettings - { - /// - /// Buffer size to use when interacting with streams. - /// - public int StreamBufferSize - { - get - { - return _StreamBufferSize; - } - set - { - if (value < 1) - { - throw new ArgumentOutOfRangeException(nameof(StreamBufferSize)); - } - - _StreamBufferSize = value; - } - } - - /// - /// Maximum number of concurrent requests. - /// - public int MaxRequests - { - get - { - return _MaxRequests; - } - set - { - if (value < 1) - { - throw new ArgumentException("Maximum requests must be greater than zero."); - } - - _MaxRequests = value; - } - } - - private int _StreamBufferSize = 65536; - private int _MaxRequests = 1024; - - /// - /// Input-output settings. - /// - public IOSettings() - { + _StreamBufferSize = value; } } /// - /// SSL settings. + /// Maximum number of concurrent requests. /// - public class SslSettings + public int MaxRequests { - /// - /// Enable or disable SSL. - /// - public bool Enable = false; - - /// - /// Require mutual authentication. - /// - public bool MutuallyAuthenticate = false; - - /// - /// Accept invalid certificates including self-signed and those that are unable to be verified. - /// - public bool AcceptInvalidAcertificates = true; - - /// - /// SSL settings. - /// - internal SslSettings() + get => _MaxRequests; + set { - } - } + if (value < 1) + { + throw new ArgumentException("Maximum requests must be greater than zero."); + } - /// - /// Headers that will be added to every response unless previously set. - /// - public class HeaderSettings - { - /// - /// Access-Control-Allow-Origin header. - /// - public string AccessControlAllowOrigin = "*"; - - /// - /// Access-Control-Allow-Methods header. - /// - public string AccessControlAllowMethods = "OPTIONS, HEAD, GET, PUT, POST, DELETE"; - - /// - /// Access-Control-Allow-Headers header. - /// - public string AccessControlAllowHeaders = "*"; - - /// - /// Access-Control-Expose-Headers header. - /// - public string AccessControlExposeHeaders = ""; - - /// - /// Accept header. - /// - public string Accept = "*/*"; - - /// - /// Accept-Language header. - /// - public string AcceptLanguage = "en-US, en"; - - /// - /// Accept-Charset header. - /// - public string AcceptCharset = "ISO-8859-1, utf-8"; - - /// - /// Connection header. - /// - public string Connection = "close"; - - /// - /// Host header. - /// - public string Host = null; - - /// - /// Headers that will be added to every response unless previously set. - /// - public HeaderSettings() - { - } - } - - /// - /// Debug logging settings. - /// Be sure to set Events.Logger in order to receive debug messages. - /// - public class DebugSettings - { - /// - /// Enable or disable debug logging of access control. - /// - public bool AccessControl = false; - - /// - /// Enable or disable debug logging of routing. - /// - public bool Routing = false; - - /// - /// Enable or disable debug logging of requests. - /// - public bool Requests = false; - - /// - /// Enable or disable debug logging of responses. - /// - public bool Responses = false; - - /// - /// Debug logging settings. - /// Be sure to set Events.Logger in order to receive debug messages. - /// - public DebugSettings() - { + _MaxRequests = value; } } } + + /// + /// SSL settings. + /// + public class SslSettings + { + /// + /// Accept invalid certificates including self-signed and those that are unable to be verified. + /// + public bool AcceptInvalidAcertificates = true; + + /// + /// Enable or disable SSL. + /// + public bool Enable; + + /// + /// Require mutual authentication. + /// + public bool MutuallyAuthenticate = false; + + /// + /// SSL settings. + /// + internal SslSettings() + { + } + } + + /// + /// Headers that will be added to every response unless previously set. + /// + public class HeaderSettings + { + /// + /// Accept header. + /// + public string Accept = "*/*"; + + /// + /// Accept-Charset header. + /// + public string AcceptCharset = "ISO-8859-1, utf-8"; + + /// + /// Accept-Language header. + /// + public string AcceptLanguage = "en-US, en"; + + /// + /// Access-Control-Allow-Headers header. + /// + public string AccessControlAllowHeaders = "*"; + + /// + /// Access-Control-Allow-Methods header. + /// + public string AccessControlAllowMethods = "OPTIONS, HEAD, GET, PUT, POST, DELETE"; + + /// + /// Access-Control-Allow-Origin header. + /// + public string AccessControlAllowOrigin = "*"; + + /// + /// Access-Control-Expose-Headers header. + /// + public string AccessControlExposeHeaders = ""; + + /// + /// Connection header. + /// + public string Connection = "close"; + + /// + /// Host header. + /// + public string Host = null; + + /// + /// Headers that will be added to every response unless previously set. + /// + public HeaderSettings() + { + } + } + + /// + /// Debug logging settings. + /// Be sure to set Events.Logger in order to receive debug messages. + /// + public class DebugSettings + { + /// + /// Enable or disable debug logging of access control. + /// + public bool AccessControl = false; + + /// + /// Enable or disable debug logging of requests. + /// + public bool Requests = false; + + /// + /// Enable or disable debug logging of responses. + /// + public bool Responses = false; + + /// + /// Enable or disable debug logging of routing. + /// + public bool Routing = false; + + /// + /// Debug logging settings. + /// Be sure to set Events.Logger in order to receive debug messages. + /// + public DebugSettings() + { + } + } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/EonaCatWebserverStatistics.cs b/EonaCat.Network/System/Web/EonaCatWebserverStatistics.cs index 3d9d1a5..8be0abf 100644 --- a/EonaCat.Network/System/Web/EonaCatWebserverStatistics.cs +++ b/EonaCat.Network/System/Web/EonaCatWebserverStatistics.cs @@ -2,148 +2,132 @@ using System.Text; using System.Threading; -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// EonaCat webserver statistics. +/// +public class EonaCatWebserverStatistics { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private readonly long[] _RequestsByMethod; // _RequestsByMethod[(int)HttpMethod.Xyz] = Count + + private long _ReceivedPayloadBytes; + private long _SentPayloadBytes; /// - /// EonaCat webserver statistics. + /// Initialize the statistics object. /// - public class EonaCatWebserverStatistics + public EonaCatWebserverStatistics() { - /// - /// The time at which the client or server was started. - /// - public DateTime StartTime { get; } = DateTime.Now.ToUniversalTime(); - - /// - /// The amount of time which the client or server has been up. - /// - public TimeSpan UpTime => DateTime.Now.ToUniversalTime() - StartTime; - - /// - /// The number of payload bytes received (incoming request body). - /// - public long ReceivedPayloadBytes + // Calculating the length for _RequestsByMethod array + var max = 0; + foreach (var value in Enum.GetValues(typeof(HttpMethod))) { - get + if ((int)value > max) { - return _ReceivedPayloadBytes; - } - internal set - { - _ReceivedPayloadBytes = value; + max = (int)value; } } - /// - /// The number of payload bytes sent (outgoing request body). - /// - public long SentPayloadBytes + _RequestsByMethod = new long[max + 1]; + } + + /// + /// The time at which the client or server was started. + /// + public DateTime StartTime { get; } = DateTime.Now.ToUniversalTime(); + + /// + /// The amount of time which the client or server has been up. + /// + public TimeSpan UpTime => DateTime.Now.ToUniversalTime() - StartTime; + + /// + /// The number of payload bytes received (incoming request body). + /// + public long ReceivedPayloadBytes + { + get => _ReceivedPayloadBytes; + internal set => _ReceivedPayloadBytes = value; + } + + /// + /// The number of payload bytes sent (outgoing request body). + /// + public long SentPayloadBytes + { + get => _SentPayloadBytes; + internal set => _SentPayloadBytes = value; + } + + /// + /// Human-readable version of the object. + /// + /// String. + public override string ToString() + { + var 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 : "); + + var foundAtLeastOne = false; + for (var i = 0; i < _RequestsByMethod.Length; i++) { - get + var count = Interlocked.Read(ref _RequestsByMethod[i]); + if (count > 0) { - return _SentPayloadBytes; - } - internal set - { - _SentPayloadBytes = value; + foundAtLeastOne = true; + sb.AppendLine($" {((HttpMethod)i).ToString().PadRight(18)} : {count.ToString("N0")}"); } } - private long _ReceivedPayloadBytes = 0; - private long _SentPayloadBytes = 0; - private readonly long[] _RequestsByMethod; // _RequestsByMethod[(int)HttpMethod.Xyz] = Count - - /// - /// Initialize the statistics object. - /// - public EonaCatWebserverStatistics() + if (!foundAtLeastOne) { - // 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]; + sb.AppendLine(" (none)"); } - /// - /// Human-readable version of the object. - /// - /// String. - public override string ToString() - { - StringBuilder sb = new StringBuilder(); - sb.AppendLine($"--- Statistics ---"); - sb.AppendLine($" Start Time : {StartTime}"); - sb.AppendLine($" Up Time : {UpTime}"); - sb.AppendLine($" Received Payload Bytes : {ReceivedPayloadBytes.ToString("N0")} bytes"); - sb.AppendLine($" Sent Payload Bytes : {SentPayloadBytes.ToString("N0")} bytes"); - sb.AppendLine($" Requests By Method : "); + return sb.ToString(); + } - 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")}"); - } - } + /// + /// Reset statistics other than StartTime and UpTime. + /// + public void Reset() + { + Interlocked.Exchange(ref _ReceivedPayloadBytes, 0); + Interlocked.Exchange(ref _SentPayloadBytes, 0); - if (!foundAtLeastOne) - { - sb.AppendLine(" (none)"); - } + for (var i = 0; i < _RequestsByMethod.Length; i++) Interlocked.Exchange(ref _RequestsByMethod[i], 0); + } - return sb.ToString(); - } + /// + /// Retrieve the number of requests received using a specific HTTP method. + /// + /// HTTP method. + /// Number of requests received using this method. + public long RequestCountByMethod(HttpMethod method) + { + return Interlocked.Read(ref _RequestsByMethod[(int)method]); + } - /// - /// Reset statistics other than StartTime and UpTime. - /// - public void Reset() - { - Interlocked.Exchange(ref _ReceivedPayloadBytes, 0); - Interlocked.Exchange(ref _SentPayloadBytes, 0); + internal void IncrementRequestCounter(HttpMethod method) + { + Interlocked.Increment(ref _RequestsByMethod[(int)method]); + } - for (int i = 0; i < _RequestsByMethod.Length; i++) - { - Interlocked.Exchange(ref _RequestsByMethod[i], 0); - } - } + internal void IncrementReceivedPayloadBytes(long len) + { + Interlocked.Add(ref _ReceivedPayloadBytes, len); + } - /// - /// Retrieve the number of requests received using a specific HTTP method. - /// - /// HTTP method. - /// Number of requests received using this method. - public long RequestCountByMethod(HttpMethod method) - { - return Interlocked.Read(ref _RequestsByMethod[(int)method]); - } - - internal void IncrementRequestCounter(HttpMethod method) - { - Interlocked.Increment(ref _RequestsByMethod[(int)method]); - } - - internal void IncrementReceivedPayloadBytes(long len) - { - Interlocked.Add(ref _ReceivedPayloadBytes, len); - } - - internal void IncrementSentPayloadBytes(long len) - { - Interlocked.Add(ref _SentPayloadBytes, len); - } + internal void IncrementSentPayloadBytes(long len) + { + Interlocked.Add(ref _SentPayloadBytes, len); } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/EventArgs/ConnectionReceivedEventArgs.cs b/EonaCat.Network/System/Web/EventArgs/ConnectionReceivedEventArgs.cs index b48f52c..5f60e4f 100644 --- a/EonaCat.Network/System/Web/EventArgs/ConnectionReceivedEventArgs.cs +++ b/EonaCat.Network/System/Web/EventArgs/ConnectionReceivedEventArgs.cs @@ -1,44 +1,42 @@ using System; -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Connection event arguments. +/// +public class ConnectionEventArgs : EventArgs { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + /// + /// Connection event arguments. + /// + /// Requestor IP address. + /// Request TCP port. + public ConnectionEventArgs(string ip, int port) + { + if (string.IsNullOrEmpty(ip)) + { + throw new ArgumentNullException(nameof(ip)); + } + + if (port < 0) + { + throw new ArgumentOutOfRangeException(nameof(port)); + } + + Ip = ip; + Port = port; + } /// - /// Connection event arguments. + /// Requestor IP address. /// - public class ConnectionEventArgs : EventArgs - { - /// - /// Requestor IP address. - /// - public string Ip { get; private set; } = null; + public string Ip { get; private set; } - /// - /// Request TCP port. - /// - public int Port { get; private set; } = 0; - - /// - /// Connection event arguments. - /// - /// Requestor IP address. - /// Request TCP port. - public ConnectionEventArgs(string ip, int port) - { - if (string.IsNullOrEmpty(ip)) - { - throw new ArgumentNullException(nameof(ip)); - } - - if (port < 0) - { - throw new ArgumentOutOfRangeException(nameof(port)); - } - - Ip = ip; - Port = port; - } - } + /// + /// Request TCP port. + /// + public int Port { get; private set; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/EventArgs/ExceptionEventArgs.cs b/EonaCat.Network/System/Web/EventArgs/ExceptionEventArgs.cs index 2b84989..4795837 100644 --- a/EonaCat.Network/System/Web/EventArgs/ExceptionEventArgs.cs +++ b/EonaCat.Network/System/Web/EventArgs/ExceptionEventArgs.cs @@ -1,103 +1,101 @@ using System; using System.Collections.Generic; -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Exception event arguments. +/// +public class ExceptionEventArgs : EventArgs { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - - /// - /// Exception event arguments. - /// - public class ExceptionEventArgs : EventArgs + internal ExceptionEventArgs(HttpContext context, Exception e) { - /// - /// IP address. - /// - public string Ip { get; private set; } = null; - - /// - /// Port number. - /// - public int Port { get; private set; } = 0; - - /// - /// HTTP method. - /// - public HttpMethod Method { get; private set; } = HttpMethod.GET; - - /// - /// URL. - /// - public string Url { get; private set; } = null; - - /// - /// Request query. - /// - public Dictionary Query { get; private set; } = new Dictionary(); - - /// - /// Request headers. - /// - public Dictionary RequestHeaders { get; private set; } = new Dictionary(); - - /// - /// Content length. - /// - public long RequestContentLength { get; private set; } = 0; - - /// - /// Response status. - /// - public int StatusCode { get; private set; } = 0; - - /// - /// Response headers. - /// - public Dictionary ResponseHeaders { get; private set; } = new Dictionary(); - - /// - /// Response content length. - /// - public long? ResponseContentLength { get; private set; } = 0; - - /// - /// Exception. - /// - public Exception Exception { get; private set; } = null; - - /// - /// JSON string of the Exception. - /// - public string Json + if (context != null) { - get - { - if (Exception != null) - { - return Exception.ToJson(true); - } - - return null; - } + Ip = context.Request.Source.IpAddress; + Port = context.Request.Source.Port; + Method = context.Request.Method; + Url = context.Request.Url.Full; + Query = context.Request.Query.Elements; + RequestHeaders = context.Request.Headers; + RequestContentLength = context.Request.ContentLength; + StatusCode = context.Response.StatusCode; + ResponseContentLength = context.Response.ContentLength; } - internal ExceptionEventArgs(HttpContext context, Exception e) + Exception = e; + } + + /// + /// IP address. + /// + public string Ip { get; private set; } + + /// + /// Port number. + /// + public int Port { get; private set; } + + /// + /// HTTP method. + /// + public HttpMethod Method { get; private set; } = HttpMethod.GET; + + /// + /// URL. + /// + public string Url { get; private set; } + + /// + /// Request query. + /// + public Dictionary Query { get; private set; } = new(); + + /// + /// Request headers. + /// + public Dictionary RequestHeaders { get; private set; } = new(); + + /// + /// Content length. + /// + public long RequestContentLength { get; private set; } + + /// + /// Response status. + /// + public int StatusCode { get; private set; } + + /// + /// Response headers. + /// + public Dictionary ResponseHeaders { get; private set; } = new(); + + /// + /// Response content length. + /// + public long? ResponseContentLength { get; private set; } = 0; + + /// + /// Exception. + /// + public Exception Exception { get; } + + /// + /// JSON string of the Exception. + /// + public string Json + { + get { - if (context != null) + if (Exception != null) { - Ip = context.Request.Source.IpAddress; - Port = context.Request.Source.Port; - Method = context.Request.Method; - Url = context.Request.Url.Full; - Query = context.Request.Query.Elements; - RequestHeaders = context.Request.Headers; - RequestContentLength = context.Request.ContentLength; - StatusCode = context.Response.StatusCode; - ResponseContentLength = context.Response.ContentLength; + return Exception.ToJson(true); } - Exception = e; + return null; } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/EventArgs/RequestEventArgs.cs b/EonaCat.Network/System/Web/EventArgs/RequestEventArgs.cs index 95fafcf..a02e10d 100644 --- a/EonaCat.Network/System/Web/EventArgs/RequestEventArgs.cs +++ b/EonaCat.Network/System/Web/EventArgs/RequestEventArgs.cs @@ -1,60 +1,58 @@ using System; using System.Collections.Generic; -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Request event arguments. +/// +public class RequestEventArgs : EventArgs { - // This file is part of the EonaCat 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 RequestEventArgs(HttpContext context) + { + Ip = context.Request.Source.IpAddress; + Port = context.Request.Source.Port; + Method = context.Request.Method; + Url = context.Request.Url.Full; + Query = context.Request.Query.Elements; + Headers = context.Request.Headers; + ContentLength = context.Request.ContentLength; + } /// - /// Request event arguments. + /// IP address. /// - public class RequestEventArgs : EventArgs - { - /// - /// IP address. - /// - public string Ip { get; private set; } = null; + public string Ip { get; private set; } - /// - /// Port number. - /// - public int Port { get; private set; } = 0; + /// + /// Port number. + /// + public int Port { get; private set; } - /// - /// HTTP method. - /// - public HttpMethod Method { get; private set; } = HttpMethod.GET; + /// + /// HTTP method. + /// + public HttpMethod Method { get; private set; } = HttpMethod.GET; - /// - /// URL. - /// - public string Url { get; private set; } = null; + /// + /// URL. + /// + public string Url { get; private set; } - /// - /// Query found in the URL. - /// - public Dictionary Query { get; private set; } = new Dictionary(); + /// + /// Query found in the URL. + /// + public Dictionary Query { get; private set; } = new(); - /// - /// Request headers. - /// - public Dictionary Headers { get; private set; } = new Dictionary(); + /// + /// Request headers. + /// + public Dictionary Headers { get; private set; } = new(); - /// - /// Content length. - /// - public long ContentLength { get; private set; } = 0; - - internal RequestEventArgs(HttpContext context) - { - Ip = context.Request.Source.IpAddress; - Port = context.Request.Source.Port; - Method = context.Request.Method; - Url = context.Request.Url.Full; - Query = context.Request.Query.Elements; - Headers = context.Request.Headers; - ContentLength = context.Request.ContentLength; - } - } + /// + /// Content length. + /// + public long ContentLength { get; private set; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/EventArgs/ResponseEventArgs.cs b/EonaCat.Network/System/Web/EventArgs/ResponseEventArgs.cs index 87e93de..6a76729 100644 --- a/EonaCat.Network/System/Web/EventArgs/ResponseEventArgs.cs +++ b/EonaCat.Network/System/Web/EventArgs/ResponseEventArgs.cs @@ -1,83 +1,81 @@ using System; using System.Collections.Generic; -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Response event arguments. +/// +public class ResponseEventArgs : EventArgs { - // This file is part of the EonaCat 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 ResponseEventArgs(HttpContext context, double totalMs) + { + Ip = context.Request.Source.IpAddress; + Port = context.Request.Source.Port; + Method = context.Request.Method; + Url = context.Request.Url.Full; + Query = context.Request.Query.Elements; + RequestHeaders = context.Request.Headers; + RequestContentLength = context.Request.ContentLength; + StatusCode = context.Response.StatusCode; + ResponseContentLength = context.Response.ContentLength; + TotalMs = totalMs; + } /// - /// Response event arguments. + /// IP address. /// - public class ResponseEventArgs : EventArgs - { - /// - /// IP address. - /// - public string Ip { get; private set; } = null; + public string Ip { get; private set; } - /// - /// Port number. - /// - public int Port { get; private set; } = 0; + /// + /// Port number. + /// + public int Port { get; private set; } - /// - /// HTTP method. - /// - public HttpMethod Method { get; private set; } = HttpMethod.GET; + /// + /// HTTP method. + /// + public HttpMethod Method { get; private set; } = HttpMethod.GET; - /// - /// URL. - /// - public string Url { get; private set; } = null; + /// + /// URL. + /// + public string Url { get; private set; } - /// - /// Request query. - /// - public Dictionary Query { get; private set; } = new Dictionary(); + /// + /// Request query. + /// + public Dictionary Query { get; private set; } = new(); - /// - /// Request headers. - /// - public Dictionary RequestHeaders { get; private set; } = new Dictionary(); + /// + /// Request headers. + /// + public Dictionary RequestHeaders { get; private set; } = new(); - /// - /// Content length. - /// - public long RequestContentLength { get; private set; } = 0; + /// + /// Content length. + /// + public long RequestContentLength { get; private set; } - /// - /// Response status. - /// - public int StatusCode { get; private set; } = 0; + /// + /// Response status. + /// + public int StatusCode { get; private set; } - /// - /// Response headers. - /// - public Dictionary ResponseHeaders { get; private set; } = new Dictionary(); + /// + /// Response headers. + /// + public Dictionary ResponseHeaders { get; private set; } = new(); - /// - /// Response content length. - /// - public long? ResponseContentLength { get; private set; } = 0; + /// + /// Response content length. + /// + public long? ResponseContentLength { get; private set; } = 0; - /// - /// Total time in processing the request and sending the response, in milliseconds. - /// - public double TotalMs { get; private set; } = 0; - - internal ResponseEventArgs(HttpContext context, double totalMs) - { - Ip = context.Request.Source.IpAddress; - Port = context.Request.Source.Port; - Method = context.Request.Method; - Url = context.Request.Url.Full; - Query = context.Request.Query.Elements; - RequestHeaders = context.Request.Headers; - RequestContentLength = context.Request.ContentLength; - StatusCode = context.Response.StatusCode; - ResponseContentLength = context.Response.ContentLength; - TotalMs = totalMs; - } - } + /// + /// Total time in processing the request and sending the response, in milliseconds. + /// + public double TotalMs { get; private set; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/Extensions/ObjectExtensions.cs b/EonaCat.Network/System/Web/Extensions/ObjectExtensions.cs index 6a358fb..e849b31 100644 --- a/EonaCat.Network/System/Web/Extensions/ObjectExtensions.cs +++ b/EonaCat.Network/System/Web/Extensions/ObjectExtensions.cs @@ -1,47 +1,45 @@ using EonaCat.Json; -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Object extensions. +/// +public static class ObjectExtensions { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - /// - /// Object extensions. + /// Return a JSON string of the object. /// - public static class ObjectExtensions + /// Object. + /// Enable or disable pretty print. + /// JSON string. + public static string ToJson(this object obj, bool pretty = false) { - /// - /// Return a JSON string of the object. - /// - /// Object. - /// Enable or disable pretty print. - /// JSON string. - public static string ToJson(this object obj, bool pretty = false) + string json; + + if (pretty) { - 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; + json = JsonHelper.ToJson( + obj, + Formatting.Indented, + new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore, + DateTimeZoneHandling = DateTimeZoneHandling.Local + }); } + else + { + json = JsonHelper.ToJson(obj, + new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore, + DateTimeZoneHandling = DateTimeZoneHandling.Local + }); + } + + return json; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/Helpers/SerializationHelper.cs b/EonaCat.Network/System/Web/Helpers/SerializationHelper.cs index 24d50cc..a0d2b61 100644 --- a/EonaCat.Network/System/Web/Helpers/SerializationHelper.cs +++ b/EonaCat.Network/System/Web/Helpers/SerializationHelper.cs @@ -1,53 +1,51 @@ -using EonaCat.Json; -using System; +using System; +using EonaCat.Json; -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +internal class SerializationHelper { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - - internal class SerializationHelper + internal static T DeserializeJson(string json) { - internal static T DeserializeJson(string json) + if (string.IsNullOrEmpty(json)) { - if (string.IsNullOrEmpty(json)) - { - throw new ArgumentNullException(nameof(json)); - } - - return JsonHelper.ToObject(json); + throw new ArgumentNullException(nameof(json)); } - internal static string SerializeJson(object obj, bool pretty) + return JsonHelper.ToObject(json); + } + + internal static string SerializeJson(object obj, bool pretty) + { + if (obj == null) { - if (obj == null) - { - return null; - } - - string json; - - if (pretty) - { - json = JsonHelper.ToJson(obj, - Formatting.Indented, - new JsonSerializerSettings - { - NullValueHandling = NullValueHandling.Ignore, - DateTimeZoneHandling = DateTimeZoneHandling.Local, - }); - } - else - { - json = JsonHelper.ToJson(obj, - new JsonSerializerSettings - { - NullValueHandling = NullValueHandling.Ignore, - DateTimeZoneHandling = DateTimeZoneHandling.Local - }); - } - - return json; + return null; } + + string json; + + if (pretty) + { + json = JsonHelper.ToJson(obj, + Formatting.Indented, + new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore, + DateTimeZoneHandling = DateTimeZoneHandling.Local + }); + } + else + { + json = JsonHelper.ToJson(obj, + new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore, + DateTimeZoneHandling = DateTimeZoneHandling.Local + }); + } + + return json; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/HttpContext.cs b/EonaCat.Network/System/Web/HttpContext.cs index 012d8d8..ca42704 100644 --- a/EonaCat.Network/System/Web/HttpContext.cs +++ b/EonaCat.Network/System/Web/HttpContext.cs @@ -1,70 +1,69 @@ -using EonaCat.Json; -using System; +using System; +using System.Net; +using EonaCat.Json; -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// HTTP context including both request and response. +/// +public class HttpContext { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private readonly HttpListenerContext _Context; /// - /// HTTP context including both request and response. + /// Instantiate the object. /// - public class HttpContext + public HttpContext() { - /// - /// The HTTP request that was received. - /// - [JsonProperty(Order = -1)] - public HttpRequest Request { get; private set; } = null; - - /// - /// Type of route. - /// - [JsonProperty(Order = 0)] - public RouteTypeEnum? RouteType { get; internal set; } = null; - - /// - /// Matched route. - /// - [JsonProperty(Order = 1)] - public object Route { get; internal set; } = null; - - /// - /// The HTTP response that will be sent. This object is preconstructed on your behalf and can be modified directly. - /// - [JsonProperty(Order = 998)] - public HttpResponse Response { get; private set; } = null; - - /// - /// User-supplied metadata. - /// - [JsonProperty(Order = 999)] - public object Metadata { get; set; } = null; - - private readonly System.Net.HttpListenerContext _Context = null; - - /// - /// Instantiate the object. - /// - public HttpContext() - { - } - - internal HttpContext(System.Net.HttpListenerContext context, EonaCatWebserverSettings settings, EonaCatWebserverEvents events) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (events == null) - { - throw new ArgumentNullException(nameof(events)); - } - - _Context = context; - Request = new HttpRequest(context); - Response = new HttpResponse(Request, context, settings, events); - } } + + internal HttpContext(HttpListenerContext context, EonaCatWebserverSettings settings, EonaCatWebserverEvents events) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (events == null) + { + throw new ArgumentNullException(nameof(events)); + } + + _Context = context; + Request = new HttpRequest(context); + Response = new HttpResponse(Request, context, settings, events); + } + + /// + /// The HTTP request that was received. + /// + [JsonProperty(Order = -1)] + public HttpRequest Request { get; } + + /// + /// Type of route. + /// + [JsonProperty(Order = 0)] + public RouteTypeEnum? RouteType { get; internal set; } = null; + + /// + /// Matched route. + /// + [JsonProperty(Order = 1)] + public object Route { get; internal set; } = null; + + /// + /// The HTTP response that will be sent. This object is preconstructed on your behalf and can be modified directly. + /// + [JsonProperty(Order = 998)] + public HttpResponse Response { get; private set; } + + /// + /// User-supplied metadata. + /// + [JsonProperty(Order = 999)] + public object Metadata { get; set; } = null; } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/HttpMethod.cs b/EonaCat.Network/System/Web/HttpMethod.cs index acd2dab..3d46afc 100644 --- a/EonaCat.Network/System/Web/HttpMethod.cs +++ b/EonaCat.Network/System/Web/HttpMethod.cs @@ -1,69 +1,59 @@ -using EonaCat.Json.Converters; -using System.Runtime.Serialization; +using System.Runtime.Serialization; +using EonaCat.Json; +using EonaCat.Json.Converters; -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// HTTP methods, i.e. GET, PUT, POST, DELETE, etc. +/// +[Converter(typeof(StringEnumConverter))] +public enum HttpMethod { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + /// + /// HTTP GET. + /// + [EnumMember(Value = "GET")] GET, /// - /// HTTP methods, i.e. GET, PUT, POST, DELETE, etc. + /// HTTP HEAD. /// - [Json.Converter(typeof(StringEnumConverter))] - public enum HttpMethod - { - /// - /// HTTP GET. - /// - [EnumMember(Value = "GET")] - GET, + [EnumMember(Value = "HEAD")] HEAD, - /// - /// HTTP HEAD. - /// - [EnumMember(Value = "HEAD")] - HEAD, + /// + /// HTTP PUT. + /// + [EnumMember(Value = "PUT")] PUT, - /// - /// HTTP PUT. - /// - [EnumMember(Value = "PUT")] - PUT, + /// + /// HTTP POST. + /// + [EnumMember(Value = "POST")] POST, - /// - /// HTTP POST. - /// - [EnumMember(Value = "POST")] - POST, + /// + /// HTTP DELETE. + /// + [EnumMember(Value = "DELETE")] DELETE, - /// - /// HTTP DELETE. - /// - [EnumMember(Value = "DELETE")] - DELETE, + /// + /// HTTP PATCH. + /// + [EnumMember(Value = "PATCH")] PATCH, - /// - /// HTTP PATCH. - /// - [EnumMember(Value = "PATCH")] - PATCH, + /// + /// HTTP CONNECT. + /// + [EnumMember(Value = "CONNECT")] CONNECT, - /// - /// HTTP CONNECT. - /// - [EnumMember(Value = "CONNECT")] - CONNECT, + /// + /// HTTP OPTIONS. + /// + [EnumMember(Value = "OPTIONS")] OPTIONS, - /// - /// HTTP OPTIONS. - /// - [EnumMember(Value = "OPTIONS")] - OPTIONS, - - /// - /// HTTP TRACE. - /// - [EnumMember(Value = "TRACE")] - TRACE - } + /// + /// HTTP TRACE. + /// + [EnumMember(Value = "TRACE")] TRACE } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/HttpRequest.cs b/EonaCat.Network/System/Web/HttpRequest.cs index 03048b9..0c58bd1 100644 --- a/EonaCat.Network/System/Web/HttpRequest.cs +++ b/EonaCat.Network/System/Web/HttpRequest.cs @@ -1,5 +1,4 @@ -using EonaCat.Json; -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -7,929 +6,897 @@ using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; +using EonaCat.Json; -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// HTTP request. +/// +public class HttpRequest { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private readonly Uri _Uri; + private byte[] _DataAsBytes; /// - /// HTTP request. + /// The stream from which to read the request body sent by the requestor (client). /// - public class HttpRequest + [JsonIgnore] public Stream Data; + + /// + /// The original HttpListenerContext from which the HttpRequest was constructed. + /// + [JsonIgnore] public HttpListenerContext ListenerContext; + + /// + /// HTTP request. + /// + public HttpRequest() { - /// - /// UTC timestamp from when the request was received. - /// - [JsonProperty(Order = -10)] - public DateTime TimestampUtc { get; private set; } = DateTime.Now.ToUniversalTime(); + } - /// - /// Thread ID on which the request exists. - /// - [JsonProperty(Order = -9)] - public int ThreadId { get; private set; } = Thread.CurrentThread.ManagedThreadId; - - /// - /// The protocol and version. - /// - [JsonProperty(Order = -8)] - public string ProtocolVersion { get; set; } = null; - - /// - /// Source (requestor) IP and port information. - /// - [JsonProperty(Order = -7)] - public SourceDetails Source { get; set; } = new SourceDetails(); - - /// - /// Destination IP and port information. - /// - [JsonProperty(Order = -6)] - public DestinationDetails Destination { get; set; } = new DestinationDetails(); - - /// - /// The HTTP method used in the request. - /// - [JsonProperty(Order = -5)] - public HttpMethod Method { get; set; } = HttpMethod.GET; - - /// - /// URL details. - /// - [JsonProperty(Order = -4)] - public UrlDetails Url { get; set; } = new UrlDetails(); - - /// - /// Query details. - /// - [JsonProperty(Order = -3)] - public QueryDetails Query { get; set; } = new QueryDetails(); - - /// - /// The headers found in the request. - /// - [JsonProperty(Order = -2)] - public Dictionary Headers { get; set; } = new Dictionary(); - - /// - /// Specifies whether or not the client requested HTTP keepalives. - /// - public bool Keepalive { get; set; } = false; - - /// - /// Indicates whether or not chunked transfer encoding was detected. - /// - public bool ChunkedTransfer { get; set; } = false; - - /// - /// Indicates whether or not the payload has been gzip compressed. - /// - public bool Gzip { get; set; } = false; - - /// - /// Indicates whether or not the payload has been deflate compressed. - /// - public bool Deflate { get; set; } = false; - - /// - /// The useragent specified in the request. - /// - public string Useragent { get; set; } = null; - - /// - /// The content type as specified by the requestor (client). - /// - [JsonProperty(Order = 990)] - public string ContentType { get; set; } = null; - - /// - /// The number of bytes in the request body. - /// - [JsonProperty(Order = 991)] - public long ContentLength { get; private set; } = 0; - - /// - /// The stream from which to read the request body sent by the requestor (client). - /// - [JsonIgnore] - public Stream Data; - - /// - /// Retrieve the request body as a byte array. This will fully read the stream. - /// - [JsonIgnore] - public byte[] DataAsBytes + /// + /// HTTP request. + /// Instantiate the object using an HttpListenerContext. + /// + /// HttpListenerContext. + public HttpRequest(HttpListenerContext context) + { + if (context == null) { - get + throw new ArgumentNullException(nameof(context)); + } + + if (context.Request == null) + { + throw new ArgumentNullException(nameof(context.Request)); + } + + ListenerContext = context; + Keepalive = context.Request.KeepAlive; + ContentLength = context.Request.ContentLength64; + Useragent = context.Request.UserAgent; + ContentType = context.Request.ContentType; + + _Uri = new Uri(context.Request.Url.ToString().Trim()); + + ThreadId = Thread.CurrentThread.ManagedThreadId; + TimestampUtc = DateTime.Now.ToUniversalTime(); + ProtocolVersion = "HTTP/" + context.Request.ProtocolVersion; + Source = new SourceDetails(context.Request.RemoteEndPoint.Address.ToString(), + context.Request.RemoteEndPoint.Port); + Destination = new DestinationDetails(context.Request.LocalEndPoint.Address.ToString(), + context.Request.LocalEndPoint.Port, _Uri.Host); + Method = (HttpMethod)Enum.Parse(typeof(HttpMethod), context.Request.HttpMethod, true); + Url = new UrlDetails(context.Request.Url.ToString().Trim(), context.Request.RawUrl.Trim()); + Query = new QueryDetails(Url.Full); + + Headers = new Dictionary(); + for (var i = 0; i < context.Request.Headers.Count; i++) + { + var key = context.Request.Headers.GetKey(i); + var val = context.Request.Headers.Get(i); + Headers = AddToDict(key, val, Headers); + } + + foreach (var curr in Headers) + { + if (string.IsNullOrEmpty(curr.Key)) { - if (_DataAsBytes != null) + continue; + } + + if (string.IsNullOrEmpty(curr.Value)) + { + continue; + } + + if (curr.Key.ToLower().Equals("transfer-encoding")) + { + if (curr.Value.ToLower().Contains("chunked")) { - return _DataAsBytes; + ChunkedTransfer = true; } - if (Data != null && ContentLength > 0) + if (curr.Value.ToLower().Contains("gzip")) { - _DataAsBytes = ReadStreamFully(Data); - return _DataAsBytes; + 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; } - return null; } } - /// - /// Retrieve the request body as a string. This will fully read the stream. - /// - [JsonIgnore] - public string DataAsString + Data = context.Request.InputStream; + } + + /// + /// UTC timestamp from when the request was received. + /// + [JsonProperty(Order = -10)] + public DateTime TimestampUtc { get; private set; } = DateTime.Now.ToUniversalTime(); + + /// + /// Thread ID on which the request exists. + /// + [JsonProperty(Order = -9)] + public int ThreadId { get; private set; } = Thread.CurrentThread.ManagedThreadId; + + /// + /// The protocol and version. + /// + [JsonProperty(Order = -8)] + public string ProtocolVersion { get; set; } + + /// + /// Source (requestor) IP and port information. + /// + [JsonProperty(Order = -7)] + public SourceDetails Source { get; set; } = new(); + + /// + /// Destination IP and port information. + /// + [JsonProperty(Order = -6)] + public DestinationDetails Destination { get; set; } = new(); + + /// + /// The HTTP method used in the request. + /// + [JsonProperty(Order = -5)] + public HttpMethod Method { get; set; } = HttpMethod.GET; + + /// + /// URL details. + /// + [JsonProperty(Order = -4)] + public UrlDetails Url { get; set; } = new(); + + /// + /// Query details. + /// + [JsonProperty(Order = -3)] + public QueryDetails Query { get; set; } = new(); + + /// + /// The headers found in the request. + /// + [JsonProperty(Order = -2)] + public Dictionary Headers { get; set; } = new(); + + /// + /// Specifies whether or not the client requested HTTP keepalives. + /// + public bool Keepalive { get; set; } + + /// + /// Indicates whether or not chunked transfer encoding was detected. + /// + public bool ChunkedTransfer { get; set; } + + /// + /// Indicates whether or not the payload has been gzip compressed. + /// + public bool Gzip { get; set; } + + /// + /// Indicates whether or not the payload has been deflate compressed. + /// + public bool Deflate { get; set; } + + /// + /// The useragent specified in the request. + /// + public string Useragent { get; set; } + + /// + /// The content type as specified by the requestor (client). + /// + [JsonProperty(Order = 990)] + public string ContentType { get; set; } + + /// + /// The number of bytes in the request body. + /// + [JsonProperty(Order = 991)] + public long ContentLength { get; } + + /// + /// Retrieve the request body as a byte array. This will fully read the stream. + /// + [JsonIgnore] + public byte[] DataAsBytes + { + get { - get + if (_DataAsBytes != null) { + return _DataAsBytes; + } + + if (Data != null && ContentLength > 0) + { + _DataAsBytes = ReadStreamFully(Data); + return _DataAsBytes; + } + + return null; + } + } + + /// + /// Retrieve the request body as a string. This will fully read the stream. + /// + [JsonIgnore] + public string DataAsString + { + get + { + if (_DataAsBytes != null) + { + return Encoding.UTF8.GetString(_DataAsBytes); + } + + if (Data != null && ContentLength > 0) + { + _DataAsBytes = ReadStreamFully(Data); if (_DataAsBytes != null) { return Encoding.UTF8.GetString(_DataAsBytes); } - - if (Data != null && ContentLength > 0) - { - _DataAsBytes = ReadStreamFully(Data); - if (_DataAsBytes != null) - { - return Encoding.UTF8.GetString(_DataAsBytes); - } - } - return null; } + + return null; + } + } + + /// + /// Retrieve a specified header value from either the headers or the querystring (case insensitive). + /// + /// + /// + public string RetrieveHeaderValue(string key) + { + if (string.IsNullOrEmpty(key)) + { + throw new ArgumentNullException(nameof(key)); } - /// - /// The original HttpListenerContext from which the HttpRequest was constructed. - /// - [JsonIgnore] - public System.Net.HttpListenerContext ListenerContext; - - private readonly Uri _Uri = null; - private byte[] _DataAsBytes = null; - - /// - /// HTTP request. - /// - public HttpRequest() + if (Headers != null && Headers.Count > 0) { - } - - /// - /// HTTP request. - /// Instantiate the object using an HttpListenerContext. - /// - /// HttpListenerContext. - public HttpRequest(System.Net.HttpListenerContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (context.Request == null) - { - throw new ArgumentNullException(nameof(context.Request)); - } - - ListenerContext = context; - Keepalive = context.Request.KeepAlive; - ContentLength = context.Request.ContentLength64; - Useragent = context.Request.UserAgent; - ContentType = context.Request.ContentType; - - _Uri = new Uri(context.Request.Url.ToString().Trim()); - - ThreadId = Thread.CurrentThread.ManagedThreadId; - TimestampUtc = DateTime.Now.ToUniversalTime(); - ProtocolVersion = "HTTP/" + context.Request.ProtocolVersion.ToString(); - Source = new SourceDetails(context.Request.RemoteEndPoint.Address.ToString(), context.Request.RemoteEndPoint.Port); - Destination = new DestinationDetails(context.Request.LocalEndPoint.Address.ToString(), context.Request.LocalEndPoint.Port, _Uri.Host); - Method = (HttpMethod)Enum.Parse(typeof(HttpMethod), context.Request.HttpMethod, true); - Url = new UrlDetails(context.Request.Url.ToString().Trim(), context.Request.RawUrl.ToString().Trim()); - Query = new QueryDetails(Url.Full); - - Headers = new Dictionary(); - for (int i = 0; i < context.Request.Headers.Count; i++) - { - string key = context.Request.Headers.GetKey(i); - string val = context.Request.Headers.Get(i); - Headers = AddToDict(key, val, Headers); - } - - foreach (KeyValuePair curr in Headers) + foreach (var curr in Headers) { if (string.IsNullOrEmpty(curr.Key)) { continue; } - if (string.IsNullOrEmpty(curr.Value)) + if (string.Compare(curr.Key.ToLower(), key.ToLower()) == 0) + { + return curr.Value; + } + } + } + + if (Query != null && Query.Elements != null && Query.Elements.Count > 0) + { + foreach (var curr in Query.Elements) + { + if (string.IsNullOrEmpty(curr.Key)) { continue; } - if (curr.Key.ToLower().Equals("transfer-encoding")) + if (string.Compare(curr.Key.ToLower(), key.ToLower()) == 0) { - 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; - } + return curr.Value; } } - - Data = context.Request.InputStream; } - /// - /// Retrieve a specified header value from either the headers or the querystring (case insensitive). - /// - /// - /// - public string RetrieveHeaderValue(string key) + return null; + } + + /// + /// Determine if a header exists. + /// + /// Header key. + /// Specify whether a case sensitive search should be used. + /// True if exists. + public bool HeaderExists(string key, bool caseSensitive) + { + if (string.IsNullOrEmpty(key)) { - if (string.IsNullOrEmpty(key)) - { - throw new ArgumentNullException(nameof(key)); - } - - if (Headers != null && Headers.Count > 0) - { - foreach (KeyValuePair curr in Headers) - { - if (string.IsNullOrEmpty(curr.Key)) - { - continue; - } - - if (string.Compare(curr.Key.ToLower(), key.ToLower()) == 0) - { - return curr.Value; - } - } - } - - if (Query != null && Query.Elements != null && Query.Elements.Count > 0) - { - foreach (KeyValuePair curr in Query.Elements) - { - if (string.IsNullOrEmpty(curr.Key)) - { - continue; - } - - if (string.Compare(curr.Key.ToLower(), key.ToLower()) == 0) - { - return curr.Value; - } - } - } - - return null; + throw new ArgumentNullException(nameof(key)); } - /// - /// Determine if a header exists. - /// - /// Header key. - /// Specify whether a case sensitive search should be used. - /// True if exists. - public bool HeaderExists(string key, bool caseSensitive) + if (Headers != null && Headers.Count > 0) { - if (string.IsNullOrEmpty(key)) + if (caseSensitive) { - throw new ArgumentNullException(nameof(key)); + return Headers.ContainsKey(key); } - if (Headers != null && Headers.Count > 0) + foreach (var header in Headers) { - if (caseSensitive) + if (string.IsNullOrEmpty(header.Key)) { - return Headers.ContainsKey(key); + continue; } - else - { - foreach (KeyValuePair header in Headers) - { - if (string.IsNullOrEmpty(header.Key)) - { - continue; - } - if (header.Key.ToLower().Trim().Equals(key)) - { - return true; - } - } + if (header.Key.ToLower().Trim().Equals(key)) + { + return true; } } - - return false; } - /// - /// Determine if a querystring entry exists. - /// - /// Querystring key. - /// Specify whether a case sensitive search should be used. - /// True if exists. - public bool QuerystringExists(string key, bool caseSensitive) + return false; + } + + /// + /// Determine if a querystring entry exists. + /// + /// Querystring key. + /// Specify whether a case sensitive search should be used. + /// True if exists. + public bool QuerystringExists(string key, bool caseSensitive) + { + if (string.IsNullOrEmpty(key)) { - if (string.IsNullOrEmpty(key)) - { - throw new ArgumentNullException(nameof(key)); - } - - if (Query != null && Query.Elements != null && Query.Elements.Count > 0) - { - if (caseSensitive) - { - return Query.Elements.ContainsKey(key); - } - else - { - foreach (KeyValuePair queryElement in Query.Elements) - { - if (string.IsNullOrEmpty(queryElement.Key)) - { - continue; - } - - if (queryElement.Key.ToLower().Trim().Equals(key)) - { - return true; - } - } - } - } - - return false; + throw new ArgumentNullException(nameof(key)); } - /// - /// For chunked transfer-encoded requests, read the next chunk. - /// It is strongly recommended that you use the ChunkedTransfer parameter before invoking this method. - /// - /// Cancellation token useful for canceling the request. - /// Chunk. - public async Task ReadChunk(CancellationToken token = default) + if (Query != null && Query.Elements != null && Query.Elements.Count > 0) { - Chunk chunk = new Chunk(); - - byte[] buffer = new byte[1]; - byte[] lenBytes = null; - int bytesRead = 0; - - while (true) + if (caseSensitive) { - 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; - } - } + return Query.Elements.ContainsKey(key); } - if (chunk.Length > 0) + foreach (var queryElement in Query.Elements) { - chunk.IsFinalChunk = false; - buffer = new byte[chunk.Length]; - bytesRead = await Data.ReadAsync(buffer, 0, buffer.Length, token).ConfigureAwait(false); - if (bytesRead == chunk.Length) + if (string.IsNullOrEmpty(queryElement.Key)) { - chunk.Data = new byte[chunk.Length]; - Buffer.BlockCopy(buffer, 0, chunk.Data, 0, chunk.Length); + continue; } - else + + if (queryElement.Key.ToLower().Trim().Equals(key)) { - throw new IOException("Expected " + chunk.Length + " bytes but only read " + bytesRead + " bytes in chunk."); + return true; } } + } + + return false; + } + + /// + /// For chunked transfer-encoded requests, read the next chunk. + /// It is strongly recommended that you use the ChunkedTransfer parameter before invoking this method. + /// + /// Cancellation token useful for canceling the request. + /// Chunk. + public async Task ReadChunk(CancellationToken token = default) + { + var chunk = new Chunk(); + + var buffer = new byte[1]; + byte[] lenBytes = null; + var bytesRead = 0; + + while (true) + { + bytesRead = await Data.ReadAsync(buffer, 0, buffer.Length, token).ConfigureAwait(false); + if (bytesRead > 0) + { + lenBytes = AppendBytes(lenBytes, buffer); + var lenStr = Encoding.UTF8.GetString(lenBytes); + + if (lenBytes[lenBytes.Length - 1] == 10) + { + lenStr = lenStr.Trim(); + + if (lenStr.Contains(";")) + { + var lenParts = lenStr.Split(new[] { ';' }, 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 { - chunk.IsFinalChunk = true; + throw new IOException("Expected " + chunk.Length + " bytes but only read " + bytesRead + + " bytes in chunk."); } + } + else + { + chunk.IsFinalChunk = true; + } - buffer = new byte[1]; + buffer = new byte[1]; - while (true) + while (true) + { + bytesRead = await Data.ReadAsync(buffer, 0, buffer.Length, token).ConfigureAwait(false); + if (bytesRead > 0) { - bytesRead = await Data.ReadAsync(buffer, 0, buffer.Length, token).ConfigureAwait(false); - if (bytesRead > 0) + if (buffer[0] == 10) { - if (buffer[0] == 10) - { - break; - } + break; } } - - return chunk; } - /// - /// Read the data stream fully and convert the data to the object type specified using JSON deserialization. - /// Note: if you use this method, you will not be able to read from the data stream afterward. - /// - /// Type. - /// Object of type specified. - public T DataAsJsonObject() where T : class - { - string json = DataAsString; - if (string.IsNullOrEmpty(json)) - { - return null; - } + return chunk; + } - return SerializationHelper.DeserializeJson(json); + /// + /// Read the data stream fully and convert the data to the object type specified using JSON deserialization. + /// Note: if you use this method, you will not be able to read from the data stream afterward. + /// + /// Type. + /// Object of type specified. + public T DataAsJsonObject() where T : class + { + var json = DataAsString; + if (string.IsNullOrEmpty(json)) + { + return null; } - private static Dictionary AddToDict(string key, string val, Dictionary existing) + return SerializationHelper.DeserializeJson(json); + } + + private static Dictionary AddToDict(string key, string val, Dictionary existing) + { + if (string.IsNullOrEmpty(key)) { - if (string.IsNullOrEmpty(key)) + return existing; + } + + var ret = new Dictionary(); + + if (existing == null) + { + ret.Add(key, val); + return ret; + } + + if (existing.ContainsKey(key)) + { + if (string.IsNullOrEmpty(val)) { return existing; } - Dictionary ret = new Dictionary(); - - if (existing == null) - { - ret.Add(key, val); - return ret; - } - else - { - if (existing.ContainsKey(key)) - { - if (string.IsNullOrEmpty(val)) - { - return existing; - } - - string tempVal = existing[key]; - tempVal += "," + val; - existing.Remove(key); - existing.Add(key, tempVal); - return existing; - } - else - { - existing.Add(key, val); - return existing; - } - } + var tempVal = existing[key]; + tempVal += "," + val; + existing.Remove(key); + existing.Add(key, tempVal); + return existing; } - private byte[] AppendBytes(byte[] orig, byte[] append) + existing.Add(key, val); + return existing; + } + + private byte[] AppendBytes(byte[] orig, byte[] append) + { + if (orig == null && append == null) { - if (orig == null && append == null) - { - return null; - } + return null; + } - byte[] ret = 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]; + if (append == null) + { + ret = new byte[orig.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 (orig == null) { - if (input == 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"); + } + + var buffer = new byte[16 * 1024]; + using (var ms = new MemoryStream()) + { + int read; + + while ((read = input.Read(buffer, 0, buffer.Length)) > 0) ms.Write(buffer, 0, read); + + return ms.ToArray(); + } + } + + private byte[] ReadStreamFully(Stream input) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + + if (!input.CanRead) + { + throw new InvalidOperationException("Input stream is not readable"); + } + + var buffer = new byte[16 * 1024]; + using (var ms = new MemoryStream()) + { + int read; + + while ((read = input.Read(buffer, 0, buffer.Length)) > 0) ms.Write(buffer, 0, read); + + var ret = ms.ToArray(); + return ret; + } + } + + /// + /// Source details. + /// + public class SourceDetails + { + /// + /// Source details. + /// + public SourceDetails() + { + } + + /// + /// Source details. + /// + /// IP address of the requestor. + /// TCP port from which the request originated on the requestor. + public SourceDetails(string ip, int port) + { + if (string.IsNullOrEmpty(ip)) { - throw new ArgumentNullException(nameof(input)); + throw new ArgumentNullException(nameof(ip)); } - if (!input.CanRead) + if (port < 0) { - throw new InvalidOperationException("Input stream is not readable"); + throw new ArgumentOutOfRangeException(nameof(port)); } - byte[] buffer = new byte[16 * 1024]; - using (MemoryStream ms = new MemoryStream()) - { - int read; + IpAddress = ip; + Port = port; + } - while ((read = input.Read(buffer, 0, buffer.Length)) > 0) + /// + /// IP address of the requestor. + /// + public string IpAddress { get; set; } + + /// + /// TCP port from which the request originated on the requestor. + /// + public int Port { get; set; } + } + + /// + /// Destination details. + /// + public class DestinationDetails + { + /// + /// Destination details. + /// + public DestinationDetails() + { + } + + /// + /// Source details. + /// + /// IP address to which the request was made. + /// TCP port on which the request was received. + /// Hostname. + public DestinationDetails(string ip, int port, string hostname) + { + if (string.IsNullOrEmpty(ip)) + { + throw new ArgumentNullException(nameof(ip)); + } + + if (port < 0) + { + throw new ArgumentOutOfRangeException(nameof(port)); + } + + if (string.IsNullOrEmpty(hostname)) + { + throw new ArgumentNullException(nameof(hostname)); + } + + IpAddress = ip; + Port = port; + Hostname = hostname; + } + + /// + /// IP address to which the request was made. + /// + public string IpAddress { get; set; } + + /// + /// TCP port on which the request was received. + /// + public int Port { get; set; } + + /// + /// Hostname to which the request was directed. + /// + public string Hostname { get; set; } + + /// + /// Hostname elements. + /// + public string[] HostnameElements + { + get + { + var hostname = Hostname; + string[] ret; + + if (!string.IsNullOrEmpty(hostname)) { - ms.Write(buffer, 0, read); + if (!IPAddress.TryParse(hostname, out _)) + { + ret = hostname.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries); + return ret; + } + + ret = new string[1]; + ret[0] = hostname; + return ret; } - return ms.ToArray(); + ret = new string[0]; + return ret; + } + } + } + + /// + /// URL details. + /// + public class UrlDetails + { + /// + /// URL details. + /// + public UrlDetails() + { + } + + /// + /// URL details. + /// + /// Full URL. + /// Raw URL. + public UrlDetails(string fullUrl, string rawUrl) + { + if (string.IsNullOrEmpty(fullUrl)) + { + throw new ArgumentNullException(nameof(fullUrl)); + } + + if (string.IsNullOrEmpty(rawUrl)) + { + throw new ArgumentNullException(nameof(rawUrl)); + } + + Full = fullUrl; + RawWithQuery = rawUrl; + } + + /// + /// Full URL. + /// + public string Full { get; set; } + + /// + /// Raw URL with query. + /// + public string RawWithQuery { get; set; } + + /// + /// Raw URL without query. + /// + public string RawWithoutQuery + { + get + { + if (!string.IsNullOrEmpty(RawWithQuery)) + { + if (RawWithQuery.Contains("?")) + { + return RawWithQuery.Substring(0, RawWithQuery.IndexOf("?")); + } + + return RawWithQuery; + } + + return null; } } - private byte[] ReadStreamFully(Stream input) + /// + /// Raw URL elements. + /// + public string[] Elements { - if (input == null) + get { - throw new ArgumentNullException(nameof(input)); - } + var rawUrl = RawWithoutQuery; - 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) + if (!string.IsNullOrEmpty(rawUrl)) { - ms.Write(buffer, 0, read); + while (rawUrl.Contains("//")) rawUrl = rawUrl.Replace("//", "/"); + + while (rawUrl.StartsWith("/")) rawUrl = rawUrl.Substring(1); + + while (rawUrl.EndsWith("/")) rawUrl = rawUrl.Substring(0, rawUrl.Length - 1); + + var encoded = rawUrl.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + if (encoded != null && encoded.Length > 0) + { + var decoded = new string[encoded.Length]; + for (var i = 0; i < encoded.Length; i++) decoded[i] = WebUtility.UrlDecode(encoded[i]); + + return decoded; + } } - byte[] ret = ms.ToArray(); + var ret = new string[0]; return ret; } } /// - /// Source details. + /// Parameters found within the URL, if using parameter routes. /// - public class SourceDetails + public Dictionary Parameters { get; set; } = new(); + } + + /// + /// Query details. + /// + public class QueryDetails + { + private readonly string _FullUrl; + + /// + /// Query details. + /// + public QueryDetails() { - /// - /// IP address of the requestor. - /// - public string IpAddress { get; set; } = null; + } - /// - /// TCP port from which the request originated on the requestor. - /// - public int Port { get; set; } = 0; - - /// - /// Source details. - /// - public SourceDetails() + /// + /// Query details. + /// + /// Full URL. + public QueryDetails(string fullUrl) + { + if (string.IsNullOrEmpty(fullUrl)) { + throw new ArgumentNullException(nameof(fullUrl)); } - /// - /// Source details. - /// - /// IP address of the requestor. - /// TCP port from which the request originated on the requestor. - public SourceDetails(string ip, int port) + _FullUrl = fullUrl; + } + + /// + /// Querystring, excluding the leading '?'. + /// + public string Querystring + { + get { - if (string.IsNullOrEmpty(ip)) + if (_FullUrl.Contains("?")) { - throw new ArgumentNullException(nameof(ip)); + return _FullUrl.Substring(_FullUrl.IndexOf("?") + 1, _FullUrl.Length - _FullUrl.IndexOf("?") - 1); } - if (port < 0) - { - throw new ArgumentOutOfRangeException(nameof(port)); - } - - IpAddress = ip; - Port = port; + return null; } } /// - /// Destination details. + /// Query elements. /// - public class DestinationDetails + public Dictionary Elements { - /// - /// IP address to which the request was made. - /// - public string IpAddress { get; set; } = null; - - /// - /// TCP port on which the request was received. - /// - public int Port { get; set; } = 0; - - /// - /// Hostname to which the request was directed. - /// - public string Hostname { get; set; } = null; - - /// - /// Hostname elements. - /// - public string[] HostnameElements + get { - get + var ret = new Dictionary(); + var qs = Querystring; + if (!string.IsNullOrEmpty(qs)) { - string hostname = Hostname; - string[] ret; - - if (!string.IsNullOrEmpty(hostname)) + var queries = qs.Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries); + if (queries.Length > 0) { - if (!IPAddress.TryParse(hostname, out _)) + for (var i = 0; i < queries.Length; i++) { - ret = hostname.Split(new char[] { '.' }, StringSplitOptions.RemoveEmptyEntries); - return ret; - } - else - { - ret = new string[1]; - ret[0] = hostname; - return ret; - } - } - - ret = new string[0]; - return ret; - } - } - - /// - /// Destination details. - /// - public DestinationDetails() - { - } - - /// - /// Source details. - /// - /// IP address to which the request was made. - /// TCP port on which the request was received. - /// Hostname. - public DestinationDetails(string ip, int port, string hostname) - { - if (string.IsNullOrEmpty(ip)) - { - throw new ArgumentNullException(nameof(ip)); - } - - if (port < 0) - { - throw new ArgumentOutOfRangeException(nameof(port)); - } - - if (string.IsNullOrEmpty(hostname)) - { - throw new ArgumentNullException(nameof(hostname)); - } - - IpAddress = ip; - Port = port; - Hostname = hostname; - } - } - - /// - /// URL details. - /// - public class UrlDetails - { - /// - /// Full URL. - /// - public string Full { get; set; } = null; - - /// - /// Raw URL with query. - /// - public string RawWithQuery { get; set; } = null; - - /// - /// Raw URL without query. - /// - public string RawWithoutQuery - { - get - { - if (!string.IsNullOrEmpty(RawWithQuery)) - { - if (RawWithQuery.Contains("?")) - { - return RawWithQuery.Substring(0, RawWithQuery.IndexOf("?")); - } - else - { - return RawWithQuery; - } - } - else - { - return null; - } - } - } - - /// - /// Raw URL elements. - /// - public string[] Elements - { - get - { - string rawUrl = RawWithoutQuery; - - if (!string.IsNullOrEmpty(rawUrl)) - { - while (rawUrl.Contains("//")) - { - rawUrl = rawUrl.Replace("//", "/"); - } - - while (rawUrl.StartsWith("/")) - { - rawUrl = rawUrl.Substring(1); - } - - while (rawUrl.EndsWith("/")) - { - rawUrl = rawUrl.Substring(0, rawUrl.Length - 1); - } - - string[] encoded = rawUrl.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries); - if (encoded != null && encoded.Length > 0) - { - string[] decoded = new string[encoded.Length]; - for (int i = 0; i < encoded.Length; i++) + var queryParts = queries[i].Split('='); + if (queryParts != null && queryParts.Length == 2) { - decoded[i] = WebUtility.UrlDecode(encoded[i]); + ret = AddToDict(queryParts[0], queryParts[1], ret); } - - return decoded; - } - } - - string[] ret = new string[0]; - return ret; - } - } - - /// - /// Parameters found within the URL, if using parameter routes. - /// - public Dictionary Parameters { get; set; } = new Dictionary(); - - /// - /// URL details. - /// - public UrlDetails() - { - } - - /// - /// URL details. - /// - /// Full URL. - /// Raw URL. - public UrlDetails(string fullUrl, string rawUrl) - { - if (string.IsNullOrEmpty(fullUrl)) - { - throw new ArgumentNullException(nameof(fullUrl)); - } - - if (string.IsNullOrEmpty(rawUrl)) - { - throw new ArgumentNullException(nameof(rawUrl)); - } - - Full = fullUrl; - RawWithQuery = rawUrl; - } - } - - /// - /// Query details. - /// - public class QueryDetails - { - /// - /// Querystring, excluding the leading '?'. - /// - public string Querystring - { - get - { - if (_FullUrl.Contains("?")) - { - return _FullUrl.Substring(_FullUrl.IndexOf("?") + 1, (_FullUrl.Length - _FullUrl.IndexOf("?") - 1)); - } - else - { - return null; - } - } - } - - /// - /// Query elements. - /// - public Dictionary Elements - { - get - { - Dictionary ret = new Dictionary(); - string qs = Querystring; - if (!string.IsNullOrEmpty(qs)) - { - string[] queries = qs.Split(new char[] { '&' }, StringSplitOptions.RemoveEmptyEntries); - if (queries.Length > 0) - { - for (int i = 0; i < queries.Length; i++) + else if (queryParts != null && queryParts.Length == 1) { - 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); - } + ret = AddToDict(queryParts[0], null, ret); } } } - - return ret; - } - } - - /// - /// Query details. - /// - public QueryDetails() - { - } - - /// - /// Query details. - /// - /// Full URL. - public QueryDetails(string fullUrl) - { - if (string.IsNullOrEmpty(fullUrl)) - { - throw new ArgumentNullException(nameof(fullUrl)); } - _FullUrl = fullUrl; + return ret; } - - private readonly string _FullUrl = null; } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/HttpResponse.cs b/EonaCat.Network/System/Web/HttpResponse.cs index e88faa8..9e2188e 100644 --- a/EonaCat.Network/System/Web/HttpResponse.cs +++ b/EonaCat.Network/System/Web/HttpResponse.cs @@ -1,659 +1,672 @@ -using EonaCat.Json; -using System; +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 +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// HTTP response. +/// +public class HttpResponse { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private readonly HttpListenerContext _Context; + private readonly EonaCatWebserverEvents _Events = new(); + private readonly Stream _outputStream; + + private readonly HttpRequest _Request; + private readonly HttpListenerResponse _Response; + private readonly EonaCatWebserverSettings _Settings = new(); + private byte[] _DataAsBytes; + + private Dictionary _Headers = new(); + private bool _HeadersSent; /// - /// HTTP response. + /// Indicates whether or not chunked transfer encoding should be indicated in the response. /// - public class HttpResponse + public bool ChunkedTransfer = false; + + /// + /// The length of the supplied response data. + /// + public long ContentLength; + + /// + /// User-supplied content-type to include in the response. + /// + public string ContentType = string.Empty; + + /// + /// The HTTP status code to return to the requestor (client). + /// + [JsonProperty(Order = -3)] public int StatusCode = 200; + + /// + /// The HTTP status description to return to the requestor (client). + /// + [JsonProperty(Order = -2)] public string StatusDescription = "OK"; + + /// + /// Instantiate the object. + /// + public HttpResponse() { - /// - /// The HTTP status code to return to the requestor (client). - /// - [JsonProperty(Order = -3)] - public int StatusCode = 200; + } - /// - /// The HTTP status description to return to the requestor (client). - /// - [JsonProperty(Order = -2)] - public string StatusDescription = "OK"; - - /// - /// User-supplied headers to include in the response. - /// - [JsonProperty(Order = -1)] - public Dictionary Headers + internal HttpResponse(HttpRequest httpRequest, HttpListenerContext context, EonaCatWebserverSettings settings, + EonaCatWebserverEvents events) + { + if (httpRequest == null) { - get - { - return _Headers; - } - set - { - if (value == null) - { - _Headers = new Dictionary(); - } - else - { - _Headers = value; - } - } + throw new ArgumentNullException(nameof(httpRequest)); } - /// - /// User-supplied content-type to include in the response. - /// - public string ContentType = string.Empty; - - /// - /// The length of the supplied response data. - /// - public long ContentLength = 0; - - /// - /// Indicates whether or not chunked transfer encoding should be indicated in the response. - /// - public bool ChunkedTransfer = false; - - /// - /// Retrieve the response body sent using a Send() or SendAsync() method. - /// - [JsonIgnore] - public string DataAsString + if (context == null) { - get + throw new ArgumentNullException(nameof(context)); + } + + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + if (events == null) + { + throw new ArgumentNullException(nameof(events)); + } + + _Request = httpRequest; + _Context = context; + _Response = _Context.Response; + _Settings = settings; + _Events = events; + + _outputStream = _Response.OutputStream; + } + + /// + /// User-supplied headers to include in the response. + /// + [JsonProperty(Order = -1)] + public Dictionary Headers + { + get => _Headers; + set + { + if (value == null) { + _Headers = new Dictionary(); + } + else + { + _Headers = value; + } + } + } + + /// + /// Retrieve the response body sent using a Send() or SendAsync() method. + /// + [JsonIgnore] + public string DataAsString + { + get + { + if (_DataAsBytes != null) + { + return Encoding.UTF8.GetString(_DataAsBytes); + } + + if (Data != null && ContentLength > 0) + { + _DataAsBytes = ReadStreamFully(Data); if (_DataAsBytes != null) { return Encoding.UTF8.GetString(_DataAsBytes); } - - if (Data != null && ContentLength > 0) - { - _DataAsBytes = ReadStreamFully(Data); - if (_DataAsBytes != null) - { - return Encoding.UTF8.GetString(_DataAsBytes); - } - } - return null; } + + return null; + } + } + + /// + /// Retrieve the response body sent using a Send() or SendAsync() method. + /// + [JsonIgnore] + public byte[] DataAsBytes + { + get + { + if (_DataAsBytes != null) + { + return _DataAsBytes; + } + + if (Data != null && ContentLength > 0) + { + _DataAsBytes = ReadStreamFully(Data); + return _DataAsBytes; + } + + return null; + } + } + + /// + /// Response data stream sent to the requestor. + /// + [JsonIgnore] + public MemoryStream Data { get; private set; } + + internal bool ResponseSent { get; private set; } + + /// + /// Send headers and no data to the requestor and terminate the connection. + /// + /// Cancellation token useful for canceling the request. + /// True if successful. + public async Task Send(CancellationToken token = default) + { + if (ChunkedTransfer) + { + throw new IOException( + "Response is configured to use chunked transfer-encoding. Use SendChunk() and SendFinalChunk()."); } - /// - /// Retrieve the response body sent using a Send() or SendAsync() method. - /// - [JsonIgnore] - public byte[] DataAsBytes + try { - get - { - if (_DataAsBytes != null) - { - return _DataAsBytes; - } - - if (Data != null && ContentLength > 0) - { - _DataAsBytes = ReadStreamFully(Data); - return _DataAsBytes; - } - return null; - } - } - - /// - /// Response data stream sent to the requestor. - /// - [JsonIgnore] - public MemoryStream Data { get; private set; } = null; - - internal bool ResponseSent { get; private set; } = false; - - private readonly HttpRequest _Request = null; - private readonly System.Net.HttpListenerContext _Context = null; - private readonly System.Net.HttpListenerResponse _Response = null; - private readonly Stream _outputStream = null; - private bool _HeadersSent = false; - private readonly EonaCatWebserverSettings _Settings = new EonaCatWebserverSettings(); - private readonly EonaCatWebserverEvents _Events = new EonaCatWebserverEvents(); - - private Dictionary _Headers = new Dictionary(); - private byte[] _DataAsBytes = null; - - /// - /// Instantiate the object. - /// - public HttpResponse() - { - } - - internal HttpResponse(HttpRequest httpRequest, System.Net.HttpListenerContext context, EonaCatWebserverSettings settings, EonaCatWebserverEvents events) - { - if (httpRequest == null) - { - throw new ArgumentNullException(nameof(httpRequest)); - } - - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (settings == null) - { - throw new ArgumentNullException(nameof(settings)); - } - - if (events == null) - { - throw new ArgumentNullException(nameof(events)); - } - - _Request = httpRequest; - _Context = context; - _Response = _Context.Response; - _Settings = settings; - _Events = events; - - _outputStream = _Response.OutputStream; - } - - /// - /// Send headers and no data to the requestor and terminate the connection. - /// - /// Cancellation token useful for canceling the request. - /// True if successful. - public async Task Send(CancellationToken token = default) - { - if (ChunkedTransfer) - { - throw new IOException("Response is configured to use chunked transfer-encoding. Use SendChunk() and SendFinalChunk()."); - } - - try - { - if (!_HeadersSent) - { - SendHeaders(); - } - - await _outputStream.FlushAsync(token).ConfigureAwait(false); - _outputStream.Close(); - - _Response?.Close(); - - ResponseSent = true; - return true; - } - catch (Exception) - { - return false; - } - } - - /// - /// Send headers with a specified content length and no data to the requestor and terminate the connection. Useful for HEAD requests where the content length must be set. - /// - /// Cancellation token useful for canceling the request. - /// Content length. - /// True if successful. - public async Task Send(long contentLength, CancellationToken token = default) - { - if (ChunkedTransfer) - { - throw new IOException("Response is configured to use chunked transfer-encoding. Use SendChunk() and SendFinalChunk()."); - } - - ContentLength = contentLength; - - try - { - if (!_HeadersSent) - { - SendHeaders(); - } - - await _outputStream.FlushAsync(token).ConfigureAwait(false); - _outputStream.Close(); - - _Response?.Close(); - - ResponseSent = true; - return true; - } - catch (Exception) - { - return false; - } - } - - /// - /// Send headers and data to the requestor and terminate the connection. - /// - /// Data. - /// Cancellation token useful for canceling the request. - /// True if successful. - public async Task Send(string data, CancellationToken token = default) - { - if (ChunkedTransfer) - { - throw new IOException("Response is configured to use chunked transfer-encoding. Use SendChunk() and SendFinalChunk()."); - } - if (!_HeadersSent) { SendHeaders(); } - byte[] bytes = null; + await _outputStream.FlushAsync(token).ConfigureAwait(false); + _outputStream.Close(); - 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(); - - _Response?.Close(); - - ResponseSent = true; - return true; - } - catch (Exception) - { - return false; - } - } - - /// - /// Send headers and data to the requestor and terminate the connection. - /// - /// Data. - /// Cancellation token useful for canceling the request. - /// True if successful. - public async Task Send(byte[] data, CancellationToken token = default) - { - if (ChunkedTransfer) - { - throw new IOException("Response is configured to use chunked transfer-encoding. Use SendChunk() and SendFinalChunk()."); - } - - if (!_HeadersSent) - { - SendHeaders(); - } - - if (data != null && data.Length > 0) - { - Data = new MemoryStream(); - await Data.WriteAsync(data, 0, data.Length, token).ConfigureAwait(false); - Data.Seek(0, SeekOrigin.Begin); - - _Response.ContentLength64 = data.Length; - ContentLength = data.Length; - } - else - { - _Response.ContentLength64 = 0; - } - - try - { - if (_Request.Method != HttpMethod.HEAD) - { - if (data != null && data.Length > 0) - { - await _outputStream.WriteAsync(data, 0, data.Length, token).ConfigureAwait(false); - } - } - - await _outputStream.FlushAsync(token).ConfigureAwait(false); - _outputStream.Close(); - - _Response?.Close(); - - ResponseSent = true; - return true; - } - catch (Exception) - { - return false; - } - } - - /// - /// Send headers and data to the requestor and terminate. - /// - /// Number of bytes to send. - /// Stream containing the data. - /// Cancellation token useful for canceling the request. - /// True if successful. - public async Task Send(long contentLength, Stream stream, CancellationToken token = default) - { - if (ChunkedTransfer) - { - throw new IOException("Response is configured to use chunked transfer-encoding. Use SendChunk() and SendFinalChunk()."); - } - - ContentLength = contentLength; - if (!_HeadersSent) - { - SendHeaders(); - } - - try - { - if (_Request.Method != HttpMethod.HEAD) - { - if (stream != null && stream.CanRead && contentLength > 0) - { - long bytesRemaining = contentLength; - - Data = new MemoryStream(); - - while (bytesRemaining > 0) - { - int bytesRead = 0; - byte[] buffer = new byte[_Settings.IO.StreamBufferSize]; - bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, token).ConfigureAwait(false); - if (bytesRead > 0) - { - await Data.WriteAsync(buffer, 0, bytesRead, token).ConfigureAwait(false); - await _outputStream.WriteAsync(buffer, 0, bytesRead, token).ConfigureAwait(false); - bytesRemaining -= bytesRead; - } - } - - stream.Close(); - stream.Dispose(); - - Data.Seek(0, SeekOrigin.Begin); - } - } - - await _outputStream.FlushAsync(token).ConfigureAwait(false); - _outputStream.Close(); - - _Response?.Close(); - - ResponseSent = true; - return true; - } - catch (Exception) - { - return false; - } - } - - /// - /// Send headers (if not already sent) and a chunk of data using chunked transfer-encoding, and keep the connection in-tact. - /// - /// Chunk of data. - /// Number of bytes to send from the chunk, i.e. the actual data size (for example, return value of FileStream.ReadAsync(buffer, 0, buffer.Length)). - /// Cancellation token useful for canceling the request. - /// True if successful. - public async Task SendChunk(byte[] chunk, int numBytes, CancellationToken token = default) - { - if (!ChunkedTransfer) - { - throw new IOException("Response is not configured to use chunked transfer-encoding. Set ChunkedTransfer to true first, otherwise use Send()."); - } - - if (!_HeadersSent) - { - SendHeaders(); - } - - if (chunk != null && chunk.Length > 0) - { - ContentLength += chunk.Length; - } - - try - { - if (chunk == null || chunk.Length < 1) - { - chunk = new byte[0]; - } - - await _outputStream.WriteAsync(chunk, 0, numBytes, token).ConfigureAwait(false); - await _outputStream.FlushAsync(token).ConfigureAwait(false); - } - catch (Exception) - { - return false; - } + _Response?.Close(); + ResponseSent = true; return true; } - - /// - /// Send headers (if not already sent) and the final chunk of data using chunked transfer-encoding and terminate the connection. - /// - /// Chunk of data./// Number of bytes to send from the chunk, i.e. the actual data size (for example, return value of FileStream.ReadAsync(buffer, 0, buffer.Length)). - /// Cancellation token useful for canceling the request. - /// True if successful. - public async Task SendFinalChunk(byte[] chunk, int numBytes, CancellationToken token = default) + catch (Exception) { - if (!ChunkedTransfer) - { - throw new IOException("Response is not configured to use chunked transfer-encoding. Set ChunkedTransfer to true first, otherwise use Send()."); - } + return false; + } + } + /// + /// Send headers with a specified content length and no data to the requestor and terminate the connection. Useful for + /// HEAD requests where the content length must be set. + /// + /// Cancellation token useful for canceling the request. + /// Content length. + /// True if successful. + public async Task Send(long contentLength, CancellationToken token = default) + { + if (ChunkedTransfer) + { + throw new IOException( + "Response is configured to use chunked transfer-encoding. Use SendChunk() and SendFinalChunk()."); + } + + ContentLength = contentLength; + + try + { if (!_HeadersSent) { SendHeaders(); } - if (chunk != null && chunk.Length > 0) - { - ContentLength += chunk.Length; - } + await _outputStream.FlushAsync(token).ConfigureAwait(false); + _outputStream.Close(); - try + _Response?.Close(); + + ResponseSent = true; + return true; + } + catch (Exception) + { + return false; + } + } + + /// + /// Send headers and data to the requestor and terminate the connection. + /// + /// Data. + /// Cancellation token useful for canceling the request. + /// True if successful. + public async Task Send(string data, CancellationToken token = default) + { + if (ChunkedTransfer) + { + throw new IOException( + "Response is configured to use chunked transfer-encoding. Use SendChunk() and SendFinalChunk()."); + } + + if (!_HeadersSent) + { + SendHeaders(); + } + + byte[] bytes = null; + + if (!string.IsNullOrEmpty(data)) + { + bytes = Encoding.UTF8.GetBytes(data); + + Data = new MemoryStream(); + await Data.WriteAsync(bytes, 0, bytes.Length, token).ConfigureAwait(false); + Data.Seek(0, SeekOrigin.Begin); + + _Response.ContentLength64 = bytes.Length; + ContentLength = bytes.Length; + } + else + { + _Response.ContentLength64 = 0; + } + + try + { + if (_Request.Method != HttpMethod.HEAD) { - if (chunk != null && chunk.Length > 0) + if (bytes != null && bytes.Length > 0) { - await _outputStream.WriteAsync(chunk, 0, numBytes, token).ConfigureAwait(false); + await _outputStream.WriteAsync(bytes, 0, bytes.Length, 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(); - - _Response?.Close(); - - ResponseSent = true; - return true; - } - catch (Exception) - { - return false; } + + await _outputStream.FlushAsync(token).ConfigureAwait(false); + _outputStream.Close(); + + _Response?.Close(); + + ResponseSent = true; + return true; + } + catch (Exception) + { + return false; + } + } + + /// + /// Send headers and data to the requestor and terminate the connection. + /// + /// Data. + /// Cancellation token useful for canceling the request. + /// True if successful. + public async Task Send(byte[] data, CancellationToken token = default) + { + if (ChunkedTransfer) + { + throw new IOException( + "Response is configured to use chunked transfer-encoding. Use SendChunk() and SendFinalChunk()."); } - /// - /// Convert the response data sent using a Send() method to the object type specified using JSON deserialization. - /// - /// Type. - /// Object of type specified. - public T DataAsJsonObject() where T : class + if (!_HeadersSent) { - string json = DataAsString; - if (string.IsNullOrEmpty(json)) - { - return null; - } - - return SerializationHelper.DeserializeJson(json); + SendHeaders(); } - private void SendHeaders() + if (data != null && data.Length > 0) { - if (_HeadersSent) - { - throw new IOException("Headers already sent."); - } + Data = new MemoryStream(); + await Data.WriteAsync(data, 0, data.Length, token).ConfigureAwait(false); + Data.Seek(0, SeekOrigin.Begin); - _Response.ContentLength64 = ContentLength; - _Response.StatusCode = StatusCode; - _Response.StatusDescription = GetStatusDescription(StatusCode); - _Response.SendChunked = ChunkedTransfer; - _Response.ContentType = ContentType; + _Response.ContentLength64 = data.Length; + ContentLength = data.Length; + } + else + { + _Response.ContentLength64 = 0; + } - if (Headers != null && Headers.Count > 0) + try + { + if (_Request.Method != HttpMethod.HEAD) { - foreach (KeyValuePair header in Headers) + if (data != null && data.Length > 0) { - if (string.IsNullOrEmpty(header.Key)) + await _outputStream.WriteAsync(data, 0, data.Length, token).ConfigureAwait(false); + } + } + + await _outputStream.FlushAsync(token).ConfigureAwait(false); + _outputStream.Close(); + + _Response?.Close(); + + ResponseSent = true; + return true; + } + catch (Exception) + { + return false; + } + } + + /// + /// Send headers and data to the requestor and terminate. + /// + /// Number of bytes to send. + /// Stream containing the data. + /// Cancellation token useful for canceling the request. + /// True if successful. + public async Task Send(long contentLength, Stream stream, CancellationToken token = default) + { + if (ChunkedTransfer) + { + throw new IOException( + "Response is configured to use chunked transfer-encoding. Use SendChunk() and SendFinalChunk()."); + } + + ContentLength = contentLength; + if (!_HeadersSent) + { + SendHeaders(); + } + + try + { + if (_Request.Method != HttpMethod.HEAD) + { + if (stream != null && stream.CanRead && contentLength > 0) + { + var bytesRemaining = contentLength; + + Data = new MemoryStream(); + + while (bytesRemaining > 0) { - continue; + var bytesRead = 0; + var 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(); + + _Response?.Close(); + + ResponseSent = true; + return true; + } + catch (Exception) + { + return false; + } + } + + /// + /// Send headers (if not already sent) and a chunk of data using chunked transfer-encoding, and keep the connection + /// in-tact. + /// + /// Chunk of data. + /// + /// Number of bytes to send from the chunk, i.e. the actual data size (for example, return value of + /// FileStream.ReadAsync(buffer, 0, buffer.Length)). + /// + /// Cancellation token useful for canceling the request. + /// True if successful. + public async Task SendChunk(byte[] chunk, int numBytes, CancellationToken token = default) + { + if (!ChunkedTransfer) + { + throw new IOException( + "Response is not configured to use chunked transfer-encoding. Set ChunkedTransfer to true first, otherwise use Send()."); + } + + if (!_HeadersSent) + { + SendHeaders(); + } + + if (chunk != null && chunk.Length > 0) + { + ContentLength += chunk.Length; + } + + try + { + if (chunk == null || chunk.Length < 1) + { + chunk = new byte[0]; + } + + await _outputStream.WriteAsync(chunk, 0, numBytes, token).ConfigureAwait(false); + await _outputStream.FlushAsync(token).ConfigureAwait(false); + } + catch (Exception) + { + return false; + } + + return true; + } + + /// + /// Send headers (if not already sent) and the final chunk of data using chunked transfer-encoding and terminate the + /// connection. + /// + /// Chunk of data. + /// /// + /// + /// Number of bytes to send from the chunk, i.e. the actual data size (for example, return value of + /// FileStream.ReadAsync(buffer, 0, buffer.Length)). + /// + /// Cancellation token useful for canceling the request. + /// True if successful. + public async Task SendFinalChunk(byte[] chunk, int numBytes, CancellationToken token = default) + { + if (!ChunkedTransfer) + { + throw new IOException( + "Response is not configured to use chunked transfer-encoding. Set ChunkedTransfer to true first, otherwise use Send()."); + } + + if (!_HeadersSent) + { + SendHeaders(); + } + + if (chunk != null && chunk.Length > 0) + { + ContentLength += chunk.Length; + } + + try + { + if (chunk != null && chunk.Length > 0) + { + await _outputStream.WriteAsync(chunk, 0, numBytes, token).ConfigureAwait(false); + } + + var endChunk = new byte[0]; + await _outputStream.WriteAsync(endChunk, 0, endChunk.Length, token).ConfigureAwait(false); + + await _outputStream.FlushAsync(token).ConfigureAwait(false); + _outputStream.Close(); + + _Response?.Close(); + + ResponseSent = true; + return true; + } + catch (Exception) + { + return false; + } + } + + /// + /// Convert the response data sent using a Send() method to the object type specified using JSON deserialization. + /// + /// Type. + /// Object of type specified. + public T DataAsJsonObject() where T : class + { + var json = DataAsString; + if (string.IsNullOrEmpty(json)) + { + return null; + } + + return SerializationHelper.DeserializeJson(json); + } + + private void SendHeaders() + { + if (_HeadersSent) + { + throw new IOException("Headers already sent."); + } + + _Response.ContentLength64 = ContentLength; + _Response.StatusCode = StatusCode; + _Response.StatusDescription = GetStatusDescription(StatusCode); + _Response.SendChunked = ChunkedTransfer; + _Response.ContentType = ContentType; + + if (Headers != null && Headers.Count > 0) + { + foreach (var header in Headers) + { + if (string.IsNullOrEmpty(header.Key)) + { + continue; + } + + _Response.AddHeader(header.Key, header.Value); + } + } + + if (_Settings.Headers != null) + { + foreach (var header in _Settings.Headers) + { + if (!Headers.Any(h => h.Key.ToLower().Equals(header.Key.ToLower()))) + { _Response.AddHeader(header.Key, header.Value); } } - - if (_Settings.Headers != null) - { - foreach (KeyValuePair header in _Settings.Headers) - { - if (!Headers.Any(h => h.Key.ToLower().Equals(header.Key.ToLower()))) - { - _Response.AddHeader(header.Key, header.Value); - } - } - } - - if (_Response.Headers != null && _Response.Headers.HasKeys()) - { - try - { - _Response.Headers.Remove(System.Net.HttpResponseHeader.Server); - _Response.AddHeader("Server", "EonaCat Server"); - } - catch (Exception) - { - // do nothing - } - } - - _HeadersSent = true; } - private string GetStatusDescription(int statusCode) + if (_Response.Headers != null && _Response.Headers.HasKeys()) { - switch (statusCode) + try { - 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"; + _Response.Headers.Remove(HttpResponseHeader.Server); + _Response.AddHeader("Server", "EonaCat Server"); + } + catch (Exception) + { + // do nothing } } - private byte[] ReadStreamFully(Stream input) + _HeadersSent = true; + } + + private string GetStatusDescription(int statusCode) + { + switch (statusCode) { - if (input == null) - { - throw new ArgumentNullException(nameof(input)); - } + case 200: + return "OK"; - if (!input.CanRead) - { - throw new InvalidOperationException("Input stream is not readable"); - } + case 201: + return "Created"; - byte[] buffer = new byte[16 * 1024]; - using (MemoryStream ms = new MemoryStream()) - { - int read; + case 301: + return "Moved Permanently"; - while ((read = input.Read(buffer, 0, buffer.Length)) > 0) - { - ms.Write(buffer, 0, read); - } + case 302: + return "Moved Temporarily"; - byte[] ret = ms.ToArray(); - return ret; - } + 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[] ReadStreamFully(Stream input) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + + if (!input.CanRead) + { + throw new InvalidOperationException("Input stream is not readable"); + } + + var buffer = new byte[16 * 1024]; + using (var ms = new MemoryStream()) + { + int read; + + while ((read = input.Read(buffer, 0, buffer.Length)) > 0) ms.Write(buffer, 0, read); + + var ret = ms.ToArray(); + return ret; } } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/MimeTypes.cs b/EonaCat.Network/System/Web/MimeTypes.cs index 224216a..ef2f422 100644 --- a/EonaCat.Network/System/Web/MimeTypes.cs +++ b/EonaCat.Network/System/Web/MimeTypes.cs @@ -1,619 +1,619 @@ 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. +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. - /// - /// MIME types and file extensions. - /// - public class MimeTypes - { - private static readonly IDictionary data = new Dictionary(StringComparer.InvariantCultureIgnoreCase) { - {".323", "text/h323"}, - {".3g2", "video/3gpp2"}, - {".3gp", "video/3gpp"}, - {".3gp2", "video/3gpp2"}, - {".3gpp", "video/3gpp"}, - {".7z", "application/x-7z-compressed"}, - {".aa", "audio/audible"}, - {".AAC", "audio/aac"}, - {".aaf", "application/octet-stream"}, - {".aax", "audio/vnd.audible.aax"}, - {".ac3", "audio/ac3"}, - {".aca", "application/octet-stream"}, - {".accda", "application/msaccess.addin"}, - {".accdb", "application/msaccess"}, - {".accdc", "application/msaccess.cab"}, - {".accde", "application/msaccess"}, - {".accdr", "application/msaccess.runtime"}, - {".accdt", "application/msaccess"}, - {".accdw", "application/msaccess.webapplication"}, - {".accft", "application/msaccess.ftemplate"}, - {".acx", "application/internet-property-stream"}, - {".AddIn", "text/xml"}, - {".ade", "application/msaccess"}, - {".adobebridge", "application/x-bridge-url"}, - {".adp", "application/msaccess"}, - {".ADT", "audio/vnd.dlna.adts"}, - {".ADTS", "audio/aac"}, - {".afm", "application/octet-stream"}, - {".ai", "application/postscript"}, - {".aif", "audio/x-aiff"}, - {".aifc", "audio/aiff"}, - {".aiff", "audio/aiff"}, - {".air", "application/vnd.adobe.air-application-installer-package+zip"}, - {".amc", "application/x-mpeg"}, - {".application", "application/x-ms-application"}, - {".art", "image/x-jg"}, - {".asa", "application/xml"}, - {".asax", "application/xml"}, - {".ascx", "application/xml"}, - {".asd", "application/octet-stream"}, - {".asf", "video/x-ms-asf"}, - {".ashx", "application/xml"}, - {".asi", "application/octet-stream"}, - {".asm", "text/plain"}, - {".asmx", "application/xml"}, - {".aspx", "application/xml"}, - {".asr", "video/x-ms-asf"}, - {".asx", "video/x-ms-asf"}, - {".atom", "application/atom+xml"}, - {".au", "audio/basic"}, - {".avi", "video/x-msvideo"}, - {".axs", "application/olescript"}, - {".bas", "text/plain"}, - {".bcpio", "application/x-bcpio"}, - {".bin", "application/octet-stream"}, - {".bmp", "image/bmp"}, - {".c", "text/plain"}, - {".cab", "application/octet-stream"}, - {".caf", "audio/x-caf"}, - {".calx", "application/vnd.ms-office.calx"}, - {".cat", "application/vnd.ms-pki.seccat"}, - {".cc", "text/plain"}, - {".cd", "text/plain"}, - {".cdda", "audio/aiff"}, - {".cdf", "application/x-cdf"}, - {".cer", "application/x-x509-ca-cert"}, - {".chm", "application/octet-stream"}, - {".class", "application/x-java-applet"}, - {".clp", "application/x-msclip"}, - {".cmx", "image/x-cmx"}, - {".cnf", "text/plain"}, - {".cod", "image/cis-cod"}, - {".config", "application/xml"}, - {".contact", "text/x-ms-contact"}, - {".coverage", "application/xml"}, - {".cpio", "application/x-cpio"}, - {".cpp", "text/plain"}, - {".crd", "application/x-mscardfile"}, - {".crl", "application/pkix-crl"}, - {".crt", "application/x-x509-ca-cert"}, - {".cs", "text/plain"}, - {".csdproj", "text/plain"}, - {".csh", "application/x-csh"}, - {".csproj", "text/plain"}, - {".css", "text/css"}, - {".csv", "text/csv"}, - {".cur", "application/octet-stream"}, - {".cxx", "text/plain"}, - {".dat", "application/octet-stream"}, - {".datasource", "application/xml"}, - {".dbproj", "text/plain"}, - {".dcr", "application/x-director"}, - {".def", "text/plain"}, - {".deploy", "application/octet-stream"}, - {".der", "application/x-x509-ca-cert"}, - {".dgml", "application/xml"}, - {".dib", "image/bmp"}, - {".dif", "video/x-dv"}, - {".dir", "application/x-director"}, - {".disco", "text/xml"}, - {".divx", "video/divx"}, - {".dll", "application/x-msdownload"}, - {".dll.config", "text/xml"}, - {".dlm", "text/dlm"}, - {".doc", "application/msword"}, - {".docm", "application/vnd.ms-word.document.macroEnabled.12"}, - {".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, - {".dot", "application/msword"}, - {".dotm", "application/vnd.ms-word.template.macroEnabled.12"}, - {".dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template"}, - {".dsp", "application/octet-stream"}, - {".dsw", "text/plain"}, - {".dtd", "text/xml"}, - {".dtsConfig", "text/xml"}, - {".dv", "video/x-dv"}, - {".dvi", "application/x-dvi"}, - {".dwf", "drawing/x-dwf"}, - {".dwp", "application/octet-stream"}, - {".dxr", "application/x-director"}, - {".eml", "message/rfc822"}, - {".emz", "application/octet-stream"}, - {".eot", "application/octet-stream"}, - {".eps", "application/postscript"}, - {".etl", "application/etl"}, - {".etx", "text/x-setext"}, - {".evy", "application/envoy"}, - {".exe", "application/octet-stream"}, - {".exe.config", "text/xml"}, - {".fdf", "application/vnd.fdf"}, - {".fif", "application/fractals"}, - {".filters", "Application/xml"}, - {".fla", "application/octet-stream"}, - {".flr", "x-world/x-vrml"}, - {".flv", "video/x-flv"}, - {".fsscript", "application/fsharp-script"}, - {".fsx", "application/fsharp-script"}, - {".generictest", "application/xml"}, - {".gif", "image/gif"}, - {".group", "text/x-ms-group"}, - {".gsm", "audio/x-gsm"}, - {".gtar", "application/x-gtar"}, - {".gz", "application/x-gzip"}, - {".h", "text/plain"}, - {".hdf", "application/x-hdf"}, - {".hdml", "text/x-hdml"}, - {".hhc", "application/x-oleobject"}, - {".hhk", "application/octet-stream"}, - {".hhp", "application/octet-stream"}, - {".hlp", "application/winhlp"}, - {".hpp", "text/plain"}, - {".hqx", "application/mac-binhex40"}, - {".hta", "application/hta"}, - {".htc", "text/x-component"}, - {".htm", "text/html"}, - {".html", "text/html"}, - {".htt", "text/webviewhtml"}, - {".hxa", "application/xml"}, - {".hxc", "application/xml"}, - {".hxd", "application/octet-stream"}, - {".hxe", "application/xml"}, - {".hxf", "application/xml"}, - {".hxh", "application/octet-stream"}, - {".hxi", "application/octet-stream"}, - {".hxk", "application/xml"}, - {".hxq", "application/octet-stream"}, - {".hxr", "application/octet-stream"}, - {".hxs", "application/octet-stream"}, - {".hxt", "text/html"}, - {".hxv", "application/xml"}, - {".hxw", "application/octet-stream"}, - {".hxx", "text/plain"}, - {".i", "text/plain"}, - {".ico", "image/x-icon"}, - {".ics", "application/octet-stream"}, - {".idl", "text/plain"}, - {".ief", "image/ief"}, - {".iii", "application/x-iphone"}, - {".inc", "text/plain"}, - {".inf", "application/octet-stream"}, - {".inl", "text/plain"}, - {".ins", "application/x-internet-signup"}, - {".ipa", "application/x-itunes-ipa"}, - {".ipg", "application/x-itunes-ipg"}, - {".ipproj", "text/plain"}, - {".ipsw", "application/x-itunes-ipsw"}, - {".iqy", "text/x-ms-iqy"}, - {".isp", "application/x-internet-signup"}, - {".ite", "application/x-itunes-ite"}, - {".itlp", "application/x-itunes-itlp"}, - {".itms", "application/x-itunes-itms"}, - {".itpc", "application/x-itunes-itpc"}, - {".IVF", "video/x-ivf"}, - {".jar", "application/java-archive"}, - {".java", "application/octet-stream"}, - {".jck", "application/liquidmotion"}, - {".jcz", "application/liquidmotion"}, - {".jfif", "image/pjpeg"}, - {".jnlp", "application/x-java-jnlp-file"}, - {".jpb", "application/octet-stream"}, - {".jpe", "image/jpeg"}, - {".jpeg", "image/jpeg"}, - {".jpg", "image/jpeg"}, - {".js", "application/x-javascript"}, - {".json", "application/json"}, - {".jsx", "text/jscript"}, - {".jsxbin", "text/plain"}, - {".latex", "application/x-latex"}, - {".library-ms", "application/windows-library+xml"}, - {".lit", "application/x-ms-reader"}, - {".loadtest", "application/xml"}, - {".lpk", "application/octet-stream"}, - {".lsf", "video/x-la-asf"}, - {".lst", "text/plain"}, - {".lsx", "video/x-la-asf"}, - {".lzh", "application/octet-stream"}, - {".m13", "application/x-msmediaview"}, - {".m14", "application/x-msmediaview"}, - {".m1v", "video/mpeg"}, - {".m2t", "video/vnd.dlna.mpeg-tts"}, - {".m2ts", "video/vnd.dlna.mpeg-tts"}, - {".m2v", "video/mpeg"}, - {".m3u", "audio/x-mpegurl"}, - {".m3u8", "audio/x-mpegurl"}, - {".m4a", "audio/m4a"}, - {".m4b", "audio/m4b"}, - {".m4p", "audio/m4p"}, - {".m4r", "audio/x-m4r"}, - {".m4v", "video/x-m4v"}, - {".mac", "image/x-macpaint"}, - {".mak", "text/plain"}, - {".man", "application/x-troff-man"}, - {".manifest", "application/x-ms-manifest"}, - {".map", "text/plain"}, - {".master", "application/xml"}, - {".mda", "application/msaccess"}, - {".mdb", "application/x-msaccess"}, - {".mde", "application/msaccess"}, - {".mdp", "application/octet-stream"}, - {".me", "application/x-troff-me"}, - {".mfp", "application/x-shockwave-flash"}, - {".mht", "message/rfc822"}, - {".mhtml", "message/rfc822"}, - {".mid", "audio/mid"}, - {".midi", "audio/mid"}, - {".mix", "application/octet-stream"}, - {".mk", "text/plain"}, - {".mmf", "application/x-smaf"}, - {".mno", "text/xml"}, - {".mny", "application/x-msmoney"}, - {".mod", "video/mpeg"}, - {".mov", "video/quicktime"}, - {".movie", "video/x-sgi-movie"}, - {".mp2", "video/mpeg"}, - {".mp2v", "video/mpeg"}, - {".mp3", "audio/mpeg"}, - {".mp4", "video/mp4"}, - {".mp4v", "video/mp4"}, - {".mpa", "video/mpeg"}, - {".mpe", "video/mpeg"}, - {".mpeg", "video/mpeg"}, - {".mpf", "application/vnd.ms-mediapackage"}, - {".mpg", "video/mpeg"}, - {".mpp", "application/vnd.ms-project"}, - {".mpv2", "video/mpeg"}, - {".mqv", "video/quicktime"}, - {".ms", "application/x-troff-ms"}, - {".msi", "application/octet-stream"}, - {".mso", "application/octet-stream"}, - {".mts", "video/vnd.dlna.mpeg-tts"}, - {".mtx", "application/xml"}, - {".mvb", "application/x-msmediaview"}, - {".mvc", "application/x-miva-compiled"}, - {".mxp", "application/x-mmxp"}, - {".nc", "application/x-netcdf"}, - {".nsc", "video/x-ms-asf"}, - {".nws", "message/rfc822"}, - {".ocx", "application/octet-stream"}, - {".oda", "application/oda"}, - {".odb", "application/vnd.oasis.opendocument.database"}, - {".odc", "application/vnd.oasis.opendocument.chart"}, - {".odf", "application/vnd.oasis.opendocument.formula"}, - {".odg", "application/vnd.oasis.opendocument.graphics"}, - {".odh", "text/plain"}, - {".odi", "application/vnd.oasis.opendocument.image"}, - {".odl", "text/plain"}, - {".odm", "application/vnd.oasis.opendocument.text-master"}, - {".odp", "application/vnd.oasis.opendocument.presentation"}, - {".ods", "application/vnd.oasis.opendocument.spreadsheet"}, - {".odt", "application/vnd.oasis.opendocument.text"}, - {".ogv", "video/ogg"}, - {".one", "application/onenote"}, - {".onea", "application/onenote"}, - {".onepkg", "application/onenote"}, - {".onetmp", "application/onenote"}, - {".onetoc", "application/onenote"}, - {".onetoc2", "application/onenote"}, - {".orderedtest", "application/xml"}, - {".osdx", "application/opensearchdescription+xml"}, - {".otg", "application/vnd.oasis.opendocument.graphics-template"}, - {".oth", "application/vnd.oasis.opendocument.text-web"}, - {".otp", "application/vnd.oasis.opendocument.presentation-template"}, - {".ots", "application/vnd.oasis.opendocument.spreadsheet-template"}, - {".ott", "application/vnd.oasis.opendocument.text-template"}, - {".oxt", "application/vnd.openofficeorg.extension"}, - {".p10", "application/pkcs10"}, - {".p12", "application/x-pkcs12"}, - {".p7b", "application/x-pkcs7-certificates"}, - {".p7c", "application/pkcs7-mime"}, - {".p7m", "application/pkcs7-mime"}, - {".p7r", "application/x-pkcs7-certreqresp"}, - {".p7s", "application/pkcs7-signature"}, - {".pbm", "image/x-portable-bitmap"}, - {".pcast", "application/x-podcast"}, - {".pct", "image/pict"}, - {".pcx", "application/octet-stream"}, - {".pcz", "application/octet-stream"}, - {".pdf", "application/pdf"}, - {".pfb", "application/octet-stream"}, - {".pfm", "application/octet-stream"}, - {".pfx", "application/x-pkcs12"}, - {".pgm", "image/x-portable-graymap"}, - {".pic", "image/pict"}, - {".pict", "image/pict"}, - {".pkgdef", "text/plain"}, - {".pkgundef", "text/plain"}, - {".pko", "application/vnd.ms-pki.pko"}, - {".pls", "audio/scpls"}, - {".pma", "application/x-perfmon"}, - {".pmc", "application/x-perfmon"}, - {".pml", "application/x-perfmon"}, - {".pmr", "application/x-perfmon"}, - {".pmw", "application/x-perfmon"}, - {".png", "image/png"}, - {".pnm", "image/x-portable-anymap"}, - {".pnt", "image/x-macpaint"}, - {".pntg", "image/x-macpaint"}, - {".pnz", "image/png"}, - {".pot", "application/vnd.ms-powerpoint"}, - {".potm", "application/vnd.ms-powerpoint.template.macroEnabled.12"}, - {".potx", "application/vnd.openxmlformats-officedocument.presentationml.template"}, - {".ppa", "application/vnd.ms-powerpoint"}, - {".ppam", "application/vnd.ms-powerpoint.addin.macroEnabled.12"}, - {".ppm", "image/x-portable-pixmap"}, - {".pps", "application/vnd.ms-powerpoint"}, - {".ppsm", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12"}, - {".ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow"}, - {".ppt", "application/vnd.ms-powerpoint"}, - {".pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12"}, - {".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"}, - {".prf", "application/pics-rules"}, - {".prm", "application/octet-stream"}, - {".prx", "application/octet-stream"}, - {".ps", "application/postscript"}, - {".psc1", "application/PowerShell"}, - {".psd", "application/octet-stream"}, - {".psess", "application/xml"}, - {".psm", "application/octet-stream"}, - {".psp", "application/octet-stream"}, - {".pub", "application/x-mspublisher"}, - {".pwz", "application/vnd.ms-powerpoint"}, - {".qht", "text/x-html-insertion"}, - {".qhtm", "text/x-html-insertion"}, - {".qt", "video/quicktime"}, - {".qti", "image/x-quicktime"}, - {".qtif", "image/x-quicktime"}, - {".qtl", "application/x-quicktimeplayer"}, - {".qxd", "application/octet-stream"}, - {".ra", "audio/x-pn-realaudio"}, - {".ram", "audio/x-pn-realaudio"}, - {".rar", "application/octet-stream"}, - {".ras", "image/x-cmu-raster"}, - {".rat", "application/rat-file"}, - {".rc", "text/plain"}, - {".rc2", "text/plain"}, - {".rct", "text/plain"}, - {".rdlc", "application/xml"}, - {".resx", "application/xml"}, - {".rf", "image/vnd.rn-realflash"}, - {".rgb", "image/x-rgb"}, - {".rgs", "text/plain"}, - {".rm", "application/vnd.rn-realmedia"}, - {".rmi", "audio/mid"}, - {".rmp", "application/vnd.rn-rn_music_package"}, - {".roff", "application/x-troff"}, - {".rpm", "audio/x-pn-realaudio-plugin"}, - {".rqy", "text/x-ms-rqy"}, - {".rtf", "application/rtf"}, - {".rtx", "text/richtext"}, - {".ruleset", "application/xml"}, - {".s", "text/plain"}, - {".safariextz", "application/x-safari-safariextz"}, - {".scd", "application/x-msschedule"}, - {".sct", "text/scriptlet"}, - {".sd2", "audio/x-sd2"}, - {".sdp", "application/sdp"}, - {".sea", "application/octet-stream"}, - {".searchConnector-ms", "application/windows-search-connector+xml"}, - {".setpay", "application/set-payment-initiation"}, - {".setreg", "application/set-registration-initiation"}, - {".settings", "application/xml"}, - {".sgimb", "application/x-sgimb"}, - {".sgml", "text/sgml"}, - {".sh", "application/x-sh"}, - {".shar", "application/x-shar"}, - {".shtml", "text/html"}, - {".sit", "application/x-stuffit"}, - {".sitemap", "application/xml"}, - {".skin", "application/xml"}, - {".sldm", "application/vnd.ms-powerpoint.slide.macroEnabled.12"}, - {".sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide"}, - {".slk", "application/vnd.ms-excel"}, - {".sln", "text/plain"}, - {".slupkg-ms", "application/x-ms-license"}, - {".smd", "audio/x-smd"}, - {".smi", "application/octet-stream"}, - {".smx", "audio/x-smd"}, - {".smz", "audio/x-smd"}, - {".snd", "audio/basic"}, - {".snippet", "application/xml"}, - {".snp", "application/octet-stream"}, - {".sol", "text/plain"}, - {".sor", "text/plain"}, - {".spc", "application/x-pkcs7-certificates"}, - {".spl", "application/futuresplash"}, - {".src", "application/x-wais-source"}, - {".srf", "text/plain"}, - {".SSISDeploymentManifest", "text/xml"}, - {".ssm", "application/streamingmedia"}, - {".sst", "application/vnd.ms-pki.certstore"}, - {".stl", "application/vnd.ms-pki.stl"}, - {".sv4cpio", "application/x-sv4cpio"}, - {".sv4crc", "application/x-sv4crc"}, - {".svc", "application/xml"}, - {".svg", "image/svg+xml"}, - {".swf", "application/x-shockwave-flash"}, - {".t", "application/x-troff"}, - {".tar", "application/x-tar"}, - {".tcl", "application/x-tcl"}, - {".testrunconfig", "application/xml"}, - {".testsettings", "application/xml"}, - {".tex", "application/x-tex"}, - {".texi", "application/x-texinfo"}, - {".texinfo", "application/x-texinfo"}, - {".tgz", "application/x-compressed"}, - {".thmx", "application/vnd.ms-officetheme"}, - {".thn", "application/octet-stream"}, - {".tif", "image/tiff"}, - {".tiff", "image/tiff"}, - {".tlh", "text/plain"}, - {".tli", "text/plain"}, - {".toc", "application/octet-stream"}, - {".tr", "application/x-troff"}, - {".trm", "application/x-msterminal"}, - {".trx", "application/xml"}, - {".ts", "video/vnd.dlna.mpeg-tts"}, - {".tsv", "text/tab-separated-values"}, - {".ttf", "application/octet-stream"}, - {".tts", "video/vnd.dlna.mpeg-tts"}, - {".txt", "text/plain"}, - {".u32", "application/octet-stream"}, - {".uls", "text/iuls"}, - {".user", "text/plain"}, - {".ustar", "application/x-ustar"}, - {".vb", "text/plain"}, - {".vbdproj", "text/plain"}, - {".vbk", "video/mpeg"}, - {".vbproj", "text/plain"}, - {".vbs", "text/vbscript"}, - {".vcf", "text/x-vcard"}, - {".vcproj", "Application/xml"}, - {".vcs", "text/plain"}, - {".vcxproj", "Application/xml"}, - {".vddproj", "text/plain"}, - {".vdp", "text/plain"}, - {".vdproj", "text/plain"}, - {".vdx", "application/vnd.ms-visio.viewer"}, - {".vml", "text/xml"}, - {".vscontent", "application/xml"}, - {".vsct", "text/xml"}, - {".vsd", "application/vnd.visio"}, - {".vsi", "application/ms-vsi"}, - {".vsix", "application/vsix"}, - {".vsixlangpack", "text/xml"}, - {".vsixmanifest", "text/xml"}, - {".vsmdi", "application/xml"}, - {".vspscc", "text/plain"}, - {".vss", "application/vnd.visio"}, - {".vsscc", "text/plain"}, - {".vssettings", "text/xml"}, - {".vssscc", "text/plain"}, - {".vst", "application/vnd.visio"}, - {".vstemplate", "text/xml"}, - {".vsto", "application/x-ms-vsto"}, - {".vsw", "application/vnd.visio"}, - {".vsx", "application/vnd.visio"}, - {".vtx", "application/vnd.visio"}, - {".wav", "audio/wav"}, - {".wave", "audio/wav"}, - {".wax", "audio/x-ms-wax"}, - {".wbk", "application/msword"}, - {".wbmp", "image/vnd.wap.wbmp"}, - {".wcm", "application/vnd.ms-works"}, - {".wdb", "application/vnd.ms-works"}, - {".wdp", "image/vnd.ms-photo"}, - {".webarchive", "application/x-safari-webarchive"}, - {".webm", "video/webm"}, - {".webtest", "application/xml"}, - {".wiq", "application/xml"}, - {".wiz", "application/msword"}, - {".wks", "application/vnd.ms-works"}, - {".WLMP", "application/wlmoviemaker"}, - {".wlpginstall", "application/x-wlpg-detect"}, - {".wlpginstall3", "application/x-wlpg3-detect"}, - {".wm", "video/x-ms-wm"}, - {".wma", "audio/x-ms-wma"}, - {".wmd", "application/x-ms-wmd"}, - {".wmf", "application/x-msmetafile"}, - {".wml", "text/vnd.wap.wml"}, - {".wmlc", "application/vnd.wap.wmlc"}, - {".wmls", "text/vnd.wap.wmlscript"}, - {".wmlsc", "application/vnd.wap.wmlscriptc"}, - {".wmp", "video/x-ms-wmp"}, - {".wmv", "video/x-ms-wmv"}, - {".wmx", "video/x-ms-wmx"}, - {".wmz", "application/x-ms-wmz"}, - {".wpl", "application/vnd.ms-wpl"}, - {".wps", "application/vnd.ms-works"}, - {".wri", "application/x-mswrite"}, - {".wrl", "x-world/x-vrml"}, - {".wrz", "x-world/x-vrml"}, - {".wsc", "text/scriptlet"}, - {".wsdl", "text/xml"}, - {".wvx", "video/x-ms-wvx"}, - {".x", "application/directx"}, - {".xaf", "x-world/x-vrml"}, - {".xaml", "application/xaml+xml"}, - {".xap", "application/x-silverlight-app"}, - {".xbap", "application/x-ms-xbap"}, - {".xbm", "image/x-xbitmap"}, - {".xdr", "text/plain"}, - {".xht", "application/xhtml+xml"}, - {".xhtml", "application/xhtml+xml"}, - {".xla", "application/vnd.ms-excel"}, - {".xlam", "application/vnd.ms-excel.addin.macroEnabled.12"}, - {".xlc", "application/vnd.ms-excel"}, - {".xld", "application/vnd.ms-excel"}, - {".xlk", "application/vnd.ms-excel"}, - {".xll", "application/vnd.ms-excel"}, - {".xlm", "application/vnd.ms-excel"}, - {".xls", "application/vnd.ms-excel"}, - {".xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12"}, - {".xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12"}, - {".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, - {".xlt", "application/vnd.ms-excel"}, - {".xltm", "application/vnd.ms-excel.template.macroEnabled.12"}, - {".xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template"}, - {".xlw", "application/vnd.ms-excel"}, - {".xml", "text/xml"}, - {".xmta", "application/xml"}, - {".xof", "x-world/x-vrml"}, - {".XOML", "text/plain"}, - {".xpm", "image/x-xpixmap"}, - {".xps", "application/vnd.ms-xpsdocument"}, - {".xrm-ms", "text/xml"}, - {".xsc", "application/xml"}, - {".xsd", "text/xml"}, - {".xsf", "text/xml"}, - {".xsl", "text/xml"}, - {".xslt", "text/xml"}, - {".xsn", "application/octet-stream"}, - {".xss", "application/xml"}, - {".xtp", "application/octet-stream"}, - {".xwd", "image/x-xwindowdump"}, - {".z", "application/x-compress"}, - {".zip", "application/zip"}, +/// +/// MIME types and file extensions. +/// +public class MimeTypes +{ + private static readonly IDictionary data = + new Dictionary(StringComparer.InvariantCultureIgnoreCase) + { + { ".323", "text/h323" }, + { ".3g2", "video/3gpp2" }, + { ".3gp", "video/3gpp" }, + { ".3gp2", "video/3gpp2" }, + { ".3gpp", "video/3gpp" }, + { ".7z", "application/x-7z-compressed" }, + { ".aa", "audio/audible" }, + { ".AAC", "audio/aac" }, + { ".aaf", "application/octet-stream" }, + { ".aax", "audio/vnd.audible.aax" }, + { ".ac3", "audio/ac3" }, + { ".aca", "application/octet-stream" }, + { ".accda", "application/msaccess.addin" }, + { ".accdb", "application/msaccess" }, + { ".accdc", "application/msaccess.cab" }, + { ".accde", "application/msaccess" }, + { ".accdr", "application/msaccess.runtime" }, + { ".accdt", "application/msaccess" }, + { ".accdw", "application/msaccess.webapplication" }, + { ".accft", "application/msaccess.ftemplate" }, + { ".acx", "application/internet-property-stream" }, + { ".AddIn", "text/xml" }, + { ".ade", "application/msaccess" }, + { ".adobebridge", "application/x-bridge-url" }, + { ".adp", "application/msaccess" }, + { ".ADT", "audio/vnd.dlna.adts" }, + { ".ADTS", "audio/aac" }, + { ".afm", "application/octet-stream" }, + { ".ai", "application/postscript" }, + { ".aif", "audio/x-aiff" }, + { ".aifc", "audio/aiff" }, + { ".aiff", "audio/aiff" }, + { ".air", "application/vnd.adobe.air-application-installer-package+zip" }, + { ".amc", "application/x-mpeg" }, + { ".application", "application/x-ms-application" }, + { ".art", "image/x-jg" }, + { ".asa", "application/xml" }, + { ".asax", "application/xml" }, + { ".ascx", "application/xml" }, + { ".asd", "application/octet-stream" }, + { ".asf", "video/x-ms-asf" }, + { ".ashx", "application/xml" }, + { ".asi", "application/octet-stream" }, + { ".asm", "text/plain" }, + { ".asmx", "application/xml" }, + { ".aspx", "application/xml" }, + { ".asr", "video/x-ms-asf" }, + { ".asx", "video/x-ms-asf" }, + { ".atom", "application/atom+xml" }, + { ".au", "audio/basic" }, + { ".avi", "video/x-msvideo" }, + { ".axs", "application/olescript" }, + { ".bas", "text/plain" }, + { ".bcpio", "application/x-bcpio" }, + { ".bin", "application/octet-stream" }, + { ".bmp", "image/bmp" }, + { ".c", "text/plain" }, + { ".cab", "application/octet-stream" }, + { ".caf", "audio/x-caf" }, + { ".calx", "application/vnd.ms-office.calx" }, + { ".cat", "application/vnd.ms-pki.seccat" }, + { ".cc", "text/plain" }, + { ".cd", "text/plain" }, + { ".cdda", "audio/aiff" }, + { ".cdf", "application/x-cdf" }, + { ".cer", "application/x-x509-ca-cert" }, + { ".chm", "application/octet-stream" }, + { ".class", "application/x-java-applet" }, + { ".clp", "application/x-msclip" }, + { ".cmx", "image/x-cmx" }, + { ".cnf", "text/plain" }, + { ".cod", "image/cis-cod" }, + { ".config", "application/xml" }, + { ".contact", "text/x-ms-contact" }, + { ".coverage", "application/xml" }, + { ".cpio", "application/x-cpio" }, + { ".cpp", "text/plain" }, + { ".crd", "application/x-mscardfile" }, + { ".crl", "application/pkix-crl" }, + { ".crt", "application/x-x509-ca-cert" }, + { ".cs", "text/plain" }, + { ".csdproj", "text/plain" }, + { ".csh", "application/x-csh" }, + { ".csproj", "text/plain" }, + { ".css", "text/css" }, + { ".csv", "text/csv" }, + { ".cur", "application/octet-stream" }, + { ".cxx", "text/plain" }, + { ".dat", "application/octet-stream" }, + { ".datasource", "application/xml" }, + { ".dbproj", "text/plain" }, + { ".dcr", "application/x-director" }, + { ".def", "text/plain" }, + { ".deploy", "application/octet-stream" }, + { ".der", "application/x-x509-ca-cert" }, + { ".dgml", "application/xml" }, + { ".dib", "image/bmp" }, + { ".dif", "video/x-dv" }, + { ".dir", "application/x-director" }, + { ".disco", "text/xml" }, + { ".divx", "video/divx" }, + { ".dll", "application/x-msdownload" }, + { ".dll.config", "text/xml" }, + { ".dlm", "text/dlm" }, + { ".doc", "application/msword" }, + { ".docm", "application/vnd.ms-word.document.macroEnabled.12" }, + { ".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }, + { ".dot", "application/msword" }, + { ".dotm", "application/vnd.ms-word.template.macroEnabled.12" }, + { ".dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template" }, + { ".dsp", "application/octet-stream" }, + { ".dsw", "text/plain" }, + { ".dtd", "text/xml" }, + { ".dtsConfig", "text/xml" }, + { ".dv", "video/x-dv" }, + { ".dvi", "application/x-dvi" }, + { ".dwf", "drawing/x-dwf" }, + { ".dwp", "application/octet-stream" }, + { ".dxr", "application/x-director" }, + { ".eml", "message/rfc822" }, + { ".emz", "application/octet-stream" }, + { ".eot", "application/octet-stream" }, + { ".eps", "application/postscript" }, + { ".etl", "application/etl" }, + { ".etx", "text/x-setext" }, + { ".evy", "application/envoy" }, + { ".exe", "application/octet-stream" }, + { ".exe.config", "text/xml" }, + { ".fdf", "application/vnd.fdf" }, + { ".fif", "application/fractals" }, + { ".filters", "Application/xml" }, + { ".fla", "application/octet-stream" }, + { ".flr", "x-world/x-vrml" }, + { ".flv", "video/x-flv" }, + { ".fsscript", "application/fsharp-script" }, + { ".fsx", "application/fsharp-script" }, + { ".generictest", "application/xml" }, + { ".gif", "image/gif" }, + { ".group", "text/x-ms-group" }, + { ".gsm", "audio/x-gsm" }, + { ".gtar", "application/x-gtar" }, + { ".gz", "application/x-gzip" }, + { ".h", "text/plain" }, + { ".hdf", "application/x-hdf" }, + { ".hdml", "text/x-hdml" }, + { ".hhc", "application/x-oleobject" }, + { ".hhk", "application/octet-stream" }, + { ".hhp", "application/octet-stream" }, + { ".hlp", "application/winhlp" }, + { ".hpp", "text/plain" }, + { ".hqx", "application/mac-binhex40" }, + { ".hta", "application/hta" }, + { ".htc", "text/x-component" }, + { ".htm", "text/html" }, + { ".html", "text/html" }, + { ".htt", "text/webviewhtml" }, + { ".hxa", "application/xml" }, + { ".hxc", "application/xml" }, + { ".hxd", "application/octet-stream" }, + { ".hxe", "application/xml" }, + { ".hxf", "application/xml" }, + { ".hxh", "application/octet-stream" }, + { ".hxi", "application/octet-stream" }, + { ".hxk", "application/xml" }, + { ".hxq", "application/octet-stream" }, + { ".hxr", "application/octet-stream" }, + { ".hxs", "application/octet-stream" }, + { ".hxt", "text/html" }, + { ".hxv", "application/xml" }, + { ".hxw", "application/octet-stream" }, + { ".hxx", "text/plain" }, + { ".i", "text/plain" }, + { ".ico", "image/x-icon" }, + { ".ics", "application/octet-stream" }, + { ".idl", "text/plain" }, + { ".ief", "image/ief" }, + { ".iii", "application/x-iphone" }, + { ".inc", "text/plain" }, + { ".inf", "application/octet-stream" }, + { ".inl", "text/plain" }, + { ".ins", "application/x-internet-signup" }, + { ".ipa", "application/x-itunes-ipa" }, + { ".ipg", "application/x-itunes-ipg" }, + { ".ipproj", "text/plain" }, + { ".ipsw", "application/x-itunes-ipsw" }, + { ".iqy", "text/x-ms-iqy" }, + { ".isp", "application/x-internet-signup" }, + { ".ite", "application/x-itunes-ite" }, + { ".itlp", "application/x-itunes-itlp" }, + { ".itms", "application/x-itunes-itms" }, + { ".itpc", "application/x-itunes-itpc" }, + { ".IVF", "video/x-ivf" }, + { ".jar", "application/java-archive" }, + { ".java", "application/octet-stream" }, + { ".jck", "application/liquidmotion" }, + { ".jcz", "application/liquidmotion" }, + { ".jfif", "image/pjpeg" }, + { ".jnlp", "application/x-java-jnlp-file" }, + { ".jpb", "application/octet-stream" }, + { ".jpe", "image/jpeg" }, + { ".jpeg", "image/jpeg" }, + { ".jpg", "image/jpeg" }, + { ".js", "application/x-javascript" }, + { ".json", "application/json" }, + { ".jsx", "text/jscript" }, + { ".jsxbin", "text/plain" }, + { ".latex", "application/x-latex" }, + { ".library-ms", "application/windows-library+xml" }, + { ".lit", "application/x-ms-reader" }, + { ".loadtest", "application/xml" }, + { ".lpk", "application/octet-stream" }, + { ".lsf", "video/x-la-asf" }, + { ".lst", "text/plain" }, + { ".lsx", "video/x-la-asf" }, + { ".lzh", "application/octet-stream" }, + { ".m13", "application/x-msmediaview" }, + { ".m14", "application/x-msmediaview" }, + { ".m1v", "video/mpeg" }, + { ".m2t", "video/vnd.dlna.mpeg-tts" }, + { ".m2ts", "video/vnd.dlna.mpeg-tts" }, + { ".m2v", "video/mpeg" }, + { ".m3u", "audio/x-mpegurl" }, + { ".m3u8", "audio/x-mpegurl" }, + { ".m4a", "audio/m4a" }, + { ".m4b", "audio/m4b" }, + { ".m4p", "audio/m4p" }, + { ".m4r", "audio/x-m4r" }, + { ".m4v", "video/x-m4v" }, + { ".mac", "image/x-macpaint" }, + { ".mak", "text/plain" }, + { ".man", "application/x-troff-man" }, + { ".manifest", "application/x-ms-manifest" }, + { ".map", "text/plain" }, + { ".master", "application/xml" }, + { ".mda", "application/msaccess" }, + { ".mdb", "application/x-msaccess" }, + { ".mde", "application/msaccess" }, + { ".mdp", "application/octet-stream" }, + { ".me", "application/x-troff-me" }, + { ".mfp", "application/x-shockwave-flash" }, + { ".mht", "message/rfc822" }, + { ".mhtml", "message/rfc822" }, + { ".mid", "audio/mid" }, + { ".midi", "audio/mid" }, + { ".mix", "application/octet-stream" }, + { ".mk", "text/plain" }, + { ".mmf", "application/x-smaf" }, + { ".mno", "text/xml" }, + { ".mny", "application/x-msmoney" }, + { ".mod", "video/mpeg" }, + { ".mov", "video/quicktime" }, + { ".movie", "video/x-sgi-movie" }, + { ".mp2", "video/mpeg" }, + { ".mp2v", "video/mpeg" }, + { ".mp3", "audio/mpeg" }, + { ".mp4", "video/mp4" }, + { ".mp4v", "video/mp4" }, + { ".mpa", "video/mpeg" }, + { ".mpe", "video/mpeg" }, + { ".mpeg", "video/mpeg" }, + { ".mpf", "application/vnd.ms-mediapackage" }, + { ".mpg", "video/mpeg" }, + { ".mpp", "application/vnd.ms-project" }, + { ".mpv2", "video/mpeg" }, + { ".mqv", "video/quicktime" }, + { ".ms", "application/x-troff-ms" }, + { ".msi", "application/octet-stream" }, + { ".mso", "application/octet-stream" }, + { ".mts", "video/vnd.dlna.mpeg-tts" }, + { ".mtx", "application/xml" }, + { ".mvb", "application/x-msmediaview" }, + { ".mvc", "application/x-miva-compiled" }, + { ".mxp", "application/x-mmxp" }, + { ".nc", "application/x-netcdf" }, + { ".nsc", "video/x-ms-asf" }, + { ".nws", "message/rfc822" }, + { ".ocx", "application/octet-stream" }, + { ".oda", "application/oda" }, + { ".odb", "application/vnd.oasis.opendocument.database" }, + { ".odc", "application/vnd.oasis.opendocument.chart" }, + { ".odf", "application/vnd.oasis.opendocument.formula" }, + { ".odg", "application/vnd.oasis.opendocument.graphics" }, + { ".odh", "text/plain" }, + { ".odi", "application/vnd.oasis.opendocument.image" }, + { ".odl", "text/plain" }, + { ".odm", "application/vnd.oasis.opendocument.text-master" }, + { ".odp", "application/vnd.oasis.opendocument.presentation" }, + { ".ods", "application/vnd.oasis.opendocument.spreadsheet" }, + { ".odt", "application/vnd.oasis.opendocument.text" }, + { ".ogv", "video/ogg" }, + { ".one", "application/onenote" }, + { ".onea", "application/onenote" }, + { ".onepkg", "application/onenote" }, + { ".onetmp", "application/onenote" }, + { ".onetoc", "application/onenote" }, + { ".onetoc2", "application/onenote" }, + { ".orderedtest", "application/xml" }, + { ".osdx", "application/opensearchdescription+xml" }, + { ".otg", "application/vnd.oasis.opendocument.graphics-template" }, + { ".oth", "application/vnd.oasis.opendocument.text-web" }, + { ".otp", "application/vnd.oasis.opendocument.presentation-template" }, + { ".ots", "application/vnd.oasis.opendocument.spreadsheet-template" }, + { ".ott", "application/vnd.oasis.opendocument.text-template" }, + { ".oxt", "application/vnd.openofficeorg.extension" }, + { ".p10", "application/pkcs10" }, + { ".p12", "application/x-pkcs12" }, + { ".p7b", "application/x-pkcs7-certificates" }, + { ".p7c", "application/pkcs7-mime" }, + { ".p7m", "application/pkcs7-mime" }, + { ".p7r", "application/x-pkcs7-certreqresp" }, + { ".p7s", "application/pkcs7-signature" }, + { ".pbm", "image/x-portable-bitmap" }, + { ".pcast", "application/x-podcast" }, + { ".pct", "image/pict" }, + { ".pcx", "application/octet-stream" }, + { ".pcz", "application/octet-stream" }, + { ".pdf", "application/pdf" }, + { ".pfb", "application/octet-stream" }, + { ".pfm", "application/octet-stream" }, + { ".pfx", "application/x-pkcs12" }, + { ".pgm", "image/x-portable-graymap" }, + { ".pic", "image/pict" }, + { ".pict", "image/pict" }, + { ".pkgdef", "text/plain" }, + { ".pkgundef", "text/plain" }, + { ".pko", "application/vnd.ms-pki.pko" }, + { ".pls", "audio/scpls" }, + { ".pma", "application/x-perfmon" }, + { ".pmc", "application/x-perfmon" }, + { ".pml", "application/x-perfmon" }, + { ".pmr", "application/x-perfmon" }, + { ".pmw", "application/x-perfmon" }, + { ".png", "image/png" }, + { ".pnm", "image/x-portable-anymap" }, + { ".pnt", "image/x-macpaint" }, + { ".pntg", "image/x-macpaint" }, + { ".pnz", "image/png" }, + { ".pot", "application/vnd.ms-powerpoint" }, + { ".potm", "application/vnd.ms-powerpoint.template.macroEnabled.12" }, + { ".potx", "application/vnd.openxmlformats-officedocument.presentationml.template" }, + { ".ppa", "application/vnd.ms-powerpoint" }, + { ".ppam", "application/vnd.ms-powerpoint.addin.macroEnabled.12" }, + { ".ppm", "image/x-portable-pixmap" }, + { ".pps", "application/vnd.ms-powerpoint" }, + { ".ppsm", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12" }, + { ".ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow" }, + { ".ppt", "application/vnd.ms-powerpoint" }, + { ".pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12" }, + { ".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation" }, + { ".prf", "application/pics-rules" }, + { ".prm", "application/octet-stream" }, + { ".prx", "application/octet-stream" }, + { ".ps", "application/postscript" }, + { ".psc1", "application/PowerShell" }, + { ".psd", "application/octet-stream" }, + { ".psess", "application/xml" }, + { ".psm", "application/octet-stream" }, + { ".psp", "application/octet-stream" }, + { ".pub", "application/x-mspublisher" }, + { ".pwz", "application/vnd.ms-powerpoint" }, + { ".qht", "text/x-html-insertion" }, + { ".qhtm", "text/x-html-insertion" }, + { ".qt", "video/quicktime" }, + { ".qti", "image/x-quicktime" }, + { ".qtif", "image/x-quicktime" }, + { ".qtl", "application/x-quicktimeplayer" }, + { ".qxd", "application/octet-stream" }, + { ".ra", "audio/x-pn-realaudio" }, + { ".ram", "audio/x-pn-realaudio" }, + { ".rar", "application/octet-stream" }, + { ".ras", "image/x-cmu-raster" }, + { ".rat", "application/rat-file" }, + { ".rc", "text/plain" }, + { ".rc2", "text/plain" }, + { ".rct", "text/plain" }, + { ".rdlc", "application/xml" }, + { ".resx", "application/xml" }, + { ".rf", "image/vnd.rn-realflash" }, + { ".rgb", "image/x-rgb" }, + { ".rgs", "text/plain" }, + { ".rm", "application/vnd.rn-realmedia" }, + { ".rmi", "audio/mid" }, + { ".rmp", "application/vnd.rn-rn_music_package" }, + { ".roff", "application/x-troff" }, + { ".rpm", "audio/x-pn-realaudio-plugin" }, + { ".rqy", "text/x-ms-rqy" }, + { ".rtf", "application/rtf" }, + { ".rtx", "text/richtext" }, + { ".ruleset", "application/xml" }, + { ".s", "text/plain" }, + { ".safariextz", "application/x-safari-safariextz" }, + { ".scd", "application/x-msschedule" }, + { ".sct", "text/scriptlet" }, + { ".sd2", "audio/x-sd2" }, + { ".sdp", "application/sdp" }, + { ".sea", "application/octet-stream" }, + { ".searchConnector-ms", "application/windows-search-connector+xml" }, + { ".setpay", "application/set-payment-initiation" }, + { ".setreg", "application/set-registration-initiation" }, + { ".settings", "application/xml" }, + { ".sgimb", "application/x-sgimb" }, + { ".sgml", "text/sgml" }, + { ".sh", "application/x-sh" }, + { ".shar", "application/x-shar" }, + { ".shtml", "text/html" }, + { ".sit", "application/x-stuffit" }, + { ".sitemap", "application/xml" }, + { ".skin", "application/xml" }, + { ".sldm", "application/vnd.ms-powerpoint.slide.macroEnabled.12" }, + { ".sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide" }, + { ".slk", "application/vnd.ms-excel" }, + { ".sln", "text/plain" }, + { ".slupkg-ms", "application/x-ms-license" }, + { ".smd", "audio/x-smd" }, + { ".smi", "application/octet-stream" }, + { ".smx", "audio/x-smd" }, + { ".smz", "audio/x-smd" }, + { ".snd", "audio/basic" }, + { ".snippet", "application/xml" }, + { ".snp", "application/octet-stream" }, + { ".sol", "text/plain" }, + { ".sor", "text/plain" }, + { ".spc", "application/x-pkcs7-certificates" }, + { ".spl", "application/futuresplash" }, + { ".src", "application/x-wais-source" }, + { ".srf", "text/plain" }, + { ".SSISDeploymentManifest", "text/xml" }, + { ".ssm", "application/streamingmedia" }, + { ".sst", "application/vnd.ms-pki.certstore" }, + { ".stl", "application/vnd.ms-pki.stl" }, + { ".sv4cpio", "application/x-sv4cpio" }, + { ".sv4crc", "application/x-sv4crc" }, + { ".svc", "application/xml" }, + { ".svg", "image/svg+xml" }, + { ".swf", "application/x-shockwave-flash" }, + { ".t", "application/x-troff" }, + { ".tar", "application/x-tar" }, + { ".tcl", "application/x-tcl" }, + { ".testrunconfig", "application/xml" }, + { ".testsettings", "application/xml" }, + { ".tex", "application/x-tex" }, + { ".texi", "application/x-texinfo" }, + { ".texinfo", "application/x-texinfo" }, + { ".tgz", "application/x-compressed" }, + { ".thmx", "application/vnd.ms-officetheme" }, + { ".thn", "application/octet-stream" }, + { ".tif", "image/tiff" }, + { ".tiff", "image/tiff" }, + { ".tlh", "text/plain" }, + { ".tli", "text/plain" }, + { ".toc", "application/octet-stream" }, + { ".tr", "application/x-troff" }, + { ".trm", "application/x-msterminal" }, + { ".trx", "application/xml" }, + { ".ts", "video/vnd.dlna.mpeg-tts" }, + { ".tsv", "text/tab-separated-values" }, + { ".ttf", "application/octet-stream" }, + { ".tts", "video/vnd.dlna.mpeg-tts" }, + { ".txt", "text/plain" }, + { ".u32", "application/octet-stream" }, + { ".uls", "text/iuls" }, + { ".user", "text/plain" }, + { ".ustar", "application/x-ustar" }, + { ".vb", "text/plain" }, + { ".vbdproj", "text/plain" }, + { ".vbk", "video/mpeg" }, + { ".vbproj", "text/plain" }, + { ".vbs", "text/vbscript" }, + { ".vcf", "text/x-vcard" }, + { ".vcproj", "Application/xml" }, + { ".vcs", "text/plain" }, + { ".vcxproj", "Application/xml" }, + { ".vddproj", "text/plain" }, + { ".vdp", "text/plain" }, + { ".vdproj", "text/plain" }, + { ".vdx", "application/vnd.ms-visio.viewer" }, + { ".vml", "text/xml" }, + { ".vscontent", "application/xml" }, + { ".vsct", "text/xml" }, + { ".vsd", "application/vnd.visio" }, + { ".vsi", "application/ms-vsi" }, + { ".vsix", "application/vsix" }, + { ".vsixlangpack", "text/xml" }, + { ".vsixmanifest", "text/xml" }, + { ".vsmdi", "application/xml" }, + { ".vspscc", "text/plain" }, + { ".vss", "application/vnd.visio" }, + { ".vsscc", "text/plain" }, + { ".vssettings", "text/xml" }, + { ".vssscc", "text/plain" }, + { ".vst", "application/vnd.visio" }, + { ".vstemplate", "text/xml" }, + { ".vsto", "application/x-ms-vsto" }, + { ".vsw", "application/vnd.visio" }, + { ".vsx", "application/vnd.visio" }, + { ".vtx", "application/vnd.visio" }, + { ".wav", "audio/wav" }, + { ".wave", "audio/wav" }, + { ".wax", "audio/x-ms-wax" }, + { ".wbk", "application/msword" }, + { ".wbmp", "image/vnd.wap.wbmp" }, + { ".wcm", "application/vnd.ms-works" }, + { ".wdb", "application/vnd.ms-works" }, + { ".wdp", "image/vnd.ms-photo" }, + { ".webarchive", "application/x-safari-webarchive" }, + { ".webm", "video/webm" }, + { ".webtest", "application/xml" }, + { ".wiq", "application/xml" }, + { ".wiz", "application/msword" }, + { ".wks", "application/vnd.ms-works" }, + { ".WLMP", "application/wlmoviemaker" }, + { ".wlpginstall", "application/x-wlpg-detect" }, + { ".wlpginstall3", "application/x-wlpg3-detect" }, + { ".wm", "video/x-ms-wm" }, + { ".wma", "audio/x-ms-wma" }, + { ".wmd", "application/x-ms-wmd" }, + { ".wmf", "application/x-msmetafile" }, + { ".wml", "text/vnd.wap.wml" }, + { ".wmlc", "application/vnd.wap.wmlc" }, + { ".wmls", "text/vnd.wap.wmlscript" }, + { ".wmlsc", "application/vnd.wap.wmlscriptc" }, + { ".wmp", "video/x-ms-wmp" }, + { ".wmv", "video/x-ms-wmv" }, + { ".wmx", "video/x-ms-wmx" }, + { ".wmz", "application/x-ms-wmz" }, + { ".wpl", "application/vnd.ms-wpl" }, + { ".wps", "application/vnd.ms-works" }, + { ".wri", "application/x-mswrite" }, + { ".wrl", "x-world/x-vrml" }, + { ".wrz", "x-world/x-vrml" }, + { ".wsc", "text/scriptlet" }, + { ".wsdl", "text/xml" }, + { ".wvx", "video/x-ms-wvx" }, + { ".x", "application/directx" }, + { ".xaf", "x-world/x-vrml" }, + { ".xaml", "application/xaml+xml" }, + { ".xap", "application/x-silverlight-app" }, + { ".xbap", "application/x-ms-xbap" }, + { ".xbm", "image/x-xbitmap" }, + { ".xdr", "text/plain" }, + { ".xht", "application/xhtml+xml" }, + { ".xhtml", "application/xhtml+xml" }, + { ".xla", "application/vnd.ms-excel" }, + { ".xlam", "application/vnd.ms-excel.addin.macroEnabled.12" }, + { ".xlc", "application/vnd.ms-excel" }, + { ".xld", "application/vnd.ms-excel" }, + { ".xlk", "application/vnd.ms-excel" }, + { ".xll", "application/vnd.ms-excel" }, + { ".xlm", "application/vnd.ms-excel" }, + { ".xls", "application/vnd.ms-excel" }, + { ".xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12" }, + { ".xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12" }, + { ".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }, + { ".xlt", "application/vnd.ms-excel" }, + { ".xltm", "application/vnd.ms-excel.template.macroEnabled.12" }, + { ".xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template" }, + { ".xlw", "application/vnd.ms-excel" }, + { ".xml", "text/xml" }, + { ".xmta", "application/xml" }, + { ".xof", "x-world/x-vrml" }, + { ".XOML", "text/plain" }, + { ".xpm", "image/x-xpixmap" }, + { ".xps", "application/vnd.ms-xpsdocument" }, + { ".xrm-ms", "text/xml" }, + { ".xsc", "application/xml" }, + { ".xsd", "text/xml" }, + { ".xsf", "text/xml" }, + { ".xsl", "text/xml" }, + { ".xslt", "text/xml" }, + { ".xsn", "application/octet-stream" }, + { ".xss", "application/xml" }, + { ".xtp", "application/octet-stream" }, + { ".xwd", "image/x-xwindowdump" }, + { ".z", "application/x-compress" }, + { ".zip", "application/zip" } }; - /// - /// Instantiates the object. - /// - public MimeTypes() + /// + /// Instantiates the object. + /// + public MimeTypes() + { + } + + /// + /// Retrieve MIME type from file extension. + /// + /// File extension. + /// String containing MIME type. + public static string GetFromExtension(string extension) + { + if (string.IsNullOrEmpty(nameof(extension))) { + return null; } - /// - /// Retrieve MIME type from file extension. - /// - /// File extension. - /// String containing MIME type. - public static string GetFromExtension(string extension) + if (!extension.StartsWith(".")) { - if (string.IsNullOrEmpty(nameof(extension))) - { - return null; - } - - if (!extension.StartsWith(".")) - { - extension = "." + extension; - } - - return data.TryGetValue(extension.ToLower(), out string mime) ? mime : "application/octet-stream"; + extension = "." + extension; } + + return data.TryGetValue(extension.ToLower(), out var mime) ? mime : "application/octet-stream"; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/ParameterRoute.cs b/EonaCat.Network/System/Web/ParameterRoute.cs index ded0cb7..0c6f1d9 100644 --- a/EonaCat.Network/System/Web/ParameterRoute.cs +++ b/EonaCat.Network/System/Web/ParameterRoute.cs @@ -1,80 +1,79 @@ -using EonaCat.Json; -using System; +using System; using System.Threading.Tasks; +using EonaCat.Json; -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Assign a method handler for when requests are received matching the supplied method and path containing parameters. +/// +public class ParameterRoute { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - /// - /// Assign a method handler for when requests are received matching the supplied method and path containing parameters. + /// Create a new route object. /// - public class ParameterRoute + /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + /// The pattern against which the raw URL should be matched. + /// The method that should be called to handle the request. + /// Globally-unique identifier. + /// User-supplied metadata. + public ParameterRoute(HttpMethod method, string path, Func handler, string guid = null, + object metadata = null) { - /// - /// Globally-unique identifier. - /// - [JsonProperty(Order = -1)] - public string GUID { get; set; } = Guid.NewGuid().ToString(); - - /// - /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. - /// - [JsonProperty(Order = 0)] - public HttpMethod Method { get; set; } = HttpMethod.GET; - - /// - /// The pattern against which the raw URL should be matched. - /// - [JsonProperty(Order = 1)] - public string Path { get; set; } = null; - - /// - /// The handler for the parameter route. - /// - [JsonIgnore] - public Func Handler { get; set; } = null; - - /// - /// User-supplied metadata. - /// - [JsonProperty(Order = 999)] - public object Metadata { get; set; } = null; - - /// - /// Create a new route object. - /// - /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. - /// The pattern against which the raw URL should be matched. - /// The method that should be called to handle the request. - /// Globally-unique identifier. - /// User-supplied metadata. - public ParameterRoute(HttpMethod method, string path, Func handler, string guid = null, object metadata = null) + if (string.IsNullOrEmpty(path)) { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } + throw new ArgumentNullException(nameof(path)); + } - if (handler == null) - { - throw new ArgumentNullException(nameof(handler)); - } + if (handler == null) + { + throw new ArgumentNullException(nameof(handler)); + } - Method = method; - Path = path; - Handler = handler; + Method = method; + Path = path; + Handler = handler; - if (!string.IsNullOrEmpty(guid)) - { - GUID = guid; - } + if (!string.IsNullOrEmpty(guid)) + { + GUID = guid; + } - if (metadata != null) - { - Metadata = metadata; - } + if (metadata != null) + { + Metadata = metadata; } } + + /// + /// Globally-unique identifier. + /// + [JsonProperty(Order = -1)] + public string GUID { get; set; } = Guid.NewGuid().ToString(); + + /// + /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + /// + [JsonProperty(Order = 0)] + public HttpMethod Method { get; set; } = HttpMethod.GET; + + /// + /// The pattern against which the raw URL should be matched. + /// + [JsonProperty(Order = 1)] + public string Path { get; set; } + + /// + /// The handler for the parameter route. + /// + [JsonIgnore] + public Func Handler { get; set; } + + /// + /// User-supplied metadata. + /// + [JsonProperty(Order = 999)] + public object Metadata { get; set; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/ParameterRouteManager.cs b/EonaCat.Network/System/Web/ParameterRouteManager.cs index 6a6db5a..3f24681 100644 --- a/EonaCat.Network/System/Web/ParameterRouteManager.cs +++ b/EonaCat.Network/System/Web/ParameterRouteManager.cs @@ -1,175 +1,177 @@ -using EonaCat.UrlMatch; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using EonaCat.UrlMatch; -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Parameter route manager. Parameter routes are used for requests using any HTTP method to any path where parameters +/// are defined in the URL. +/// For example, /{version}/api. +/// For a matching URL, the HttpRequest.Url.Parameters will contain a key called 'version' with the value found in the +/// URL. +/// +public class ParameterRouteManager { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private readonly object _Lock = new(); + private readonly Dictionary> _Routes = new(); /// - /// 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. + /// Instantiate the object. /// - public class ParameterRouteManager + public ParameterRouteManager() { - /// - /// Directly access the underlying URL matching library. - /// This is helpful in case you want to specify the matching behavior should multiple matches exist. - /// - public Matcher Matcher { get; } = new Matcher(); + } - private readonly object _Lock = new object(); - private readonly Dictionary> _Routes = new Dictionary>(); + /// + /// Directly access the underlying URL matching library. + /// This is helpful in case you want to specify the matching behavior should multiple matches exist. + /// + public Matcher Matcher { get; } = new(); - /// - /// Instantiate the object. - /// - public ParameterRouteManager() + /// + /// Add a route. + /// + /// The HTTP method. + /// URL path, i.e. /path/to/resource. + /// Method to invoke. + /// Globally-unique identifier. + /// User-supplied metadata. + public void Add(HttpMethod method, string path, Func handler, string guid = null, + object metadata = null) + { + if (string.IsNullOrEmpty(path)) { + throw new ArgumentNullException(nameof(path)); } - /// - /// Add a route. - /// - /// The HTTP method. - /// URL path, i.e. /path/to/resource. - /// Method to invoke. - /// Globally-unique identifier. - /// User-supplied metadata. - public void Add(HttpMethod method, string path, Func handler, string guid = null, object metadata = null) + if (handler == 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); - } + throw new ArgumentNullException(nameof(handler)); } - /// - /// Remove a route. - /// - /// The HTTP method. - /// URL path. - public void Remove(HttpMethod method, string path) + lock (_Lock) { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } + var pr = new ParameterRoute(method, path, handler, guid, metadata); + _Routes.Add(pr, handler); + } + } - lock (_Lock) + /// + /// Remove a route. + /// + /// The HTTP method. + /// URL path. + public void Remove(HttpMethod method, string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException(nameof(path)); + } + + lock (_Lock) + { + if (_Routes.Any(r => r.Key.Method == method && r.Key.Path.Equals(path))) { - if (_Routes.Any(r => r.Key.Method == method && r.Key.Path.Equals(path))) + var removeList = _Routes.Where(r => r.Key.Method == method && r.Key.Path.Equals(path)) + .Select(r => r.Key) + .ToList(); + + foreach (var remove in removeList) { - List removeList = _Routes.Where(r => r.Key.Method == method && r.Key.Path.Equals(path)) - .Select(r => r.Key) - .ToList(); - - foreach (ParameterRoute remove in removeList) - { - _Routes.Remove(remove); - } + _Routes.Remove(remove); } } } + } - /// - /// Retrieve a parameter route. - /// - /// The HTTP method. - /// URL path. - /// ParameterRoute if the route exists, otherwise null. - public ParameterRoute Get(HttpMethod method, string path) + /// + /// Retrieve a parameter route. + /// + /// The HTTP method. + /// URL path. + /// ParameterRoute if the route exists, otherwise null. + public ParameterRoute Get(HttpMethod method, string path) + { + if (string.IsNullOrEmpty(path)) { - 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; + throw new ArgumentNullException(nameof(path)); } - /// - /// Check if a content route exists. - /// - /// The HTTP method. - /// URL path. - /// True if exists. - public bool Exists(HttpMethod method, string path) + lock (_Lock) { - if (string.IsNullOrEmpty(path)) + if (_Routes.Any(r => r.Key.Method == method && r.Key.Path.Equals(path))) { - throw new ArgumentNullException(nameof(path)); - } - - lock (_Lock) - { - return _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; } } - /// - /// Match a request method and URL to a handler method. - /// - /// The HTTP method. - /// URL path. - /// Values extracted from the URL. - /// Matching route. - /// True if match exists. - public Func Match(HttpMethod method, string path, out Dictionary vals, out ParameterRoute pr) + return null; + } + + /// + /// Check if a content route exists. + /// + /// The HTTP method. + /// URL path. + /// True if exists. + public bool Exists(HttpMethod method, string path) + { + if (string.IsNullOrEmpty(path)) { - pr = null; - vals = null; - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } + throw new ArgumentNullException(nameof(path)); + } - string consolidatedPath = BuildConsolidatedPath(method, path); + lock (_Lock) + { + return _Routes.Any(r => r.Key.Method == method && r.Key.Path.Equals(path)); + } + } - lock (_Lock) + /// + /// Match a request method and URL to a handler method. + /// + /// The HTTP method. + /// URL path. + /// Values extracted from the URL. + /// Matching route. + /// True if match exists. + public Func Match(HttpMethod method, string path, out Dictionary vals, + out ParameterRoute pr) + { + pr = null; + vals = null; + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException(nameof(path)); + } + + var consolidatedPath = BuildConsolidatedPath(method, path); + + lock (_Lock) + { + foreach (var route in _Routes) { - foreach (KeyValuePair> route in _Routes) - { - if (Matcher.Match( + if (Matcher.Match( consolidatedPath, BuildConsolidatedPath(route.Key.Method, route.Key.Path), out vals)) - { - pr = route.Key; - return route.Value; - } + { + pr = route.Key; + return route.Value; } } - - return null; } - private string BuildConsolidatedPath(HttpMethod method, string path) - { - return method.ToString() + " " + path; - } + return null; + } + + private string BuildConsolidatedPath(HttpMethod method, string path) + { + return method + " " + path; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/RouteTypeEnum.cs b/EonaCat.Network/System/Web/RouteTypeEnum.cs index 79c52e5..0bdfc21 100644 --- a/EonaCat.Network/System/Web/RouteTypeEnum.cs +++ b/EonaCat.Network/System/Web/RouteTypeEnum.cs @@ -1,45 +1,39 @@ -using EonaCat.Json.Converters; -using System.Runtime.Serialization; +using System.Runtime.Serialization; +using EonaCat.Json; +using EonaCat.Json.Converters; -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Route type. +/// +[Converter(typeof(StringEnumConverter))] +public enum RouteTypeEnum { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + /// + /// Default route. + /// + [EnumMember(Value = "Default")] Default, /// - /// Route type. + /// Content route. /// - [Json.Converter(typeof(StringEnumConverter))] - public enum RouteTypeEnum - { - /// - /// Default route. - /// - [EnumMember(Value = "Default")] - Default, + [EnumMember(Value = "Content")] Content, - /// - /// Content route. - /// - [EnumMember(Value = "Content")] - Content, + /// + /// Static route. + /// + [EnumMember(Value = "Static")] Static, - /// - /// Static route. - /// - [EnumMember(Value = "Static")] - Static, + /// + /// Parameter route. + /// + [EnumMember(Value = "Parameter")] Parameter, - /// - /// Parameter route. - /// - [EnumMember(Value = "Parameter")] - Parameter, - - /// - /// Dynamic route. - /// - [EnumMember(Value = "Dynamic")] - Dynamic - } + /// + /// Dynamic route. + /// + [EnumMember(Value = "Dynamic")] Dynamic } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/StaticRoute.cs b/EonaCat.Network/System/Web/StaticRoute.cs index aed6c3f..f3f096d 100644 --- a/EonaCat.Network/System/Web/StaticRoute.cs +++ b/EonaCat.Network/System/Web/StaticRoute.cs @@ -1,91 +1,90 @@ -using EonaCat.Json; -using System; +using System; using System.Threading.Tasks; +using EonaCat.Json; -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Assign a method handler for when requests are received matching the supplied method and path. +/// +public class StaticRoute { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - /// - /// Assign a method handler for when requests are received matching the supplied method and path. + /// Create a new route object. /// - public class StaticRoute + /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + /// The raw URL, i.e. /foo/bar/. Be sure this begins and ends with '/'. + /// The method that should be called to handle the request. + /// Globally-unique identifier. + /// User-supplied metadata. + public StaticRoute(HttpMethod method, string path, Func handler, string guid = null, + object metadata = null) { - /// - /// Globally-unique identifier. - /// - [JsonProperty(Order = -1)] - public string GUID { get; set; } = Guid.NewGuid().ToString(); - - /// - /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. - /// - [JsonProperty(Order = 0)] - public HttpMethod Method { get; set; } = HttpMethod.GET; - - /// - /// The raw URL, i.e. /foo/bar/. Be sure this begins and ends with '/'. - /// - [JsonProperty(Order = 1)] - public string Path { get; set; } = null; - - /// - /// The handler for the static route. - /// - [JsonIgnore] - public Func Handler { get; set; } = null; - - /// - /// User-supplied metadata. - /// - [JsonProperty(Order = 999)] - public object Metadata { get; set; } = null; - - /// - /// Create a new route object. - /// - /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. - /// The raw URL, i.e. /foo/bar/. Be sure this begins and ends with '/'. - /// The method that should be called to handle the request. - /// Globally-unique identifier. - /// User-supplied metadata. - public StaticRoute(HttpMethod method, string path, Func handler, string guid = null, object metadata = null) + if (string.IsNullOrEmpty(path)) { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } + throw new ArgumentNullException(nameof(path)); + } - if (handler == null) - { - throw new ArgumentNullException(nameof(handler)); - } + if (handler == null) + { + throw new ArgumentNullException(nameof(handler)); + } - Method = method; + Method = method; - Path = path.ToLower(); - if (!Path.StartsWith("/")) - { - Path = "/" + Path; - } + Path = path.ToLower(); + if (!Path.StartsWith("/")) + { + Path = "/" + Path; + } - if (!Path.EndsWith("/")) - { - Path = Path + "/"; - } + if (!Path.EndsWith("/")) + { + Path = Path + "/"; + } - Handler = handler; + Handler = handler; - if (!string.IsNullOrEmpty(guid)) - { - GUID = guid; - } + if (!string.IsNullOrEmpty(guid)) + { + GUID = guid; + } - if (metadata != null) - { - Metadata = metadata; - } + if (metadata != null) + { + Metadata = metadata; } } + + /// + /// Globally-unique identifier. + /// + [JsonProperty(Order = -1)] + public string GUID { get; set; } = Guid.NewGuid().ToString(); + + /// + /// The HTTP method, i.e. GET, PUT, POST, DELETE, etc. + /// + [JsonProperty(Order = 0)] + public HttpMethod Method { get; set; } = HttpMethod.GET; + + /// + /// The raw URL, i.e. /foo/bar/. Be sure this begins and ends with '/'. + /// + [JsonProperty(Order = 1)] + public string Path { get; set; } + + /// + /// The handler for the static route. + /// + [JsonIgnore] + public Func Handler { get; set; } + + /// + /// User-supplied metadata. + /// + [JsonProperty(Order = 999)] + public object Metadata { get; set; } } \ No newline at end of file diff --git a/EonaCat.Network/System/Web/StaticRouteManager.cs b/EonaCat.Network/System/Web/StaticRouteManager.cs index b4dec20..f7333c7 100644 --- a/EonaCat.Network/System/Web/StaticRouteManager.cs +++ b/EonaCat.Network/System/Web/StaticRouteManager.cs @@ -3,220 +3,211 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -namespace EonaCat.Network +namespace EonaCat.Network; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Static route manager. Static routes are used for requests using any HTTP method to a specific path. +/// +public class StaticRouteManager { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. + private readonly object _Lock = new(); + private readonly List _Routes = new(); /// - /// Static route manager. Static routes are used for requests using any HTTP method to a specific path. + /// Instantiate the object. /// - public class StaticRouteManager + public StaticRouteManager() { - private readonly List _Routes = new List(); - private readonly object _Lock = new object(); + } - /// - /// Instantiate the object. - /// - public StaticRouteManager() + /// + /// Add a route. + /// + /// The HTTP method. + /// URL path, i.e. /path/to/resource. + /// Method to invoke. + /// Globally-unique identifier. + /// User-supplied metadata. + public void Add(HttpMethod method, string path, Func handler, string guid = null, + object metadata = null) + { + if (string.IsNullOrEmpty(path)) { + throw new ArgumentNullException(nameof(path)); } - /// - /// Add a route. - /// - /// The HTTP method. - /// URL path, i.e. /path/to/resource. - /// Method to invoke. - /// Globally-unique identifier. - /// User-supplied metadata. - public void Add(HttpMethod method, string path, Func handler, string guid = null, object metadata = null) + if (handler == null) { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } - - if (handler == null) - { - throw new ArgumentNullException(nameof(handler)); - } - - StaticRoute r = new StaticRoute(method, path, handler, guid, metadata); - Add(r); + throw new ArgumentNullException(nameof(handler)); } - /// - /// Remove a route. - /// - /// The HTTP method. - /// URL path. - public void Remove(HttpMethod method, string path) + var r = new StaticRoute(method, path, handler, guid, metadata); + Add(r); + } + + /// + /// Remove a route. + /// + /// The HTTP method. + /// URL path. + public void Remove(HttpMethod method, string path) + { + if (string.IsNullOrEmpty(path)) { - if (string.IsNullOrEmpty(path)) + throw new ArgumentNullException(nameof(path)); + } + + var r = Get(method, path); + if (r == null || r == default(StaticRoute)) + { + return; + } + + lock (_Lock) + { + _Routes.Remove(r); + } + } + + /// + /// Retrieve a static route. + /// + /// The HTTP method. + /// URL path. + /// StaticRoute if the route exists, otherwise null. + public StaticRoute Get(HttpMethod method, string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException(nameof(path)); + } + + path = path.ToLower(); + if (!path.StartsWith("/")) + { + path = "/" + path; + } + + if (!path.EndsWith("/")) + { + path = path + "/"; + } + + lock (_Lock) + { + var curr = _Routes.FirstOrDefault(i => i.Method == method && i.Path == path); + if (curr == null || curr == default(StaticRoute)) { - throw new ArgumentNullException(nameof(path)); + return null; } - StaticRoute r = Get(method, path); - if (r == null || r == default(StaticRoute)) - { - return; - } - else - { - lock (_Lock) - { - _Routes.Remove(r); - } + return curr; + } + } - return; + /// + /// Check if a static route exists. + /// + /// The HTTP method. + /// URL path. + /// True if exists. + public bool Exists(HttpMethod method, string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException(nameof(path)); + } + + path = path.ToLower(); + if (!path.StartsWith("/")) + { + path = "/" + path; + } + + if (!path.EndsWith("/")) + { + path = path + "/"; + } + + lock (_Lock) + { + var curr = _Routes.FirstOrDefault(i => i.Method == method && i.Path == path); + if (curr == null || curr == default(StaticRoute)) + { + return false; } } - /// - /// Retrieve a static route. - /// - /// The HTTP method. - /// URL path. - /// StaticRoute if the route exists, otherwise null. - public StaticRoute Get(HttpMethod method, string path) + return true; + } + + /// + /// Match a request method and URL to a handler method. + /// + /// The HTTP method. + /// URL path. + /// Matching route. + /// Method to invoke. + public Func Match(HttpMethod method, string path, out StaticRoute route) + { + route = null; + if (string.IsNullOrEmpty(path)) { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } - - path = path.ToLower(); - if (!path.StartsWith("/")) - { - path = "/" + path; - } - - if (!path.EndsWith("/")) - { - path = path + "/"; - } - - lock (_Lock) - { - StaticRoute curr = _Routes.FirstOrDefault(i => i.Method == method && i.Path == path); - if (curr == null || curr == default(StaticRoute)) - { - return null; - } - else - { - return curr; - } - } + throw new ArgumentNullException(nameof(path)); } - /// - /// Check if a static route exists. - /// - /// The HTTP method. - /// URL path. - /// True if exists. - public bool Exists(HttpMethod method, string path) + path = path.ToLower(); + if (!path.StartsWith("/")) { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } - - path = path.ToLower(); - if (!path.StartsWith("/")) - { - path = "/" + path; - } - - if (!path.EndsWith("/")) - { - path = path + "/"; - } - - lock (_Lock) - { - StaticRoute curr = _Routes.FirstOrDefault(i => i.Method == method && i.Path == path); - if (curr == null || curr == default(StaticRoute)) - { - return false; - } - } - - return true; + path = "/" + path; } - /// - /// Match a request method and URL to a handler method. - /// - /// The HTTP method. - /// URL path. - /// Matching route. - /// Method to invoke. - public Func Match(HttpMethod method, string path, out StaticRoute route) + if (!path.EndsWith("/")) { - route = null; - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } - - path = path.ToLower(); - if (!path.StartsWith("/")) - { - path = "/" + path; - } - - if (!path.EndsWith("/")) - { - path = path + "/"; - } - - lock (_Lock) - { - StaticRoute curr = _Routes.FirstOrDefault(i => i.Method == method && i.Path == path); - if (curr == null || curr == default(StaticRoute)) - { - return null; - } - else - { - route = curr; - return curr.Handler; - } - } + path = path + "/"; } - private void Add(StaticRoute route) + lock (_Lock) { - if (route == null) + var curr = _Routes.FirstOrDefault(i => i.Method == method && i.Path == path); + if (curr == null || curr == default(StaticRoute)) { - throw new ArgumentNullException(nameof(route)); + return null; } - route.Path = route.Path.ToLower(); - if (!route.Path.StartsWith("/")) - { - route.Path = "/" + route.Path; - } + route = curr; + return curr.Handler; + } + } - if (!route.Path.EndsWith("/")) - { - route.Path = route.Path + "/"; - } + private void Add(StaticRoute route) + { + if (route == null) + { + throw new ArgumentNullException(nameof(route)); + } - if (Exists(route.Method, route.Path)) - { - return; - } + route.Path = route.Path.ToLower(); + if (!route.Path.StartsWith("/")) + { + route.Path = "/" + route.Path; + } - lock (_Lock) - { - _Routes.Add(route); - } + if (!route.Path.EndsWith("/")) + { + route.Path = route.Path + "/"; + } + + if (Exists(route.Method, route.Path)) + { + return; + } + + lock (_Lock) + { + _Routes.Add(route); } } } \ No newline at end of file