Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
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\Http3SupportedAttribute.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>
194 changes: 194 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,194 @@
// 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]
[Http3Supported]
[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()
{
var handler = new HttpClientHandler();
handler.MaxResponseHeadersLength = 128;
handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
using var client = new HttpClient(handler);
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}""";
var handler = new HttpClientHandler();
// Needed on CI, the IIS Express cert we use isn't trusted there.
handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
using var client = new HttpClient(handler);
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}""";
var handler = new HttpClientHandler();
// Needed on CI, the IIS Express cert we use isn't trusted there.
handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
using var client = new HttpClient(handler);
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";
var handler = new HttpClientHandler();
// Needed on CI, the IIS Express cert we use isn't trusted there.
handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
using var client = new HttpClient(handler);
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";
var handler = new HttpClientHandler();
// Needed on CI, the IIS Express cert we use isn't trusted there.
handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
using var client = new HttpClient(handler);
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";
var handler = new HttpClientHandler();
// Needed on CI, the IIS Express cert we use isn't trusted there.
handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
using var client = new HttpClient(handler);
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";
var handler = new HttpClientHandler();
// Needed on CI, the IIS Express cert we use isn't trusted there.
handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
using var client = new HttpClient(handler);
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";
var handler = new HttpClientHandler();
// Needed on CI, the IIS Express cert we use isn't trusted there.
handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
using var client = new HttpClient(handler);
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
}
}
}
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\Http3SupportedAttribute.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 Task Http3_ResponseTrailers(HttpContext context)
{
try
{
Assert.True(context.Request.IsHttps);
context.Response.AppendTrailer("custom", "value");
return context.Response.WriteAsync(context.Request.Protocol);
}
catch (Exception ex)
{
return 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,10 +3,9 @@

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
Expand Down