├── .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