From c90f511237dc9171f521e9ee2a440e1f2806c3ce Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Fri, 6 Jun 2025 13:34:52 +0200 Subject: [PATCH 1/5] tests --- .../Kestrel/Core/test/TlsListenerTests.cs | 4 +- .../TlsListenerTests.cs | 57 +++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/Servers/Kestrel/Core/test/TlsListenerTests.cs b/src/Servers/Kestrel/Core/test/TlsListenerTests.cs index 4bce89de208d..51137d1c9f18 100644 --- a/src/Servers/Kestrel/Core/test/TlsListenerTests.cs +++ b/src/Servers/Kestrel/Core/test/TlsListenerTests.cs @@ -158,8 +158,8 @@ public async Task RunTlsClientHelloCallbackTest_DeterministicallyReads() Assert.Equal(5, readResult.Buffer.Length); // ensuring that we have read limited number of times - Assert.True(reader.ReadAsyncCounter is >= 2 && reader.ReadAsyncCounter is <= 4, - $"Expected ReadAsync() to happen about 2-4 times. Actually happened {reader.ReadAsyncCounter} times."); + Assert.True(reader.ReadAsyncCounter is >= 2 && reader.ReadAsyncCounter is <= 5, + $"Expected ReadAsync() to happen about 2-5 times. Actually happened {reader.ReadAsyncCounter} times."); } private async Task RunTlsClientHelloCallbackTest_WithMultipleSegments( diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TlsListenerTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TlsListenerTests.cs index f91ea27eae8f..ccd07d088553 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TlsListenerTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TlsListenerTests.cs @@ -18,6 +18,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Xunit.Sdk; namespace InMemory.FunctionalTests; @@ -66,4 +67,60 @@ await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions Assert.True(tlsClientHelloCallbackInvoked); } + + [Fact] + public async Task TlsClientHelloBytesCallback_PreCanceledToken() + { + var tlsClientHelloCallbackInvoked = false; + + var testContext = new TestServiceContext(LoggerFactory); + await using (var server = new TestServer(context => Task.CompletedTask, + testContext, + listenOptions => + { + listenOptions.UseHttps(_x509Certificate2, httpsOptions => + { + httpsOptions.TlsClientHelloBytesCallback = (connection, clientHelloBytes) => + { + Logger.LogDebug("[Received TlsClientHelloBytesCallback] Connection: {0}; TLS client hello buffer: {1}", connection.ConnectionId, clientHelloBytes.Length); + tlsClientHelloCallbackInvoked = true; + Assert.True(clientHelloBytes.Length > 32); + Assert.NotNull(connection); + }; + }); + })) + { + using (var connection = server.CreateConnection()) + { + using (var sslStream = new SslStream(connection.Stream, false, (sender, cert, chain, errors) => true, null)) + { + var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(1)); + var token = cancellationTokenSource.Token; + + try + { + await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions + { + TargetHost = "localhost", + EnabledSslProtocols = SslProtocols.None + }, token); + + var request = Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\nHost:\r\n\r\n"); + await sslStream.WriteAsync(request, 0, request.Length, token); + await sslStream.ReadAsync(new Memory(new byte[1024]), token); + } + catch (Exception ex) when (ex is OperationCanceledException or TaskCanceledException) + { + // expected + } + catch (Exception ex) + { + ThrowsException.ForIncorrectExceptionType(typeof(OperationCanceledException), ex); + } + } + } + } + + Assert.False(tlsClientHelloCallbackInvoked); + } } From da3355f07202c49e3b7f31fb0af6513224beb5bd Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Fri, 6 Jun 2025 16:03:37 +0200 Subject: [PATCH 2/5] stabilize exception --- src/Servers/Kestrel/Core/test/TlsListenerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Servers/Kestrel/Core/test/TlsListenerTests.cs b/src/Servers/Kestrel/Core/test/TlsListenerTests.cs index 51137d1c9f18..9ab6f944082c 100644 --- a/src/Servers/Kestrel/Core/test/TlsListenerTests.cs +++ b/src/Servers/Kestrel/Core/test/TlsListenerTests.cs @@ -122,7 +122,7 @@ public async Task RunTlsClientHelloCallbackTest_WithPendingCancellation() await writer.WriteAsync(new byte[2] { 0x03, 0x01 }); cts.Cancel(); - await Assert.ThrowsAsync(async () => await listenerTask); + await VerifyThrowsAnyAsync(() => listenerTask, typeof(OperationCanceledException), typeof(TaskCanceledException)); Assert.False(tlsClientHelloCallbackInvoked); } From 5d3e42d057b39c3025c489e000841b0dcb553814 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Fri, 6 Jun 2025 20:05:47 +0200 Subject: [PATCH 3/5] use xunit API + use handshake timeout --- .../Kestrel/Core/test/TlsListenerTests.cs | 34 ++----------- .../TlsListenerTests.cs | 51 ++++++++++++++++--- 2 files changed, 48 insertions(+), 37 deletions(-) diff --git a/src/Servers/Kestrel/Core/test/TlsListenerTests.cs b/src/Servers/Kestrel/Core/test/TlsListenerTests.cs index 9ab6f944082c..6350465dbe41 100644 --- a/src/Servers/Kestrel/Core/test/TlsListenerTests.cs +++ b/src/Servers/Kestrel/Core/test/TlsListenerTests.cs @@ -70,9 +70,7 @@ public async Task RunTlsClientHelloCallbackTest_WithExtraShortLastingToken() var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(3)); await writer.WriteAsync(new byte[1] { 0x16 }); - await VerifyThrowsAnyAsync( - async () => await listener.OnTlsClientHelloAsync(transportConnection, cts.Token), - typeof(OperationCanceledException), typeof(TaskCanceledException)); + await Assert.ThrowsAnyAsync(() => listener.OnTlsClientHelloAsync(transportConnection, cts.Token)); Assert.False(tlsClientHelloCallbackInvoked); } @@ -95,9 +93,7 @@ public async Task RunTlsClientHelloCallbackTest_WithPreCanceledToken() cts.Cancel(); await writer.WriteAsync(new byte[1] { 0x16 }); - await VerifyThrowsAnyAsync( - async () => await listener.OnTlsClientHelloAsync(transportConnection, cts.Token), - typeof(OperationCanceledException), typeof(TaskCanceledException)); + await Assert.ThrowsAnyAsync(() => listener.OnTlsClientHelloAsync(transportConnection, cts.Token)); Assert.False(tlsClientHelloCallbackInvoked); } @@ -122,7 +118,7 @@ public async Task RunTlsClientHelloCallbackTest_WithPendingCancellation() await writer.WriteAsync(new byte[2] { 0x03, 0x01 }); cts.Cancel(); - await VerifyThrowsAnyAsync(() => listenerTask, typeof(OperationCanceledException), typeof(TaskCanceledException)); + await Assert.ThrowsAnyAsync(() => listenerTask); Assert.False(tlsClientHelloCallbackInvoked); } @@ -623,28 +619,4 @@ public static IEnumerable InvalidClientHelloData_Segmented() _invalidTlsClientHelloHeader, _invalid3BytesMessage, _invalid9BytesMessage, _invalidUnknownProtocolVersion1, _invalidUnknownProtocolVersion2, _invalidIncorrectHandshakeMessageType }; - - static async Task VerifyThrowsAnyAsync(Func code, params Type[] exceptionTypes) - { - if (exceptionTypes == null || exceptionTypes.Length == 0) - { - throw new ArgumentException("At least one exception type must be provided.", nameof(exceptionTypes)); - } - - try - { - await code(); - } - catch (Exception ex) - { - if (exceptionTypes.Any(type => type.IsInstanceOfType(ex))) - { - return; - } - - throw ThrowsException.ForIncorrectExceptionType(exceptionTypes.First(), ex); - } - - throw ThrowsException.ForNoException(exceptionTypes.First()); - } } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TlsListenerTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TlsListenerTests.cs index ccd07d088553..a116420e622c 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TlsListenerTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TlsListenerTests.cs @@ -18,6 +18,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; using Xunit.Sdk; namespace InMemory.FunctionalTests; @@ -97,19 +98,59 @@ public async Task TlsClientHelloBytesCallback_PreCanceledToken() var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(1)); var token = cancellationTokenSource.Token; + await Assert.ThrowsAnyAsync(() => sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions + { + TargetHost = "localhost", + EnabledSslProtocols = SslProtocols.None + }, token)); + } + } + } + + Assert.False(tlsClientHelloCallbackInvoked); + } + + [Fact] + public async Task TlsClientHelloBytesCallback_UsesOptionsTimeout() + { + var testContext = new TestServiceContext(LoggerFactory); + await using (var server = new TestServer(context => Task.CompletedTask, + testContext, + listenOptions => + { + listenOptions.UseHttps(_x509Certificate2, httpsOptions => + { + httpsOptions.HandshakeTimeout = TimeSpan.FromMilliseconds(1); + + httpsOptions.TlsClientHelloBytesCallback = (connection, clientHelloBytes) => + { + Logger.LogDebug("[Received TlsClientHelloBytesCallback] Connection: {0}; TLS client hello buffer: {1}", connection.ConnectionId, clientHelloBytes.Length); + Assert.True(clientHelloBytes.Length > 32); + Assert.NotNull(connection); + }; + }); + })) + { + using (var connection = server.CreateConnection()) + { + using (var sslStream = new SslStream(connection.Stream, false, (sender, cert, chain, errors) => true, null)) + { try { await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions { TargetHost = "localhost", EnabledSslProtocols = SslProtocols.None - }, token); + }); var request = Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\nHost:\r\n\r\n"); - await sslStream.WriteAsync(request, 0, request.Length, token); - await sslStream.ReadAsync(new Memory(new byte[1024]), token); + await sslStream.WriteAsync(request, 0, request.Length); + await sslStream.ReadAsync(new Memory(new byte[1024])); } - catch (Exception ex) when (ex is OperationCanceledException or TaskCanceledException) + catch (Exception ex) + when (ex is OperationCanceledException or TaskCanceledException // when cancellation comes from tls listener + or IOException // when the underlying stream is closed due to timeout + ) { // expected } @@ -120,7 +161,5 @@ await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions } } } - - Assert.False(tlsClientHelloCallbackInvoked); } } From 3c1f92e6540c25fdbdb2b8163bc5a75d89fdf279 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Sat, 7 Jun 2025 00:14:23 +0200 Subject: [PATCH 4/5] make meaningfull test --- .../TlsListenerTests.cs | 70 +++---------------- 1 file changed, 8 insertions(+), 62 deletions(-) diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TlsListenerTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TlsListenerTests.cs index a116420e622c..02f49e84a2e6 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TlsListenerTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TlsListenerTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.IO.Pipelines; using System.Net; using System.Net.Security; using System.Security.Authentication; @@ -70,10 +71,9 @@ await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions } [Fact] - public async Task TlsClientHelloBytesCallback_PreCanceledToken() + public async Task TlsClientHelloBytesCallback_UsesOptionsTimeout() { var tlsClientHelloCallbackInvoked = false; - var testContext = new TestServiceContext(LoggerFactory); await using (var server = new TestServer(context => Task.CompletedTask, testContext, @@ -81,6 +81,8 @@ public async Task TlsClientHelloBytesCallback_PreCanceledToken() { listenOptions.UseHttps(_x509Certificate2, httpsOptions => { + httpsOptions.HandshakeTimeout = TimeSpan.FromMilliseconds(1); + httpsOptions.TlsClientHelloBytesCallback = (connection, clientHelloBytes) => { Logger.LogDebug("[Received TlsClientHelloBytesCallback] Connection: {0}; TLS client hello buffer: {1}", connection.ConnectionId, clientHelloBytes.Length); @@ -95,71 +97,15 @@ public async Task TlsClientHelloBytesCallback_PreCanceledToken() { using (var sslStream = new SslStream(connection.Stream, false, (sender, cert, chain, errors) => true, null)) { - var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(1)); - var token = cancellationTokenSource.Token; + await connection.TransportConnection.Input.WriteAsync(new byte[] { 0x16 }); + var readResult = await connection.TransportConnection.Output.ReadAsync(); - await Assert.ThrowsAnyAsync(() => sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions - { - TargetHost = "localhost", - EnabledSslProtocols = SslProtocols.None - }, token)); + // HttpsConnectionMiddleware catches the exception, so we can only check the effects of the timeout here + Assert.True(readResult.IsCompleted); } } } Assert.False(tlsClientHelloCallbackInvoked); } - - [Fact] - public async Task TlsClientHelloBytesCallback_UsesOptionsTimeout() - { - var testContext = new TestServiceContext(LoggerFactory); - await using (var server = new TestServer(context => Task.CompletedTask, - testContext, - listenOptions => - { - listenOptions.UseHttps(_x509Certificate2, httpsOptions => - { - httpsOptions.HandshakeTimeout = TimeSpan.FromMilliseconds(1); - - httpsOptions.TlsClientHelloBytesCallback = (connection, clientHelloBytes) => - { - Logger.LogDebug("[Received TlsClientHelloBytesCallback] Connection: {0}; TLS client hello buffer: {1}", connection.ConnectionId, clientHelloBytes.Length); - Assert.True(clientHelloBytes.Length > 32); - Assert.NotNull(connection); - }; - }); - })) - { - using (var connection = server.CreateConnection()) - { - using (var sslStream = new SslStream(connection.Stream, false, (sender, cert, chain, errors) => true, null)) - { - try - { - await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions - { - TargetHost = "localhost", - EnabledSslProtocols = SslProtocols.None - }); - - var request = Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\nHost:\r\n\r\n"); - await sslStream.WriteAsync(request, 0, request.Length); - await sslStream.ReadAsync(new Memory(new byte[1024])); - } - catch (Exception ex) - when (ex is OperationCanceledException or TaskCanceledException // when cancellation comes from tls listener - or IOException // when the underlying stream is closed due to timeout - ) - { - // expected - } - catch (Exception ex) - { - ThrowsException.ForIncorrectExceptionType(typeof(OperationCanceledException), ex); - } - } - } - } - } } From 8c02e0edaddb8265709cba71ea67071fdd76827c Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Sat, 7 Jun 2025 00:14:58 +0200 Subject: [PATCH 5/5] remove sslStream --- .../test/InMemory.FunctionalTests/TlsListenerTests.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TlsListenerTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TlsListenerTests.cs index 02f49e84a2e6..f835e452ff21 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TlsListenerTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TlsListenerTests.cs @@ -95,14 +95,11 @@ public async Task TlsClientHelloBytesCallback_UsesOptionsTimeout() { using (var connection = server.CreateConnection()) { - using (var sslStream = new SslStream(connection.Stream, false, (sender, cert, chain, errors) => true, null)) - { - await connection.TransportConnection.Input.WriteAsync(new byte[] { 0x16 }); - var readResult = await connection.TransportConnection.Output.ReadAsync(); + await connection.TransportConnection.Input.WriteAsync(new byte[] { 0x16 }); + var readResult = await connection.TransportConnection.Output.ReadAsync(); - // HttpsConnectionMiddleware catches the exception, so we can only check the effects of the timeout here - Assert.True(readResult.IsCompleted); - } + // HttpsConnectionMiddleware catches the exception, so we can only check the effects of the timeout here + Assert.True(readResult.IsCompleted); } }