diff --git a/KestrelHttpServer.sln b/KestrelHttpServer.sln index 41323981f7..9bc9681d39 100644 --- a/KestrelHttpServer.sln +++ b/KestrelHttpServer.sln @@ -128,6 +128,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlaintextApp", "samples\Pla EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlatformBenchmarks", "benchmarkapps\PlatformBenchmarks\PlatformBenchmarks.csproj", "{7C24EAB8-57A9-4613-A8A6-4C21BB7D260D}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Kestrel.InMemory.FunctionalTests", "test\Kestrel.InMemory.FunctionalTests\Kestrel.InMemory.FunctionalTests.csproj", "{B5422347-E919-431D-9EF2-C352FFE4D6C1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Kestrel.Transport.Sockets.BindTests", "test\Kestrel.Transport.Sockets.BindTests\Kestrel.Transport.Sockets.BindTests.csproj", "{9254C3EB-196B-402F-A059-34FEA6140500}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Kestrel.Transport.Libuv.BindTests", "test\Kestrel.Transport.Libuv.BindTests\Kestrel.Transport.Libuv.BindTests.csproj", "{FB9C6B61-0A7B-4FFA-B772-A754316B262E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -378,6 +384,42 @@ Global {7C24EAB8-57A9-4613-A8A6-4C21BB7D260D}.Release|x64.Build.0 = Release|Any CPU {7C24EAB8-57A9-4613-A8A6-4C21BB7D260D}.Release|x86.ActiveCfg = Release|Any CPU {7C24EAB8-57A9-4613-A8A6-4C21BB7D260D}.Release|x86.Build.0 = Release|Any CPU + {B5422347-E919-431D-9EF2-C352FFE4D6C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B5422347-E919-431D-9EF2-C352FFE4D6C1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5422347-E919-431D-9EF2-C352FFE4D6C1}.Debug|x64.ActiveCfg = Debug|Any CPU + {B5422347-E919-431D-9EF2-C352FFE4D6C1}.Debug|x64.Build.0 = Debug|Any CPU + {B5422347-E919-431D-9EF2-C352FFE4D6C1}.Debug|x86.ActiveCfg = Debug|Any CPU + {B5422347-E919-431D-9EF2-C352FFE4D6C1}.Debug|x86.Build.0 = Debug|Any CPU + {B5422347-E919-431D-9EF2-C352FFE4D6C1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B5422347-E919-431D-9EF2-C352FFE4D6C1}.Release|Any CPU.Build.0 = Release|Any CPU + {B5422347-E919-431D-9EF2-C352FFE4D6C1}.Release|x64.ActiveCfg = Release|Any CPU + {B5422347-E919-431D-9EF2-C352FFE4D6C1}.Release|x64.Build.0 = Release|Any CPU + {B5422347-E919-431D-9EF2-C352FFE4D6C1}.Release|x86.ActiveCfg = Release|Any CPU + {B5422347-E919-431D-9EF2-C352FFE4D6C1}.Release|x86.Build.0 = Release|Any CPU + {9254C3EB-196B-402F-A059-34FEA6140500}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9254C3EB-196B-402F-A059-34FEA6140500}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9254C3EB-196B-402F-A059-34FEA6140500}.Debug|x64.ActiveCfg = Debug|Any CPU + {9254C3EB-196B-402F-A059-34FEA6140500}.Debug|x64.Build.0 = Debug|Any CPU + {9254C3EB-196B-402F-A059-34FEA6140500}.Debug|x86.ActiveCfg = Debug|Any CPU + {9254C3EB-196B-402F-A059-34FEA6140500}.Debug|x86.Build.0 = Debug|Any CPU + {9254C3EB-196B-402F-A059-34FEA6140500}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9254C3EB-196B-402F-A059-34FEA6140500}.Release|Any CPU.Build.0 = Release|Any CPU + {9254C3EB-196B-402F-A059-34FEA6140500}.Release|x64.ActiveCfg = Release|Any CPU + {9254C3EB-196B-402F-A059-34FEA6140500}.Release|x64.Build.0 = Release|Any CPU + {9254C3EB-196B-402F-A059-34FEA6140500}.Release|x86.ActiveCfg = Release|Any CPU + {9254C3EB-196B-402F-A059-34FEA6140500}.Release|x86.Build.0 = Release|Any CPU + {FB9C6B61-0A7B-4FFA-B772-A754316B262E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FB9C6B61-0A7B-4FFA-B772-A754316B262E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB9C6B61-0A7B-4FFA-B772-A754316B262E}.Debug|x64.ActiveCfg = Debug|Any CPU + {FB9C6B61-0A7B-4FFA-B772-A754316B262E}.Debug|x64.Build.0 = Debug|Any CPU + {FB9C6B61-0A7B-4FFA-B772-A754316B262E}.Debug|x86.ActiveCfg = Debug|Any CPU + {FB9C6B61-0A7B-4FFA-B772-A754316B262E}.Debug|x86.Build.0 = Debug|Any CPU + {FB9C6B61-0A7B-4FFA-B772-A754316B262E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FB9C6B61-0A7B-4FFA-B772-A754316B262E}.Release|Any CPU.Build.0 = Release|Any CPU + {FB9C6B61-0A7B-4FFA-B772-A754316B262E}.Release|x64.ActiveCfg = Release|Any CPU + {FB9C6B61-0A7B-4FFA-B772-A754316B262E}.Release|x64.Build.0 = Release|Any CPU + {FB9C6B61-0A7B-4FFA-B772-A754316B262E}.Release|x86.ActiveCfg = Release|Any CPU + {FB9C6B61-0A7B-4FFA-B772-A754316B262E}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -405,6 +447,9 @@ Global {A7994A41-CAF8-47A7-8975-F101F75B5BC1} = {8A3D00B8-1CCF-4BE6-A060-11104CE2D9CE} {CE5523AE-6E38-4E20-998F-C64E02C5CC51} = {8A3D00B8-1CCF-4BE6-A060-11104CE2D9CE} {7C24EAB8-57A9-4613-A8A6-4C21BB7D260D} = {A95C3BE1-B850-4265-97A0-777ADCCD437F} + {B5422347-E919-431D-9EF2-C352FFE4D6C1} = {D3273454-EA07-41D2-BF0B-FCC3675C2483} + {9254C3EB-196B-402F-A059-34FEA6140500} = {D3273454-EA07-41D2-BF0B-FCC3675C2483} + {FB9C6B61-0A7B-4FFA-B772-A754316B262E} = {D3273454-EA07-41D2-BF0B-FCC3675C2483} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2D10D020-6770-47CA-BB8D-2C23FE3AE071} diff --git a/src/Kestrel.Core/HttpsConnectionAdapterOptions.cs b/src/Kestrel.Core/HttpsConnectionAdapterOptions.cs index cf6bd88236..2e2059f962 100644 --- a/src/Kestrel.Core/HttpsConnectionAdapterOptions.cs +++ b/src/Kestrel.Core/HttpsConnectionAdapterOptions.cs @@ -90,5 +90,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https _handshakeTimeout = value != Timeout.InfiniteTimeSpan ? value : TimeSpan.MaxValue; } } + + // For testing + internal Action OnHandshakeStarted; } } diff --git a/src/Kestrel.Core/Internal/HttpsConnectionAdapter.cs b/src/Kestrel.Core/Internal/HttpsConnectionAdapter.cs index 4eaeffff5c..28c729b8d9 100644 --- a/src/Kestrel.Core/Internal/HttpsConnectionAdapter.cs +++ b/src/Kestrel.Core/Internal/HttpsConnectionAdapter.cs @@ -127,6 +127,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal var timeoutFeature = context.Features.Get(); timeoutFeature.SetTimeout(_options.HandshakeTimeout); + _options.OnHandshakeStarted?.Invoke(); + try { #if NETCOREAPP2_1 diff --git a/src/Kestrel.Core/Internal/ServiceContext.cs b/src/Kestrel.Core/Internal/ServiceContext.cs index 1020a6fdbd..1ca1beb237 100644 --- a/src/Kestrel.Core/Internal/ServiceContext.cs +++ b/src/Kestrel.Core/Internal/ServiceContext.cs @@ -21,6 +21,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal public HttpConnectionManager ConnectionManager { get; set; } + public Heartbeat Heartbeat { get; set; } + public KestrelServerOptions ServerOptions { get; set; } } } diff --git a/src/Kestrel.Core/KestrelServer.cs b/src/Kestrel.Core/KestrelServer.cs index 9caf8d0505..a6cea3cc0f 100644 --- a/src/Kestrel.Core/KestrelServer.cs +++ b/src/Kestrel.Core/KestrelServer.cs @@ -21,7 +21,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core public class KestrelServer : IServer { private readonly List _transports = new List(); - private readonly Heartbeat _heartbeat; private readonly IServerAddressesFeature _serverAddresses; private readonly ITransportFactory _transportFactory; @@ -47,13 +46,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core _transportFactory = transportFactory; ServiceContext = serviceContext; - var httpHeartbeatManager = new HttpHeartbeatManager(serviceContext.ConnectionManager); - _heartbeat = new Heartbeat( - new IHeartbeatHandler[] { serviceContext.DateHeaderValueManager, httpHeartbeatManager }, - serviceContext.SystemClock, - DebuggerWrapper.Singleton, - Trace); - Features = new FeatureCollection(); _serverAddresses = new ServerAddressesFeature(); Features.Set(_serverAddresses); @@ -82,6 +74,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core var systemClock = new SystemClock(); var dateHeaderValueManager = new DateHeaderValueManager(systemClock); + var httpHeartbeatManager = new HttpHeartbeatManager(connectionManager); + var heartbeat = new Heartbeat( + new IHeartbeatHandler[] { dateHeaderValueManager, httpHeartbeatManager }, + systemClock, + DebuggerWrapper.Singleton, + trace); + // TODO: This logic will eventually move into the IConnectionHandler and off // the service context once we get to https://github.com/aspnet/KestrelHttpServer/issues/1662 PipeScheduler scheduler = null; @@ -106,7 +105,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core SystemClock = systemClock, DateHeaderValueManager = dateHeaderValueManager, ConnectionManager = connectionManager, - ServerOptions = serverOptions + Heartbeat = heartbeat, + ServerOptions = serverOptions, }; } @@ -137,7 +137,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core throw new InvalidOperationException(CoreStrings.ServerAlreadyStarted); } _hasStarted = true; - _heartbeat.Start(); + + ServiceContext.Heartbeat?.Start(); async Task OnBind(ListenOptions endpoint) { @@ -203,7 +204,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core } await Task.WhenAll(tasks).ConfigureAwait(false); - _heartbeat.Dispose(); + ServiceContext.Heartbeat?.Dispose(); } catch (Exception ex) { diff --git a/src/Kestrel.Core/Properties/AssemblyInfo.cs b/src/Kestrel.Core/Properties/AssemblyInfo.cs index dd8570eefb..9e4800deed 100644 --- a/src/Kestrel.Core/Properties/AssemblyInfo.cs +++ b/src/Kestrel.Core/Properties/AssemblyInfo.cs @@ -6,6 +6,9 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Server.Kestrel.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Libuv.FunctionalTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Sockets.FunctionalTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("InMemory.FunctionalTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Sockets.BindTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Libuv.BindTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Server.Kestrel.Core.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Kestrel.Performance, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Kestrel.Transport.Abstractions/Properties/AssemblyInfo.cs b/src/Kestrel.Transport.Abstractions/Properties/AssemblyInfo.cs index a4f9a58d1e..7056daa197 100644 --- a/src/Kestrel.Transport.Abstractions/Properties/AssemblyInfo.cs +++ b/src/Kestrel.Transport.Abstractions/Properties/AssemblyInfo.cs @@ -6,3 +6,6 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Server.Kestrel.Core.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Sockets.FunctionalTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Libuv.FunctionalTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("InMemory.FunctionalTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Sockets.BindTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Libuv.BindTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Kestrel.Transport.Libuv/AssemblyInfo.cs b/src/Kestrel.Transport.Libuv/AssemblyInfo.cs index 29df5a481f..2d46c7a1e7 100644 --- a/src/Kestrel.Transport.Libuv/AssemblyInfo.cs +++ b/src/Kestrel.Transport.Libuv/AssemblyInfo.cs @@ -4,3 +4,4 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Libuv.FunctionalTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Libuv.BindTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Kestrel.Transport.Sockets/AssemblyInfo.cs b/src/Kestrel.Transport.Sockets/AssemblyInfo.cs index 89edc0376a..effc0ed2ca 100644 --- a/src/Kestrel.Transport.Sockets/AssemblyInfo.cs +++ b/src/Kestrel.Transport.Sockets/AssemblyInfo.cs @@ -4,3 +4,4 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Sockets.FunctionalTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Sockets.BindTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/test/Kestrel.Core.Tests/ConnectionDispatcherTests.cs b/test/Kestrel.Core.Tests/ConnectionDispatcherTests.cs index cbf4c4b88b..dbf838f562 100644 --- a/test/Kestrel.Core.Tests/ConnectionDispatcherTests.cs +++ b/test/Kestrel.Core.Tests/ConnectionDispatcherTests.cs @@ -49,7 +49,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests public async Task OnConnectionCompletesTransportPipesAfterReturning() { var serviceContext = new TestServiceContext(); - var tcs = new TaskCompletionSource(); var dispatcher = new ConnectionDispatcher(serviceContext, _ => Task.CompletedTask); var mockConnection = new Mock(); diff --git a/test/Kestrel.Core.Tests/Kestrel.Core.Tests.csproj b/test/Kestrel.Core.Tests/Kestrel.Core.Tests.csproj index 2074ed6526..a8e44fa8c8 100644 --- a/test/Kestrel.Core.Tests/Kestrel.Core.Tests.csproj +++ b/test/Kestrel.Core.Tests/Kestrel.Core.Tests.csproj @@ -8,9 +8,9 @@ - + + - diff --git a/test/Kestrel.FunctionalTests/KeepAliveTimeoutTests.cs b/test/Kestrel.FunctionalTests/KeepAliveTimeoutTests.cs deleted file mode 100644 index 90c2b77cfa..0000000000 --- a/test/Kestrel.FunctionalTests/KeepAliveTimeoutTests.cs +++ /dev/null @@ -1,256 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -#if !INNER_LOOP -using System; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; -using Microsoft.AspNetCore.Testing; -using Microsoft.Extensions.Logging.Testing; -using Xunit; - -namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests -{ - public class KeepAliveTimeoutTests : LoggedTest - { - private static readonly TimeSpan _keepAliveTimeout = TimeSpan.FromSeconds(10); - private static readonly TimeSpan _longDelay = TimeSpan.FromSeconds(30); - private static readonly TimeSpan _shortDelay = TimeSpan.FromSeconds(_longDelay.TotalSeconds / 10); - - [Fact] - public Task TestKeepAliveTimeout() - { - // Delays in these tests cannot be much longer than expected. - // Call Task.Run() to get rid of Xunit's synchronization context, - // otherwise it can cause unexpectedly longer delays when multiple tests - // are running in parallel. These tests becomes flaky on slower - // hardware because the continuations for the delay tasks might take too long to be - // scheduled if running on Xunit's synchronization context. - return Task.Run(async () => - { - var longRunningCancellationTokenSource = new CancellationTokenSource(); - var upgradeCancellationTokenSource = new CancellationTokenSource(); - - using (var server = CreateServer(longRunningCancellationTokenSource.Token, upgradeCancellationTokenSource.Token)) - { - var tasks = new[] - { - ConnectionClosedWhenKeepAliveTimeoutExpires(server), - ConnectionKeptAliveBetweenRequests(server), - ConnectionNotTimedOutWhileRequestBeingSent(server), - ConnectionNotTimedOutWhileAppIsRunning(server, longRunningCancellationTokenSource), - ConnectionTimesOutWhenOpenedButNoRequestSent(server), - KeepAliveTimeoutDoesNotApplyToUpgradedConnections(server, upgradeCancellationTokenSource) - }; - - await Task.WhenAll(tasks); - } - }); - } - - private async Task ConnectionClosedWhenKeepAliveTimeoutExpires(TestServer server) - { - using (var connection = server.CreateConnection()) - { - await connection.Send( - "GET / HTTP/1.1", - "Host:", - "", - ""); - await ReceiveResponse(connection); - await connection.WaitForConnectionClose().TimeoutAfter(_longDelay); - } - } - - private async Task ConnectionKeptAliveBetweenRequests(TestServer server) - { - using (var connection = server.CreateConnection()) - { - for (var i = 0; i < 10; i++) - { - await connection.Send( - "GET / HTTP/1.1", - "Host:", - "", - ""); - - // Don't change this to Task.Delay. See https://github.com/aspnet/KestrelHttpServer/issues/1684#issuecomment-330285740. - Thread.Sleep(_shortDelay); - } - - for (var i = 0; i < 10; i++) - { - await ReceiveResponse(connection); - } - } - } - - private async Task ConnectionNotTimedOutWhileRequestBeingSent(TestServer server) - { - using (var connection = server.CreateConnection()) - { - var cts = new CancellationTokenSource(); - cts.CancelAfter(_longDelay); - - await connection.Send( - "POST /consume HTTP/1.1", - "Host:", - "Transfer-Encoding: chunked", - "", - ""); - - while (!cts.IsCancellationRequested) - { - await connection.Send( - "1", - "a", - ""); - await Task.Delay(_shortDelay); - } - - await connection.Send( - "0", - "", - ""); - await ReceiveResponse(connection); - } - } - - private async Task ConnectionNotTimedOutWhileAppIsRunning(TestServer server, CancellationTokenSource cts) - { - using (var connection = server.CreateConnection()) - { - await connection.Send( - "GET /longrunning HTTP/1.1", - "Host:", - "", - ""); - cts.CancelAfter(_longDelay); - - while (!cts.IsCancellationRequested) - { - await Task.Delay(1000); - } - - await ReceiveResponse(connection); - - await connection.Send( - "GET / HTTP/1.1", - "Host:", - "", - ""); - await ReceiveResponse(connection); - } - } - - private async Task ConnectionTimesOutWhenOpenedButNoRequestSent(TestServer server) - { - using (var connection = server.CreateConnection()) - { - await Task.Delay(_longDelay); - await connection.WaitForConnectionClose().TimeoutAfter(_longDelay); - } - } - - private async Task KeepAliveTimeoutDoesNotApplyToUpgradedConnections(TestServer server, CancellationTokenSource cts) - { - using (var connection = server.CreateConnection()) - { - await connection.Send( - "GET /upgrade HTTP/1.1", - "Host:", - "Connection: Upgrade", - "", - ""); - await connection.Receive( - "HTTP/1.1 101 Switching Protocols", - "Connection: Upgrade", - ""); - await connection.ReceiveStartsWith("Date: "); - await connection.Receive( - "", - ""); - cts.CancelAfter(_longDelay); - - while (!cts.IsCancellationRequested) - { - await Task.Delay(1000); - } - - await connection.Receive("hello, world"); - } - } - - private TestServer CreateServer(CancellationToken longRunningCt, CancellationToken upgradeCt) - { - return new TestServer(httpContext => App(httpContext, longRunningCt, upgradeCt), new TestServiceContext(LoggerFactory) - { - // Use real SystemClock so timeouts trigger. - SystemClock = new SystemClock(), - ServerOptions = - { - AddServerHeader = false, - Limits = - { - KeepAliveTimeout = _keepAliveTimeout, - MinRequestBodyDataRate = null - } - } - }); - } - - private async Task App(HttpContext httpContext, CancellationToken longRunningCt, CancellationToken upgradeCt) - { - var ct = httpContext.RequestAborted; - var responseStream = httpContext.Response.Body; - var responseBytes = Encoding.ASCII.GetBytes("hello, world"); - - if (httpContext.Request.Path == "/longrunning") - { - while (!longRunningCt.IsCancellationRequested) - { - await Task.Delay(1000); - } - } - else if (httpContext.Request.Path == "/upgrade") - { - using (var stream = await httpContext.Features.Get().UpgradeAsync()) - { - while (!upgradeCt.IsCancellationRequested) - { - await Task.Delay(_longDelay); - } - - responseStream = stream; - } - } - else if (httpContext.Request.Path == "/consume") - { - var buffer = new byte[1024]; - while (await httpContext.Request.Body.ReadAsync(buffer, 0, buffer.Length) > 0) ; - } - - await responseStream.WriteAsync(responseBytes, 0, responseBytes.Length); - } - - private async Task ReceiveResponse(TestConnection connection) - { - await connection.Receive( - "HTTP/1.1 200 OK", - ""); - await connection.ReceiveStartsWith("Date: "); - await connection.Receive( - "Transfer-Encoding: chunked", - "", - "c", - "hello, world", - "0", - "", - ""); - } - } -} -#endif \ No newline at end of file diff --git a/test/Kestrel.FunctionalTests/LoggingConnectionAdapterTests.cs b/test/Kestrel.FunctionalTests/LoggingConnectionAdapterTests.cs deleted file mode 100644 index 728d9a16fb..0000000000 --- a/test/Kestrel.FunctionalTests/LoggingConnectionAdapterTests.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Net; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Testing; -using Microsoft.Extensions.Logging.Testing; -using Xunit; - -namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests -{ - public class LoggingConnectionAdapterTests : LoggedTest - { - [Fact] - public async Task LoggingConnectionAdapterCanBeAddedBeforeAndAfterHttpsAdapter() - { - var host = TransportSelector.GetWebHostBuilder() - .ConfigureServices(AddTestLogging) - .UseKestrel(options => - { - options.Listen(new IPEndPoint(IPAddress.Loopback, 0), listenOptions => - { - listenOptions.UseConnectionLogging(); - listenOptions.UseHttps(TestResources.TestCertificatePath, "testPassword"); - listenOptions.UseConnectionLogging(); - }); - }) - .Configure(app => - { - app.Run(context => - { - context.Response.ContentLength = 12; - return context.Response.WriteAsync("Hello World!"); - }); - }) - .Build(); - - using (host) - { - await host.StartAsync(); - - var response = await HttpClientSlim.GetStringAsync($"https://localhost:{host.GetPort()}/", validateCertificate: false) - .DefaultTimeout(); - - Assert.Equal("Hello World!", response); - } - } - } -} diff --git a/test/Kestrel.FunctionalTests/RequestHeadersTimeoutTests.cs b/test/Kestrel.FunctionalTests/RequestHeadersTimeoutTests.cs deleted file mode 100644 index 57fe8fc274..0000000000 --- a/test/Kestrel.FunctionalTests/RequestHeadersTimeoutTests.cs +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.IO; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; -using Microsoft.AspNetCore.Testing; -using Microsoft.Extensions.Logging.Testing; -using Xunit; - -namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests -{ - public class RequestHeadersTimeoutTests : LoggedTest - { - private static readonly TimeSpan RequestHeadersTimeout = TimeSpan.FromSeconds(10); - private static readonly TimeSpan LongDelay = TimeSpan.FromSeconds(30); - private static readonly TimeSpan ShortDelay = TimeSpan.FromSeconds(LongDelay.TotalSeconds / 10); - - [Fact] - public async Task TestRequestHeadersTimeout() - { - using (var server = CreateServer()) - { - var tasks = new[] - { - ConnectionAbortedWhenRequestHeadersNotReceivedInTime(server, "Host:\r\n"), - ConnectionAbortedWhenRequestHeadersNotReceivedInTime(server, "Host:\r\nContent-Length: 1\r\n"), - ConnectionAbortedWhenRequestHeadersNotReceivedInTime(server, "Host:\r\nContent-Length: 1\r\n\r"), - RequestHeadersTimeoutCanceledAfterHeadersReceived(server), - ConnectionAbortedWhenRequestLineNotReceivedInTime(server, "P"), - ConnectionAbortedWhenRequestLineNotReceivedInTime(server, "POST / HTTP/1.1\r"), - TimeoutNotResetOnEachRequestLineCharacterReceived(server) - }; - - await Task.WhenAll(tasks); - } - } - - private async Task ConnectionAbortedWhenRequestHeadersNotReceivedInTime(TestServer server, string headers) - { - using (var connection = server.CreateConnection()) - { - await connection.Send( - "GET / HTTP/1.1", - headers); - await ReceiveTimeoutResponse(connection); - } - } - - private async Task RequestHeadersTimeoutCanceledAfterHeadersReceived(TestServer server) - { - using (var connection = server.CreateConnection()) - { - await connection.Send( - "POST / HTTP/1.1", - "Host:", - "Content-Length: 1", - "", - ""); - await Task.Delay(RequestHeadersTimeout); - await connection.Send( - "a"); - await ReceiveResponse(connection); - } - } - - private async Task ConnectionAbortedWhenRequestLineNotReceivedInTime(TestServer server, string requestLine) - { - using (var connection = server.CreateConnection()) - { - await connection.Send(requestLine); - await ReceiveTimeoutResponse(connection); - } - } - - private async Task TimeoutNotResetOnEachRequestLineCharacterReceived(TestServer server) - { - using (var connection = server.CreateConnection()) - { - await Assert.ThrowsAsync(async () => - { - foreach (var ch in "POST / HTTP/1.1\r\nHost:\r\n\r\n") - { - await connection.Send(ch.ToString()); - await Task.Delay(ShortDelay); - } - }); - } - } - - private TestServer CreateServer() - { - return new TestServer(async httpContext => - { - await httpContext.Request.Body.ReadAsync(new byte[1], 0, 1); - await httpContext.Response.WriteAsync("hello, world"); - }, - new TestServiceContext(LoggerFactory) - { - // Use real SystemClock so timeouts trigger. - SystemClock = new SystemClock(), - ServerOptions = - { - AddServerHeader = false, - Limits = - { - RequestHeadersTimeout = RequestHeadersTimeout, - MinRequestBodyDataRate = null - } - } - }); - } - - private async Task ReceiveResponse(TestConnection connection) - { - await connection.Receive( - "HTTP/1.1 200 OK", - ""); - await connection.ReceiveStartsWith("Date: "); - await connection.Receive( - "Transfer-Encoding: chunked", - "", - "c", - "hello, world", - "0", - "", - ""); - } - - private async Task ReceiveTimeoutResponse(TestConnection connection) - { - await connection.Receive( - "HTTP/1.1 408 Request Timeout", - "Connection: close", - ""); - await connection.ReceiveStartsWith("Date: "); - await connection.ReceiveForcedEnd( - "Content-Length: 0", - "", - ""); - } - } -} \ No newline at end of file diff --git a/test/Kestrel.FunctionalTests/RequestTests.cs b/test/Kestrel.FunctionalTests/RequestTests.cs deleted file mode 100644 index e4ff7237bc..0000000000 --- a/test/Kestrel.FunctionalTests/RequestTests.cs +++ /dev/null @@ -1,1923 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Concurrent; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.IO.Pipelines; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Sockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Connections; -using Microsoft.AspNetCore.Connections.Features; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Server.Kestrel.Core; -using Microsoft.AspNetCore.Server.Kestrel.Core.Features; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; -using Microsoft.AspNetCore.Testing; -using Microsoft.AspNetCore.Testing.xunit; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Testing; -using Moq; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Xunit; - -namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests -{ - public class RequestTests : LoggedTest - { - private const int _connectionStartedEventId = 1; - private const int _connectionResetEventId = 19; - private static readonly int _semaphoreWaitTimeout = Debugger.IsAttached ? 10000 : 2500; - - public static TheoryData ConnectionAdapterData => new TheoryData - { - new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)), - new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) - { - ConnectionAdapters = { new PassThroughConnectionAdapter() } - } - }; - - [Theory] - [InlineData(10 * 1024 * 1024, true)] - // In the following dataset, send at least 2GB. - // Never change to a lower value, otherwise regression testing for - // https://github.com/aspnet/KestrelHttpServer/issues/520#issuecomment-188591242 - // will be lost. - [InlineData((long)int.MaxValue + 1, false)] - public void LargeUpload(long contentLength, bool checkBytes) - { - const int bufferLength = 1024 * 1024; - Assert.True(contentLength % bufferLength == 0, $"{nameof(contentLength)} sent must be evenly divisible by {bufferLength}."); - Assert.True(bufferLength % 256 == 0, $"{nameof(bufferLength)} must be evenly divisible by 256"); - - var builder = TransportSelector.GetWebHostBuilder() - .ConfigureServices(AddTestLogging) - .UseKestrel(options => - { - options.Limits.MaxRequestBodySize = contentLength; - options.Limits.MinRequestBodyDataRate = null; - }) - .UseUrls("http://127.0.0.1:0/") - .Configure(app => - { - app.Run(async context => - { - // Read the full request body - long total = 0; - var receivedBytes = new byte[bufferLength]; - var received = 0; - while ((received = await context.Request.Body.ReadAsync(receivedBytes, 0, receivedBytes.Length)) > 0) - { - if (checkBytes) - { - for (var i = 0; i < received; i++) - { - // Do not use Assert.Equal here, it is to slow for this hot path - Assert.True((byte)((total + i) % 256) == receivedBytes[i], "Data received is incorrect"); - } - } - - total += received; - } - - await context.Response.WriteAsync(total.ToString(CultureInfo.InvariantCulture)); - }); - }); - - using (var host = builder.Build()) - { - host.Start(); - - using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) - { - socket.Connect(new IPEndPoint(IPAddress.Loopback, host.GetPort())); - socket.Send(Encoding.ASCII.GetBytes($"POST / HTTP/1.0\r\nContent-Length: {contentLength}\r\n\r\n")); - - var contentBytes = new byte[bufferLength]; - - if (checkBytes) - { - for (var i = 0; i < contentBytes.Length; i++) - { - contentBytes[i] = (byte)i; - } - } - - for (var i = 0; i < contentLength / contentBytes.Length; i++) - { - socket.Send(contentBytes); - } - - var response = new StringBuilder(); - var responseBytes = new byte[4096]; - var received = 0; - while ((received = socket.Receive(responseBytes)) > 0) - { - response.Append(Encoding.ASCII.GetString(responseBytes, 0, received)); - } - - Assert.Contains(contentLength.ToString(CultureInfo.InvariantCulture), response.ToString()); - } - } - } - - [Fact] - public Task RemoteIPv4Address() - { - return TestRemoteIPAddress("127.0.0.1", "127.0.0.1", "127.0.0.1"); - } - - [ConditionalFact] - [IPv6SupportedCondition] - public Task RemoteIPv6Address() - { - return TestRemoteIPAddress("[::1]", "[::1]", "::1"); - } - - [Fact] - public async Task DoesNotHangOnConnectionCloseRequest() - { - var builder = TransportSelector.GetWebHostBuilder() - .UseKestrel() - .UseUrls("http://127.0.0.1:0") - .ConfigureServices(AddTestLogging) - .Configure(app => - { - app.Run(async context => - { - await context.Response.WriteAsync("hello, world"); - }); - }); - - using (var host = builder.Build()) - using (var client = new HttpClient()) - { - host.Start(); - - client.DefaultRequestHeaders.Connection.Clear(); - client.DefaultRequestHeaders.Connection.Add("close"); - - var response = await client.GetAsync($"http://127.0.0.1:{host.GetPort()}/"); - response.EnsureSuccessStatusCode(); - } - } - - [Fact] - public async Task StreamsAreNotPersistedAcrossRequests() - { - var requestBodyPersisted = false; - var responseBodyPersisted = false; - - var builder = TransportSelector.GetWebHostBuilder() - .UseKestrel() - .UseUrls("http://127.0.0.1:0") - .ConfigureServices(AddTestLogging) - .Configure(app => - { - app.Run(async context => - { - if (context.Request.Body is MemoryStream) - { - requestBodyPersisted = true; - } - - if (context.Response.Body is MemoryStream) - { - responseBodyPersisted = true; - } - - context.Request.Body = new MemoryStream(); - context.Response.Body = new MemoryStream(); - - await context.Response.WriteAsync("hello, world"); - }); - }); - - using (var host = builder.Build()) - { - host.Start(); - - using (var client = new HttpClient { BaseAddress = new Uri($"http://127.0.0.1:{host.GetPort()}") }) - { - await client.GetAsync("/"); - await client.GetAsync("/"); - - Assert.False(requestBodyPersisted); - Assert.False(responseBodyPersisted); - } - } - } - - [Fact] - public void CanUpgradeRequestWithConnectionKeepAliveUpgradeHeader() - { - var dataRead = false; - var builder = TransportSelector.GetWebHostBuilder() - .UseKestrel() - .UseUrls("http://127.0.0.1:0") - .ConfigureServices(AddTestLogging) - .Configure(app => - { - app.Run(async context => - { - var stream = await context.Features.Get().UpgradeAsync(); - var data = new byte[3]; - var bytesRead = 0; - - while (bytesRead < 3) - { - bytesRead += await stream.ReadAsync(data, bytesRead, data.Length - bytesRead); - } - - dataRead = Encoding.ASCII.GetString(data, 0, 3) == "abc"; - }); - }); - - using (var host = builder.Build()) - { - host.Start(); - - using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) - { - socket.Connect(new IPEndPoint(IPAddress.Loopback, host.GetPort())); - socket.Send(Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\nHost:\r\nConnection: keep-alive, upgrade\r\n\r\n")); - socket.Send(Encoding.ASCII.GetBytes("abc")); - - while (socket.Receive(new byte[1024]) > 0) ; - } - } - - Assert.True(dataRead); - } - - [Fact] - public async Task ConnectionResetPriorToRequestIsLoggedAsDebug() - { - var connectionStarted = new SemaphoreSlim(0); - var connectionReset = new SemaphoreSlim(0); - var loggedHigherThanDebug = false; - - var mockLogger = new Mock(); - mockLogger - .Setup(logger => logger.IsEnabled(It.IsAny())) - .Returns(true); - mockLogger - .Setup(logger => logger.Log(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())) - .Callback>((logLevel, eventId, state, exception, formatter) => - { - Logger.Log(logLevel, eventId, state, exception, formatter); - if (eventId.Id == _connectionStartedEventId) - { - connectionStarted.Release(); - } - else if (eventId.Id == _connectionResetEventId) - { - connectionReset.Release(); - } - - if (logLevel > LogLevel.Debug) - { - loggedHigherThanDebug = true; - } - }); - - var mockLoggerFactory = new Mock(); - mockLoggerFactory - .Setup(factory => factory.CreateLogger(It.IsAny())) - .Returns(Logger); - mockLoggerFactory - .Setup(factory => factory.CreateLogger(It.IsIn("Microsoft.AspNetCore.Server.Kestrel", - "Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv", - "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets"))) - .Returns(mockLogger.Object); - - using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(mockLoggerFactory.Object))) - { - using (var connection = server.CreateConnection()) - { - // Wait until connection is established - Assert.True(await connectionStarted.WaitAsync(TestConstants.DefaultTimeout)); - - connection.Reset(); - } - - // If the reset is correctly logged as Debug, the wait below should complete shortly. - // This check MUST come before disposing the server, otherwise there's a race where the RST - // is still in flight when the connection is aborted, leading to the reset never being received - // and therefore not logged. - Assert.True(await connectionReset.WaitAsync(TestConstants.DefaultTimeout)); - } - - Assert.False(loggedHigherThanDebug); - } - - [Fact] - public async Task ConnectionResetBetweenRequestsIsLoggedAsDebug() - { - var connectionReset = new SemaphoreSlim(0); - var loggedHigherThanDebug = false; - - var mockLogger = new Mock(); - mockLogger - .Setup(logger => logger.IsEnabled(It.IsAny())) - .Returns(true); - mockLogger - .Setup(logger => logger.Log(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())) - .Callback>((logLevel, eventId, state, exception, formatter) => - { - Logger.Log(logLevel, eventId, state, exception, formatter); - if (eventId.Id == _connectionResetEventId) - { - connectionReset.Release(); - } - - if (logLevel > LogLevel.Debug) - { - loggedHigherThanDebug = true; - } - }); - - var mockLoggerFactory = new Mock(); - mockLoggerFactory - .Setup(factory => factory.CreateLogger(It.IsAny())) - .Returns(Logger); - mockLoggerFactory - .Setup(factory => factory.CreateLogger(It.IsIn("Microsoft.AspNetCore.Server.Kestrel", - "Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv", - "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets"))) - .Returns(mockLogger.Object); - - using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(mockLoggerFactory.Object))) - { - using (var connection = server.CreateConnection()) - { - await connection.Send( - "GET / HTTP/1.1", - "Host:", - "", - ""); - - // Make sure the response is fully received, so a write failure (e.g. EPIPE) doesn't cause - // a more critical log message. - await connection.Receive( - "HTTP/1.1 200 OK", - $"Date: {server.Context.DateHeaderValue}", - "Content-Length: 0", - "", - ""); - - connection.Reset(); - // Force a reset - } - - // If the reset is correctly logged as Debug, the wait below should complete shortly. - // This check MUST come before disposing the server, otherwise there's a race where the RST - // is still in flight when the connection is aborted, leading to the reset never being received - // and therefore not logged. - Assert.True(await connectionReset.WaitAsync(TestConstants.DefaultTimeout)); - } - - Assert.False(loggedHigherThanDebug); - } - - [Fact] - public async Task ConnectionResetMidRequestIsLoggedAsDebug() - { - var requestStarted = new SemaphoreSlim(0); - var connectionReset = new SemaphoreSlim(0); - var connectionClosing = new SemaphoreSlim(0); - var loggedHigherThanDebug = false; - - var mockLogger = new Mock(); - mockLogger - .Setup(logger => logger.IsEnabled(It.IsAny())) - .Returns(true); - mockLogger - .Setup(logger => logger.Log(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())) - .Callback>((logLevel, eventId, state, exception, formatter) => - { - Logger.Log(logLevel, eventId, state, exception, formatter); - var log = $"Log {logLevel}[{eventId}]: {formatter(state, exception)} {exception}"; - TestOutputHelper.WriteLine(log); - - if (eventId.Id == _connectionResetEventId) - { - connectionReset.Release(); - } - - if (logLevel > LogLevel.Debug) - { - loggedHigherThanDebug = true; - } - }); - - var mockLoggerFactory = new Mock(); - mockLoggerFactory - .Setup(factory => factory.CreateLogger(It.IsAny())) - .Returns(Logger); - mockLoggerFactory - .Setup(factory => factory.CreateLogger(It.IsIn("Microsoft.AspNetCore.Server.Kestrel", - "Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv", - "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets"))) - .Returns(mockLogger.Object); - - using (var server = new TestServer(async context => - { - requestStarted.Release(); - await connectionClosing.WaitAsync(); - }, - new TestServiceContext(mockLoggerFactory.Object))) - { - using (var connection = server.CreateConnection()) - { - await connection.SendEmptyGet(); - - // Wait until connection is established - Assert.True(await requestStarted.WaitAsync(TestConstants.DefaultTimeout), "request should have started"); - - connection.Reset(); - } - - // If the reset is correctly logged as Debug, the wait below should complete shortly. - // This check MUST come before disposing the server, otherwise there's a race where the RST - // is still in flight when the connection is aborted, leading to the reset never being received - // and therefore not logged. - Assert.True(await connectionReset.WaitAsync(TestConstants.DefaultTimeout), "Connection reset event should have been logged"); - connectionClosing.Release(); - } - - Assert.False(loggedHigherThanDebug, "Logged event should not have been higher than debug."); - } - - [Fact] - public async Task ThrowsOnReadAfterConnectionError() - { - var requestStarted = new SemaphoreSlim(0); - var connectionReset = new SemaphoreSlim(0); - var appDone = new SemaphoreSlim(0); - var expectedExceptionThrown = false; - - var builder = TransportSelector.GetWebHostBuilder() - .ConfigureServices(AddTestLogging) - .UseKestrel() - .UseUrls("http://127.0.0.1:0") - .Configure(app => app.Run(async context => - { - requestStarted.Release(); - Assert.True(await connectionReset.WaitAsync(_semaphoreWaitTimeout)); - - try - { - await context.Request.Body.ReadAsync(new byte[1], 0, 1); - } - catch (ConnectionResetException) - { - expectedExceptionThrown = true; - } - - appDone.Release(); - })); - - using (var host = builder.Build()) - { - host.Start(); - - using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) - { - socket.Connect(new IPEndPoint(IPAddress.Loopback, host.GetPort())); - socket.LingerState = new LingerOption(true, 0); - socket.Send(Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\nHost:\r\nContent-Length: 1\r\n\r\n")); - Assert.True(await requestStarted.WaitAsync(_semaphoreWaitTimeout)); - } - - connectionReset.Release(); - - Assert.True(await appDone.WaitAsync(_semaphoreWaitTimeout)); - Assert.True(expectedExceptionThrown); - } - } - - [Fact] - public async Task RequestAbortedTokenFiredOnClientFIN() - { - var appStarted = new SemaphoreSlim(0); - var requestAborted = new SemaphoreSlim(0); - var builder = TransportSelector.GetWebHostBuilder() - .UseKestrel() - .UseUrls("http://127.0.0.1:0") - .ConfigureServices(AddTestLogging) - .Configure(app => app.Run(async context => - { - appStarted.Release(); - - var token = context.RequestAborted; - token.Register(() => requestAborted.Release(2)); - await requestAborted.WaitAsync().DefaultTimeout(); - })); - - using (var host = builder.Build()) - { - host.Start(); - - using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) - { - socket.Connect(new IPEndPoint(IPAddress.Loopback, host.GetPort())); - socket.Send(Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\nHost:\r\n\r\n")); - await appStarted.WaitAsync(); - socket.Shutdown(SocketShutdown.Send); - await requestAborted.WaitAsync().DefaultTimeout(); - } - } - } - - [Fact] - public void AbortingTheConnectionSendsFIN() - { - var builder = TransportSelector.GetWebHostBuilder() - .UseKestrel() - .UseUrls("http://127.0.0.1:0") - .ConfigureServices(AddTestLogging) - .Configure(app => app.Run(context => - { - context.Abort(); - return Task.CompletedTask; - })); - - using (var host = builder.Build()) - { - host.Start(); - - using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) - { - socket.Connect(new IPEndPoint(IPAddress.Loopback, host.GetPort())); - socket.Send(Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\nHost:\r\n\r\n")); - int result = socket.Receive(new byte[32]); - Assert.Equal(0, result); - } - } - } - - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task ConnectionClosedTokenFiresOnClientFIN(ListenOptions listenOptions) - { - var testContext = new TestServiceContext(LoggerFactory); - var appStartedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var connectionClosedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - using (var server = new TestServer(context => - { - appStartedTcs.SetResult(null); - - var connectionLifetimeFeature = context.Features.Get(); - connectionLifetimeFeature.ConnectionClosed.Register(() => connectionClosedTcs.SetResult(null)); - - return Task.CompletedTask; - }, testContext, listenOptions)) - { - using (var connection = server.CreateConnection()) - { - await connection.Send( - "GET / HTTP/1.1", - "Host:", - "", - ""); - - await appStartedTcs.Task.DefaultTimeout(); - - connection.Shutdown(SocketShutdown.Send); - - await connectionClosedTcs.Task.DefaultTimeout(); - } - } - } - - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task ConnectionClosedTokenFiresOnServerFIN(ListenOptions listenOptions) - { - var testContext = new TestServiceContext(LoggerFactory); - var connectionClosedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - using (var server = new TestServer(context => - { - var connectionLifetimeFeature = context.Features.Get(); - connectionLifetimeFeature.ConnectionClosed.Register(() => connectionClosedTcs.SetResult(null)); - - return Task.CompletedTask; - }, testContext, listenOptions)) - { - using (var connection = server.CreateConnection()) - { - await connection.Send( - "GET / HTTP/1.1", - "Host:", - "Connection: close", - "", - ""); - - await connectionClosedTcs.Task.DefaultTimeout(); - - await connection.ReceiveEnd($"HTTP/1.1 200 OK", - "Connection: close", - $"Date: {server.Context.DateHeaderValue}", - "Content-Length: 0", - "", - ""); - } - } - } - - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task ConnectionClosedTokenFiresOnServerAbort(ListenOptions listenOptions) - { - var testContext = new TestServiceContext(LoggerFactory); - var connectionClosedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - using (var server = new TestServer(context => - { - var connectionLifetimeFeature = context.Features.Get(); - connectionLifetimeFeature.ConnectionClosed.Register(() => connectionClosedTcs.SetResult(null)); - - context.Abort(); - - return Task.CompletedTask; - }, testContext, listenOptions)) - { - using (var connection = server.CreateConnection()) - { - await connection.Send( - "GET / HTTP/1.1", - "Host:", - "", - ""); - - await connectionClosedTcs.Task.DefaultTimeout(); - await connection.ReceiveForcedEnd(); - } - } - } - - [Theory] - [InlineData("http://localhost/abs/path", "/abs/path", null)] - [InlineData("https://localhost/abs/path", "/abs/path", null)] // handles mismatch scheme - [InlineData("https://localhost:22/abs/path", "/abs/path", null)] // handles mismatched ports - [InlineData("https://differenthost/abs/path", "/abs/path", null)] // handles mismatched hostname - [InlineData("http://localhost/", "/", null)] - [InlineData("http://root@contoso.com/path", "/path", null)] - [InlineData("http://root:password@contoso.com/path", "/path", null)] - [InlineData("https://localhost/", "/", null)] - [InlineData("http://localhost", "/", null)] - [InlineData("http://127.0.0.1/", "/", null)] - [InlineData("http://[::1]/", "/", null)] - [InlineData("http://[::1]:8080/", "/", null)] - [InlineData("http://localhost?q=123&w=xyz", "/", "123")] - [InlineData("http://localhost/?q=123&w=xyz", "/", "123")] - [InlineData("http://localhost/path?q=123&w=xyz", "/path", "123")] - [InlineData("http://localhost/path%20with%20space?q=abc%20123", "/path with space", "abc 123")] - public async Task CanHandleRequestsWithUrlInAbsoluteForm(string requestUrl, string expectedPath, string queryValue) - { - var pathTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var rawTargetTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var queryTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - using (var server = new TestServer(async context => - { - pathTcs.TrySetResult(context.Request.Path); - queryTcs.TrySetResult(context.Request.Query); - rawTargetTcs.TrySetResult(context.Features.Get().RawTarget); - await context.Response.WriteAsync("Done"); - }, new TestServiceContext(LoggerFactory))) - { - using (var connection = server.CreateConnection()) - { - var requestTarget = new Uri(requestUrl, UriKind.Absolute); - var host = requestTarget.Authority; - if (requestTarget.IsDefaultPort) - { - host += ":" + requestTarget.Port; - } - - await connection.Send( - $"GET {requestUrl} HTTP/1.1", - "Content-Length: 0", - $"Host: {host}", - "", - ""); - - await connection.Receive($"HTTP/1.1 200 OK", - $"Date: {server.Context.DateHeaderValue}", - "Transfer-Encoding: chunked", - "", - "4", - "Done") - .DefaultTimeout(); - - await Task.WhenAll(pathTcs.Task, rawTargetTcs.Task, queryTcs.Task).DefaultTimeout(); - Assert.Equal(new PathString(expectedPath), pathTcs.Task.Result); - Assert.Equal(requestUrl, rawTargetTcs.Task.Result); - if (queryValue == null) - { - Assert.False(queryTcs.Task.Result.ContainsKey("q")); - } - else - { - Assert.Equal(queryValue, queryTcs.Task.Result["q"]); - } - } - } - } - - [Fact] - public async Task AppCanSetTraceIdentifier() - { - const string knownId = "xyz123"; - using (var server = new TestServer(async context => - { - context.TraceIdentifier = knownId; - await context.Response.WriteAsync(context.TraceIdentifier); - }, new TestServiceContext(LoggerFactory))) - { - var requestId = await HttpClientSlim.GetStringAsync($"http://{server.EndPoint}") - .DefaultTimeout(); - Assert.Equal(knownId, requestId); - } - } - - [Fact] - public async Task TraceIdentifierIsUnique() - { - const int identifierLength = 22; - const int iterations = 10; - - using (var server = new TestServer(async context => - { - Assert.Equal(identifierLength, Encoding.ASCII.GetByteCount(context.TraceIdentifier)); - context.Response.ContentLength = identifierLength; - await context.Response.WriteAsync(context.TraceIdentifier); - }, new TestServiceContext(LoggerFactory))) - { - var usedIds = new ConcurrentBag(); - var uri = $"http://{server.EndPoint}"; - - // requests on separate connections in parallel - Parallel.For(0, iterations, async i => - { - var id = await HttpClientSlim.GetStringAsync(uri); - Assert.DoesNotContain(id, usedIds.ToArray()); - usedIds.Add(id); - }); - - // requests on same connection - using (var connection = server.CreateConnection()) - { - var buffer = new char[identifierLength]; - for (var i = 0; i < iterations; i++) - { - await connection.SendEmptyGet(); - - await connection.Receive($"HTTP/1.1 200 OK", - $"Date: {server.Context.DateHeaderValue}", - $"Content-Length: {identifierLength}", - "", - "").DefaultTimeout(); - - var read = await connection.Reader.ReadAsync(buffer, 0, identifierLength); - Assert.Equal(identifierLength, read); - var id = new string(buffer, 0, read); - Assert.DoesNotContain(id, usedIds.ToArray()); - usedIds.Add(id); - } - } - } - } - - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task Http11KeptAliveByDefault(ListenOptions listenOptions) - { - var testContext = new TestServiceContext(LoggerFactory); - - using (var server = new TestServer(TestApp.EchoAppChunked, testContext, listenOptions)) - { - using (var connection = server.CreateConnection()) - { - await connection.Send( - "GET / HTTP/1.1", - "Host:", - "", - "GET / HTTP/1.1", - "Host:", - "Connection: close", - "Content-Length: 7", - "", - "Goodbye"); - await connection.ReceiveEnd( - "HTTP/1.1 200 OK", - $"Date: {testContext.DateHeaderValue}", - "Content-Length: 0", - "", - "HTTP/1.1 200 OK", - "Connection: close", - $"Date: {testContext.DateHeaderValue}", - "Content-Length: 7", - "", - "Goodbye"); - } - } - } - - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task Http10NotKeptAliveByDefault(ListenOptions listenOptions) - { - var testContext = new TestServiceContext(LoggerFactory); - - using (var server = new TestServer(TestApp.EchoApp, testContext, listenOptions)) - { - using (var connection = server.CreateConnection()) - { - await connection.Send( - "GET / HTTP/1.0", - "", - ""); - await connection.ReceiveForcedEnd( - "HTTP/1.1 200 OK", - "Connection: close", - $"Date: {testContext.DateHeaderValue}", - "Content-Length: 0", - "", - ""); - } - - using (var connection = server.CreateConnection()) - { - await connection.Send( - "POST / HTTP/1.0", - "Content-Length: 11", - "", - "Hello World"); - await connection.ReceiveForcedEnd( - "HTTP/1.1 200 OK", - "Connection: close", - $"Date: {testContext.DateHeaderValue}", - "", - "Hello World"); - } - } - } - - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task Http10KeepAlive(ListenOptions listenOptions) - { - var testContext = new TestServiceContext(LoggerFactory); - - using (var server = new TestServer(TestApp.EchoAppChunked, testContext, listenOptions)) - { - using (var connection = server.CreateConnection()) - { - await connection.Send( - "GET / HTTP/1.0", - "Connection: keep-alive", - "", - "POST / HTTP/1.0", - "Content-Length: 7", - "", - "Goodbye"); - await connection.Receive( - "HTTP/1.1 200 OK", - "Connection: keep-alive", - $"Date: {testContext.DateHeaderValue}", - "Content-Length: 0", - "\r\n"); - await connection.ReceiveForcedEnd( - "HTTP/1.1 200 OK", - "Connection: close", - $"Date: {testContext.DateHeaderValue}", - "Content-Length: 7", - "", - "Goodbye"); - } - } - } - - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task Http10KeepAliveNotHonoredIfResponseContentLengthNotSet(ListenOptions listenOptions) - { - var testContext = new TestServiceContext(LoggerFactory); - - using (var server = new TestServer(TestApp.EchoApp, testContext, listenOptions)) - { - using (var connection = server.CreateConnection()) - { - await connection.Send( - "GET / HTTP/1.0", - "Connection: keep-alive", - "", - ""); - - await connection.Receive( - "HTTP/1.1 200 OK", - "Connection: keep-alive", - $"Date: {testContext.DateHeaderValue}", - "Content-Length: 0", - "\r\n"); - - await connection.Send( - "POST / HTTP/1.0", - "Connection: keep-alive", - "Content-Length: 7", - "", - "Goodbye"); - - await connection.ReceiveForcedEnd( - "HTTP/1.1 200 OK", - "Connection: close", - $"Date: {testContext.DateHeaderValue}", - "", - "Goodbye"); - } - } - } - - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task Http10KeepAliveHonoredIfResponseContentLengthSet(ListenOptions listenOptions) - { - var testContext = new TestServiceContext(LoggerFactory); - - using (var server = new TestServer(TestApp.EchoAppChunked, testContext, listenOptions)) - { - using (var connection = server.CreateConnection()) - { - await connection.Send( - "POST / HTTP/1.0", - "Content-Length: 11", - "Connection: keep-alive", - "", - "Hello World"); - - await connection.Receive( - "HTTP/1.1 200 OK", - "Connection: keep-alive", - $"Date: {testContext.DateHeaderValue}", - "Content-Length: 11", - "", - "Hello World"); - - await connection.Send( - "POST / HTTP/1.0", - "Connection: keep-alive", - "Content-Length: 11", - "", - "Hello Again"); - - await connection.Receive( - "HTTP/1.1 200 OK", - "Connection: keep-alive", - $"Date: {testContext.DateHeaderValue}", - "Content-Length: 11", - "", - "Hello Again"); - - await connection.Send( - "POST / HTTP/1.0", - "Content-Length: 7", - "", - "Goodbye"); - - await connection.ReceiveForcedEnd( - "HTTP/1.1 200 OK", - "Connection: close", - $"Date: {testContext.DateHeaderValue}", - "Content-Length: 7", - "", - "Goodbye"); - } - } - } - - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task Expect100ContinueHonored(ListenOptions listenOptions) - { - var testContext = new TestServiceContext(LoggerFactory); - - using (var server = new TestServer(TestApp.EchoAppChunked, testContext, listenOptions)) - { - using (var connection = server.CreateConnection()) - { - await connection.Send( - "POST / HTTP/1.1", - "Host:", - "Expect: 100-continue", - "Connection: close", - "Content-Length: 11", - "\r\n"); - await connection.Receive( - "HTTP/1.1 100 Continue", - "", - ""); - await connection.Send("Hello World"); - await connection.ReceiveForcedEnd( - "HTTP/1.1 200 OK", - "Connection: close", - $"Date: {testContext.DateHeaderValue}", - "Content-Length: 11", - "", - "Hello World"); - } - } - } - - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task ZeroContentLengthAssumedOnNonKeepAliveRequestsWithoutContentLengthOrTransferEncodingHeader(ListenOptions listenOptions) - { - var testContext = new TestServiceContext(LoggerFactory); - - using (var server = new TestServer(async httpContext => - { - // This will hang if 0 content length is not assumed by the server - Assert.Equal(0, await httpContext.Request.Body.ReadAsync(new byte[1], 0, 1).DefaultTimeout()); - }, testContext, listenOptions)) - { - using (var connection = server.CreateConnection()) - { - // Use Send instead of SendEnd to ensure the connection will remain open while - // the app runs and reads 0 bytes from the body nonetheless. This checks that - // https://github.com/aspnet/KestrelHttpServer/issues/1104 is not regressing. - await connection.Send( - "GET / HTTP/1.1", - "Host:", - "Connection: close", - "", - ""); - await connection.ReceiveForcedEnd( - "HTTP/1.1 200 OK", - "Connection: close", - $"Date: {testContext.DateHeaderValue}", - "Content-Length: 0", - "", - ""); - } - - using (var connection = server.CreateConnection()) - { - await connection.Send( - "GET / HTTP/1.0", - "Host:", - "", - ""); - await connection.ReceiveForcedEnd( - "HTTP/1.1 200 OK", - "Connection: close", - $"Date: {testContext.DateHeaderValue}", - "Content-Length: 0", - "", - ""); - } - } - } - - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task ConnectionClosesWhenFinReceivedBeforeRequestCompletes(ListenOptions listenOptions) - { - var testContext = new TestServiceContext(LoggerFactory); - // FIN callbacks are scheduled so run inline to make this test more reliable - testContext.Scheduler = PipeScheduler.Inline; - - using (var server = new TestServer(TestApp.EchoAppChunked, testContext, listenOptions)) - { - using (var connection = server.CreateConnection()) - { - await connection.Send( - "POST / HTTP/1.1"); - connection.Shutdown(SocketShutdown.Send); - await connection.ReceiveForcedEnd(); - } - - using (var connection = server.CreateConnection()) - { - await connection.Send( - "POST / HTTP/1.1", - "Host:", - "Content-Length: 7"); - connection.Shutdown(SocketShutdown.Send); - await connection.ReceiveForcedEnd(); - } - } - } - - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task RequestsCanBeAbortedMidRead(ListenOptions listenOptions) - { - const int applicationAbortedConnectionId = 34; - - var testContext = new TestServiceContext(LoggerFactory); - - var readTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var registrationTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var requestId = 0; - - using (var server = new TestServer(async httpContext => - { - requestId++; - - var response = httpContext.Response; - var request = httpContext.Request; - var lifetime = httpContext.Features.Get(); - - lifetime.RequestAborted.Register(() => registrationTcs.TrySetResult(requestId)); - - if (requestId == 1) - { - response.Headers["Content-Length"] = new[] { "5" }; - - await response.WriteAsync("World"); - } - else - { - var readTask = request.Body.CopyToAsync(Stream.Null); - - lifetime.Abort(); - - try - { - await readTask; - } - catch (Exception ex) - { - readTcs.SetException(ex); - throw; - } - - readTcs.SetException(new Exception("This shouldn't be reached.")); - } - }, testContext, listenOptions)) - { - using (var connection = server.CreateConnection()) - { - // Full request and response - await connection.Send( - "POST / HTTP/1.1", - "Host:", - "Content-Length: 5", - "", - "Hello"); - - await connection.Receive( - "HTTP/1.1 200 OK", - $"Date: {testContext.DateHeaderValue}", - "Content-Length: 5", - "", - "World"); - - // Never send the body so CopyToAsync always fails. - await connection.Send("POST / HTTP/1.1", - "Host:", - "Content-Length: 5", - "", - ""); - await connection.WaitForConnectionClose(); - } - } - - await Assert.ThrowsAsync(async () => await readTcs.Task); - - // The cancellation token for only the last request should be triggered. - var abortedRequestId = await registrationTcs.Task; - Assert.Equal(2, abortedRequestId); - - Assert.Single(TestSink.Writes.Where(w => w.LoggerName == "Microsoft.AspNetCore.Server.Kestrel" && - w.EventId == applicationAbortedConnectionId)); - } - - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task ServerCanAbortConnectionAfterUnobservedClose(ListenOptions listenOptions) - { - const int connectionPausedEventId = 4; - const int connectionFinSentEventId = 7; - const int maxRequestBufferSize = 4096; - - var readCallbackUnwired = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var clientClosedConnection = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var serverClosedConnection = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var appFuncCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - var mockLogger = new Mock(); - mockLogger - .Setup(logger => logger.IsEnabled(It.IsAny())) - .Returns(true); - mockLogger - .Setup(logger => logger.Log(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())) - .Callback>((logLevel, eventId, state, exception, formatter) => - { - if (eventId.Id == connectionPausedEventId) - { - readCallbackUnwired.TrySetResult(null); - } - else if (eventId.Id == connectionFinSentEventId) - { - serverClosedConnection.SetResult(null); - } - - Logger.Log(logLevel, eventId, state, exception, formatter); - }); - - var mockLoggerFactory = new Mock(); - mockLoggerFactory - .Setup(factory => factory.CreateLogger(It.IsAny())) - .Returns(Logger); - mockLoggerFactory - .Setup(factory => factory.CreateLogger(It.IsIn("Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv", - "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets"))) - .Returns(mockLogger.Object); - - var mockKestrelTrace = new Mock(Logger) { CallBase = true }; - var testContext = new TestServiceContext(mockLoggerFactory.Object) - { - Log = mockKestrelTrace.Object, - ServerOptions = - { - Limits = - { - MaxRequestBufferSize = maxRequestBufferSize, - MaxRequestLineSize = maxRequestBufferSize, - MaxRequestHeadersTotalSize = maxRequestBufferSize, - } - } - }; - - var scratchBuffer = new byte[maxRequestBufferSize * 8]; - - using (var server = new TestServer(async context => - { - await clientClosedConnection.Task; - - context.Abort(); - - await serverClosedConnection.Task; - - appFuncCompleted.SetResult(null); - }, testContext, listenOptions)) - { - using (var connection = server.CreateConnection()) - { - await connection.Send( - "POST / HTTP/1.1", - "Host:", - $"Content-Length: {scratchBuffer.Length}", - "", - ""); - - var ignore = connection.Stream.WriteAsync(scratchBuffer, 0, scratchBuffer.Length); - - // Wait until the read callback is no longer hooked up so that the connection disconnect isn't observed. - await readCallbackUnwired.Task.DefaultTimeout(); - } - - clientClosedConnection.SetResult(null); - - await appFuncCompleted.Task.DefaultTimeout(); - } - - mockKestrelTrace.Verify(t => t.ConnectionStop(It.IsAny()), Times.Once()); - } - - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task AppCanHandleClientAbortingConnectionMidRequest(ListenOptions listenOptions) - { - var readTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var appStartedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - var mockKestrelTrace = new Mock(Logger) { CallBase = true }; - var testContext = new TestServiceContext() - { - Log = mockKestrelTrace.Object, - }; - - var scratchBuffer = new byte[4096]; - - using (var server = new TestServer(async context => - { - appStartedTcs.SetResult(null); - - try - { - await context.Request.Body.CopyToAsync(Stream.Null);; - } - catch (Exception ex) - { - readTcs.SetException(ex); - throw; - } - - readTcs.SetException(new Exception("This shouldn't be reached.")); - - }, testContext, listenOptions)) - { - using (var connection = server.CreateConnection()) - { - await connection.Send( - "POST / HTTP/1.1", - "Host:", - $"Content-Length: {scratchBuffer.Length * 2}", - "", - ""); - - await appStartedTcs.Task.DefaultTimeout(); - - await connection.Stream.WriteAsync(scratchBuffer, 0, scratchBuffer.Length); - - connection.Reset(); - } - - await Assert.ThrowsAnyAsync(() => readTcs.Task).DefaultTimeout(); - } - - mockKestrelTrace.Verify(t => t.ConnectionStop(It.IsAny()), Times.Once()); - } - - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task RequestHeadersAreResetOnEachRequest(ListenOptions listenOptions) - { - var testContext = new TestServiceContext(LoggerFactory); - - IHeaderDictionary originalRequestHeaders = null; - var firstRequest = true; - - using (var server = new TestServer(httpContext => - { - var requestFeature = httpContext.Features.Get(); - - if (firstRequest) - { - originalRequestHeaders = requestFeature.Headers; - requestFeature.Headers = new HttpRequestHeaders(); - firstRequest = false; - } - else - { - Assert.Same(originalRequestHeaders, requestFeature.Headers); - } - - return Task.CompletedTask; - }, testContext, listenOptions)) - { - using (var connection = server.CreateConnection()) - { - await connection.Send( - "GET / HTTP/1.1", - "Host:", - "", - "GET / HTTP/1.1", - "Host:", - "", - ""); - await connection.ReceiveEnd( - "HTTP/1.1 200 OK", - $"Date: {testContext.DateHeaderValue}", - "Content-Length: 0", - "", - "HTTP/1.1 200 OK", - $"Date: {testContext.DateHeaderValue}", - "Content-Length: 0", - "", - ""); - } - } - } - - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task UpgradeRequestIsNotKeptAliveOrChunked(ListenOptions listenOptions) - { - const string message = "Hello World"; - - var testContext = new TestServiceContext(LoggerFactory); - - using (var server = new TestServer(async context => - { - var upgradeFeature = context.Features.Get(); - var duplexStream = await upgradeFeature.UpgradeAsync(); - - var buffer = new byte[message.Length]; - var read = 0; - while (read < message.Length) - { - read += await duplexStream.ReadAsync(buffer, read, buffer.Length - read).DefaultTimeout(); - } - - await duplexStream.WriteAsync(buffer, 0, read); - }, testContext, listenOptions)) - { - using (var connection = server.CreateConnection()) - { - await connection.Send( - "GET / HTTP/1.1", - "Host:", - "Connection: Upgrade", - "", - message); - await connection.ReceiveForcedEnd( - "HTTP/1.1 101 Switching Protocols", - "Connection: Upgrade", - $"Date: {testContext.DateHeaderValue}", - "", - message); - } - } - } - - [Fact] - public async Task HeadersAndStreamsAreReusedAcrossRequests() - { - var testContext = new TestServiceContext(LoggerFactory); - var streamCount = 0; - var requestHeadersCount = 0; - var responseHeadersCount = 0; - var loopCount = 20; - Stream lastStream = null; - IHeaderDictionary lastRequestHeaders = null; - IHeaderDictionary lastResponseHeaders = null; - - using (var server = new TestServer(async context => - { - if (context.Request.Body != lastStream) - { - lastStream = context.Request.Body; - streamCount++; - } - if (context.Request.Headers != lastRequestHeaders) - { - lastRequestHeaders = context.Request.Headers; - requestHeadersCount++; - } - if (context.Response.Headers != lastResponseHeaders) - { - lastResponseHeaders = context.Response.Headers; - responseHeadersCount++; - } - - var ms = new MemoryStream(); - await context.Request.Body.CopyToAsync(ms); - var request = ms.ToArray(); - - context.Response.ContentLength = request.Length; - - await context.Response.Body.WriteAsync(request, 0, request.Length); - }, testContext)) - { - using (var connection = server.CreateConnection()) - { - var requestData = - Enumerable.Repeat("GET / HTTP/1.1\r\nHost:\r\n", loopCount) - .Concat(new[] { "GET / HTTP/1.1\r\nHost:\r\nContent-Length: 7\r\nConnection: close\r\n\r\nGoodbye" }); - - var response = string.Join("\r\n", new string[] { - "HTTP/1.1 200 OK", - $"Date: {testContext.DateHeaderValue}", - "Content-Length: 0", - ""}); - - var lastResponse = string.Join("\r\n", new string[] - { - "HTTP/1.1 200 OK", - "Connection: close", - $"Date: {testContext.DateHeaderValue}", - "Content-Length: 7", - "", - "Goodbye" - }); - - var responseData = - Enumerable.Repeat(response, loopCount) - .Concat(new[] { lastResponse }); - - await connection.Send(requestData.ToArray()); - - await connection.ReceiveEnd(responseData.ToArray()); - } - - Assert.Equal(1, streamCount); - Assert.Equal(1, requestHeadersCount); - Assert.Equal(1, responseHeadersCount); - } - } - - [Theory] - [MemberData(nameof(HostHeaderData))] - public async Task MatchesValidRequestTargetAndHostHeader(string request, string hostHeader) - { - using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory))) - { - using (var connection = server.CreateConnection()) - { - await connection.Send($"{request} HTTP/1.1", - $"Host: {hostHeader}", - "", - ""); - - await connection.Receive("HTTP/1.1 200 OK"); - } - } - } - - [Fact] - public async Task ServerConsumesKeepAliveContentLengthRequest() - { - // The app doesn't read the request body, so it should be consumed by the server - using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory))) - { - using (var connection = server.CreateConnection()) - { - await connection.Send( - "POST / HTTP/1.1", - "Host:", - "Content-Length: 5", - "", - "hello"); - - await connection.Receive( - "HTTP/1.1 200 OK", - $"Date: {server.Context.DateHeaderValue}", - "Content-Length: 0", - "", - ""); - - // If the server consumed the previous request properly, the - // next request should be successful - await connection.Send( - "POST / HTTP/1.1", - "Host:", - "Content-Length: 5", - "", - "world"); - - await connection.Receive( - "HTTP/1.1 200 OK", - $"Date: {server.Context.DateHeaderValue}", - "Content-Length: 0", - "", - ""); - } - } - } - - [Fact] - public async Task ServerConsumesKeepAliveChunkedRequest() - { - // The app doesn't read the request body, so it should be consumed by the server - using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory))) - { - using (var connection = server.CreateConnection()) - { - await connection.Send( - "POST / HTTP/1.1", - "Host:", - "Transfer-Encoding: chunked", - "", - "5", - "hello", - "5", - "world", - "0", - "Trailer: value", - "", - ""); - - await connection.Receive( - "HTTP/1.1 200 OK", - $"Date: {server.Context.DateHeaderValue}", - "Content-Length: 0", - "", - ""); - - // If the server consumed the previous request properly, the - // next request should be successful - await connection.Send( - "POST / HTTP/1.1", - "Host:", - "Content-Length: 5", - "", - "world"); - - await connection.Receive( - "HTTP/1.1 200 OK", - $"Date: {server.Context.DateHeaderValue}", - "Content-Length: 0", - "", - ""); - } - } - } - - [Fact] - public async Task NonKeepAliveRequestNotConsumedByAppCompletes() - { - // The app doesn't read the request body, so it should be consumed by the server - using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory))) - { - using (var connection = server.CreateConnection()) - { - await connection.SendAll( - "POST / HTTP/1.0", - "Host:", - "Content-Length: 5", - "", - "hello"); - - await connection.ReceiveForcedEnd( - "HTTP/1.1 200 OK", - "Connection: close", - $"Date: {server.Context.DateHeaderValue}", - "Content-Length: 0", - "", - ""); - } - } - } - - [Fact] - public async Task UpgradedRequestNotConsumedByAppCompletes() - { - // The app doesn't read the request body, so it should be consumed by the server - using (var server = new TestServer(async context => - { - var upgradeFeature = context.Features.Get(); - var duplexStream = await upgradeFeature.UpgradeAsync(); - - var response = Encoding.ASCII.GetBytes("goodbye"); - await duplexStream.WriteAsync(response, 0, response.Length); - }, new TestServiceContext(LoggerFactory))) - { - using (var connection = server.CreateConnection()) - { - await connection.SendAll( - "GET / HTTP/1.1", - "Host:", - "Connection: upgrade", - "", - "hello"); - - await connection.ReceiveForcedEnd( - "HTTP/1.1 101 Switching Protocols", - "Connection: Upgrade", - $"Date: {server.Context.DateHeaderValue}", - "", - "goodbye"); - } - } - } - - [Fact] - public async Task DoesNotEnforceRequestBodyMinimumDataRateOnUpgradedRequest() - { - var appEvent = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var delayEvent = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var serviceContext = new TestServiceContext(LoggerFactory) - { - SystemClock = new SystemClock() - }; - - using (var server = new TestServer(async context => - { - context.Features.Get().MinDataRate = - new MinDataRate(bytesPerSecond: double.MaxValue, gracePeriod: Heartbeat.Interval + TimeSpan.FromTicks(1)); - - using (var stream = await context.Features.Get().UpgradeAsync()) - { - appEvent.SetResult(null); - - // Read once to go through one set of TryPauseTimingReads()/TryResumeTimingReads() calls - await stream.ReadAsync(new byte[1], 0, 1); - - await delayEvent.Task.DefaultTimeout(); - - // Read again to check that the connection is still alive - await stream.ReadAsync(new byte[1], 0, 1); - - // Send a response to distinguish from the timeout case where the 101 is still received, but without any content - var response = Encoding.ASCII.GetBytes("hello"); - await stream.WriteAsync(response, 0, response.Length); - } - }, serviceContext)) - { - using (var connection = server.CreateConnection()) - { - await connection.Send( - "GET / HTTP/1.1", - "Host:", - "Connection: upgrade", - "", - "a"); - - await appEvent.Task.DefaultTimeout(); - - await Task.Delay(TimeSpan.FromSeconds(5)); - - delayEvent.SetResult(null); - - await connection.Send("b"); - - await connection.Receive( - "HTTP/1.1 101 Switching Protocols", - "Connection: Upgrade", - ""); - await connection.ReceiveStartsWith( - $"Date: "); - await connection.ReceiveForcedEnd( - "", - "hello"); - } - } - } - - [Fact] - public async Task SynchronousReadsAllowedByDefault() - { - var firstRequest = true; - - using (var server = new TestServer(async context => - { - var bodyControlFeature = context.Features.Get(); - Assert.True(bodyControlFeature.AllowSynchronousIO); - - var buffer = new byte[6]; - var offset = 0; - - // The request body is 5 bytes long. The 6th byte (buffer[5]) is only used for writing the response body. - buffer[5] = (byte)(firstRequest ? '1' : '2'); - - if (firstRequest) - { - while (offset < 5) - { - offset += context.Request.Body.Read(buffer, offset, 5 - offset); - } - - firstRequest = false; - } - else - { - bodyControlFeature.AllowSynchronousIO = false; - - // Synchronous reads now throw. - var ioEx = Assert.Throws(() => context.Request.Body.Read(new byte[1], 0, 1)); - Assert.Equal(CoreStrings.SynchronousReadsDisallowed, ioEx.Message); - - var ioEx2 = Assert.Throws(() => context.Request.Body.CopyTo(Stream.Null)); - Assert.Equal(CoreStrings.SynchronousReadsDisallowed, ioEx2.Message); - - while (offset < 5) - { - offset += await context.Request.Body.ReadAsync(buffer, offset, 5 - offset); - } - } - - Assert.Equal(0, await context.Request.Body.ReadAsync(new byte[1], 0, 1)); - Assert.Equal("Hello", Encoding.ASCII.GetString(buffer, 0, 5)); - - context.Response.ContentLength = 6; - await context.Response.Body.WriteAsync(buffer, 0, 6); - }, new TestServiceContext(LoggerFactory))) - { - using (var connection = server.CreateConnection()) - { - await connection.Send( - "POST / HTTP/1.1", - "Host:", - "Content-Length: 5", - "", - "HelloPOST / HTTP/1.1", - "Host:", - "Content-Length: 5", - "", - "Hello"); - await connection.Receive( - "HTTP/1.1 200 OK", - $"Date: {server.Context.DateHeaderValue}", - "Content-Length: 6", - "", - "Hello1HTTP/1.1 200 OK", - $"Date: {server.Context.DateHeaderValue}", - "Content-Length: 6", - "", - "Hello2"); - } - } - } - - [Fact] - public async Task SynchronousReadsCanBeDisallowedGlobally() - { - var testContext = new TestServiceContext(LoggerFactory) - { - ServerOptions = { AllowSynchronousIO = false } - }; - - using (var server = new TestServer(async context => - { - var bodyControlFeature = context.Features.Get(); - Assert.False(bodyControlFeature.AllowSynchronousIO); - - // Synchronous reads now throw. - var ioEx = Assert.Throws(() => context.Request.Body.Read(new byte[1], 0, 1)); - Assert.Equal(CoreStrings.SynchronousReadsDisallowed, ioEx.Message); - - var ioEx2 = Assert.Throws(() => context.Request.Body.CopyTo(Stream.Null)); - Assert.Equal(CoreStrings.SynchronousReadsDisallowed, ioEx2.Message); - - var buffer = new byte[5]; - var offset = 0; - while (offset < 5) - { - offset += await context.Request.Body.ReadAsync(buffer, offset, 5 - offset); - } - - Assert.Equal(0, await context.Request.Body.ReadAsync(new byte[1], 0, 1)); - Assert.Equal("Hello", Encoding.ASCII.GetString(buffer)); - }, testContext)) - { - using (var connection = server.CreateConnection()) - { - await connection.Send( - "POST / HTTP/1.1", - "Host:", - "Content-Length: 5", - "", - "Hello"); - await connection.Receive( - "HTTP/1.1 200 OK", - $"Date: {server.Context.DateHeaderValue}", - "Content-Length: 0", - "", - ""); - } - } - } - - private async Task TestRemoteIPAddress(string registerAddress, string requestAddress, string expectAddress) - { - var builder = TransportSelector.GetWebHostBuilder() - .UseKestrel() - .UseUrls($"http://{registerAddress}:0") - .ConfigureServices(AddTestLogging) - .Configure(app => - { - app.Run(async context => - { - var connection = context.Connection; - await context.Response.WriteAsync(JsonConvert.SerializeObject(new - { - RemoteIPAddress = connection.RemoteIpAddress?.ToString(), - RemotePort = connection.RemotePort, - LocalIPAddress = connection.LocalIpAddress?.ToString(), - LocalPort = connection.LocalPort - })); - }); - }); - - using (var host = builder.Build()) - using (var client = new HttpClient()) - { - host.Start(); - - var response = await client.GetAsync($"http://{requestAddress}:{host.GetPort()}/"); - response.EnsureSuccessStatusCode(); - - var connectionFacts = await response.Content.ReadAsStringAsync(); - Assert.NotEmpty(connectionFacts); - - var facts = JsonConvert.DeserializeObject(connectionFacts); - Assert.Equal(expectAddress, facts["RemoteIPAddress"].Value()); - Assert.NotEmpty(facts["RemotePort"].Value()); - } - } - - public static TheoryData HostHeaderData => HttpParsingData.HostHeaderData; - } -} diff --git a/test/Kestrel.FunctionalTests/BadHttpRequestTests.cs b/test/Kestrel.InMemory.FunctionalTests/BadHttpRequestTests.cs similarity index 94% rename from test/Kestrel.FunctionalTests/BadHttpRequestTests.cs rename to test/Kestrel.InMemory.FunctionalTests/BadHttpRequestTests.cs index 2954b3f7c2..d58615c8a7 100644 --- a/test/Kestrel.FunctionalTests/BadHttpRequestTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/BadHttpRequestTests.cs @@ -6,14 +6,14 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; +using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { public class BadHttpRequestTests : LoggedTest { @@ -158,7 +158,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests await context.Request.Body.ReadAsync(new byte[1], 0, 1); }, new TestServiceContext(LoggerFactory))) { - using (var connection = new TestConnection(server.Port)) + using (var connection = server.CreateConnection()) { await connection.SendAll( "GET ? HTTP/1.1", @@ -175,7 +175,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests [Fact] public async Task TestRequestSplitting() { - using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory, Mock.Of()))) + using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory))) { using (var client = server.CreateConnection()) { @@ -191,7 +191,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests private async Task TestBadRequest(string request, string expectedResponseStatusCode, string expectedExceptionMessage, string expectedAllowHeader = null) { BadHttpRequestException loggedException = null; - var mockKestrelTrace = new Mock(); + var mockKestrelTrace = new Mock(Logger) { CallBase = true }; mockKestrelTrace .Setup(trace => trace.IsEnabled(LogLevel.Information)) .Returns(true); @@ -212,7 +212,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests Assert.Equal(expectedExceptionMessage, loggedException.Message); } - private async Task ReceiveBadRequestResponse(TestConnection connection, string expectedResponseStatusCode, string expectedDateHeaderValue, string expectedAllowHeader = null) + private async Task ReceiveBadRequestResponse(InMemoryConnection connection, string expectedResponseStatusCode, string expectedDateHeaderValue, string expectedAllowHeader = null) { var lines = new[] { diff --git a/test/Kestrel.FunctionalTests/CertificateLoaderTests.cs b/test/Kestrel.InMemory.FunctionalTests/CertificateLoaderTests.cs similarity index 97% rename from test/Kestrel.FunctionalTests/CertificateLoaderTests.cs rename to test/Kestrel.InMemory.FunctionalTests/CertificateLoaderTests.cs index 86cbaa288d..fcad4f5a46 100644 --- a/test/Kestrel.FunctionalTests/CertificateLoaderTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/CertificateLoaderTests.cs @@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging.Testing; using Xunit; -namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { public class CertificateLoaderTests : LoggedTest { diff --git a/test/Kestrel.FunctionalTests/ChunkedRequestTests.cs b/test/Kestrel.InMemory.FunctionalTests/ChunkedRequestTests.cs similarity index 89% rename from test/Kestrel.FunctionalTests/ChunkedRequestTests.cs rename to test/Kestrel.InMemory.FunctionalTests/ChunkedRequestTests.cs index 02122cff42..28cc4dd1d9 100644 --- a/test/Kestrel.FunctionalTests/ChunkedRequestTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/ChunkedRequestTests.cs @@ -5,35 +5,20 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Net; -using System.Net.Sockets; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; +using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging.Testing; using Xunit; -using Xunit.Abstractions; -namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { public class ChunkedRequestTests : LoggedTest { - public ChunkedRequestTests(ITestOutputHelper output) : base(output) - { - } - - public static TheoryData ConnectionAdapterData => new TheoryData - { - new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)), - new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) - { - ConnectionAdapters = { new PassThroughConnectionAdapter() } - } - }; - private async Task App(HttpContext httpContext) { var request = httpContext.Request; @@ -62,13 +47,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests await response.Body.WriteAsync(bytes, 0, bytes.Length); } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task Http10TransferEncoding(ListenOptions listenOptions) + [Fact] + public async Task Http10TransferEncoding() { var testContext = new TestServiceContext(LoggerFactory); - using (var server = new TestServer(App, testContext, listenOptions)) + using (var server = new TestServer(App, testContext)) { using (var connection = server.CreateConnection()) { @@ -92,13 +76,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task Http10KeepAliveTransferEncoding(ListenOptions listenOptions) + [Fact] + public async Task Http10KeepAliveTransferEncoding() { var testContext = new TestServiceContext(); - using (var server = new TestServer(AppChunked, testContext, listenOptions)) + using (var server = new TestServer(AppChunked, testContext)) { using (var connection = server.CreateConnection()) { @@ -134,9 +117,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task RequestBodyIsConsumedAutomaticallyIfAppDoesntConsumeItFully(ListenOptions listenOptions) + [Fact] + public async Task RequestBodyIsConsumedAutomaticallyIfAppDoesntConsumeItFully() { var testContext = new TestServiceContext(LoggerFactory); @@ -150,7 +132,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests response.Headers["Content-Length"] = new[] { "11" }; await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11); - }, testContext, listenOptions)) + }, testContext)) { using (var connection = server.CreateConnection()) { @@ -189,9 +171,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task TrailingHeadersAreParsed(ListenOptions listenOptions) + [Fact] + public async Task TrailingHeadersAreParsed() { var requestCount = 10; var requestsReceived = 0; @@ -222,7 +203,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests response.Headers["Content-Length"] = new[] { "11" }; await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11); - }, new TestServiceContext(LoggerFactory), listenOptions)) + }, new TestServiceContext(LoggerFactory))) { var response = string.Join("\r\n", new string[] { "HTTP/1.1 200 OK", @@ -275,9 +256,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task TrailingHeadersCountTowardsHeadersTotalSizeLimit(ListenOptions listenOptions) + [Fact] + public async Task TrailingHeadersCountTowardsHeadersTotalSizeLimit() { const string transferEncodingHeaderLine = "Transfer-Encoding: chunked"; const string headerLine = "Header: value"; @@ -293,7 +273,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { var buffer = new byte[128]; while (await context.Request.Body.ReadAsync(buffer, 0, buffer.Length) != 0) ; // read to end - }, testContext, listenOptions)) + }, testContext)) { using (var connection = server.CreateConnection()) { @@ -320,9 +300,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task TrailingHeadersCountTowardsHeaderCountLimit(ListenOptions listenOptions) + [Fact] + public async Task TrailingHeadersCountTowardsHeaderCountLimit() { const string transferEncodingHeaderLine = "Transfer-Encoding: chunked"; const string headerLine = "Header: value"; @@ -335,7 +314,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { var buffer = new byte[128]; while (await context.Request.Body.ReadAsync(buffer, 0, buffer.Length) != 0) ; // read to end - }, testContext, listenOptions)) + }, testContext)) { using (var connection = server.CreateConnection()) { @@ -362,9 +341,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task ExtensionsAreIgnored(ListenOptions listenOptions) + [Fact] + public async Task ExtensionsAreIgnored() { var testContext = new TestServiceContext(LoggerFactory); var requestCount = 10; @@ -396,7 +374,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests response.Headers["Content-Length"] = new[] { "11" }; await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11); - }, testContext, listenOptions)) + }, testContext)) { var response = string.Join("\r\n", new string[] { "HTTP/1.1 200 OK", @@ -449,9 +427,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task InvalidLengthResultsIn400(ListenOptions listenOptions) + [Fact] + public async Task InvalidLengthResultsIn400() { var testContext = new TestServiceContext(LoggerFactory); using (var server = new TestServer(async httpContext => @@ -469,7 +446,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests response.Headers["Content-Length"] = new[] { "11" }; await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11); - }, testContext, listenOptions)) + }, testContext)) { using (var connection = server.CreateConnection()) { @@ -493,9 +470,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task InvalidSizedDataResultsIn400(ListenOptions listenOptions) + [Fact] + public async Task InvalidSizedDataResultsIn400() { var testContext = new TestServiceContext(LoggerFactory); using (var server = new TestServer(async httpContext => @@ -513,7 +489,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests response.Headers["Content-Length"] = new[] { "11" }; await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11); - }, testContext, listenOptions)) + }, testContext)) { using (var connection = server.CreateConnection()) { @@ -539,15 +515,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task ChunkedNotFinalTransferCodingResultsIn400(ListenOptions listenOptions) + [Fact] + public async Task ChunkedNotFinalTransferCodingResultsIn400() { var testContext = new TestServiceContext(LoggerFactory); using (var server = new TestServer(httpContext => { return Task.CompletedTask; - }, testContext, listenOptions)) + }, testContext)) { using (var connection = server.CreateConnection()) { @@ -643,9 +618,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task ClosingConnectionMidChunkPrefixThrows(ListenOptions listenOptions) + [Fact] + public async Task ClosingConnectionMidChunkPrefixThrows() { var testContext = new TestServiceContext(LoggerFactory); var readStartedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -668,7 +642,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { exTcs.SetException(ex); } - }, testContext, listenOptions)) + }, testContext)) { using (var connection = server.CreateConnection()) { @@ -681,7 +655,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests await readStartedTcs.Task.TimeoutAfter(TestConstants.DefaultTimeout); - connection.Socket.Shutdown(SocketShutdown.Send); + connection.ShutdownSend(); await connection.ReceiveEnd(); diff --git a/test/Kestrel.FunctionalTests/ChunkedResponseTests.cs b/test/Kestrel.InMemory.FunctionalTests/ChunkedResponseTests.cs similarity index 85% rename from test/Kestrel.FunctionalTests/ChunkedResponseTests.cs rename to test/Kestrel.InMemory.FunctionalTests/ChunkedResponseTests.cs index 6cb1a9e439..8fcb728e32 100644 --- a/test/Kestrel.FunctionalTests/ChunkedResponseTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/ChunkedResponseTests.cs @@ -2,32 +2,21 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging.Testing; using Xunit; -namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { public class ChunkedResponseTests : LoggedTest { - public static TheoryData ConnectionAdapterData => new TheoryData - { - new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)), - new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) - { - ConnectionAdapters = { new PassThroughConnectionAdapter() } - } - }; - - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task ResponsesAreChunkedAutomatically(ListenOptions listenOptions) + [Fact] + public async Task ResponsesAreChunkedAutomatically() { var testContext = new TestServiceContext(LoggerFactory); @@ -36,7 +25,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests var response = httpContext.Response; await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello "), 0, 6); await response.Body.WriteAsync(Encoding.ASCII.GetBytes("World!"), 0, 6); - }, testContext, listenOptions)) + }, testContext)) { using (var connection = server.CreateConnection()) { @@ -61,9 +50,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task ResponsesAreNotChunkedAutomaticallyForHttp10Requests(ListenOptions listenOptions) + [Fact] + public async Task ResponsesAreNotChunkedAutomaticallyForHttp10Requests() { var testContext = new TestServiceContext(LoggerFactory); @@ -71,7 +59,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { await httpContext.Response.WriteAsync("Hello "); await httpContext.Response.WriteAsync("World!"); - }, testContext, listenOptions)) + }, testContext)) { using (var connection = server.CreateConnection()) { @@ -90,9 +78,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task ResponsesAreChunkedAutomaticallyForHttp11NonKeepAliveRequests(ListenOptions listenOptions) + [Fact] + public async Task ResponsesAreChunkedAutomaticallyForHttp11NonKeepAliveRequests() { var testContext = new TestServiceContext(LoggerFactory); @@ -100,7 +87,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { await httpContext.Response.WriteAsync("Hello "); await httpContext.Response.WriteAsync("World!"); - }, testContext, listenOptions)) + }, testContext)) { using (var connection = server.CreateConnection()) { @@ -127,9 +114,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task SettingConnectionCloseHeaderInAppDoesNotDisableChunking(ListenOptions listenOptions) + [Fact] + public async Task SettingConnectionCloseHeaderInAppDoesNotDisableChunking() { var testContext = new TestServiceContext(LoggerFactory); @@ -138,7 +124,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests httpContext.Response.Headers["Connection"] = "close"; await httpContext.Response.WriteAsync("Hello "); await httpContext.Response.WriteAsync("World!"); - }, testContext, listenOptions)) + }, testContext)) { using (var connection = server.CreateConnection()) { @@ -164,9 +150,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task ZeroLengthWritesAreIgnored(ListenOptions listenOptions) + [Fact] + public async Task ZeroLengthWritesAreIgnored() { var testContext = new TestServiceContext(LoggerFactory); @@ -176,7 +161,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello "), 0, 6); await response.Body.WriteAsync(new byte[0], 0, 0); await response.Body.WriteAsync(Encoding.ASCII.GetBytes("World!"), 0, 6); - }, testContext, listenOptions)) + }, testContext)) { using (var connection = server.CreateConnection()) { @@ -201,9 +186,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task ZeroLengthWritesFlushHeaders(ListenOptions listenOptions) + [Fact] + public async Task ZeroLengthWritesFlushHeaders() { var testContext = new TestServiceContext(LoggerFactory); @@ -217,7 +201,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests await flushed.WaitAsync(); await response.WriteAsync("Hello World!"); - }, testContext, listenOptions)) + }, testContext)) { using (var connection = server.CreateConnection()) { @@ -246,9 +230,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task EmptyResponseBodyHandledCorrectlyWithZeroLengthWrite(ListenOptions listenOptions) + [Fact] + public async Task EmptyResponseBodyHandledCorrectlyWithZeroLengthWrite() { var testContext = new TestServiceContext(LoggerFactory); @@ -256,7 +239,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { var response = httpContext.Response; await response.Body.WriteAsync(new byte[0], 0, 0); - }, testContext, listenOptions)) + }, testContext)) { using (var connection = server.CreateConnection()) { @@ -277,9 +260,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task ConnectionClosedIfExceptionThrownAfterWrite(ListenOptions listenOptions) + [Fact] + public async Task ConnectionClosedIfExceptionThrownAfterWrite() { var testContext = new TestServiceContext(LoggerFactory); @@ -288,7 +270,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests var response = httpContext.Response; await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World!"), 0, 12); throw new Exception(); - }, testContext, listenOptions)) + }, testContext)) { using (var connection = server.CreateConnection()) { @@ -311,9 +293,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task ConnectionClosedIfExceptionThrownAfterZeroLengthWrite(ListenOptions listenOptions) + [Fact] + public async Task ConnectionClosedIfExceptionThrownAfterZeroLengthWrite() { var testContext = new TestServiceContext(LoggerFactory); @@ -322,7 +303,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests var response = httpContext.Response; await response.Body.WriteAsync(new byte[0], 0, 0); throw new Exception(); - }, testContext, listenOptions)) + }, testContext)) { using (var connection = server.CreateConnection()) { @@ -344,9 +325,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task WritesAreFlushedPriorToResponseCompletion(ListenOptions listenOptions) + [Fact] + public async Task WritesAreFlushedPriorToResponseCompletion() { var testContext = new TestServiceContext(LoggerFactory); @@ -361,7 +341,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests await flushWh.Task.DefaultTimeout(); await response.Body.WriteAsync(Encoding.ASCII.GetBytes("World!"), 0, 6); - }, testContext, listenOptions)) + }, testContext)) { using (var connection = server.CreateConnection()) { @@ -391,9 +371,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task ChunksCanBeWrittenManually(ListenOptions listenOptions) + [Fact] + public async Task ChunksCanBeWrittenManually() { var testContext = new TestServiceContext(LoggerFactory); @@ -405,7 +384,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests await response.Body.WriteAsync(Encoding.ASCII.GetBytes("6\r\nHello \r\n"), 0, 11); await response.Body.WriteAsync(Encoding.ASCII.GetBytes("6\r\nWorld!\r\n"), 0, 11); await response.Body.WriteAsync(Encoding.ASCII.GetBytes("0\r\n\r\n"), 0, 5); - }, testContext, listenOptions)) + }, testContext)) { using (var connection = server.CreateConnection()) { diff --git a/test/Kestrel.FunctionalTests/ConnectionAdapterTests.cs b/test/Kestrel.InMemory.FunctionalTests/ConnectionAdapterTests.cs similarity index 93% rename from test/Kestrel.FunctionalTests/ConnectionAdapterTests.cs rename to test/Kestrel.InMemory.FunctionalTests/ConnectionAdapterTests.cs index a4c719bbde..d5b8f4e761 100644 --- a/test/Kestrel.FunctionalTests/ConnectionAdapterTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/ConnectionAdapterTests.cs @@ -4,17 +4,17 @@ using System; using System.IO; using System.Net; -using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal; +using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging.Testing; using Xunit; -namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { public class ConnectionAdapterTests : LoggedTest { @@ -93,7 +93,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests using (var connection = server.CreateConnection()) { // FIN - connection.Shutdown(SocketShutdown.Send); + connection.ShutdownSend(); await connection.WaitForConnectionClose(); } } @@ -114,7 +114,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests using (var connection = server.CreateConnection()) { // FIN - connection.Shutdown(SocketShutdown.Send); + connection.ShutdownSend(); await connection.WaitForConnectionClose(); } } @@ -156,20 +156,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { using (var connection = server.CreateConnection()) { - // Will throw because the exception in the connection adapter will close the connection. - await Assert.ThrowsAsync(async () => - { - await connection.Send( - "POST / HTTP/1.0", - "Content-Length: 1000", - "\r\n"); + await connection.Send( + "POST / HTTP/1.0", + "Content-Length: 1000", + "\r\n"); - for (var i = 0; i < 1000; i++) - { - await connection.Send("a"); - await Task.Delay(5); - } - }); + await connection.WaitForConnectionClose(); } } } diff --git a/test/Kestrel.FunctionalTests/ConnectionLimitTests.cs b/test/Kestrel.InMemory.FunctionalTests/ConnectionLimitTests.cs similarity index 95% rename from test/Kestrel.FunctionalTests/ConnectionLimitTests.cs rename to test/Kestrel.InMemory.FunctionalTests/ConnectionLimitTests.cs index 3e4fb75822..20c911ac33 100644 --- a/test/Kestrel.FunctionalTests/ConnectionLimitTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/ConnectionLimitTests.cs @@ -10,13 +10,14 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; +using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; using Microsoft.AspNetCore.Server.Kestrel.Tests; using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing.xunit; using Microsoft.Extensions.Logging.Testing; using Xunit; -namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { public class ConnectionLimitTests : LoggedTest { @@ -62,7 +63,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } }, max: 1)) - using (var disposables = new DisposableStack()) + using (var disposables = new DisposableStack()) { var upgraded = server.CreateConnection(); disposables.Push(upgraded); @@ -87,7 +88,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests catch { } // connection should close without sending any data - await rejected.WaitForConnectionClose().DefaultTimeout(); + await rejected.WaitForConnectionClose(); } } } @@ -103,7 +104,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests await context.Response.WriteAsync("Hello"); await requestTcs.Task; }, max)) - using (var disposables = new DisposableStack()) + using (var disposables = new DisposableStack()) { for (var i = 0; i < max; i++) { @@ -127,7 +128,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests catch { } // connection should close without sending any data - await connection.WaitForConnectionClose().DefaultTimeout(); + await connection.WaitForConnectionClose(); } } diff --git a/test/Kestrel.FunctionalTests/DefaultHeaderTests.cs b/test/Kestrel.InMemory.FunctionalTests/DefaultHeaderTests.cs similarity index 91% rename from test/Kestrel.FunctionalTests/DefaultHeaderTests.cs rename to test/Kestrel.InMemory.FunctionalTests/DefaultHeaderTests.cs index c9a564c68f..cd093c0880 100644 --- a/test/Kestrel.FunctionalTests/DefaultHeaderTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/DefaultHeaderTests.cs @@ -2,11 +2,12 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging.Testing; using Xunit; -namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { public class DefaultHeaderTests : LoggedTest { diff --git a/test/Kestrel.FunctionalTests/EventSourceTests.cs b/test/Kestrel.InMemory.FunctionalTests/EventSourceTests.cs similarity index 96% rename from test/Kestrel.FunctionalTests/EventSourceTests.cs rename to test/Kestrel.InMemory.FunctionalTests/EventSourceTests.cs index 299c530ec6..5c1c8b6cc8 100644 --- a/test/Kestrel.FunctionalTests/EventSourceTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/EventSourceTests.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.Tracing; @@ -9,11 +8,12 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; +using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging.Testing; using Xunit; -namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { public class EventSourceTests : LoggedTest { diff --git a/test/Kestrel.FunctionalTests/Http2/PipeReaderFactory.cs b/test/Kestrel.InMemory.FunctionalTests/Http2/PipeReaderFactory.cs similarity index 92% rename from test/Kestrel.FunctionalTests/Http2/PipeReaderFactory.cs rename to test/Kestrel.InMemory.FunctionalTests/Http2/PipeReaderFactory.cs index f3e2c90332..75d04b3285 100644 --- a/test/Kestrel.FunctionalTests/Http2/PipeReaderFactory.cs +++ b/test/Kestrel.InMemory.FunctionalTests/Http2/PipeReaderFactory.cs @@ -1,11 +1,14 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; +using System.IO; +using System.IO.Pipelines; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal; -namespace System.IO.Pipelines +namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.Http2 { internal class PipeReaderFactory { diff --git a/test/Kestrel.FunctionalTests/Http2/TlsTests.cs b/test/Kestrel.InMemory.FunctionalTests/Http2/TlsTests.cs similarity index 71% rename from test/Kestrel.FunctionalTests/Http2/TlsTests.cs rename to test/Kestrel.InMemory.FunctionalTests/Http2/TlsTests.cs index 28eef0675d..98d041a09c 100644 --- a/test/Kestrel.FunctionalTests/Http2/TlsTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/Http2/TlsTests.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.IO; using System.IO.Pipelines; -using System.Net; using System.Net.Security; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; @@ -17,12 +16,13 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; +using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing.xunit; using Microsoft.Extensions.Logging.Testing; using Xunit; -namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.Http2 +namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.Http2 { [OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Missing SslStream ALPN support: https://github.com/dotnet/corefx/issues/30492")] [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win81, @@ -41,31 +41,32 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.Http2 Assert.Equal(tlsFeature.ApplicationProtocol, SslApplicationProtocol.Http2.Protocol); return context.Response.WriteAsync("hello world " + context.Request.Protocol); - }, new TestServiceContext(LoggerFactory), - kestrelOptions => + }, + new TestServiceContext(LoggerFactory), + listenOptions => { - kestrelOptions.Listen(IPAddress.Loopback, 0, listenOptions => + listenOptions.Protocols = HttpProtocols.Http2; + listenOptions.UseHttps(_x509Certificate2, httpsOptions => { - listenOptions.Protocols = HttpProtocols.Http2; - listenOptions.UseHttps(_x509Certificate2, httpsOptions => - { - httpsOptions.SslProtocols = SslProtocols.Tls11 | SslProtocols.Tls12; - }); + httpsOptions.SslProtocols = SslProtocols.Tls11 | SslProtocols.Tls12; }); })) { - var connection = server.CreateConnection(); - var sslStream = new SslStream(connection.Stream); - await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions() + using (var connection = server.CreateConnection()) { - TargetHost = "localhost", - RemoteCertificateValidationCallback = (_, __, ___, ____) => true, - ApplicationProtocols = new List() { SslApplicationProtocol.Http2, SslApplicationProtocol.Http11 }, - EnabledSslProtocols = SslProtocols.Tls11, // Intentionally less than the required 1.2 - }, CancellationToken.None); + var sslStream = new SslStream(connection.Stream); + await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions() + { + TargetHost = "localhost", + RemoteCertificateValidationCallback = (_, __, ___, ____) => true, + ApplicationProtocols = new List() { SslApplicationProtocol.Http2, SslApplicationProtocol.Http11 }, + EnabledSslProtocols = SslProtocols.Tls11, // Intentionally less than the required 1.2 + }, CancellationToken.None); - var reader = PipeReaderFactory.CreateFromStream(PipeOptions.Default, sslStream, CancellationToken.None); - await WaitForConnectionErrorAsync(reader, ignoreNonGoAwayFrames: false, expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.INADEQUATE_SECURITY); + var reader = PipeReaderFactory.CreateFromStream(PipeOptions.Default, sslStream, CancellationToken.None); + await WaitForConnectionErrorAsync(reader, ignoreNonGoAwayFrames: false, expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.INADEQUATE_SECURITY); + reader.Complete(); + } } } diff --git a/test/Kestrel.FunctionalTests/HttpConnectionManagerTests.cs b/test/Kestrel.InMemory.FunctionalTests/HttpConnectionManagerTests.cs similarity index 85% rename from test/Kestrel.FunctionalTests/HttpConnectionManagerTests.cs rename to test/Kestrel.InMemory.FunctionalTests/HttpConnectionManagerTests.cs index ef8ac428b1..2ffa932aba 100644 --- a/test/Kestrel.FunctionalTests/HttpConnectionManagerTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/HttpConnectionManagerTests.cs @@ -5,7 +5,8 @@ using System; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; +using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing.xunit; using Microsoft.Extensions.Logging; @@ -13,7 +14,7 @@ using Microsoft.Extensions.Logging.Testing; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { public class HttpConnectionManagerTests : LoggedTest { @@ -31,7 +32,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests var logWh = new SemaphoreSlim(0); var appStartedWh = new SemaphoreSlim(0); - var mockTrace = new Mock(); + var mockTrace = new Mock(Logger) { CallBase = true }; mockTrace .Setup(trace => trace.ApplicationNeverCompleted(It.IsAny())) .Callback(() => @@ -39,13 +40,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests logWh.Release(); }); + var testContext = new TestServiceContext(new LoggerFactory(), mockTrace.Object); + testContext.InitializeHeartbeat(); + using (var server = new TestServer(context => { appStartedWh.Release(); var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); return tcs.Task; }, - new TestServiceContext(new LoggerFactory(), mockTrace.Object))) + testContext)) { using (var connection = server.CreateConnection()) { diff --git a/test/Kestrel.FunctionalTests/HttpProtocolSelectionTests.cs b/test/Kestrel.InMemory.FunctionalTests/HttpProtocolSelectionTests.cs similarity index 61% rename from test/Kestrel.FunctionalTests/HttpProtocolSelectionTests.cs rename to test/Kestrel.InMemory.FunctionalTests/HttpProtocolSelectionTests.cs index c52894cc35..4af9d531a3 100644 --- a/test/Kestrel.FunctionalTests/HttpProtocolSelectionTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/HttpProtocolSelectionTests.cs @@ -5,16 +5,14 @@ using System; using System.Net; using System.Text; using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; +using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Testing; using Xunit; -namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { public class HttpProtocolSelectionTests : TestApplicationErrorLoggerLoggedTest { @@ -45,22 +43,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests private async Task TestSuccess(HttpProtocols serverProtocols, string request, string expectedResponse) { - var builder = TransportSelector.GetWebHostBuilder() - .ConfigureServices(AddTestLogging) - .UseKestrel(options => - { - options.Listen(IPAddress.Loopback, 0, listenOptions => - { - listenOptions.Protocols = serverProtocols; - }); - }) - .Configure(app => app.Run(context => Task.CompletedTask)); - - using (var host = builder.Build()) + var testContext = new TestServiceContext(LoggerFactory); + var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) { - host.Start(); + Protocols = serverProtocols + }; - using (var connection = new TestConnection(host.GetPort())) + using (var server = new TestServer(context => Task.CompletedTask, testContext, listenOptions)) + { + using (var connection = server.CreateConnection()) { await connection.Send(request); await connection.Receive(expectedResponse); @@ -71,21 +62,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests private async Task TestError(HttpProtocols serverProtocols, string expectedErrorMessage) where TException : Exception { - var builder = TransportSelector.GetWebHostBuilder() - .ConfigureServices(AddTestLogging) - .UseKestrel(options => options.Listen(IPAddress.Loopback, 0, listenOptions => - { - listenOptions.Protocols = serverProtocols; - })) - .Configure(app => app.Run(context => Task.CompletedTask)); - - using (var host = builder.Build()) + var testContext = new TestServiceContext(LoggerFactory); + var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) { - host.Start(); + Protocols = serverProtocols + }; - using (var connection = new TestConnection(host.GetPort())) + using (var server = new TestServer(context => Task.CompletedTask, testContext, listenOptions)) + { + using (var connection = server.CreateConnection()) { - await connection.WaitForConnectionClose().DefaultTimeout(); + await connection.WaitForConnectionClose(); } } diff --git a/test/Kestrel.FunctionalTests/HttpsConnectionAdapterTests.cs b/test/Kestrel.InMemory.FunctionalTests/HttpsConnectionAdapterTests.cs similarity index 91% rename from test/Kestrel.FunctionalTests/HttpsConnectionAdapterTests.cs rename to test/Kestrel.InMemory.FunctionalTests/HttpsConnectionAdapterTests.cs index d4c6c0c230..572e717b80 100644 --- a/test/Kestrel.FunctionalTests/HttpsConnectionAdapterTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/HttpsConnectionAdapterTests.cs @@ -8,11 +8,9 @@ using System.Linq; using System.Net; using System.Net.Http; using System.Net.Security; -using System.Net.Sockets; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Text; -using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http; @@ -20,11 +18,12 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.AspNetCore.Server.Kestrel.Https.Internal; +using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging.Testing; using Xunit; -namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { public class HttpsConnectionAdapterTests : LoggedTest { @@ -44,7 +43,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests using (var server = new TestServer(App, new TestServiceContext(LoggerFactory), listenOptions)) { - var result = await HttpClientSlim.PostAsync($"https://localhost:{server.Port}/", + var result = await server.HttpClientSlim.PostAsync($"https://localhost:{server.Port}/", new FormUrlEncodedContent(new[] { new KeyValuePair("content", "Hello World?") }), @@ -80,7 +79,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests return context.Response.WriteAsync("hello world"); }, new TestServiceContext(LoggerFactory), listenOptions)) { - var result = await HttpClientSlim.GetStringAsync($"https://localhost:{server.Port}/", validateCertificate: false); + var result = await server.HttpClientSlim.GetStringAsync($"https://localhost:{server.Port}/", validateCertificate: false); Assert.Equal("hello world", result); } } @@ -104,7 +103,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests using (var server = new TestServer(App, new TestServiceContext(LoggerFactory), listenOptions)) { await Assert.ThrowsAnyAsync( - () => HttpClientSlim.GetStringAsync($"https://localhost:{server.Port}/")); + () => server.HttpClientSlim.GetStringAsync($"https://localhost:{server.Port}/")); } } @@ -131,7 +130,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests return context.Response.WriteAsync("hello world"); }, new TestServiceContext(LoggerFactory), listenOptions)) { - var result = await HttpClientSlim.GetStringAsync($"https://localhost:{server.Port}/", validateCertificate: false); + var result = await server.HttpClientSlim.GetStringAsync($"https://localhost:{server.Port}/", validateCertificate: false); Assert.Equal("hello world", result); } } @@ -157,12 +156,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory), listenOptions)) { - using (var client = new TcpClient()) + using (var connection = server.CreateConnection()) { // SslStream is used to ensure the certificate is actually passed to the server // HttpClient might not send the certificate because it is invalid or it doesn't match any // of the certificate authorities sent by the server in the SSL handshake. - var stream = await OpenSslStream(client, server); + var stream = OpenSslStream(connection.Stream); await stream.AuthenticateAsClientAsync("localhost", new X509CertificateCollection(), SslProtocols.Tls12 | SslProtocols.Tls11, false); Assert.True(stream.RemoteCertificate.Equals(_x509Certificate2)); } @@ -198,12 +197,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests }; using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory), listenOptions)) { - using (var client = new TcpClient()) + using (var connection = server.CreateConnection()) { // SslStream is used to ensure the certificate is actually passed to the server // HttpClient might not send the certificate because it is invalid or it doesn't match any // of the certificate authorities sent by the server in the SSL handshake. - var stream = await OpenSslStream(client, server); + var stream = OpenSslStream(connection.Stream); await stream.AuthenticateAsClientAsync("localhost", new X509CertificateCollection(), SslProtocols.Tls12 | SslProtocols.Tls11, false); Assert.True(stream.RemoteCertificate.Equals(_x509Certificate2)); Assert.Equal(1, selectorCalled); @@ -244,22 +243,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests }; using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory), listenOptions)) { - using (var client = new TcpClient()) + using (var connection = server.CreateConnection()) { // SslStream is used to ensure the certificate is actually passed to the server // HttpClient might not send the certificate because it is invalid or it doesn't match any // of the certificate authorities sent by the server in the SSL handshake. - var stream = await OpenSslStream(client, server); + var stream = OpenSslStream(connection.Stream); await stream.AuthenticateAsClientAsync("localhost", new X509CertificateCollection(), SslProtocols.Tls12 | SslProtocols.Tls11, false); Assert.True(stream.RemoteCertificate.Equals(_x509Certificate2)); Assert.Equal(1, selectorCalled); } - using (var client = new TcpClient()) + using (var connection = server.CreateConnection()) { // SslStream is used to ensure the certificate is actually passed to the server // HttpClient might not send the certificate because it is invalid or it doesn't match any // of the certificate authorities sent by the server in the SSL handshake. - var stream = await OpenSslStream(client, server); + var stream = OpenSslStream(connection.Stream); await stream.AuthenticateAsClientAsync("localhost", new X509CertificateCollection(), SslProtocols.Tls12 | SslProtocols.Tls11, false); Assert.True(stream.RemoteCertificate.Equals(_x509Certificate2NoExt)); Assert.Equal(2, selectorCalled); @@ -287,12 +286,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests }; using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory), listenOptions)) { - using (var client = new TcpClient()) + using (var connection = server.CreateConnection()) { // SslStream is used to ensure the certificate is actually passed to the server // HttpClient might not send the certificate because it is invalid or it doesn't match any // of the certificate authorities sent by the server in the SSL handshake. - var stream = await OpenSslStream(client, server); + var stream = OpenSslStream(connection.Stream); await Assert.ThrowsAsync(() => stream.AuthenticateAsClientAsync("localhost", new X509CertificateCollection(), SslProtocols.Tls12 | SslProtocols.Tls11, false)); Assert.Equal(1, selectorCalled); @@ -330,12 +329,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests }; using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory), listenOptions)) { - using (var client = new TcpClient()) + using (var connection = server.CreateConnection()) { // SslStream is used to ensure the certificate is actually passed to the server // HttpClient might not send the certificate because it is invalid or it doesn't match any // of the certificate authorities sent by the server in the SSL handshake. - var stream = await OpenSslStream(client, server); + var stream = OpenSslStream(connection.Stream); await stream.AuthenticateAsClientAsync("localhost", new X509CertificateCollection(), SslProtocols.Tls12 | SslProtocols.Tls11, false); Assert.True(stream.RemoteCertificate.Equals(_x509Certificate2)); Assert.Equal(1, selectorCalled); @@ -363,12 +362,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests }; using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory), listenOptions)) { - using (var client = new TcpClient()) + using (var connection = server.CreateConnection()) { // SslStream is used to ensure the certificate is actually passed to the server // HttpClient might not send the certificate because it is invalid or it doesn't match any // of the certificate authorities sent by the server in the SSL handshake. - var stream = await OpenSslStream(client, server); + var stream = OpenSslStream(connection.Stream); await Assert.ThrowsAsync(() => stream.AuthenticateAsClientAsync("localhost", new X509CertificateCollection(), SslProtocols.Tls12 | SslProtocols.Tls11, false)); Assert.Equal(1, selectorCalled); @@ -404,12 +403,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests return context.Response.WriteAsync("hello world"); }, new TestServiceContext(LoggerFactory), listenOptions)) { - using (var client = new TcpClient()) + using (var connection = server.CreateConnection()) { // SslStream is used to ensure the certificate is actually passed to the server // HttpClient might not send the certificate because it is invalid or it doesn't match any // of the certificate authorities sent by the server in the SSL handshake. - var stream = await OpenSslStream(client, server); + var stream = OpenSslStream(connection.Stream); await stream.AuthenticateAsClientAsync("localhost", new X509CertificateCollection(), SslProtocols.Tls12 | SslProtocols.Tls11, false); await AssertConnectionResult(stream, true); } @@ -429,7 +428,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests using (var server = new TestServer(context => context.Response.WriteAsync(context.Request.Scheme), new TestServiceContext(LoggerFactory), listenOptions)) { - var result = await HttpClientSlim.GetStringAsync($"https://localhost:{server.Port}/", validateCertificate: false); + var result = await server.HttpClientSlim.GetStringAsync($"https://localhost:{server.Port}/", validateCertificate: false); Assert.Equal("https", result); } } @@ -455,9 +454,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests // SslStream is used to ensure the certificate is actually passed to the server // HttpClient might not send the certificate because it is invalid or it doesn't match any // of the certificate authorities sent by the server in the SSL handshake. - using (var client = new TcpClient()) + using (var connection = server.CreateConnection()) { - var stream = await OpenSslStream(client, server); + var stream = OpenSslStream(connection.Stream); var ex = await Assert.ThrowsAsync( async () => await stream.AuthenticateAsClientAsync("localhost", new X509CertificateCollection(), SslProtocols.Tls, false)); } @@ -491,9 +490,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory), listenOptions)) { - using (var client = new TcpClient()) + using (var connection = server.CreateConnection()) { - var stream = await OpenSslStream(client, server); + var stream = OpenSslStream(connection.Stream); await stream.AuthenticateAsClientAsync("localhost", new X509CertificateCollection(), SslProtocols.Tls12 | SslProtocols.Tls11, false); await AssertConnectionResult(stream, true); Assert.True(clientCertificateValidationCalled); @@ -521,9 +520,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory), listenOptions)) { - using (var client = new TcpClient()) + using (var connection = server.CreateConnection()) { - var stream = await OpenSslStream(client, server); + var stream = OpenSslStream(connection.Stream); await stream.AuthenticateAsClientAsync("localhost", new X509CertificateCollection(), SslProtocols.Tls12 | SslProtocols.Tls11, false); await AssertConnectionResult(stream, false); } @@ -549,9 +548,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory), listenOptions)) { - using (var client = new TcpClient()) + using (var connection = server.CreateConnection()) { - var stream = await OpenSslStream(client, server); + var stream = OpenSslStream(connection.Stream); await stream.AuthenticateAsClientAsync("localhost", new X509CertificateCollection(), SslProtocols.Tls12 | SslProtocols.Tls11, false); await AssertConnectionResult(stream, false); } @@ -589,9 +588,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests // SslStream is used to ensure the certificate is actually passed to the server // HttpClient might not send the certificate because it is invalid or it doesn't match any // of the certificate authorities sent by the server in the SSL handshake. - using (var client = new TcpClient()) + using (var connection = server.CreateConnection()) { - var stream = await OpenSslStream(client, server); + var stream = OpenSslStream(connection.Stream); await stream.AuthenticateAsClientAsync("localhost", new X509CertificateCollection(), SslProtocols.Tls12 | SslProtocols.Tls11, false); await AssertConnectionResult(stream, true); } @@ -668,13 +667,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - private static async Task OpenSslStream(TcpClient client, TestServer server, X509Certificate2 clientCertificate = null) + private static SslStream OpenSslStream(Stream rawStream, X509Certificate2 clientCertificate = null) { - await client.ConnectAsync("127.0.0.1", server.Port); - var stream = new SslStream(client.GetStream(), false, (sender, certificate, chain, errors) => true, + return new SslStream(rawStream, false, (sender, certificate, chain, errors) => true, (sender, host, certificates, certificate, issuers) => clientCertificate ?? _x509Certificate2); - - return stream; } private static async Task AssertConnectionResult(SslStream stream, bool success) diff --git a/test/Kestrel.FunctionalTests/HttpsTests.cs b/test/Kestrel.InMemory.FunctionalTests/HttpsTests.cs similarity index 65% rename from test/Kestrel.FunctionalTests/HttpsTests.cs rename to test/Kestrel.InMemory.FunctionalTests/HttpsTests.cs index ef79d815ff..5d3ce03935 100644 --- a/test/Kestrel.FunctionalTests/HttpsTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/HttpsTests.cs @@ -4,19 +4,18 @@ using System; using System.Collections.Generic; using System.IO; -using System.Net; using System.Net.Security; -using System.Net.Sockets; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.AspNetCore.Server.Kestrel.Https.Internal; +using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -24,7 +23,7 @@ using Microsoft.Extensions.Logging.Abstractions.Internal; using Microsoft.Extensions.Logging.Testing; using Xunit; -namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { public class HttpsTests : LoggedTest { @@ -121,23 +120,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests var loggerProvider = new HandshakeErrorLoggerProvider(); LoggerFactory.AddProvider(loggerProvider); - var hostBuilder = TransportSelector.GetWebHostBuilder() - .UseKestrel(options => + using (var server = new TestServer(context => Task.CompletedTask, + new TestServiceContext(LoggerFactory), + listenOptions => { - options.Listen(new IPEndPoint(IPAddress.Loopback, 0), listenOptions => - { - listenOptions.UseHttps(TestResources.TestCertificatePath, "testPassword"); - }); - }) - .ConfigureServices(AddTestLogging) - .ConfigureLogging(builder => builder.AddProvider(loggerProvider)) - .Configure(app => { }); - - using (var host = hostBuilder.Build()) + listenOptions.UseHttps(TestResources.TestCertificatePath, "testPassword"); + })) { - host.Start(); - - using (await HttpClientSlim.GetSocket(new Uri($"http://127.0.0.1:{host.GetPort()}/"))) + using (var connection = server.CreateConnection()) { // Close socket immediately } @@ -157,26 +147,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests var loggerProvider = new HandshakeErrorLoggerProvider(); LoggerFactory.AddProvider(loggerProvider); - var hostBuilder = TransportSelector.GetWebHostBuilder() - .UseKestrel(options => + using (var server = new TestServer(context => Task.CompletedTask, + new TestServiceContext(LoggerFactory), + listenOptions => { - options.Listen(new IPEndPoint(IPAddress.Loopback, 0), listenOptions => - { - listenOptions.UseHttps(TestResources.TestCertificatePath, "testPassword"); - }); - }) - .ConfigureServices(AddTestLogging) - .Configure(app => { }); - - using (var host = hostBuilder.Build()) + listenOptions.UseHttps(TestResources.TestCertificatePath, "testPassword"); + })) { - host.Start(); - - using (var socket = await HttpClientSlim.GetSocket(new Uri($"https://127.0.0.1:{host.GetPort()}/"))) - using (var stream = new NetworkStream(socket)) + using (var connection = server.CreateConnection()) { // Send null bytes and close socket - await stream.WriteAsync(new byte[10], 0, 10); + await connection.Stream.WriteAsync(new byte[10], 0, 10); } await loggerProvider.FilterLogger.LogTcs.Task.DefaultTimeout(); @@ -194,17 +175,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { var loggerProvider = new HandshakeErrorLoggerProvider(); LoggerFactory.AddProvider(loggerProvider); - var hostBuilder = TransportSelector.GetWebHostBuilder() - .UseKestrel(options => - { - options.Listen(new IPEndPoint(IPAddress.Loopback, 0), listenOptions => - { - listenOptions.UseHttps(TestResources.TestCertificatePath, "testPassword"); - }); - }) - .ConfigureServices(AddTestLogging) - .ConfigureLogging(builder => builder.AddProvider(loggerProvider)) - .Configure(app => app.Run(async httpContext => + + using (var server = new TestServer(async httpContext => { var ct = httpContext.RequestAborted; while (!ct.IsCancellationRequested) @@ -219,15 +191,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests // Don't regard connection abort as an error } } - })); - - using (var host = hostBuilder.Build()) + }, + new TestServiceContext(LoggerFactory), + listenOptions => + { + listenOptions.UseHttps(TestResources.TestCertificatePath, "testPassword"); + })) { - host.Start(); - - using (var socket = await HttpClientSlim.GetSocket(new Uri($"https://127.0.0.1:{host.GetPort()}/"))) - using (var stream = new NetworkStream(socket, ownsSocket: false)) - using (var sslStream = new SslStream(stream, true, (sender, certificate, chain, errors) => true)) + using (var connection = server.CreateConnection()) + using (var sslStream = new SslStream(connection.Stream, true, (sender, certificate, chain, errors) => true)) { await sslStream.AuthenticateAsClientAsync("127.0.0.1", clientCertificates: null, enabledSslProtocols: SslProtocols.Tls11 | SslProtocols.Tls12, @@ -249,17 +221,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var loggerProvider = new HandshakeErrorLoggerProvider(); LoggerFactory.AddProvider(loggerProvider); - var hostBuilder = TransportSelector.GetWebHostBuilder() - .UseKestrel(options => - { - options.Listen(new IPEndPoint(IPAddress.Loopback, 0), listenOptions => - { - listenOptions.UseHttps(TestResources.TestCertificatePath, "testPassword"); - }); - }) - .ConfigureServices(AddTestLogging) - .ConfigureLogging(builder => builder.AddProvider(loggerProvider)) - .Configure(app => app.Run(async httpContext => + + using (var server = new TestServer(async httpContext => { httpContext.Abort(); try @@ -271,15 +234,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { tcs.SetException(ex); } - })); - - using (var host = hostBuilder.Build()) + }, + new TestServiceContext(LoggerFactory), + listenOptions => + { + listenOptions.UseHttps(TestResources.TestCertificatePath, "testPassword"); + })) { - host.Start(); - - using (var socket = await HttpClientSlim.GetSocket(new Uri($"https://127.0.0.1:{host.GetPort()}/"))) - using (var stream = new NetworkStream(socket, ownsSocket: false)) - using (var sslStream = new SslStream(stream, true, (sender, certificate, chain, errors) => true)) + using (var connection = server.CreateConnection()) + using (var sslStream = new SslStream(connection.Stream, true, (sender, certificate, chain, errors) => true)) { await sslStream.AuthenticateAsClientAsync("127.0.0.1", clientCertificates: null, enabledSslProtocols: SslProtocols.Tls11 | SslProtocols.Tls12, @@ -290,9 +253,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests await sslStream.ReadAsync(new byte[32], 0, 32); } - } - await tcs.Task.DefaultTimeout(); + await tcs.Task.DefaultTimeout(); + } } // Regression test for https://github.com/aspnet/KestrelHttpServer/issues/1693 @@ -301,25 +264,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { var loggerProvider = new HandshakeErrorLoggerProvider(); LoggerFactory.AddProvider(loggerProvider); - var hostBuilder = TransportSelector.GetWebHostBuilder() - .UseKestrel(options => + + using (var server = new TestServer(context => Task.CompletedTask, + new TestServiceContext(LoggerFactory), + listenOptions => { - options.Listen(new IPEndPoint(IPAddress.Loopback, 0), listenOptions => - { - listenOptions.UseHttps(TestResources.TestCertificatePath, "testPassword"); - }); - }) - .ConfigureServices(AddTestLogging) - .ConfigureLogging(builder => builder.AddProvider(loggerProvider)) - .Configure(app => app.Run(httpContext => Task.CompletedTask)); - - using (var host = hostBuilder.Build()) + listenOptions.UseHttps(TestResources.TestCertificatePath, "testPassword"); + })) { - host.Start(); - - using (var socket = await HttpClientSlim.GetSocket(new Uri($"https://127.0.0.1:{host.GetPort()}/"))) - using (var stream = new NetworkStream(socket, ownsSocket: false)) - using (var sslStream = new SslStream(stream, true, (sender, certificate, chain, errors) => true)) + using (var connection = server.CreateConnection()) + using (var sslStream = new SslStream(connection.Stream, true, (sender, certificate, chain, errors) => true)) { await sslStream.AuthenticateAsClientAsync("127.0.0.1", clientCertificates: null, enabledSslProtocols: SslProtocols.Tls11 | SslProtocols.Tls12, @@ -337,28 +291,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests var loggerProvider = new HandshakeErrorLoggerProvider(); LoggerFactory.AddProvider(loggerProvider); - var hostBuilder = TransportSelector.GetWebHostBuilder() - .UseKestrel(options => + using (var server = new TestServer(context => Task.CompletedTask, + new TestServiceContext(LoggerFactory), + listenOptions => { - options.Listen(new IPEndPoint(IPAddress.Loopback, 0), listenOptions => - { - listenOptions.UseHttps(TestResources.TestCertificatePath, "testPassword"); - }); - }) - .ConfigureServices(AddTestLogging) - .ConfigureLogging(builder => builder.AddProvider(loggerProvider)) - .Configure(app => { }); - - using (var host = hostBuilder.Build()) + listenOptions.UseHttps(TestResources.TestCertificatePath, "testPassword"); + })) { - host.Start(); - - using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) + using (var connection = server.CreateConnection()) { - socket.Connect(new IPEndPoint(IPAddress.Loopback, host.GetPort())); - - // Close socket immediately - socket.LingerState = new LingerOption(true, 0); + connection.Reset(); } } } @@ -368,30 +310,37 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { var loggerProvider = new HandshakeErrorLoggerProvider(); LoggerFactory.AddProvider(loggerProvider); - var hostBuilder = TransportSelector.GetWebHostBuilder() - .UseKestrel(options => + + var testContext = new TestServiceContext(LoggerFactory); + var heartbeatManager = new HttpHeartbeatManager(testContext.ConnectionManager); + + var handshakeStartedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + TimeSpan handshakeTimeout = default; + + using (var server = new TestServer(context => Task.CompletedTask, + testContext, + listenOptions => { - options.Listen(new IPEndPoint(IPAddress.Loopback, 0), listenOptions => + listenOptions.UseHttps(o => { - listenOptions.UseHttps(o => - { - o.ServerCertificate = new X509Certificate2(TestResources.TestCertificatePath, "testPassword"); - o.HandshakeTimeout = TimeSpan.FromSeconds(1); - }); + o.ServerCertificate = new X509Certificate2(TestResources.TestCertificatePath, "testPassword"); + o.OnHandshakeStarted = () => handshakeStartedTcs.SetResult(null); + + handshakeTimeout = o.HandshakeTimeout; }); - }) - .ConfigureServices(AddTestLogging) - .Configure(app => app.Run(httpContext => Task.CompletedTask)); - - using (var host = hostBuilder.Build()) + })) { - host.Start(); - - using (var socket = await HttpClientSlim.GetSocket(new Uri($"https://127.0.0.1:{host.GetPort()}/"))) - using (var stream = new NetworkStream(socket, ownsSocket: false)) + using (var connection = server.CreateConnection()) { - // No data should be sent and the connection should be closed in well under 30 seconds. - Assert.Equal(0, await stream.ReadAsync(new byte[1], 0, 1).DefaultTimeout()); + // HttpsConnectionAdapter dispatches via Task.Run() before starting the handshake. + // Wait for the handshake to start before advancing the system clock. + await handshakeStartedTcs.Task.DefaultTimeout(); + + // Min amount of time between requests that triggers a handshake timeout. + testContext.MockSystemClock.UtcNow += handshakeTimeout + Heartbeat.Interval + TimeSpan.FromTicks(1); + heartbeatManager.OnHeartbeat(testContext.SystemClock.UtcNow); + + Assert.Equal(0, await connection.Stream.ReadAsync(new byte[1], 0, 1).DefaultTimeout()); } } @@ -405,24 +354,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { var loggerProvider = new HandshakeErrorLoggerProvider(); LoggerFactory.AddProvider(loggerProvider); - var hostBuilder = TransportSelector.GetWebHostBuilder() - .UseKestrel(options => + + using (var server = new TestServer(context => Task.CompletedTask, + new TestServiceContext(LoggerFactory), + listenOptions => { - options.Listen(new IPEndPoint(IPAddress.Loopback, 0), listenOptions => - { - listenOptions.UseHttps(TestResources.TestCertificatePath, "testPassword"); - }); - }) - .ConfigureServices(AddTestLogging) - .Configure(app => app.Run(httpContext => Task.CompletedTask)); - - using (var host = hostBuilder.Build()) + listenOptions.UseHttps(TestResources.TestCertificatePath, "testPassword"); + })) { - host.Start(); - - using (var socket = await HttpClientSlim.GetSocket(new Uri($"https://127.0.0.1:{host.GetPort()}/"))) - using (var stream = new NetworkStream(socket, ownsSocket: false)) - using (var sslStream = new SslStream(stream, true, (sender, certificate, chain, errors) => true)) + using (var connection = server.CreateConnection()) + using (var sslStream = new SslStream(connection.Stream, true, (sender, certificate, chain, errors) => true)) { // SslProtocols.Tls is TLS 1.0 which isn't supported by Kestrel by default. await Assert.ThrowsAsync(() => diff --git a/test/Kestrel.InMemory.FunctionalTests/KeepAliveTimeoutTests.cs b/test/Kestrel.InMemory.FunctionalTests/KeepAliveTimeoutTests.cs new file mode 100644 index 0000000000..2cf8739ca4 --- /dev/null +++ b/test/Kestrel.InMemory.FunctionalTests/KeepAliveTimeoutTests.cs @@ -0,0 +1,266 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; +using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Logging.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests +{ + public class KeepAliveTimeoutTests : LoggedTest + { + private static readonly TimeSpan _keepAliveTimeout = TimeSpan.FromSeconds(10); + private static readonly TimeSpan _longDelay = TimeSpan.FromSeconds(30); + private static readonly TimeSpan _shortDelay = TimeSpan.FromSeconds(_longDelay.TotalSeconds / 10); + + private readonly TaskCompletionSource _firstRequestReceived = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + [Fact] + public async Task ConnectionClosedWhenKeepAliveTimeoutExpires() + { + var testContext = new TestServiceContext(LoggerFactory); + var heartbeatManager = new HttpHeartbeatManager(testContext.ConnectionManager); + + using (var server = CreateServer(testContext)) + using (var connection = server.CreateConnection()) + { + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "", + ""); + await ReceiveResponse(connection, testContext); + + // Min amount of time between requests that triggers a keep-alive timeout. + testContext.MockSystemClock.UtcNow += _keepAliveTimeout + Heartbeat.Interval + TimeSpan.FromTicks(1); + heartbeatManager.OnHeartbeat(testContext.SystemClock.UtcNow); + + await connection.WaitForConnectionClose(); + } + } + + [Fact] + public async Task ConnectionKeptAliveBetweenRequests() + { + var testContext = new TestServiceContext(LoggerFactory); + var heartbeatManager = new HttpHeartbeatManager(testContext.ConnectionManager); + + using (var server = CreateServer(testContext)) + using (var connection = server.CreateConnection()) + { + for (var i = 0; i < 10; i++) + { + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "", + ""); + await ReceiveResponse(connection, testContext); + + // Max amount of time between requests that doesn't trigger a keep-alive timeout. + testContext.MockSystemClock.UtcNow += _keepAliveTimeout + Heartbeat.Interval; + heartbeatManager.OnHeartbeat(testContext.SystemClock.UtcNow); + } + } + } + + [Fact] + public async Task ConnectionNotTimedOutWhileRequestBeingSent() + { + var testContext = new TestServiceContext(LoggerFactory); + var heartbeatManager = new HttpHeartbeatManager(testContext.ConnectionManager); + + using (var server = CreateServer(testContext)) + using (var connection = server.CreateConnection()) + { + await connection.Send( + "POST /consume HTTP/1.1", + "Host:", + "Transfer-Encoding: chunked", + "", + ""); + + await _firstRequestReceived.Task.DefaultTimeout(); + + for (var totalDelay = TimeSpan.Zero; totalDelay < _longDelay; totalDelay += _shortDelay) + { + await connection.Send( + "1", + "a", + ""); + + testContext.MockSystemClock.UtcNow += _shortDelay; + heartbeatManager.OnHeartbeat(testContext.SystemClock.UtcNow); + } + + await connection.Send( + "0", + "", + ""); + await ReceiveResponse(connection, testContext); + } + } + + [Fact] + private async Task ConnectionNotTimedOutWhileAppIsRunning() + { + var testContext = new TestServiceContext(LoggerFactory); + var heartbeatManager = new HttpHeartbeatManager(testContext.ConnectionManager); + var cts = new CancellationTokenSource(); + + using (var server = CreateServer(testContext, longRunningCt: cts.Token)) + using (var connection = server.CreateConnection()) + { + await connection.Send( + "GET /longrunning HTTP/1.1", + "Host:", + "", + ""); + + await _firstRequestReceived.Task.DefaultTimeout(); + + for (var totalDelay = TimeSpan.Zero; totalDelay < _longDelay; totalDelay += _shortDelay) + { + testContext.MockSystemClock.UtcNow += _shortDelay; + heartbeatManager.OnHeartbeat(testContext.SystemClock.UtcNow); + } + + cts.Cancel(); + + await ReceiveResponse(connection, testContext); + + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "", + ""); + await ReceiveResponse(connection, testContext); + } + } + + [Fact] + private async Task ConnectionTimesOutWhenOpenedButNoRequestSent() + { + var testContext = new TestServiceContext(LoggerFactory); + var heartbeatManager = new HttpHeartbeatManager(testContext.ConnectionManager); + + using (var server = CreateServer(testContext)) + using (var connection = server.CreateConnection()) + { + // Min amount of time between requests that triggers a keep-alive timeout. + testContext.MockSystemClock.UtcNow += _keepAliveTimeout + Heartbeat.Interval + TimeSpan.FromTicks(1); + heartbeatManager.OnHeartbeat(testContext.SystemClock.UtcNow); + + await connection.WaitForConnectionClose(); + } + } + + [Fact] + private async Task KeepAliveTimeoutDoesNotApplyToUpgradedConnections() + { + var testContext = new TestServiceContext(LoggerFactory); + var heartbeatManager = new HttpHeartbeatManager(testContext.ConnectionManager); + var cts = new CancellationTokenSource(); + + using (var server = CreateServer(testContext, upgradeCt: cts.Token)) + using (var connection = server.CreateConnection()) + { + await connection.Send( + "GET /upgrade HTTP/1.1", + "Host:", + "Connection: Upgrade", + "", + ""); + await connection.Receive( + "HTTP/1.1 101 Switching Protocols", + "Connection: Upgrade", + $"Date: {testContext.DateHeaderValue}", + "", + ""); + + for (var totalDelay = TimeSpan.Zero; totalDelay < _longDelay; totalDelay += _shortDelay) + { + testContext.MockSystemClock.UtcNow += _shortDelay; + heartbeatManager.OnHeartbeat(testContext.SystemClock.UtcNow); + } + + cts.Cancel(); + + await connection.Receive("hello, world"); + } + } + + private TestServer CreateServer(TestServiceContext context, CancellationToken longRunningCt = default, CancellationToken upgradeCt = default) + { + context.ServerOptions.AddServerHeader = false; + context.ServerOptions.Limits.KeepAliveTimeout = _keepAliveTimeout; + context.ServerOptions.Limits.MinRequestBodyDataRate = null; + + return new TestServer(httpContext => App(httpContext, longRunningCt, upgradeCt), context); + } + + private async Task App(HttpContext httpContext, CancellationToken longRunningCt, CancellationToken upgradeCt) + { + var ct = httpContext.RequestAborted; + var responseStream = httpContext.Response.Body; + var responseBytes = Encoding.ASCII.GetBytes("hello, world"); + + _firstRequestReceived.TrySetResult(null); + + if (httpContext.Request.Path == "/longrunning") + { + await CancellationTokenAsTask(longRunningCt); + } + else if (httpContext.Request.Path == "/upgrade") + { + using (var stream = await httpContext.Features.Get().UpgradeAsync()) + { + await CancellationTokenAsTask(upgradeCt); + + responseStream = stream; + } + } + else if (httpContext.Request.Path == "/consume") + { + var buffer = new byte[1024]; + while (await httpContext.Request.Body.ReadAsync(buffer, 0, buffer.Length) > 0) ; + } + + await responseStream.WriteAsync(responseBytes, 0, responseBytes.Length); + } + + private async Task ReceiveResponse(InMemoryConnection connection, TestServiceContext testContext) + { + await connection.Receive( + "HTTP/1.1 200 OK", + $"Date: {testContext.DateHeaderValue}", + "Transfer-Encoding: chunked", + "", + "c", + "hello, world", + "0", + "", + ""); + } + + private static Task CancellationTokenAsTask(CancellationToken token) + { + if (token.IsCancellationRequested) + { + return Task.CompletedTask; + } + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + token.Register(() => tcs.SetResult(null)); + return tcs.Task; + } + } +} \ No newline at end of file diff --git a/test/Kestrel.InMemory.FunctionalTests/Kestrel.InMemory.FunctionalTests.csproj b/test/Kestrel.InMemory.FunctionalTests/Kestrel.InMemory.FunctionalTests.csproj new file mode 100644 index 0000000000..6d4915600c --- /dev/null +++ b/test/Kestrel.InMemory.FunctionalTests/Kestrel.InMemory.FunctionalTests.csproj @@ -0,0 +1,28 @@ + + + + InMemory.FunctionalTests + Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests + $(StandardTestTfms) + true + + + + + + + + + + + + + + + + + + + + + diff --git a/test/Kestrel.InMemory.FunctionalTests/LoggingConnectionAdapterTests.cs b/test/Kestrel.InMemory.FunctionalTests/LoggingConnectionAdapterTests.cs new file mode 100644 index 0000000000..5eca246b35 --- /dev/null +++ b/test/Kestrel.InMemory.FunctionalTests/LoggingConnectionAdapterTests.cs @@ -0,0 +1,40 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Logging.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests +{ + public class LoggingConnectionAdapterTests : LoggedTest + { + [Fact] + public async Task LoggingConnectionAdapterCanBeAddedBeforeAndAfterHttpsAdapter() + { + using (var server = new TestServer(context => + { + context.Response.ContentLength = 12; + return context.Response.WriteAsync("Hello World!"); + }, + new TestServiceContext(LoggerFactory), + listenOptions => + { + listenOptions.UseConnectionLogging(); + listenOptions.UseHttps(TestResources.TestCertificatePath, "testPassword"); + listenOptions.UseConnectionLogging(); + })) + { + var response = await server.HttpClientSlim.GetStringAsync($"https://localhost:{server.Port}/", validateCertificate: false) + .DefaultTimeout(); + + + Assert.Equal("Hello World!", response); + } + } + } +} diff --git a/test/Kestrel.FunctionalTests/MaxRequestBodySizeTests.cs b/test/Kestrel.InMemory.FunctionalTests/MaxRequestBodySizeTests.cs similarity index 99% rename from test/Kestrel.FunctionalTests/MaxRequestBodySizeTests.cs rename to test/Kestrel.InMemory.FunctionalTests/MaxRequestBodySizeTests.cs index 4bc664f558..351bf7f7ec 100644 --- a/test/Kestrel.FunctionalTests/MaxRequestBodySizeTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/MaxRequestBodySizeTests.cs @@ -7,11 +7,12 @@ using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging.Testing; using Xunit; -namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { public class MaxRequestBodySizeTests : LoggedTest { diff --git a/test/Kestrel.FunctionalTests/MaxRequestLineSizeTests.cs b/test/Kestrel.InMemory.FunctionalTests/MaxRequestLineSizeTests.cs similarity index 92% rename from test/Kestrel.FunctionalTests/MaxRequestLineSizeTests.cs rename to test/Kestrel.InMemory.FunctionalTests/MaxRequestLineSizeTests.cs index 6007877262..27f903e64d 100644 --- a/test/Kestrel.FunctionalTests/MaxRequestLineSizeTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/MaxRequestLineSizeTests.cs @@ -4,11 +4,12 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging.Testing; using Xunit; -namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { public class MaxRequestLineSizeTests : LoggedTest { @@ -29,7 +30,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { using (var server = CreateServer(limit)) { - using (var connection = new TestConnection(server.Port)) + using (var connection = server.CreateConnection()) { await connection.Send(request); await connection.ReceiveEnd( @@ -55,7 +56,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { using (var server = CreateServer(requestLine.Length - 1)) { - using (var connection = new TestConnection(server.Port)) + using (var connection = server.CreateConnection()) { await connection.SendAll(requestLine); await connection.ReceiveForcedEnd( diff --git a/test/Kestrel.InMemory.FunctionalTests/Properties/AssemblyInfo.cs b/test/Kestrel.InMemory.FunctionalTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..678468c757 --- /dev/null +++ b/test/Kestrel.InMemory.FunctionalTests/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Extensions.Logging.Testing; + +[assembly: ShortClassName] diff --git a/test/Kestrel.FunctionalTests/RequestBodyTimeoutTests.cs b/test/Kestrel.InMemory.FunctionalTests/RequestBodyTimeoutTests.cs similarity index 89% rename from test/Kestrel.FunctionalTests/RequestBodyTimeoutTests.cs rename to test/Kestrel.InMemory.FunctionalTests/RequestBodyTimeoutTests.cs index f6371ff051..e308b03a99 100644 --- a/test/Kestrel.FunctionalTests/RequestBodyTimeoutTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/RequestBodyTimeoutTests.cs @@ -2,19 +2,18 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Features; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; +using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; using Xunit; -namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { public class RequestBodyTimeoutTests : LoggedTest { @@ -22,12 +21,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests public async Task RequestTimesOutWhenRequestBodyNotReceivedAtSpecifiedMinimumRate() { var gracePeriod = TimeSpan.FromSeconds(5); - var systemClock = new MockSystemClock(); - var serviceContext = new TestServiceContext(LoggerFactory) - { - SystemClock = systemClock, - DateHeaderValueManager = new DateHeaderValueManager(systemClock) - }; + var serviceContext = new TestServiceContext(LoggerFactory); + var heartbeatManager = new HttpHeartbeatManager(serviceContext.ConnectionManager); var appRunningEvent = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -74,7 +69,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests ""); await appRunningEvent.Task.DefaultTimeout(); - systemClock.UtcNow += gracePeriod + TimeSpan.FromSeconds(1); + + serviceContext.MockSystemClock.UtcNow += gracePeriod + TimeSpan.FromSeconds(1); + heartbeatManager.OnHeartbeat(serviceContext.SystemClock.UtcNow); await connection.Receive( "HTTP/1.1 408 Request Timeout", @@ -93,12 +90,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests public async Task RequestTimesOutWhenNotDrainedWithinDrainTimeoutPeriod() { // This test requires a real clock since we can't control when the drain timeout is set - var systemClock = new SystemClock(); - var serviceContext = new TestServiceContext(LoggerFactory) - { - SystemClock = systemClock, - DateHeaderValueManager = new DateHeaderValueManager(systemClock), - }; + var serviceContext = new TestServiceContext(LoggerFactory); + serviceContext.InitializeHeartbeat(); var appRunningEvent = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -142,12 +135,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests public async Task ConnectionClosedEvenIfAppSwallowsException() { var gracePeriod = TimeSpan.FromSeconds(5); - var systemClock = new MockSystemClock(); - var serviceContext = new TestServiceContext(LoggerFactory) - { - SystemClock = systemClock, - DateHeaderValueManager = new DateHeaderValueManager(systemClock) - }; + var serviceContext = new TestServiceContext(LoggerFactory); + var heartbeatManager = new HttpHeartbeatManager(serviceContext.ConnectionManager); var appRunningTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var exceptionSwallowedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -190,7 +179,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests ""); await appRunningTcs.Task.DefaultTimeout(); - systemClock.UtcNow += gracePeriod + TimeSpan.FromSeconds(1); + + serviceContext.MockSystemClock.UtcNow += gracePeriod + TimeSpan.FromSeconds(1); + heartbeatManager.OnHeartbeat(serviceContext.SystemClock.UtcNow); + await exceptionSwallowedTcs.Task.DefaultTimeout(); await connection.Receive( diff --git a/test/Kestrel.FunctionalTests/RequestHeaderLimitsTests.cs b/test/Kestrel.InMemory.FunctionalTests/RequestHeaderLimitsTests.cs similarity index 92% rename from test/Kestrel.FunctionalTests/RequestHeaderLimitsTests.cs rename to test/Kestrel.InMemory.FunctionalTests/RequestHeaderLimitsTests.cs index 6de4c37a99..6976cb5953 100644 --- a/test/Kestrel.FunctionalTests/RequestHeaderLimitsTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/RequestHeaderLimitsTests.cs @@ -5,11 +5,12 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging.Testing; using Xunit; -namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { public class RequestHeaderLimitsTests : LoggedTest { @@ -28,7 +29,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests using (var server = CreateServer(maxRequestHeadersTotalSize: headers.Length + extraLimit)) { - using (var connection = new TestConnection(server.Port)) + using (var connection = server.CreateConnection()) { await connection.Send($"GET / HTTP/1.1\r\n{headers}\r\n"); await connection.ReceiveEnd( @@ -60,7 +61,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests using (var server = CreateServer(maxRequestHeaderCount: maxHeaderCount)) { - using (var connection = new TestConnection(server.Port)) + using (var connection = server.CreateConnection()) { await connection.Send($"GET / HTTP/1.1\r\n{headers}\r\n"); await connection.ReceiveEnd( @@ -86,7 +87,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests using (var server = CreateServer(maxRequestHeadersTotalSize: headers.Length - 1)) { - using (var connection = new TestConnection(server.Port)) + using (var connection = server.CreateConnection()) { await connection.SendAll($"GET / HTTP/1.1\r\n{headers}\r\n"); await connection.ReceiveForcedEnd( @@ -110,7 +111,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests using (var server = CreateServer(maxRequestHeaderCount: maxHeaderCount)) { - using (var connection = new TestConnection(server.Port)) + using (var connection = server.CreateConnection()) { await connection.SendAll($"GET / HTTP/1.1\r\n{headers}\r\n"); await connection.ReceiveForcedEnd( diff --git a/test/Kestrel.InMemory.FunctionalTests/RequestHeadersTimeoutTests.cs b/test/Kestrel.InMemory.FunctionalTests/RequestHeadersTimeoutTests.cs new file mode 100644 index 0000000000..d6a924bf61 --- /dev/null +++ b/test/Kestrel.InMemory.FunctionalTests/RequestHeadersTimeoutTests.cs @@ -0,0 +1,159 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO.Pipelines; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; +using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Logging.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests +{ + public class RequestHeadersTimeoutTests : LoggedTest + { + private static readonly TimeSpan RequestHeadersTimeout = TimeSpan.FromSeconds(10); + private static readonly TimeSpan LongDelay = TimeSpan.FromSeconds(30); + private static readonly TimeSpan ShortDelay = TimeSpan.FromSeconds(LongDelay.TotalSeconds / 10); + + [Theory] + [InlineData("Host:\r\n")] + [InlineData("Host:\r\nContent-Length: 1\r\n")] + [InlineData("Host:\r\nContent-Length: 1\r\n\r")] + public async Task ConnectionAbortedWhenRequestHeadersNotReceivedInTime(string headers) + { + var testContext = new TestServiceContext(LoggerFactory); + var heartbeatManager = new HttpHeartbeatManager(testContext.ConnectionManager); + + using (var server = CreateServer(testContext)) + using (var connection = server.CreateConnection()) + { + await connection.Send( + "GET / HTTP/1.1", + headers); + + // Min amount of time between requests that triggers a request headers timeout. + testContext.MockSystemClock.UtcNow += RequestHeadersTimeout + Heartbeat.Interval + TimeSpan.FromTicks(1); + heartbeatManager.OnHeartbeat(testContext.SystemClock.UtcNow); + + await ReceiveTimeoutResponse(connection, testContext); + } + } + + [Fact] + public async Task RequestHeadersTimeoutCanceledAfterHeadersReceived() + { + var testContext = new TestServiceContext(LoggerFactory); + var heartbeatManager = new HttpHeartbeatManager(testContext.ConnectionManager); + + using (var server = CreateServer(testContext)) + using (var connection = server.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + "Host:", + "Content-Length: 1", + "", + ""); + + // Min amount of time between requests that triggers a request headers timeout. + testContext.MockSystemClock.UtcNow += RequestHeadersTimeout + Heartbeat.Interval + TimeSpan.FromTicks(1); + heartbeatManager.OnHeartbeat(testContext.SystemClock.UtcNow); + + await connection.Send( + "a"); + + await ReceiveResponse(connection, testContext); + } + } + + [Theory] + [InlineData("P")] + [InlineData("POST / HTTP/1.1\r")] + public async Task ConnectionAbortedWhenRequestLineNotReceivedInTime(string requestLine) + { + var testContext = new TestServiceContext(LoggerFactory); + var heartbeatManager = new HttpHeartbeatManager(testContext.ConnectionManager); + + using (var server = CreateServer(testContext)) + using (var connection = server.CreateConnection()) + { + await connection.Send(requestLine); + + // Min amount of time between requests that triggers a request headers timeout. + testContext.MockSystemClock.UtcNow += RequestHeadersTimeout + Heartbeat.Interval + TimeSpan.FromTicks(1); + heartbeatManager.OnHeartbeat(testContext.SystemClock.UtcNow); + + await ReceiveTimeoutResponse(connection, testContext); + } + } + + [Fact] + public async Task TimeoutNotResetOnEachRequestLineCharacterReceived() + { + var testContext = new TestServiceContext(LoggerFactory); + var heartbeatManager = new HttpHeartbeatManager(testContext.ConnectionManager); + + using (var server = CreateServer(testContext)) + using (var connection = server.CreateConnection()) + { + // When the in-memory connection is aborted, the input PipeWriter is completed behind the scenes + // so eventually connection.Send() throws an InvalidOperationException. + await Assert.ThrowsAsync(async () => + { + foreach (var ch in "POST / HTTP/1.1\r\nHost:\r\n\r\n") + { + await connection.Send(ch.ToString()); + + testContext.MockSystemClock.UtcNow += ShortDelay; + heartbeatManager.OnHeartbeat(testContext.SystemClock.UtcNow); + } + }); + + await ReceiveTimeoutResponse(connection, testContext); + } + } + + private TestServer CreateServer(TestServiceContext context) + { + // Ensure request headers timeout is started as soon as the tests send requests. + context.Scheduler = PipeScheduler.Inline; + context.ServerOptions.Limits.RequestHeadersTimeout = RequestHeadersTimeout; + context.ServerOptions.Limits.MinRequestBodyDataRate = null; + + return new TestServer(async httpContext => + { + await httpContext.Request.Body.ReadAsync(new byte[1], 0, 1); + await httpContext.Response.WriteAsync("hello, world"); + }, context); + } + + private async Task ReceiveResponse(InMemoryConnection connection, TestServiceContext testContext) + { + await connection.Receive( + "HTTP/1.1 200 OK", + $"Date: {testContext.DateHeaderValue}", + "Transfer-Encoding: chunked", + "", + "c", + "hello, world", + "0", + "", + ""); + } + + private async Task ReceiveTimeoutResponse(InMemoryConnection connection, TestServiceContext testContext) + { + await connection.Receive( + "HTTP/1.1 408 Request Timeout", + "Connection: close", + $"Date: {testContext.DateHeaderValue}", + "Content-Length: 0", + "", + ""); + } + } +} \ No newline at end of file diff --git a/test/Kestrel.FunctionalTests/RequestTargetProcessingTests.cs b/test/Kestrel.InMemory.FunctionalTests/RequestTargetProcessingTests.cs similarity index 94% rename from test/Kestrel.FunctionalTests/RequestTargetProcessingTests.cs rename to test/Kestrel.InMemory.FunctionalTests/RequestTargetProcessingTests.cs index 6bf18030a3..8cfd3edaf9 100644 --- a/test/Kestrel.FunctionalTests/RequestTargetProcessingTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/RequestTargetProcessingTests.cs @@ -1,18 +1,17 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System.Net; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; +using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging.Testing; using Xunit; -namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { public class RequestTargetProcessingTests : LoggedTest { @@ -20,7 +19,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests public async Task RequestPathIsNotNormalized() { var testContext = new TestServiceContext(LoggerFactory); - var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); using (var server = new TestServer(async context => { @@ -28,7 +26,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests context.Response.Headers.ContentLength = 11; await context.Response.WriteAsync("Hello World"); - }, testContext, listenOptions)) + }, testContext)) { using (var connection = server.CreateConnection()) { diff --git a/test/Kestrel.InMemory.FunctionalTests/RequestTests.cs b/test/Kestrel.InMemory.FunctionalTests/RequestTests.cs new file mode 100644 index 0000000000..07c4d642e3 --- /dev/null +++ b/test/Kestrel.InMemory.FunctionalTests/RequestTests.cs @@ -0,0 +1,1062 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Concurrent; +using System.IO; +using System.IO.Pipelines; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Core.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; +using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Logging.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests +{ + public class RequestTests : LoggedTest + { + [Fact] + public async Task StreamsAreNotPersistedAcrossRequests() + { + var requestBodyPersisted = false; + var responseBodyPersisted = false; + + using (var server = new TestServer(async context => + { + if (context.Request.Body is MemoryStream) + { + requestBodyPersisted = true; + } + + if (context.Response.Body is MemoryStream) + { + responseBodyPersisted = true; + } + + context.Request.Body = new MemoryStream(); + context.Response.Body = new MemoryStream(); + + await context.Response.WriteAsync("hello, world"); + }, new TestServiceContext(LoggerFactory))) + { + Assert.Equal(string.Empty, await server.HttpClientSlim.GetStringAsync($"http://localhost:{server.Port}/")); + Assert.Equal(string.Empty, await server.HttpClientSlim.GetStringAsync($"http://localhost:{server.Port}/")); + + Assert.False(requestBodyPersisted); + Assert.False(responseBodyPersisted); + } + } + + [Fact] + public async Task CanUpgradeRequestWithConnectionKeepAliveUpgradeHeader() + { + var testContext = new TestServiceContext(); + var dataRead = false; + + using (var server = new TestServer(async context => + { + var stream = await context.Features.Get().UpgradeAsync(); + var data = new byte[3]; + var bytesRead = 0; + + while (bytesRead < 3) + { + bytesRead += await stream.ReadAsync(data, bytesRead, data.Length - bytesRead); + } + + dataRead = Encoding.ASCII.GetString(data, 0, 3) == "abc"; + }, new TestServiceContext(LoggerFactory))) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "GET / HTTP/1.1", + "Host:\r\nConnection: keep-alive, upgrade", + "", + "abc"); + + await connection.ReceiveEnd( + "HTTP/1.1 101 Switching Protocols", + "Connection: Upgrade", + $"Date: {testContext.DateHeaderValue}", + "", + ""); + } + } + + Assert.True(dataRead); + } + + [Theory] + [InlineData("http://localhost/abs/path", "/abs/path", null)] + [InlineData("https://localhost/abs/path", "/abs/path", null)] // handles mismatch scheme + [InlineData("https://localhost:22/abs/path", "/abs/path", null)] // handles mismatched ports + [InlineData("https://differenthost/abs/path", "/abs/path", null)] // handles mismatched hostname + [InlineData("http://localhost/", "/", null)] + [InlineData("http://root@contoso.com/path", "/path", null)] + [InlineData("http://root:password@contoso.com/path", "/path", null)] + [InlineData("https://localhost/", "/", null)] + [InlineData("http://localhost", "/", null)] + [InlineData("http://127.0.0.1/", "/", null)] + [InlineData("http://[::1]/", "/", null)] + [InlineData("http://[::1]:8080/", "/", null)] + [InlineData("http://localhost?q=123&w=xyz", "/", "123")] + [InlineData("http://localhost/?q=123&w=xyz", "/", "123")] + [InlineData("http://localhost/path?q=123&w=xyz", "/path", "123")] + [InlineData("http://localhost/path%20with%20space?q=abc%20123", "/path with space", "abc 123")] + public async Task CanHandleRequestsWithUrlInAbsoluteForm(string requestUrl, string expectedPath, string queryValue) + { + var pathTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var rawTargetTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var queryTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + using (var server = new TestServer(async context => + { + pathTcs.TrySetResult(context.Request.Path); + queryTcs.TrySetResult(context.Request.Query); + rawTargetTcs.TrySetResult(context.Features.Get().RawTarget); + await context.Response.WriteAsync("Done"); + }, new TestServiceContext(LoggerFactory))) + { + using (var connection = server.CreateConnection()) + { + var requestTarget = new Uri(requestUrl, UriKind.Absolute); + var host = requestTarget.Authority; + if (requestTarget.IsDefaultPort) + { + host += ":" + requestTarget.Port; + } + + await connection.Send( + $"GET {requestUrl} HTTP/1.1", + "Content-Length: 0", + $"Host: {host}", + "", + ""); + + await connection.Receive($"HTTP/1.1 200 OK", + $"Date: {server.Context.DateHeaderValue}", + "Transfer-Encoding: chunked", + "", + "4", + "Done"); + + await Task.WhenAll(pathTcs.Task, rawTargetTcs.Task, queryTcs.Task).DefaultTimeout(); + Assert.Equal(new PathString(expectedPath), pathTcs.Task.Result); + Assert.Equal(requestUrl, rawTargetTcs.Task.Result); + if (queryValue == null) + { + Assert.False(queryTcs.Task.Result.ContainsKey("q")); + } + else + { + Assert.Equal(queryValue, queryTcs.Task.Result["q"]); + } + } + } + } + + [Fact] + public async Task AppCanSetTraceIdentifier() + { + const string knownId = "xyz123"; + using (var server = new TestServer(async context => + { + context.TraceIdentifier = knownId; + await context.Response.WriteAsync(context.TraceIdentifier); + }, new TestServiceContext(LoggerFactory))) + { + var requestId = await server.HttpClientSlim.GetStringAsync($"http://localhost:{server.Port}/"); + Assert.Equal(knownId, requestId); + } + } + + [Fact] + public async Task TraceIdentifierIsUnique() + { + const int identifierLength = 22; + const int iterations = 10; + + using (var server = new TestServer(async context => + { + Assert.Equal(identifierLength, Encoding.ASCII.GetByteCount(context.TraceIdentifier)); + context.Response.ContentLength = identifierLength; + await context.Response.WriteAsync(context.TraceIdentifier); + }, new TestServiceContext(LoggerFactory))) + { + var usedIds = new ConcurrentBag(); + + // requests on separate connections in parallel + Parallel.For(0, iterations, async i => + { + var id = await server.HttpClientSlim.GetStringAsync($"http://localhost:{server.Port}/"); + Assert.DoesNotContain(id, usedIds.ToArray()); + usedIds.Add(id); + }); + + // requests on same connection + using (var connection = server.CreateConnection()) + { + var buffer = new char[identifierLength]; + for (var i = 0; i < iterations; i++) + { + await connection.SendEmptyGet(); + + await connection.Receive($"HTTP/1.1 200 OK", + $"Date: {server.Context.DateHeaderValue}", + $"Content-Length: {identifierLength}", + "", + ""); + + var read = await connection.Reader.ReadAsync(buffer, 0, identifierLength); + Assert.Equal(identifierLength, read); + var id = new string(buffer, 0, read); + Assert.DoesNotContain(id, usedIds.ToArray()); + usedIds.Add(id); + } + } + } + } + + [Fact] + public async Task Http11KeptAliveByDefault() + { + var testContext = new TestServiceContext(LoggerFactory); + + using (var server = new TestServer(TestApp.EchoAppChunked, testContext)) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "", + "GET / HTTP/1.1", + "Host:", + "Connection: close", + "Content-Length: 7", + "", + "Goodbye"); + await connection.ReceiveEnd( + "HTTP/1.1 200 OK", + $"Date: {testContext.DateHeaderValue}", + "Content-Length: 0", + "", + "HTTP/1.1 200 OK", + "Connection: close", + $"Date: {testContext.DateHeaderValue}", + "Content-Length: 7", + "", + "Goodbye"); + } + } + } + + + [Fact] + public async Task Http10NotKeptAliveByDefault() + { + var testContext = new TestServiceContext(LoggerFactory); + + using (var server = new TestServer(TestApp.EchoApp, testContext)) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "GET / HTTP/1.0", + "", + ""); + await connection.ReceiveForcedEnd( + "HTTP/1.1 200 OK", + "Connection: close", + $"Date: {testContext.DateHeaderValue}", + "Content-Length: 0", + "", + ""); + } + + using (var connection = server.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.0", + "Content-Length: 11", + "", + "Hello World"); + await connection.ReceiveForcedEnd( + "HTTP/1.1 200 OK", + "Connection: close", + $"Date: {testContext.DateHeaderValue}", + "", + "Hello World"); + } + } + } + + [Fact] + public async Task Http10KeepAlive() + { + var testContext = new TestServiceContext(LoggerFactory); + + using (var server = new TestServer(TestApp.EchoAppChunked, testContext)) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "GET / HTTP/1.0", + "Connection: keep-alive", + "", + "POST / HTTP/1.0", + "Content-Length: 7", + "", + "Goodbye"); + await connection.Receive( + "HTTP/1.1 200 OK", + "Connection: keep-alive", + $"Date: {testContext.DateHeaderValue}", + "Content-Length: 0", + "\r\n"); + await connection.ReceiveForcedEnd( + "HTTP/1.1 200 OK", + "Connection: close", + $"Date: {testContext.DateHeaderValue}", + "Content-Length: 7", + "", + "Goodbye"); + } + } + } + + [Fact] + public async Task Http10KeepAliveNotHonoredIfResponseContentLengthNotSet() + { + var testContext = new TestServiceContext(LoggerFactory); + + using (var server = new TestServer(TestApp.EchoApp, testContext)) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "GET / HTTP/1.0", + "Connection: keep-alive", + "", + ""); + + await connection.Receive( + "HTTP/1.1 200 OK", + "Connection: keep-alive", + $"Date: {testContext.DateHeaderValue}", + "Content-Length: 0", + "\r\n"); + + await connection.Send( + "POST / HTTP/1.0", + "Connection: keep-alive", + "Content-Length: 7", + "", + "Goodbye"); + + await connection.ReceiveForcedEnd( + "HTTP/1.1 200 OK", + "Connection: close", + $"Date: {testContext.DateHeaderValue}", + "", + "Goodbye"); + } + } + } + + [Fact] + public async Task Http10KeepAliveHonoredIfResponseContentLengthSet() + { + var testContext = new TestServiceContext(LoggerFactory); + + using (var server = new TestServer(TestApp.EchoAppChunked, testContext)) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.0", + "Content-Length: 11", + "Connection: keep-alive", + "", + "Hello World"); + + await connection.Receive( + "HTTP/1.1 200 OK", + "Connection: keep-alive", + $"Date: {testContext.DateHeaderValue}", + "Content-Length: 11", + "", + "Hello World"); + + await connection.Send( + "POST / HTTP/1.0", + "Connection: keep-alive", + "Content-Length: 11", + "", + "Hello Again"); + + await connection.Receive( + "HTTP/1.1 200 OK", + "Connection: keep-alive", + $"Date: {testContext.DateHeaderValue}", + "Content-Length: 11", + "", + "Hello Again"); + + await connection.Send( + "POST / HTTP/1.0", + "Content-Length: 7", + "", + "Goodbye"); + + await connection.ReceiveForcedEnd( + "HTTP/1.1 200 OK", + "Connection: close", + $"Date: {testContext.DateHeaderValue}", + "Content-Length: 7", + "", + "Goodbye"); + } + } + } + + [Fact] + public async Task Expect100ContinueHonored() + { + var testContext = new TestServiceContext(LoggerFactory); + + using (var server = new TestServer(TestApp.EchoAppChunked, testContext)) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + "Host:", + "Expect: 100-continue", + "Connection: close", + "Content-Length: 11", + "\r\n"); + await connection.Receive( + "HTTP/1.1 100 Continue", + "", + ""); + await connection.Send("Hello World"); + await connection.ReceiveForcedEnd( + "HTTP/1.1 200 OK", + "Connection: close", + $"Date: {testContext.DateHeaderValue}", + "Content-Length: 11", + "", + "Hello World"); + } + } + } + + [Fact] + public async Task ZeroContentLengthAssumedOnNonKeepAliveRequestsWithoutContentLengthOrTransferEncodingHeader() + { + var testContext = new TestServiceContext(LoggerFactory); + + using (var server = new TestServer(async httpContext => + { + // This will hang if 0 content length is not assumed by the server + Assert.Equal(0, await httpContext.Request.Body.ReadAsync(new byte[1], 0, 1).DefaultTimeout()); + }, testContext)) + { + using (var connection = server.CreateConnection()) + { + // Use Send instead of SendEnd to ensure the connection will remain open while + // the app runs and reads 0 bytes from the body nonetheless. This checks that + // https://github.com/aspnet/KestrelHttpServer/issues/1104 is not regressing. + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "Connection: close", + "", + ""); + await connection.ReceiveForcedEnd( + "HTTP/1.1 200 OK", + "Connection: close", + $"Date: {testContext.DateHeaderValue}", + "Content-Length: 0", + "", + ""); + } + + using (var connection = server.CreateConnection()) + { + await connection.Send( + "GET / HTTP/1.0", + "Host:", + "", + ""); + await connection.ReceiveForcedEnd( + "HTTP/1.1 200 OK", + "Connection: close", + $"Date: {testContext.DateHeaderValue}", + "Content-Length: 0", + "", + ""); + } + } + } + + [Fact] + public async Task ConnectionClosesWhenFinReceivedBeforeRequestCompletes() + { + var testContext = new TestServiceContext(LoggerFactory); + // FIN callbacks are scheduled so run inline to make this test more reliable + testContext.Scheduler = PipeScheduler.Inline; + + using (var server = new TestServer(TestApp.EchoAppChunked, testContext)) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1"); + connection.ShutdownSend(); + await connection.ReceiveForcedEnd(); + } + + using (var connection = server.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + "Host:", + "Content-Length: 7"); + connection.ShutdownSend(); + await connection.ReceiveForcedEnd(); + } + } + } + + [Fact] + public async Task RequestHeadersAreResetOnEachRequest() + { + var testContext = new TestServiceContext(LoggerFactory); + + IHeaderDictionary originalRequestHeaders = null; + var firstRequest = true; + + using (var server = new TestServer(httpContext => + { + var requestFeature = httpContext.Features.Get(); + + if (firstRequest) + { + originalRequestHeaders = requestFeature.Headers; + requestFeature.Headers = new HttpRequestHeaders(); + firstRequest = false; + } + else + { + Assert.Same(originalRequestHeaders, requestFeature.Headers); + } + + return Task.CompletedTask; + }, testContext)) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "", + "GET / HTTP/1.1", + "Host:", + "", + ""); + await connection.ReceiveEnd( + "HTTP/1.1 200 OK", + $"Date: {testContext.DateHeaderValue}", + "Content-Length: 0", + "", + "HTTP/1.1 200 OK", + $"Date: {testContext.DateHeaderValue}", + "Content-Length: 0", + "", + ""); + } + } + } + + [Fact] + public async Task UpgradeRequestIsNotKeptAliveOrChunked() + { + const string message = "Hello World"; + + var testContext = new TestServiceContext(LoggerFactory); + + using (var server = new TestServer(async context => + { + var upgradeFeature = context.Features.Get(); + var duplexStream = await upgradeFeature.UpgradeAsync(); + + var buffer = new byte[message.Length]; + var read = 0; + while (read < message.Length) + { + read += await duplexStream.ReadAsync(buffer, read, buffer.Length - read).DefaultTimeout(); + } + + await duplexStream.WriteAsync(buffer, 0, read); + }, testContext)) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "Connection: Upgrade", + "", + message); + await connection.ReceiveForcedEnd( + "HTTP/1.1 101 Switching Protocols", + "Connection: Upgrade", + $"Date: {testContext.DateHeaderValue}", + "", + message); + } + } + } + + [Fact] + public async Task HeadersAndStreamsAreReusedAcrossRequests() + { + var testContext = new TestServiceContext(LoggerFactory); + var streamCount = 0; + var requestHeadersCount = 0; + var responseHeadersCount = 0; + var loopCount = 20; + Stream lastStream = null; + IHeaderDictionary lastRequestHeaders = null; + IHeaderDictionary lastResponseHeaders = null; + + using (var server = new TestServer(async context => + { + if (context.Request.Body != lastStream) + { + lastStream = context.Request.Body; + streamCount++; + } + if (context.Request.Headers != lastRequestHeaders) + { + lastRequestHeaders = context.Request.Headers; + requestHeadersCount++; + } + if (context.Response.Headers != lastResponseHeaders) + { + lastResponseHeaders = context.Response.Headers; + responseHeadersCount++; + } + + var ms = new MemoryStream(); + await context.Request.Body.CopyToAsync(ms); + var request = ms.ToArray(); + + context.Response.ContentLength = request.Length; + + await context.Response.Body.WriteAsync(request, 0, request.Length); + }, testContext)) + { + using (var connection = server.CreateConnection()) + { + var requestData = + Enumerable.Repeat("GET / HTTP/1.1\r\nHost:\r\n", loopCount) + .Concat(new[] { "GET / HTTP/1.1\r\nHost:\r\nContent-Length: 7\r\nConnection: close\r\n\r\nGoodbye" }); + + var response = string.Join("\r\n", new string[] { + "HTTP/1.1 200 OK", + $"Date: {testContext.DateHeaderValue}", + "Content-Length: 0", + ""}); + + var lastResponse = string.Join("\r\n", new string[] + { + "HTTP/1.1 200 OK", + "Connection: close", + $"Date: {testContext.DateHeaderValue}", + "Content-Length: 7", + "", + "Goodbye" + }); + + var responseData = + Enumerable.Repeat(response, loopCount) + .Concat(new[] { lastResponse }); + + await connection.Send(requestData.ToArray()); + + await connection.ReceiveEnd(responseData.ToArray()); + } + + Assert.Equal(1, streamCount); + Assert.Equal(1, requestHeadersCount); + Assert.Equal(1, responseHeadersCount); + } + } + + [Theory] + [MemberData(nameof(HostHeaderData))] + public async Task MatchesValidRequestTargetAndHostHeader(string request, string hostHeader) + { + using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory))) + { + using (var connection = server.CreateConnection()) + { + await connection.Send($"{request} HTTP/1.1", + $"Host: {hostHeader}", + "", + ""); + + await connection.Receive("HTTP/1.1 200 OK"); + } + } + } + + [Fact] + public async Task ServerConsumesKeepAliveContentLengthRequest() + { + // The app doesn't read the request body, so it should be consumed by the server + using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory))) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + "Host:", + "Content-Length: 5", + "", + "hello"); + + await connection.Receive( + "HTTP/1.1 200 OK", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 0", + "", + ""); + + // If the server consumed the previous request properly, the + // next request should be successful + await connection.Send( + "POST / HTTP/1.1", + "Host:", + "Content-Length: 5", + "", + "world"); + + await connection.Receive( + "HTTP/1.1 200 OK", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 0", + "", + ""); + } + } + } + + [Fact] + public async Task ServerConsumesKeepAliveChunkedRequest() + { + // The app doesn't read the request body, so it should be consumed by the server + using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory))) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + "Host:", + "Transfer-Encoding: chunked", + "", + "5", + "hello", + "5", + "world", + "0", + "Trailer: value", + "", + ""); + + await connection.Receive( + "HTTP/1.1 200 OK", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 0", + "", + ""); + + // If the server consumed the previous request properly, the + // next request should be successful + await connection.Send( + "POST / HTTP/1.1", + "Host:", + "Content-Length: 5", + "", + "world"); + + await connection.Receive( + "HTTP/1.1 200 OK", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 0", + "", + ""); + } + } + } + + [Fact] + public async Task NonKeepAliveRequestNotConsumedByAppCompletes() + { + // The app doesn't read the request body, so it should be consumed by the server + using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory))) + { + using (var connection = server.CreateConnection()) + { + await connection.SendAll( + "POST / HTTP/1.0", + "Host:", + "Content-Length: 5", + "", + "hello"); + + await connection.ReceiveForcedEnd( + "HTTP/1.1 200 OK", + "Connection: close", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 0", + "", + ""); + } + } + } + + [Fact] + public async Task UpgradedRequestNotConsumedByAppCompletes() + { + // The app doesn't read the request body, so it should be consumed by the server + using (var server = new TestServer(async context => + { + var upgradeFeature = context.Features.Get(); + var duplexStream = await upgradeFeature.UpgradeAsync(); + + var response = Encoding.ASCII.GetBytes("goodbye"); + await duplexStream.WriteAsync(response, 0, response.Length); + }, new TestServiceContext(LoggerFactory))) + { + using (var connection = server.CreateConnection()) + { + await connection.SendAll( + "GET / HTTP/1.1", + "Host:", + "Connection: upgrade", + "", + "hello"); + + await connection.ReceiveForcedEnd( + "HTTP/1.1 101 Switching Protocols", + "Connection: Upgrade", + $"Date: {server.Context.DateHeaderValue}", + "", + "goodbye"); + } + } + } + + + [Fact] + public async Task DoesNotEnforceRequestBodyMinimumDataRateOnUpgradedRequest() + { + var appEvent = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var delayEvent = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var serviceContext = new TestServiceContext(LoggerFactory); + var heartbeatManager = new HttpHeartbeatManager(serviceContext.ConnectionManager); + + using (var server = new TestServer(async context => + { + context.Features.Get().MinDataRate = + new MinDataRate(bytesPerSecond: double.MaxValue, gracePeriod: Heartbeat.Interval + TimeSpan.FromTicks(1)); + + using (var stream = await context.Features.Get().UpgradeAsync()) + { + appEvent.SetResult(null); + + // Read once to go through one set of TryPauseTimingReads()/TryResumeTimingReads() calls + await stream.ReadAsync(new byte[1], 0, 1); + + await delayEvent.Task.DefaultTimeout(); + + // Read again to check that the connection is still alive + await stream.ReadAsync(new byte[1], 0, 1); + + // Send a response to distinguish from the timeout case where the 101 is still received, but without any content + var response = Encoding.ASCII.GetBytes("hello"); + await stream.WriteAsync(response, 0, response.Length); + } + }, serviceContext)) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "Connection: upgrade", + "", + "a"); + + await appEvent.Task.DefaultTimeout(); + + serviceContext.MockSystemClock.UtcNow += TimeSpan.FromSeconds(5); + heartbeatManager.OnHeartbeat(serviceContext.SystemClock.UtcNow); + + delayEvent.SetResult(null); + + await connection.Send("b"); + + await connection.Receive( + "HTTP/1.1 101 Switching Protocols", + "Connection: Upgrade", + ""); + await connection.ReceiveStartsWith( + $"Date: "); + await connection.ReceiveForcedEnd( + "", + "hello"); + } + } + } + + [Fact] + public async Task SynchronousReadsAllowedByDefault() + { + var firstRequest = true; + + using (var server = new TestServer(async context => + { + var bodyControlFeature = context.Features.Get(); + Assert.True(bodyControlFeature.AllowSynchronousIO); + + var buffer = new byte[6]; + var offset = 0; + + // The request body is 5 bytes long. The 6th byte (buffer[5]) is only used for writing the response body. + buffer[5] = (byte)(firstRequest ? '1' : '2'); + + if (firstRequest) + { + while (offset < 5) + { + offset += context.Request.Body.Read(buffer, offset, 5 - offset); + } + + firstRequest = false; + } + else + { + bodyControlFeature.AllowSynchronousIO = false; + + // Synchronous reads now throw. + var ioEx = Assert.Throws(() => context.Request.Body.Read(new byte[1], 0, 1)); + Assert.Equal(CoreStrings.SynchronousReadsDisallowed, ioEx.Message); + + var ioEx2 = Assert.Throws(() => context.Request.Body.CopyTo(Stream.Null)); + Assert.Equal(CoreStrings.SynchronousReadsDisallowed, ioEx2.Message); + + while (offset < 5) + { + offset += await context.Request.Body.ReadAsync(buffer, offset, 5 - offset); + } + } + + Assert.Equal(0, await context.Request.Body.ReadAsync(new byte[1], 0, 1)); + Assert.Equal("Hello", Encoding.ASCII.GetString(buffer, 0, 5)); + + context.Response.ContentLength = 6; + await context.Response.Body.WriteAsync(buffer, 0, 6); + }, new TestServiceContext(LoggerFactory))) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + "Host:", + "Content-Length: 5", + "", + "HelloPOST / HTTP/1.1", + "Host:", + "Content-Length: 5", + "", + "Hello"); + await connection.Receive( + "HTTP/1.1 200 OK", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 6", + "", + "Hello1HTTP/1.1 200 OK", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 6", + "", + "Hello2"); + } + } + } + + [Fact] + public async Task SynchronousReadsCanBeDisallowedGlobally() + { + var testContext = new TestServiceContext(LoggerFactory) + { + ServerOptions = { AllowSynchronousIO = false } + }; + + using (var server = new TestServer(async context => + { + var bodyControlFeature = context.Features.Get(); + Assert.False(bodyControlFeature.AllowSynchronousIO); + + // Synchronous reads now throw. + var ioEx = Assert.Throws(() => context.Request.Body.Read(new byte[1], 0, 1)); + Assert.Equal(CoreStrings.SynchronousReadsDisallowed, ioEx.Message); + + var ioEx2 = Assert.Throws(() => context.Request.Body.CopyTo(Stream.Null)); + Assert.Equal(CoreStrings.SynchronousReadsDisallowed, ioEx2.Message); + + var buffer = new byte[5]; + var offset = 0; + while (offset < 5) + { + offset += await context.Request.Body.ReadAsync(buffer, offset, 5 - offset); + } + + Assert.Equal(0, await context.Request.Body.ReadAsync(new byte[1], 0, 1)); + Assert.Equal("Hello", Encoding.ASCII.GetString(buffer)); + }, testContext)) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + "Host:", + "Content-Length: 5", + "", + "Hello"); + await connection.Receive( + "HTTP/1.1 200 OK", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 0", + "", + ""); + } + } + } + + public static TheoryData HostHeaderData => HttpParsingData.HostHeaderData; + } +} diff --git a/test/Kestrel.FunctionalTests/ResponseTests.cs b/test/Kestrel.InMemory.FunctionalTests/ResponseTests.cs similarity index 65% rename from test/Kestrel.FunctionalTests/ResponseTests.cs rename to test/Kestrel.InMemory.FunctionalTests/ResponseTests.cs index b23a5fe868..23d6e390a7 100644 --- a/test/Kestrel.FunctionalTests/ResponseTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/ResponseTests.cs @@ -1,182 +1,65 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Net; -using System.Net.Http; -using System.Net.Security; -using System.Net.Sockets; -using System.Security.Authentication; -using System.Security.Cryptography.X509Certificates; using System.Text; -using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core; -using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; -using Microsoft.AspNetCore.Server.Kestrel.Https; -using Microsoft.AspNetCore.Server.Kestrel.Https.Internal; +using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; -using Microsoft.Extensions.Primitives; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { public class ResponseTests : TestApplicationErrorLoggerLoggedTest { - public static TheoryData ConnectionAdapterData => new TheoryData - { - new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)), - new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) - { - ConnectionAdapters = { new PassThroughConnectionAdapter() } - } - }; - - [Fact] - public async Task LargeDownload() - { - var hostBuilder = TransportSelector.GetWebHostBuilder() - .UseKestrel() - .UseUrls("http://127.0.0.1:0/") - .ConfigureServices(AddTestLogging) - .Configure(app => - { - app.Run(async context => - { - var bytes = new byte[1024]; - for (int i = 0; i < bytes.Length; i++) - { - bytes[i] = (byte)i; - } - - context.Response.ContentLength = bytes.Length * 1024; - - for (int i = 0; i < 1024; i++) - { - await context.Response.Body.WriteAsync(bytes, 0, bytes.Length); - } - }); - }); - - using (var host = hostBuilder.Build()) - { - host.Start(); - - using (var client = new HttpClient()) - { - var response = await client.GetAsync($"http://127.0.0.1:{host.GetPort()}/"); - response.EnsureSuccessStatusCode(); - var responseBody = await response.Content.ReadAsStreamAsync(); - - // Read the full response body - var total = 0; - var bytes = new byte[1024]; - var count = await responseBody.ReadAsync(bytes, 0, bytes.Length); - while (count > 0) - { - for (int i = 0; i < count; i++) - { - Assert.Equal(total % 256, bytes[i]); - total++; - } - count = await responseBody.ReadAsync(bytes, 0, bytes.Length); - } - } - } - } - - [Theory, MemberData(nameof(NullHeaderData))] - public async Task IgnoreNullHeaderValues(string headerName, StringValues headerValue, string expectedValue) - { - var hostBuilder = TransportSelector.GetWebHostBuilder() - .UseKestrel() - .UseUrls("http://127.0.0.1:0/") - .ConfigureServices(AddTestLogging) - .Configure(app => - { - app.Run(async context => - { - context.Response.Headers.Add(headerName, headerValue); - - await context.Response.WriteAsync(""); - }); - }); - - using (var host = hostBuilder.Build()) - { - host.Start(); - - using (var client = new HttpClient()) - { - var response = await client.GetAsync($"http://127.0.0.1:{host.GetPort()}/"); - response.EnsureSuccessStatusCode(); - - var headers = response.Headers; - - if (expectedValue == null) - { - Assert.False(headers.Contains(headerName)); - } - else - { - Assert.True(headers.Contains(headerName)); - Assert.Equal(headers.GetValues(headerName).Single(), expectedValue); - } - } - } - } - [Fact] public async Task OnCompleteCalledEvenWhenOnStartingNotCalled() { var onStartingCalled = false; - var onCompletedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + TaskCompletionSource onCompletedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var hostBuilder = TransportSelector.GetWebHostBuilder() - .UseKestrel() - .UseUrls("http://127.0.0.1:0/") - .ConfigureServices(AddTestLogging) - .Configure(app => - { - app.Run(context => - { - context.Response.OnStarting(() => Task.Run(() => onStartingCalled = true)); - context.Response.OnCompleted(() => Task.Run(() => - { - onCompletedTcs.SetResult(null); - })); - - // Prevent OnStarting call (see HttpProtocol.ProcessRequestsAsync()). - throw new Exception(); - }); - }); - - using (var host = hostBuilder.Build()) + using (var server = new TestServer(context => { - host.Start(); - - using (var client = new HttpClient()) + context.Response.OnStarting(() => Task.Run(() => onStartingCalled = true)); + context.Response.OnCompleted(() => Task.Run(() => { - var response = await client.GetAsync($"http://127.0.0.1:{host.GetPort()}/"); + onCompletedTcs.SetResult(null); + })); + + // Prevent OnStarting call (see HttpProtocol.ProcessRequestsAsync()). + throw new Exception(); + }, new TestServiceContext(LoggerFactory))) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "", + ""); + + await connection.Receive( + $"HTTP/1.1 500 Internal Server Error", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 0", + "", + ""); - Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); - Assert.False(onStartingCalled); await onCompletedTcs.Task.DefaultTimeout(); + Assert.False(onStartingCalled); } } } @@ -186,30 +69,31 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { InvalidOperationException ex = null; - var hostBuilder = TransportSelector.GetWebHostBuilder() - .UseKestrel() - .UseUrls("http://127.0.0.1:0/") - .ConfigureServices(AddTestLogging) - .Configure(app => - { - app.Run(async context => - { - await context.Response.WriteAsync("hello, world"); - await context.Response.Body.FlushAsync(); - ex = Assert.Throws(() => context.Response.OnStarting(_ => Task.CompletedTask, null)); - }); - }); - - using (var host = hostBuilder.Build()) + using (var server = new TestServer(async context => { - host.Start(); - - using (var client = new HttpClient()) + await context.Response.WriteAsync("hello, world"); + await context.Response.Body.FlushAsync(); + ex = Assert.Throws(() => context.Response.OnStarting(_ => Task.CompletedTask, null)); + }, new TestServiceContext(LoggerFactory))) + { + using (var connection = server.CreateConnection()) { - var response = await client.GetAsync($"http://127.0.0.1:{host.GetPort()}/"); + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "", + ""); + + await connection.Receive($"HTTP/1.1 200 OK", + $"Date: {server.Context.DateHeaderValue}", + "Transfer-Encoding: chunked", + "", + "c", + "hello, world", + "0", + "", + ""); - // Despite the error, the response had already started - Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(ex); } } @@ -313,27 +197,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests [Fact] public async Task OnCompletedExceptionShouldNotPreventAResponse() { - var hostBuilder = TransportSelector.GetWebHostBuilder() - .UseKestrel() - .UseUrls("http://127.0.0.1:0/") - .ConfigureServices(AddTestLogging) - .Configure(app => - { - app.Run(async context => - { - context.Response.OnCompleted(_ => throw new Exception(), null); - await context.Response.WriteAsync("hello, world"); - }); - }); - - using (var host = hostBuilder.Build()) + using (var server = new TestServer(async context => { - host.Start(); - - using (var client = new HttpClient()) + context.Response.OnCompleted(_ => throw new Exception(), null); + await context.Response.WriteAsync("hello, world"); + }, new TestServiceContext(LoggerFactory))) + { + using (var connection = server.CreateConnection()) { - var response = await client.GetAsync($"http://127.0.0.1:{host.GetPort()}/"); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "", + ""); + + await connection.Receive($"HTTP/1.1 200 OK", + $"Date: {server.Context.DateHeaderValue}", + "Transfer-Encoding: chunked", + "", + "c", + "hello, world", + "0", + "", + ""); } } } @@ -342,30 +228,33 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests public async Task OnCompletedShouldNotBlockAResponse() { var delayTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var hostBuilder = TransportSelector.GetWebHostBuilder() - .UseKestrel() - .UseUrls("http://127.0.0.1:0/") - .ConfigureServices(AddTestLogging) - .Configure(app => - { - app.Run(async context => - { - context.Response.OnCompleted(async () => - { - await delayTcs.Task; - }); - await context.Response.WriteAsync("hello, world"); - }); - }); - using (var host = hostBuilder.Build()) + using (var server = new TestServer(async context => { - host.Start(); - - using (var client = new HttpClient()) + context.Response.OnCompleted(async () => { - var response = await client.GetAsync($"http://127.0.0.1:{host.GetPort()}/"); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + await delayTcs.Task; + }); + await context.Response.WriteAsync("hello, world"); + }, new TestServiceContext(LoggerFactory))) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "", + ""); + + await connection.Receive($"HTTP/1.1 200 OK", + $"Date: {server.Context.DateHeaderValue}", + "Transfer-Encoding: chunked", + "", + "c", + "hello, world", + "0", + "", + ""); } delayTcs.SetResult(null); @@ -394,6 +283,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests "Transfer-Encoding: chunked", "", "gg"); + await connection.ReceiveForcedEnd( "HTTP/1.1 200 OK", $"Date: {server.Context.DateHeaderValue}", @@ -406,95 +296,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests await onCompletedTcs.Task.DefaultTimeout(); } - private static async Task ResponseStatusCodeSetBeforeHttpContextDispose( - ITestSink testSink, - ILoggerFactory loggerFactory, - RequestDelegate handler, - HttpStatusCode? expectedClientStatusCode, - HttpStatusCode expectedServerStatusCode, - bool sendMalformedRequest = false) - { - var mockHttpContextFactory = new Mock(); - mockHttpContextFactory.Setup(f => f.Create(It.IsAny())) - .Returns(fc => new DefaultHttpContext(fc)); - - var disposedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - mockHttpContextFactory.Setup(f => f.Dispose(It.IsAny())) - .Callback(c => - { - disposedTcs.TrySetResult(c.Response.StatusCode); - }); - - using (var server = new TestServer(handler, new TestServiceContext(loggerFactory), - new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)), - services => services.AddSingleton(mockHttpContextFactory.Object))) - { - if (!sendMalformedRequest) - { - using (var client = new HttpClient()) - { - try - { - var response = await client.GetAsync($"http://127.0.0.1:{server.Port}/"); - Assert.Equal(expectedClientStatusCode, response.StatusCode); - } - catch - { - if (expectedClientStatusCode != null) - { - throw; - } - } - } - } - else - { - using (var connection = new TestConnection(server.Port)) - { - await connection.Send( - "POST / HTTP/1.1", - "Host:", - "Transfer-Encoding: chunked", - "", - "gg"); - if (expectedClientStatusCode == HttpStatusCode.OK) - { - await connection.ReceiveForcedEnd( - "HTTP/1.1 200 OK", - $"Date: {server.Context.DateHeaderValue}", - "Content-Length: 0", - "", - ""); - } - else - { - await connection.ReceiveForcedEnd( - "HTTP/1.1 400 Bad Request", - "Connection: close", - $"Date: {server.Context.DateHeaderValue}", - "Content-Length: 0", - "", - ""); - } - } - } - - var disposedStatusCode = await disposedTcs.Task.DefaultTimeout(); - Assert.Equal(expectedServerStatusCode, (HttpStatusCode)disposedStatusCode); - } - - if (sendMalformedRequest) - { - Assert.Contains(testSink.Writes, w => w.EventId.Id == 17 && w.LogLevel == LogLevel.Information && w.Exception is BadHttpRequestException - && ((BadHttpRequestException)w.Exception).StatusCode == StatusCodes.Status400BadRequest); - } - else - { - Assert.DoesNotContain(testSink.Writes, w => w.EventId.Id == 17 && w.LogLevel == LogLevel.Information && w.Exception is BadHttpRequestException - && ((BadHttpRequestException)w.Exception).StatusCode == StatusCodes.Status400BadRequest); - } - } - // https://github.com/aspnet/KestrelHttpServer/pull/1111/files#r80584475 explains the reason for this test. [Fact] public async Task NoErrorResponseSentWhenAppSwallowsBadRequestException() @@ -620,7 +421,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests const string response = "hello, world"; var logTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var mockKestrelTrace = new Mock(); + var mockKestrelTrace = new Mock(Logger) { CallBase = true }; mockKestrelTrace .Setup(trace => trace.ConnectionHeadResponseBodyWrite(It.IsAny(), response.Length)) .Callback((connectionId, count) => logTcs.SetResult(null)); @@ -685,7 +486,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests "", "hello,"); - await connection.WaitForConnectionClose().DefaultTimeout(); + await connection.WaitForConnectionClose(); } } @@ -809,7 +610,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests public async Task WhenAppWritesLessThanContentLengthErrorLogged() { var logTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var mockTrace = new Mock(); + var mockTrace = new Mock(Logger) { CallBase = true }; mockTrace .Setup(trace => trace.ApplicationError(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((connectionId, requestId, ex) => @@ -846,7 +647,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests await logTcs.Task.DefaultTimeout(); // The server should close the connection in this situation. - await connection.WaitForConnectionClose().DefaultTimeout(); + await connection.WaitForConnectionClose(); } } @@ -861,21 +662,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests [Fact] public async Task WhenAppWritesLessThanContentLengthButRequestIsAbortedErrorNotLogged() { - var requestAborted = new SemaphoreSlim(0); - var mockTrace = new Mock(); + var requestAborted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var mockTrace = new Mock(Logger) { CallBase = true }; using (var server = new TestServer(async httpContext => { httpContext.RequestAborted.Register(() => { - requestAborted.Release(2); + requestAborted.SetResult(null); }); httpContext.Response.ContentLength = 12; await httpContext.Response.WriteAsync("hello,"); // Wait until the request is aborted so we know HttpProtocol will skip the response content length check. - Assert.True(await requestAborted.WaitAsync(TestConstants.DefaultTimeout)); + await requestAborted.Task.DefaultTimeout(); }, new TestServiceContext(LoggerFactory, mockTrace.Object))) { using (var connection = server.CreateConnection()) @@ -899,7 +700,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests // Await before disposing the server to prevent races between the // abort triggered by the connection RST and the abort called when // disposing the server. - Assert.True(await requestAborted.WaitAsync(TestConstants.DefaultTimeout)); + await requestAborted.Task.DefaultTimeout(); } // With the server disposed we know all connections were drained and all messages were logged. @@ -1082,13 +883,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests [Fact] public async Task HeadResponseBodyNotWrittenWithAsyncWrite() { - var flushed = new SemaphoreSlim(0, 1); + var flushed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); using (var server = new TestServer(async httpContext => { httpContext.Response.ContentLength = 12; await httpContext.Response.WriteAsync("hello, world"); - await flushed.WaitAsync(); + await flushed.Task; }, new TestServiceContext(LoggerFactory))) { using (var connection = server.CreateConnection()) @@ -1105,7 +906,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests "", ""); - flushed.Release(); + flushed.SetResult(null); } } } @@ -1113,14 +914,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests [Fact] public async Task HeadResponseBodyNotWrittenWithSyncWrite() { - var flushed = new SemaphoreSlim(0, 1); + var flushed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var serviceContext = new TestServiceContext(LoggerFactory) { ServerOptions = { AllowSynchronousIO = true } }; using (var server = new TestServer(async httpContext => { httpContext.Response.ContentLength = 12; httpContext.Response.Body.Write(Encoding.ASCII.GetBytes("hello, world"), 0, 12); - await flushed.WaitAsync(); + await flushed.Task; }, serviceContext)) { using (var connection = server.CreateConnection()) @@ -1137,7 +939,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests "", ""); - flushed.Release(); + flushed.SetResult(null); } } } @@ -1145,13 +947,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests [Fact] public async Task ZeroLengthWritesFlushHeaders() { - var flushed = new SemaphoreSlim(0, 1); + var flushed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); using (var server = new TestServer(async httpContext => { httpContext.Response.ContentLength = 12; await httpContext.Response.WriteAsync(""); - await flushed.WaitAsync(); + await flushed.Task; await httpContext.Response.WriteAsync("hello, world"); }, new TestServiceContext(LoggerFactory))) { @@ -1169,60 +971,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests "", ""); - flushed.Release(); + flushed.SetResult(null); await connection.ReceiveEnd("hello, world"); } } } - [Fact] - public async Task WriteAfterConnectionCloseNoops() - { - var connectionClosed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var requestStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var appCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - using (var server = new TestServer(async httpContext => - { - try - { - requestStarted.SetResult(null); - await connectionClosed.Task.DefaultTimeout(); - httpContext.Response.ContentLength = 12; - await httpContext.Response.WriteAsync("hello, world"); - appCompleted.TrySetResult(null); - } - catch (Exception ex) - { - appCompleted.TrySetException(ex); - } - }, new TestServiceContext(LoggerFactory))) - { - using (var connection = server.CreateConnection()) - { - await connection.Send( - "GET / HTTP/1.1", - "Host:", - "", - ""); - - await requestStarted.Task.DefaultTimeout(); - connection.Shutdown(SocketShutdown.Send); - await connection.WaitForConnectionClose().DefaultTimeout(); - } - - connectionClosed.SetResult(null); - - await appCompleted.Task.DefaultTimeout(); - } - } - [Fact] public async Task AppCanWriteOwnBadRequestResponse() { var expectedResponse = string.Empty; - var responseWritten = new SemaphoreSlim(0); + var responseWritten = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); using (var server = new TestServer(async httpContext => { @@ -1236,7 +996,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; httpContext.Response.ContentLength = ex.Message.Length; await httpContext.Response.WriteAsync(ex.Message); - responseWritten.Release(); + responseWritten.SetResult(null); } }, new TestServiceContext(LoggerFactory))) { @@ -1248,7 +1008,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests "Transfer-Encoding: chunked", "", "gg"); - await responseWritten.WaitAsync().DefaultTimeout(); + await responseWritten.Task.DefaultTimeout(); await connection.ReceiveEnd( "HTTP/1.1 400 Bad Request", $"Date: {server.Context.DateHeaderValue}", @@ -1793,13 +1553,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task Http11ResponseSentToHttp10Request(ListenOptions listenOptions) + [Fact] + public async Task Http11ResponseSentToHttp10Request() { var serviceContext = new TestServiceContext(LoggerFactory); - using (var server = new TestServer(TestApp.EchoApp, serviceContext, listenOptions)) + using (var server = new TestServer(TestApp.EchoApp, serviceContext)) { using (var connection = server.CreateConnection()) { @@ -1818,13 +1577,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task ZeroContentLengthSetAutomaticallyAfterNoWrites(ListenOptions listenOptions) + [Fact] + public async Task ZeroContentLengthSetAutomaticallyAfterNoWrites() { - var testContext= new TestServiceContext(LoggerFactory); + var testContext = new TestServiceContext(LoggerFactory); - using (var server = new TestServer(TestApp.EmptyApp, testContext, listenOptions)) + using (var server = new TestServer(TestApp.EmptyApp, testContext)) { using (var connection = server.CreateConnection()) { @@ -1851,16 +1609,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task ZeroContentLengthSetAutomaticallyForNonKeepAliveRequests(ListenOptions listenOptions) + [Fact] + public async Task ZeroContentLengthSetAutomaticallyForNonKeepAliveRequests() { - var testContext= new TestServiceContext(LoggerFactory); + var testContext = new TestServiceContext(LoggerFactory); using (var server = new TestServer(async httpContext => { Assert.Equal(0, await httpContext.Request.Body.ReadAsync(new byte[1], 0, 1).DefaultTimeout()); - }, testContext, listenOptions)) + }, testContext)) { using (var connection = server.CreateConnection()) { @@ -1896,13 +1653,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task ZeroContentLengthNotSetAutomaticallyForHeadRequests(ListenOptions listenOptions) + [Fact] + public async Task ZeroContentLengthNotSetAutomaticallyForHeadRequests() { - var testContext= new TestServiceContext(LoggerFactory); + var testContext = new TestServiceContext(LoggerFactory); - using (var server = new TestServer(TestApp.EmptyApp, testContext, listenOptions)) + using (var server = new TestServer(TestApp.EmptyApp, testContext)) { using (var connection = server.CreateConnection()) { @@ -1920,11 +1676,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task ZeroContentLengthNotSetAutomaticallyForCertainStatusCodes(ListenOptions listenOptions) + [Fact] + public async Task ZeroContentLengthNotSetAutomaticallyForCertainStatusCodes() { - var testContext= new TestServiceContext(LoggerFactory); + var testContext = new TestServiceContext(LoggerFactory); using (var server = new TestServer(async httpContext => { @@ -1936,7 +1691,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests var statusString = await reader.ReadLineAsync(); response.StatusCode = int.Parse(statusString); } - }, testContext, listenOptions)) + }, testContext)) { using (var connection = server.CreateConnection()) { @@ -1977,11 +1732,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task ConnectionClosedAfter101Response(ListenOptions listenOptions) + [Fact] + public async Task ConnectionClosedAfter101Response() { - var testContext= new TestServiceContext(LoggerFactory); + var testContext = new TestServiceContext(LoggerFactory); using (var server = new TestServer(async httpContext => { @@ -1989,7 +1743,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests var stream = await httpContext.Features.Get().UpgradeAsync(); var response = Encoding.ASCII.GetBytes("hello, world"); await stream.WriteAsync(response, 0, response.Length); - }, testContext, listenOptions)) + }, testContext)) { using (var connection = server.CreateConnection()) { @@ -2024,11 +1778,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task ThrowingResultsIn500Response(ListenOptions listenOptions) + [Fact] + public async Task ThrowingResultsIn500Response() { - var testContext= new TestServiceContext(LoggerFactory); + var testContext = new TestServiceContext(LoggerFactory); bool onStartingCalled = false; @@ -2044,7 +1797,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests // Anything added to the ResponseHeaders dictionary is ignored response.Headers["Content-Length"] = "11"; throw new Exception(); - }, testContext, listenOptions)) + }, testContext)) { using (var connection = server.CreateConnection()) { @@ -2075,14 +1828,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests Assert.Equal(2, TestApplicationErrorLogger.Messages.Where(message => message.LogLevel == LogLevel.Error).Count()); } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task ThrowingInOnStartingResultsInFailedWritesAnd500Response(ListenOptions listenOptions) + + [Fact] + public async Task ThrowingInOnStartingResultsInFailedWritesAnd500Response() { var callback1Called = false; var callback2CallCount = 0; - var testContext= new TestServiceContext(LoggerFactory); + var testContext = new TestServiceContext(LoggerFactory); using (var server = new TestServer(async httpContext => { @@ -2102,7 +1855,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests var writeException = await Assert.ThrowsAsync(async () => await response.Body.FlushAsync()); Assert.Same(onStartingException, writeException.InnerException); - }, testContext, listenOptions)) + }, testContext)) { using (var connection = server.CreateConnection()) { @@ -2134,11 +1887,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests Assert.Equal(2, TestApplicationErrorLogger.Messages.Where(message => message.LogLevel == LogLevel.Error).Count()); } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task ThrowingInOnCompletedIsLogged(ListenOptions listenOptions) + [Fact] + public async Task ThrowingInOnCompletedIsLogged() { - var testContext= new TestServiceContext(LoggerFactory); + var testContext = new TestServiceContext(LoggerFactory); var onCompletedCalled1 = false; var onCompletedCalled2 = false; @@ -2160,7 +1912,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests response.Headers["Content-Length"] = new[] { "11" }; await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11); - }, testContext, listenOptions)) + }, testContext)) { using (var connection = server.CreateConnection()) { @@ -2184,11 +1936,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests Assert.True(onCompletedCalled2); } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task ThrowingAfterWritingKillsConnection(ListenOptions listenOptions) + [Fact] + public async Task ThrowingAfterWritingKillsConnection() { - var testContext= new TestServiceContext(LoggerFactory); + var testContext = new TestServiceContext(LoggerFactory); bool onStartingCalled = false; @@ -2204,7 +1955,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests response.Headers["Content-Length"] = new[] { "11" }; await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11); throw new Exception(); - }, testContext, listenOptions)) + }, testContext)) { using (var connection = server.CreateConnection()) { @@ -2226,11 +1977,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests Assert.Single(TestApplicationErrorLogger.Messages, message => message.LogLevel == LogLevel.Error); } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task ThrowingAfterPartialWriteKillsConnection(ListenOptions listenOptions) + [Fact] + public async Task ThrowingAfterPartialWriteKillsConnection() { - var testContext= new TestServiceContext(LoggerFactory); + var testContext = new TestServiceContext(LoggerFactory); bool onStartingCalled = false; @@ -2246,7 +1996,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests response.Headers["Content-Length"] = new[] { "11" }; await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello"), 0, 5); throw new Exception(); - }, testContext, listenOptions)) + }, testContext)) { using (var connection = server.CreateConnection()) { @@ -2268,286 +2018,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests Assert.Single(TestApplicationErrorLogger.Messages, message => message.LogLevel == LogLevel.Error); } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task ThrowsOnWriteWithRequestAbortedTokenAfterRequestIsAborted(ListenOptions listenOptions) + + [Fact] + public async Task NoErrorsLoggedWhenServerEndsConnectionBeforeClient() { - // This should match _maxBytesPreCompleted in SocketOutput - var maxBytesPreCompleted = 65536; - - // Ensure string is long enough to disable write-behind buffering - var largeString = new string('a', maxBytesPreCompleted + 1); - - var writeTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var requestAbortedWh = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var requestStartWh = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - using (var server = new TestServer(async httpContext => - { - requestStartWh.SetResult(null); - - var response = httpContext.Response; - var request = httpContext.Request; - var lifetime = httpContext.Features.Get(); - - lifetime.RequestAborted.Register(() => requestAbortedWh.SetResult(null)); - await requestAbortedWh.Task.DefaultTimeout(); - - try - { - await response.WriteAsync(largeString, lifetime.RequestAborted); - } - catch (Exception ex) - { - writeTcs.SetException(ex); - throw; - } - - writeTcs.SetException(new Exception("This shouldn't be reached.")); - }, new TestServiceContext(LoggerFactory), listenOptions)) - { - using (var connection = server.CreateConnection()) - { - await connection.Send( - "POST / HTTP/1.1", - "Host:", - "Content-Length: 0", - "", - ""); - - await requestStartWh.Task.DefaultTimeout(); - } - - // Write failed - can throw TaskCanceledException or OperationCanceledException, - // depending on how far the canceled write goes. - await Assert.ThrowsAnyAsync(async () => await writeTcs.Task).DefaultTimeout(); - - // RequestAborted tripped - await requestAbortedWh.Task.DefaultTimeout(); - } - } - - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task WritingToConnectionAfterUnobservedCloseTriggersRequestAbortedToken(ListenOptions listenOptions) - { - const int connectionPausedEventId = 4; - const int maxRequestBufferSize = 4096; - - var requestAborted = false; - var readCallbackUnwired = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var clientClosedConnection = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var writeTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - var mockKestrelTrace = new Mock(Logger) { CallBase = true }; - var mockLogger = new Mock(); - mockLogger - .Setup(logger => logger.IsEnabled(It.IsAny())) - .Returns(true); - mockLogger - .Setup(logger => logger.Log(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())) - .Callback>((logLevel, eventId, state, exception, formatter) => - { - if (eventId.Id == connectionPausedEventId) - { - readCallbackUnwired.TrySetResult(null); - } - - Logger.Log(logLevel, eventId, state, exception, formatter); - }); - - var mockLoggerFactory = new Mock(); - mockLoggerFactory - .Setup(factory => factory.CreateLogger(It.IsAny())) - .Returns(Logger); - mockLoggerFactory - .Setup(factory => factory.CreateLogger(It.IsIn("Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv", - "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets"))) - .Returns(mockLogger.Object); - - var testContext = new TestServiceContext(mockLoggerFactory.Object) - { - Log = mockKestrelTrace.Object, - ServerOptions = - { - Limits = - { - MaxRequestBufferSize = maxRequestBufferSize, - MaxRequestLineSize = maxRequestBufferSize, - MaxRequestHeadersTotalSize = maxRequestBufferSize, - } - } - }; - - var scratchBuffer = new byte[maxRequestBufferSize * 8]; - - using (var server = new TestServer(async context => - { - context.RequestAborted.Register(() => - { - requestAborted = true; - }); - - await clientClosedConnection.Task; - - try - { - for (var i = 0; i < 1000; i++) - { - await context.Response.Body.WriteAsync(scratchBuffer, 0, scratchBuffer.Length, context.RequestAborted); - await Task.Delay(10); - } - } - catch (Exception ex) - { - writeTcs.SetException(ex); - throw; - } - - writeTcs.SetException(new Exception("This shouldn't be reached.")); - }, testContext, listenOptions)) - { - using (var connection = server.CreateConnection()) - { - await connection.Send( - "POST / HTTP/1.1", - "Host:", - $"Content-Length: {scratchBuffer.Length}", - "", - ""); - - var ignore = connection.Stream.WriteAsync(scratchBuffer, 0, scratchBuffer.Length); - - // Wait until the read callback is no longer hooked up so that the connection disconnect isn't observed. - await readCallbackUnwired.Task.DefaultTimeout(); - } - - clientClosedConnection.SetResult(null); - - await Assert.ThrowsAnyAsync(() => writeTcs.Task).DefaultTimeout(); - } - - mockKestrelTrace.Verify(t => t.ConnectionStop(It.IsAny()), Times.Once()); - Assert.True(requestAborted); - } - - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task AppCanHandleClientAbortingConnectionMidResponse(ListenOptions listenOptions) - { - const int connectionResetEventId = 19; - const int connectionFinEventId = 6; - //const int connectionStopEventId = 2; - - const int responseBodySegmentSize = 65536; - const int responseBodySegmentCount = 100; - - var appCompletedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var requestAborted = false; - - var scratchBuffer = new byte[responseBodySegmentSize]; - - using (var server = new TestServer(async context => - { - context.RequestAborted.Register(() => - { - requestAborted = true; - }); - - for (var i = 0; i < responseBodySegmentCount; i++) - { - await context.Response.Body.WriteAsync(scratchBuffer, 0, scratchBuffer.Length); - await Task.Delay(10); - } - - appCompletedTcs.SetResult(null); - }, new TestServiceContext(LoggerFactory), listenOptions)) - { - using (var connection = server.CreateConnection()) - { - await connection.Send( - "GET / HTTP/1.1", - "Host:", - "", - ""); - - // Read just part of the response and close the connection. - // https://github.com/aspnet/KestrelHttpServer/issues/2554 - await connection.Stream.ReadAsync(scratchBuffer, 0, scratchBuffer.Length); - - connection.Reset(); - } - - await appCompletedTcs.Task.DefaultTimeout(); - - // After the app is done with the write loop, the connection reset should be logged. - // On Linux and macOS, the connection close is still sometimes observed as a FIN despite the LingerState. - var presShutdownTransportLogs = TestSink.Writes.Where( - w => w.LoggerName == "Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv" || - w.LoggerName == "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets"); - var connectionResetLogs = presShutdownTransportLogs.Where( - w => w.EventId == connectionResetEventId || - (!TestPlatformHelper.IsWindows && w.EventId == connectionFinEventId)); - - Assert.NotEmpty(connectionResetLogs); - } - - // TODO: Figure out what the following assertion is flaky. The server shouldn't shutdown before all - // the connections are closed, yet sometimes the connection stop log isn't observed here. - //var coreLogs = TestSink.Writes.Where(w => w.LoggerName == "Microsoft.AspNetCore.Server.Kestrel"); - //Assert.Single(coreLogs.Where(w => w.EventId == connectionStopEventId)); - - Assert.True(requestAborted, "RequestAborted token didn't fire."); - - var transportLogs = TestSink.Writes.Where(w => w.LoggerName == "Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv" || - w.LoggerName == "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets"); - Assert.Empty(transportLogs.Where(w => w.LogLevel > LogLevel.Debug)); - } - - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task ClientAbortingConnectionImmediatelyIsNotLoggedHigherThanDebug(ListenOptions listenOptions) - { - // Attempt multiple connections to be extra sure the resets are consistently logged appropriately. - const int numConnections = 10; - - // There's not guarantee that the app even gets invoked in this test. The connection reset can be observed - // as early as accept. - using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory), listenOptions)) - { - for (var i = 0; i < numConnections; i++) - { - using (var connection = server.CreateConnection()) - { - await connection.Send( - "GET / HTTP/1.1", - "Host:", - "", - ""); - - connection.Reset(); - } - } - } - - var transportLogs = TestSink.Writes.Where(w => w.LoggerName == "Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv" || - w.LoggerName == "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets"); - - Assert.Empty(transportLogs.Where(w => w.LogLevel > LogLevel.Debug)); - } - - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task NoErrorsLoggedWhenServerEndsConnectionBeforeClient(ListenOptions listenOptions) - { - var testContext= new TestServiceContext(LoggerFactory); + var testContext = new TestServiceContext(LoggerFactory); using (var server = new TestServer(async httpContext => { var response = httpContext.Response; response.Headers["Content-Length"] = new[] { "11" }; await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11); - }, testContext, listenOptions)) + }, testContext)) { using (var connection = server.CreateConnection()) { @@ -2568,17 +2050,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests Assert.Empty(TestApplicationErrorLogger.Messages.Where(message => message.LogLevel == LogLevel.Error)); } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task NoResponseSentWhenConnectionIsClosedByServerBeforeClientFinishesSendingRequest(ListenOptions listenOptions) + [Fact] + public async Task NoResponseSentWhenConnectionIsClosedByServerBeforeClientFinishesSendingRequest() { - var testContext= new TestServiceContext(LoggerFactory); + var testContext = new TestServiceContext(LoggerFactory); using (var server = new TestServer(httpContext => { httpContext.Abort(); return Task.CompletedTask; - }, testContext, listenOptions)) + }, testContext)) { using (var connection = server.CreateConnection()) { @@ -2592,11 +2073,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task ResponseHeadersAreResetOnEachRequest(ListenOptions listenOptions) + [Fact] + public async Task ResponseHeadersAreResetOnEachRequest() { - var testContext= new TestServiceContext(LoggerFactory); + var testContext = new TestServiceContext(LoggerFactory); IHeaderDictionary originalResponseHeaders = null; var firstRequest = true; @@ -2617,7 +2097,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } return Task.CompletedTask; - }, testContext, listenOptions)) + }, testContext)) { using (var connection = server.CreateConnection()) { @@ -2643,13 +2123,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task OnStartingCallbacksAreCalledInLastInFirstOutOrder(ListenOptions listenOptions) + [Fact] + public async Task OnStartingCallbacksAreCalledInLastInFirstOutOrder() { const string response = "hello, world"; - var testContext= new TestServiceContext(LoggerFactory); + var testContext = new TestServiceContext(LoggerFactory); var callOrder = new Stack(); var onStartingTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -2670,7 +2149,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests context.Response.ContentLength = response.Length; await context.Response.WriteAsync(response); - }, testContext, listenOptions)) + }, testContext)) { using (var connection = server.CreateConnection()) { @@ -2695,13 +2174,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests Assert.Equal(2, callOrder.Pop()); } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task OnCompletedCallbacksAreCalledInLastInFirstOutOrder(ListenOptions listenOptions) + [Fact] + public async Task OnCompletedCallbacksAreCalledInLastInFirstOutOrder() { const string response = "hello, world"; - var testContext= new TestServiceContext(LoggerFactory); + var testContext = new TestServiceContext(LoggerFactory); var callOrder = new Stack(); var onCompletedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -2722,7 +2200,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests context.Response.ContentLength = response.Length; await context.Response.WriteAsync(response); - }, testContext, listenOptions)) + }, testContext)) { using (var connection = server.CreateConnection()) { @@ -2837,424 +2315,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - [Fact] - public async Task ConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate() - { - using (StartLog(out var loggerFactory, "ConnClosedWhenRespDoesNotSatisfyMin")) - { - var logger = loggerFactory.CreateLogger($"{ typeof(ResponseTests).FullName}.{ nameof(ConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate)}"); - const int chunkSize = 1024; - const int chunks = 256 * 1024; - var responseSize = chunks * chunkSize; - var chunkData = new byte[chunkSize]; - - var responseRateTimeoutMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var connectionStopMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var requestAborted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var appFuncCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - var mockKestrelTrace = new Mock(); - mockKestrelTrace - .Setup(trace => trace.ResponseMininumDataRateNotSatisfied(It.IsAny(), It.IsAny())) - .Callback(() => responseRateTimeoutMessageLogged.SetResult(null)); - mockKestrelTrace - .Setup(trace => trace.ConnectionStop(It.IsAny())) - .Callback(() => connectionStopMessageLogged.SetResult(null)); - - var testContext = new TestServiceContext - { - LoggerFactory = loggerFactory, - Log = mockKestrelTrace.Object, - SystemClock = new SystemClock(), - ServerOptions = - { - Limits = - { - MinResponseDataRate = new MinDataRate(bytesPerSecond: 1024 * 1024, gracePeriod: TimeSpan.FromSeconds(2)) - } - } - }; - - var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); - listenOptions.ConnectionAdapters.Add(new LoggingConnectionAdapter(loggerFactory.CreateLogger())); - - var appLogger = loggerFactory.CreateLogger("App"); - async Task App(HttpContext context) - { - appLogger.LogInformation("Request received"); - context.RequestAborted.Register(() => requestAborted.SetResult(null)); - - context.Response.ContentLength = responseSize; - - try - { - for (var i = 0; i < chunks; i++) - { - await context.Response.Body.WriteAsync(chunkData, 0, chunkData.Length, context.RequestAborted); - appLogger.LogInformation("Wrote chunk of {chunkSize} bytes", chunkSize); - } - } - catch (OperationCanceledException) - { - appFuncCompleted.SetResult(null); - throw; - } - } - - using (var server = new TestServer(App, testContext, listenOptions)) - { - using (var connection = server.CreateConnection()) - { - logger.LogInformation("Sending request"); - await connection.Send( - "GET / HTTP/1.1", - "Host:", - "", - ""); - - logger.LogInformation("Sent request"); - - var sw = Stopwatch.StartNew(); - logger.LogInformation("Waiting for connection to abort."); - - await requestAborted.Task.DefaultTimeout(); - await responseRateTimeoutMessageLogged.Task.DefaultTimeout(); - await connectionStopMessageLogged.Task.DefaultTimeout(); - await appFuncCompleted.Task.DefaultTimeout(); - await AssertStreamAborted(connection.Reader.BaseStream, chunkSize * chunks); - - sw.Stop(); - logger.LogInformation("Connection was aborted after {totalMilliseconds}ms.", sw.ElapsedMilliseconds); - } - } - } - } - - [Fact] - public async Task HttpsConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate() - { - const int chunkSize = 1024; - const int chunks = 256 * 1024; - var chunkData = new byte[chunkSize]; - - var certificate = new X509Certificate2(TestResources.TestCertificatePath, "testPassword"); - - var responseRateTimeoutMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var connectionStopMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var aborted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var appFuncCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - var mockKestrelTrace = new Mock(); - mockKestrelTrace - .Setup(trace => trace.ResponseMininumDataRateNotSatisfied(It.IsAny(), It.IsAny())) - .Callback(() => responseRateTimeoutMessageLogged.SetResult(null)); - mockKestrelTrace - .Setup(trace => trace.ConnectionStop(It.IsAny())) - .Callback(() => connectionStopMessageLogged.SetResult(null)); - - var testContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object) - { - SystemClock = new SystemClock(), - ServerOptions = - { - Limits = - { - MinResponseDataRate = new MinDataRate(bytesPerSecond: 1024 * 1024, gracePeriod: TimeSpan.FromSeconds(2)) - } - } - }; - - var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) - { - ConnectionAdapters = - { - new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions { ServerCertificate = certificate }) - } - }; - - using (var server = new TestServer(async context => - { - context.RequestAborted.Register(() => - { - aborted.SetResult(null); - }); - - context.Response.ContentLength = chunks * chunkSize; - - try - { - for (var i = 0; i < chunks; i++) - { - await context.Response.Body.WriteAsync(chunkData, 0, chunkData.Length, context.RequestAborted); - } - } - catch (OperationCanceledException) - { - appFuncCompleted.SetResult(null); - throw; - } - }, testContext, listenOptions)) - { - using (var connection = server.CreateConnection()) - { - using (var sslStream = new SslStream(connection.Reader.BaseStream, false, (sender, cert, chain, errors) => true, null)) - { - await sslStream.AuthenticateAsClientAsync("localhost", new X509CertificateCollection(), SslProtocols.Tls12 | SslProtocols.Tls11, false); - - var request = Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\nHost:\r\n\r\n"); - await sslStream.WriteAsync(request, 0, request.Length); - - await aborted.Task.DefaultTimeout(); - await responseRateTimeoutMessageLogged.Task.DefaultTimeout(); - await connectionStopMessageLogged.Task.DefaultTimeout(); - await appFuncCompleted.Task.DefaultTimeout(); - - // Temporary workaround for a deadlock when reading from an aborted client SslStream on Mac and Linux. - if (TestPlatformHelper.IsWindows) - { - await AssertStreamAborted(sslStream, chunkSize * chunks); - } - else - { - await AssertStreamAborted(connection.Reader.BaseStream, chunkSize * chunks); - } - } - } - } - } - - [Fact] - public async Task ConnectionClosedWhenBothRequestAndResponseExperienceBackPressure() - { - const int bufferSize = 65536; - const int bufferCount = 100; - var responseSize = bufferCount * bufferSize; - var buffer = new byte[bufferSize]; - - var responseRateTimeoutMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var connectionStopMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var requestAborted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var copyToAsyncCts = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - var mockKestrelTrace = new Mock(); - mockKestrelTrace - .Setup(trace => trace.ResponseMininumDataRateNotSatisfied(It.IsAny(), It.IsAny())) - .Callback(() => responseRateTimeoutMessageLogged.SetResult(null)); - mockKestrelTrace - .Setup(trace => trace.ConnectionStop(It.IsAny())) - .Callback(() => connectionStopMessageLogged.SetResult(null)); - - var testContext = new TestServiceContext - { - LoggerFactory = LoggerFactory, - Log = mockKestrelTrace.Object, - SystemClock = new SystemClock(), - ServerOptions = - { - Limits = - { - MinResponseDataRate = new MinDataRate(bytesPerSecond: 1024 * 1024, gracePeriod: TimeSpan.FromSeconds(2)), - MaxRequestBodySize = responseSize - } - } - }; - - var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); - - async Task App(HttpContext context) - { - context.RequestAborted.Register(() => - { - requestAborted.SetResult(null); - }); - - try - { - await context.Request.Body.CopyToAsync(context.Response.Body); - } - catch (Exception ex) - { - copyToAsyncCts.SetException(ex); - throw; - } - - copyToAsyncCts.SetException(new Exception("This shouldn't be reached.")); - } - - using (var server = new TestServer(App, testContext, listenOptions)) - { - using (var connection = server.CreateConnection()) - { - // Close the connection with the last request so AssertStreamCompleted actually completes. - await connection.Send( - "POST / HTTP/1.1", - "Host:", - $"Content-Length: {responseSize}", - "", - ""); - - var sendTask = Task.Run(async () => - { - for (var i = 0; i < bufferCount; i++) - { - await connection.Stream.WriteAsync(buffer, 0, buffer.Length); - await Task.Delay(10); - } - }); - - await requestAborted.Task.DefaultTimeout(); - await responseRateTimeoutMessageLogged.Task.DefaultTimeout(); - await connectionStopMessageLogged.Task.DefaultTimeout(); - - // Expect OperationCanceledException instead of IOException because the server initiated the abort due to a response rate timeout. - await Assert.ThrowsAnyAsync(() => copyToAsyncCts.Task).DefaultTimeout(); - await AssertStreamAborted(connection.Stream, responseSize); - } - } - } - - [Fact] - public async Task ConnectionNotClosedWhenClientSatisfiesMinimumDataRateGivenLargeResponseChunks() - { - var chunkSize = 64 * 128 * 1024; - var chunkCount = 4; - var chunkData = new byte[chunkSize]; - - var requestAborted = false; - var appFuncCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var mockKestrelTrace = new Mock(); - - var testContext = new TestServiceContext - { - Log = mockKestrelTrace.Object, - SystemClock = new SystemClock(), - ServerOptions = - { - Limits = - { - MinResponseDataRate = new MinDataRate(bytesPerSecond: 240, gracePeriod: TimeSpan.FromSeconds(2)) - } - } - }; - - var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); - - async Task App(HttpContext context) - { - context.RequestAborted.Register(() => - { - requestAborted = true; - }); - - for (var i = 0; i < chunkCount; i++) - { - await context.Response.Body.WriteAsync(chunkData, 0, chunkData.Length, context.RequestAborted); - } - - appFuncCompleted.SetResult(null); - } - - using (var server = new TestServer(App, testContext, listenOptions)) - { - using (var connection = server.CreateConnection()) - { - // Close the connection with the last request so AssertStreamCompleted actually completes. - await connection.Send( - "GET / HTTP/1.1", - "Host:", - "Connection: close", - "", - ""); - - var minTotalOutputSize = chunkCount * chunkSize; - - // Make sure consuming a single chunk exceeds the 2 second timeout. - var targetBytesPerSecond = chunkSize / 4; - await AssertStreamCompleted(connection.Reader.BaseStream, minTotalOutputSize, targetBytesPerSecond); - await appFuncCompleted.Task.DefaultTimeout(); - - mockKestrelTrace.Verify(t => t.ResponseMininumDataRateNotSatisfied(It.IsAny(), It.IsAny()), Times.Never()); - mockKestrelTrace.Verify(t => t.ConnectionStop(It.IsAny()), Times.Once()); - Assert.False(requestAborted); - } - } - } - - [Fact] - public async Task ConnectionNotClosedWhenClientSatisfiesMinimumDataRateGivenLargeResponseHeaders() - { - var headerSize = 1024 * 1024; // 1 MB for each header value - var headerCount = 64; // 64 MB of headers per response - var requestCount = 4; // Minimum of 256 MB of total response headers - var headerValue = new string('a', headerSize); - var headerStringValues = new StringValues(Enumerable.Repeat(headerValue, headerCount).ToArray()); - - var requestAborted = false; - var mockKestrelTrace = new Mock(); - - var testContext = new TestServiceContext - { - Log = mockKestrelTrace.Object, - SystemClock = new SystemClock(), - ServerOptions = - { - Limits = - { - MinResponseDataRate = new MinDataRate(bytesPerSecond: 240, gracePeriod: TimeSpan.FromSeconds(2)) - } - } - }; - - var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); - - async Task App(HttpContext context) - { - context.RequestAborted.Register(() => - { - requestAborted = true; - }); - - context.Response.Headers[$"X-Custom-Header"] = headerStringValues; - context.Response.ContentLength = 0; - - await context.Response.Body.FlushAsync(); - } - - using (var server = new TestServer(App, testContext, listenOptions)) - { - using (var connection = server.CreateConnection()) - { - for (var i = 0; i < requestCount - 1; i++) - { - await connection.Send( - "GET / HTTP/1.1", - "Host:", - "", - ""); - } - - // Close the connection with the last request so AssertStreamCompleted actually completes. - await connection.Send( - "GET / HTTP/1.1", - "Host:", - "Connection: close", - "", - ""); - - var responseSize = headerSize * headerCount; - var minTotalOutputSize = requestCount * responseSize; - - // Make sure consuming a single set of response headers exceeds the 2 second timeout. - var targetBytesPerSecond = responseSize / 4; - await AssertStreamCompleted(connection.Reader.BaseStream, minTotalOutputSize, targetBytesPerSecond); - - mockKestrelTrace.Verify(t => t.ResponseMininumDataRateNotSatisfied(It.IsAny(), It.IsAny()), Times.Never()); - mockKestrelTrace.Verify(t => t.ConnectionStop(It.IsAny()), Times.Once()); - Assert.False(requestAborted); - } - } - } - [Fact] public async Task NonZeroContentLengthFor304StatusCodeIsAllowed() { @@ -3284,77 +2344,115 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - private async Task AssertStreamAborted(Stream stream, int totalBytes) + private static async Task ResponseStatusCodeSetBeforeHttpContextDispose( + ITestSink testSink, + ILoggerFactory loggerFactory, + RequestDelegate handler, + HttpStatusCode? expectedClientStatusCode, + HttpStatusCode expectedServerStatusCode, + bool sendMalformedRequest = false) { - var receiveBuffer = new byte[64 * 1024]; - var totalReceived = 0; + var mockHttpContextFactory = new Mock(); + mockHttpContextFactory.Setup(f => f.Create(It.IsAny())) + .Returns(fc => new DefaultHttpContext(fc)); - try - { - while (totalReceived < totalBytes) + var disposedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + mockHttpContextFactory.Setup(f => f.Dispose(It.IsAny())) + .Callback(c => { - var bytes = await stream.ReadAsync(receiveBuffer, 0, receiveBuffer.Length).DefaultTimeout(); + disposedTcs.TrySetResult(c.Response.StatusCode); + }); - if (bytes == 0) + using (var server = new TestServer(handler, new TestServiceContext(loggerFactory), + options => options.ListenOptions.Add(new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))), + services => services.AddSingleton(mockHttpContextFactory.Object))) + { + using (var connection = server.CreateConnection()) + { + if (!sendMalformedRequest) { - break; + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "Connection: close", + "", + ""); + + using (var reader = new StreamReader(connection.Stream, Encoding.ASCII, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true)) + { + try + { + var response = await reader.ReadToEndAsync().DefaultTimeout(); + Assert.Equal(expectedClientStatusCode, GetStatus(response)); + } + catch + { + if (expectedClientStatusCode != null) + { + throw; + } + } + } } + else + { + await connection.Send( + "POST / HTTP/1.1", + "Host:", + "Transfer-Encoding: chunked", + "", + "gg"); - totalReceived += bytes; + if (expectedClientStatusCode == HttpStatusCode.OK) + { + await connection.ReceiveForcedEnd( + "HTTP/1.1 200 OK", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 0", + "", + ""); + } + else + { + await connection.ReceiveForcedEnd( + "HTTP/1.1 400 Bad Request", + "Connection: close", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 0", + "", + ""); + } + } } - } - catch (IOException) - { - // This is expected given an abort. + + var disposedStatusCode = await disposedTcs.Task.DefaultTimeout(); + Assert.Equal(expectedServerStatusCode, (HttpStatusCode)disposedStatusCode); } - Assert.True(totalReceived < totalBytes, $"{nameof(AssertStreamAborted)} Stream completed successfully."); + if (sendMalformedRequest) + { + Assert.Contains(testSink.Writes, w => w.EventId.Id == 17 && w.LogLevel == LogLevel.Information && w.Exception is BadHttpRequestException + && ((BadHttpRequestException)w.Exception).StatusCode == StatusCodes.Status400BadRequest); + } + else + { + Assert.DoesNotContain(testSink.Writes, w => w.EventId.Id == 17 && w.LogLevel == LogLevel.Information && w.Exception is BadHttpRequestException + && ((BadHttpRequestException)w.Exception).StatusCode == StatusCodes.Status400BadRequest); + } } - private async Task AssertStreamCompleted(Stream stream, long minimumBytes, int targetBytesPerSecond) + private static HttpStatusCode GetStatus(string response) { - var receiveBuffer = new byte[64 * 1024]; - var received = 0; - var totalReceived = 0; - var startTime = DateTimeOffset.UtcNow; + var statusStart = response.IndexOf(' ') + 1; + var statusEnd = response.IndexOf(' ', statusStart) - 1; + var statusLength = statusEnd - statusStart + 1; - do + if (statusLength < 1) { - received = await stream.ReadAsync(receiveBuffer, 0, receiveBuffer.Length); - totalReceived += received; - - var expectedTimeElapsed = TimeSpan.FromSeconds(totalReceived / targetBytesPerSecond); - var timeElapsed = DateTimeOffset.UtcNow - startTime; - if (timeElapsed < expectedTimeElapsed) - { - await Task.Delay(expectedTimeElapsed - timeElapsed); - } - } while (received > 0); - - Assert.True(totalReceived >= minimumBytes, $"{nameof(AssertStreamCompleted)} Stream aborted prematurely."); - } - - public static TheoryData NullHeaderData - { - get - { - var dataset = new TheoryData(); - - // Unknown headers - dataset.Add("NullString", (string)null, null); - dataset.Add("EmptyString", "", ""); - dataset.Add("NullStringArray", new string[] { null }, null); - dataset.Add("EmptyStringArray", new string[] { "" }, ""); - dataset.Add("MixedStringArray", new string[] { null, "" }, ""); - // Known headers - dataset.Add("Location", (string)null, null); - dataset.Add("Location", "", ""); - dataset.Add("Location", new string[] { null }, null); - dataset.Add("Location", new string[] { "" }, ""); - dataset.Add("Location", new string[] { null, "" }, ""); - - return dataset; + throw new InvalidDataException($"No StatusCode found in '{response}'"); } + + return (HttpStatusCode)int.Parse(response.Substring(statusStart, statusLength)); } } } diff --git a/test/Kestrel.InMemory.FunctionalTests/TestTransport/InMemoryConnection.cs b/test/Kestrel.InMemory.FunctionalTests/TestTransport/InMemoryConnection.cs new file mode 100644 index 0000000000..e88948a72e --- /dev/null +++ b/test/Kestrel.InMemory.FunctionalTests/TestTransport/InMemoryConnection.cs @@ -0,0 +1,40 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal; +using Microsoft.AspNetCore.Testing; + +namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport +{ + public class InMemoryConnection : StreamBackedTestConnection + { + private readonly InMemoryTransportConnection _transportConnection; + + public InMemoryConnection(InMemoryTransportConnection transportConnection) + : base(new RawStream(transportConnection.Output, transportConnection.Input)) + { + _transportConnection = transportConnection; + } + + public override void Reset() + { + _transportConnection.Input.Complete(new ConnectionResetException(string.Empty)); + _transportConnection.OnClosed(); + } + + public override void ShutdownSend() + { + _transportConnection.Input.Complete(); + _transportConnection.OnClosed(); + } + + public override void Dispose() + { + _transportConnection.Input.Complete(); + _transportConnection.Output.Complete(); + _transportConnection.OnClosed(); + base.Dispose(); + } + } +} diff --git a/test/Kestrel.InMemory.FunctionalTests/TestTransport/InMemoryHttpClientSlim.cs b/test/Kestrel.InMemory.FunctionalTests/TestTransport/InMemoryHttpClientSlim.cs new file mode 100644 index 0000000000..557cedd4bf --- /dev/null +++ b/test/Kestrel.InMemory.FunctionalTests/TestTransport/InMemoryHttpClientSlim.cs @@ -0,0 +1,137 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Globalization; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Security; +using System.Security.Authentication; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport +{ + /// + /// Lightweight version of HttpClient implemented on top of an arbitrary Stream. + /// + public class InMemoryHttpClientSlim + { + private readonly TestServer _inMemoryTestServer; + + public InMemoryHttpClientSlim(TestServer testServer) + { + _inMemoryTestServer = testServer; + } + + public async Task GetStringAsync(string requestUri, bool validateCertificate = true) + => await GetStringAsync(new Uri(requestUri), validateCertificate).ConfigureAwait(false); + + public async Task GetStringAsync(Uri requestUri, bool validateCertificate = true) + { + using (var connection = _inMemoryTestServer.CreateConnection()) + using (var stream = await GetStream(connection.Stream, requestUri, validateCertificate).ConfigureAwait(false)) + { + using (var writer = new StreamWriter(stream, Encoding.ASCII, bufferSize: 1024, leaveOpen: true)) + { + await writer.WriteAsync($"GET {requestUri.PathAndQuery} HTTP/1.0\r\n").ConfigureAwait(false); + await writer.WriteAsync($"Host: {GetHost(requestUri)}\r\n").ConfigureAwait(false); + await writer.WriteAsync("\r\n").ConfigureAwait(false); + } + + return await ReadResponse(stream).ConfigureAwait(false); + } + } + + internal static string GetHost(Uri requestUri) + { + var authority = requestUri.Authority; + if (requestUri.HostNameType == UriHostNameType.IPv6) + { + // Make sure there's no % scope id. https://github.com/aspnet/KestrelHttpServer/issues/2637 + var address = IPAddress.Parse(requestUri.Host); + address = new IPAddress(address.GetAddressBytes()); // Drop scope Id. + if (requestUri.IsDefaultPort) + { + authority = $"[{address}]"; + } + else + { + authority = $"[{address}]:{requestUri.Port.ToString(CultureInfo.InvariantCulture)}"; + } + } + return authority; + } + + public async Task PostAsync(string requestUri, HttpContent content, bool validateCertificate = true) + => await PostAsync(new Uri(requestUri), content, validateCertificate).ConfigureAwait(false); + + public async Task PostAsync(Uri requestUri, HttpContent content, bool validateCertificate = true) + { + using (var connection = _inMemoryTestServer.CreateConnection()) + using (var stream = await GetStream(connection.Stream, requestUri, validateCertificate).ConfigureAwait(false)) + { + using (var writer = new StreamWriter(stream, Encoding.ASCII, bufferSize: 1024, leaveOpen: true)) + { + await writer.WriteAsync($"POST {requestUri.PathAndQuery} HTTP/1.0\r\n").ConfigureAwait(false); + await writer.WriteAsync($"Host: {requestUri.Authority}\r\n").ConfigureAwait(false); + await writer.WriteAsync($"Content-Type: {content.Headers.ContentType}\r\n").ConfigureAwait(false); + await writer.WriteAsync($"Content-Length: {content.Headers.ContentLength}\r\n").ConfigureAwait(false); + await writer.WriteAsync("\r\n").ConfigureAwait(false); + } + + await content.CopyToAsync(stream).ConfigureAwait(false); + + return await ReadResponse(stream).ConfigureAwait(false); + } + } + + private static async Task ReadResponse(Stream stream) + { + using (var reader = new StreamReader(stream, Encoding.ASCII, detectEncodingFromByteOrderMarks: true, + bufferSize: 1024, leaveOpen: true)) + { + var response = await reader.ReadToEndAsync().DefaultTimeout().ConfigureAwait(false); + + var status = GetStatus(response); + new HttpResponseMessage(status).EnsureSuccessStatusCode(); + + var body = response.Substring(response.IndexOf("\r\n\r\n") + 4); + return body; + } + } + + private static HttpStatusCode GetStatus(string response) + { + var statusStart = response.IndexOf(' ') + 1; + var statusEnd = response.IndexOf(' ', statusStart) - 1; + var statusLength = statusEnd - statusStart + 1; + + if (statusLength < 1) + { + throw new InvalidDataException($"No StatusCode found in '{response}'"); + } + + return (HttpStatusCode)int.Parse(response.Substring(statusStart, statusLength)); + } + + private static async Task GetStream(Stream rawStream, Uri requestUri, bool validateCertificate) + { + if (requestUri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) + { + var sslStream = new SslStream(rawStream, leaveInnerStreamOpen: false, userCertificateValidationCallback: + validateCertificate ? null : (RemoteCertificateValidationCallback)((a, b, c, d) => true)); + + await sslStream.AuthenticateAsClientAsync(requestUri.Host, clientCertificates: null, + enabledSslProtocols: SslProtocols.Tls11 | SslProtocols.Tls12, + checkCertificateRevocation: validateCertificate).ConfigureAwait(false); + return sslStream; + } + else + { + return rawStream; + } + } + } +} \ No newline at end of file diff --git a/test/Kestrel.InMemory.FunctionalTests/TestTransport/InMemoryTransportConnection.cs b/test/Kestrel.InMemory.FunctionalTests/TestTransport/InMemoryTransportConnection.cs new file mode 100644 index 0000000000..984f6f5b22 --- /dev/null +++ b/test/Kestrel.InMemory.FunctionalTests/TestTransport/InMemoryTransportConnection.cs @@ -0,0 +1,60 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Buffers; +using System.IO.Pipelines; +using System.Net; +using System.Threading; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; + +namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport +{ + public class InMemoryTransportConnection : TransportConnection, IDisposable + { + private readonly CancellationTokenSource _connectionClosedTokenSource = new CancellationTokenSource(); + + private bool _isClosed; + + public InMemoryTransportConnection(MemoryPool memoryPool) + { + MemoryPool = memoryPool; + + LocalAddress = IPAddress.Loopback; + RemoteAddress = IPAddress.Loopback; + + ConnectionClosed = _connectionClosedTokenSource.Token; + } + + public override MemoryPool MemoryPool { get; } + public override PipeScheduler InputWriterScheduler => PipeScheduler.ThreadPool; + public override PipeScheduler OutputReaderScheduler => PipeScheduler.ThreadPool; + + public override void Abort(ConnectionAbortedException abortReason) + { + Input.Complete(abortReason); + } + + public void OnClosed() + { + if (_isClosed) + { + return; + } + + _isClosed = true; + + ThreadPool.QueueUserWorkItem(state => + { + var self = (InMemoryTransportConnection)state; + self._connectionClosedTokenSource.Cancel(); + }, this); + } + + public void Dispose() + { + _connectionClosedTokenSource.Dispose(); + } + } +} diff --git a/test/Kestrel.InMemory.FunctionalTests/TestTransport/InMemoryTransportFactory.cs b/test/Kestrel.InMemory.FunctionalTests/TestTransport/InMemoryTransportFactory.cs new file mode 100644 index 0000000000..e099222bc2 --- /dev/null +++ b/test/Kestrel.InMemory.FunctionalTests/TestTransport/InMemoryTransportFactory.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; + +namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport +{ + public class InMemoryTransportFactory : ITransportFactory + { + public ITransport Create(IEndPointInformation endPointInformation, IConnectionDispatcher dispatcher) + { + if (ConnectionDispatcher != null) + { + throw new InvalidOperationException("InMemoryTransportFactory doesn't support creating multiple endpoints"); + } + + ConnectionDispatcher = dispatcher; + + return new NoopTransport(); + } + + public IConnectionDispatcher ConnectionDispatcher { get; private set; } + + private class NoopTransport : ITransport + { + public Task BindAsync() + { + return Task.CompletedTask; + } + + public Task StopAsync() + { + return Task.CompletedTask; + } + + public Task UnbindAsync() + { + return Task.CompletedTask; + } + } + } +} diff --git a/test/Kestrel.InMemory.FunctionalTests/TestTransport/TestServer.cs b/test/Kestrel.InMemory.FunctionalTests/TestTransport/TestServer.cs new file mode 100644 index 0000000000..eab6513a53 --- /dev/null +++ b/test/Kestrel.InMemory.FunctionalTests/TestTransport/TestServer.cs @@ -0,0 +1,155 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Buffers; +using System.Diagnostics; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport +{ + /// + /// In-memory TestServer + /// _memoryPool; + private readonly RequestDelegate _app; + private readonly InMemoryTransportFactory _transportFactory; + private readonly IWebHost _host; + + public TestServer(RequestDelegate app) + : this(app, new TestServiceContext()) + { + } + + public TestServer(RequestDelegate app, TestServiceContext context) + : this(app, context, new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))) + { + // The endpoint is ignored, but this ensures no cert loading happens for HTTPS endpoints. + } + + public TestServer(RequestDelegate app, TestServiceContext context, ListenOptions listenOptions) + : this(app, context, options => options.ListenOptions.Add(listenOptions), _ => { }) + { + } + + public TestServer(RequestDelegate app, TestServiceContext context, Action configureListenOptions) + : this(app, context, options => + { + var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) + { + KestrelServerOptions = options + }; + + configureListenOptions(listenOptions); + options.ListenOptions.Add(listenOptions); + }, + _ => { }) + { + } + + public TestServer(RequestDelegate app, TestServiceContext context, Action configureKestrel, Action configureServices) + { + _app = app; + Context = context; + _memoryPool = context.MemoryPoolFactory(); + _transportFactory = new InMemoryTransportFactory(); + HttpClientSlim = new InMemoryHttpClientSlim(this); + + + var hostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + configureServices(services); + + services.AddSingleton(this); + services.AddSingleton(context.LoggerFactory); + + services.AddSingleton(sp => + { + context.ServerOptions.ApplicationServices = sp; + configureKestrel(context.ServerOptions); + return new KestrelServer(_transportFactory, context); + }); + }); + + _host = hostBuilder.Build(); + + _host.Start(); + } + + public int Port => 0; + + public TestServiceContext Context { get; } + + public InMemoryHttpClientSlim HttpClientSlim { get; } + + public InMemoryConnection CreateConnection() + { + var transportConnection = new InMemoryTransportConnection(_memoryPool); + _ = HandleConnection(transportConnection); + return new InMemoryConnection(transportConnection); + } + + public Task StopAsync() + { + return _host.StopAsync(); + } + + public void Dispose() + { + _host.Dispose(); + _memoryPool.Dispose(); + } + + void IStartup.Configure(IApplicationBuilder app) + { + app.Run(_app); + } + + IServiceProvider IStartup.ConfigureServices(IServiceCollection services) + { + return services.BuildServiceProvider(); + } + + private async Task HandleConnection(InMemoryTransportConnection transportConnection) + { + try + { + var middlewareTask = _transportFactory.ConnectionDispatcher.OnConnection(transportConnection); + var transportTask = CancellationTokenAsTask(transportConnection.ConnectionClosed); + + await transportTask; + await middlewareTask; + + transportConnection.Dispose(); + } + catch (Exception ex) + { + Debug.Assert(false, $"Unexpected exception: {ex}."); + } + } + + private static Task CancellationTokenAsTask(CancellationToken token) + { + if (token.IsCancellationRequested) + { + return Task.CompletedTask; + } + + var tcs = new TaskCompletionSource(); + token.Register(() => tcs.SetResult(null)); + return tcs.Task; + } + } +} diff --git a/test/Kestrel.FunctionalTests/UpgradeTests.cs b/test/Kestrel.InMemory.FunctionalTests/UpgradeTests.cs similarity index 97% rename from test/Kestrel.FunctionalTests/UpgradeTests.cs rename to test/Kestrel.InMemory.FunctionalTests/UpgradeTests.cs index 2b4027d37c..42f5659d5e 100644 --- a/test/Kestrel.FunctionalTests/UpgradeTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/UpgradeTests.cs @@ -7,12 +7,13 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; +using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; using Microsoft.AspNetCore.Server.Kestrel.Tests; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging.Testing; using Xunit; -namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { public class UpgradeTests : LoggedTest { @@ -138,7 +139,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests $"Date: {server.Context.DateHeaderValue}", "", ""); - await connection.WaitForConnectionClose().DefaultTimeout(); + await connection.WaitForConnectionClose(); } var ex = await Assert.ThrowsAsync(async () => await upgradeTcs.Task.DefaultTimeout()); @@ -275,7 +276,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } }, serviceContext)) { - using (var disposables = new DisposableStack()) + using (var disposables = new DisposableStack()) { for (var i = 0; i < limit; i++) { diff --git a/test/Kestrel.FunctionalTests/HttpsConnectionAdapterOptionsTest.cs b/test/Kestrel.Tests/HttpsConnectionAdapterOptionsTest.cs similarity index 96% rename from test/Kestrel.FunctionalTests/HttpsConnectionAdapterOptionsTest.cs rename to test/Kestrel.Tests/HttpsConnectionAdapterOptionsTest.cs index 7d32d4be96..13b80b629b 100644 --- a/test/Kestrel.FunctionalTests/HttpsConnectionAdapterOptionsTest.cs +++ b/test/Kestrel.Tests/HttpsConnectionAdapterOptionsTest.cs @@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Https; using Xunit; -namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +namespace Microsoft.AspNetCore.Server.Kestrel.Tests { public class HttpsConnectionAdapterOptionsTests { diff --git a/test/Kestrel.Tests/Kestrel.Tests.csproj b/test/Kestrel.Tests/Kestrel.Tests.csproj index 6ecbb0b1e2..ec0ae9149a 100644 --- a/test/Kestrel.Tests/Kestrel.Tests.csproj +++ b/test/Kestrel.Tests/Kestrel.Tests.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/test/Kestrel.FunctionalTests/AddressRegistrationTests.cs b/test/Kestrel.Transport.BindTests/AddressRegistrationTests.cs similarity index 100% rename from test/Kestrel.FunctionalTests/AddressRegistrationTests.cs rename to test/Kestrel.Transport.BindTests/AddressRegistrationTests.cs diff --git a/test/Kestrel.Transport.BindTests/Properties/AssemblyInfo.cs b/test/Kestrel.Transport.BindTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..678468c757 --- /dev/null +++ b/test/Kestrel.Transport.BindTests/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Extensions.Logging.Testing; + +[assembly: ShortClassName] diff --git a/test/Kestrel.Transport.FunctionalTests/ConnectionAdapterTests.cs b/test/Kestrel.Transport.FunctionalTests/ConnectionAdapterTests.cs new file mode 100644 index 0000000000..704f521d64 --- /dev/null +++ b/test/Kestrel.Transport.FunctionalTests/ConnectionAdapterTests.cs @@ -0,0 +1,60 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Logging.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +{ + public class ConnectionAdapterTests : LoggedTest + { + [Fact] + public async Task ThrowingSynchronousConnectionAdapterDoesNotCrashServer() + { + var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) + { + ConnectionAdapters = { new ThrowingConnectionAdapter() } + }; + + var serviceContext = new TestServiceContext(LoggerFactory); + + using (var server = new TestServer(TestApp.EchoApp, serviceContext, listenOptions)) + { + using (var connection = server.CreateConnection()) + { + // Will throw because the exception in the connection adapter will close the connection. + await Assert.ThrowsAsync(async () => + { + await connection.Send( + "POST / HTTP/1.0", + "Content-Length: 1000", + "\r\n"); + + for (var i = 0; i < 1000; i++) + { + await connection.Send("a"); + await Task.Delay(5); + } + }); + } + } + } + + private class ThrowingConnectionAdapter : IConnectionAdapter + { + public bool IsHttps => false; + + public Task OnConnectionAsync(ConnectionAdapterContext context) + { + throw new Exception(); + } + } + } +} diff --git a/test/Kestrel.FunctionalTests/Http2/H2SpecTests.cs b/test/Kestrel.Transport.FunctionalTests/Http2/H2SpecTests.cs similarity index 100% rename from test/Kestrel.FunctionalTests/Http2/H2SpecTests.cs rename to test/Kestrel.Transport.FunctionalTests/Http2/H2SpecTests.cs diff --git a/test/Kestrel.FunctionalTests/Http2/HandshakeTests.cs b/test/Kestrel.Transport.FunctionalTests/Http2/HandshakeTests.cs similarity index 100% rename from test/Kestrel.FunctionalTests/Http2/HandshakeTests.cs rename to test/Kestrel.Transport.FunctionalTests/Http2/HandshakeTests.cs diff --git a/test/Kestrel.FunctionalTests/Http2/ShutdownTests.cs b/test/Kestrel.Transport.FunctionalTests/Http2/ShutdownTests.cs similarity index 91% rename from test/Kestrel.FunctionalTests/Http2/ShutdownTests.cs rename to test/Kestrel.Transport.FunctionalTests/Http2/ShutdownTests.cs index 0950898df1..0166a0cbc6 100644 --- a/test/Kestrel.FunctionalTests/Http2/ShutdownTests.cs +++ b/test/Kestrel.Transport.FunctionalTests/Http2/ShutdownTests.cs @@ -86,13 +86,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.Http2 { var requestStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var requestUnblocked = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var memoryPoolFactory = new DiagnosticMemoryPoolFactory(allowLateReturn: true); + + var testContext = new TestServiceContext(LoggerFactory) + { + MemoryPoolFactory = memoryPoolFactory.Create + }; + // Abortive shutdown leaves one request hanging - using (var server = new TestServer(TransportSelector.GetWebHostBuilder(new DiagnosticMemoryPoolFactory(allowLateReturn: true).Create), async context => + using (var server = new TestServer(async context => { requestStarted.SetResult(null); await requestUnblocked.Task.DefaultTimeout(); await context.Response.WriteAsync("hello world " + context.Request.Protocol); - }, new TestServiceContext(LoggerFactory), + }, + testContext, kestrelOptions => { kestrelOptions.Listen(IPAddress.Loopback, 0, listenOptions => @@ -114,6 +123,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.Http2 Assert.Contains(TestApplicationErrorLogger.Messages, m => m.Message.Contains("is closed. The last processed stream ID was 1.")); Assert.Contains(TestApplicationErrorLogger.Messages, m => m.Message.Contains("Some connections failed to close gracefully during server shutdown.")); Assert.DoesNotContain(TestApplicationErrorLogger.Messages, m => m.Message.Contains("Request finished in")); + + requestUnblocked.SetResult(null); + + await memoryPoolFactory.WhenAllBlocksReturned(TestConstants.DefaultTimeout); } } } diff --git a/test/Kestrel.FunctionalTests/MaxRequestBufferSizeTests.cs b/test/Kestrel.Transport.FunctionalTests/MaxRequestBufferSizeTests.cs similarity index 100% rename from test/Kestrel.FunctionalTests/MaxRequestBufferSizeTests.cs rename to test/Kestrel.Transport.FunctionalTests/MaxRequestBufferSizeTests.cs diff --git a/test/Kestrel.FunctionalTests/Properties/AssemblyInfo.cs b/test/Kestrel.Transport.FunctionalTests/Properties/AssemblyInfo.cs similarity index 100% rename from test/Kestrel.FunctionalTests/Properties/AssemblyInfo.cs rename to test/Kestrel.Transport.FunctionalTests/Properties/AssemblyInfo.cs diff --git a/test/Kestrel.Transport.FunctionalTests/RequestTests.cs b/test/Kestrel.Transport.FunctionalTests/RequestTests.cs new file mode 100644 index 0000000000..81ef08d614 --- /dev/null +++ b/test/Kestrel.Transport.FunctionalTests/RequestTests.cs @@ -0,0 +1,854 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Connections.Features; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; +using Microsoft.AspNetCore.Testing; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; +using Moq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +{ + public class RequestTests : LoggedTest + { + private const int _connectionStartedEventId = 1; + private const int _connectionResetEventId = 19; + private static readonly int _semaphoreWaitTimeout = Debugger.IsAttached ? 10000 : 2500; + + public static TheoryData ConnectionAdapterData => new TheoryData + { + new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)), + new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) + { + ConnectionAdapters = { new PassThroughConnectionAdapter() } + } + }; + + [Theory] + [InlineData(10 * 1024 * 1024, true)] + // In the following dataset, send at least 2GB. + // Never change to a lower value, otherwise regression testing for + // https://github.com/aspnet/KestrelHttpServer/issues/520#issuecomment-188591242 + // will be lost. + [InlineData((long)int.MaxValue + 1, false)] + public void LargeUpload(long contentLength, bool checkBytes) + { + const int bufferLength = 1024 * 1024; + Assert.True(contentLength % bufferLength == 0, $"{nameof(contentLength)} sent must be evenly divisible by {bufferLength}."); + Assert.True(bufferLength % 256 == 0, $"{nameof(bufferLength)} must be evenly divisible by 256"); + + var builder = TransportSelector.GetWebHostBuilder() + .ConfigureServices(AddTestLogging) + .UseKestrel(options => + { + options.Limits.MaxRequestBodySize = contentLength; + options.Limits.MinRequestBodyDataRate = null; + }) + .UseUrls("http://127.0.0.1:0/") + .Configure(app => + { + app.Run(async context => + { + // Read the full request body + long total = 0; + var receivedBytes = new byte[bufferLength]; + var received = 0; + while ((received = await context.Request.Body.ReadAsync(receivedBytes, 0, receivedBytes.Length)) > 0) + { + if (checkBytes) + { + for (var i = 0; i < received; i++) + { + // Do not use Assert.Equal here, it is to slow for this hot path + Assert.True((byte)((total + i) % 256) == receivedBytes[i], "Data received is incorrect"); + } + } + + total += received; + } + + await context.Response.WriteAsync(total.ToString(CultureInfo.InvariantCulture)); + }); + }); + + using (var host = builder.Build()) + { + host.Start(); + + using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) + { + socket.Connect(new IPEndPoint(IPAddress.Loopback, host.GetPort())); + socket.Send(Encoding.ASCII.GetBytes($"POST / HTTP/1.0\r\nContent-Length: {contentLength}\r\n\r\n")); + + var contentBytes = new byte[bufferLength]; + + if (checkBytes) + { + for (var i = 0; i < contentBytes.Length; i++) + { + contentBytes[i] = (byte)i; + } + } + + for (var i = 0; i < contentLength / contentBytes.Length; i++) + { + socket.Send(contentBytes); + } + + var response = new StringBuilder(); + var responseBytes = new byte[4096]; + var received = 0; + while ((received = socket.Receive(responseBytes)) > 0) + { + response.Append(Encoding.ASCII.GetString(responseBytes, 0, received)); + } + + Assert.Contains(contentLength.ToString(CultureInfo.InvariantCulture), response.ToString()); + } + } + } + + [Fact] + public Task RemoteIPv4Address() + { + return TestRemoteIPAddress("127.0.0.1", "127.0.0.1", "127.0.0.1"); + } + + [ConditionalFact] + [IPv6SupportedCondition] + public Task RemoteIPv6Address() + { + return TestRemoteIPAddress("[::1]", "[::1]", "::1"); + } + + [Fact] + public async Task DoesNotHangOnConnectionCloseRequest() + { + var builder = TransportSelector.GetWebHostBuilder() + .UseKestrel() + .UseUrls("http://127.0.0.1:0") + .ConfigureServices(AddTestLogging) + .Configure(app => + { + app.Run(async context => + { + await context.Response.WriteAsync("hello, world"); + }); + }); + + using (var host = builder.Build()) + using (var client = new HttpClient()) + { + host.Start(); + + client.DefaultRequestHeaders.Connection.Clear(); + client.DefaultRequestHeaders.Connection.Add("close"); + + var response = await client.GetAsync($"http://127.0.0.1:{host.GetPort()}/"); + response.EnsureSuccessStatusCode(); + } + } + + [Fact] + public async Task ConnectionResetPriorToRequestIsLoggedAsDebug() + { + var connectionStarted = new SemaphoreSlim(0); + var connectionReset = new SemaphoreSlim(0); + var loggedHigherThanDebug = false; + + var mockLogger = new Mock(); + mockLogger + .Setup(logger => logger.IsEnabled(It.IsAny())) + .Returns(true); + mockLogger + .Setup(logger => logger.Log(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())) + .Callback>((logLevel, eventId, state, exception, formatter) => + { + Logger.Log(logLevel, eventId, state, exception, formatter); + if (eventId.Id == _connectionStartedEventId) + { + connectionStarted.Release(); + } + else if (eventId.Id == _connectionResetEventId) + { + connectionReset.Release(); + } + + if (logLevel > LogLevel.Debug) + { + loggedHigherThanDebug = true; + } + }); + + var mockLoggerFactory = new Mock(); + mockLoggerFactory + .Setup(factory => factory.CreateLogger(It.IsAny())) + .Returns(Logger); + mockLoggerFactory + .Setup(factory => factory.CreateLogger(It.IsIn("Microsoft.AspNetCore.Server.Kestrel", + "Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv", + "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets"))) + .Returns(mockLogger.Object); + + using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(mockLoggerFactory.Object))) + { + using (var connection = server.CreateConnection()) + { + // Wait until connection is established + Assert.True(await connectionStarted.WaitAsync(TestConstants.DefaultTimeout)); + + connection.Reset(); + } + + // If the reset is correctly logged as Debug, the wait below should complete shortly. + // This check MUST come before disposing the server, otherwise there's a race where the RST + // is still in flight when the connection is aborted, leading to the reset never being received + // and therefore not logged. + Assert.True(await connectionReset.WaitAsync(TestConstants.DefaultTimeout)); + } + + Assert.False(loggedHigherThanDebug); + } + + [Fact] + public async Task ConnectionResetBetweenRequestsIsLoggedAsDebug() + { + var connectionReset = new SemaphoreSlim(0); + var loggedHigherThanDebug = false; + + var mockLogger = new Mock(); + mockLogger + .Setup(logger => logger.IsEnabled(It.IsAny())) + .Returns(true); + mockLogger + .Setup(logger => logger.Log(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())) + .Callback>((logLevel, eventId, state, exception, formatter) => + { + Logger.Log(logLevel, eventId, state, exception, formatter); + if (eventId.Id == _connectionResetEventId) + { + connectionReset.Release(); + } + + if (logLevel > LogLevel.Debug) + { + loggedHigherThanDebug = true; + } + }); + + var mockLoggerFactory = new Mock(); + mockLoggerFactory + .Setup(factory => factory.CreateLogger(It.IsAny())) + .Returns(Logger); + mockLoggerFactory + .Setup(factory => factory.CreateLogger(It.IsIn("Microsoft.AspNetCore.Server.Kestrel", + "Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv", + "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets"))) + .Returns(mockLogger.Object); + + using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(mockLoggerFactory.Object))) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "", + ""); + + // Make sure the response is fully received, so a write failure (e.g. EPIPE) doesn't cause + // a more critical log message. + await connection.Receive( + "HTTP/1.1 200 OK", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 0", + "", + ""); + + connection.Reset(); + // Force a reset + } + + // If the reset is correctly logged as Debug, the wait below should complete shortly. + // This check MUST come before disposing the server, otherwise there's a race where the RST + // is still in flight when the connection is aborted, leading to the reset never being received + // and therefore not logged. + Assert.True(await connectionReset.WaitAsync(TestConstants.DefaultTimeout)); + } + + Assert.False(loggedHigherThanDebug); + } + + [Fact] + public async Task ConnectionResetMidRequestIsLoggedAsDebug() + { + var requestStarted = new SemaphoreSlim(0); + var connectionReset = new SemaphoreSlim(0); + var connectionClosing = new SemaphoreSlim(0); + var loggedHigherThanDebug = false; + + var mockLogger = new Mock(); + mockLogger + .Setup(logger => logger.IsEnabled(It.IsAny())) + .Returns(true); + mockLogger + .Setup(logger => logger.Log(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())) + .Callback>((logLevel, eventId, state, exception, formatter) => + { + Logger.Log(logLevel, eventId, state, exception, formatter); + var log = $"Log {logLevel}[{eventId}]: {formatter(state, exception)} {exception}"; + TestOutputHelper.WriteLine(log); + + if (eventId.Id == _connectionResetEventId) + { + connectionReset.Release(); + } + + if (logLevel > LogLevel.Debug) + { + loggedHigherThanDebug = true; + } + }); + + var mockLoggerFactory = new Mock(); + mockLoggerFactory + .Setup(factory => factory.CreateLogger(It.IsAny())) + .Returns(Logger); + mockLoggerFactory + .Setup(factory => factory.CreateLogger(It.IsIn("Microsoft.AspNetCore.Server.Kestrel", + "Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv", + "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets"))) + .Returns(mockLogger.Object); + + using (var server = new TestServer(async context => + { + requestStarted.Release(); + await connectionClosing.WaitAsync(); + }, + new TestServiceContext(mockLoggerFactory.Object))) + { + using (var connection = server.CreateConnection()) + { + await connection.SendEmptyGet(); + + // Wait until connection is established + Assert.True(await requestStarted.WaitAsync(TestConstants.DefaultTimeout), "request should have started"); + + connection.Reset(); + } + + // If the reset is correctly logged as Debug, the wait below should complete shortly. + // This check MUST come before disposing the server, otherwise there's a race where the RST + // is still in flight when the connection is aborted, leading to the reset never being received + // and therefore not logged. + Assert.True(await connectionReset.WaitAsync(TestConstants.DefaultTimeout), "Connection reset event should have been logged"); + connectionClosing.Release(); + } + + Assert.False(loggedHigherThanDebug, "Logged event should not have been higher than debug."); + } + + [Fact] + public async Task ThrowsOnReadAfterConnectionError() + { + var requestStarted = new SemaphoreSlim(0); + var connectionReset = new SemaphoreSlim(0); + var appDone = new SemaphoreSlim(0); + var expectedExceptionThrown = false; + + var builder = TransportSelector.GetWebHostBuilder() + .ConfigureServices(AddTestLogging) + .UseKestrel() + .UseUrls("http://127.0.0.1:0") + .Configure(app => app.Run(async context => + { + requestStarted.Release(); + Assert.True(await connectionReset.WaitAsync(_semaphoreWaitTimeout)); + + try + { + await context.Request.Body.ReadAsync(new byte[1], 0, 1); + } + catch (ConnectionResetException) + { + expectedExceptionThrown = true; + } + + appDone.Release(); + })); + + using (var host = builder.Build()) + { + host.Start(); + + using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) + { + socket.Connect(new IPEndPoint(IPAddress.Loopback, host.GetPort())); + socket.LingerState = new LingerOption(true, 0); + socket.Send(Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\nHost:\r\nContent-Length: 1\r\n\r\n")); + Assert.True(await requestStarted.WaitAsync(_semaphoreWaitTimeout)); + } + + connectionReset.Release(); + + Assert.True(await appDone.WaitAsync(_semaphoreWaitTimeout)); + Assert.True(expectedExceptionThrown); + } + } + + [Fact] + public async Task RequestAbortedTokenFiredOnClientFIN() + { + var appStarted = new SemaphoreSlim(0); + var requestAborted = new SemaphoreSlim(0); + var builder = TransportSelector.GetWebHostBuilder() + .UseKestrel() + .UseUrls("http://127.0.0.1:0") + .ConfigureServices(AddTestLogging) + .Configure(app => app.Run(async context => + { + appStarted.Release(); + + var token = context.RequestAborted; + token.Register(() => requestAborted.Release(2)); + await requestAborted.WaitAsync().DefaultTimeout(); + })); + + using (var host = builder.Build()) + { + host.Start(); + + using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) + { + socket.Connect(new IPEndPoint(IPAddress.Loopback, host.GetPort())); + socket.Send(Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\nHost:\r\n\r\n")); + await appStarted.WaitAsync(); + socket.Shutdown(SocketShutdown.Send); + await requestAborted.WaitAsync().DefaultTimeout(); + } + } + } + + [Fact] + public void AbortingTheConnectionSendsFIN() + { + var builder = TransportSelector.GetWebHostBuilder() + .UseKestrel() + .UseUrls("http://127.0.0.1:0") + .ConfigureServices(AddTestLogging) + .Configure(app => app.Run(context => + { + context.Abort(); + return Task.CompletedTask; + })); + + using (var host = builder.Build()) + { + host.Start(); + + using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) + { + socket.Connect(new IPEndPoint(IPAddress.Loopback, host.GetPort())); + socket.Send(Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\nHost:\r\n\r\n")); + int result = socket.Receive(new byte[32]); + Assert.Equal(0, result); + } + } + } + + [Theory] + [MemberData(nameof(ConnectionAdapterData))] + public async Task ConnectionClosedTokenFiresOnClientFIN(ListenOptions listenOptions) + { + var testContext = new TestServiceContext(LoggerFactory); + var appStartedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var connectionClosedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + using (var server = new TestServer(context => + { + appStartedTcs.SetResult(null); + + var connectionLifetimeFeature = context.Features.Get(); + connectionLifetimeFeature.ConnectionClosed.Register(() => connectionClosedTcs.SetResult(null)); + + return Task.CompletedTask; + }, testContext, listenOptions)) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "", + ""); + + await appStartedTcs.Task.DefaultTimeout(); + + connection.Shutdown(SocketShutdown.Send); + + await connectionClosedTcs.Task.DefaultTimeout(); + } + } + } + + [Theory] + [MemberData(nameof(ConnectionAdapterData))] + public async Task ConnectionClosedTokenFiresOnServerFIN(ListenOptions listenOptions) + { + var testContext = new TestServiceContext(LoggerFactory); + var connectionClosedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + using (var server = new TestServer(context => + { + var connectionLifetimeFeature = context.Features.Get(); + connectionLifetimeFeature.ConnectionClosed.Register(() => connectionClosedTcs.SetResult(null)); + + return Task.CompletedTask; + }, testContext, listenOptions)) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "Connection: close", + "", + ""); + + await connectionClosedTcs.Task.DefaultTimeout(); + + await connection.ReceiveEnd($"HTTP/1.1 200 OK", + "Connection: close", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 0", + "", + ""); + } + } + } + + [Theory] + [MemberData(nameof(ConnectionAdapterData))] + public async Task ConnectionClosedTokenFiresOnServerAbort(ListenOptions listenOptions) + { + var testContext = new TestServiceContext(LoggerFactory); + var connectionClosedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + using (var server = new TestServer(context => + { + var connectionLifetimeFeature = context.Features.Get(); + connectionLifetimeFeature.ConnectionClosed.Register(() => connectionClosedTcs.SetResult(null)); + + context.Abort(); + + return Task.CompletedTask; + }, testContext, listenOptions)) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "", + ""); + + await connectionClosedTcs.Task.DefaultTimeout(); + await connection.ReceiveForcedEnd(); + } + } + } + + [Theory] + [MemberData(nameof(ConnectionAdapterData))] + public async Task RequestsCanBeAbortedMidRead(ListenOptions listenOptions) + { + const int applicationAbortedConnectionId = 34; + + var testContext = new TestServiceContext(LoggerFactory); + + var readTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var registrationTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var requestId = 0; + + using (var server = new TestServer(async httpContext => + { + requestId++; + + var response = httpContext.Response; + var request = httpContext.Request; + var lifetime = httpContext.Features.Get(); + + lifetime.RequestAborted.Register(() => registrationTcs.TrySetResult(requestId)); + + if (requestId == 1) + { + response.Headers["Content-Length"] = new[] { "5" }; + + await response.WriteAsync("World"); + } + else + { + var readTask = request.Body.CopyToAsync(Stream.Null); + + lifetime.Abort(); + + try + { + await readTask; + } + catch (Exception ex) + { + readTcs.SetException(ex); + throw; + } + + readTcs.SetException(new Exception("This shouldn't be reached.")); + } + }, testContext, listenOptions)) + { + using (var connection = server.CreateConnection()) + { + // Full request and response + await connection.Send( + "POST / HTTP/1.1", + "Host:", + "Content-Length: 5", + "", + "Hello"); + + await connection.Receive( + "HTTP/1.1 200 OK", + $"Date: {testContext.DateHeaderValue}", + "Content-Length: 5", + "", + "World"); + + // Never send the body so CopyToAsync always fails. + await connection.Send("POST / HTTP/1.1", + "Host:", + "Content-Length: 5", + "", + ""); + await connection.WaitForConnectionClose(); + } + } + + await Assert.ThrowsAsync(async () => await readTcs.Task); + + // The cancellation token for only the last request should be triggered. + var abortedRequestId = await registrationTcs.Task; + Assert.Equal(2, abortedRequestId); + + Assert.Single(TestSink.Writes.Where(w => w.LoggerName == "Microsoft.AspNetCore.Server.Kestrel" && + w.EventId == applicationAbortedConnectionId)); + } + + [Theory] + [MemberData(nameof(ConnectionAdapterData))] + public async Task ServerCanAbortConnectionAfterUnobservedClose(ListenOptions listenOptions) + { + const int connectionPausedEventId = 4; + const int connectionFinSentEventId = 7; + const int maxRequestBufferSize = 4096; + + var readCallbackUnwired = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var clientClosedConnection = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var serverClosedConnection = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var appFuncCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var mockLogger = new Mock(); + mockLogger + .Setup(logger => logger.IsEnabled(It.IsAny())) + .Returns(true); + mockLogger + .Setup(logger => logger.Log(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())) + .Callback>((logLevel, eventId, state, exception, formatter) => + { + if (eventId.Id == connectionPausedEventId) + { + readCallbackUnwired.TrySetResult(null); + } + else if (eventId.Id == connectionFinSentEventId) + { + serverClosedConnection.SetResult(null); + } + + Logger.Log(logLevel, eventId, state, exception, formatter); + }); + + var mockLoggerFactory = new Mock(); + mockLoggerFactory + .Setup(factory => factory.CreateLogger(It.IsAny())) + .Returns(Logger); + mockLoggerFactory + .Setup(factory => factory.CreateLogger(It.IsIn("Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv", + "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets"))) + .Returns(mockLogger.Object); + + var mockKestrelTrace = new Mock(Logger) { CallBase = true }; + var testContext = new TestServiceContext(mockLoggerFactory.Object) + { + Log = mockKestrelTrace.Object, + ServerOptions = + { + Limits = + { + MaxRequestBufferSize = maxRequestBufferSize, + MaxRequestLineSize = maxRequestBufferSize, + MaxRequestHeadersTotalSize = maxRequestBufferSize, + } + } + }; + + var scratchBuffer = new byte[maxRequestBufferSize * 8]; + + using (var server = new TestServer(async context => + { + await clientClosedConnection.Task; + + context.Abort(); + + await serverClosedConnection.Task; + + appFuncCompleted.SetResult(null); + }, testContext, listenOptions)) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + "Host:", + $"Content-Length: {scratchBuffer.Length}", + "", + ""); + + var ignore = connection.Stream.WriteAsync(scratchBuffer, 0, scratchBuffer.Length); + + // Wait until the read callback is no longer hooked up so that the connection disconnect isn't observed. + await readCallbackUnwired.Task.DefaultTimeout(); + } + + clientClosedConnection.SetResult(null); + + await appFuncCompleted.Task.DefaultTimeout(); + } + + mockKestrelTrace.Verify(t => t.ConnectionStop(It.IsAny()), Times.Once()); + } + + [Theory] + [MemberData(nameof(ConnectionAdapterData))] + public async Task AppCanHandleClientAbortingConnectionMidRequest(ListenOptions listenOptions) + { + var readTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var appStartedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var mockKestrelTrace = new Mock(Logger) { CallBase = true }; + var testContext = new TestServiceContext() + { + Log = mockKestrelTrace.Object, + }; + + var scratchBuffer = new byte[4096]; + + using (var server = new TestServer(async context => + { + appStartedTcs.SetResult(null); + + try + { + await context.Request.Body.CopyToAsync(Stream.Null);; + } + catch (Exception ex) + { + readTcs.SetException(ex); + throw; + } + + readTcs.SetException(new Exception("This shouldn't be reached.")); + + }, testContext, listenOptions)) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + "Host:", + $"Content-Length: {scratchBuffer.Length * 2}", + "", + ""); + + await appStartedTcs.Task.DefaultTimeout(); + + await connection.Stream.WriteAsync(scratchBuffer, 0, scratchBuffer.Length); + + connection.Reset(); + } + + await Assert.ThrowsAnyAsync(() => readTcs.Task).DefaultTimeout(); + } + + mockKestrelTrace.Verify(t => t.ConnectionStop(It.IsAny()), Times.Once()); + } + + private async Task TestRemoteIPAddress(string registerAddress, string requestAddress, string expectAddress) + { + var builder = TransportSelector.GetWebHostBuilder() + .UseKestrel() + .UseUrls($"http://{registerAddress}:0") + .ConfigureServices(AddTestLogging) + .Configure(app => + { + app.Run(async context => + { + var connection = context.Connection; + await context.Response.WriteAsync(JsonConvert.SerializeObject(new + { + RemoteIPAddress = connection.RemoteIpAddress?.ToString(), + RemotePort = connection.RemotePort, + LocalIPAddress = connection.LocalIpAddress?.ToString(), + LocalPort = connection.LocalPort + })); + }); + }); + + using (var host = builder.Build()) + using (var client = new HttpClient()) + { + host.Start(); + + var response = await client.GetAsync($"http://{requestAddress}:{host.GetPort()}/"); + response.EnsureSuccessStatusCode(); + + var connectionFacts = await response.Content.ReadAsStringAsync(); + Assert.NotEmpty(connectionFacts); + + var facts = JsonConvert.DeserializeObject(connectionFacts); + Assert.Equal(expectAddress, facts["RemoteIPAddress"].Value()); + Assert.NotEmpty(facts["RemotePort"].Value()); + } + } + } +} diff --git a/test/Kestrel.Transport.FunctionalTests/ResponseTests.cs b/test/Kestrel.Transport.FunctionalTests/ResponseTests.cs new file mode 100644 index 0000000000..88d54f6d22 --- /dev/null +++ b/test/Kestrel.Transport.FunctionalTests/ResponseTests.cs @@ -0,0 +1,945 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Security; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; +using Microsoft.AspNetCore.Server.Kestrel.Https; +using Microsoft.AspNetCore.Server.Kestrel.Https.Internal; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +{ + public class ResponseTests : TestApplicationErrorLoggerLoggedTest + { + public static TheoryData ConnectionAdapterData => new TheoryData + { + new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)), + new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) + { + ConnectionAdapters = { new PassThroughConnectionAdapter() } + } + }; + + [Fact] + public async Task LargeDownload() + { + var hostBuilder = TransportSelector.GetWebHostBuilder() + .UseKestrel() + .UseUrls("http://127.0.0.1:0/") + .ConfigureServices(AddTestLogging) + .Configure(app => + { + app.Run(async context => + { + var bytes = new byte[1024]; + for (int i = 0; i < bytes.Length; i++) + { + bytes[i] = (byte)i; + } + + context.Response.ContentLength = bytes.Length * 1024; + + for (int i = 0; i < 1024; i++) + { + await context.Response.Body.WriteAsync(bytes, 0, bytes.Length); + } + }); + }); + + using (var host = hostBuilder.Build()) + { + host.Start(); + + using (var client = new HttpClient()) + { + var response = await client.GetAsync($"http://127.0.0.1:{host.GetPort()}/"); + response.EnsureSuccessStatusCode(); + var responseBody = await response.Content.ReadAsStreamAsync(); + + // Read the full response body + var total = 0; + var bytes = new byte[1024]; + var count = await responseBody.ReadAsync(bytes, 0, bytes.Length); + while (count > 0) + { + for (int i = 0; i < count; i++) + { + Assert.Equal(total % 256, bytes[i]); + total++; + } + count = await responseBody.ReadAsync(bytes, 0, bytes.Length); + } + } + } + } + + [Theory, MemberData(nameof(NullHeaderData))] + public async Task IgnoreNullHeaderValues(string headerName, StringValues headerValue, string expectedValue) + { + var hostBuilder = TransportSelector.GetWebHostBuilder() + .UseKestrel() + .UseUrls("http://127.0.0.1:0/") + .ConfigureServices(AddTestLogging) + .Configure(app => + { + app.Run(async context => + { + context.Response.Headers.Add(headerName, headerValue); + + await context.Response.WriteAsync(""); + }); + }); + + using (var host = hostBuilder.Build()) + { + host.Start(); + + using (var client = new HttpClient()) + { + var response = await client.GetAsync($"http://127.0.0.1:{host.GetPort()}/"); + response.EnsureSuccessStatusCode(); + + var headers = response.Headers; + + if (expectedValue == null) + { + Assert.False(headers.Contains(headerName)); + } + else + { + Assert.True(headers.Contains(headerName)); + Assert.Equal(headers.GetValues(headerName).Single(), expectedValue); + } + } + } + } + + [Theory] + [MemberData(nameof(ConnectionAdapterData))] + public async Task WriteAfterConnectionCloseNoops(ListenOptions listenOptions) + { + var connectionClosed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var requestStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var appCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + using (var server = new TestServer(async httpContext => + { + try + { + requestStarted.SetResult(null); + await connectionClosed.Task.DefaultTimeout(); + httpContext.Response.ContentLength = 12; + await httpContext.Response.WriteAsync("hello, world"); + appCompleted.TrySetResult(null); + } + catch (Exception ex) + { + appCompleted.TrySetException(ex); + } + }, new TestServiceContext(LoggerFactory), listenOptions)) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "", + ""); + + await requestStarted.Task.DefaultTimeout(); + connection.ShutdownSend(); + await connection.WaitForConnectionClose(); + } + + connectionClosed.SetResult(null); + + await appCompleted.Task.DefaultTimeout(); + } + } + + [Theory] + [MemberData(nameof(ConnectionAdapterData))] + public async Task ThrowsOnWriteWithRequestAbortedTokenAfterRequestIsAborted(ListenOptions listenOptions) + { + // This should match _maxBytesPreCompleted in SocketOutput + var maxBytesPreCompleted = 65536; + + // Ensure string is long enough to disable write-behind buffering + var largeString = new string('a', maxBytesPreCompleted + 1); + + var writeTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var requestAbortedWh = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var requestStartWh = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + using (var server = new TestServer(async httpContext => + { + requestStartWh.SetResult(null); + + var response = httpContext.Response; + var request = httpContext.Request; + var lifetime = httpContext.Features.Get(); + + lifetime.RequestAborted.Register(() => requestAbortedWh.SetResult(null)); + await requestAbortedWh.Task.DefaultTimeout(); + + try + { + await response.WriteAsync(largeString, lifetime.RequestAborted); + } + catch (Exception ex) + { + writeTcs.SetException(ex); + throw; + } + + writeTcs.SetException(new Exception("This shouldn't be reached.")); + }, new TestServiceContext(LoggerFactory), listenOptions)) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + "Host:", + "Content-Length: 0", + "", + ""); + + await requestStartWh.Task.DefaultTimeout(); + } + + // Write failed - can throw TaskCanceledException or OperationCanceledException, + // depending on how far the canceled write goes. + await Assert.ThrowsAnyAsync(async () => await writeTcs.Task).DefaultTimeout(); + + // RequestAborted tripped + await requestAbortedWh.Task.DefaultTimeout(); + } + } + + [Theory] + [MemberData(nameof(ConnectionAdapterData))] + public async Task WritingToConnectionAfterUnobservedCloseTriggersRequestAbortedToken(ListenOptions listenOptions) + { + const int connectionPausedEventId = 4; + const int maxRequestBufferSize = 4096; + + var requestAborted = false; + var readCallbackUnwired = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var clientClosedConnection = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var writeTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var mockKestrelTrace = new Mock(Logger) { CallBase = true }; + var mockLogger = new Mock(); + mockLogger + .Setup(logger => logger.IsEnabled(It.IsAny())) + .Returns(true); + mockLogger + .Setup(logger => logger.Log(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())) + .Callback>((logLevel, eventId, state, exception, formatter) => + { + if (eventId.Id == connectionPausedEventId) + { + readCallbackUnwired.TrySetResult(null); + } + + Logger.Log(logLevel, eventId, state, exception, formatter); + }); + + var mockLoggerFactory = new Mock(); + mockLoggerFactory + .Setup(factory => factory.CreateLogger(It.IsAny())) + .Returns(Logger); + mockLoggerFactory + .Setup(factory => factory.CreateLogger(It.IsIn("Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv", + "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets"))) + .Returns(mockLogger.Object); + + var testContext = new TestServiceContext(mockLoggerFactory.Object) + { + Log = mockKestrelTrace.Object, + ServerOptions = + { + Limits = + { + MaxRequestBufferSize = maxRequestBufferSize, + MaxRequestLineSize = maxRequestBufferSize, + MaxRequestHeadersTotalSize = maxRequestBufferSize, + } + } + }; + + var scratchBuffer = new byte[maxRequestBufferSize * 8]; + + using (var server = new TestServer(async context => + { + context.RequestAborted.Register(() => + { + requestAborted = true; + }); + + await clientClosedConnection.Task; + + try + { + for (var i = 0; i < 1000; i++) + { + await context.Response.Body.WriteAsync(scratchBuffer, 0, scratchBuffer.Length, context.RequestAborted); + await Task.Delay(10); + } + } + catch (Exception ex) + { + writeTcs.SetException(ex); + throw; + } + + writeTcs.SetException(new Exception("This shouldn't be reached.")); + }, testContext, listenOptions)) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "POST / HTTP/1.1", + "Host:", + $"Content-Length: {scratchBuffer.Length}", + "", + ""); + + var ignore = connection.Stream.WriteAsync(scratchBuffer, 0, scratchBuffer.Length); + + // Wait until the read callback is no longer hooked up so that the connection disconnect isn't observed. + await readCallbackUnwired.Task.DefaultTimeout(); + } + + clientClosedConnection.SetResult(null); + + await Assert.ThrowsAnyAsync(() => writeTcs.Task).DefaultTimeout(); + } + + mockKestrelTrace.Verify(t => t.ConnectionStop(It.IsAny()), Times.Once()); + Assert.True(requestAborted); + } + + [Theory] + [MemberData(nameof(ConnectionAdapterData))] + public async Task AppCanHandleClientAbortingConnectionMidResponse(ListenOptions listenOptions) + { + const int connectionResetEventId = 19; + const int connectionFinEventId = 6; + //const int connectionStopEventId = 2; + + const int responseBodySegmentSize = 65536; + const int responseBodySegmentCount = 100; + + var appCompletedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var requestAborted = false; + + var scratchBuffer = new byte[responseBodySegmentSize]; + + using (var server = new TestServer(async context => + { + context.RequestAborted.Register(() => + { + requestAborted = true; + }); + + for (var i = 0; i < responseBodySegmentCount; i++) + { + await context.Response.Body.WriteAsync(scratchBuffer, 0, scratchBuffer.Length); + await Task.Delay(10); + } + + appCompletedTcs.SetResult(null); + }, new TestServiceContext(LoggerFactory), listenOptions)) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "", + ""); + + // Read just part of the response and close the connection. + // https://github.com/aspnet/KestrelHttpServer/issues/2554 + await connection.Stream.ReadAsync(scratchBuffer, 0, scratchBuffer.Length); + + connection.Reset(); + } + + await appCompletedTcs.Task.DefaultTimeout(); + + // After the app is done with the write loop, the connection reset should be logged. + // On Linux and macOS, the connection close is still sometimes observed as a FIN despite the LingerState. + var presShutdownTransportLogs = TestSink.Writes.Where( + w => w.LoggerName == "Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv" || + w.LoggerName == "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets"); + var connectionResetLogs = presShutdownTransportLogs.Where( + w => w.EventId == connectionResetEventId || + (!TestPlatformHelper.IsWindows && w.EventId == connectionFinEventId)); + + Assert.NotEmpty(connectionResetLogs); + } + + // TODO: Figure out what the following assertion is flaky. The server shouldn't shutdown before all + // the connections are closed, yet sometimes the connection stop log isn't observed here. + //var coreLogs = TestSink.Writes.Where(w => w.LoggerName == "Microsoft.AspNetCore.Server.Kestrel"); + //Assert.Single(coreLogs.Where(w => w.EventId == connectionStopEventId)); + + Assert.True(requestAborted, "RequestAborted token didn't fire."); + + var transportLogs = TestSink.Writes.Where(w => w.LoggerName == "Microsoft.AspNetCore.Server.Kestrel" || + w.LoggerName == "Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv" || + w.LoggerName == "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets"); + + Assert.Empty(transportLogs.Where(w => w.LogLevel > LogLevel.Debug)); + } + + [Theory] + [MemberData(nameof(ConnectionAdapterData))] + public async Task ClientAbortingConnectionImmediatelyIsNotLoggedHigherThanDebug(ListenOptions listenOptions) + { + // Attempt multiple connections to be extra sure the resets are consistently logged appropriately. + const int numConnections = 10; + + // There's not guarantee that the app even gets invoked in this test. The connection reset can be observed + // as early as accept. + using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory), listenOptions)) + { + for (var i = 0; i < numConnections; i++) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "", + ""); + + connection.Reset(); + } + } + } + + var transportLogs = TestSink.Writes.Where(w => w.LoggerName == "Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv" || + w.LoggerName == "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets"); + + // The "Microsoft.AspNetCore.Server.Kestrel" logger may contain info level logs because resetting the connection can cause + // partial headers to be read leading to a bad request. + var coreLogs = TestSink.Writes.Where(w => w.LoggerName == "Microsoft.AspNetCore.Server.Kestrel"); + + Assert.Empty(transportLogs.Where(w => w.LogLevel > LogLevel.Debug)); + Assert.Empty(coreLogs.Where(w => w.LogLevel > LogLevel.Information)); + } + + [Fact] + public async Task ConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate() + { + using (StartLog(out var loggerFactory, "ConnClosedWhenRespDoesNotSatisfyMin")) + { + var logger = loggerFactory.CreateLogger($"{ typeof(ResponseTests).FullName}.{ nameof(ConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate)}"); + const int chunkSize = 1024; + const int chunks = 256 * 1024; + var responseSize = chunks * chunkSize; + var chunkData = new byte[chunkSize]; + + var responseRateTimeoutMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var connectionStopMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var requestAborted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var appFuncCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var mockKestrelTrace = new Mock(Logger) { CallBase = true }; + mockKestrelTrace + .Setup(trace => trace.ResponseMininumDataRateNotSatisfied(It.IsAny(), It.IsAny())) + .Callback(() => responseRateTimeoutMessageLogged.SetResult(null)); + mockKestrelTrace + .Setup(trace => trace.ConnectionStop(It.IsAny())) + .Callback(() => connectionStopMessageLogged.SetResult(null)); + + var testContext = new TestServiceContext + { + LoggerFactory = loggerFactory, + Log = mockKestrelTrace.Object, + ServerOptions = + { + Limits = + { + MinResponseDataRate = new MinDataRate(bytesPerSecond: 1024 * 1024, gracePeriod: TimeSpan.FromSeconds(2)) + } + } + }; + + testContext.InitializeHeartbeat(); + + var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); + listenOptions.ConnectionAdapters.Add(new LoggingConnectionAdapter(loggerFactory.CreateLogger())); + + var appLogger = loggerFactory.CreateLogger("App"); + async Task App(HttpContext context) + { + appLogger.LogInformation("Request received"); + context.RequestAborted.Register(() => requestAborted.SetResult(null)); + + context.Response.ContentLength = responseSize; + + try + { + for (var i = 0; i < chunks; i++) + { + await context.Response.Body.WriteAsync(chunkData, 0, chunkData.Length, context.RequestAborted); + appLogger.LogInformation("Wrote chunk of {chunkSize} bytes", chunkSize); + } + } + catch (OperationCanceledException) + { + appFuncCompleted.SetResult(null); + throw; + } + } + + using (var server = new TestServer(App, testContext, listenOptions)) + { + using (var connection = server.CreateConnection()) + { + logger.LogInformation("Sending request"); + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "", + ""); + + logger.LogInformation("Sent request"); + + var sw = Stopwatch.StartNew(); + logger.LogInformation("Waiting for connection to abort."); + + await requestAborted.Task.DefaultTimeout(); + await responseRateTimeoutMessageLogged.Task.DefaultTimeout(); + await connectionStopMessageLogged.Task.DefaultTimeout(); + await appFuncCompleted.Task.DefaultTimeout(); + await AssertStreamAborted(connection.Reader.BaseStream, chunkSize * chunks); + + sw.Stop(); + logger.LogInformation("Connection was aborted after {totalMilliseconds}ms.", sw.ElapsedMilliseconds); + } + } + } + } + + [Fact] + public async Task HttpsConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate() + { + const int chunkSize = 1024; + const int chunks = 256 * 1024; + var chunkData = new byte[chunkSize]; + + var certificate = new X509Certificate2(TestResources.TestCertificatePath, "testPassword"); + + var responseRateTimeoutMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var connectionStopMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var aborted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var appFuncCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var mockKestrelTrace = new Mock(Logger) { CallBase = true }; + mockKestrelTrace + .Setup(trace => trace.ResponseMininumDataRateNotSatisfied(It.IsAny(), It.IsAny())) + .Callback(() => responseRateTimeoutMessageLogged.SetResult(null)); + mockKestrelTrace + .Setup(trace => trace.ConnectionStop(It.IsAny())) + .Callback(() => connectionStopMessageLogged.SetResult(null)); + + var testContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object) + { + ServerOptions = + { + Limits = + { + MinResponseDataRate = new MinDataRate(bytesPerSecond: 1024 * 1024, gracePeriod: TimeSpan.FromSeconds(2)) + } + } + }; + + testContext.InitializeHeartbeat(); + + var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) + { + ConnectionAdapters = + { + new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions { ServerCertificate = certificate }) + } + }; + + using (var server = new TestServer(async context => + { + context.RequestAborted.Register(() => + { + aborted.SetResult(null); + }); + + context.Response.ContentLength = chunks * chunkSize; + + try + { + for (var i = 0; i < chunks; i++) + { + await context.Response.Body.WriteAsync(chunkData, 0, chunkData.Length, context.RequestAborted); + } + } + catch (OperationCanceledException) + { + appFuncCompleted.SetResult(null); + throw; + } + }, testContext, listenOptions)) + { + using (var connection = server.CreateConnection()) + { + using (var sslStream = new SslStream(connection.Reader.BaseStream, false, (sender, cert, chain, errors) => true, null)) + { + await sslStream.AuthenticateAsClientAsync("localhost", new X509CertificateCollection(), SslProtocols.Tls12 | SslProtocols.Tls11, false); + + var request = Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\nHost:\r\n\r\n"); + await sslStream.WriteAsync(request, 0, request.Length); + + await aborted.Task.DefaultTimeout(); + await responseRateTimeoutMessageLogged.Task.DefaultTimeout(); + await connectionStopMessageLogged.Task.DefaultTimeout(); + await appFuncCompleted.Task.DefaultTimeout(); + + await AssertStreamAborted(connection.Reader.BaseStream, chunkSize * chunks); + } + } + } + } + + [Fact] + public async Task ConnectionClosedWhenBothRequestAndResponseExperienceBackPressure() + { + const int bufferSize = 65536; + const int bufferCount = 100; + var responseSize = bufferCount * bufferSize; + var buffer = new byte[bufferSize]; + + var responseRateTimeoutMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var connectionStopMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var requestAborted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var copyToAsyncCts = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var mockKestrelTrace = new Mock(Logger) { CallBase = true }; + mockKestrelTrace + .Setup(trace => trace.ResponseMininumDataRateNotSatisfied(It.IsAny(), It.IsAny())) + .Callback(() => responseRateTimeoutMessageLogged.SetResult(null)); + mockKestrelTrace + .Setup(trace => trace.ConnectionStop(It.IsAny())) + .Callback(() => connectionStopMessageLogged.SetResult(null)); + + var testContext = new TestServiceContext + { + LoggerFactory = LoggerFactory, + Log = mockKestrelTrace.Object, + ServerOptions = + { + Limits = + { + MinResponseDataRate = new MinDataRate(bytesPerSecond: 1024 * 1024, gracePeriod: TimeSpan.FromSeconds(2)), + MaxRequestBodySize = responseSize + } + } + }; + + testContext.InitializeHeartbeat(); + + var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); + + async Task App(HttpContext context) + { + context.RequestAborted.Register(() => + { + requestAborted.SetResult(null); + }); + + try + { + await context.Request.Body.CopyToAsync(context.Response.Body); + } + catch (Exception ex) + { + copyToAsyncCts.SetException(ex); + throw; + } + + copyToAsyncCts.SetException(new Exception("This shouldn't be reached.")); + } + + using (var server = new TestServer(App, testContext, listenOptions)) + { + using (var connection = server.CreateConnection()) + { + // Close the connection with the last request so AssertStreamCompleted actually completes. + await connection.Send( + "POST / HTTP/1.1", + "Host:", + $"Content-Length: {responseSize}", + "", + ""); + + var sendTask = Task.Run(async () => + { + for (var i = 0; i < bufferCount; i++) + { + await connection.Stream.WriteAsync(buffer, 0, buffer.Length); + await Task.Delay(10); + } + }); + + await requestAborted.Task.DefaultTimeout(); + await responseRateTimeoutMessageLogged.Task.DefaultTimeout(); + await connectionStopMessageLogged.Task.DefaultTimeout(); + + // Expect OperationCanceledException instead of IOException because the server initiated the abort due to a response rate timeout. + await Assert.ThrowsAnyAsync(() => copyToAsyncCts.Task).DefaultTimeout(); + await AssertStreamAborted(connection.Stream, responseSize); + } + } + } + + [Fact] + public async Task ConnectionNotClosedWhenClientSatisfiesMinimumDataRateGivenLargeResponseChunks() + { + var chunkSize = 64 * 128 * 1024; + var chunkCount = 4; + var chunkData = new byte[chunkSize]; + + var requestAborted = false; + var appFuncCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var mockKestrelTrace = new Mock(Logger) { CallBase = true }; + + var testContext = new TestServiceContext + { + Log = mockKestrelTrace.Object, + ServerOptions = + { + Limits = + { + MinResponseDataRate = new MinDataRate(bytesPerSecond: 240, gracePeriod: TimeSpan.FromSeconds(2)) + } + } + }; + + testContext.InitializeHeartbeat(); + + var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); + + async Task App(HttpContext context) + { + context.RequestAborted.Register(() => + { + requestAborted = true; + }); + + for (var i = 0; i < chunkCount; i++) + { + await context.Response.Body.WriteAsync(chunkData, 0, chunkData.Length, context.RequestAborted); + } + + appFuncCompleted.SetResult(null); + } + + using (var server = new TestServer(App, testContext, listenOptions)) + { + using (var connection = server.CreateConnection()) + { + // Close the connection with the last request so AssertStreamCompleted actually completes. + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "Connection: close", + "", + ""); + + var minTotalOutputSize = chunkCount * chunkSize; + + // Make sure consuming a single chunk exceeds the 2 second timeout. + var targetBytesPerSecond = chunkSize / 4; + await AssertStreamCompleted(connection.Reader.BaseStream, minTotalOutputSize, targetBytesPerSecond); + await appFuncCompleted.Task.DefaultTimeout(); + + mockKestrelTrace.Verify(t => t.ResponseMininumDataRateNotSatisfied(It.IsAny(), It.IsAny()), Times.Never()); + mockKestrelTrace.Verify(t => t.ConnectionStop(It.IsAny()), Times.Once()); + Assert.False(requestAborted); + } + } + } + + [Fact] + public async Task ConnectionNotClosedWhenClientSatisfiesMinimumDataRateGivenLargeResponseHeaders() + { + var headerSize = 1024 * 1024; // 1 MB for each header value + var headerCount = 64; // 64 MB of headers per response + var requestCount = 4; // Minimum of 256 MB of total response headers + var headerValue = new string('a', headerSize); + var headerStringValues = new StringValues(Enumerable.Repeat(headerValue, headerCount).ToArray()); + + var requestAborted = false; + var mockKestrelTrace = new Mock(Logger) { CallBase = true }; + + var testContext = new TestServiceContext + { + Log = mockKestrelTrace.Object, + ServerOptions = + { + Limits = + { + MinResponseDataRate = new MinDataRate(bytesPerSecond: 240, gracePeriod: TimeSpan.FromSeconds(2)) + } + } + }; + + testContext.InitializeHeartbeat(); + + var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); + + async Task App(HttpContext context) + { + context.RequestAborted.Register(() => + { + requestAborted = true; + }); + + context.Response.Headers[$"X-Custom-Header"] = headerStringValues; + context.Response.ContentLength = 0; + + await context.Response.Body.FlushAsync(); + } + + using (var server = new TestServer(App, testContext, listenOptions)) + { + using (var connection = server.CreateConnection()) + { + for (var i = 0; i < requestCount - 1; i++) + { + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "", + ""); + } + + // Close the connection with the last request so AssertStreamCompleted actually completes. + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "Connection: close", + "", + ""); + + var responseSize = headerSize * headerCount; + var minTotalOutputSize = requestCount * responseSize; + + // Make sure consuming a single set of response headers exceeds the 2 second timeout. + var targetBytesPerSecond = responseSize / 4; + await AssertStreamCompleted(connection.Reader.BaseStream, minTotalOutputSize, targetBytesPerSecond); + + mockKestrelTrace.Verify(t => t.ResponseMininumDataRateNotSatisfied(It.IsAny(), It.IsAny()), Times.Never()); + mockKestrelTrace.Verify(t => t.ConnectionStop(It.IsAny()), Times.Once()); + Assert.False(requestAborted); + } + } + } + + private async Task AssertStreamAborted(Stream stream, int totalBytes) + { + var receiveBuffer = new byte[64 * 1024]; + var totalReceived = 0; + + try + { + while (totalReceived < totalBytes) + { + var bytes = await stream.ReadAsync(receiveBuffer, 0, receiveBuffer.Length).DefaultTimeout(); + + if (bytes == 0) + { + break; + } + + totalReceived += bytes; + } + } + catch (IOException) + { + // This is expected given an abort. + } + + Assert.True(totalReceived < totalBytes, $"{nameof(AssertStreamAborted)} Stream completed successfully."); + } + + private async Task AssertStreamCompleted(Stream stream, long minimumBytes, int targetBytesPerSecond) + { + var receiveBuffer = new byte[64 * 1024]; + var received = 0; + var totalReceived = 0; + var startTime = DateTimeOffset.UtcNow; + + do + { + received = await stream.ReadAsync(receiveBuffer, 0, receiveBuffer.Length); + totalReceived += received; + + var expectedTimeElapsed = TimeSpan.FromSeconds(totalReceived / targetBytesPerSecond); + var timeElapsed = DateTimeOffset.UtcNow - startTime; + if (timeElapsed < expectedTimeElapsed) + { + await Task.Delay(expectedTimeElapsed - timeElapsed); + } + } while (received > 0); + + Assert.True(totalReceived >= minimumBytes, $"{nameof(AssertStreamCompleted)} Stream aborted prematurely."); + } + + public static TheoryData NullHeaderData + { + get + { + var dataset = new TheoryData(); + + // Unknown headers + dataset.Add("NullString", (string)null, null); + dataset.Add("EmptyString", "", ""); + dataset.Add("NullStringArray", new string[] { null }, null); + dataset.Add("EmptyStringArray", new string[] { "" }, ""); + dataset.Add("MixedStringArray", new string[] { null, "" }, ""); + // Known headers + dataset.Add("Location", (string)null, null); + dataset.Add("Location", "", ""); + dataset.Add("Location", new string[] { null }, null); + dataset.Add("Location", new string[] { "" }, ""); + dataset.Add("Location", new string[] { null, "" }, ""); + + return dataset; + } + } + } +} diff --git a/test/Kestrel.Transport.Libuv.BindTests/Kestrel.Transport.Libuv.BindTests.csproj b/test/Kestrel.Transport.Libuv.BindTests/Kestrel.Transport.Libuv.BindTests.csproj new file mode 100644 index 0000000000..edf6bd9a96 --- /dev/null +++ b/test/Kestrel.Transport.Libuv.BindTests/Kestrel.Transport.Libuv.BindTests.csproj @@ -0,0 +1,30 @@ + + + + Libuv.BindTests + Libuv.BindTests + $(StandardTestTfms) + true + Libuv.BindTests + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/Kestrel.Transport.Libuv.FunctionalTests/Kestrel.Transport.Libuv.FunctionalTests.csproj b/test/Kestrel.Transport.Libuv.FunctionalTests/Kestrel.Transport.Libuv.FunctionalTests.csproj index 8a25878b94..b2e93c9777 100644 --- a/test/Kestrel.Transport.Libuv.FunctionalTests/Kestrel.Transport.Libuv.FunctionalTests.csproj +++ b/test/Kestrel.Transport.Libuv.FunctionalTests/Kestrel.Transport.Libuv.FunctionalTests.csproj @@ -11,9 +11,11 @@ - - - + + + + + diff --git a/test/Kestrel.Transport.Libuv.Tests/Kestrel.Transport.Libuv.Tests.csproj b/test/Kestrel.Transport.Libuv.Tests/Kestrel.Transport.Libuv.Tests.csproj index ef4f1d7742..0ad5d2346d 100644 --- a/test/Kestrel.Transport.Libuv.Tests/Kestrel.Transport.Libuv.Tests.csproj +++ b/test/Kestrel.Transport.Libuv.Tests/Kestrel.Transport.Libuv.Tests.csproj @@ -9,7 +9,7 @@ - + diff --git a/test/Kestrel.Transport.Libuv.Tests/LibuvTransportTests.cs b/test/Kestrel.Transport.Libuv.Tests/LibuvTransportTests.cs index a93967b37d..3d51c3c482 100644 --- a/test/Kestrel.Transport.Libuv.Tests/LibuvTransportTests.cs +++ b/test/Kestrel.Transport.Libuv.Tests/LibuvTransportTests.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -47,12 +46,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests await transport.StopAsync(); } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task TransportCanBindUnbindAndStop(ListenOptions listenOptions) + [Fact] + public async Task TransportCanBindUnbindAndStop() { var transportContext = new TestLibuvTransportContext(); - var transport = new LibuvTransport(transportContext, listenOptions); + var transport = new LibuvTransport(transportContext, new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))); await transport.BindAsync(); await transport.UnbindAsync(); diff --git a/test/Kestrel.Transport.Sockets.BindTests/Kestrel.Transport.Sockets.BindTests.csproj b/test/Kestrel.Transport.Sockets.BindTests/Kestrel.Transport.Sockets.BindTests.csproj new file mode 100644 index 0000000000..4aa0791ed7 --- /dev/null +++ b/test/Kestrel.Transport.Sockets.BindTests/Kestrel.Transport.Sockets.BindTests.csproj @@ -0,0 +1,30 @@ + + + + Sockets.BindTests + Sockets.BindTests + $(StandardTestTfms) + true + Sockets.BindTests + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/Kestrel.Transport.Sockets.FunctionalTests/Kestrel.Transport.Sockets.FunctionalTests.csproj b/test/Kestrel.Transport.Sockets.FunctionalTests/Kestrel.Transport.Sockets.FunctionalTests.csproj index b57b0c3f93..e717642f93 100644 --- a/test/Kestrel.Transport.Sockets.FunctionalTests/Kestrel.Transport.Sockets.FunctionalTests.csproj +++ b/test/Kestrel.Transport.Sockets.FunctionalTests/Kestrel.Transport.Sockets.FunctionalTests.csproj @@ -5,14 +5,16 @@ Sockets.FunctionalTests $(StandardTestTfms) $(DefineConstants);MACOS - $(DefineConstants);SOCKETS true + Sockets.FunctionalTests - - - + + + + + diff --git a/test/Kestrel.FunctionalTests/DiagnosticMemoryPoolFactory.cs b/test/shared/FunctionalTestHelpers/DiagnosticMemoryPoolFactory.cs similarity index 100% rename from test/Kestrel.FunctionalTests/DiagnosticMemoryPoolFactory.cs rename to test/shared/FunctionalTestHelpers/DiagnosticMemoryPoolFactory.cs diff --git a/test/Kestrel.FunctionalTests/TestHelpers/TestApplicationErrorLoggerLoggedTest.cs b/test/shared/FunctionalTestHelpers/TestApplicationErrorLoggerLoggedTest.cs similarity index 100% rename from test/Kestrel.FunctionalTests/TestHelpers/TestApplicationErrorLoggerLoggedTest.cs rename to test/shared/FunctionalTestHelpers/TestApplicationErrorLoggerLoggedTest.cs diff --git a/test/shared/StreamBackedTestConnection.cs b/test/shared/StreamBackedTestConnection.cs new file mode 100644 index 0000000000..59cae5dd6c --- /dev/null +++ b/test/shared/StreamBackedTestConnection.cs @@ -0,0 +1,203 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Testing +{ + /// + /// Summary description for TestConnection + /// + public abstract class StreamBackedTestConnection : IDisposable + { + private static readonly TimeSpan Timeout = TimeSpan.FromMinutes(1); + + private readonly Stream _stream; + private readonly StreamReader _reader; + + protected StreamBackedTestConnection(Stream stream) + { + _stream = stream; + _reader = new StreamReader(_stream, Encoding.ASCII); + } + + public Stream Stream => _stream; + + public StreamReader Reader => _reader; + + public abstract void ShutdownSend(); + + public abstract void Reset(); + + public virtual void Dispose() + { + _stream.Dispose(); + } + + public Task SendEmptyGet() + { + return Send("GET / HTTP/1.1", + "Host:", + "", + ""); + } + + public Task SendEmptyGetWithUpgradeAndKeepAlive() + => SendEmptyGetWithConnection("Upgrade, keep-alive"); + + public Task SendEmptyGetWithUpgrade() + => SendEmptyGetWithConnection("Upgrade"); + + public Task SendEmptyGetAsKeepAlive() + => SendEmptyGetWithConnection("keep-alive"); + + private Task SendEmptyGetWithConnection(string connection) + { + return Send("GET / HTTP/1.1", + "Host:", + "Connection: " + connection, + "", + ""); + } + + public async Task SendAll(params string[] lines) + { + var text = string.Join("\r\n", lines); + var writer = new StreamWriter(_stream, Encoding.GetEncoding("iso-8859-1")); + await writer.WriteAsync(text).ConfigureAwait(false); + await writer.FlushAsync().ConfigureAwait(false); + await _stream.FlushAsync().ConfigureAwait(false); + } + + public async Task Send(params string[] lines) + { + var text = string.Join("\r\n", lines); + var writer = new StreamWriter(_stream, Encoding.GetEncoding("iso-8859-1")); + for (var index = 0; index < text.Length; index++) + { + var ch = text[index]; + writer.Write(ch); + await writer.FlushAsync().ConfigureAwait(false); + // Re-add delay to help find socket input consumption bugs more consistently + //await Task.Delay(TimeSpan.FromMilliseconds(5)); + } + await writer.FlushAsync().ConfigureAwait(false); + await _stream.FlushAsync().ConfigureAwait(false); + } + + public async Task Receive(params string[] lines) + { + var expected = string.Join("\r\n", lines); + var actual = new char[expected.Length]; + var offset = 0; + + try + { + while (offset < expected.Length) + { + var data = new byte[expected.Length]; + var task = _reader.ReadAsync(actual, offset, actual.Length - offset); + if (!Debugger.IsAttached) + { + task = task.TimeoutAfter(Timeout); + } + var count = await task.ConfigureAwait(false); + if (count == 0) + { + break; + } + offset += count; + } + } + catch (TimeoutException ex) when (offset != 0) + { + throw new TimeoutException($"Did not receive a complete response within {Timeout}.{Environment.NewLine}{Environment.NewLine}" + + $"Expected:{Environment.NewLine}{expected}{Environment.NewLine}{Environment.NewLine}" + + $"Actual:{Environment.NewLine}{new string(actual, 0, offset)}{Environment.NewLine}", + ex); + } + + Assert.Equal(expected, new string(actual, 0, offset)); + } + + public async Task ReceiveEnd(params string[] lines) + { + await Receive(lines).ConfigureAwait(false); + ShutdownSend(); + var ch = new char[128]; + var count = await _reader.ReadAsync(ch, 0, 128).TimeoutAfter(Timeout).ConfigureAwait(false); + var text = new string(ch, 0, count); + Assert.Equal("", text); + } + + public async Task ReceiveForcedEnd(params string[] lines) + { + await Receive(lines).ConfigureAwait(false); + + try + { + var ch = new char[128]; + var count = await _reader.ReadAsync(ch, 0, 128).TimeoutAfter(Timeout).ConfigureAwait(false); + var text = new string(ch, 0, count); + Assert.Equal("", text); + } + catch (IOException) + { + // The server is forcefully closing the connection so an IOException: + // "Unable to read data from the transport connection: An existing connection was forcibly closed by the remote host." + // isn't guaranteed but not unexpected. + } + } + + public async Task ReceiveStartsWith(string prefix, int maxLineLength = 1024) + { + var actual = new char[maxLineLength]; + var offset = 0; + + while (offset < maxLineLength) + { + // Read one char at a time so we don't read past the end of the line. + var task = _reader.ReadAsync(actual, offset, 1); + if (!Debugger.IsAttached) + { + task = task.TimeoutAfter(Timeout); + } + var count = await task.ConfigureAwait(false); + if (count == 0) + { + break; + } + + Assert.True(count == 1); + offset++; + + if (actual[offset - 1] == '\n') + { + break; + } + } + + var actualLine = new string(actual, 0, offset); + Assert.StartsWith(prefix, actualLine); + } + + public async Task WaitForConnectionClose() + { + var buffer = new byte[128]; + var bytesTransferred = await _stream.ReadAsync(buffer, 0, 128).TimeoutAfter(Timeout); + + if (bytesTransferred > 0) + { + throw new IOException( + $"Expected connection close, received data instead: \"{_reader.CurrentEncoding.GetString(buffer, 0, bytesTransferred)}\""); + } + } + } +} diff --git a/test/shared/TestConnection.cs b/test/shared/TestConnection.cs index 2d760a5bc8..595b8292cb 100644 --- a/test/shared/TestConnection.cs +++ b/test/shared/TestConnection.cs @@ -2,27 +2,17 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Diagnostics; -using System.IO; using System.Net; using System.Net.Sockets; -using System.Text; -using System.Threading.Tasks; -using Xunit; namespace Microsoft.AspNetCore.Testing { /// /// Summary description for TestConnection /// - public class TestConnection : IDisposable + public class TestConnection : StreamBackedTestConnection { - private static readonly TimeSpan Timeout = TimeSpan.FromMinutes(1); - - private readonly bool _ownsSocket; private readonly Socket _socket; - private readonly NetworkStream _stream; - private readonly StreamReader _reader; public TestConnection(int port) : this(port, AddressFamily.InterNetwork) @@ -40,217 +30,29 @@ namespace Microsoft.AspNetCore.Testing } private TestConnection(Socket socket, bool ownsSocket) + : base(new NetworkStream(socket, ownsSocket: ownsSocket)) { - _ownsSocket = ownsSocket; _socket = socket; - _stream = new NetworkStream(_socket, ownsSocket: false); - _reader = new StreamReader(_stream, Encoding.ASCII); } public Socket Socket => _socket; - public Stream Stream => _stream; - - public StreamReader Reader => _reader; - - public void Dispose() - { - _stream.Dispose(); - - if (_ownsSocket) - { - _socket.Dispose(); - } - } - - public Task SendEmptyGet() - { - return Send("GET / HTTP/1.1", - "Host:", - "", - ""); - } - - public Task SendEmptyGetWithUpgradeAndKeepAlive() - => SendEmptyGetWithConnection("Upgrade, keep-alive"); - - public Task SendEmptyGetWithUpgrade() - => SendEmptyGetWithConnection("Upgrade"); - - public Task SendEmptyGetAsKeepAlive() - => SendEmptyGetWithConnection("keep-alive"); - - private Task SendEmptyGetWithConnection(string connection) - { - return Send("GET / HTTP/1.1", - "Host:", - "Connection: " + connection, - "", - ""); - } - - public async Task SendAll(params string[] lines) - { - var text = string.Join("\r\n", lines); - var writer = new StreamWriter(_stream, Encoding.GetEncoding("iso-8859-1")); - await writer.WriteAsync(text).ConfigureAwait(false); - await writer.FlushAsync().ConfigureAwait(false); - await _stream.FlushAsync().ConfigureAwait(false); - } - - public async Task Send(params string[] lines) - { - var text = string.Join("\r\n", lines); - var writer = new StreamWriter(_stream, Encoding.GetEncoding("iso-8859-1")); - for (var index = 0; index < text.Length; index++) - { - var ch = text[index]; - writer.Write(ch); - await writer.FlushAsync().ConfigureAwait(false); - // Re-add delay to help find socket input consumption bugs more consistently - //await Task.Delay(TimeSpan.FromMilliseconds(5)); - } - await writer.FlushAsync().ConfigureAwait(false); - await _stream.FlushAsync().ConfigureAwait(false); - } - - public async Task Receive(params string[] lines) - { - var expected = string.Join("\r\n", lines); - var actual = new char[expected.Length]; - var offset = 0; - - try - { - while (offset < expected.Length) - { - var data = new byte[expected.Length]; - var task = _reader.ReadAsync(actual, offset, actual.Length - offset); - if (!Debugger.IsAttached) - { - task = task.TimeoutAfter(Timeout); - } - var count = await task.ConfigureAwait(false); - if (count == 0) - { - break; - } - offset += count; - } - } - catch (TimeoutException ex) when (offset != 0) - { - throw new TimeoutException($"Did not receive a complete response within {Timeout}.{Environment.NewLine}{Environment.NewLine}" + - $"Expected:{Environment.NewLine}{expected}{Environment.NewLine}{Environment.NewLine}" + - $"Actual:{Environment.NewLine}{new string(actual, 0, offset)}{Environment.NewLine}", - ex); - } - - Assert.Equal(expected, new string(actual, 0, offset)); - } - - public async Task ReceiveEnd(params string[] lines) - { - await Receive(lines).ConfigureAwait(false); - _socket.Shutdown(SocketShutdown.Send); - var ch = new char[128]; - var count = await _reader.ReadAsync(ch, 0, 128).TimeoutAfter(Timeout).ConfigureAwait(false); - var text = new string(ch, 0, count); - Assert.Equal("", text); - } - - public async Task ReceiveForcedEnd(params string[] lines) - { - await Receive(lines).ConfigureAwait(false); - - try - { - var ch = new char[128]; - var count = await _reader.ReadAsync(ch, 0, 128).TimeoutAfter(Timeout).ConfigureAwait(false); - var text = new string(ch, 0, count); - Assert.Equal("", text); - } - catch (IOException) - { - // The server is forcefully closing the connection so an IOException: - // "Unable to read data from the transport connection: An existing connection was forcibly closed by the remote host." - // isn't guaranteed but not unexpected. - } - } - - public async Task ReceiveStartsWith(string prefix, int maxLineLength = 1024) - { - var actual = new char[maxLineLength]; - var offset = 0; - - while (offset < maxLineLength) - { - // Read one char at a time so we don't read past the end of the line. - var task = _reader.ReadAsync(actual, offset, 1); - if (!Debugger.IsAttached) - { - Assert.True(task.Wait(4000), "timeout"); - } - var count = await task.ConfigureAwait(false); - if (count == 0) - { - break; - } - - Assert.True(count == 1); - offset++; - - if (actual[offset - 1] == '\n') - { - break; - } - } - - var actualLine = new string(actual, 0, offset); - Assert.StartsWith(prefix, actualLine); - } - public void Shutdown(SocketShutdown how) { _socket.Shutdown(how); } - public void Reset() + public override void ShutdownSend() + { + Shutdown(SocketShutdown.Send); + } + + public override void Reset() { _socket.LingerState = new LingerOption(true, 0); _socket.Dispose(); } - public Task WaitForConnectionClose() - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var eventArgs = new SocketAsyncEventArgs(); - eventArgs.SetBuffer(new byte[128], 0, 128); - eventArgs.Completed += ReceiveAsyncCompleted; - eventArgs.UserToken = tcs; - - if (!_socket.ReceiveAsync(eventArgs)) - { - ReceiveAsyncCompleted(this, eventArgs); - } - - return tcs.Task; - } - - private void ReceiveAsyncCompleted(object sender, SocketAsyncEventArgs e) - { - var tcs = (TaskCompletionSource)e.UserToken; - if (e.BytesTransferred == 0) - { - tcs.SetResult(null); - } - else - { - tcs.SetException(new IOException( - $"Expected connection close, received data instead: \"{_reader.CurrentEncoding.GetString(e.Buffer, 0, e.BytesTransferred)}\"")); - } - } - public static Socket CreateConnectedLoopbackSocket(int port) => CreateConnectedLoopbackSocket(port, AddressFamily.InterNetwork); public static Socket CreateConnectedLoopbackSocket(int port, AddressFamily addressFamily) diff --git a/test/shared/TestResources.cs b/test/shared/TestResources.cs index 3218a1eaca..626922afc1 100644 --- a/test/shared/TestResources.cs +++ b/test/shared/TestResources.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Testing { public static class TestResources { - private static readonly string _baseDir = Directory.GetCurrentDirectory(); + private static readonly string _baseDir = Path.Combine(Directory.GetCurrentDirectory(), "shared", "TestCertificates"); public static string TestCertificatePath { get; } = Path.Combine(_baseDir, "testCert.pfx"); public static string GetCertPath(string name) => Path.Combine(_baseDir, name); diff --git a/test/shared/TestServiceContext.cs b/test/shared/TestServiceContext.cs index 1c5e853d08..f8b68dd906 100644 --- a/test/shared/TestServiceContext.cs +++ b/test/shared/TestServiceContext.cs @@ -1,11 +1,14 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; +using System.Buffers; using System.IO.Pipelines; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; +using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Testing @@ -36,13 +39,28 @@ namespace Microsoft.AspNetCore.Testing return new KestrelTrace(loggerFactory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel")); } + public void InitializeHeartbeat() + { + MockSystemClock = null; + SystemClock = new SystemClock(); + DateHeaderValueManager = new DateHeaderValueManager(SystemClock); + + var heartbeatManager = new HttpHeartbeatManager(ConnectionManager); + Heartbeat = new Heartbeat( + new IHeartbeatHandler[] { DateHeaderValueManager, heartbeatManager }, + SystemClock, + DebuggerWrapper.Singleton, + Log); + } + private void Initialize(ILoggerFactory loggerFactory, IKestrelTrace kestrelTrace) { LoggerFactory = loggerFactory; Log = kestrelTrace; Scheduler = PipeScheduler.ThreadPool; - SystemClock = new MockSystemClock(); - DateHeaderValueManager = new DateHeaderValueManager(SystemClock); + MockSystemClock = new MockSystemClock(); + SystemClock = MockSystemClock; + DateHeaderValueManager = new DateHeaderValueManager(MockSystemClock); ConnectionManager = new HttpConnectionManager(Log, ResourceCounter.Unlimited); HttpParser = new HttpParser(Log.IsEnabled(LogLevel.Information)); ServerOptions = new KestrelServerOptions @@ -53,6 +71,10 @@ namespace Microsoft.AspNetCore.Testing public ILoggerFactory LoggerFactory { get; set; } + public MockSystemClock MockSystemClock { get; set; } + + public Func> MemoryPoolFactory { get; set; } = KestrelMemoryPool.Create; + public string DateHeaderValue => DateHeaderValueManager.GetDateHeaderValues().String; } } diff --git a/test/Kestrel.FunctionalTests/TestHelpers/H2SpecCommands.cs b/test/shared/TransportTestHelpers/H2SpecCommands.cs similarity index 100% rename from test/Kestrel.FunctionalTests/TestHelpers/H2SpecCommands.cs rename to test/shared/TransportTestHelpers/H2SpecCommands.cs diff --git a/test/Kestrel.FunctionalTests/TestHelpers/HostNameIsReachableAttribute.cs b/test/shared/TransportTestHelpers/HostNameIsReachableAttribute.cs similarity index 100% rename from test/Kestrel.FunctionalTests/TestHelpers/HostNameIsReachableAttribute.cs rename to test/shared/TransportTestHelpers/HostNameIsReachableAttribute.cs diff --git a/test/Kestrel.FunctionalTests/TestHelpers/IPv6ScopeIdPresentConditionAttribute.cs b/test/shared/TransportTestHelpers/IPv6ScopeIdPresentConditionAttribute.cs similarity index 100% rename from test/Kestrel.FunctionalTests/TestHelpers/IPv6ScopeIdPresentConditionAttribute.cs rename to test/shared/TransportTestHelpers/IPv6ScopeIdPresentConditionAttribute.cs diff --git a/test/Kestrel.FunctionalTests/TestHelpers/IPv6SupportedConditionAttribute.cs b/test/shared/TransportTestHelpers/IPv6SupportedConditionAttribute.cs similarity index 100% rename from test/Kestrel.FunctionalTests/TestHelpers/IPv6SupportedConditionAttribute.cs rename to test/shared/TransportTestHelpers/IPv6SupportedConditionAttribute.cs diff --git a/test/Kestrel.FunctionalTests/TestHelpers/IWebHostPortExtensions.cs b/test/shared/TransportTestHelpers/IWebHostPortExtensions.cs similarity index 100% rename from test/Kestrel.FunctionalTests/TestHelpers/IWebHostPortExtensions.cs rename to test/shared/TransportTestHelpers/IWebHostPortExtensions.cs diff --git a/test/Kestrel.FunctionalTests/TestHelpers/TestServer.cs b/test/shared/TransportTestHelpers/TestServer.cs similarity index 90% rename from test/Kestrel.FunctionalTests/TestHelpers/TestServer.cs rename to test/shared/TransportTestHelpers/TestServer.cs index 8017e4313e..ab28a8eab1 100644 --- a/test/Kestrel.FunctionalTests/TestHelpers/TestServer.cs +++ b/test/shared/TransportTestHelpers/TestServer.cs @@ -44,20 +44,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } public TestServer(RequestDelegate app, TestServiceContext context, ListenOptions listenOptions, Action configureServices) - : this(TransportSelector.GetWebHostBuilder(), app, context, options => options.ListenOptions.Add(listenOptions), configureServices) + : this(app, context, options => options.ListenOptions.Add(listenOptions), configureServices) { } public TestServer(RequestDelegate app, TestServiceContext context, Action configureKestrel) - : this(TransportSelector.GetWebHostBuilder(), app, context, configureKestrel, _ => { }) + : this(app, context, configureKestrel, _ => { }) { } - public TestServer(IWebHostBuilder builder, RequestDelegate app, TestServiceContext context, Action configureKestrel, Action configureServices) + public TestServer(RequestDelegate app, TestServiceContext context, Action configureKestrel, Action configureServices) { _app = app; Context = context; - _host = builder + _host = TransportSelector.GetWebHostBuilder(context.MemoryPoolFactory) .UseKestrel(options => { configureKestrel(options);