├── .gitignore ├── Directory.build.props ├── LICENSE.TXT ├── NetworkToolkit.Benchmarks ├── Assembly.cs ├── HeaderParsing.cs ├── NetworkToolkit.Benchmarks.csproj ├── NetworkToolkitConfig.cs ├── Program.cs ├── SerializedGet.cs └── SimpleHttp1Server.cs ├── NetworkToolkit.ProfilerTest ├── NetworkToolkit.ProfilerTest.csproj └── Program.cs ├── NetworkToolkit.Tests ├── Connections │ ├── MemoryConnectionFactoryTests.cs │ ├── MemoryConnectionTests.cs │ └── SslConnectionFactoryTests.cs ├── Http │ ├── Http1Tests.cs │ ├── HttpGenericTests.cs │ ├── PooledHttp1Tests.cs │ ├── Servers │ │ ├── Http1TestChunkedStream.cs │ │ ├── Http1TestConnection.cs │ │ ├── Http1TestContentLengthStream.cs │ │ ├── Http1TestLengthlessStream.cs │ │ ├── Http1TestServer.cs │ │ ├── Http1TestStream.cs │ │ ├── HttpTestConnection.cs │ │ ├── HttpTestRequest.cs │ │ ├── HttpTestServer.cs │ │ └── HttpTestStream.cs │ └── TestHeadersSink.cs ├── NetworkToolkit.Tests.csproj ├── TaskTimeoutExtensions.cs ├── TestCertificates.cs ├── TestExtensions.cs ├── TestStreamBase.cs ├── TestsBase.cs ├── TricklingConnectionFactory.cs └── TricklingStream.cs ├── NetworkToolkit.sln ├── NetworkToolkit ├── Assembly.cs ├── Connections │ ├── Connection.cs │ ├── ConnectionFactory.cs │ ├── ConnectionListener.cs │ ├── ConnectionProperties.cs │ ├── ConnectionPropertyExtensions.cs │ ├── ConnectionPropertyKey.cs │ ├── FilteringConnection.cs │ ├── FilteringConnectionFactory.cs │ ├── FilteringConnectionListener.cs │ ├── HttpTunnelConnectionFactory.cs │ ├── IConnectionProperties.cs │ ├── MemoryConnection.cs │ ├── MemoryConnectionFactory.cs │ ├── SocketConnectionFactory.cs │ ├── SslConnectionFactory.cs │ └── WriteBufferingConnectionFactory.cs ├── Http │ ├── AuthorityKey.cs │ ├── Headers │ │ ├── AcceptEncodingHeader.cs │ │ ├── MethodHeader.cs │ │ ├── PathHeader.cs │ │ ├── SchemeHeader.cs │ │ └── StatusHeader.cs │ ├── PreparedHeader.cs │ ├── PreparedHeaderName.cs │ ├── PreparedHeaderSet.cs │ ├── PrimitiveHttpContentStream.cs │ ├── PrimitiveHttpMessageHandler.cs │ ├── PrimitiveHttpResponseMessage.cs │ └── Primitives │ │ ├── HPack.cs │ │ ├── HPackDecoder.cs │ │ ├── HPackDynamicTable.cs │ │ ├── Http1Connection.Parsers.cs │ │ ├── Http1Connection.cs │ │ ├── Http1Request.cs │ │ ├── Http2Connection.cs │ │ ├── Http2Frame.cs │ │ ├── Http2Request.cs │ │ ├── HttpBaseConnection.cs │ │ ├── HttpConnection.cs │ │ ├── HttpConnectionStatus.cs │ │ ├── HttpContentStream.cs │ │ ├── HttpHeaderFlags.cs │ │ ├── HttpPrimitiveVersion.cs │ │ ├── HttpReadType.cs │ │ ├── HttpRequest.cs │ │ ├── IHttpHeadersSink.cs │ │ ├── PooledHttpConnection.cs │ │ ├── SslClientConnectionProperties.cs │ │ └── ValueHttpRequest.cs ├── ICancellableAsyncDisposable.cs ├── ICompletableStream.cs ├── IScatterGatherStream.cs ├── IntrusiveLinkedList.cs ├── NetworkStreamEnhanced.cs ├── NetworkToolkit.csproj ├── NullHttpHeaderSink.cs ├── Parsing │ ├── ArrayBuffer.cs │ ├── CountedBuffer.cs │ └── VectorArrayBuffer.cs ├── ResettableValueTaskSource.cs ├── SocketTaskEventArgs.cs ├── StreamExtensions.cs ├── TaskToApm.cs ├── Tools.cs ├── TunnelEndPoint.cs └── WriteBufferingStream.cs ├── README.md ├── THIRD-PARTY-NOTICES.TXT └── examples ├── Directory.build.props └── http ├── PreparedHeadersSample ├── PreparedHeadersSample.csproj └── Program.cs └── SimpleRequestSample ├── Program.cs └── SimpleRequestSample.csproj /Directory.build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | enable 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LICENSE.TXT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) .NET Foundation and Contributors 4 | 5 | All rights reserved. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /NetworkToolkit.Benchmarks/Assembly.cs: -------------------------------------------------------------------------------- 1 | [module: System.Runtime.CompilerServices.SkipLocalsInit] 2 | -------------------------------------------------------------------------------- /NetworkToolkit.Benchmarks/NetworkToolkit.Benchmarks.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net5.0 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /NetworkToolkit.Benchmarks/NetworkToolkitConfig.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Configs; 2 | using BenchmarkDotNet.Diagnosers; 3 | using BenchmarkDotNet.Jobs; 4 | 5 | namespace NetworkToolkit.Benchmarks 6 | { 7 | public class NetworkToolkitConfig : ManualConfig 8 | { 9 | public NetworkToolkitConfig() 10 | { 11 | AddDiagnoser(MemoryDiagnoser.Default); 12 | AddJob(Job.Default 13 | .WithGcServer(true) 14 | .WithEnvironmentVariable("DOTNET_SYSTEM_THREADING_POOLASYNCVALUETASKS", "1")); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /NetworkToolkit.Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Running; 2 | 3 | namespace NetworkToolkit.Benchmarks 4 | { 5 | class Program 6 | { 7 | static void Main(string[] args) 8 | { 9 | BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /NetworkToolkit.Benchmarks/SimpleHttp1Server.cs: -------------------------------------------------------------------------------- 1 | using NetworkToolkit.Connections; 2 | using System; 3 | using System.IO; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace NetworkToolkit.Benchmarks 8 | { 9 | internal sealed class SimpleHttp1Server : IAsyncDisposable 10 | { 11 | private readonly CancellationTokenSource _cts = new(); 12 | private readonly ConnectionListener _listener; 13 | private readonly byte[] _trigger; 14 | private readonly byte[] _response; 15 | 16 | public SimpleHttp1Server(ConnectionListener listener, byte[] trigger, byte[] response) 17 | { 18 | _listener = listener; 19 | _trigger = trigger; 20 | _response = response; 21 | _ = ListenAsync(); 22 | } 23 | 24 | public async ValueTask DisposeAsync() 25 | { 26 | _cts.Cancel(); 27 | await _listener.DisposeAsync().ConfigureAwait(false); 28 | } 29 | 30 | private async Task ListenAsync() 31 | { 32 | Connection? con; 33 | while ((con = await _listener.AcceptConnectionAsync(cancellationToken: _cts.Token)) != null) 34 | { 35 | _ = RunConnectionAsync(con); 36 | } 37 | } 38 | 39 | private async Task RunConnectionAsync(Connection connection) 40 | { 41 | try 42 | { 43 | await using (connection.ConfigureAwait(false)) 44 | using (var readBuffer = new ArrayBuffer(4096)) 45 | { 46 | Stream stream = connection.Stream; 47 | 48 | while (true) 49 | { 50 | int triggerIdx; 51 | while ((triggerIdx = readBuffer.ActiveSpan.IndexOf(_trigger)) == -1) 52 | { 53 | readBuffer.EnsureAvailableSpace(1); 54 | 55 | int readLen = await stream.ReadAsync(readBuffer.AvailableMemory).ConfigureAwait(false); 56 | if (readLen == 0) return; 57 | 58 | readBuffer.Commit(readLen); 59 | } 60 | 61 | readBuffer.Discard(triggerIdx + _trigger.Length); 62 | 63 | await stream.WriteAsync(_response).ConfigureAwait(false); 64 | await stream.FlushAsync().ConfigureAwait(false); 65 | } 66 | } 67 | } 68 | catch (Exception ex) 69 | { 70 | Console.Error.WriteLine(ex); 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /NetworkToolkit.ProfilerTest/NetworkToolkit.ProfilerTest.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net5.0 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /NetworkToolkit.ProfilerTest/Program.cs: -------------------------------------------------------------------------------- 1 | using NetworkToolkit.Benchmarks; 2 | using NetworkToolkit.Connections; 3 | using NetworkToolkit.Http; 4 | using NetworkToolkit.Http.Primitives; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Diagnostics; 8 | using System.Net.Http; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | 12 | namespace NetworkToolkit.ProfilerTest 13 | { 14 | class Program 15 | { 16 | static async Task Main(string[] args) 17 | { 18 | Environment.SetEnvironmentVariable("DOTNET_SYSTEM_THREADING_POOLASYNCVALUETASKS", "1"); 19 | 20 | await using ConnectionFactory connectionFactory = new MemoryConnectionFactory(); 21 | 22 | await using ConnectionListener listener = await connectionFactory.ListenAsync(); 23 | await using SimpleHttp1Server server = new(listener, triggerBytes, responseBytes); 24 | 25 | await using Connection connection = await connectionFactory.ConnectAsync(listener.EndPoint!); 26 | await using HttpConnection httpConnection = new Http1Connection(connection, HttpPrimitiveVersion.Version11); 27 | 28 | if (!Debugger.IsAttached) 29 | { 30 | Console.WriteLine("Press any key to continue, once profiler is attached..."); 31 | Console.ReadKey(); 32 | } 33 | 34 | for (int i = 0; i < 1000000; ++i) 35 | { 36 | await using ValueHttpRequest request = (await httpConnection.CreateNewRequestAsync(HttpPrimitiveVersion.Version11, HttpVersionPolicy.RequestVersionExact)) 37 | ?? throw new Exception("HttpConnection failed to return a request"); 38 | 39 | request.ConfigureRequest(contentLength: 0, hasTrailingHeaders: false); 40 | request.WriteRequest(HttpRequest.GetMethod, authority, pathAndQuery); 41 | 42 | request.WriteHeader(preparedRequestHeaders); 43 | 44 | foreach ((byte[] name, byte[] value) in dynamicRequestHeaders) 45 | { 46 | request.WriteHeader(name, value); 47 | } 48 | 49 | await request.CompleteRequestAsync(); 50 | 51 | while (await request.ReadAsync() != HttpReadType.EndOfStream) 52 | { 53 | // do nothing, just draining. 54 | } 55 | } 56 | } 57 | 58 | static readonly byte[] authority = Encoding.ASCII.GetBytes("localhost"); 59 | static readonly byte[] pathAndQuery = Encoding.ASCII.GetBytes("/"); 60 | 61 | static readonly PreparedHeaderSet preparedRequestHeaders = 62 | new PreparedHeaderSet 63 | { 64 | { "accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" }, 65 | { "accept-encoding", "gzip, deflate, br" }, 66 | { "accept-language", "en-US,en;q=0.9" }, 67 | { "sec-fetch-dest", "document" }, 68 | { "sec-fetch-mode", "navigate" }, 69 | { "sec-fetch-site", "none" }, 70 | { "sec-fetch-user", "?1" }, 71 | { "upgrade-insecure-requests", "1" }, 72 | { "user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36 Edg/86.0.622.69" } 73 | }; 74 | 75 | static readonly List<(byte[], byte[])> dynamicRequestHeaders = new() 76 | { 77 | (Encoding.ASCII.GetBytes("cookie"), Encoding.ASCII.GetBytes("cookie: aaaa=000000000000000000000000000000000000; bbb=111111111111111111111111111; ccccc=22222222222222222222222222; dddddd=333333333333333333333333333333333333333333333333333333333333333333333; eeee=444444444444444444444444444444444444444444444444444444444444444444444444444")) 78 | }; 79 | 80 | static readonly byte[] triggerBytes = Encoding.ASCII.GetBytes("\r\n\r\n"); 81 | static readonly byte[] responseBytes = Encoding.ASCII.GetBytes("HTTP/1.1 200 OK\r\n" + 82 | "Content-Length: 0\r\n" + 83 | "Accept-Ranges: bytes\r\n" + 84 | "Cache-Control: private\r\n" + 85 | "Content-Security-Policy: upgrade-insecure-requests; frame-ancestors 'self' https://stackexchange.com\r\n" + 86 | "Content-Type: text/html; charset=utf-8\r\n" + 87 | "Date: Mon, 16 Nov 2020 23:35:36 GMT\r\n" + 88 | "Feature-Policy: microphone 'none'; speaker 'none'\r\n" + 89 | "Server: Microsoft-IIS/10.0\r\n" + 90 | "Strict-Transport-Security: max-age=15552000\r\n" + 91 | "Vary: Accept-Encoding,Fastly-SSL\r\n" + 92 | "Via: 1.1 varnish\r\n" + 93 | "x-account-id: 12345\r\n" + 94 | "x-aspnet-duration-ms: 44\r\n" + 95 | "x-cache: MISS\r\n" + 96 | "x-cache-hits: 0\r\n" + 97 | "x-dns-prefetch-control: off\r\n" + 98 | "x-flags: QA\r\n" + 99 | "x-frame-options: SAMEORIGIN\r\n" + 100 | "x-http-count: 2\r\n" + 101 | "x-http-duration-ms: 8\r\n" + 102 | "x-is-crawler: 0\r\n" + 103 | "x-page-view: 1\r\n" + 104 | "x-providence-cookie: aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\r\n" + 105 | "x-redis-count: 22\r\n" + 106 | "x-redis-duration-ms: 2\r\n" + 107 | "x-request-guid: aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\r\n" + 108 | "x-route-name: Home/Index\r\n" + 109 | "x-served-by: cache-sea4460-SEA\r\n" + 110 | "x-sql-count: 12\r\n" + 111 | "x-sql-duration-ms: 12\r\n" + 112 | "x-timer: S1605569737.604081,VS0,VE106\r\n" + 113 | "\r\n"); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /NetworkToolkit.Tests/Connections/MemoryConnectionFactoryTests.cs: -------------------------------------------------------------------------------- 1 | using NetworkToolkit.Connections; 2 | using System.Net.Sockets; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | 7 | namespace NetworkToolkit.Tests.Connections 8 | { 9 | public class MemoryConnectionFactoryTests : TestsBase 10 | { 11 | [Theory] 12 | [InlineData(false), InlineData(true)] 13 | public async Task Connect_Success(bool clientFirst) 14 | { 15 | await using ConnectionFactory factory = new MemoryConnectionFactory(); 16 | await using ConnectionListener listener = await factory.ListenAsync(); 17 | 18 | using var semaphore = new SemaphoreSlim(0); 19 | 20 | await RunClientServer(async () => 21 | { 22 | if (!clientFirst) 23 | { 24 | bool success = await semaphore.WaitAsync(10_000); 25 | Assert.True(success); 26 | } 27 | 28 | ValueTask task = factory.ConnectAsync(listener.EndPoint!); 29 | if (clientFirst) semaphore.Release(); 30 | 31 | await using Connection connection = await task; 32 | }, 33 | async () => 34 | { 35 | if (clientFirst) 36 | { 37 | bool success = await semaphore.WaitAsync(10_000); 38 | Assert.True(success); 39 | } 40 | 41 | ValueTask task = listener.AcceptConnectionAsync(); 42 | if (!clientFirst) semaphore.Release(); 43 | 44 | await using Connection? connection = await task; 45 | Assert.NotNull(connection); 46 | }); 47 | } 48 | 49 | [Fact] 50 | public async Task Listener_DisposeCancelsConnect_Success() 51 | { 52 | await using ConnectionFactory factory = new MemoryConnectionFactory(); 53 | await using ConnectionListener listener = await factory.ListenAsync(); 54 | 55 | ValueTask connectTask = factory.ConnectAsync(listener.EndPoint!); 56 | 57 | await listener.DisposeAsync(); 58 | 59 | SocketException ex = await Assert.ThrowsAsync(async () => 60 | { 61 | await using Connection connection = await connectTask; 62 | }).ConfigureAwait(false); 63 | 64 | Assert.Equal(SocketError.ConnectionRefused, ex.SocketErrorCode); 65 | } 66 | 67 | [Fact] 68 | public async Task Listener_DisposeCancelsAccept_Success() 69 | { 70 | await using ConnectionFactory factory = new MemoryConnectionFactory(); 71 | await using ConnectionListener listener = await factory.ListenAsync(); 72 | 73 | ValueTask acceptTask = listener.AcceptConnectionAsync(); 74 | 75 | await listener.DisposeAsync(); 76 | 77 | Connection? connection = await acceptTask; 78 | Assert.Null(connection); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /NetworkToolkit.Tests/Connections/MemoryConnectionTests.cs: -------------------------------------------------------------------------------- 1 | using NetworkToolkit.Connections; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using Xunit; 5 | 6 | namespace NetworkToolkit.Tests.Connections 7 | { 8 | public class MemoryConnectionTests : TestsBase 9 | { 10 | [Fact] 11 | public async Task ReadWrite_Success() 12 | { 13 | const string ClientTestValue = "ClientString1234"; 14 | const string ServerTestValue = "ServerString5678"; 15 | 16 | (Connection clientConnection, Connection serverConnection) = MemoryConnection.Create(); 17 | 18 | await using (clientConnection) 19 | await using (serverConnection) 20 | { 21 | await RunClientServer(async () => 22 | { 23 | using (var writer = new StreamWriter(clientConnection.Stream, leaveOpen: true)) 24 | { 25 | await writer.WriteLineAsync(ClientTestValue); 26 | } 27 | await ((ICompletableStream)clientConnection.Stream).CompleteWritesAsync(); 28 | 29 | using (var reader = new StreamReader(clientConnection.Stream)) 30 | { 31 | Assert.Equal(ServerTestValue, await reader.ReadLineAsync()); 32 | Assert.Null(await reader.ReadLineAsync()); 33 | } 34 | }, 35 | async () => 36 | { 37 | using (var writer = new StreamWriter(serverConnection.Stream, leaveOpen: true)) 38 | { 39 | await writer.WriteLineAsync(ServerTestValue); 40 | } 41 | await ((ICompletableStream)serverConnection.Stream).CompleteWritesAsync(); 42 | 43 | using (var reader = new StreamReader(serverConnection.Stream)) 44 | { 45 | Assert.Equal(ClientTestValue, await reader.ReadLineAsync()); 46 | Assert.Null(await reader.ReadLineAsync()); 47 | } 48 | }); 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /NetworkToolkit.Tests/Connections/SslConnectionFactoryTests.cs: -------------------------------------------------------------------------------- 1 | using NetworkToolkit.Connections; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Net.Security; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | 9 | namespace NetworkToolkit.Tests.Connections 10 | { 11 | public class SslConnectionFactoryTests : TestsBase 12 | { 13 | [Fact] 14 | public async Task Connect_SelfSigned_Success() 15 | { 16 | var protocols = new List { new SslApplicationProtocol("test") }; 17 | 18 | var connectProperties = new ConnectionProperties(); 19 | connectProperties.Add(SslConnectionFactory.SslClientAuthenticationOptionsPropertyKey, new SslClientAuthenticationOptions 20 | { 21 | TargetHost = "localhost", 22 | ApplicationProtocols = protocols, 23 | RemoteCertificateValidationCallback = delegate { return true; } 24 | }); 25 | 26 | var listenProperties = new ConnectionProperties(); 27 | listenProperties.Add(SslConnectionFactory.SslServerAuthenticationOptionsPropertyKey, new SslServerAuthenticationOptions 28 | { 29 | ApplicationProtocols = protocols, 30 | ServerCertificate = TestCertificates.GetSelfSigned13ServerCertificate() 31 | }); 32 | 33 | byte[] sendBuffer = Encoding.ASCII.GetBytes("Testing 123"); 34 | 35 | await using ConnectionFactory factory = new SslConnectionFactory(new MemoryConnectionFactory()); 36 | await using ConnectionListener listener = await factory.ListenAsync(options: listenProperties); 37 | 38 | await RunClientServer( 39 | async () => 40 | { 41 | await using Connection connection = await factory.ConnectAsync(listener.EndPoint!, connectProperties); 42 | await connection.Stream.WriteAsync(sendBuffer); 43 | }, 44 | async () => 45 | { 46 | await using Connection? connection = await listener.AcceptConnectionAsync(); 47 | Assert.NotNull(connection); 48 | Debug.Assert(connection != null); 49 | 50 | byte[] buffer = new byte[sendBuffer.Length + 1]; 51 | int readLen = await connection.Stream.ReadAsync(buffer); 52 | Assert.Equal(sendBuffer, buffer[..readLen]); 53 | 54 | readLen = await connection.Stream.ReadAsync(buffer); 55 | Assert.Equal(0, readLen); 56 | }).ConfigureAwait(false); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /NetworkToolkit.Tests/Http/PooledHttp1Tests.cs: -------------------------------------------------------------------------------- 1 | using NetworkToolkit.Connections; 2 | using NetworkToolkit.Http.Primitives; 3 | using NetworkToolkit.Tests.Http.Servers; 4 | using System.Net; 5 | using System.Net.Security; 6 | using System.Threading.Tasks; 7 | 8 | namespace NetworkToolkit.Tests.Http 9 | { 10 | public class PooledHttp1Tests : HttpGenericTests 11 | { 12 | internal override HttpPrimitiveVersion Version => HttpPrimitiveVersion.Version11; 13 | 14 | internal override async Task CreateTestServerAsync(ConnectionFactory connectionFactory) => 15 | new Http1TestServer(await connectionFactory.ListenAsync(options: CreateListenerProperties()).ConfigureAwait(false)); 16 | 17 | internal override Task CreateTestClientAsync(ConnectionFactory connectionFactory, EndPoint endPoint) 18 | { 19 | IConnectionProperties? properties = CreateConnectProperties(); 20 | 21 | SslClientAuthenticationOptions? sslOptions = null; 22 | properties?.TryGetProperty(SslConnectionFactory.SslClientAuthenticationOptionsPropertyKey, out sslOptions); 23 | 24 | return Task.FromResult(new PooledHttpConnection(connectionFactory, endPoint, sslOptions)); 25 | } 26 | } 27 | 28 | public class PooledHttp1SslTests : PooledHttp1Tests 29 | { 30 | internal override bool UseSsl => true; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /NetworkToolkit.Tests/Http/Servers/Http1TestChunkedStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | 7 | namespace NetworkToolkit.Tests.Http.Servers 8 | { 9 | internal sealed class Http1TestChunkedStream : TestStreamBase 10 | { 11 | private readonly Http1TestConnection _con; 12 | private long? _totalLengthRemaining; 13 | private long? _curChunkLengthRemaining; 14 | 15 | public override bool CanRead => true; 16 | 17 | public Http1TestChunkedStream(Http1TestConnection con, long? length) 18 | { 19 | _con = con; 20 | _totalLengthRemaining = length; 21 | } 22 | 23 | public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) 24 | { 25 | if (_curChunkLengthRemaining == 0) 26 | { 27 | string dataTrailer = await _con.ReadLineAsync().ConfigureAwait(false); 28 | Assert.Empty(dataTrailer); 29 | 30 | _curChunkLengthRemaining = null; 31 | } 32 | 33 | if (_curChunkLengthRemaining == null) 34 | { 35 | string lengthString = await _con.ReadLineAsync().ConfigureAwait(false); 36 | _curChunkLengthRemaining = long.Parse(lengthString, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture); 37 | 38 | if (_curChunkLengthRemaining == 0) 39 | { 40 | // final chunk. 41 | if (_totalLengthRemaining is not null and not 0) 42 | { 43 | throw new Exception("Chunked stream contains less data than Content-Length indicates."); 44 | } 45 | return 0; 46 | } 47 | } 48 | 49 | int recvLen = (int)Math.Min(buffer.Length, _curChunkLengthRemaining.Value); 50 | 51 | if (_con._readBuffer.ActiveLength != 0) 52 | { 53 | cancellationToken.ThrowIfCancellationRequested(); 54 | recvLen = Math.Min(recvLen, _con._readBuffer.ActiveLength); 55 | _con._readBuffer.ActiveSpan[..recvLen].CopyTo(buffer.Span); 56 | _con._readBuffer.Discard(recvLen); 57 | } 58 | else 59 | { 60 | recvLen = await _con._stream.ReadAsync(buffer[..recvLen], cancellationToken).ConfigureAwait(false); 61 | if (recvLen == 0) throw new Exception($"Unexpected end of stream with minimum {_totalLengthRemaining ?? _curChunkLengthRemaining} bytes remaining."); 62 | } 63 | 64 | _curChunkLengthRemaining -= recvLen; 65 | _totalLengthRemaining -= recvLen; 66 | 67 | if (_totalLengthRemaining < 0) 68 | { 69 | throw new Exception("Chunked stream contains more data than Content-Length indicates."); 70 | } 71 | 72 | return recvLen; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /NetworkToolkit.Tests/Http/Servers/Http1TestContentLengthStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace NetworkToolkit.Tests.Http.Servers 7 | { 8 | internal class Http1TestContentLengthStream : TestStreamBase 9 | { 10 | private readonly Http1TestConnection _con; 11 | private readonly Http1TestStream? _stream; 12 | private long _lengthRemaining; 13 | 14 | public override bool CanRead => true; 15 | 16 | public Http1TestContentLengthStream(Http1TestConnection con, Http1TestStream? stream, long length) 17 | { 18 | _con = con; 19 | _stream = stream; 20 | _lengthRemaining = length; 21 | } 22 | 23 | protected override void Dispose(bool disposing) 24 | { 25 | Debug.Assert(disposing); 26 | } 27 | 28 | public override ValueTask DisposeAsync(CancellationToken cancellationToken) => 29 | default; 30 | 31 | public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) 32 | { 33 | if (_lengthRemaining == 0) 34 | { 35 | cancellationToken.ThrowIfCancellationRequested(); 36 | return 0; 37 | } 38 | 39 | int recvLen = (int)Math.Min(buffer.Length, _lengthRemaining); 40 | 41 | if (_con._readBuffer.ActiveLength != 0) 42 | { 43 | cancellationToken.ThrowIfCancellationRequested(); 44 | recvLen = Math.Min(recvLen, _con._readBuffer.ActiveLength); 45 | _con._readBuffer.ActiveSpan[..recvLen].CopyTo(buffer.Span); 46 | _con._readBuffer.Discard(recvLen); 47 | _lengthRemaining -= recvLen; 48 | 49 | if (_lengthRemaining == 0) 50 | { 51 | _stream?.ReleaseNextReader(); 52 | } 53 | 54 | return recvLen; 55 | } 56 | else 57 | { 58 | recvLen = await _con._stream.ReadAsync(buffer[..recvLen], cancellationToken).ConfigureAwait(false); 59 | if (recvLen == 0) throw new Exception($"Unexpected end of stream with {_lengthRemaining} bytes remaining."); 60 | 61 | _lengthRemaining -= recvLen; 62 | 63 | if (_lengthRemaining == 0) 64 | { 65 | _stream?.ReleaseNextReader(); 66 | } 67 | 68 | return recvLen; 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /NetworkToolkit.Tests/Http/Servers/Http1TestLengthlessStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace NetworkToolkit.Tests.Http.Servers 7 | { 8 | internal sealed class Http1TestLengthlessStream : TestStreamBase 9 | { 10 | private readonly Http1TestConnection _con; 11 | 12 | public override bool CanRead => true; 13 | 14 | public Http1TestLengthlessStream(Http1TestConnection con) 15 | { 16 | _con = con; 17 | } 18 | 19 | public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) 20 | { 21 | if (_con._readBuffer.ActiveLength != 0) 22 | { 23 | cancellationToken.ThrowIfCancellationRequested(); 24 | 25 | int recvLen = Math.Min(buffer.Length, _con._readBuffer.ActiveLength); 26 | _con._readBuffer.ActiveSpan[..recvLen].CopyTo(buffer.Span); 27 | _con._readBuffer.Discard(recvLen); 28 | return recvLen; 29 | } 30 | else 31 | { 32 | return await _con._stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /NetworkToolkit.Tests/Http/Servers/Http1TestServer.cs: -------------------------------------------------------------------------------- 1 | using NetworkToolkit.Connections; 2 | using System.Diagnostics; 3 | using System.Net; 4 | using System.Threading.Tasks; 5 | 6 | namespace NetworkToolkit.Tests.Http.Servers 7 | { 8 | internal sealed class Http1TestServer : HttpTestServer 9 | { 10 | private readonly ConnectionListener _listener; 11 | 12 | public override EndPoint? EndPoint => _listener.EndPoint; 13 | 14 | public Http1TestServer(ConnectionListener connectionListener) 15 | { 16 | _listener = connectionListener; 17 | } 18 | 19 | public override async Task AcceptAsync() 20 | { 21 | Connection? connection = await _listener.AcceptConnectionAsync().ConfigureAwait(false); 22 | Debug.Assert(connection != null); 23 | return new Http1TestConnection(connection); 24 | } 25 | 26 | public override ValueTask DisposeAsync() => 27 | _listener.DisposeAsync(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /NetworkToolkit.Tests/Http/Servers/Http1TestStream.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace NetworkToolkit.Tests.Http.Servers 7 | { 8 | internal sealed class Http1TestStream : HttpTestStream 9 | { 10 | private readonly int _streamIdx; 11 | private readonly Http1TestConnection _connection; 12 | internal readonly SemaphoreSlim _readSemaphore = new SemaphoreSlim(0); 13 | internal readonly SemaphoreSlim _writeSemaphore = new SemaphoreSlim(0); 14 | internal bool _nextReaderReleased, _nextWriterReleased; 15 | 16 | public Http1TestStream(Http1TestConnection connection, int streamIdx) 17 | { 18 | _connection = connection; 19 | _streamIdx = streamIdx; 20 | } 21 | 22 | public override ValueTask DisposeAsync() => 23 | default; 24 | 25 | public override string ToString() => 26 | _streamIdx.ToString(); 27 | 28 | public override async Task ReceiveRequestAsync() 29 | { 30 | await _readSemaphore.WaitAsync().ConfigureAwait(false); 31 | return await _connection.ReceiveRequestAsync(this).ConfigureAwait(false); 32 | } 33 | 34 | public override Stream ReceiveContentStream() => 35 | _connection.ReceiveContentStream(this); 36 | 37 | public override Task ReceiveTrailingHeadersAsync() => 38 | _connection.ReceiveTrailingHeadersAsync(this); 39 | 40 | public override async Task SendResponseAsync(int statusCode = 200, TestHeadersSink? headers = null, string? content = null, TestHeadersSink? trailingHeaders = null) 41 | { 42 | await _writeSemaphore.WaitAsync(); 43 | await _connection.SendResponseAsync(this, statusCode, headers, content, chunkedContent: null, trailingHeaders).ConfigureAwait(false); 44 | } 45 | 46 | public override async Task SendChunkedResponseAsync(int statusCode = 200, TestHeadersSink? headers = null, IList? content = null, TestHeadersSink? trailingHeaders = null) 47 | { 48 | await _writeSemaphore.WaitAsync(); 49 | await _connection.SendResponseAsync(this, statusCode, headers, content: null, chunkedContent: content, trailingHeaders).ConfigureAwait(false); 50 | } 51 | 52 | public async Task SendRawResponseAsync(string response) 53 | { 54 | await _writeSemaphore.WaitAsync(); 55 | await _connection.SendRawResponseAsync(this, response).ConfigureAwait(false); 56 | } 57 | 58 | internal void ReleaseNextReader() 59 | { 60 | if (!_nextReaderReleased) 61 | { 62 | _nextReaderReleased = true; 63 | _connection.ReleaseNextReader(); 64 | } 65 | } 66 | 67 | internal void ReleaseNextWriter() 68 | { 69 | if (!_nextWriterReleased) 70 | { 71 | _nextWriterReleased = true; 72 | _connection.ReleaseNextWriter(); 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /NetworkToolkit.Tests/Http/Servers/HttpTestConnection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace NetworkToolkit.Tests.Http.Servers 5 | { 6 | internal abstract class HttpTestConnection : IAsyncDisposable 7 | { 8 | public abstract ValueTask DisposeAsync(); 9 | public abstract Task AcceptStreamAsync(); 10 | 11 | public async Task ReceiveAndSendSingleRequestAsync(int statusCode = 200, TestHeadersSink? headers = null, string? content = null, TestHeadersSink? trailingHeaders = null) 12 | { 13 | HttpTestStream stream = await AcceptStreamAsync().ConfigureAwait(false); 14 | await using (stream.ConfigureAwait(false)) 15 | { 16 | return await stream.ReceiveAndSendAsync(statusCode, headers, content, trailingHeaders).ConfigureAwait(false); 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NetworkToolkit.Tests/Http/Servers/HttpTestRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NetworkToolkit.Tests.Http.Servers 4 | { 5 | internal record HttpTestRequest( 6 | string Method, 7 | string PathAndQuery, 8 | Version Version, 9 | TestHeadersSink Headers); 10 | 11 | internal record HttpTestFullRequest( 12 | string Method, 13 | string PathAndQuery, 14 | Version Version, 15 | TestHeadersSink Headers, 16 | string Content, 17 | TestHeadersSink TrailingHeaders); 18 | } 19 | -------------------------------------------------------------------------------- /NetworkToolkit.Tests/Http/Servers/HttpTestServer.cs: -------------------------------------------------------------------------------- 1 | using NetworkToolkit.Connections; 2 | using System; 3 | using System.Net; 4 | using System.Threading.Tasks; 5 | 6 | namespace NetworkToolkit.Tests.Http.Servers 7 | { 8 | internal abstract class HttpTestServer : IAsyncDisposable 9 | { 10 | public abstract ValueTask DisposeAsync(); 11 | public abstract Task AcceptAsync(); 12 | 13 | public abstract EndPoint? EndPoint { get; } 14 | 15 | public Uri Uri 16 | { 17 | get 18 | { 19 | var uriBuilder = new UriBuilder 20 | { 21 | Scheme = Uri.UriSchemeHttp, 22 | Path = "/" 23 | }; 24 | 25 | switch (EndPoint) 26 | { 27 | case DnsEndPoint dnsEp: 28 | uriBuilder.Host = dnsEp.Host; 29 | uriBuilder.Port = dnsEp.Port; 30 | break; 31 | case IPEndPoint ipEp: 32 | uriBuilder.Host = ipEp.Address.ToString(); 33 | uriBuilder.Port = ipEp.Port; 34 | break; 35 | default: 36 | uriBuilder.Host = "localhost"; 37 | uriBuilder.Port = 80; 38 | break; 39 | } 40 | 41 | return uriBuilder.Uri; 42 | } 43 | } 44 | 45 | public async Task ReceiveAndSendSingleRequestAsync(int statusCode = 200, TestHeadersSink? headers = null, string? content = null, TestHeadersSink? trailingHeaders = null) 46 | { 47 | HttpTestConnection connection = await AcceptAsync().ConfigureAwait(false); 48 | await using (connection.ConfigureAwait(false)) 49 | { 50 | return await connection.ReceiveAndSendSingleRequestAsync(statusCode, headers, content, trailingHeaders).ConfigureAwait(false); 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /NetworkToolkit.Tests/Http/Servers/HttpTestStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace NetworkToolkit.Tests.Http.Servers 8 | { 9 | internal abstract class HttpTestStream : IAsyncDisposable 10 | { 11 | public abstract ValueTask DisposeAsync(); 12 | public abstract Task ReceiveRequestAsync(); 13 | public abstract Stream ReceiveContentStream(); 14 | public abstract Task ReceiveTrailingHeadersAsync(); 15 | public abstract Task SendResponseAsync(int statusCode = 200, TestHeadersSink? headers = null, string? content = null, TestHeadersSink? trailingHeaders = null); 16 | public abstract Task SendChunkedResponseAsync(int statusCode = 200, TestHeadersSink? headers = null, IList? content = null, TestHeadersSink? trailingHeaders = null); 17 | 18 | public async Task ReceiveContentStringAsync() 19 | { 20 | Stream stream = ReceiveContentStream(); 21 | await using (stream.ConfigureAwait(false)) 22 | { 23 | using var sr = new StreamReader(stream, Encoding.UTF8, leaveOpen: true); 24 | return await sr.ReadToEndAsync().ConfigureAwait(false); 25 | } 26 | } 27 | 28 | public async Task ReceiveFullRequestAsync() 29 | { 30 | HttpTestRequest request = await ReceiveRequestAsync().ConfigureAwait(false); 31 | string content = await ReceiveContentStringAsync().ConfigureAwait(false); 32 | TestHeadersSink trailingHeaders = await ReceiveTrailingHeadersAsync().ConfigureAwait(false); 33 | return new HttpTestFullRequest(request.Method, request.PathAndQuery, request.Version, request.Headers, content, trailingHeaders); 34 | } 35 | 36 | public async Task ReceiveAndSendAsync(int statusCode = 200, TestHeadersSink? headers = null, string? content = null, TestHeadersSink? trailingHeaders = null) 37 | { 38 | HttpTestFullRequest request = await ReceiveFullRequestAsync().ConfigureAwait(false); 39 | await SendResponseAsync(statusCode, headers, content, trailingHeaders).ConfigureAwait(false); 40 | return request; 41 | } 42 | 43 | public async Task ReceiveAndSendChunkedAsync(int statusCode = 200, TestHeadersSink? headers = null, IList? content = null, TestHeadersSink? trailingHeaders = null) 44 | { 45 | HttpTestFullRequest request = await ReceiveFullRequestAsync().ConfigureAwait(false); 46 | await SendChunkedResponseAsync(statusCode, headers, content, trailingHeaders).ConfigureAwait(false); 47 | return request; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /NetworkToolkit.Tests/Http/TestHeadersSink.cs: -------------------------------------------------------------------------------- 1 | using NetworkToolkit.Http.Primitives; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.Linq; 6 | using System.Text; 7 | using Xunit; 8 | 9 | namespace NetworkToolkit.Tests.Http 10 | { 11 | public sealed class TestHeadersSink : Dictionary>, IHttpHeadersSink 12 | { 13 | private IEnumerable<(string headerName, string headerValue, int index)> Flattened => this 14 | .SelectMany(kvp => kvp.Value.Select((value, index) => (value, index)), (kvp, value) => (headerName: kvp.Key, headerValue: value.value, headerIndex: value.index)); 15 | 16 | public TestHeadersSink() : base(StringComparer.OrdinalIgnoreCase) 17 | { 18 | } 19 | 20 | public void OnHeader(object? state, ReadOnlySpan headerName, ReadOnlySpan headerValue) 21 | { 22 | string nameAscii = Encoding.ASCII.GetString(headerName); 23 | string valueAscii = Encoding.ASCII.GetString(headerValue); 24 | Add(nameAscii, valueAscii); 25 | } 26 | 27 | public string GetSingleValue(string headerName) 28 | { 29 | bool hasValues = TryGetSingleValue(headerName, out string? value); 30 | Assert.True(hasValues); 31 | return value!; 32 | } 33 | 34 | public bool TryGetSingleValue(string headerName, [NotNullWhen(true)] out string? headerValue) 35 | { 36 | bool hasValues = TryGetValue(headerName, out List? values); 37 | if (hasValues) 38 | { 39 | headerValue = Assert.Single(values!); 40 | return true; 41 | } 42 | else 43 | { 44 | headerValue = null; 45 | return false; 46 | } 47 | } 48 | 49 | public void Add(string headerName, string headerValue) 50 | { 51 | if (!TryGetValue(headerName, out List? values)) 52 | { 53 | values = new List(); 54 | Add(headerName, values); 55 | } 56 | 57 | values.Add(headerValue); 58 | } 59 | 60 | public bool Contains(TestHeadersSink headers) => 61 | headers.Flattened.Except(Flattened).Any() == false; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /NetworkToolkit.Tests/NetworkToolkit.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net5.0 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /NetworkToolkit.Tests/TaskTimeoutExtensions.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | 7 | /// 8 | /// Task timeout helper based on https://devblogs.microsoft.com/pfxteam/crafting-a-task-timeoutafter-method/ 9 | /// 10 | namespace System.Threading.Tasks 11 | { 12 | public static class TaskTimeoutExtensions 13 | { 14 | public static async Task WithCancellation(this Task task, CancellationToken cancellationToken) 15 | { 16 | var tcs = new TaskCompletionSource(); 17 | using (cancellationToken.Register(s => ((TaskCompletionSource)s!).TrySetResult(true), tcs)) 18 | { 19 | if (task != await Task.WhenAny(task, tcs.Task).ConfigureAwait(false)) 20 | { 21 | throw new OperationCanceledException(cancellationToken); 22 | } 23 | await task; // already completed; propagate any exception 24 | } 25 | } 26 | 27 | public static Task TimeoutAfter(this Task task, int millisecondsTimeout) 28 | => task.TimeoutAfter(TimeSpan.FromMilliseconds(millisecondsTimeout)); 29 | 30 | public static async Task TimeoutAfter(this Task task, TimeSpan timeout) 31 | { 32 | var cts = new CancellationTokenSource(); 33 | 34 | if (task == await Task.WhenAny(task, Task.Delay(timeout, cts.Token)).ConfigureAwait(false)) 35 | { 36 | cts.Cancel(); 37 | await task.ConfigureAwait(false); 38 | } 39 | else 40 | { 41 | throw new TimeoutException($"Task timed out after {timeout}"); 42 | } 43 | } 44 | 45 | public static Task TimeoutAfter(this Task task, int millisecondsTimeout) 46 | => task.TimeoutAfter(TimeSpan.FromMilliseconds(millisecondsTimeout)); 47 | 48 | public static async Task TimeoutAfter(this Task task, TimeSpan timeout) 49 | { 50 | var cts = new CancellationTokenSource(); 51 | 52 | if (task == await Task.WhenAny(task, Task.Delay(timeout, cts.Token)).ConfigureAwait(false)) 53 | { 54 | cts.Cancel(); 55 | return await task.ConfigureAwait(false); 56 | } 57 | else 58 | { 59 | throw new TimeoutException($"Task timed out after {timeout}"); 60 | } 61 | } 62 | 63 | #if !NETFRAMEWORK 64 | public static Task TimeoutAfter(this ValueTask task, int millisecondsTimeout) 65 | => task.AsTask().TimeoutAfter(TimeSpan.FromMilliseconds(millisecondsTimeout)); 66 | 67 | public static Task TimeoutAfter(this ValueTask task, TimeSpan timeout) 68 | => task.AsTask().TimeoutAfter(timeout); 69 | 70 | public static Task TimeoutAfter(this ValueTask task, int millisecondsTimeout) 71 | => task.AsTask().TimeoutAfter(TimeSpan.FromMilliseconds(millisecondsTimeout)); 72 | 73 | public static Task TimeoutAfter(this ValueTask task, TimeSpan timeout) 74 | => task.AsTask().TimeoutAfter(timeout); 75 | #endif 76 | 77 | public static async Task WhenAllOrAnyFailed(this Task[] tasks, int millisecondsTimeout) 78 | { 79 | var cts = new CancellationTokenSource(); 80 | Task task = tasks.WhenAllOrAnyFailed(); 81 | if (task == await Task.WhenAny(task, Task.Delay(millisecondsTimeout, cts.Token)).ConfigureAwait(false)) 82 | { 83 | cts.Cancel(); 84 | await task.ConfigureAwait(false); 85 | } 86 | else 87 | { 88 | throw new TimeoutException($"{nameof(WhenAllOrAnyFailed)} timed out after {millisecondsTimeout}ms"); 89 | } 90 | } 91 | 92 | public static async Task WhenAllOrAnyFailed(this Task[] tasks) 93 | { 94 | try 95 | { 96 | await WhenAllOrAnyFailedCore(tasks).ConfigureAwait(false); 97 | } 98 | catch 99 | { 100 | // Wait a bit to allow other tasks to complete so we can include their exceptions 101 | // in the error we throw. 102 | using (var cts = new CancellationTokenSource()) 103 | { 104 | await Task.WhenAny( 105 | Task.WhenAll(tasks), 106 | Task.Delay(3_000, cts.Token)).ConfigureAwait(false); // arbitrary delay; can be dialed up or down in the future 107 | } 108 | 109 | var exceptions = new List(); 110 | foreach (Task t in tasks) 111 | { 112 | switch (t.Status) 113 | { 114 | case TaskStatus.Faulted: exceptions.Add(t.Exception!); break; 115 | case TaskStatus.Canceled: exceptions.Add(new TaskCanceledException(t)); break; 116 | } 117 | } 118 | 119 | Debug.Assert(exceptions.Count > 0); 120 | if (exceptions.Count > 1) 121 | { 122 | throw new AggregateException(exceptions); 123 | } 124 | throw; 125 | } 126 | } 127 | 128 | private static Task WhenAllOrAnyFailedCore(this Task[] tasks) 129 | { 130 | int remaining = tasks.Length; 131 | var tcs = new TaskCompletionSource(); 132 | foreach (Task t in tasks) 133 | { 134 | t.ContinueWith(a => 135 | { 136 | if (a.IsFaulted) 137 | { 138 | tcs.TrySetException(a.Exception!.InnerExceptions); 139 | Interlocked.Decrement(ref remaining); 140 | } 141 | else if (a.IsCanceled) 142 | { 143 | tcs.TrySetCanceled(); 144 | Interlocked.Decrement(ref remaining); 145 | } 146 | else if (Interlocked.Decrement(ref remaining) == 0) 147 | { 148 | tcs.TrySetResult(true); 149 | } 150 | }, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default); 151 | } 152 | return tcs.Task; 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /NetworkToolkit.Tests/TestCertificates.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using System.Security.Cryptography; 4 | using System.Security.Cryptography.X509Certificates; 5 | 6 | namespace NetworkToolkit.Tests 7 | { 8 | internal static class TestCertificates 9 | { 10 | static readonly X509Certificate2 s_SelfSignedServerCert = CreateSelfSigned13ServerCertificate(); 11 | 12 | public static X509Certificate2 GetSelfSigned13ServerCertificate() => 13 | new X509Certificate2(s_SelfSignedServerCert); 14 | 15 | private static X509Certificate2 CreateSelfSigned13ServerCertificate() 16 | { 17 | using RSA rsa = RSA.Create(); 18 | 19 | var sanBuilder = new SubjectAlternativeNameBuilder(); 20 | sanBuilder.AddDnsName("localhost"); 21 | 22 | var certReq = new CertificateRequest("CN=localhost", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); 23 | certReq.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false)); 24 | certReq.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, false)); 25 | certReq.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, true)); 26 | certReq.CertificateExtensions.Add(sanBuilder.Build()); 27 | 28 | X509Certificate2 innerCert = certReq.CreateSelfSigned(DateTimeOffset.UtcNow.AddMonths(-1), DateTimeOffset.UtcNow.AddMonths(1)); 29 | 30 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 31 | { 32 | using (innerCert) 33 | { 34 | return new X509Certificate2(innerCert.Export(X509ContentType.Pfx)); 35 | } 36 | } 37 | else 38 | { 39 | return innerCert; 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /NetworkToolkit.Tests/TestExtensions.cs: -------------------------------------------------------------------------------- 1 | using NetworkToolkit.Http.Primitives; 2 | using NetworkToolkit.Tests.Http; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace NetworkToolkit.Tests 9 | { 10 | internal static class TestExtensions 11 | { 12 | public static async Task ReadAllHeadersAsync(this ValueHttpRequest request) 13 | { 14 | var sink = new TestHeadersSink(); 15 | 16 | if (await request.ReadToHeadersAsync().ConfigureAwait(false)) 17 | { 18 | await request.ReadHeadersAsync(sink, state: null).ConfigureAwait(false); 19 | } 20 | 21 | return sink; 22 | } 23 | 24 | public static async Task ReadAllTrailingHeadersAsync(this ValueHttpRequest request) 25 | { 26 | var sink = new TestHeadersSink(); 27 | 28 | if (await request.ReadToTrailingHeadersAsync().ConfigureAwait(false)) 29 | { 30 | await request.ReadHeadersAsync(sink, state: null).ConfigureAwait(false); 31 | } 32 | 33 | return sink; 34 | } 35 | 36 | public static async Task ReadAllContentAsync(this ValueHttpRequest request) 37 | { 38 | var memoryStream = new MemoryStream(); 39 | 40 | var contentStream = new HttpContentStream(request, ownsRequest: false); 41 | await using (contentStream.ConfigureAwait(false)) 42 | { 43 | await contentStream.CopyToAsync(memoryStream).ConfigureAwait(false); 44 | } 45 | 46 | return memoryStream.ToArray(); 47 | } 48 | 49 | public static async Task ReadAllContentAsStringAsync(this ValueHttpRequest request) 50 | => Encoding.UTF8.GetString(await ReadAllContentAsync(request).ConfigureAwait(false)); 51 | 52 | public static void WriteHeaders(this ValueHttpRequest request, TestHeadersSink headers) 53 | { 54 | foreach (KeyValuePair> header in headers) 55 | { 56 | foreach (string headerValue in header.Value) 57 | { 58 | request.WriteHeader(header.Key, headerValue); 59 | } 60 | } 61 | } 62 | 63 | public static void WriteTrailingHeaders(this ValueHttpRequest request, TestHeadersSink headers) 64 | { 65 | foreach (KeyValuePair> header in headers) 66 | { 67 | foreach (string headerValue in header.Value) 68 | { 69 | request.WriteTrailingHeader(header.Key, headerValue); 70 | } 71 | } 72 | } 73 | 74 | public static ValueTask WriteContentAsync(this ValueHttpRequest request, string content) => 75 | request.WriteContentAsync(Encoding.UTF8.GetBytes(content)); 76 | 77 | public static async Task WriteContentAsync(this ValueHttpRequest request, List content) 78 | { 79 | foreach (string chunk in content) 80 | { 81 | await request.WriteContentAsync(chunk).ConfigureAwait(false); 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /NetworkToolkit.Tests/TestStreamBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Runtime.ExceptionServices; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace NetworkToolkit.Tests 10 | { 11 | internal abstract class TestStreamBase : Stream, IScatterGatherStream, ICompletableStream, ICancellableAsyncDisposable 12 | { 13 | public bool CanScatterGather => true; 14 | public virtual bool CanCompleteWrites => false; 15 | 16 | public override bool CanRead => false; 17 | 18 | public override bool CanWrite => false; 19 | 20 | public override bool CanSeek => false; 21 | public override long Length => throw new InvalidOperationException(); 22 | public override long Position { get => throw new InvalidOperationException(); set => throw new InvalidOperationException(); } 23 | 24 | public sealed override ValueTask DisposeAsync() => 25 | DisposeAsync(CancellationToken.None); 26 | 27 | public virtual ValueTask DisposeAsync(CancellationToken cancellationToken) => 28 | default; 29 | 30 | public virtual ValueTask CompleteWritesAsync(CancellationToken cancellationToken = default) => 31 | ValueTask.FromException(ExceptionDispatchInfo.SetCurrentStackTrace(new InvalidOperationException())); 32 | 33 | public override void Flush() => throw new NotImplementedException(); 34 | 35 | public override long Seek(long offset, SeekOrigin origin) => throw new NotImplementedException(); 36 | 37 | public override void SetLength(long value) => throw new NotImplementedException(); 38 | 39 | public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) => 40 | ValueTask.FromException(ExceptionDispatchInfo.SetCurrentStackTrace(new InvalidOperationException())); 41 | 42 | public virtual ValueTask ReadAsync(IReadOnlyList> buffers, CancellationToken cancellationToken = default) => 43 | ReadAsync(buffers.Count != 0 ? buffers[0] : default, cancellationToken); 44 | 45 | public sealed override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => 46 | ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); 47 | 48 | public sealed override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) => 49 | TaskToApm.Begin(ReadAsync(buffer, offset, count), callback, state); 50 | 51 | public sealed override int EndRead(IAsyncResult asyncResult) => 52 | TaskToApm.End(asyncResult); 53 | 54 | public sealed override int Read(byte[] buffer, int offset, int count) => 55 | Tools.BlockForResult(ReadAsync(buffer.AsMemory(offset, count))); 56 | 57 | public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => 58 | ValueTask.FromException(ExceptionDispatchInfo.SetCurrentStackTrace(new InvalidOperationException())); 59 | 60 | public virtual async ValueTask WriteAsync(IReadOnlyList> buffers, CancellationToken cancellationToken = default) 61 | { 62 | for (int i = 0, count = buffers.Count; i != count; ++i) 63 | { 64 | await WriteAsync(buffers[i], cancellationToken).ConfigureAwait(false); 65 | } 66 | } 67 | 68 | public sealed override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => 69 | WriteAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); 70 | 71 | public sealed override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) => 72 | TaskToApm.Begin(WriteAsync(buffer, offset, count), callback, state); 73 | 74 | public sealed override void EndWrite(IAsyncResult asyncResult) => 75 | TaskToApm.End(asyncResult); 76 | 77 | public sealed override void Write(byte[] buffer, int offset, int count) => 78 | Tools.BlockForResult(WriteAsync(buffer.AsMemory(offset, count))); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /NetworkToolkit.Tests/TestsBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading.Tasks; 4 | 5 | namespace NetworkToolkit.Tests 6 | { 7 | public class TestsBase 8 | { 9 | public int DefaultTestTimeout = 500; // in milliseconds. 10 | 11 | public async Task RunClientServer(Func clientFunc, Func serverFunc, int? millisecondsTimeout = null) 12 | { 13 | Task[] tasks = new[] 14 | { 15 | Task.Run(() => clientFunc()), 16 | Task.Run(() => serverFunc()) 17 | }; 18 | 19 | if (Debugger.IsAttached) 20 | { 21 | await tasks.WhenAllOrAnyFailed().ConfigureAwait(false); 22 | } 23 | else 24 | { 25 | await tasks.WhenAllOrAnyFailed(millisecondsTimeout ?? DefaultTestTimeout).ConfigureAwait(false); 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /NetworkToolkit.Tests/TricklingConnectionFactory.cs: -------------------------------------------------------------------------------- 1 | using NetworkToolkit.Connections; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace NetworkToolkit.Tests 10 | { 11 | internal sealed class TricklingConnectionFactory : FilteringConnectionFactory 12 | { 13 | private static int[] s_defaultTrickleSequence = new[] { 1 }; 14 | private readonly IEnumerable _trickleSequence = s_defaultTrickleSequence; 15 | 16 | public IEnumerable TrickleSequence 17 | { 18 | get => _trickleSequence; 19 | init 20 | { 21 | Debug.Assert(!value.Any(x => x <= 0)); 22 | _trickleSequence = value.ToArray(); 23 | } 24 | } 25 | 26 | public bool ForceAsync { get; init; } 27 | 28 | public TricklingConnectionFactory(ConnectionFactory baseFactory) 29 | : base(baseFactory) 30 | { 31 | } 32 | 33 | public override async ValueTask ConnectAsync(EndPoint endPoint, IConnectionProperties? options = null, CancellationToken cancellationToken = default) 34 | { 35 | Connection c = await BaseFactory.ConnectAsync(endPoint, options, cancellationToken).ConfigureAwait(false); 36 | return new FilteringConnection(c, new TricklingStream(c.Stream, _trickleSequence, ForceAsync)); 37 | } 38 | 39 | public override async ValueTask ListenAsync(EndPoint? endPoint = null, IConnectionProperties? options = null, CancellationToken cancellationToken = default) 40 | { 41 | ConnectionListener listener = await BaseFactory.ListenAsync(endPoint, options, cancellationToken).ConfigureAwait(false); 42 | return new TricklingListener(listener, _trickleSequence, ForceAsync); 43 | } 44 | 45 | private sealed class TricklingListener : FilteringConnectionListener 46 | { 47 | private readonly IEnumerable _trickleSequence; 48 | private readonly bool _forceAsync; 49 | 50 | public TricklingListener(ConnectionListener baseListener, IEnumerable trickleSequence, bool forceAsync) : base(baseListener) 51 | { 52 | _trickleSequence = trickleSequence; 53 | _forceAsync = forceAsync; 54 | } 55 | 56 | public override async ValueTask AcceptConnectionAsync(IConnectionProperties? options = null, CancellationToken cancellationToken = default) 57 | { 58 | Connection? c = await BaseListener.AcceptConnectionAsync(options, cancellationToken).ConfigureAwait(false); 59 | return c != null ? new FilteringConnection(c, new TricklingStream(c.Stream, _trickleSequence, _forceAsync)) : null; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /NetworkToolkit.Tests/TricklingStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace NetworkToolkit.Tests 10 | { 11 | internal sealed class TricklingStream : TestStreamBase 12 | { 13 | private readonly Stream _baseStream; 14 | private readonly int[] _trickleSequence; 15 | private readonly bool _forceAsync; 16 | private int _readIdx; 17 | 18 | public override bool CanRead => _baseStream.CanRead; 19 | public override bool CanWrite => _baseStream.CanWrite; 20 | public override bool CanCompleteWrites => _baseStream is ICompletableStream s && s.CanCompleteWrites; 21 | 22 | public TricklingStream(Stream baseStream, IEnumerable trickleSequence, bool forceAsync) 23 | { 24 | _baseStream = baseStream; 25 | _trickleSequence = trickleSequence.ToArray(); 26 | _forceAsync = forceAsync; 27 | Debug.Assert(_trickleSequence.Length > 0); 28 | } 29 | 30 | protected override void Dispose(bool disposing) 31 | { 32 | if(disposing) _baseStream.Dispose(); 33 | } 34 | 35 | public override ValueTask DisposeAsync(CancellationToken cancellationToken) => 36 | _baseStream.DisposeAsync(cancellationToken); 37 | 38 | public override async ValueTask CompleteWritesAsync(CancellationToken cancellationToken = default) 39 | { 40 | if (_baseStream is ICompletableStream s && s.CanCompleteWrites) 41 | { 42 | await s.CompleteWritesAsync(cancellationToken).ConfigureAwait(false); 43 | } 44 | else 45 | { 46 | throw new NotImplementedException(); 47 | } 48 | } 49 | 50 | public override void Flush() => 51 | _baseStream.Flush(); 52 | 53 | public override Task FlushAsync(CancellationToken cancellationToken) => 54 | _baseStream.FlushAsync(cancellationToken); 55 | 56 | private int NextReadSize() 57 | { 58 | int size = _trickleSequence[_readIdx]; 59 | _readIdx = (_readIdx + 1) % _trickleSequence.Length; 60 | return size; 61 | } 62 | 63 | public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) 64 | { 65 | int readLength = Math.Min(buffer.Length, NextReadSize()); 66 | 67 | ValueTask readTask = _baseStream.ReadAsync(buffer.Slice(0, readLength), cancellationToken); 68 | 69 | if (readTask.IsCompleted && _forceAsync) 70 | { 71 | await Task.Yield(); 72 | } 73 | 74 | return await readTask.ConfigureAwait(false); 75 | } 76 | 77 | public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => 78 | _baseStream.WriteAsync(buffer, cancellationToken); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /NetworkToolkit.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30524.135 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetworkToolkit", "NetworkToolkit\NetworkToolkit.csproj", "{B120512A-8073-40FE-B76A-6B960B4961F7}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{8E172989-D93E-4746-A1A0-39978742AE5F}" 9 | ProjectSection(SolutionItems) = preProject 10 | examples\Directory.build.props = examples\Directory.build.props 11 | EndProjectSection 12 | EndProject 13 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{1E40D58A-9500-4F40-AED4-39078E5B6C55}" 14 | EndProject 15 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{FFD75251-93AA-4A7B-910B-D41654EBBFC1}" 16 | EndProject 17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetworkToolkit.Tests", "NetworkToolkit.Tests\NetworkToolkit.Tests.csproj", "{7799779B-AB53-48F5-863B-C382A6CF5BF5}" 18 | EndProject 19 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{67C37A73-F870-423D-A32B-227F2BD8AC7F}" 20 | ProjectSection(SolutionItems) = preProject 21 | .editorconfig = .editorconfig 22 | Directory.build.props = Directory.build.props 23 | README.md = README.md 24 | EndProjectSection 25 | EndProject 26 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetworkToolkit.Benchmarks", "NetworkToolkit.Benchmarks\NetworkToolkit.Benchmarks.csproj", "{878D7833-7AE8-46C9-ACA7-71805EF2634B}" 27 | EndProject 28 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetworkToolkit.ProfilerTest", "NetworkToolkit.ProfilerTest\NetworkToolkit.ProfilerTest.csproj", "{3905480E-3812-4A38-A4E4-0560118863AD}" 29 | EndProject 30 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PreparedHeadersSample", "examples\http\PreparedHeadersSample\PreparedHeadersSample.csproj", "{CEA951CB-F889-486D-9E72-1B87EE485E6D}" 31 | EndProject 32 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleRequestSample", "examples\http\SimpleRequestSample\SimpleRequestSample.csproj", "{B88CA8A9-D5CA-4DAC-923A-D52D481153EA}" 33 | EndProject 34 | Global 35 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 36 | Debug|Any CPU = Debug|Any CPU 37 | Release|Any CPU = Release|Any CPU 38 | EndGlobalSection 39 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 40 | {B120512A-8073-40FE-B76A-6B960B4961F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {B120512A-8073-40FE-B76A-6B960B4961F7}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {B120512A-8073-40FE-B76A-6B960B4961F7}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {B120512A-8073-40FE-B76A-6B960B4961F7}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {7799779B-AB53-48F5-863B-C382A6CF5BF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {7799779B-AB53-48F5-863B-C382A6CF5BF5}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {7799779B-AB53-48F5-863B-C382A6CF5BF5}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {7799779B-AB53-48F5-863B-C382A6CF5BF5}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {878D7833-7AE8-46C9-ACA7-71805EF2634B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {878D7833-7AE8-46C9-ACA7-71805EF2634B}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {878D7833-7AE8-46C9-ACA7-71805EF2634B}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {878D7833-7AE8-46C9-ACA7-71805EF2634B}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {3905480E-3812-4A38-A4E4-0560118863AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {3905480E-3812-4A38-A4E4-0560118863AD}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {3905480E-3812-4A38-A4E4-0560118863AD}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {3905480E-3812-4A38-A4E4-0560118863AD}.Release|Any CPU.Build.0 = Release|Any CPU 56 | {CEA951CB-F889-486D-9E72-1B87EE485E6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 57 | {CEA951CB-F889-486D-9E72-1B87EE485E6D}.Debug|Any CPU.Build.0 = Debug|Any CPU 58 | {CEA951CB-F889-486D-9E72-1B87EE485E6D}.Release|Any CPU.ActiveCfg = Release|Any CPU 59 | {CEA951CB-F889-486D-9E72-1B87EE485E6D}.Release|Any CPU.Build.0 = Release|Any CPU 60 | {B88CA8A9-D5CA-4DAC-923A-D52D481153EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 61 | {B88CA8A9-D5CA-4DAC-923A-D52D481153EA}.Debug|Any CPU.Build.0 = Debug|Any CPU 62 | {B88CA8A9-D5CA-4DAC-923A-D52D481153EA}.Release|Any CPU.ActiveCfg = Release|Any CPU 63 | {B88CA8A9-D5CA-4DAC-923A-D52D481153EA}.Release|Any CPU.Build.0 = Release|Any CPU 64 | EndGlobalSection 65 | GlobalSection(SolutionProperties) = preSolution 66 | HideSolutionNode = FALSE 67 | EndGlobalSection 68 | GlobalSection(NestedProjects) = preSolution 69 | {B120512A-8073-40FE-B76A-6B960B4961F7} = {1E40D58A-9500-4F40-AED4-39078E5B6C55} 70 | {7799779B-AB53-48F5-863B-C382A6CF5BF5} = {FFD75251-93AA-4A7B-910B-D41654EBBFC1} 71 | {878D7833-7AE8-46C9-ACA7-71805EF2634B} = {FFD75251-93AA-4A7B-910B-D41654EBBFC1} 72 | {3905480E-3812-4A38-A4E4-0560118863AD} = {FFD75251-93AA-4A7B-910B-D41654EBBFC1} 73 | {CEA951CB-F889-486D-9E72-1B87EE485E6D} = {8E172989-D93E-4746-A1A0-39978742AE5F} 74 | {B88CA8A9-D5CA-4DAC-923A-D52D481153EA} = {8E172989-D93E-4746-A1A0-39978742AE5F} 75 | EndGlobalSection 76 | GlobalSection(ExtensibilityGlobals) = postSolution 77 | SolutionGuid = {0A202D08-3F30-44C0-BFFC-8FF60BC39D8D} 78 | EndGlobalSection 79 | EndGlobal 80 | -------------------------------------------------------------------------------- /NetworkToolkit/Assembly.cs: -------------------------------------------------------------------------------- 1 | [module: System.Runtime.CompilerServices.SkipLocalsInit] 2 | -------------------------------------------------------------------------------- /NetworkToolkit/Connections/Connection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Net; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace NetworkToolkit.Connections 9 | { 10 | /// 11 | /// A Stream-oriented connection. 12 | /// 13 | public abstract class Connection : ICancellableAsyncDisposable, IConnectionProperties 14 | { 15 | private Stream? _stream; 16 | private int _disposed; 17 | 18 | /// 19 | /// The connection's local endpoint, if any. 20 | /// 21 | public abstract EndPoint? LocalEndPoint { get; } 22 | 23 | /// 24 | /// The connection's remote endpoint, if any. 25 | /// 26 | public abstract EndPoint? RemoteEndPoint { get; } 27 | 28 | /// 29 | /// The connection's stream. 30 | /// 31 | public Stream Stream => _disposed == 2 ? throw new ObjectDisposedException(GetType().Name) : _stream!; 32 | 33 | /// 34 | /// Constructs a new with a stream. 35 | /// 36 | /// The connection's stream. 37 | protected Connection(Stream stream) 38 | { 39 | _stream = stream ?? throw new ArgumentNullException(nameof(stream)); 40 | } 41 | 42 | /// 43 | public ValueTask DisposeAsync() 44 | { 45 | return DisposeAsync(CancellationToken.None); 46 | } 47 | 48 | /// 49 | public async ValueTask DisposeAsync(CancellationToken cancellationToken) 50 | { 51 | if (Interlocked.Exchange(ref _disposed, 1) != 0) 52 | { 53 | return; 54 | } 55 | 56 | await DisposeAsyncCore(cancellationToken).ConfigureAwait(false); 57 | Volatile.Write(ref _disposed, 2); 58 | 59 | Stream? stream = _stream; 60 | Debug.Assert(stream != null); 61 | 62 | _stream = null; 63 | 64 | await stream.DisposeAsync(cancellationToken).ConfigureAwait(false); 65 | } 66 | 67 | /// 68 | /// Disposes of the connection. 69 | /// The connection's will be disposed immediately after . 70 | /// 71 | /// 72 | /// A cancellation token for the asynchronous operation. 73 | /// If canceled, the dispose may finish sooner but with only minimal cleanup, i.e. without flushing buffers to disk. 74 | /// 75 | /// A representing the asynchronous operation. 76 | protected abstract ValueTask DisposeAsyncCore(CancellationToken cancellationToken); 77 | 78 | /// 79 | public virtual bool TryGetProperty(Type type, out object? value) 80 | { 81 | value = null; 82 | return false; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /NetworkToolkit/Connections/ConnectionFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace NetworkToolkit.Connections 7 | { 8 | /// 9 | /// A connection factory. 10 | /// 11 | public abstract class ConnectionFactory : ICancellableAsyncDisposable 12 | { 13 | private int _disposed; 14 | 15 | /// 16 | public ValueTask DisposeAsync() 17 | { 18 | return DisposeAsync(CancellationToken.None); 19 | } 20 | 21 | /// 22 | public ValueTask DisposeAsync(CancellationToken cancellationToken) 23 | { 24 | return Interlocked.Exchange(ref _disposed, 1) == 0 25 | ? DisposeAsyncCore(cancellationToken) 26 | : default; 27 | } 28 | 29 | /// 30 | /// Disposes of the connection factory. 31 | /// 32 | /// 33 | /// A cancellation token for the asynchronous operation. 34 | /// If canceled, the dispose may finish sooner but with only minimal cleanup, i.e. without flushing buffers to disk. 35 | /// 36 | /// A representing the asynchronous operation. 37 | protected abstract ValueTask DisposeAsyncCore(CancellationToken cancellationToken); 38 | 39 | /// 40 | /// Establishes a new to an . 41 | /// 42 | /// The to continue to. 43 | /// Any options used to control the operation. 44 | /// A cancellation token for the asynchronous operation. 45 | /// An established . 46 | public abstract ValueTask ConnectAsync(EndPoint endPoint, IConnectionProperties? options = null, CancellationToken cancellationToken = default); 47 | 48 | /// 49 | /// Starts listening at an . 50 | /// 51 | /// The to listen on, if any. 52 | /// Any options used to control the operation. 53 | /// A cancellation token for the asynchronous operation. 54 | /// An active . 55 | public abstract ValueTask ListenAsync(EndPoint? endPoint = null, IConnectionProperties? options = null, CancellationToken cancellationToken = default); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /NetworkToolkit/Connections/ConnectionListener.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace NetworkToolkit.Connections 7 | { 8 | /// 9 | /// A connection listener. 10 | /// 11 | public abstract class ConnectionListener : IAsyncDisposable 12 | { 13 | private int _disposed; 14 | 15 | /// 16 | /// The listener's local , if any. 17 | /// 18 | public abstract EndPoint? EndPoint { get; } 19 | 20 | /// 21 | public ValueTask DisposeAsync() 22 | { 23 | return DisposeAsync(CancellationToken.None); 24 | } 25 | 26 | /// 27 | public ValueTask DisposeAsync(CancellationToken cancellationToken) 28 | { 29 | return Interlocked.Exchange(ref _disposed, 1) == 0 30 | ? DisposeAsyncCore(cancellationToken) 31 | : default; 32 | } 33 | 34 | /// 35 | /// Disposes of the connection. 36 | /// 37 | /// 38 | /// A cancellation token for the asynchronous operation. 39 | /// If canceled, the dispose may finish sooner but with only minimal cleanup, i.e. without flushing buffers to disk. 40 | /// 41 | /// A representing the asynchronous operation. 42 | protected abstract ValueTask DisposeAsyncCore(CancellationToken cancellationToken); 43 | 44 | /// 45 | /// Accepts a new , if available. 46 | /// 47 | /// Any options used to control the operation. 48 | /// A cancellation token for the asynchronous operation. 49 | /// 50 | /// If the listener is active, an established . 51 | /// If the operation was cancelled, null. 52 | /// 53 | public abstract ValueTask AcceptConnectionAsync(IConnectionProperties? options = null, CancellationToken cancellationToken = default); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /NetworkToolkit/Connections/ConnectionProperties.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace NetworkToolkit.Connections 5 | { 6 | /// 7 | /// A collection of connection properties. 8 | /// 9 | public sealed class ConnectionProperties : IConnectionProperties 10 | { 11 | private readonly Dictionary _values = new(); 12 | private bool _frozen; 13 | 14 | /// 15 | /// Adds a new property to the conenction. 16 | /// 17 | /// The type of property to add. 18 | /// The key of the property. 19 | /// The property's value. 20 | public void Add(ConnectionPropertyKey propertyKey, T value) 21 | { 22 | if (_frozen) throw new InvalidOperationException($"The {nameof(ConnectionProperties)} can not be altered after a property has been retrieved."); 23 | _values.Add(typeof(T), value); 24 | } 25 | 26 | /// 27 | public bool TryGetProperty(Type type, out object? value) 28 | { 29 | if (type == null) throw new ArgumentNullException(nameof(type)); 30 | 31 | _frozen = true; 32 | return _values.TryGetValue(type, out value); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /NetworkToolkit/Connections/ConnectionPropertyExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace NetworkToolkit.Connections 5 | { 6 | /// 7 | /// Extension methods for 8 | /// 9 | public static class ConnectionPropertyExtensions 10 | { 11 | /// 12 | /// Gets a property, throwing an exception if none found. 13 | /// 14 | /// The type of the property to retrieve. 15 | /// The to retrieve the property from. 16 | /// The key of the property. 17 | /// The value of the retrieved property. 18 | public static T GetProperty(this IConnectionProperties properties, ConnectionPropertyKey propertyKey) 19 | { 20 | if (properties == null) throw new ArgumentNullException(nameof(properties)); 21 | 22 | if (properties.TryGetProperty(typeof(T), out object? objectValue) && objectValue is T typedValue) 23 | { 24 | return typedValue; 25 | } 26 | 27 | throw new Exception($"Property of type '{nameof(T)}' does not exist in this {nameof(IConnectionProperties)}, but is required."); 28 | } 29 | 30 | /// 31 | /// Gets a property. 32 | /// 33 | /// The type of the property to retrieve. 34 | /// The to retrieve the property from. 35 | /// The key of the property. 36 | /// The value of the property retrieved. 37 | /// 38 | /// If a property for the given was found, true. 39 | /// Otherwise, false. 40 | /// 41 | public static bool TryGetProperty(this IConnectionProperties properties, ConnectionPropertyKey propertyKey, [MaybeNullWhen(false)] out T value) 42 | { 43 | if (properties == null) throw new ArgumentNullException(nameof(properties)); 44 | 45 | if (properties.TryGetProperty(typeof(T), out object? objectValue) && objectValue is T typedValue) 46 | { 47 | value = typedValue; 48 | return true; 49 | } 50 | else 51 | { 52 | value = default; 53 | return false; 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /NetworkToolkit/Connections/ConnectionPropertyKey.cs: -------------------------------------------------------------------------------- 1 | namespace NetworkToolkit.Connections 2 | { 3 | /// 4 | /// A connection property key. 5 | /// 6 | /// The type of property this key represents. 7 | public struct ConnectionPropertyKey 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /NetworkToolkit/Connections/FilteringConnection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace NetworkToolkit.Connections 11 | { 12 | /// 13 | /// A connection that filters another connection. 14 | /// 15 | public class FilteringConnection : Connection 16 | { 17 | /// 18 | /// The base connection. 19 | /// 20 | protected Connection BaseConnection { get; } 21 | 22 | /// 23 | public override EndPoint? LocalEndPoint => BaseConnection.LocalEndPoint; 24 | 25 | /// 26 | public override EndPoint? RemoteEndPoint => BaseConnection.RemoteEndPoint; 27 | 28 | /// 29 | /// Instantiates a new 30 | /// 31 | /// The base connection for the . 32 | /// The connection's stream. 33 | public FilteringConnection(Connection baseConnection, Stream stream) : base(stream) 34 | { 35 | BaseConnection = baseConnection ?? throw new ArgumentNullException(nameof(baseConnection)); 36 | } 37 | 38 | /// 39 | protected override async ValueTask DisposeAsyncCore(CancellationToken cancellationToken) 40 | { 41 | await Stream.DisposeAsync(cancellationToken).ConfigureAwait(false); 42 | await BaseConnection.DisposeAsync(cancellationToken).ConfigureAwait(false); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /NetworkToolkit/Connections/FilteringConnectionFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace NetworkToolkit.Connections 6 | { 7 | /// 8 | /// A connection factory that filters another connection factory. 9 | /// 10 | public abstract class FilteringConnectionFactory : ConnectionFactory 11 | { 12 | /// 13 | /// The base connection factory. 14 | /// 15 | protected ConnectionFactory BaseFactory { get; } 16 | 17 | /// 18 | /// Instantiates a new . 19 | /// 20 | /// The base connection factory for the . 21 | public FilteringConnectionFactory(ConnectionFactory baseFactory) 22 | { 23 | BaseFactory = baseFactory ?? throw new ArgumentNullException(nameof(baseFactory)); 24 | } 25 | 26 | /// 27 | protected override ValueTask DisposeAsyncCore(CancellationToken cancellationToken) 28 | => BaseFactory.DisposeAsync(cancellationToken); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /NetworkToolkit/Connections/FilteringConnectionListener.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace NetworkToolkit.Connections 7 | { 8 | /// 9 | /// A connection listener that filters another connection. 10 | /// 11 | public abstract class FilteringConnectionListener : ConnectionListener 12 | { 13 | /// 14 | /// The base connection listener. 15 | /// 16 | protected ConnectionListener BaseListener { get; } 17 | 18 | /// 19 | public override EndPoint? EndPoint => BaseListener.EndPoint; 20 | 21 | /// 22 | /// Instantiates a new . 23 | /// 24 | /// The base connection listener for the . 25 | public FilteringConnectionListener(ConnectionListener baseListener) 26 | { 27 | BaseListener = baseListener ?? throw new ArgumentNullException(nameof(baseListener)); 28 | } 29 | 30 | /// 31 | protected override ValueTask DisposeAsyncCore(CancellationToken cancellationToken) 32 | => BaseListener.DisposeAsync(cancellationToken); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /NetworkToolkit/Connections/HttpTunnelConnectionFactory.cs: -------------------------------------------------------------------------------- 1 | using NetworkToolkit.Http.Primitives; 2 | using System; 3 | using System.Diagnostics; 4 | using System.Globalization; 5 | using System.Net; 6 | using System.Net.Http; 7 | using System.Net.Sockets; 8 | using System.Runtime.ExceptionServices; 9 | using System.Text; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | 13 | namespace NetworkToolkit.Connections 14 | { 15 | /// 16 | /// A filtering connection factory implementing an HTTP tunnel. 17 | /// 18 | public sealed class HttpTunnelConnectionFactory : ConnectionFactory 19 | { 20 | private HttpConnection _httpConnection; 21 | private readonly HttpPrimitiveVersion _httpVersion; 22 | private readonly HttpVersionPolicy _httpVersionPolicy; 23 | private readonly bool _ownsConnection; 24 | 25 | /// 26 | /// Instantiates a new . 27 | /// 28 | /// The to open a tunnel over. 29 | /// The HTTP version to establish the connect tunnel over. 30 | /// A policy controlling what HTTP version will be used for the connect tunnel. 31 | /// If true, the will be disposed when the tunnel is disposed. 32 | public HttpTunnelConnectionFactory(HttpConnection httpConnection, HttpPrimitiveVersion httpVersion, HttpVersionPolicy httpVersionPolicy, bool ownsConnection = false) 33 | { 34 | _httpConnection = httpConnection ?? throw new ArgumentNullException(nameof(httpConnection)); 35 | _httpVersion = httpVersion ?? throw new ArgumentNullException(nameof(httpVersion)); 36 | _httpVersionPolicy = httpVersionPolicy; 37 | _ownsConnection = ownsConnection; 38 | } 39 | 40 | /// 41 | public override async ValueTask ConnectAsync(EndPoint endPoint, IConnectionProperties? options = null, CancellationToken cancellationToken = default) 42 | { 43 | string authority = endPoint switch 44 | { 45 | DnsEndPoint dns => Tools.EscapeIdnHost(dns.Host) + ":" + dns.Port.ToString(CultureInfo.InvariantCulture), 46 | IPEndPoint ip4 when ip4.AddressFamily == AddressFamily.InterNetwork => ip4.Address.ToString() + ":" + ip4.Port.ToString(CultureInfo.InvariantCulture), 47 | IPEndPoint ip6 when ip6.AddressFamily == AddressFamily.InterNetworkV6 => "[" + ip6.Address.ToString() + "]:" + ip6.Port.ToString(CultureInfo.InvariantCulture), 48 | null => throw new ArgumentNullException(nameof(endPoint)), 49 | _ => throw new ArgumentException($"{nameof(EndPoint)} is of an unsupported type. Must be one of {nameof(DnsEndPoint)} or {nameof(IPEndPoint)}", nameof(endPoint)) 50 | }; 51 | 52 | byte[] authorityBytes = Encoding.ASCII.GetBytes(authority); 53 | 54 | ValueHttpRequest request = (await _httpConnection.CreateNewRequestAsync(_httpVersion, _httpVersionPolicy, cancellationToken).ConfigureAwait(false)) 55 | ?? throw new Exception($"{nameof(HttpConnection)} in use by {nameof(HttpTunnelConnectionFactory)} has been closed by peer."); 56 | 57 | try 58 | { 59 | request.ConfigureRequest(contentLength: null, hasTrailingHeaders: false); 60 | request.WriteConnectRequest(authorityBytes); 61 | await request.FlushHeadersAsync(cancellationToken).ConfigureAwait(false); 62 | 63 | bool hasResponse = await request.ReadToFinalResponseAsync(cancellationToken).ConfigureAwait(false); 64 | Debug.Assert(hasResponse); 65 | 66 | if ((int)request.StatusCode > 299) 67 | { 68 | throw new Exception($"Connect to HTTP tunnel failed; received status code {request.StatusCode}."); 69 | } 70 | 71 | var localEndPoint = new TunnelEndPoint(request.LocalEndPoint, request.RemoteEndPoint); 72 | var stream = new HttpContentStream(request, ownsRequest: true); 73 | return new HttpTunnelConnection(localEndPoint, endPoint, stream); 74 | } 75 | catch 76 | { 77 | await request.DisposeAsync(cancellationToken).ConfigureAwait(false); 78 | throw; 79 | } 80 | } 81 | 82 | /// 83 | public override ValueTask ListenAsync(EndPoint? endPoint, IConnectionProperties? options = null, CancellationToken cancellationToken = default) 84 | { 85 | return ValueTask.FromException(ExceptionDispatchInfo.SetCurrentStackTrace(new NotSupportedException($"{nameof(HttpTunnelConnectionFactory)} does not support listening."))); 86 | } 87 | 88 | /// 89 | protected override ValueTask DisposeAsyncCore(CancellationToken cancellationToken) 90 | { 91 | if (_ownsConnection) 92 | { 93 | return _httpConnection.DisposeAsync(cancellationToken); 94 | } 95 | 96 | return default; 97 | } 98 | 99 | private sealed class HttpTunnelConnection : Connection 100 | { 101 | public HttpTunnelConnection(EndPoint localEndPoint, EndPoint remoteEndPoint, HttpContentStream stream) : base(stream) 102 | { 103 | LocalEndPoint = localEndPoint; 104 | RemoteEndPoint = remoteEndPoint; 105 | } 106 | 107 | public override EndPoint? LocalEndPoint { get; } 108 | 109 | public override EndPoint? RemoteEndPoint { get; } 110 | 111 | protected override ValueTask DisposeAsyncCore(CancellationToken cancellationToken) 112 | { 113 | return default; 114 | } 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /NetworkToolkit/Connections/IConnectionProperties.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NetworkToolkit.Connections 4 | { 5 | /// 6 | /// A read-only collection of connection properties. 7 | /// 8 | public interface IConnectionProperties 9 | { 10 | /// 11 | /// Gets a property. 12 | /// 13 | /// The type of the property to retrieve. 14 | /// The value of the property retrieved. 15 | /// 16 | /// If a property for the given was found, true. 17 | /// Otherwise, false. 18 | /// 19 | bool TryGetProperty(Type type, out object? value); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /NetworkToolkit/Connections/MemoryConnectionFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Diagnostics; 4 | using System.IO.Pipelines; 5 | using System.Net; 6 | using System.Net.Sockets; 7 | using System.Runtime.ExceptionServices; 8 | using System.Threading; 9 | using System.Threading.Channels; 10 | using System.Threading.Tasks; 11 | 12 | namespace NetworkToolkit.Connections 13 | { 14 | /// 15 | /// A factory of in-memory connections. 16 | /// 17 | /// 18 | /// Once a has been opened via , 19 | /// calls to should use the returned by . 20 | /// 21 | public sealed class MemoryConnectionFactory : ConnectionFactory 22 | { 23 | private readonly ConcurrentDictionary>> _incomingConnection = new (); 24 | 25 | /// 26 | /// Options used when creating the client-side pipe. 27 | /// 28 | public PipeOptions ClientPipeOptions { get; init; } = PipeOptions.Default; 29 | 30 | /// 31 | /// Options used when creating the server-side pipe. 32 | /// 33 | public PipeOptions ServerPipeOptions { get; init; } = PipeOptions.Default; 34 | 35 | /// 36 | protected override ValueTask DisposeAsyncCore(CancellationToken cancellationToken) 37 | { 38 | return default; 39 | } 40 | 41 | /// 42 | public override ValueTask ConnectAsync(EndPoint endPoint, IConnectionProperties? options = null, CancellationToken cancellationToken = default) 43 | { 44 | if (endPoint == null) return ValueTask.FromException(ExceptionDispatchInfo.SetCurrentStackTrace(new ArgumentNullException(nameof(endPoint)))); 45 | if (cancellationToken.IsCancellationRequested) return ValueTask.FromException(ExceptionDispatchInfo.SetCurrentStackTrace(new SocketException((int)SocketError.OperationAborted))); 46 | 47 | if (_incomingConnection.TryGetValue(endPoint, out Channel>? channel)) 48 | { 49 | var tcs = new TaskCompletionSource(); 50 | if (channel.Writer.TryWrite(tcs)) 51 | { 52 | return new ValueTask(tcs.Task); 53 | } 54 | } 55 | 56 | return ValueTask.FromException(ExceptionDispatchInfo.SetCurrentStackTrace(new SocketException((int)SocketError.ConnectionRefused))); 57 | } 58 | 59 | /// 60 | public override ValueTask ListenAsync(EndPoint? endPoint = null, IConnectionProperties? options = null, CancellationToken cancellationToken = default) 61 | { 62 | if (cancellationToken.IsCancellationRequested) 63 | { 64 | return ValueTask.FromException(ExceptionDispatchInfo.SetCurrentStackTrace(new SocketException((int)SocketError.OperationAborted))); 65 | } 66 | 67 | endPoint ??= new SentinelEndPoint(); 68 | 69 | Channel> channel = Channel.CreateUnbounded>(); 70 | 71 | if (!_incomingConnection.TryAdd(endPoint, channel)) 72 | { 73 | return ValueTask.FromException(ExceptionDispatchInfo.SetCurrentStackTrace(new SocketException((int)SocketError.AddressAlreadyInUse))); 74 | } 75 | 76 | return new ValueTask(new Listener(channel, _incomingConnection, endPoint, ClientPipeOptions, ServerPipeOptions)); 77 | } 78 | 79 | private sealed class SentinelEndPoint : EndPoint 80 | { 81 | public override AddressFamily AddressFamily => AddressFamily.Unspecified; 82 | } 83 | 84 | private sealed class Listener : ConnectionListener 85 | { 86 | private readonly Channel> _channel; 87 | private readonly ConcurrentDictionary>> _incomingConnection; 88 | private readonly PipeOptions _clientPipeOptions, _serverPipeOptions; 89 | private readonly EndPoint _endPoint; 90 | 91 | public override EndPoint? EndPoint => _endPoint; 92 | 93 | public Listener(Channel> channel, ConcurrentDictionary>> incomingConnection, EndPoint endPoint, PipeOptions clientPipeOptions, PipeOptions serverPipeOptions) 94 | { 95 | _channel = channel; 96 | _incomingConnection = incomingConnection; 97 | _clientPipeOptions = clientPipeOptions; 98 | _serverPipeOptions = serverPipeOptions; 99 | _endPoint = endPoint; 100 | } 101 | 102 | protected override ValueTask DisposeAsyncCore(CancellationToken cancellationToken) 103 | { 104 | bool removed = _incomingConnection.TryRemove(_endPoint, out Channel>? channel); 105 | Debug.Assert(removed); 106 | Debug.Assert(channel == _channel); 107 | 108 | channel.Writer.TryComplete(); 109 | 110 | while (channel.Reader.TryRead(out TaskCompletionSource? tcs)) 111 | { 112 | tcs.SetException(new SocketException((int)SocketError.ConnectionRefused)); 113 | } 114 | 115 | return default; 116 | } 117 | 118 | public override async ValueTask AcceptConnectionAsync(IConnectionProperties? options = null, CancellationToken cancellationToken = default) 119 | { 120 | TaskCompletionSource tcs; 121 | 122 | try 123 | { 124 | tcs = await _channel.Reader.ReadAsync(cancellationToken).ConfigureAwait(false); 125 | } 126 | catch (ChannelClosedException) 127 | { 128 | return null; 129 | } 130 | 131 | (Connection clientConnection, Connection serverConnection) = MemoryConnection.Create(new SentinelEndPoint(), _clientPipeOptions, _endPoint, _serverPipeOptions); 132 | 133 | tcs.SetResult(clientConnection); 134 | return serverConnection; 135 | } 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /NetworkToolkit/Connections/SslConnectionFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Security; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace NetworkToolkit.Connections 8 | { 9 | /// 10 | /// A connection factory using SSL. 11 | /// 12 | public sealed class SslConnectionFactory : ConnectionFactory 13 | { 14 | private readonly ConnectionFactory _baseFactory; 15 | 16 | /// 17 | /// A connection property used to pass a to . 18 | /// 19 | public static ConnectionPropertyKey SslClientAuthenticationOptionsPropertyKey => new(); 20 | 21 | /// 22 | /// A connection property used to pass a to . 23 | /// 24 | public static ConnectionPropertyKey SslServerAuthenticationOptionsPropertyKey => new(); 25 | 26 | /// 27 | /// A connection property that returns the underlying of an established . 28 | /// 29 | public static ConnectionPropertyKey SslStreamPropertyKey => new(); 30 | 31 | /// 32 | /// Instantiates a new . 33 | /// 34 | /// The base factory for the . 35 | public SslConnectionFactory(ConnectionFactory baseFactory) 36 | { 37 | _baseFactory = baseFactory; 38 | } 39 | 40 | /// 41 | public override async ValueTask ConnectAsync(EndPoint endPoint, IConnectionProperties? options = null, CancellationToken cancellationToken = default) 42 | { 43 | if (options == null) throw new ArgumentNullException(nameof(options)); 44 | 45 | SslClientAuthenticationOptions sslOptions = options.GetProperty(SslClientAuthenticationOptionsPropertyKey); 46 | 47 | Connection baseConnection = await _baseFactory.ConnectAsync(endPoint, options, cancellationToken).ConfigureAwait(false); 48 | SslStream? stream = null; 49 | 50 | try 51 | { 52 | stream = new SslStream(baseConnection.Stream, leaveInnerStreamOpen: false); 53 | 54 | await stream.AuthenticateAsClientAsync(sslOptions, cancellationToken).ConfigureAwait(false); 55 | return new SslConnection(baseConnection, stream); 56 | } 57 | catch 58 | { 59 | if (stream != null) await stream.DisposeAsync().ConfigureAwait(false); 60 | await baseConnection.DisposeAsync(cancellationToken).ConfigureAwait(false); 61 | throw; 62 | } 63 | } 64 | 65 | /// 66 | public override async ValueTask ListenAsync(EndPoint? endPoint = null, IConnectionProperties? options = null, CancellationToken cancellationToken = default) 67 | { 68 | if (options == null) throw new ArgumentNullException(nameof(options)); 69 | SslServerAuthenticationOptions sslOptions = options.GetProperty(SslServerAuthenticationOptionsPropertyKey); 70 | 71 | ConnectionListener baseListener = await _baseFactory.ListenAsync(endPoint, options, cancellationToken).ConfigureAwait(false); 72 | return new SslListener(baseListener, sslOptions); 73 | } 74 | 75 | /// 76 | protected override ValueTask DisposeAsyncCore(CancellationToken cancellationToken) 77 | { 78 | return _baseFactory.DisposeAsync(cancellationToken); 79 | } 80 | 81 | private sealed class SslListener : FilteringConnectionListener 82 | { 83 | private readonly SslServerAuthenticationOptions _sslOptions; 84 | 85 | public SslListener(ConnectionListener baseListener, SslServerAuthenticationOptions sslOptions) : base(baseListener) 86 | { 87 | _sslOptions = sslOptions; 88 | } 89 | 90 | public override async ValueTask AcceptConnectionAsync(IConnectionProperties? options = null, CancellationToken cancellationToken = default) 91 | { 92 | Connection? baseConnection = await BaseListener.AcceptConnectionAsync(options, cancellationToken).ConfigureAwait(false); 93 | 94 | if (baseConnection == null) 95 | { 96 | return null; 97 | } 98 | 99 | SslStream? stream = null; 100 | 101 | try 102 | { 103 | stream = new SslStream(baseConnection.Stream, leaveInnerStreamOpen: false); 104 | await stream.AuthenticateAsServerAsync(_sslOptions, cancellationToken).ConfigureAwait(false); 105 | return new SslConnection(baseConnection, stream); 106 | } 107 | catch 108 | { 109 | if(stream != null) await stream.DisposeAsync().ConfigureAwait(false); 110 | await baseConnection.DisposeAsync(cancellationToken).ConfigureAwait(false); 111 | throw; 112 | } 113 | } 114 | } 115 | 116 | private sealed class SslConnection : FilteringConnection 117 | { 118 | public SslConnection(Connection baseConnection, SslStream stream) : base(baseConnection, stream) 119 | { 120 | } 121 | 122 | public override bool TryGetProperty(Type type, out object? value) 123 | { 124 | if (type == typeof(SslStream)) 125 | { 126 | value = (SslStream)Stream; 127 | return true; 128 | } 129 | 130 | return BaseConnection.TryGetProperty(type, out value); 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /NetworkToolkit/Connections/WriteBufferingConnectionFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace NetworkToolkit.Connections 6 | { 7 | /// 8 | /// A connection factory that adds write buffering to underlying connections. 9 | /// 10 | public sealed class WriteBufferingConnectionFactory : FilteringConnectionFactory 11 | { 12 | /// 13 | /// Instantiates a new . 14 | /// 15 | /// The underlying factory that will have write buffering added to its connections. 16 | public WriteBufferingConnectionFactory(ConnectionFactory baseFactory) : base(baseFactory) 17 | { 18 | } 19 | 20 | /// 21 | public override async ValueTask ConnectAsync(EndPoint endPoint, IConnectionProperties? options = null, CancellationToken cancellationToken = default) 22 | { 23 | Connection con = await BaseFactory.ConnectAsync(endPoint, options, cancellationToken).ConfigureAwait(false); 24 | return new FilteringConnection(con, new WriteBufferingStream(con.Stream)); 25 | } 26 | 27 | /// 28 | public override async ValueTask ListenAsync(EndPoint? endPoint = null, IConnectionProperties? options = null, CancellationToken cancellationToken = default) 29 | { 30 | ConnectionListener listener = await BaseFactory.ListenAsync(endPoint, options, cancellationToken).ConfigureAwait(false); 31 | return new WriteBufferingConnectionListener(listener); 32 | } 33 | 34 | private sealed class WriteBufferingConnectionListener : FilteringConnectionListener 35 | { 36 | public WriteBufferingConnectionListener(ConnectionListener baseListener) : base(baseListener) 37 | { 38 | } 39 | 40 | public override async ValueTask AcceptConnectionAsync(IConnectionProperties? options = null, CancellationToken cancellationToken = default) 41 | { 42 | Connection? con = await BaseListener.AcceptConnectionAsync(options, cancellationToken).ConfigureAwait(false); 43 | if (con == null) return con; 44 | 45 | return new FilteringConnection(con, new WriteBufferingStream(con.Stream)); 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /NetworkToolkit/Http/AuthorityKey.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NetworkToolkit.Http 4 | { 5 | internal readonly struct AuthorityKey : IEquatable 6 | { 7 | public string IdnHost { get; } 8 | public int Port { get; } 9 | 10 | public AuthorityKey(string idnHost, int port) 11 | { 12 | IdnHost = idnHost; 13 | Port = port; 14 | } 15 | 16 | public bool Equals(AuthorityKey other) => 17 | Port == other.Port 18 | && string.Equals(IdnHost, other.IdnHost, StringComparison.Ordinal); 19 | 20 | public override bool Equals(object? obj) => 21 | obj is AuthorityKey key && Equals(key); 22 | 23 | public override int GetHashCode() => 24 | HashCode.Combine(IdnHost, Port); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /NetworkToolkit/Http/Headers/AcceptEncodingHeader.cs: -------------------------------------------------------------------------------- 1 | namespace NetworkToolkit.Http.Headers 2 | { 3 | /// 4 | /// The Accept-Encoding header. 5 | /// 6 | public sealed class AcceptEncodingHeader : PreparedHeaderName 7 | { 8 | internal AcceptEncodingHeader() 9 | : base("Accept-Encoding", http2StaticIndex: 16) 10 | { 11 | GzipDeflate = new PreparedHeader(this, "gzip, deflate", http2StaticIndex: 16); 12 | } 13 | 14 | /// 15 | /// The "Accept-Encoding: gzip, deflate" header. 16 | /// 17 | public PreparedHeader GzipDeflate { get; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /NetworkToolkit/Http/Headers/MethodHeader.cs: -------------------------------------------------------------------------------- 1 | namespace NetworkToolkit.Http.Headers 2 | { 3 | /// 4 | /// The :method pseudo-header. 5 | /// 6 | internal sealed class MethodHeader : PreparedHeaderName 7 | { 8 | public MethodHeader() 9 | : base(":method", http2StaticIndex: 2) 10 | { 11 | Get = new PreparedHeader(this, "GET", http2StaticIndex: 2); 12 | Post = new PreparedHeader(this, "POST", http2StaticIndex: 3); 13 | } 14 | 15 | public PreparedHeader Get { get; } 16 | public PreparedHeader Post { get; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /NetworkToolkit/Http/Headers/PathHeader.cs: -------------------------------------------------------------------------------- 1 | namespace NetworkToolkit.Http.Headers 2 | { 3 | /// 4 | /// The :path pseudo-header. 5 | /// 6 | internal sealed class PathHeader : PreparedHeaderName 7 | { 8 | public PathHeader() 9 | : base(":path", http2StaticIndex: 4) 10 | { 11 | Root = new PreparedHeader(this, "/", http2StaticIndex: 4); 12 | IndexHtml = new PreparedHeader(this, "/index.html", http2StaticIndex: 5); 13 | } 14 | 15 | public PreparedHeader Root { get; } 16 | public PreparedHeader IndexHtml { get; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /NetworkToolkit/Http/Headers/SchemeHeader.cs: -------------------------------------------------------------------------------- 1 | namespace NetworkToolkit.Http.Headers 2 | { 3 | /// 4 | /// The :scheme pseudo-header. 5 | /// 6 | internal sealed class SchemeHeader : PreparedHeaderName 7 | { 8 | public SchemeHeader() 9 | : base(":scheme", http2StaticIndex: 6) 10 | { 11 | Http = new PreparedHeader(this, "http", http2StaticIndex: 6); 12 | Https = new PreparedHeader(this, "https", http2StaticIndex: 7); 13 | } 14 | 15 | public PreparedHeader Http { get; } 16 | public PreparedHeader Https { get; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /NetworkToolkit/Http/Headers/StatusHeader.cs: -------------------------------------------------------------------------------- 1 | namespace NetworkToolkit.Http.Headers 2 | { 3 | /// 4 | /// The :status pseudo-header. 5 | /// 6 | internal sealed class StatusHeader : PreparedHeaderName 7 | { 8 | public StatusHeader() 9 | : base(":status", http2StaticIndex: 8) 10 | { 11 | OK = new PreparedHeader(this, "200", http2StaticIndex: 8); 12 | NoContent = new PreparedHeader(this, "204", http2StaticIndex: 9); 13 | PartialContent = new PreparedHeader(this, "206", http2StaticIndex: 10); 14 | NotModified = new PreparedHeader(this, "304", http2StaticIndex: 11); 15 | BadRequest = new PreparedHeader(this, "400", http2StaticIndex: 12); 16 | NotFound = new PreparedHeader(this, "404", http2StaticIndex: 13); 17 | InternalServerError = new PreparedHeader(this, "500", http2StaticIndex: 14); 18 | } 19 | 20 | public PreparedHeader OK { get; } 21 | public PreparedHeader NoContent { get; } 22 | public PreparedHeader PartialContent { get; } 23 | public PreparedHeader NotModified { get; } 24 | public PreparedHeader BadRequest { get; } 25 | public PreparedHeader NotFound { get; } 26 | public PreparedHeader InternalServerError { get; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /NetworkToolkit/Http/PreparedHeader.cs: -------------------------------------------------------------------------------- 1 | using NetworkToolkit.Http.Primitives; 2 | using System; 3 | using System.Text; 4 | 5 | namespace NetworkToolkit.Http 6 | { 7 | /// 8 | /// A prepared header, name and value. 9 | /// 10 | public sealed class PreparedHeader 11 | { 12 | internal readonly PreparedHeaderName _name; 13 | internal readonly byte[] 14 | _value, 15 | _http1Encoded, 16 | _http2Encoded; 17 | internal readonly uint _http2StaticIndex; 18 | 19 | /// 20 | /// The name of the header. 21 | /// 22 | public string Name => _name.Name; 23 | 24 | /// 25 | /// The value of the header. 26 | /// 27 | public string Value { get; } 28 | 29 | private PreparedHeader(PreparedHeaderName name, string value, byte[] valueEncoded, uint http2StaticIndex) 30 | { 31 | Value = value; 32 | _name = name; 33 | _value = valueEncoded; 34 | 35 | int http1EncodedLen = Http1Connection.GetEncodeHeaderLength(name._http1Encoded, valueEncoded); 36 | _http1Encoded = new byte[http1EncodedLen]; 37 | Http1Connection.EncodeHeader(name._http1Encoded, valueEncoded, _http1Encoded); 38 | 39 | _http2Encoded = 40 | http2StaticIndex != 0 ? HPack.EncodeIndexedHeader(http2StaticIndex) : 41 | name._http2StaticIndex != 0 ? HPack.EncodeHeaderWithoutIndexing(name._http2StaticIndex, valueEncoded) : 42 | HPack.EncodeHeaderWithoutIndexing(name._http2Encoded, valueEncoded); 43 | 44 | _http2StaticIndex = http2StaticIndex; 45 | } 46 | 47 | internal PreparedHeader(PreparedHeaderName name, string value, uint http2StaticIndex = 0) 48 | : this(name, value, Encoding.ASCII.GetBytes(value), http2StaticIndex) 49 | { 50 | } 51 | 52 | /// 53 | /// Instantiates a new . 54 | /// 55 | /// The of the header. 56 | /// The value of the header. This value will be ASCII-encoded. 57 | public PreparedHeader(PreparedHeaderName name, string value) 58 | : this(name, value, 0) 59 | { 60 | } 61 | 62 | /// 63 | /// Instantiates a new . 64 | /// 65 | /// The of the header. 66 | /// The value of the header. 67 | public PreparedHeader(PreparedHeaderName name, ReadOnlySpan value) 68 | : this(name, Encoding.ASCII.GetString(value), value.ToArray(), 0) 69 | { 70 | } 71 | 72 | /// 73 | /// Instantiates a new . 74 | /// 75 | /// The name of the header. 76 | /// The value of the header. This value will be ASCII-encoded. 77 | public PreparedHeader(string name, string value) 78 | : this(new PreparedHeaderName(name), value, 0) 79 | { 80 | } 81 | 82 | /// 83 | /// Instantiates a new . 84 | /// 85 | /// The name of the header. 86 | /// The value of the header. 87 | public PreparedHeader(string name, ReadOnlySpan value) 88 | : this(new PreparedHeaderName(name), Encoding.ASCII.GetString(value), value.ToArray(), 0) 89 | { 90 | } 91 | 92 | /// 93 | public override string ToString() => 94 | Name + ": " + Value; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /NetworkToolkit/Http/PreparedHeaderSet.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Text; 6 | using System.Threading; 7 | 8 | namespace NetworkToolkit.Http 9 | { 10 | /// 11 | /// A set of prepared headers, used to more efficiently write frequently reused headers. 12 | /// 13 | public sealed class PreparedHeaderSet : IEnumerable 14 | { 15 | private readonly List _headers = new List(); 16 | private byte[]? _http1Value, _http2Value; 17 | 18 | internal byte[] Http1Value => _http1Value ?? GetHttp1ValueSlow(); 19 | internal byte[] Http2Value => _http2Value ?? GetHttp2ValueSlow(); 20 | 21 | /// 22 | /// Adds a header to the 23 | /// 24 | /// The header to add. 25 | public void Add(PreparedHeader header) 26 | { 27 | lock (_headers) 28 | { 29 | if (_http1Value is null) 30 | { 31 | _headers.Add(header); 32 | return; 33 | } 34 | } 35 | 36 | throw new Exception($"Unable to add to {nameof(PreparedHeaderSet)} after it has been used."); 37 | } 38 | 39 | /// 40 | /// Adds a header to the 41 | /// 42 | /// The name of the header to add. 43 | /// The value of the header to add. The value will be ASCII-encoded. 44 | public void Add(PreparedHeaderName name, string value) => 45 | Add(new PreparedHeader(name, value)); 46 | 47 | /// 48 | /// Adds a header to the 49 | /// 50 | /// The name of the header to add. 51 | /// The value of the header to add. 52 | public void Add(PreparedHeaderName name, ReadOnlySpan value) => 53 | Add(new PreparedHeader(name, value)); 54 | 55 | /// 56 | /// Adds a header to the 57 | /// 58 | /// The name of the header to add. 59 | /// The value of the header to add. The value will be ASCII-encoded. 60 | public void Add(string name, string value) => 61 | Add(new PreparedHeader(name, value)); 62 | 63 | /// 64 | /// Adds a header to the 65 | /// 66 | /// The name of the header to add. 67 | /// The value of the header to add. 68 | public void Add(string name, ReadOnlySpan value) => 69 | Add(new PreparedHeader(name, value)); 70 | 71 | private byte[] GetHttp1ValueSlow() 72 | { 73 | lock (_headers) 74 | { 75 | if (_http1Value is null) 76 | { 77 | GetValuesSlow(); 78 | } 79 | 80 | return _http1Value!; 81 | } 82 | } 83 | 84 | private byte[] GetHttp2ValueSlow() 85 | { 86 | lock (_headers) 87 | { 88 | if (_http2Value is null) 89 | { 90 | GetValuesSlow(); 91 | } 92 | 93 | return _http2Value!; 94 | } 95 | } 96 | 97 | private void GetValuesSlow() 98 | { 99 | Debug.Assert(Monitor.IsEntered(_headers)); 100 | 101 | int totalHttp1Len = 0; 102 | int totalHttp2Len = 0; 103 | 104 | foreach (PreparedHeader header in _headers) 105 | { 106 | checked 107 | { 108 | totalHttp1Len += header._http1Encoded.Length; 109 | totalHttp2Len += header._http2Encoded.Length; 110 | } 111 | } 112 | 113 | byte[] http1Value = new byte[totalHttp1Len]; 114 | byte[] http2Value = new byte[totalHttp2Len]; 115 | 116 | Span write1Pos = http1Value; 117 | Span write2Pos = http2Value; 118 | 119 | foreach (PreparedHeader header in _headers) 120 | { 121 | header._http1Encoded.AsSpan().CopyTo(write1Pos); 122 | write1Pos = write1Pos[header._http1Encoded.Length..]; 123 | 124 | header._http2Encoded.AsSpan().CopyTo(write2Pos); 125 | write2Pos = write2Pos[header._http2Encoded.Length..]; 126 | } 127 | 128 | Volatile.Write(ref _http1Value, http1Value); 129 | Volatile.Write(ref _http2Value, http2Value); 130 | } 131 | 132 | /// 133 | public override string ToString() => Encoding.ASCII.GetString(Http1Value); 134 | 135 | /// 136 | public IEnumerator GetEnumerator() 137 | { 138 | if (_http1Value is null) 139 | { 140 | // ensure _headers is frozen. 141 | GetValuesSlow(); 142 | } 143 | 144 | return _headers.GetEnumerator(); 145 | } 146 | 147 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /NetworkToolkit/Http/PrimitiveHttpContentStream.cs: -------------------------------------------------------------------------------- 1 | using NetworkToolkit.Http.Primitives; 2 | using System; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace NetworkToolkit.Http 7 | { 8 | internal sealed class PrimitiveHttpContentStream : HttpContentStream 9 | { 10 | public PrimitiveHttpResponseMessage? ResponseMessage { get; set; } 11 | 12 | public PrimitiveHttpContentStream(ValueHttpRequest request) : base(request, ownsRequest: true) 13 | { 14 | } 15 | 16 | public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) 17 | { 18 | int len = await base.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); 19 | 20 | if (len == 0 && ResponseMessage is PrimitiveHttpResponseMessage response) 21 | { 22 | if (await _request.ReadToTrailingHeadersAsync(cancellationToken).ConfigureAwait(false)) 23 | { 24 | await _request.ReadHeadersAsync(response, PrimitiveHttpResponseMessage.TrailingHeadersSinkState, cancellationToken).ConfigureAwait(false); 25 | } 26 | 27 | ResponseMessage = null; 28 | await _request.DisposeAsync(cancellationToken).ConfigureAwait(false); 29 | } 30 | 31 | return len; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /NetworkToolkit/Http/PrimitiveHttpMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using NetworkToolkit.Http.Primitives; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Net.Http; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace NetworkToolkit.Http 9 | { 10 | /// 11 | /// A that operates over . 12 | /// 13 | public sealed class PrimitiveHttpMessageHandler : HttpMessageHandler 14 | { 15 | /// 16 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 17 | { 18 | throw new NotImplementedException(); 19 | } 20 | 21 | private async Task SendAsync(HttpConnection connection, HttpRequestMessage request, CancellationToken cancellationToken) 22 | { 23 | HttpPrimitiveVersion requestVersion = request.Version switch 24 | { 25 | { MajorRevision: 1, MinorRevision: 0 } => HttpPrimitiveVersion.Version10, 26 | { MajorRevision: 1 } => HttpPrimitiveVersion.Version11, 27 | _ => throw new ArgumentException($"Unknown HTTP version {request.Version}") 28 | }; 29 | 30 | if (request.RequestUri == null) 31 | { 32 | throw new ArgumentException($"{nameof(request)}.{nameof(request.RequestUri)} must not be null."); 33 | } 34 | 35 | ValueHttpRequest httpRequest = (await connection.CreateNewRequestAsync(requestVersion, request.VersionPolicy, cancellationToken).ConfigureAwait(false)) 36 | ?? throw new HttpRequestException($"{nameof(HttpConnection)} used by {nameof(PrimitiveHttpMessageHandler)} has been closed by peer."); 37 | 38 | try 39 | { 40 | long? contentLength = 41 | request.Content == null ? 0L : 42 | request.Content.Headers.ContentLength is long contentLengthHeader ? contentLengthHeader : 43 | null; 44 | 45 | // HttpRequestMessage has no support for sending trailing headers. 46 | httpRequest.ConfigureRequest(contentLength, hasTrailingHeaders: false); 47 | httpRequest.WriteRequest(request.Method, request.RequestUri); 48 | 49 | foreach (KeyValuePair> header in request.Headers) 50 | { 51 | httpRequest.WriteHeader(header.Key, header.Value, ";"); 52 | } 53 | 54 | PrimitiveHttpContentStream stream = new PrimitiveHttpContentStream(httpRequest); 55 | 56 | if (request.Content != null) 57 | { 58 | await request.Content.CopyToAsync(stream, cancellationToken).ConfigureAwait(false); 59 | } 60 | 61 | await httpRequest.CompleteRequestAsync(cancellationToken).ConfigureAwait(false); 62 | 63 | if (!await httpRequest.ReadToFinalResponseAsync(cancellationToken).ConfigureAwait(false)) 64 | { 65 | throw new HttpRequestException("Unexpected end of stream before response received."); 66 | } 67 | 68 | var responseContent = new StreamContent(stream); 69 | 70 | var response = new PrimitiveHttpResponseMessage 71 | { 72 | StatusCode = httpRequest.StatusCode, 73 | Version = httpRequest.Version!, 74 | Content = responseContent 75 | }; 76 | 77 | stream.ResponseMessage = response; 78 | 79 | if (await httpRequest.ReadToHeadersAsync(cancellationToken).ConfigureAwait(false)) 80 | { 81 | await httpRequest.ReadHeadersAsync(response, state: null, cancellationToken).ConfigureAwait(false); 82 | } 83 | 84 | return response; 85 | } 86 | catch 87 | { 88 | await httpRequest.DisposeAsync(cancellationToken).ConfigureAwait(false); 89 | throw; 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /NetworkToolkit/Http/PrimitiveHttpResponseMessage.cs: -------------------------------------------------------------------------------- 1 | using NetworkToolkit.Http.Primitives; 2 | using System; 3 | using System.Net.Http; 4 | using System.Text; 5 | 6 | namespace NetworkToolkit.Http 7 | { 8 | internal sealed class PrimitiveHttpResponseMessage : HttpResponseMessage, IHttpHeadersSink 9 | { 10 | public static readonly object TrailingHeadersSinkState = new object(); 11 | 12 | public void OnHeader(object? state, ReadOnlySpan headerName, ReadOnlySpan headerValue) 13 | { 14 | string headerNameString = Encoding.ASCII.GetString(headerName); 15 | string headerValueString = Encoding.ASCII.GetString(headerValue); 16 | 17 | if (state != TrailingHeaders) 18 | { 19 | if (!Headers.TryAddWithoutValidation(headerNameString, headerValueString)) 20 | { 21 | Content.Headers.TryAddWithoutValidation(headerNameString, headerValueString); 22 | } 23 | } 24 | else 25 | { 26 | TrailingHeaders.TryAddWithoutValidation(headerNameString, headerValueString); 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /NetworkToolkit/Http/Primitives/HPackDecoder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Numerics; 3 | 4 | namespace NetworkToolkit.Http.Primitives 5 | { 6 | internal static class HPackDecoder 7 | { 8 | const int RfcEntrySizeAdjust = 32; 9 | 10 | public static int Decode(ReadOnlySpan buffer, IHttpHeadersSink sink, object? state) 11 | { 12 | int originalLength = buffer.Length; 13 | 14 | while (buffer.Length != 0) 15 | { 16 | HttpHeaderFlags flags; 17 | ulong nameIndex; 18 | int headerLength; 19 | byte prefixMask; 20 | 21 | byte firstByte = buffer[0]; 22 | 23 | switch (BitOperations.LeadingZeroCount(firstByte) - 24) 24 | { 25 | case 0: 26 | if (HPack.TryDecodeIndexedHeader(firstByte, buffer, out nameIndex, out headerLength)) 27 | { 28 | buffer = buffer.Slice(headerLength); 29 | OnHeader(sink, state, nameIndex); 30 | continue; 31 | } 32 | else 33 | { 34 | return originalLength - buffer.Length; 35 | } 36 | case 1: 37 | prefixMask = HPack.IncrementalIndexingMask; 38 | flags = HttpHeaderFlags.None; 39 | break; 40 | case 2: // Dynamic table size update. 41 | if (HPack.TryDecodeDynamicTableSizeUpdate(firstByte, buffer, out nameIndex, out headerLength)) 42 | { 43 | buffer = buffer.Slice(headerLength); 44 | OnDynamicTableSizeUpdate(nameIndex); 45 | continue; 46 | } 47 | else 48 | { 49 | return originalLength - buffer.Length; 50 | } 51 | case 3: // Literal header never indexed. 52 | prefixMask = HPack.WithoutIndexingOrNeverIndexMask; 53 | flags = HttpHeaderFlags.NeverCompressed; 54 | break; 55 | default: // Literal header without indexing. 56 | prefixMask = HPack.WithoutIndexingOrNeverIndexMask; 57 | flags = HttpHeaderFlags.None; 58 | break; 59 | } 60 | 61 | if (!HPack.TryDecodeHeader(prefixMask, flags, firstByte, buffer, out nameIndex, out ReadOnlySpan name, out ReadOnlySpan value, out flags, out headerLength)) 62 | { 63 | return originalLength - buffer.Length; 64 | } 65 | 66 | buffer = buffer.Slice(headerLength); 67 | 68 | if (nameIndex != 0) 69 | { 70 | OnHeader(sink, state, nameIndex, value, flags); 71 | } 72 | else 73 | { 74 | OnHeader(sink, state, name, value, flags); 75 | } 76 | } 77 | 78 | return originalLength - buffer.Length; 79 | } 80 | 81 | private static void OnDynamicTableSizeUpdate(ulong newSize) 82 | { 83 | throw new Exception("Dynamic table update not supported."); 84 | } 85 | 86 | private static void OnHeader(IHttpHeadersSink sink, object? state, ulong headerIndex) 87 | { 88 | PreparedHeader v = GetHeaderForIndex(headerIndex); 89 | OnHeader(sink, state, v._name._http2Encoded, v._value, HttpHeaderFlags.None); 90 | } 91 | 92 | private static void OnHeader(IHttpHeadersSink sink, object? state, ulong headerNameIndex, ReadOnlySpan headerValue, HttpHeaderFlags flags) 93 | { 94 | PreparedHeader v = GetHeaderForIndex(headerNameIndex); 95 | OnHeader(sink, state, v._name._http2Encoded, headerValue, flags); 96 | } 97 | 98 | private static void OnHeader(IHttpHeadersSink sink, object? state, ReadOnlySpan headerName, ReadOnlySpan headerValue, HttpHeaderFlags flags) 99 | { 100 | if (headerName.Length > 1 && headerName[0] == ':') 101 | { 102 | // TODO: check for pseudo headers. 103 | } 104 | 105 | sink.OnHeader(state, headerName, headerValue, flags); 106 | } 107 | 108 | private static PreparedHeader GetHeaderForIndex(ulong headerIndex) 109 | { 110 | if (headerIndex is > 0 and <= HPack.StaticTableMaxIndex) 111 | { 112 | return HPack.GetStaticHeader((uint)headerIndex); 113 | } 114 | 115 | throw new Exception($"Invalid header index {headerIndex}"); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /NetworkToolkit/Http/Primitives/HPackDynamicTable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NetworkToolkit.Http.Primitives 4 | { 5 | class HPackDynamicTable 6 | { 7 | public PreparedHeader Get(ulong headerIndex) 8 | { 9 | throw new NotImplementedException(); 10 | } 11 | 12 | public PreparedHeader Push(ulong nameIndex, ReadOnlySpan value) 13 | { 14 | throw new NotImplementedException(); 15 | } 16 | 17 | public PreparedHeader Push(ReadOnlySpan name, ReadOnlySpan value) 18 | { 19 | throw new NotImplementedException(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /NetworkToolkit/Http/Primitives/Http2Frame.cs: -------------------------------------------------------------------------------- 1 | using NetworkToolkit.Parsing; 2 | using System; 3 | using System.Buffers.Binary; 4 | using System.Diagnostics; 5 | using System.Runtime.CompilerServices; 6 | 7 | namespace NetworkToolkit.Http.Primitives 8 | { 9 | internal static class Http2Frame 10 | { 11 | public const byte DataFrame = 0; 12 | public const byte HeadersFrame = 1; 13 | public const byte PriorityFrame = 2; 14 | public const byte ResetStreamFrame = 3; 15 | public const byte SettingsFrame = 4; 16 | public const byte PushPromiseFrame = 5; 17 | public const byte PingFrame = 6; 18 | public const byte GoAwayFrame = 7; 19 | public const byte WindowUpdateFrame = 8; 20 | public const byte ContinuationFrame = 9; 21 | 22 | public const int FrameHeaderLength = 9; 23 | 24 | public const int DataFrameHeaderLength = 10; 25 | public const int HeadersFrameHeaderLength = 16; 26 | public const int RstStreamFrameLength = 13; 27 | public const int InitialSettingsFrameLength = 33; 28 | public const int PingFrameLength = 17; 29 | public const int GoAwayFrameHeaderLength = 17; 30 | public const int WindowUpdateFrameLength = 13; 31 | 32 | public const int SettingLength = 6; 33 | 34 | public static void EncodeDataFrameHeader(uint payloadLength, Http2DataFrameFlags flags, uint streamId, Span buffer) 35 | { 36 | Debug.Assert(payloadLength <= 0xFFFFFF); 37 | Debug.Assert(!flags.HasFlag(Http2DataFrameFlags.Padded)); 38 | Debug.Assert(streamId < 0x80000000); 39 | Debug.Assert(buffer.Length >= DataFrameHeaderLength); 40 | 41 | buffer[0] = (byte)(payloadLength >> 16); 42 | buffer[1] = (byte)(payloadLength >> 8); 43 | buffer[2] = (byte)payloadLength; 44 | buffer[3] = 0x0; // DATA frame. 45 | buffer[4] = (byte)flags; 46 | BinaryPrimitives.WriteUInt32BigEndian(buffer[5..], streamId); 47 | buffer[9] = 0; // pad length. 48 | } 49 | 50 | public static void EncodeHeadersFrameHeader(uint payloadLength, Http2HeadersFrameFlags flags, uint streamId, Span buffer) 51 | { 52 | Debug.Assert(payloadLength <= 0xFFFFFF); 53 | Debug.Assert(!flags.HasFlag(Http2HeadersFrameFlags.Padded)); 54 | Debug.Assert(streamId < 0x80000000); 55 | Debug.Assert(buffer.Length >= HeadersFrameHeaderLength); 56 | 57 | buffer[0] = (byte)(payloadLength >> 16); 58 | buffer[1] = (byte)(payloadLength >> 8); 59 | buffer[2] = (byte)payloadLength; 60 | buffer[3] = 0x1; // HEADERS frame. 61 | buffer[4] = (byte)flags; 62 | buffer[5] = 0; // pad length. 63 | BinaryPrimitives.WriteUInt32BigEndian(buffer[6..], streamId); 64 | BitConverter.TryWriteBytes(buffer[10..], (uint)0); // pad length, stream dependency ABC 65 | BitConverter.TryWriteBytes(buffer[14..], (ushort)0); // stream dependency D, Weight 66 | } 67 | 68 | public static void EncodeRstStreamFrame(uint streamId, uint errorCode, Span buffer) 69 | { 70 | Debug.Assert(streamId < 0x80000000); 71 | Debug.Assert(buffer.Length >= RstStreamFrameLength); 72 | 73 | buffer[0] = 0; // payloadLength A 74 | buffer[1] = 0; // payloadLength B 75 | buffer[2] = 0; // payloadLength C 76 | buffer[3] = 0x3; // RST_STREAM frame. 77 | buffer[4] = 0; // flags 78 | BinaryPrimitives.WriteUInt32BigEndian(buffer[5..], streamId); 79 | BinaryPrimitives.WriteUInt32BigEndian(buffer[9..], errorCode); 80 | BitConverter.TryWriteBytes(buffer[14..], (ushort)0); // stream dependency D, Weight 81 | } 82 | 83 | public static void EncodeInitialSettingsFrame(uint headerTableSize, uint maxFrameSize, uint maxHeaderListSize, Span buffer) 84 | { 85 | Debug.Assert(maxFrameSize < (1 << 24)); 86 | Debug.Assert(buffer.Length >= InitialSettingsFrameLength); 87 | 88 | BinaryPrimitives.WriteUInt32BigEndian(buffer, 0x00001804); // payloadLength ABC, SETTINGS frame 89 | BitConverter.TryWriteBytes(buffer[4..], (ushort)0); // flags, streamId ABC 90 | buffer[8] = 0; // streamId D 91 | BinaryPrimitives.TryWriteUInt16BigEndian(buffer[9..], 0x1); // SETTINGS_HEADER_TABLE_SIZE 92 | BinaryPrimitives.TryWriteUInt32BigEndian(buffer[11..], headerTableSize); 93 | BinaryPrimitives.TryWriteUInt32BigEndian(buffer[15..], 0x00020000); // SETTINGS_ENABLE_PUSH, 0 AB 94 | BinaryPrimitives.TryWriteUInt32BigEndian(buffer[19..], 0x00000005); // 0 CD, SETTINGS_FRAME_MAX_SIZE 95 | BinaryPrimitives.TryWriteUInt32BigEndian(buffer[23..], maxFrameSize); 96 | BinaryPrimitives.TryWriteUInt16BigEndian(buffer[27..], 0x6); // SETTINGS_MAX_HEADER_LIST_SIZE 97 | BinaryPrimitives.TryWriteUInt32BigEndian(buffer[29..], maxHeaderListSize); 98 | } 99 | 100 | public static void EncodeSettingsAckFrame(Span buffer) 101 | { 102 | //Debug.Assert(buffer.Length >= SettingsAckFrameLength); 103 | 104 | BinaryPrimitives.WriteUInt32BigEndian(buffer, 0x00000004); // payloadLength ABC, SETTINGS frame 105 | BinaryPrimitives.WriteUInt32BigEndian(buffer[4..], 0x01000000); // flags, streamId ABC 106 | buffer[8] = 0; // streamId D 107 | } 108 | 109 | public static void EncodePingAckFrame(ulong pingData, Span buffer) 110 | { 111 | Debug.Assert(buffer.Length >= PingFrameLength); 112 | 113 | BinaryPrimitives.WriteUInt32BigEndian(buffer, 0x00000806); // payloadLength ABC, PING frame 114 | BinaryPrimitives.WriteUInt32BigEndian(buffer[4..], 0x01000000); // flags, streamId ABC 115 | buffer[8] = 0; // streamId D 116 | BitConverter.TryWriteBytes(buffer[9..], pingData); 117 | } 118 | 119 | public static void EncodeWindowUpdateFrame(uint windowSizeIncrement, uint streamId, Span buffer) 120 | { 121 | Debug.Assert(windowSizeIncrement > 0); 122 | Debug.Assert(windowSizeIncrement < 0x80000000); 123 | Debug.Assert(streamId < 0x80000000); 124 | Debug.Assert(buffer.Length >= WindowUpdateFrameLength); 125 | 126 | BinaryPrimitives.WriteUInt32BigEndian(buffer, 0x00000408); // payloadLength ABC, WINDOW_UPDATE frame 127 | buffer[4] = 0x00; 128 | BinaryPrimitives.WriteUInt32BigEndian(buffer[5..], streamId); 129 | BinaryPrimitives.WriteUInt32BigEndian(buffer[9..], windowSizeIncrement); 130 | } 131 | } 132 | 133 | internal enum Http2DataFrameFlags : byte 134 | { 135 | EndStream = 0x1, 136 | Padded = 0x8 137 | } 138 | 139 | internal enum Http2HeadersFrameFlags : byte 140 | { 141 | EndStream = 0x1, 142 | EndHeaders = 0x4, 143 | Padded = 0x8, 144 | Priority = 0x20 145 | } 146 | 147 | internal enum Http2ContinuationFrameFlags : byte 148 | { 149 | EndHeaders = 0x4 150 | } 151 | 152 | internal enum Http2SettingsFrameFlags : byte 153 | { 154 | Ack = 0x1 155 | } 156 | 157 | internal enum Http2PingFrameFlags : byte 158 | { 159 | Ack = 0x1 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /NetworkToolkit/Http/Primitives/Http2Request.cs: -------------------------------------------------------------------------------- 1 | using NetworkToolkit.Parsing; 2 | using System; 3 | using System.Collections.Concurrent; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using System.Runtime.CompilerServices; 7 | using System.Runtime.InteropServices; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace NetworkToolkit.Http.Primitives 12 | { 13 | internal sealed class Http2Request : HttpRequest 14 | { 15 | private readonly ConcurrentQueue _frames = new ConcurrentQueue(); 16 | private ReadState _state; 17 | private int _processing; 18 | 19 | public void OnStatus(int statusCode) 20 | { 21 | } 22 | 23 | public void OnHeaders(bool endHeaders, bool endStream, CountedSegment segment) 24 | { 25 | segment = segment.Slice(); 26 | 27 | int frameData = 28 | Frame.FrameTypeHeaders 29 | | (endHeaders ? Frame.EndHeaders : 0) 30 | | (endStream ? Frame.EndStream : 0); 31 | 32 | AddNewFrame(new Frame(frameData, errorCode: 0u, segment)); 33 | } 34 | 35 | public void OnData(bool endStream, CountedSegment segment) 36 | { 37 | segment = segment.Slice(); 38 | 39 | int frameData = 40 | Frame.FrameTypeData 41 | | (endStream ? Frame.EndStream : 0); 42 | 43 | AddNewFrame(new Frame(frameData, errorCode: 0u, segment)); 44 | } 45 | 46 | public void OnRstStream(uint errorCode) 47 | { 48 | AddNewFrame(new Frame(Frame.FrameTypeRstStream, errorCode, segment: default)); 49 | } 50 | 51 | public void OnGoAway(uint errorCode) 52 | { 53 | AddNewFrame(new Frame(Frame.FrameTypeGoAway, errorCode, segment: default)); 54 | } 55 | 56 | private void AddNewFrame(in Frame frame) 57 | { 58 | _frames.Enqueue(frame); 59 | 60 | if (Interlocked.Exchange(ref _processing, 1) == 0) 61 | { 62 | ProcessFrames(); 63 | } 64 | } 65 | 66 | private void ProcessFrames() 67 | { 68 | try 69 | { 70 | do 71 | { 72 | while (_frames.TryDequeue(out Frame result)) 73 | { 74 | ProcessFrame(result); 75 | } 76 | 77 | Volatile.Write(ref _processing, 0); 78 | } 79 | while (!_frames.IsEmpty && Interlocked.Exchange(ref _processing, 1) == 0); 80 | } 81 | catch (Exception ex) 82 | { 83 | // TODO: set connection exception. 84 | } 85 | } 86 | 87 | private void ProcessFrame(in Frame frame) 88 | { 89 | } 90 | 91 | protected internal override ValueTask DisposeAsync(int version, CancellationToken cancellationToken) 92 | { 93 | throw new NotImplementedException(); 94 | } 95 | 96 | protected internal override void ConfigureRequest(int version, long? contentLength, bool hasTrailingHeaders) 97 | { 98 | throw new NotImplementedException(); 99 | } 100 | 101 | protected internal override void WriteConnectRequest(int version, ReadOnlySpan authority) 102 | { 103 | throw new NotImplementedException(); 104 | } 105 | 106 | protected internal override void WriteRequest(int version, ReadOnlySpan method, ReadOnlySpan authority, ReadOnlySpan pathAndQuery) 107 | { 108 | throw new NotImplementedException(); 109 | } 110 | 111 | protected internal override void WriteHeader(int version, ReadOnlySpan name, ReadOnlySpan value) 112 | { 113 | throw new NotImplementedException(); 114 | } 115 | 116 | protected internal override void WriteHeader(int version, PreparedHeaderSet headers) 117 | { 118 | throw new NotImplementedException(); 119 | } 120 | 121 | protected internal override void WriteTrailingHeader(int version, ReadOnlySpan name, ReadOnlySpan value) 122 | { 123 | throw new NotImplementedException(); 124 | } 125 | 126 | protected internal override ValueTask FlushHeadersAsync(int version, CancellationToken cancellationToken) 127 | { 128 | throw new NotImplementedException(); 129 | } 130 | 131 | protected internal override ValueTask WriteContentAsync(int version, ReadOnlyMemory buffer, CancellationToken cancellationToken) 132 | { 133 | throw new NotImplementedException(); 134 | } 135 | 136 | protected internal override ValueTask WriteContentAsync(int version, IReadOnlyList> buffers, CancellationToken cancellationToken) 137 | { 138 | throw new NotImplementedException(); 139 | } 140 | 141 | protected internal override ValueTask FlushContentAsync(int version, CancellationToken cancellationToken) 142 | { 143 | throw new NotImplementedException(); 144 | } 145 | 146 | protected internal override ValueTask CompleteRequestAsync(int version, CancellationToken cancellationToken) 147 | { 148 | throw new NotImplementedException(); 149 | } 150 | 151 | protected internal override ValueTask ReadAsync(int version, CancellationToken cancellationToken) 152 | { 153 | throw new NotImplementedException(); 154 | } 155 | 156 | protected internal override ValueTask ReadHeadersAsync(int version, IHttpHeadersSink headersSink, object? state, CancellationToken cancellationToken) 157 | { 158 | throw new NotImplementedException(); 159 | } 160 | 161 | protected internal override ValueTask ReadContentAsync(int version, Memory buffer, CancellationToken cancellationToken) 162 | { 163 | throw new NotImplementedException(); 164 | } 165 | 166 | protected internal override ValueTask ReadContentAsync(int version, IReadOnlyList> buffers, CancellationToken cancellationToken) 167 | { 168 | throw new NotImplementedException(); 169 | } 170 | 171 | private readonly struct Frame 172 | { 173 | public const int FrameTypeMask = 3; 174 | public const int FrameTypeHeaders = 0; 175 | public const int FrameTypeData = 1; 176 | public const int FrameTypeRstStream = 2; 177 | public const int FrameTypeGoAway = 3; 178 | 179 | public const int EndHeaders = 4; 180 | public const int EndStream = 8; 181 | 182 | public CountedSegment Segment { get; } 183 | public int Data { get; } 184 | public uint ErrorCode { get; } 185 | 186 | public Frame(int data, uint errorCode, CountedSegment segment) 187 | { 188 | Data = data; 189 | ErrorCode = errorCode; 190 | Segment = segment; 191 | } 192 | } 193 | 194 | private enum ReadState 195 | { 196 | None, 197 | ExpectingPseudoHeaders, 198 | ExpectingHeaders, 199 | ExpectingData, 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /NetworkToolkit/Http/Primitives/HttpBaseConnection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace NetworkToolkit.Http.Primitives 7 | { 8 | /// 9 | /// A base connection used for provided implementations. 10 | /// 11 | public abstract class HttpBaseConnection : HttpConnection 12 | { 13 | private long _creationTicks, _lastUsedTicks; 14 | 15 | internal HttpBaseConnection() 16 | { 17 | long curTicks = Environment.TickCount64; 18 | _creationTicks = curTicks; 19 | _lastUsedTicks = curTicks; 20 | } 21 | 22 | internal bool IsExpired(long curTicks, TimeSpan lifetimeLimit, TimeSpan idleLimit) 23 | { 24 | return Tools.TimeoutExpired(curTicks, _creationTicks, lifetimeLimit) 25 | || Tools.TimeoutExpired(curTicks, _lastUsedTicks, idleLimit); 26 | } 27 | 28 | /// 29 | /// Refreshes the last used time of the connection. 30 | /// 31 | /// The number of ticks to set the connection's last used time to. 32 | protected void RefreshLastUsed(long curTicks) 33 | { 34 | _lastUsedTicks = curTicks; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /NetworkToolkit/Http/Primitives/HttpConnection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace NetworkToolkit.Http.Primitives 7 | { 8 | /// 9 | /// A HTTP connection. 10 | /// 11 | public abstract class HttpConnection : ICancellableAsyncDisposable 12 | { 13 | /// 14 | /// The current status of the connection. 15 | /// 16 | /// 17 | /// This should not be relied on to assume 18 | /// 19 | /// will succeed. The connection may be closed between checking status and creating a request. 20 | /// 21 | public abstract HttpConnectionStatus Status { get; } 22 | 23 | /// 24 | /// Opens a new request on the connection. 25 | /// 26 | /// The HTTP version of the request to make. 27 | /// A policy controlling version selection for the request. 28 | /// A cancellation token for this operation. 29 | /// 30 | /// If a request can be made, a instance used to make a single request. 31 | /// Otherwise, null to indicate the connection is not accepting new requests. 32 | /// 33 | /// 34 | /// This should return null if the connection has been gracefully closed e.g. connection reset, received GOAWAY, etc. 35 | /// 36 | public abstract ValueTask CreateNewRequestAsync(HttpPrimitiveVersion version, HttpVersionPolicy versionPolicy, CancellationToken cancellationToken = default); 37 | 38 | /// 39 | public ValueTask DisposeAsync() 40 | { 41 | return DisposeAsync(CancellationToken.None); 42 | } 43 | 44 | /// 45 | public abstract ValueTask DisposeAsync(CancellationToken cancellationToken); 46 | 47 | /// 48 | /// Prunes any resources that have expired past a certain age. 49 | /// 50 | public abstract ValueTask PrunePoolsAsync(long curTicks, TimeSpan lifetimeLimit, TimeSpan idleLimit, CancellationToken cancellationToken = default); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /NetworkToolkit/Http/Primitives/HttpConnectionStatus.cs: -------------------------------------------------------------------------------- 1 | namespace NetworkToolkit.Http.Primitives 2 | { 3 | /// 4 | /// The current status of an . 5 | /// 6 | public enum HttpConnectionStatus 7 | { 8 | /// 9 | /// The is open and accepting requests. 10 | /// 11 | Open, 12 | /// 13 | /// The is open, but might reject requests. 14 | /// may return null. 15 | /// 16 | Closing, 17 | /// 18 | /// The has been closed and is no longer accepting requests. 19 | /// will return null. 20 | /// 21 | Closed 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /NetworkToolkit/Http/Primitives/HttpHeaderFlags.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NetworkToolkit.Http.Primitives 4 | { 5 | /// 6 | /// Defines flags indicating properties of a header. 7 | /// 8 | [Flags] 9 | public enum HttpHeaderFlags 10 | { 11 | /// 12 | /// No flags set. 13 | /// 14 | None = 0, 15 | 16 | /// 17 | /// The header's name is huffman coded. 18 | /// 19 | /// 20 | /// TODO: make this HTTP/2 specific? 21 | /// 22 | NameHuffmanCoded = 1, 23 | 24 | /// 25 | /// The header's value is huffman coded. 26 | /// 27 | /// 28 | /// TODO: make this HTTP/2 specific? 29 | /// 30 | ValueHuffmanCoded = 2, 31 | 32 | /// 33 | /// The header must never be compressed. 34 | /// 35 | NeverCompressed = 4 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /NetworkToolkit/Http/Primitives/HttpPrimitiveVersion.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NetworkToolkit.Http.Primitives 4 | { 5 | /// 6 | /// A HTTP version. 7 | /// 8 | public sealed class HttpPrimitiveVersion 9 | { 10 | /// 11 | /// HTTP/1.0 12 | /// 13 | public static HttpPrimitiveVersion Version10 { get; } = new HttpPrimitiveVersion(1, 0, BitConverter.IsLittleEndian ? 0x302E312F50545448UL : 0x485454502F312E30UL); 14 | 15 | /// 16 | /// HTTP/1.1 17 | /// 18 | public static HttpPrimitiveVersion Version11 { get; } = new HttpPrimitiveVersion(1, 1, BitConverter.IsLittleEndian ? 0x312E312F50545448UL : 0x485454502F312E31UL); 19 | 20 | internal readonly ulong _encoded; 21 | 22 | /// 23 | /// The major version. 24 | /// 25 | public int Major { get; } 26 | 27 | /// 28 | /// The minor version. 29 | /// 30 | public int Minor { get; } 31 | 32 | private HttpPrimitiveVersion(int majorVersion, int minorVersion, ulong encoded) 33 | { 34 | Major = majorVersion; 35 | Minor = minorVersion; 36 | _encoded = encoded; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /NetworkToolkit/Http/Primitives/HttpReadType.cs: -------------------------------------------------------------------------------- 1 | namespace NetworkToolkit.Http.Primitives 2 | { 3 | /// 4 | /// Indicates the type of element read from a . 5 | /// 6 | public enum HttpReadType 7 | { 8 | /// 9 | /// Default/uninitialized value. 10 | /// should be called to read the first element. 11 | /// 12 | None, 13 | 14 | /// 15 | /// HTTP response. 16 | /// and are now valid. 17 | /// can be received zero or more times. 18 | /// 19 | InformationalResponse, 20 | 21 | /// 22 | /// HTTP response. 23 | /// and are now valid. 24 | /// will only be returned for a final response, not informational responses. 25 | /// 26 | FinalResponse, 27 | 28 | /// 29 | /// HTTP response headers. 30 | /// should be called to read headers. 31 | /// 32 | Headers, 33 | 34 | /// 35 | /// HTTP response content. 36 | /// should be called, until it returns 0, to read content. 37 | /// can be received more than once and it is possible for other elements to be intermixed between them. 38 | /// 39 | Content, 40 | 41 | /// 42 | /// HTTP trailing headers. 43 | /// should be called to read headers. 44 | /// 45 | TrailingHeaders, 46 | 47 | /// 48 | /// The has been fully ready and may be disposed. 49 | /// 50 | EndOfStream, 51 | 52 | /// 53 | /// The ALTSVC extension frame in HTTP/2. 54 | /// 55 | AltSvc 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /NetworkToolkit/Http/Primitives/IHttpHeadersSink.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NetworkToolkit.Http.Primitives 4 | { 5 | /// 6 | /// A sink used to receive HTTP headers. 7 | /// 8 | public interface IHttpHeadersSink 9 | { 10 | /// 11 | /// Called when a header has been received. 12 | /// 13 | /// User state passed to . 14 | /// The header's name. 15 | /// The header's value. 16 | void OnHeader(object? state, ReadOnlySpan headerName, ReadOnlySpan headerValue); 17 | 18 | /// 19 | /// Called when a header has been received. 20 | /// 21 | /// User state passed to . 22 | /// The header's name. 23 | /// The header's value. 24 | /// Flags for the header. 25 | void OnHeader(object? state, ReadOnlySpan headerName, ReadOnlySpan headerValue, HttpHeaderFlags flags) 26 | { 27 | if (flags.HasFlag(HttpHeaderFlags.NameHuffmanCoded)) 28 | { 29 | // TODO: decode header name. 30 | } 31 | 32 | if (flags.HasFlag(HttpHeaderFlags.ValueHuffmanCoded)) 33 | { 34 | // TODO: decode header value. 35 | } 36 | 37 | OnHeader(state, headerName, headerValue); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /NetworkToolkit/Http/Primitives/SslClientConnectionProperties.cs: -------------------------------------------------------------------------------- 1 | using NetworkToolkit.Connections; 2 | using System; 3 | using System.Net.Security; 4 | 5 | namespace NetworkToolkit.Http.Primitives 6 | { 7 | internal sealed class SslClientConnectionProperties : SslClientAuthenticationOptions, IConnectionProperties 8 | { 9 | public bool TryGetProperty(Type type, out object? value) 10 | { 11 | if (type == typeof(SslClientAuthenticationOptions)) 12 | { 13 | value = this; 14 | return true; 15 | } 16 | 17 | value = null; 18 | return false; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /NetworkToolkit/ICancellableAsyncDisposable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace NetworkToolkit 6 | { 7 | /// 8 | /// An that can be cancelled. 9 | /// 10 | public interface ICancellableAsyncDisposable : IAsyncDisposable 11 | { 12 | /// 13 | /// Disposes of any native resources. 14 | /// 15 | /// 16 | /// A cancellation token for the asynchronous operation. 17 | /// If canceled, the dispose may finish sooner but with only minimal cleanup, i.e. without flushing buffers to disk. 18 | /// 19 | /// A representing the asynchronous operation. 20 | ValueTask DisposeAsync(CancellationToken cancellationToken); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /NetworkToolkit/ICompletableStream.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace NetworkToolkit 6 | { 7 | /// 8 | /// A extension that allows completion of writes against a duplex stream. 9 | /// 10 | public interface ICompletableStream 11 | { 12 | /// 13 | /// If true, the method is implemented. 14 | /// 15 | bool CanCompleteWrites { get; } 16 | 17 | /// 18 | /// Signals that writes to a duplex stream are complete. 19 | /// This will mark the end of the stream for the remote side's reads. 20 | /// 21 | /// A cancellation token for the asynchronous operation. 22 | /// A representing the asynchronous operation. 23 | public ValueTask CompleteWritesAsync(CancellationToken cancellationToken = default); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /NetworkToolkit/IScatterGatherStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace NetworkToolkit 8 | { 9 | /// 10 | /// A extension that enables scattered reads and gathered writes. 11 | /// 12 | public interface IScatterGatherStream 13 | { 14 | /// 15 | /// If true, the and methods will perform optimal scattered reads and gathered writes. 16 | /// 17 | bool CanScatterGather { get; } 18 | 19 | /// 20 | /// Reads a list of buffers as a single I/O. 21 | /// 22 | /// The buffers to read. 23 | /// A cancellation token for the asynchronous operation. 24 | /// The number of bytes read. 25 | ValueTask ReadAsync(IReadOnlyList> buffers, CancellationToken cancellationToken = default); 26 | 27 | /// 28 | /// Writes a list of buffers as a single I/O. 29 | /// 30 | /// The buffers to write. 31 | /// A cancellation token for the asynchronous operation. 32 | /// A representing the asynchronous operation. 33 | ValueTask WriteAsync(IReadOnlyList> buffers, CancellationToken cancellationToken = default); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /NetworkToolkit/IntrusiveLinkedList.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace NetworkToolkit 4 | { 5 | internal struct IntrusiveLinkedList where TNode : class, IIntrusiveLinkedListNode 6 | { 7 | private TNode? _first, _last; 8 | 9 | public TNode? Front => _first; 10 | public TNode? Back => _last; 11 | 12 | public void PushFront(TNode node) 13 | { 14 | ref IntrusiveLinkedNodeHeader header = ref node.ListHeader; 15 | 16 | Debug.Assert(header.Prev == null); 17 | Debug.Assert(header.Next == null); 18 | 19 | if (_first is TNode first) 20 | { 21 | header.Next = first; 22 | first.ListHeader.Prev = node; 23 | _first = node; 24 | } 25 | else 26 | { 27 | _first = node; 28 | _last = node; 29 | } 30 | } 31 | 32 | public void PushBack(TNode node) 33 | { 34 | ref IntrusiveLinkedNodeHeader header = ref node.ListHeader; 35 | 36 | Debug.Assert(header.Prev == null); 37 | Debug.Assert(header.Next == null); 38 | 39 | if (_last is TNode last) 40 | { 41 | header.Prev = last; 42 | last.ListHeader.Next = node; 43 | _last = node; 44 | } 45 | else 46 | { 47 | _first = node; 48 | _last = node; 49 | } 50 | } 51 | 52 | public TNode? PopFront() 53 | { 54 | if (_first is not TNode first) 55 | { 56 | return null; 57 | } 58 | 59 | ref IntrusiveLinkedNodeHeader header = ref first.ListHeader; 60 | 61 | if (header.Next is TNode next) 62 | { 63 | next.ListHeader.Prev = null; 64 | header.Next = null; 65 | _first = next; 66 | } 67 | else 68 | { 69 | _first = null; 70 | _last = null; 71 | } 72 | 73 | return first; 74 | } 75 | 76 | public TNode? PopBack() 77 | { 78 | if (_last is not TNode last) 79 | { 80 | return null; 81 | } 82 | 83 | ref IntrusiveLinkedNodeHeader header = ref last.ListHeader; 84 | 85 | if (header.Prev is TNode prev) 86 | { 87 | prev.ListHeader.Next = null; 88 | header.Prev = null; 89 | _last = prev; 90 | } 91 | else 92 | { 93 | _first = null; 94 | _last = null; 95 | } 96 | 97 | return last; 98 | } 99 | 100 | public void Remove(TNode node) 101 | { 102 | ref IntrusiveLinkedNodeHeader header = ref node.ListHeader; 103 | 104 | TNode? prev = header.Prev; 105 | TNode? next = header.Next; 106 | 107 | if (prev is not null) 108 | { 109 | prev.ListHeader.Next = next; 110 | header.Prev = null; 111 | } 112 | else 113 | { 114 | Debug.Assert(node == _first); 115 | _first = next; 116 | } 117 | 118 | if (next is not null) 119 | { 120 | next.ListHeader.Prev = prev; 121 | header.Next = null; 122 | } 123 | else 124 | { 125 | Debug.Assert(node == _last); 126 | _last = prev; 127 | } 128 | } 129 | } 130 | 131 | internal struct IntrusiveLinkedNodeHeader where TNode : class, IIntrusiveLinkedListNode 132 | { 133 | public TNode? Prev, Next; 134 | } 135 | 136 | internal interface IIntrusiveLinkedListNode where TNode : class, IIntrusiveLinkedListNode 137 | { 138 | public ref IntrusiveLinkedNodeHeader ListHeader { get; } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /NetworkToolkit/NetworkToolkit.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net5.0 5 | true 6 | true 7 | 1.0.0 8 | alpha2 9 | scalablecory 10 | Networking primitives for use with .NET, such as a low-level HTTP client and connection abstractions. 11 | © 2020, Cory Nelson 12 | MIT 13 | http;httpclient;socket;sockets 14 | https://github.com/scalablecory/NetworkToolkit.git 15 | NETWORKTOOLKIT_MAINLIB 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /NetworkToolkit/NullHttpHeaderSink.cs: -------------------------------------------------------------------------------- 1 | using NetworkToolkit.Http.Primitives; 2 | using System; 3 | 4 | namespace NetworkToolkit 5 | { 6 | internal sealed class NullHttpHeaderSink : IHttpHeadersSink 7 | { 8 | public static readonly NullHttpHeaderSink Instance = new NullHttpHeaderSink(); 9 | 10 | public void OnHeader(object? state, ReadOnlySpan headerName, ReadOnlySpan headerValue) 11 | { 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /NetworkToolkit/Parsing/ArrayBuffer.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System; 6 | using System.Buffers; 7 | using System.Diagnostics; 8 | using System.Runtime.InteropServices; 9 | 10 | namespace NetworkToolkit 11 | { 12 | /// 13 | /// A buffer used to assist with parsing/serialization when sending and receiving data. 14 | /// 15 | /// 16 | /// This is a mutable buffer. Copying and disposing twice will corrupt array pool. 17 | /// 18 | [StructLayout(LayoutKind.Auto)] 19 | public struct ArrayBuffer : IDisposable 20 | { 21 | private byte[] _bytes; 22 | private int _activeStart; 23 | private int _availableStart; 24 | 25 | // Invariants: 26 | // 0 <= _activeStart <= _availableStart <= bytes.Length 27 | 28 | /// 29 | /// Instantiates a new . 30 | /// 31 | /// The initial size of the buffer. 32 | public ArrayBuffer(int initialSize) 33 | { 34 | _bytes = ArrayPool.Shared.Rent(initialSize); 35 | _activeStart = 0; 36 | _availableStart = 0; 37 | } 38 | 39 | /// 40 | public void Dispose() 41 | { 42 | _activeStart = 0; 43 | _availableStart = 0; 44 | 45 | byte[] array = _bytes; 46 | 47 | if (array != null) 48 | { 49 | _bytes = null!; 50 | ArrayPool.Shared.Return(array); 51 | } 52 | } 53 | 54 | /// 55 | /// The number of bytes committed to the buffer. 56 | /// 57 | public int ActiveLength => _availableStart - _activeStart; 58 | 59 | /// 60 | /// The bytes committed to the buffer. 61 | /// 62 | public Span ActiveSpan => new Span(_bytes, _activeStart, _availableStart - _activeStart); 63 | 64 | /// 65 | /// The bytes committed to the buffer. 66 | /// 67 | public Memory ActiveMemory => new Memory(_bytes, _activeStart, _availableStart - _activeStart); 68 | 69 | /// 70 | /// The number of free bytes available in the buffer. 71 | /// 72 | public int AvailableLength => _bytes.Length - _availableStart; 73 | 74 | /// 75 | /// Free bytes in the buffer. 76 | /// 77 | public Span AvailableSpan => new Span(_bytes, _availableStart, AvailableLength); 78 | 79 | /// 80 | /// Free bytes in the buffer. 81 | /// 82 | public Memory AvailableMemory => new Memory(_bytes, _availableStart, _bytes.Length - _availableStart); 83 | 84 | /// 85 | /// The total capacity of the buffer. 86 | /// 87 | public int Capacity => _bytes.Length; 88 | 89 | /// 90 | /// Discards a number of active bytes. 91 | /// 92 | /// The number of bytes to discard. 93 | public void Discard(int byteCount) 94 | { 95 | Debug.Assert(byteCount <= ActiveLength, $"Expected {byteCount} <= {ActiveLength}"); 96 | _activeStart += byteCount; 97 | 98 | if (_activeStart == _availableStart) 99 | { 100 | _activeStart = 0; 101 | _availableStart = 0; 102 | } 103 | } 104 | 105 | /// 106 | /// Commits a number of bytes from Available to Active. 107 | /// 108 | /// The number of bytes to commit. 109 | public void Commit(int byteCount) 110 | { 111 | Debug.Assert(byteCount <= AvailableLength); 112 | _availableStart += byteCount; 113 | } 114 | 115 | /// 116 | /// Ensures is at least . 117 | /// 118 | /// The minimum number of bytes to make available. 119 | public void EnsureAvailableSpace(int byteCount) 120 | { 121 | if (byteCount > AvailableLength) 122 | { 123 | EnsureAvailableSpaceSlow(byteCount); 124 | } 125 | } 126 | 127 | private void EnsureAvailableSpaceSlow(int byteCount) 128 | { 129 | 130 | int totalFree = _activeStart + AvailableLength; 131 | if (byteCount <= totalFree) 132 | { 133 | // We can free up enough space by just shifting the bytes down, so do so. 134 | Buffer.BlockCopy(_bytes, _activeStart, _bytes, 0, ActiveLength); 135 | _availableStart = ActiveLength; 136 | _activeStart = 0; 137 | Debug.Assert(byteCount <= AvailableLength); 138 | return; 139 | } 140 | 141 | // Double the size of the buffer until we have enough space. 142 | int desiredSize = ActiveLength + byteCount; 143 | int newSize = _bytes.Length; 144 | do 145 | { 146 | newSize *= 2; 147 | } while (newSize < desiredSize); 148 | 149 | byte[] newBytes = ArrayPool.Shared.Rent(newSize); 150 | byte[] oldBytes = _bytes; 151 | 152 | if (ActiveLength != 0) 153 | { 154 | Buffer.BlockCopy(oldBytes, _activeStart, newBytes, 0, ActiveLength); 155 | } 156 | 157 | _availableStart = ActiveLength; 158 | _activeStart = 0; 159 | 160 | _bytes = newBytes; 161 | ArrayPool.Shared.Return(oldBytes); 162 | 163 | Debug.Assert(byteCount <= AvailableLength); 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /NetworkToolkit/Parsing/VectorArrayBuffer.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | #nullable enable 6 | using NetworkToolkit.Http.Primitives; 7 | using System; 8 | using System.Buffers; 9 | using System.Diagnostics; 10 | using System.Runtime.InteropServices; 11 | using System.Runtime.Intrinsics; 12 | 13 | namespace NetworkToolkit 14 | { 15 | // Warning: Mutable struct! 16 | // The purpose of this struct is to simplify buffer management. 17 | // It manages a sliding buffer where bytes can be added at the end and removed at the beginning. 18 | // [ActiveSpan/Memory] contains the current buffer contents; these bytes will be preserved 19 | // (copied, if necessary) on any call to EnsureAvailableBytes. 20 | // [AvailableSpan/Memory] contains the available bytes past the end of the current content, 21 | // and can be written to in order to add data to the end of the buffer. 22 | // Commit(byteCount) will extend the ActiveSpan by [byteCount] bytes into the AvailableSpan. 23 | // Discard(byteCount) will discard [byteCount] bytes as the beginning of the ActiveSpan. 24 | 25 | /// 26 | /// Identical to , but with padded address space to ensure vector operations can read past end of buffer. 27 | /// 28 | [StructLayout(LayoutKind.Auto)] 29 | internal struct VectorArrayBuffer : IDisposable 30 | { 31 | // It is assumed that (due to how virtual memory works) we do 32 | // not need to add padding to the front of the buffer. 33 | private const int PrefixPaddingLength = 0; 34 | // Pad the back of the array with enough bytes to ensure we 35 | // can always safely read an Vector256 at any index within returned spans. 36 | private static int SuffixPaddingLength => Http1Connection.HeaderBufferPadding; 37 | private static int TotalPaddingLength => PrefixPaddingLength + SuffixPaddingLength; 38 | 39 | private byte[] _bytes; 40 | private int _activeStart; 41 | private int _availableStart; 42 | 43 | // Invariants: 44 | // 0 <= _activeStart <= _availableStart <= (bytes.Length - TotalPaddingLength) 45 | 46 | public VectorArrayBuffer(int initialSize) 47 | { 48 | _bytes = Rent(initialSize + TotalPaddingLength); 49 | _activeStart = 0; 50 | _availableStart = 0; 51 | } 52 | 53 | public void Dispose() 54 | { 55 | _activeStart = 0; 56 | _availableStart = 0; 57 | 58 | byte[] array = _bytes; 59 | 60 | if (array != null) 61 | { 62 | _bytes = null!; 63 | ArrayPool.Shared.Return(array); 64 | } 65 | } 66 | 67 | public int ActiveLength => _availableStart - _activeStart; 68 | public Span ActiveSpan => new Span(_bytes, _activeStart + PrefixPaddingLength, ActiveLength); 69 | public Memory ActiveMemory => new Memory(_bytes, _activeStart + PrefixPaddingLength, ActiveLength); 70 | 71 | public int AvailableLength => Capacity - _availableStart; 72 | public Span AvailableSpan => new Span(_bytes, _availableStart + PrefixPaddingLength, AvailableLength); 73 | public Memory AvailableMemory => new Memory(_bytes, _availableStart + PrefixPaddingLength, AvailableLength); 74 | 75 | public int Capacity => _bytes.Length - TotalPaddingLength; 76 | 77 | private static byte[] Rent(int byteCount) 78 | { 79 | byte[] array = ArrayPool.Shared.Rent(byteCount); 80 | array.AsSpan().Clear(); 81 | 82 | // zero out the prefix and suffix padding, so they won't match any compares later on. 83 | if(PrefixPaddingLength != 0) 84 | { 85 | #pragma warning disable CS0162 // Unreachable code detected 86 | array.AsSpan(0, PrefixPaddingLength).Clear(); 87 | #pragma warning restore CS0162 // Unreachable code detected 88 | } 89 | 90 | array.AsSpan(array.Length - SuffixPaddingLength, SuffixPaddingLength).Clear(); 91 | 92 | return array; 93 | } 94 | 95 | public void Discard(int byteCount) 96 | { 97 | Debug.Assert(byteCount <= ActiveLength, $"Expected {byteCount} <= {ActiveLength}"); 98 | _activeStart += byteCount; 99 | 100 | if (_activeStart == _availableStart) 101 | { 102 | _activeStart = 0; 103 | _availableStart = 0; 104 | } 105 | } 106 | 107 | public void Commit(int byteCount) 108 | { 109 | Debug.Assert(byteCount <= AvailableLength); 110 | _availableStart += byteCount; 111 | } 112 | 113 | // Ensure at least [byteCount] bytes to write to. 114 | public void EnsureAvailableSpace(int byteCount) 115 | { 116 | if (byteCount > AvailableLength) 117 | { 118 | EnsureAvailableSpaceSlow(byteCount); 119 | } 120 | } 121 | 122 | private void EnsureAvailableSpaceSlow(int byteCount) 123 | { 124 | int totalFree = _activeStart + AvailableLength; 125 | if (byteCount <= totalFree) 126 | { 127 | // We can free up enough space by just shifting the bytes down, so do so. 128 | Buffer.BlockCopy(_bytes, _activeStart + PrefixPaddingLength, _bytes, PrefixPaddingLength, ActiveLength); 129 | _availableStart = ActiveLength; 130 | _activeStart = 0; 131 | Debug.Assert(byteCount <= AvailableLength); 132 | return; 133 | } 134 | 135 | // Double the size of the buffer until we have enough space. 136 | int desiredSize = ActiveLength + byteCount; 137 | int newSize = Capacity; 138 | do 139 | { 140 | newSize *= 2; 141 | } while (newSize < desiredSize); 142 | newSize += TotalPaddingLength; 143 | 144 | byte[] newBytes = Rent(newSize); 145 | byte[] oldBytes = _bytes; 146 | 147 | if (ActiveLength != 0) 148 | { 149 | Buffer.BlockCopy(oldBytes, _activeStart + PrefixPaddingLength, newBytes, PrefixPaddingLength, ActiveLength); 150 | } 151 | 152 | _availableStart = ActiveLength; 153 | _activeStart = 0; 154 | _bytes = newBytes; 155 | 156 | ArrayPool.Shared.Return(oldBytes); 157 | 158 | Debug.Assert(byteCount <= AvailableLength); 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /NetworkToolkit/ResettableValueTaskSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Threading.Tasks.Sources; 4 | 5 | namespace NetworkToolkit 6 | { 7 | internal sealed class ResettableValueTaskSource : IValueTaskSource, IValueTaskSource 8 | { 9 | private ManualResetValueTaskSourceCore _source; 10 | 11 | public ValueTask Task => new ValueTask(this, _source.Version); 12 | public ValueTask UntypedTask => new ValueTask(this, _source.Version); 13 | 14 | public void Reset() => 15 | _source.Reset(); 16 | 17 | public void SetResult(T result) => 18 | _source.SetResult(result); 19 | 20 | public void SetException(Exception ex) => 21 | _source.SetException(ex); 22 | 23 | T IValueTaskSource.GetResult(short token) => 24 | _source.GetResult(token); 25 | 26 | void IValueTaskSource.GetResult(short token) => 27 | _source.GetResult(token); 28 | 29 | ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => 30 | _source.GetStatus(token); 31 | 32 | ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => 33 | _source.GetStatus(token); 34 | 35 | void IValueTaskSource.OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) => 36 | _source.OnCompleted(continuation, state, token, flags); 37 | 38 | void IValueTaskSource.OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) => 39 | _source.OnCompleted(continuation, state, token, flags); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /NetworkToolkit/SocketTaskEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Sockets; 3 | using System.Threading.Tasks; 4 | using System.Threading.Tasks.Sources; 5 | 6 | namespace NetworkToolkit 7 | { 8 | internal class SocketTaskEventArgs : SocketAsyncEventArgs, IValueTaskSource, IValueTaskSource 9 | { 10 | private ManualResetValueTaskSourceCore _taskSource; 11 | 12 | public ValueTask Task => new ValueTask(this, _taskSource.Version); 13 | 14 | public ValueTask GenericTask => new ValueTask(this, _taskSource.Version); 15 | 16 | public SocketTaskEventArgs() : base(unsafeSuppressExecutionContextFlow: true) 17 | { 18 | } 19 | 20 | public void Reset() => 21 | _taskSource.Reset(); 22 | 23 | public void SetResult(T value) => 24 | _taskSource.SetResult(value); 25 | 26 | public void SetException(Exception error) => 27 | _taskSource.SetException(error); 28 | 29 | void IValueTaskSource.GetResult(short token) => 30 | _taskSource.GetResult(token); 31 | 32 | T IValueTaskSource.GetResult(short token) => 33 | _taskSource.GetResult(token); 34 | 35 | ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => 36 | _taskSource.GetStatus(token); 37 | 38 | ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => 39 | _taskSource.GetStatus(token); 40 | 41 | void IValueTaskSource.OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) => 42 | _taskSource.OnCompleted(continuation, state, token, flags); 43 | 44 | void IValueTaskSource.OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) => 45 | _taskSource.OnCompleted(continuation, state, token, flags); 46 | 47 | protected override void OnCompleted(SocketAsyncEventArgs e) 48 | { 49 | SetResult(default!); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /NetworkToolkit/StreamExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace NetworkToolkit 6 | { 7 | internal static class StreamExtensions 8 | { 9 | public static ValueTask DisposeAsync(this Stream stream, CancellationToken cancellationToken) 10 | { 11 | if (stream is ICancellableAsyncDisposable disposable) 12 | { 13 | return disposable.DisposeAsync(cancellationToken); 14 | } 15 | else 16 | { 17 | return stream.DisposeAsync(); 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /NetworkToolkit/TaskToApm.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | // Helper methods for using Tasks to implement the APM pattern. 5 | // 6 | // Example usage, wrapping a Task-returning FooAsync method with Begin/EndFoo methods: 7 | // 8 | // public IAsyncResult BeginFoo(..., AsyncCallback? callback, object? state) => 9 | // TaskToApm.Begin(FooAsync(...), callback, state); 10 | // 11 | // public int EndFoo(IAsyncResult asyncResult) => 12 | // TaskToApm.End(asyncResult); 13 | 14 | #nullable enable 15 | using System; 16 | using System.Diagnostics; 17 | using System.Diagnostics.CodeAnalysis; 18 | using System.Threading; 19 | using System.Threading.Tasks; 20 | 21 | namespace NetworkToolkit 22 | { 23 | /// 24 | /// Provides support for efficiently using Tasks to implement the APM (Begin/End) pattern. 25 | /// 26 | internal static class TaskToApm 27 | { 28 | /// 29 | /// Marshals the Task as an IAsyncResult, using the supplied callback and state 30 | /// to implement the APM pattern. 31 | /// 32 | /// The Task to be marshaled. 33 | /// The callback to be invoked upon completion. 34 | /// The state to be stored in the IAsyncResult. 35 | /// An IAsyncResult to represent the task's asynchronous operation. 36 | public static IAsyncResult Begin(Task task, AsyncCallback? callback, object? state) => 37 | new TaskAsyncResult(task, state, callback); 38 | 39 | /// Processes an IAsyncResult returned by Begin. 40 | /// The IAsyncResult to unwrap. 41 | public static void End(IAsyncResult asyncResult) 42 | { 43 | if (asyncResult is TaskAsyncResult twar) 44 | { 45 | twar._task.GetAwaiter().GetResult(); 46 | return; 47 | } 48 | 49 | ThrowArgumentException(asyncResult); 50 | } 51 | 52 | /// Processes an IAsyncResult returned by Begin. 53 | /// The IAsyncResult to unwrap. 54 | public static TResult End(IAsyncResult asyncResult) 55 | { 56 | if (asyncResult is TaskAsyncResult twar && twar._task is Task task) 57 | { 58 | return task.GetAwaiter().GetResult(); 59 | } 60 | 61 | ThrowArgumentException(asyncResult); 62 | return default!; // unreachable 63 | } 64 | 65 | /// Throws an argument exception for the invalid . 66 | [DoesNotReturn] 67 | private static void ThrowArgumentException(IAsyncResult asyncResult) => 68 | throw (asyncResult is null ? 69 | new ArgumentNullException(nameof(asyncResult)) : 70 | new ArgumentException(null, nameof(asyncResult))); 71 | 72 | /// Provides a simple IAsyncResult that wraps a Task. 73 | /// 74 | /// We could use the Task as the IAsyncResult if the Task's AsyncState is the same as the object state, 75 | /// but that's very rare, in particular in a situation where someone cares about allocation, and always 76 | /// using TaskAsyncResult simplifies things and enables additional optimizations. 77 | /// 78 | internal sealed class TaskAsyncResult : IAsyncResult 79 | { 80 | /// The wrapped Task. 81 | internal readonly Task _task; 82 | /// Callback to invoke when the wrapped task completes. 83 | private readonly AsyncCallback? _callback; 84 | 85 | /// Initializes the IAsyncResult with the Task to wrap and the associated object state. 86 | /// The Task to wrap. 87 | /// The new AsyncState value. 88 | /// Callback to invoke when the wrapped task completes. 89 | internal TaskAsyncResult(Task task, object? state, AsyncCallback? callback) 90 | { 91 | Debug.Assert(task != null); 92 | _task = task; 93 | AsyncState = state; 94 | 95 | if (task.IsCompleted) 96 | { 97 | // Synchronous completion. Invoke the callback. No need to store it. 98 | CompletedSynchronously = true; 99 | callback?.Invoke(this); 100 | } 101 | else if (callback != null) 102 | { 103 | // Asynchronous completion, and we have a callback; schedule it. We use OnCompleted rather than ContinueWith in 104 | // order to avoid running synchronously if the task has already completed by the time we get here but still run 105 | // synchronously as part of the task's completion if the task completes after (the more common case). 106 | _callback = callback; 107 | _task.ConfigureAwait(continueOnCapturedContext: false) 108 | .GetAwaiter() 109 | .OnCompleted(InvokeCallback); // allocates a delegate, but avoids a closure 110 | } 111 | } 112 | 113 | /// Invokes the callback. 114 | private void InvokeCallback() 115 | { 116 | Debug.Assert(!CompletedSynchronously); 117 | Debug.Assert(_callback != null); 118 | _callback.Invoke(this); 119 | } 120 | 121 | /// Gets a user-defined object that qualifies or contains information about an asynchronous operation. 122 | public object? AsyncState { get; } 123 | /// Gets a value that indicates whether the asynchronous operation completed synchronously. 124 | /// This is set lazily based on whether the has completed by the time this object is created. 125 | public bool CompletedSynchronously { get; } 126 | /// Gets a value that indicates whether the asynchronous operation has completed. 127 | public bool IsCompleted => _task.IsCompleted; 128 | /// Gets a that is used to wait for an asynchronous operation to complete. 129 | public WaitHandle AsyncWaitHandle => ((IAsyncResult)_task).AsyncWaitHandle; 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /NetworkToolkit/Tools.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace NetworkToolkit 8 | { 9 | internal static class Tools 10 | { 11 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 12 | public static unsafe int UnsafeByteOffset(ReadOnlySpan start, ReadOnlySpan end) => 13 | (int)(void*)Unsafe.ByteOffset(ref MemoryMarshal.GetReference(start), ref MemoryMarshal.GetReference(end)); 14 | 15 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 16 | public static unsafe int UnsafeByteOffset(ReadOnlySpan start, ref byte end) => 17 | (int)(void*)Unsafe.ByteOffset(ref MemoryMarshal.GetReference(start), ref end); 18 | 19 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 20 | public static unsafe int UnsafeByteOffset(ref byte start, ReadOnlySpan end) => 21 | (int)(void*)Unsafe.ByteOffset(ref start, ref MemoryMarshal.GetReference(end)); 22 | 23 | public static bool TimeoutExpired(long curTicks, long fromTicks, TimeSpan timeoutLimit) 24 | { 25 | return timeoutLimit != Timeout.InfiniteTimeSpan && new TimeSpan((curTicks - fromTicks) * TimeSpan.TicksPerMillisecond) > timeoutLimit; 26 | } 27 | 28 | public static string EscapeIdnHost(string hostName) => 29 | new UriBuilder() { Scheme = Uri.UriSchemeHttp, Host = hostName, Port = 80 }.Uri.IdnHost; 30 | 31 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 32 | public static void BlockForResult(in ValueTask task) 33 | { 34 | if (task.IsCompleted) 35 | { 36 | task.GetAwaiter().GetResult(); 37 | } 38 | else 39 | { 40 | task.AsTask().GetAwaiter().GetResult(); 41 | } 42 | } 43 | 44 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 45 | public static T BlockForResult(in ValueTask task) 46 | { 47 | return task.IsCompleted ? task.GetAwaiter().GetResult() : task.AsTask().GetAwaiter().GetResult(); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /NetworkToolkit/TunnelEndPoint.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | 4 | namespace NetworkToolkit 5 | { 6 | internal sealed class TunnelEndPoint : EndPoint 7 | { 8 | public EndPoint? LocalEndPoint { get; } 9 | public EndPoint? RemoteEndPoint { get; } 10 | 11 | public TunnelEndPoint(EndPoint? localEndPoint, EndPoint? remoteEndPoint) 12 | { 13 | LocalEndPoint = localEndPoint; 14 | RemoteEndPoint = remoteEndPoint; 15 | } 16 | 17 | public override string ToString() => 18 | $"{{ {LocalEndPoint?.ToString() ?? "unknown"} -> {RemoteEndPoint?.ToString() ?? "unknown"} }}"; 19 | 20 | public override bool Equals(object? obj) => 21 | obj is TunnelEndPoint ep && 22 | (ep.LocalEndPoint != null) == (LocalEndPoint != null) && 23 | (ep.RemoteEndPoint != null) == (RemoteEndPoint != null) && 24 | ep.LocalEndPoint?.Equals(LocalEndPoint) != false && 25 | ep.RemoteEndPoint?.Equals(RemoteEndPoint) != false; 26 | 27 | public override int GetHashCode() => 28 | HashCode.Combine(LocalEndPoint, RemoteEndPoint); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NetworkToolkit 2 | 3 | This project contains networking primitives for use with .NET. It can be obtained from NuGet. [![Nuget](https://img.shields.io/nuget/v/NetworkToolkit)](https://www.nuget.org/packages/NetworkToolkit) 4 | 5 | This is a bit of a prototyping playground right now and APIs will not be perfectly stable. 6 | 7 | ## HTTP Primitives 8 | 9 | NetworkToolkit provides a lower-level HTTP client that prioritizes performance and flexibility. Once warmed up, it processes requests using zero allocations. 10 | 11 | ### Performance 12 | 13 | In the great majority of cases, `HttpClient` will be fast enough. Generally I/O will dominate a request's total time. However, if peak perf is needed, this library will be faster, especially as the number of headers in the request/response increase. 14 | 15 | Benchmarks are incomplete, so this should not be viewed as an exhaustive comparison: 16 | 17 | | Method | RequestHeaders | ResponseBytes | Mean | Error | StdDev | Median | Ratio | RatioSD | 18 | |------------------ |--------------- |-------------- |----------:|----------:|----------:|----------:|------:|--------:| 19 | | Primitive | Minimal | Minimal | 10.806 us | 0.3304 us | 0.9741 us | 10.985 us | 0.51 | 0.05 | 20 | | PrimitivePrepared | Minimal | Minimal | 9.278 us | 0.2091 us | 0.6066 us | 9.298 us | 0.44 | 0.04 | 21 | | SocketsHandler | Minimal | Minimal | 21.323 us | 0.4380 us | 1.2136 us | 21.442 us | 1.00 | 0.00 | 22 | | | | | | | | | | | 23 | | Primitive | Minimal | StackOverflow | 13.665 us | 0.6509 us | 1.9089 us | 13.187 us | 0.16 | 0.02 | 24 | | PrimitivePrepared | Minimal | StackOverflow | 14.108 us | 0.6328 us | 1.8559 us | 13.432 us | 0.17 | 0.03 | 25 | | SocketsHandler | Minimal | StackOverflow | 86.356 us | 1.7149 us | 4.2707 us | 87.476 us | 1.00 | 0.00 | 26 | | | | | | | | | | | 27 | | Primitive | Normal | Minimal | 11.053 us | 0.2498 us | 0.7366 us | 11.149 us | 0.32 | 0.03 | 28 | | PrimitivePrepared | Normal | Minimal | 10.636 us | 0.2867 us | 0.8455 us | 10.701 us | 0.31 | 0.03 | 29 | | SocketsHandler | Normal | Minimal | 34.775 us | 0.6940 us | 1.9231 us | 35.172 us | 1.00 | 0.00 | 30 | | | | | | | | | | | 31 | | Primitive | Normal | StackOverflow | 15.080 us | 0.6158 us | 1.7866 us | 14.874 us | 0.16 | 0.03 | 32 | | PrimitivePrepared | Normal | StackOverflow | 13.964 us | 0.5963 us | 1.7490 us | 13.257 us | 0.15 | 0.02 | 33 | | SocketsHandler | Normal | StackOverflow | 94.221 us | 2.4801 us | 7.3127 us | 96.337 us | 1.00 | 0.00 | 34 | 35 | ### A simple GET request using exactly one HTTP/1 connection, no pooling 36 | 37 | Avoid a connection pool to have exact control over connections. 38 | 39 | ```c# 40 | await using ConnectionFactory connectionFactory = new SocketConnectionFactory(); 41 | await using Connection connection = await connectionFactory.ConnectAsync(new DnsEndPoint("microsoft.com", 80)); 42 | await using HttpConnection httpConnection = new Http1Connection(connection); 43 | await using ValueHttpRequest request = (await httpConnection.CreateNewRequestAsync(HttpPrimitiveVersion.Version11, HttpVersionPolicy.RequestVersionExact)).Value; 44 | 45 | request.ConfigureRequest(contentLength: 0, hasTrailingHeaders: false); 46 | request.WriteRequest(HttpMethod.Get, new Uri("http://microsoft.com")); 47 | request.WriteHeader("Accept", "text/html"); 48 | await request.CompleteRequestAsync(); 49 | 50 | await request.ReadToFinalResponseAsync(); 51 | Console.WriteLine($"Got response {request.StatusCode}."); 52 | 53 | if(await request.ReadToHeadersAsync()) 54 | { 55 | await request.ReadHeadersAsync(...); 56 | } 57 | 58 | if(await request.ReadToContentAsync()) 59 | { 60 | int len; 61 | byte[] buffer = ArrayPool.Shared.Rent(4096); 62 | 63 | do 64 | { 65 | while((len = await request.ReadContentAsync(buffer)) != 0) 66 | { 67 | ForwardData(buffer[..len]); 68 | } 69 | } 70 | while(await request.ReadToNextContentAsync()); 71 | 72 | ArrayPool.Shared.Return(buffer); 73 | } 74 | 75 | await request.DrainAsync(); 76 | ``` 77 | 78 | ### Using the opt-in connection pool 79 | 80 | A single-host connection pool handles concurrent HTTP/1 requests, H2C upgrade, ALPN negotiation for HTTP/2, etc. 81 | 82 | ```c# 83 | await using ConnectionFactory connectionFactory = new SocketConnectionFactory(); 84 | await using HttpConnection httpConnection = new PooledHttpConnection(connectionFactory, new DnsEndPoint("microsoft.com", 80), sslTargetHost: null); 85 | await using ValueHttpRequest request = (await httpConnection.CreateNewRequestAsync(HttpPrimitiveVersion.Version11, HttpVersionPolicy.RequestVersionExact)).Value; 86 | ``` 87 | 88 | ### Optimize frequently-used headers 89 | 90 | Prepare frequently-used headers to reduce CPU costs by pre-validating and caching protocol encoding. In the future, this will light up dynamic table compression in HTTP/2 and HTTP/3. 91 | 92 | ```c# 93 | PreparedHeaderSet preparedHeaders = new PreparedHeaderSet() 94 | { 95 | { "User-Agent", "NetworkToolkit" }, 96 | { "Accept", "text/html" } 97 | }; 98 | 99 | await using ValueHttpRequest request = ...; 100 | 101 | request.ConfigureRequest(contentLength: 0, hasTrailingHeaders: false); 102 | request.WriteRequest(HttpMethod.Get, new Uri("http://microsoft.com")); 103 | request.WriteHeader(preparedHeaders); 104 | ``` 105 | 106 | ### Avoiding strings 107 | 108 | Avoid `string` and `Uri` allocations, optimize away some related processing, and get tight control over encoding by passing in `ReadOnlySpan`: 109 | 110 | ```c# 111 | ReadOnlySpan method = HttpRequest.GetMethod; // "GET" 112 | ReadOnlySpan authority = ...; // "microsoft.com:80" 113 | ReadOnlySpan pathAndQuery = ...; // "/" 114 | 115 | ReadOnlySpan headerName = ...; // "Accept" 116 | ReadOnlySpan headerValue = ...; // "text/html" 117 | 118 | await using ValueHttpRequest request = ...; 119 | request.ConfigureRequest(contentLength: 0, hasTrailingHeaders: false); 120 | request.WriteRequest(method, authority, pathAndQuery); 121 | request.WriteHeader(headerName, headerValue); 122 | ``` 123 | 124 | ### Advanced processing 125 | 126 | Access extensions like HTTP/2 ALT-SVC frames, or efficiently forward requests (reverse proxy) by processing requests as a loosely structured stream: 127 | 128 | ```c# 129 | await using ValueHttpRequest request = ...; 130 | while(await request.ReadAsync() != HttpReadType.EndOfStream) 131 | { 132 | switch(request.ReadType) 133 | { 134 | case HttpReadType.InformationalResponse: 135 | ProcessInformationalResponse(request.StatusCode, request.Version); 136 | break; 137 | case HttpReadType.FinalResponse: 138 | ProcessResponse(request.StatusCode, request.Version); 139 | break; 140 | case HttpReadType.Headers: 141 | await request.ReadHeadersAsync(...); 142 | break; 143 | case HttpReadType.Content: 144 | int len; 145 | byte[] buffer = ArrayPool.Shared.Rent(4096); 146 | 147 | while((len = await request.ReadContentAsync(buffer)) != 0) 148 | { 149 | ForwardData(buffer[..len]); 150 | } 151 | 152 | ArrayPool.Shared.Return(buffer); 153 | break; 154 | case HttpReadType.TrailingHeaders: 155 | await request.ReadHeadersAsync(...); 156 | break; 157 | case HttpReadType.AltSvc: 158 | ProcessAltSvc(request.AltSvc); 159 | break; 160 | } 161 | } 162 | ``` 163 | 164 | ## Connection Abstractions 165 | 166 | Connection abstractions used to abstract establishment of `Stream`-based connections. -------------------------------------------------------------------------------- /THIRD-PARTY-NOTICES.TXT: -------------------------------------------------------------------------------- 1 | NetworkToolkit uses code Copyright (c) .NET Foundation, that is under the same license as NetworkToolkit. -------------------------------------------------------------------------------- /examples/Directory.build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/http/PreparedHeadersSample/PreparedHeadersSample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | Exe 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/http/PreparedHeadersSample/Program.cs: -------------------------------------------------------------------------------- 1 | using NetworkToolkit.Connections; 2 | using NetworkToolkit.Http.Primitives; 3 | using System; 4 | using System.Net; 5 | using System.Net.Http; 6 | using System.Threading.Tasks; 7 | 8 | namespace PreparedHeadersSample 9 | { 10 | class Program 11 | { 12 | static async Task Main(string[] args) 13 | { 14 | PreparedHeaderSet preparedHeaders = 15 | new PreparedHeaderSetBuilder() 16 | .AddHeader("User-Agent", "NetworkToolkit") 17 | .AddHeader("Accept", "text/html") 18 | .Build(); 19 | 20 | await using ConnectionFactory connectionFactory = new SocketConnectionFactory(); 21 | await using Connection connection = await connectionFactory.ConnectAsync(new DnsEndPoint("microsoft.com", 80)); 22 | await using HttpConnection httpConnection = new Http1Connection(connection, HttpPrimitiveVersion.Version11); 23 | 24 | int requestCounter = 0; 25 | await SingleRequest(); 26 | await SingleRequest(); 27 | 28 | async Task SingleRequest() 29 | { 30 | await using ValueHttpRequest request = (await httpConnection.CreateNewRequestAsync(HttpPrimitiveVersion.Version11, HttpVersionPolicy.RequestVersionExact)).Value; 31 | 32 | request.ConfigureRequest(contentLength: 0, hasTrailingHeaders: false); 33 | request.WriteRequest(HttpMethod.Get, new Uri("http://microsoft.com")); 34 | request.WriteHeader(preparedHeaders); 35 | request.WriteHeader("X-Example-RequestNo", requestCounter++.ToString()); 36 | await request.CompleteRequestAsync(); 37 | await request.DrainAsync(); 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/http/SimpleRequestSample/Program.cs: -------------------------------------------------------------------------------- 1 | using NetworkToolkit.Connections; 2 | using NetworkToolkit.Http.Primitives; 3 | using System; 4 | using System.Net; 5 | using System.Net.Http; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace SimpleRequestSample 10 | { 11 | class Program 12 | { 13 | static async Task Main(string[] args) 14 | { 15 | await using ConnectionFactory connectionFactory = new SocketConnectionFactory(); 16 | await using Connection connection = await connectionFactory.ConnectAsync(new DnsEndPoint("microsoft.com", 80)); 17 | await using HttpConnection httpConnection = new Http1Connection(connection, HttpPrimitiveVersion.Version11); 18 | 19 | await using (ValueHttpRequest request = (await httpConnection.CreateNewRequestAsync(HttpPrimitiveVersion.Version11, HttpVersionPolicy.RequestVersionExact)).Value) 20 | { 21 | request.ConfigureRequest(contentLength: 0, hasTrailingHeaders: false); 22 | request.WriteRequest(HttpMethod.Get, new Uri("http://microsoft.com")); 23 | request.WriteHeader("Accept", "text/html"); 24 | await request.CompleteRequestAsync(); 25 | 26 | await request.ReadToFinalResponseAsync(); 27 | Console.WriteLine($"Final response code: {request.StatusCode}"); 28 | 29 | if (await request.ReadToHeadersAsync()) 30 | { 31 | await request.ReadHeadersAsync(new PrintingHeadersSink(), state: null); 32 | } 33 | else 34 | { 35 | Console.WriteLine("No headers received."); 36 | } 37 | 38 | if (await request.ReadToContentAsync()) 39 | { 40 | long totalLen = 0; 41 | 42 | var buffer = new byte[4096]; 43 | int readLen; 44 | 45 | do 46 | { 47 | while ((readLen = await request.ReadContentAsync(buffer)) != 0) 48 | { 49 | totalLen += readLen; 50 | } 51 | } 52 | while (await request.ReadToNextContentAsync()); 53 | 54 | Console.WriteLine($"Received {totalLen} byte response."); 55 | } 56 | else 57 | { 58 | Console.WriteLine("No content received."); 59 | } 60 | 61 | if (await request.ReadToTrailingHeadersAsync()) 62 | { 63 | await request.ReadHeadersAsync(new PrintingHeadersSink(), state: null); 64 | } 65 | else 66 | { 67 | Console.WriteLine("No trailing headers received."); 68 | } 69 | } 70 | } 71 | } 72 | 73 | sealed class PrintingHeadersSink : IHttpHeadersSink 74 | { 75 | public void OnHeader(object state, ReadOnlySpan headerName, ReadOnlySpan headerValue) 76 | { 77 | Console.WriteLine($"{Encoding.ASCII.GetString(headerName)}: {Encoding.ASCII.GetString(headerValue)}"); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /examples/http/SimpleRequestSample/SimpleRequestSample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net5.0 6 | 7 | 8 | 9 | --------------------------------------------------------------------------------