Skip to content

Commit

Permalink
Enable HTTP/2 client cert authentication in WinHttpHandler (#33158)
Browse files Browse the repository at this point in the history
Pre-release WinHTTP's version supports client cert authentication over HTTP/2, but the feature must be explicitly opted-in. PR sets WINHTTP_OPTION_ENABLE_HTTP2_PLUS_CLIENT_CERT to TRUE before invoking WinHttpConnect if the request's protocol is HTTP/2 and scheme is HTTPS.

This PR also enables all HTTP 1.1 tests for WinHttpHandler on .Net Core and Framework and the most of HTTP/2 tests on .Net Core.
  • Loading branch information
alnikola committed Mar 19, 2020
1 parent e47e7be commit 188243a
Show file tree
Hide file tree
Showing 33 changed files with 900 additions and 164 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,11 @@ internal partial class WinHttp

public const uint WINHTTP_OPTION_ASSURED_NON_BLOCKING_CALLBACKS = 111;

public const uint WINHTTP_OPTION_ENABLE_HTTP2_PLUS_CLIENT_CERT = 161;
public const uint WINHTTP_OPTION_ENABLE_HTTP_PROTOCOL = 133;
public const uint WINHTTP_OPTION_HTTP_PROTOCOL_USED = 134;
public const uint WINHTTP_PROTOCOL_FLAG_HTTP2 = 0x1;
public const uint WINHTTP_HTTP2_PLUS_CLIENT_CERT_FLAG = 0x1;

public const uint WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET = 114;
public const uint WINHTTP_OPTION_WEB_SOCKET_CLOSE_TIMEOUT = 115;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,8 @@ private async Task ProcessRequests()

// Send a response in the JSON format that the client expects
string username = context.User.Identity.Name;
await context.Response.OutputStream.WriteAsync(System.Text.Encoding.UTF8.GetBytes($"{{\"authenticated\": \"true\", \"user\": \"{username}\" }}"));
byte[] bytes = System.Text.Encoding.UTF8.GetBytes($"{{\"authenticated\": \"true\", \"user\": \"{username}\" }}");
await context.Response.OutputStream.WriteAsync(bytes);

context.Response.Close();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public class GenericLoopbackOptions
public IPAddress Address { get; set; } = IPAddress.Loopback;
public bool UseSsl { get; set; } = PlatformDetection.SupportsAlpn && !Capability.Http2ForceUnencryptedLoopback();
public SslProtocols SslProtocols { get; set; } =
#if !NETSTANDARD2_0
#if !NETSTANDARD2_0 && !NETFRAMEWORK
SslProtocols.Tls13 |
#endif
SslProtocols.Tls12;
Expand Down
6 changes: 4 additions & 2 deletions src/libraries/Common/tests/System/Net/Http/Http2Frames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -548,10 +548,12 @@ public override void WriteTo(Span<byte> buffer)
BinaryPrimitives.WriteUInt16BigEndian(buffer, checked((ushort)Origin.Length));
buffer = buffer.Slice(2);

Encoding.ASCII.GetBytes(Origin, buffer);
var tmpBuffer = Encoding.ASCII.GetBytes(Origin);
tmpBuffer.CopyTo(buffer);
buffer = buffer.Slice(Origin.Length);

Encoding.ASCII.GetBytes(AltSvc, buffer);
tmpBuffer = Encoding.ASCII.GetBytes(AltSvc);
tmpBuffer.CopyTo(buffer);
}

public override string ToString() => $"{base.ToString()}\n{nameof(Origin)}: {Origin}\n{nameof(AltSvc)}: {AltSvc}";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
// See the LICENSE file in the project root for more information.

using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http.Functional.Tests;
using System.Net.Security;
using System.Net.Sockets;
Expand All @@ -28,6 +28,7 @@ public class Http2LoopbackConnection : GenericLoopbackConnection
private readonly byte[] _prefix;
public string PrefixString => Encoding.UTF8.GetString(_prefix, 0, _prefix.Length);
public bool IsInvalid => _connectionSocket == null;
public Stream Stream => _connectionStream;

public Http2LoopbackConnection(Socket socket, Http2Options httpOptions)
{
Expand All @@ -40,6 +41,7 @@ public Http2LoopbackConnection(Socket socket, Http2Options httpOptions)

using (var cert = Configuration.Certificates.GetServerCertificate())
{
#if !NETFRAMEWORK
SslServerAuthenticationOptions options = new SslServerAuthenticationOptions();

options.EnabledSslProtocols = httpOptions.SslProtocols;
Expand All @@ -51,9 +53,12 @@ public Http2LoopbackConnection(Socket socket, Http2Options httpOptions)

options.ServerCertificate = cert;

options.ClientCertificateRequired = false;
options.ClientCertificateRequired = httpOptions.ClientCertificateRequired;

sslStream.AuthenticateAsServerAsync(options, CancellationToken.None).Wait();
#else
sslStream.AuthenticateAsServerAsync(cert, httpOptions.ClientCertificateRequired, httpOptions.SslProtocols, checkCertificateRevocation: false).Wait();
#endif
}

_connectionStream = sslStream;
Expand All @@ -64,6 +69,10 @@ public Http2LoopbackConnection(Socket socket, Http2Options httpOptions)
{
throw new Exception("Connection stream closed while attempting to read connection preface.");
}
else if (Text.Encoding.ASCII.GetString(_prefix).Contains("HTTP/1.1"))
{
throw new Exception("HTTP 1.1 request received.");
}
}

public async Task SendConnectionPrefaceAsync()
Expand Down Expand Up @@ -331,7 +340,7 @@ private static (int bytesConsumed, string value) DecodeString(ReadOnlySpan<byte>
}
else
{
string value = Encoding.ASCII.GetString(headerBlock.Slice(bytesConsumed, stringLength));
string value = Encoding.ASCII.GetString(headerBlock.Slice(bytesConsumed, stringLength).ToArray());
return (bytesConsumed + stringLength, value);
}
}
Expand Down
18 changes: 15 additions & 3 deletions src/libraries/Common/tests/System/Net/Http/Http2LoopbackServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,14 @@ public override async Task AcceptConnectionAsync(Func<GenericLoopbackConnection,
}
}

public static async Task CreateClientAndServerAsync(Func<Uri, Task> clientFunc, Func<Http2LoopbackServer, Task> serverFunc, int timeout = 60_000)
public static Task CreateClientAndServerAsync(Func<Uri, Task> clientFunc, Func<Http2LoopbackServer, Task> serverFunc, int timeout = 60_000)
{
using (var server = Http2LoopbackServer.CreateServer())
return CreateClientAndServerAsync(clientFunc, serverFunc, null, timeout);
}

public static async Task CreateClientAndServerAsync(Func<Uri, Task> clientFunc, Func<Http2LoopbackServer, Task> serverFunc, Http2Options http2Options, int timeout = 60_000)
{
using (var server = Http2LoopbackServer.CreateServer(http2Options ?? new Http2Options()))
{
Task clientTask = clientFunc(server.Address);
Task serverTask = serverFunc(server);
Expand All @@ -197,6 +202,8 @@ public class Http2Options : GenericLoopbackOptions
{
public int ListenBacklog { get; set; } = 1;

public bool ClientCertificateRequired { get; set; }

public Http2Options()
{
UseSsl = PlatformDetection.SupportsAlpn && !Capability.Http2ForceUnencryptedLoopback();
Expand Down Expand Up @@ -237,7 +244,7 @@ public override async Task CreateServerAsync(Func<GenericLoopbackServer, Uri, Ta
}
}

public override Version Version => HttpVersion.Version20;
public override Version Version => HttpVersion20.Value;
}

public enum ProtocolErrors
Expand All @@ -257,4 +264,9 @@ public enum ProtocolErrors
INADEQUATE_SECURITY = 0xc,
HTTP_1_1_REQUIRED = 0xd
}

public static class HttpVersion20
{
public static readonly Version Value = new Version(2, 0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@ public void SingletonReturnsTrue()
[InlineData(SslProtocols.Tls, true)]
[InlineData(SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls, false)]
[InlineData(SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls, true)]
#if !NETFRAMEWORK
[InlineData(SslProtocols.Tls13 | SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls, false)]
[InlineData(SslProtocols.Tls13 | SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls, true)]
#endif
[InlineData(SslProtocols.None, false)]
[InlineData(SslProtocols.None, true)]
public async Task SetDelegate_ConnectionSucceeds(SslProtocols acceptedProtocol, bool requestOnlyThisProtocol)
Expand All @@ -64,7 +66,11 @@ public async Task SetDelegate_ConnectionSucceeds(SslProtocols acceptedProtocol,
// restrictions on minimum TLS/SSL version
// We currently know that some platforms like Debian 10 OpenSSL
// will by default block < TLS 1.2
#if !NETFRAMEWORK
handler.SslProtocols = SslProtocols.Tls13 | SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls;
#else
handler.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls;
#endif
}

var options = new LoopbackServer.Options { UseSsl = true, SslProtocols = acceptedProtocol };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,12 @@ public async Task Credentials_ServerUsesWindowsAuthentication_Success(string ser
[InlineData("Negotiate")]
public async Task Credentials_ServerChallengesWithWindowsAuth_ClientSendsWindowsAuthHeader(string authScheme)
{
#if WINHTTPHANDLER_TEST
if (UseVersion > HttpVersion.Version11)
{
throw new SkipTestException($"Test doesn't support {UseVersion} protocol.");
}
#endif
await LoopbackServerFactory.CreateClientAndServerAsync(
async uri =>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,24 @@ public abstract class HttpClientHandler_Cancellation_Test : HttpClientHandlerTes
{
public HttpClientHandler_Cancellation_Test(ITestOutputHelper output) : base(output) { }

[Theory]
[ConditionalTheory]
[InlineData(false, CancellationMode.Token)]
[InlineData(true, CancellationMode.Token)]
public async Task PostAsync_CancelDuringRequestContentSend_TaskCanceledQuickly(bool chunkedTransfer, CancellationMode mode)
{
if (LoopbackServerFactory.Version >= HttpVersion.Version20 && chunkedTransfer)
if (LoopbackServerFactory.Version >= HttpVersion20.Value && chunkedTransfer)
{
// There is no chunked encoding in HTTP/2 and later
return;
}

#if WINHTTPHANDLER_TEST
if (UseVersion >= HttpVersion20.Value)
{
throw new SkipTestException($"Test doesn't support {UseVersion} protocol.");
}
#endif

var serverRelease = new TaskCompletionSource<bool>();
await LoopbackServerFactory.CreateClientAndServerAsync(async uri =>
{
Expand Down Expand Up @@ -76,16 +83,23 @@ public async Task PostAsync_CancelDuringRequestContentSend_TaskCanceledQuickly(b
});
}

[Theory]
[ConditionalTheory]
[MemberData(nameof(OneBoolAndCancellationMode))]
public async Task GetAsync_CancelDuringResponseHeadersReceived_TaskCanceledQuickly(bool connectionClose, CancellationMode mode)
{
if (LoopbackServerFactory.Version >= HttpVersion.Version20 && connectionClose)
if (LoopbackServerFactory.Version >= HttpVersion20.Value && connectionClose)
{
// There is no Connection header in HTTP/2 and later
return;
}

#if WINHTTPHANDLER_TEST
if (UseVersion >= HttpVersion20.Value)
{
throw new SkipTestException($"Test doesn't support {UseVersion} protocol.");
}
#endif

using (HttpClient client = CreateHttpClient())
{
client.Timeout = Timeout.InfiniteTimeSpan;
Expand Down Expand Up @@ -130,7 +144,7 @@ public async Task GetAsync_CancelDuringResponseHeadersReceived_TaskCanceledQuick
[MemberData(nameof(TwoBoolsAndCancellationMode))]
public async Task GetAsync_CancelDuringResponseBodyReceived_Buffered_TaskCanceledQuickly(bool chunkedTransfer, bool connectionClose, CancellationMode mode)
{
if (LoopbackServerFactory.Version >= HttpVersion.Version20 && (chunkedTransfer || connectionClose))
if (LoopbackServerFactory.Version >= HttpVersion20.Value && (chunkedTransfer || connectionClose))
{
// There is no chunked encoding or connection header in HTTP/2 and later
return;
Expand Down Expand Up @@ -182,16 +196,23 @@ public async Task GetAsync_CancelDuringResponseBodyReceived_Buffered_TaskCancele
}
}

[Theory]
[ConditionalTheory]
[MemberData(nameof(ThreeBools))]
public async Task GetAsync_CancelDuringResponseBodyReceived_Unbuffered_TaskCanceledQuickly(bool chunkedTransfer, bool connectionClose, bool readOrCopyToAsync)
{
if (LoopbackServerFactory.Version >= HttpVersion.Version20 && (chunkedTransfer || connectionClose))
if (LoopbackServerFactory.Version >= HttpVersion20.Value && (chunkedTransfer || connectionClose))
{
// There is no chunked encoding or connection header in HTTP/2 and later
return;
}

#if WINHTTPHANDLER_TEST
if (UseVersion >= HttpVersion20.Value)
{
throw new SkipTestException($"Test doesn't support {UseVersion} protocol.");
}
#endif

using (HttpClient client = CreateHttpClient())
{
client.Timeout = Timeout.InfiniteTimeSpan;
Expand Down Expand Up @@ -237,14 +258,19 @@ public async Task GetAsync_CancelDuringResponseBodyReceived_Unbuffered_TaskCance
});
}
}

[Theory]
[ConditionalTheory]
[InlineData(CancellationMode.CancelPendingRequests, false)]
[InlineData(CancellationMode.DisposeHttpClient, false)]
[InlineData(CancellationMode.CancelPendingRequests, true)]
[InlineData(CancellationMode.DisposeHttpClient, true)]
public async Task GetAsync_CancelPendingRequests_DoesntCancelReadAsyncOnResponseStream(CancellationMode mode, bool copyToAsync)
{
#if WINHTTPHANDLER_TEST
if (UseVersion >= HttpVersion20.Value)
{
throw new SkipTestException($"Test doesn't support {UseVersion} protocol.");
}
#endif
using (HttpClient client = CreateHttpClient())
{
client.Timeout = Timeout.InfiniteTimeSpan;
Expand Down Expand Up @@ -312,7 +338,7 @@ public async Task GetAsync_CancelPendingRequests_DoesntCancelReadAsyncOnResponse
[ConditionalFact]
public async Task MaxConnectionsPerServer_WaitingConnectionsAreCancelable()
{
if (LoopbackServerFactory.Version >= HttpVersion.Version20)
if (LoopbackServerFactory.Version >= HttpVersion20.Value)
{
// HTTP/2 does not use connection limits.
throw new SkipTestException("Not supported on HTTP/2 and later");
Expand Down Expand Up @@ -490,11 +516,18 @@ public static IEnumerable<object[]> PostAsync_Cancel_CancellationTokenPassedToCo
}
}

#if !NETFRAMEWORK
[OuterLoop("Uses Task.Delay")]
[Theory]
[ConditionalTheory]
[MemberData(nameof(PostAsync_Cancel_CancellationTokenPassedToContent_MemberData))]
public async Task PostAsync_Cancel_CancellationTokenPassedToContent(HttpContent content, CancellationTokenSource cancellationTokenSource)
{
#if WINHTTPHANDLER_TEST
if (UseVersion > HttpVersion.Version11)
{
throw new SkipTestException($"Test doesn't support {UseVersion} protocol.");
}
#endif
await LoopbackServerFactory.CreateClientAndServerAsync(
async uri =>
{
Expand All @@ -518,6 +551,7 @@ public async Task PostAsync_Cancel_CancellationTokenPassedToContent(HttpContent
catch (Exception) { }
});
}
#endif

private async Task ValidateClientCancellationAsync(Func<Task> clientBodyAsync)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ public async Task Manual_CertificateOnlySentWhenValid_Success(int certIndex, boo
{
_output.WriteLine(
"Client cert: {0}",
((X509Certificate2)sslStream.RemoteCertificate).GetNameInfo(X509NameType.SimpleName, false));
new X509Certificate2(sslStream.RemoteCertificate.Export(X509ContentType.Cert)).GetNameInfo(X509NameType.SimpleName, false));
Assert.Equal(cert, sslStream.RemoteCertificate);
}
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,11 @@ public async Task GetAsync_AddMultipleCookieHeaders_CookiesSent()

private string GetCookieValue(HttpRequestData request)
{
#if !NETFRAMEWORK
if (LoopbackServerFactory.Version < HttpVersion.Version20)
#else
if (LoopbackServerFactory.Version < HttpVersion20.Value)
#endif
{
// HTTP/1.x must have only one value.
return request.GetSingleHeaderValue("Cookie");
Expand Down Expand Up @@ -603,7 +607,9 @@ public static IEnumerable<object[]> CookieNamesValuesAndUseCookies()
yield return new object[] { "ABC", "123", useCookies };
yield return new object[] { "Hello", "World", useCookies };
yield return new object[] { "foo", "bar", useCookies };
#if !NETFRAMEWORK
yield return new object[] { "Hello World", "value", useCookies };
#endif
yield return new object[] { ".AspNetCore.Session", "RAExEmXpoCbueP_QYM", useCookies };

yield return new object[]
Expand Down
Loading

0 comments on commit 188243a

Please sign in to comment.