Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Servers/HttpSys/test/FunctionalTests/Http3Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
namespace Microsoft.AspNetCore.Server.HttpSys
{
[MsQuicSupported] // Required by HttpClient
[Http3Supported]
[HttpSysHttp3Supported]
public class Http3Tests
{
[ConditionalFact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
6 changes: 6 additions & 0 deletions src/Servers/IIS/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<Project>
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
<PropertyGroup>
<KestrelSharedSourceRoot>$(MSBuildThisFileDirectory)..\Kestrel\shared\</KestrelSharedSourceRoot>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we consider moving this to https://github.com/dotnet/aspnetcore/tree/main/src/Shared/Kestrel or something? Is there a reason it needs to be special?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's grown very organically, it used to only be shared within Kestrel projects, now it's shared across all the servers. It might be worth moving, but not in this PR. Will, file an issue?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

</PropertyGroup>
</Project>
14 changes: 12 additions & 2 deletions src/Servers/IIS/IIS/src/Core/IISHttpContextOfT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -65,8 +66,17 @@ public override async Task<bool> ProcessRequestAsync()
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)
Expand Down
178 changes: 178 additions & 0 deletions src/Servers/IIS/IIS/test/IIS.FunctionalTests/Http3Tests.cs
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: consider passing these in to SetupClient to further streamline the tests

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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand All @@ -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>
Expand Down
105 changes: 105 additions & 0 deletions src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1550,6 +1550,111 @@ public Task OnCompletedThrows(HttpContext httpContext)
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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down