Skip to content

Commit

Permalink
Fix HTTP/3 ALPN (#56215)
Browse files Browse the repository at this point in the history
* Fix HTTP/3 ALPN

* Add tests
  • Loading branch information
CarnaViire authored Jul 29, 2021
1 parent 61335ca commit 121baa8
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,7 @@ public Http3LoopbackServer(QuicImplementationProvider quicImplementationProvider
EnabledSslProtocols = options.SslProtocols,
ApplicationProtocols = new List<SslApplicationProtocol>
{
new SslApplicationProtocol("h3-31"),
new SslApplicationProtocol("h3-30"),
new SslApplicationProtocol("h3-29")
new SslApplicationProtocol(options.Alpn)
},
ServerCertificate = _cert,
ClientCertificateRequired = false
Expand Down Expand Up @@ -122,13 +120,17 @@ private static Http3Options CreateOptions(GenericLoopbackOptions options)
}
public class Http3Options : GenericLoopbackOptions
{
public int MaxUnidirectionalStreams {get; set; }
public int MaxUnidirectionalStreams { get; set; }

public int MaxBidirectionalStreams { get; set; }

public string Alpn { get; set; }

public int MaxBidirectionalStreams {get; set; }
public Http3Options()
{
MaxUnidirectionalStreams = 100;
MaxBidirectionalStreams = 100;
Alpn = "h3";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ namespace System.Net.Http
internal sealed class Http3Connection : HttpConnectionBase
{
// TODO: once HTTP/3 is standardized, create APIs for this.
public static readonly SslApplicationProtocol Http3ApplicationProtocol29 = new SslApplicationProtocol("h3-29");
public static readonly SslApplicationProtocol Http3ApplicationProtocol30 = new SslApplicationProtocol("h3-30");
public static readonly SslApplicationProtocol Http3ApplicationProtocol31 = new SslApplicationProtocol("h3-31");
public static readonly SslApplicationProtocol Http3ApplicationProtocol = new SslApplicationProtocol("h3");

private readonly HttpConnectionPool _pool;
private readonly HttpAuthority? _origin;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ private static List<SslApplicationProtocol> CreateHttp3ApplicationProtocols()
if (IsHttp3Supported())
{
// TODO: Once the HTTP/3 versions are part of SslApplicationProtocol, see https://github.com/dotnet/runtime/issues/1293, move this back to field initialization.
return new List<SslApplicationProtocol>() { Http3Connection.Http3ApplicationProtocol31, Http3Connection.Http3ApplicationProtocol30, Http3Connection.Http3ApplicationProtocol29 };
return new List<SslApplicationProtocol>() { Http3Connection.Http3ApplicationProtocol };
}

return null!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using System.Net.Quic;
using System.Net.Security;
using System.Net.Test.Common;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -633,6 +634,128 @@ private static async Task SendDataForever(Http3LoopbackStream stream)
}
}

[ConditionalFact(nameof(IsMsQuicSupported))]
public async Task Alpn_H3_Success()
{
// Mock doesn't use ALPN.
if (UseQuicImplementationProvider == QuicImplementationProviders.Mock)
{
return;
}

var options = new Http3Options() { Alpn = "h3" };
using Http3LoopbackServer server = CreateHttp3LoopbackServer(options);

using var clientDone = new SemaphoreSlim(0);
using var serverDone = new SemaphoreSlim(0);

Task serverTask = Task.Run(async () =>
{
using Http3LoopbackConnection connection = (Http3LoopbackConnection)await server.EstablishGenericConnectionAsync();

SslApplicationProtocol negotiatedAlpn = ExtractMsQuicNegotiatedAlpn(connection);
Assert.Equal(new SslApplicationProtocol("h3"), negotiatedAlpn);

using Http3LoopbackStream stream = await connection.AcceptRequestStreamAsync();
await stream.HandleRequestAsync();

serverDone.Release();
await clientDone.WaitAsync();
});

Task clientTask = Task.Run(async () =>
{
using HttpClient client = CreateHttpClient();

using HttpRequestMessage request = new()
{
Method = HttpMethod.Get,
RequestUri = server.Address,
Version = HttpVersion30,
VersionPolicy = HttpVersionPolicy.RequestVersionExact
};
HttpResponseMessage response = await client.SendAsync(request).WaitAsync(TimeSpan.FromSeconds(10));

response.EnsureSuccessStatusCode();
Assert.Equal(HttpVersion.Version30, response.Version);

clientDone.Release();
await serverDone.WaitAsync();
});

await new[] { clientTask, serverTask }.WhenAllOrAnyFailed(200_000);
}

[ConditionalFact(nameof(IsMsQuicSupported))]
public async Task Alpn_NonH3_NegotiationFailure()
{
// Mock doesn't use ALPN.
if (UseQuicImplementationProvider == QuicImplementationProviders.Mock)
{
return;
}

var options = new Http3Options() { Alpn = "h3-29" }; // anything other than "h3"
using Http3LoopbackServer server = CreateHttp3LoopbackServer(options);

using var clientDone = new SemaphoreSlim(0);

Task serverTask = Task.Run(async () =>
{
// ALPN handshake handled by transport, app level will not get any notification
await clientDone.WaitAsync();
});

Task clientTask = Task.Run(async () =>
{
using HttpClient client = CreateHttpClient();

using HttpRequestMessage request = new()
{
Method = HttpMethod.Get,
RequestUri = server.Address,
Version = HttpVersion30,
VersionPolicy = HttpVersionPolicy.RequestVersionExact
};

HttpRequestException ex = await Assert.ThrowsAsync<HttpRequestException>(() => client.SendAsync(request).WaitAsync(TimeSpan.FromSeconds(10)));
Assert.Contains("ALPN_NEG_FAILURE", ex.Message);

clientDone.Release();
});

await new[] { clientTask, serverTask }.WhenAllOrAnyFailed(200_000);
}

private SslApplicationProtocol ExtractMsQuicNegotiatedAlpn(Http3LoopbackConnection loopbackConnection)
{
// TODO: rewrite after object structure change
// current structure:
// Http3LoopbackConnection -> private QuicConnection _connection
// QuicConnection -> private QuicConnectionProvider _provider (= MsQuicConnection)
// MsQuicConnection -> private SslApplicationProtocol _negotiatedAlpnProtocol

FieldInfo quicConnectionField = loopbackConnection.GetType().GetField("_connection", BindingFlags.Instance | BindingFlags.NonPublic);
Assert.NotNull(quicConnectionField);
object quicConnection = quicConnectionField.GetValue(loopbackConnection);
Assert.NotNull(quicConnection);
Assert.Equal("QuicConnection", quicConnection.GetType().Name);

FieldInfo msQuicConnectionField = quicConnection.GetType().GetField("_provider", BindingFlags.Instance | BindingFlags.NonPublic);
Assert.NotNull(msQuicConnectionField);
object msQuicConnection = msQuicConnectionField.GetValue(quicConnection);
Assert.NotNull(msQuicConnection);
Assert.Equal("MsQuicConnection", msQuicConnection.GetType().Name);

FieldInfo alpnField = msQuicConnection.GetType().GetField("_negotiatedAlpnProtocol", BindingFlags.Instance | BindingFlags.NonPublic);
Assert.NotNull(alpnField);
object alpn = alpnField.GetValue(msQuicConnection);
Assert.NotNull(alpn);
Assert.IsType<SslApplicationProtocol>(alpn);

return (SslApplicationProtocol)alpn;
}

/// <summary>
/// These are public interop test servers for various QUIC and HTTP/3 implementations,
/// taken from https://github.com/quicwg/base-drafts/wiki/Implementations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ void ConfigureListenOptions(ListenOptions listenOptions)
{
host = host.UseQuic(options =>
{
options.Alpn = "h3-29";
options.Alpn = "h3";
options.IdleTimeout = TimeSpan.FromMinutes(1);
});
}
Expand Down

0 comments on commit 121baa8

Please sign in to comment.