diff --git a/src/Servers/HttpSys/test/FunctionalTests/Http3Tests.cs b/src/Servers/HttpSys/test/FunctionalTests/Http3Tests.cs index 69051dd32ff07cb13096be432b98166766d051a3..87f7e1b68674b5a125732d1a2746288268f44262 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/Http3Tests.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/Http3Tests.cs @@ -17,7 +17,7 @@ using Xunit; namespace Microsoft.AspNetCore.Server.HttpSys { [MsQuicSupported] // Required by HttpClient - [Http3Supported] + [HttpSysHttp3Supported] public class Http3Tests { [ConditionalFact] diff --git a/src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj b/src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj index c574f2941644248bb5948fe6728df76713ff2713..43bac347fcd92ee3e118d785af08e5b0ea58324a 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj +++ b/src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj @@ -23,6 +23,7 @@ <Compile Include="$(KestrelSharedSourceRoot)test\TestResources.cs" LinkBase="shared" /> <Content Include="$(KestrelSharedSourceRoot)test\TestCertificates\*.pfx" LinkBase="shared\TestCertificates" CopyToOutputDirectory="PreserveNewest" /> <Compile Include="$(KestrelSharedSourceRoot)test\TransportTestHelpers\MsQuicSupportedAttribute.cs" LinkBase="shared\" /> + <Compile Include="$(KestrelSharedSourceRoot)test\TransportTestHelpers\HttpSysHttp3SupportedAttribute.cs" LinkBase="shared\" /> </ItemGroup> <ItemGroup> diff --git a/src/Servers/IIS/Directory.Build.props b/src/Servers/IIS/Directory.Build.props new file mode 100644 index 0000000000000000000000000000000000000000..ba4f039671d75c90ab456ff9f005205ded17795f --- /dev/null +++ b/src/Servers/IIS/Directory.Build.props @@ -0,0 +1,6 @@ +<Project> + <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" /> + <PropertyGroup> + <KestrelSharedSourceRoot>$(MSBuildThisFileDirectory)..\Kestrel\shared\</KestrelSharedSourceRoot> + </PropertyGroup> +</Project> diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpContextOfT.cs b/src/Servers/IIS/IIS/src/Core/IISHttpContextOfT.cs index 2766c803529dcdcb2c4de196ea8993fe74787aff..34ef1725f557a7f3a1c896b0d206dddc8f01cdca 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpContextOfT.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpContextOfT.cs @@ -3,6 +3,7 @@ using System; using System.Buffers; +using System.Net; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting.Server; @@ -65,8 +66,17 @@ namespace Microsoft.AspNetCore.Server.IIS.Core if (!success && HasResponseStarted && NativeMethods.HttpHasResponse4(_requestNativeHandle)) { // HTTP/2 INTERNAL_ERROR = 0x2 https://tools.ietf.org/html/rfc7540#section-7 - // Otherwise the default is Cancel = 0x8. - SetResetCode(2); + // Otherwise the default is Cancel = 0x8 (h2) or 0x010c (h3). + if (HttpVersion == System.Net.HttpVersion.Version20) + { + // HTTP/2 INTERNAL_ERROR = 0x2 https://tools.ietf.org/html/rfc7540#section-7 + SetResetCode(2); + } + else if (HttpVersion == System.Net.HttpVersion.Version30) + { + // HTTP/3 H3_INTERNAL_ERROR = 0x0102 https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-8.1 + SetResetCode(0x0102); + } } if (!_requestAborted) diff --git a/src/Servers/IIS/IIS/test/IIS.FunctionalTests/Http3Tests.cs b/src/Servers/IIS/IIS/test/IIS.FunctionalTests/Http3Tests.cs new file mode 100644 index 0000000000000000000000000000000000000000..4b889be972c8c4996af7d6241f608ee0baa9ba13 --- /dev/null +++ b/src/Servers/IIS/IIS/test/IIS.FunctionalTests/Http3Tests.cs @@ -0,0 +1,178 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Quic; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests; +using Microsoft.AspNetCore.Server.IntegrationTesting.Common; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; +using Microsoft.Win32; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests +{ + [MsQuicSupported] + [HttpSysHttp3Supported] + [Collection(IISHttpsTestSiteCollection.Name)] + public class Http3Tests + { + public Http3Tests(IISTestSiteFixture fixture) + { + var port = TestPortHelper.GetNextSSLPort(); + fixture.DeploymentParameters.ApplicationBaseUriHint = $"https://localhost:{port}/"; + fixture.DeploymentParameters.AddHttpsToServerConfig(); + fixture.DeploymentParameters.SetWindowsAuth(false); + Fixture = fixture; + } + + public IISTestSiteFixture Fixture { get; } + + [ConditionalFact] + public async Task Http3_Direct() + { + using var client = SetUpClient(); + client.DefaultRequestVersion = HttpVersion.Version30; + client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact; + var response = await client.GetAsync(Fixture.Client.BaseAddress.ToString() + "Http3_Direct"); + + response.EnsureSuccessStatusCode(); + Assert.Equal(HttpVersion.Version30, response.Version); + Assert.Equal("HTTP/3", await response.Content.ReadAsStringAsync()); + } + + [ConditionalFact] + public async Task Http3_AltSvcHeader_UpgradeFromHttp1() + { + var address = Fixture.Client.BaseAddress.ToString() + "Http3_AltSvcHeader_UpgradeFromHttp1"; + + var altsvc = $@"h3="":{new Uri(address).Port}"""; + using var client = SetUpClient(); + client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher; + + // First request is HTTP/1.1, gets an alt-svc response + var request = new HttpRequestMessage(HttpMethod.Get, address); + request.Version = HttpVersion.Version11; + request.VersionPolicy = HttpVersionPolicy.RequestVersionExact; + var response1 = await client.SendAsync(request); + response1.EnsureSuccessStatusCode(); + Assert.Equal("HTTP/1.1", await response1.Content.ReadAsStringAsync()); + Assert.Equal(altsvc, response1.Headers.GetValues(HeaderNames.AltSvc).SingleOrDefault()); + + // Second request is HTTP/3 + var response3 = await client.GetAsync(address); + Assert.Equal(HttpVersion.Version30, response3.Version); + Assert.Equal("HTTP/3", await response3.Content.ReadAsStringAsync()); + } + + [ConditionalFact] + public async Task Http3_AltSvcHeader_UpgradeFromHttp2() + { + var address = Fixture.Client.BaseAddress.ToString() + "Http3_AltSvcHeader_UpgradeFromHttp2"; + + var altsvc = $@"h3="":{new Uri(address).Port}"""; + using var client = SetUpClient(); + client.DefaultRequestVersion = HttpVersion.Version20; + client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher; + + // First request is HTTP/2, gets an alt-svc response + var response2 = await client.GetAsync(address); + response2.EnsureSuccessStatusCode(); + Assert.Equal(altsvc, response2.Headers.GetValues(HeaderNames.AltSvc).SingleOrDefault()); + Assert.Equal("HTTP/2", await response2.Content.ReadAsStringAsync()); + + // Second request is HTTP/3 + var response3 = await client.GetStringAsync(address); + Assert.Equal("HTTP/3", response3); + } + + [ConditionalFact] + public async Task Http3_ResponseTrailers() + { + var address = Fixture.Client.BaseAddress.ToString() + "Http3_ResponseTrailers"; + using var client = SetUpClient(); + client.DefaultRequestVersion = HttpVersion.Version30; + client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact; + var response = await client.GetAsync(address); + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadAsStringAsync(); + Assert.Equal("HTTP/3", result); + Assert.Equal("value", response.TrailingHeaders.GetValues("custom").SingleOrDefault()); + } + + [ConditionalFact] + public async Task Http3_ResetBeforeHeaders() + { + var address = Fixture.Client.BaseAddress.ToString() + "Http3_ResetBeforeHeaders"; + using var client = SetUpClient(); + client.DefaultRequestVersion = HttpVersion.Version30; + client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact; + var ex = await Assert.ThrowsAsync<HttpRequestException>(() => client.GetAsync(address)); + var qex = Assert.IsType<QuicStreamAbortedException>(ex.InnerException); + Assert.Equal(0x010b, qex.ErrorCode); + } + + [ConditionalFact] + public async Task Http3_ResetAfterHeaders() + { + var address = Fixture.Client.BaseAddress.ToString() + "Http3_ResetAfterHeaders"; + using var client = SetUpClient(); + client.DefaultRequestVersion = HttpVersion.Version30; + client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact; + var response = await client.GetAsync(address, HttpCompletionOption.ResponseHeadersRead); + await client.GetAsync(Fixture.Client.BaseAddress.ToString() + "Http3_ResetAfterHeaders_SetResult"); + response.EnsureSuccessStatusCode(); + var ex = await Assert.ThrowsAsync<HttpRequestException>(() => response.Content.ReadAsStringAsync()); + var qex = Assert.IsType<QuicStreamAbortedException>(ex.InnerException?.InnerException?.InnerException); + Assert.Equal(0x010c, qex.ErrorCode); // H3_REQUEST_CANCELLED + } + + [ConditionalFact] + public async Task Http3_AppExceptionAfterHeaders_InternalError() + { + var address = Fixture.Client.BaseAddress.ToString() + "Http3_AppExceptionAfterHeaders_InternalError"; + using var client = SetUpClient(); + client.DefaultRequestVersion = HttpVersion.Version30; + client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact; + + var response = await client.GetAsync(address, HttpCompletionOption.ResponseHeadersRead); + await client.GetAsync(Fixture.Client.BaseAddress.ToString() + "Http3_AppExceptionAfterHeaders_InternalError_SetResult"); + response.EnsureSuccessStatusCode(); + var ex = await Assert.ThrowsAsync<HttpRequestException>(() => response.Content.ReadAsStringAsync()); + var qex = Assert.IsType<QuicStreamAbortedException>(ex.InnerException?.InnerException?.InnerException); + Assert.Equal(0x0102, qex.ErrorCode); // H3_INTERNAL_ERROR + } + + [ConditionalFact] + public async Task Http3_Abort_Cancel() + { + var address = Fixture.Client.BaseAddress.ToString() + "Http3_Abort_Cancel"; + using var client = SetUpClient(); + client.DefaultRequestVersion = HttpVersion.Version30; + client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact; + + var ex = await Assert.ThrowsAsync<HttpRequestException>(() => client.GetAsync(address)); + var qex = Assert.IsType<QuicStreamAbortedException>(ex.InnerException); + Assert.Equal(0x010c, qex.ErrorCode); // H3_REQUEST_CANCELLED + } + + private HttpClient SetUpClient() + { + var handler = new HttpClientHandler(); + // Needed on CI, the IIS Express cert we use isn't trusted there. + handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; + return new HttpClient(handler); + } + } +} diff --git a/src/Servers/IIS/IIS/test/IIS.FunctionalTests/IIS.FunctionalTests.csproj b/src/Servers/IIS/IIS/test/IIS.FunctionalTests/IIS.FunctionalTests.csproj index a7aab6e07a5b08c81ba3070a039cdcde6274fd6f..5aa5b73cb0579f3a7ac93e2db829a97272ea2caa 100644 --- a/src/Servers/IIS/IIS/test/IIS.FunctionalTests/IIS.FunctionalTests.csproj +++ b/src/Servers/IIS/IIS/test/IIS.FunctionalTests/IIS.FunctionalTests.csproj @@ -11,6 +11,11 @@ <Import Project="../FunctionalTest.props" /> + <ItemGroup> + <!-- Required for QUIC & HTTP/3 in .NET 6 - https://github.com/dotnet/runtime/pull/55332 --> + <RuntimeHostConfigurationOption Include="System.Net.SocketsHttpHandler.Http3Support" Value="true" /> + </ItemGroup> + <ItemGroup> <ProjectReference Include="..\testassets\IIS.Common.TestLib\IIS.Common.TestLib.csproj" /> <ProjectReference Include="..\testassets\InProcessWebSite\InProcessWebSite.csproj"> @@ -29,6 +34,8 @@ <Compile Include="$(SharedSourceRoot)ValueTaskExtensions\**\*.cs" LinkBase="Shared\" /> <Compile Remove="$(SharedSourceRoot)ServerInfrastructure\DuplexPipe.cs" /> <Compile Include="$(SharedSourceRoot)TaskToApm.cs" Link="Shared\TaskToApm.cs" /> + <Compile Include="$(KestrelSharedSourceRoot)test\TransportTestHelpers\MsQuicSupportedAttribute.cs" LinkBase="Shared\" /> + <Compile Include="$(KestrelSharedSourceRoot)test\TransportTestHelpers\HttpSysHttp3SupportedAttribute.cs" LinkBase="shared\" /> </ItemGroup> <ItemGroup> diff --git a/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs b/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs index 4e4c0b652adf7e007574f2d66f17cdcb5649b6a9..2e3a900295274a0fe8402f7571b923933841f517 100644 --- a/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs +++ b/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs @@ -1550,6 +1550,111 @@ namespace TestSite return Task.CompletedTask; } + public Task Http3_Direct(HttpContext context) + { + try + { + Assert.True(context.Request.IsHttps); + return context.Response.WriteAsync(context.Request.Protocol); + } + catch (Exception ex) + { + return context.Response.WriteAsync(ex.ToString()); + } + } + + public Task Http3_AltSvcHeader_UpgradeFromHttp1(HttpContext context) + { + var altsvc = $@"h3="":{context.Connection.LocalPort}"""; + try + { + Assert.True(context.Request.IsHttps); + context.Response.Headers.AltSvc = altsvc; + return context.Response.WriteAsync(context.Request.Protocol); + } + catch (Exception ex) + { + return context.Response.WriteAsync(ex.ToString()); + } + } + + public Task Http3_AltSvcHeader_UpgradeFromHttp2(HttpContext context) + { + return Http3_AltSvcHeader_UpgradeFromHttp1(context); + } + + public async Task Http3_ResponseTrailers(HttpContext context) + { + try + { + Assert.True(context.Request.IsHttps); + await context.Response.WriteAsync(context.Request.Protocol); + context.Response.AppendTrailer("custom", "value"); + } + catch (Exception ex) + { + await context.Response.WriteAsync(ex.ToString()); + } + } + + public Task Http3_ResetBeforeHeaders(HttpContext context) + { + try + { + Assert.True(context.Request.IsHttps); + context.Features.Get<IHttpResetFeature>().Reset(0x010b); // H3_REQUEST_REJECTED + return Task.CompletedTask; + } + catch (Exception ex) + { + return context.Response.WriteAsync(ex.ToString()); + } + } + + private TaskCompletionSource _http3_ResetAfterHeadersCts = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + public async Task Http3_ResetAfterHeaders(HttpContext context) + { + try + { + Assert.True(context.Request.IsHttps); + await context.Response.Body.FlushAsync(); + await _http3_ResetAfterHeadersCts.Task; + context.Features.Get<IHttpResetFeature>().Reset(0x010c); // H3_REQUEST_CANCELLED + } + catch (Exception ex) + { + await context.Response.WriteAsync(ex.ToString()); + } + } + + public Task Http3_ResetAfterHeaders_SetResult(HttpContext context) + { + _http3_ResetAfterHeadersCts.SetResult(); + return Task.CompletedTask; + } + + private TaskCompletionSource _http3_AppExceptionAfterHeaders_InternalErrorCts = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + public async Task Http3_AppExceptionAfterHeaders_InternalError(HttpContext context) + { + await context.Response.Body.FlushAsync(); + await _http3_AppExceptionAfterHeaders_InternalErrorCts.Task; + throw new Exception("App Exception"); + } + + public Task Http3_AppExceptionAfterHeaders_InternalError_SetResult(HttpContext context) + { + _http3_AppExceptionAfterHeaders_InternalErrorCts.SetResult(); + return Task.CompletedTask; + } + + public Task Http3_Abort_Cancel(HttpContext context) + { + context.Abort(); + return Task.CompletedTask; + } + internal static readonly HashSet<(string, StringValues, StringValues)> NullTrailers = new HashSet<(string, StringValues, StringValues)>() { ("NullString", (string)null, (string)null), diff --git a/src/Servers/HttpSys/test/FunctionalTests/Http3SupportedAttribute.cs b/src/Servers/Kestrel/shared/test/TransportTestHelpers/HttpSysHttp3SupportedAttribute.cs similarity index 90% rename from src/Servers/HttpSys/test/FunctionalTests/Http3SupportedAttribute.cs rename to src/Servers/Kestrel/shared/test/TransportTestHelpers/HttpSysHttp3SupportedAttribute.cs index e3a2bd25d7cc394f9ada5b5170c74ad90940ab49..4c2256a212ae581d52e2e29ae1e9a20e21d78b35 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/Http3SupportedAttribute.cs +++ b/src/Servers/Kestrel/shared/test/TransportTestHelpers/HttpSysHttp3SupportedAttribute.cs @@ -3,13 +3,12 @@ using System; using System.Net.Quic; -using Microsoft.AspNetCore.Testing; using Microsoft.Win32; -namespace Microsoft.AspNetCore.Server.HttpSys +namespace Microsoft.AspNetCore.Testing { [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)] - public class Http3SupportedAttribute : Attribute, ITestCondition + public class HttpSysHttp3SupportedAttribute : Attribute, ITestCondition { // We have the same OS and TLS version requirements as MsQuic so check that first. public bool IsMet => QuicImplementationProviders.MsQuic.IsSupported && IsRegKeySet;