diff --git a/src/Servers/HttpSys/src/FeatureContext.cs b/src/Servers/HttpSys/src/FeatureContext.cs
index 27a38a84d8..9088f60590 100644
--- a/src/Servers/HttpSys/src/FeatureContext.cs
+++ b/src/Servers/HttpSys/src/FeatureContext.cs
@@ -6,10 +6,12 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net;
+using System.Security.Authentication;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Connections.Features;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Features.Authentication;
@@ -24,6 +26,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
IHttpResponseFeature,
IHttpSendFileFeature,
ITlsConnectionFeature,
+ ITlsHandshakeFeature,
// ITlsTokenBindingFeature, TODO: https://github.com/aspnet/HttpSysServer/issues/231
IHttpBufferingFeature,
IHttpRequestLifetimeFeature,
@@ -336,6 +339,12 @@ namespace Microsoft.AspNetCore.Server.HttpSys
{
return Request.IsHttps ? this : null;
}
+
+ internal ITlsHandshakeFeature GetTlsHandshakeFeature()
+ {
+ return Request.IsHttps ? this : null;
+ }
+
/* TODO: https://github.com/aspnet/HttpSysServer/issues/231
byte[] ITlsTokenBindingFeature.GetProvidedTokenBindingId() => Request.GetProvidedTokenBindingId();
@@ -482,6 +491,20 @@ namespace Microsoft.AspNetCore.Server.HttpSys
set => Request.MaxRequestBodySize = value;
}
+ SslProtocols ITlsHandshakeFeature.Protocol => Request.Protocol;
+
+ CipherAlgorithmType ITlsHandshakeFeature.CipherAlgorithm => Request.CipherAlgorithm;
+
+ int ITlsHandshakeFeature.CipherStrength => Request.CipherStrength;
+
+ HashAlgorithmType ITlsHandshakeFeature.HashAlgorithm => Request.HashAlgorithm;
+
+ int ITlsHandshakeFeature.HashStrength => Request.HashStrength;
+
+ ExchangeAlgorithmType ITlsHandshakeFeature.KeyExchangeAlgorithm => Request.KeyExchangeAlgorithm;
+
+ int ITlsHandshakeFeature.KeyExchangeStrength => Request.KeyExchangeStrength;
+
internal async Task OnResponseStart()
{
if (_responseStarted)
diff --git a/src/Servers/HttpSys/src/Http503VerbosityLevel .cs b/src/Servers/HttpSys/src/Http503VerbosityLevel.cs
similarity index 100%
rename from src/Servers/HttpSys/src/Http503VerbosityLevel .cs
rename to src/Servers/HttpSys/src/Http503VerbosityLevel.cs
diff --git a/src/Servers/HttpSys/src/Microsoft.AspNetCore.Server.HttpSys.csproj b/src/Servers/HttpSys/src/Microsoft.AspNetCore.Server.HttpSys.csproj
index 9117a4d39b..2ccf8af84c 100644
--- a/src/Servers/HttpSys/src/Microsoft.AspNetCore.Server.HttpSys.csproj
+++ b/src/Servers/HttpSys/src/Microsoft.AspNetCore.Server.HttpSys.csproj
@@ -16,6 +16,7 @@
+
diff --git a/src/Servers/HttpSys/src/RequestProcessing/Request.cs b/src/Servers/HttpSys/src/RequestProcessing/Request.cs
index 3dedf347a9..9112dfaca9 100644
--- a/src/Servers/HttpSys/src/RequestProcessing/Request.cs
+++ b/src/Servers/HttpSys/src/RequestProcessing/Request.cs
@@ -5,6 +5,7 @@ using System;
using System.Globalization;
using System.IO;
using System.Net;
+using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Security.Principal;
using System.Threading;
@@ -83,6 +84,11 @@ namespace Microsoft.AspNetCore.Server.HttpSys
User = _nativeRequestContext.GetUser();
+ if (IsHttps)
+ {
+ GetTlsHandshakeResults();
+ }
+
// GetTlsTokenBindingInfo(); TODO: https://github.com/aspnet/HttpSysServer/issues/231
// Finished directly accessing the HTTP_REQUEST structure.
@@ -232,6 +238,60 @@ namespace Microsoft.AspNetCore.Server.HttpSys
internal WindowsPrincipal User { get; }
+ public SslProtocols Protocol { get; private set; }
+
+ public CipherAlgorithmType CipherAlgorithm { get; private set; }
+
+ public int CipherStrength { get; private set; }
+
+ public HashAlgorithmType HashAlgorithm { get; private set; }
+
+ public int HashStrength { get; private set; }
+
+ public ExchangeAlgorithmType KeyExchangeAlgorithm { get; private set; }
+
+ public int KeyExchangeStrength { get; private set; }
+
+ private void GetTlsHandshakeResults()
+ {
+ var handshake = _nativeRequestContext.GetTlsHandshake();
+
+ Protocol = handshake.Protocol;
+ // The OS considers client and server TLS as different enum values. SslProtocols choose to combine those for some reason.
+ // We need to fill in the client bits so the enum shows the expected protocol.
+ // https://docs.microsoft.com/en-us/windows/desktop/api/schannel/ns-schannel-_secpkgcontext_connectioninfo
+ // Compare to https://referencesource.microsoft.com/#System/net/System/Net/SecureProtocols/_SslState.cs,8905d1bf17729de3
+#pragma warning disable CS0618 // Type or member is obsolete
+ if ((Protocol & SslProtocols.Ssl2) != 0)
+ {
+ Protocol |= SslProtocols.Ssl2;
+ }
+ if ((Protocol & SslProtocols.Ssl3) != 0)
+ {
+ Protocol |= SslProtocols.Ssl3;
+ }
+#pragma warning restore CS0618 // Type or member is obsolete
+ if ((Protocol & SslProtocols.Tls) != 0)
+ {
+ Protocol |= SslProtocols.Tls;
+ }
+ if ((Protocol & SslProtocols.Tls11) != 0)
+ {
+ Protocol |= SslProtocols.Tls11;
+ }
+ if ((Protocol & SslProtocols.Tls12) != 0)
+ {
+ Protocol |= SslProtocols.Tls12;
+ }
+
+ CipherAlgorithm = handshake.CipherType;
+ CipherStrength = (int)handshake.CipherStrength;
+ HashAlgorithm = handshake.HashType;
+ HashStrength = (int)handshake.HashStrength;
+ KeyExchangeAlgorithm = handshake.KeyExchangeType;
+ KeyExchangeStrength = (int)handshake.KeyExchangeStrength;
+ }
+
// Populates the client certificate. The result may be null if there is no client cert.
// TODO: Does it make sense for this to be invoked multiple times (e.g. renegotiate)? Client and server code appear to
// enable this, but it's unclear what Http.Sys would do.
diff --git a/src/Servers/HttpSys/src/StandardFeatureCollection.cs b/src/Servers/HttpSys/src/StandardFeatureCollection.cs
index 2adf51d1fc..69158b62d1 100644
--- a/src/Servers/HttpSys/src/StandardFeatureCollection.cs
+++ b/src/Servers/HttpSys/src/StandardFeatureCollection.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
+using Microsoft.AspNetCore.Connections.Features;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Features.Authentication;
@@ -19,6 +20,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
{ typeof(IHttpResponseFeature), _identityFunc },
{ typeof(IHttpSendFileFeature), _identityFunc },
{ typeof(ITlsConnectionFeature), ctx => ctx.GetTlsConnectionFeature() },
+ { typeof(ITlsHandshakeFeature), ctx => ctx.GetTlsHandshakeFeature() },
// { typeof(ITlsTokenBindingFeature), ctx => ctx.GetTlsTokenBindingFeature() }, TODO: https://github.com/aspnet/HttpSysServer/issues/231
{ typeof(IHttpBufferingFeature), _identityFunc },
{ typeof(IHttpRequestLifetimeFeature), _identityFunc },
diff --git a/src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs b/src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs
index 47b02a3092..e0a59cbf9b 100644
--- a/src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs
+++ b/src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs
@@ -1,12 +1,16 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+using System;
using System.IO;
using System.Net.Http;
+using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Connections.Features;
+using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Testing.xunit;
using Xunit;
@@ -15,40 +19,38 @@ namespace Microsoft.AspNetCore.Server.HttpSys
{
public class HttpsTests
{
- private const string Address = "https://localhost:9090/";
-
- [ConditionalFact(Skip = "TODO: Add trait filtering support so these SSL tests don't get run on teamcity or the command line."), Trait("scheme", "https")]
+ [ConditionalFact]
public async Task Https_200OK_Success()
{
- using (Utilities.CreateHttpsServer(httpContext =>
+ using (Utilities.CreateDynamicHttpsServer(out var address, httpContext =>
{
return Task.FromResult(0);
}))
{
- string response = await SendRequestAsync(Address);
+ string response = await SendRequestAsync(address);
Assert.Equal(string.Empty, response);
}
}
- [ConditionalFact(Skip = "TODO: Add trait filtering support so these SSL tests don't get run on teamcity or the command line."), Trait("scheme", "https")]
+ [ConditionalFact]
public async Task Https_SendHelloWorld_Success()
{
- using (Utilities.CreateHttpsServer(httpContext =>
+ using (Utilities.CreateDynamicHttpsServer(out var address, httpContext =>
{
byte[] body = Encoding.UTF8.GetBytes("Hello World");
httpContext.Response.ContentLength = body.Length;
return httpContext.Response.Body.WriteAsync(body, 0, body.Length);
}))
{
- string response = await SendRequestAsync(Address);
+ string response = await SendRequestAsync(address);
Assert.Equal("Hello World", response);
}
}
- [ConditionalFact(Skip = "TODO: Add trait filtering support so these SSL tests don't get run on teamcity or the command line."), Trait("scheme", "https")]
+ [ConditionalFact]
public async Task Https_EchoHelloWorld_Success()
{
- using (Utilities.CreateHttpsServer(httpContext =>
+ using (Utilities.CreateDynamicHttpsServer(out var address, httpContext =>
{
string input = new StreamReader(httpContext.Request.Body).ReadToEnd();
Assert.Equal("Hello World", input);
@@ -58,15 +60,15 @@ namespace Microsoft.AspNetCore.Server.HttpSys
return Task.FromResult(0);
}))
{
- string response = await SendRequestAsync(Address, "Hello World");
+ string response = await SendRequestAsync(address, "Hello World");
Assert.Equal("Hello World", response);
}
}
- [ConditionalFact(Skip = "TODO: Add trait filtering support so these SSL tests don't get run on teamcity or the command line."), Trait("scheme", "https")]
+ [ConditionalFact]
public async Task Https_ClientCertNotSent_ClientCertNotPresent()
{
- using (Utilities.CreateHttpsServer(async httpContext =>
+ using (Utilities.CreateDynamicHttpsServer(out var address, async httpContext =>
{
var tls = httpContext.Features.Get();
Assert.NotNull(tls);
@@ -75,15 +77,15 @@ namespace Microsoft.AspNetCore.Server.HttpSys
Assert.Null(tls.ClientCertificate);
}))
{
- string response = await SendRequestAsync(Address);
+ string response = await SendRequestAsync(address);
Assert.Equal(string.Empty, response);
}
}
- [ConditionalFact(Skip = "TODO: Add trait filtering support so these SSL tests don't get run on teamcity or the command line."), Trait("scheme", "https")]
+ [ConditionalFact(Skip = "Manual test only, client certs are not always available.")]
public async Task Https_ClientCertRequested_ClientCertPresent()
{
- using (Utilities.CreateHttpsServer(async httpContext =>
+ using (Utilities.CreateDynamicHttpsServer(out var address, async httpContext =>
{
var tls = httpContext.Features.Get();
Assert.NotNull(tls);
@@ -94,7 +96,37 @@ namespace Microsoft.AspNetCore.Server.HttpSys
{
X509Certificate2 cert = FindClientCert();
Assert.NotNull(cert);
- string response = await SendRequestAsync(Address, cert);
+ string response = await SendRequestAsync(address, cert);
+ Assert.Equal(string.Empty, response);
+ }
+ }
+
+ [ConditionalFact]
+ public async Task Https_SetsITlsHandshakeFeature()
+ {
+ using (Utilities.CreateDynamicHttpsServer(out var address, httpContext =>
+ {
+ try
+ {
+ var tlsFeature = httpContext.Features.Get();
+ Assert.NotNull(tlsFeature);
+ Assert.True(tlsFeature.Protocol > SslProtocols.None, "Protocol");
+ Assert.True(Enum.IsDefined(typeof(SslProtocols), tlsFeature.Protocol), "Defined"); // Mapping is required, make sure it's current
+ Assert.True(tlsFeature.CipherAlgorithm > CipherAlgorithmType.Null, "Cipher");
+ Assert.True(tlsFeature.CipherStrength > 0, "CipherStrength");
+ Assert.True(tlsFeature.HashAlgorithm > HashAlgorithmType.None, "HashAlgorithm");
+ Assert.True(tlsFeature.HashStrength >= 0, "HashStrength"); // May be 0 for some algorithms
+ Assert.True(tlsFeature.KeyExchangeAlgorithm > ExchangeAlgorithmType.None, "KeyExchangeAlgorithm");
+ Assert.True(tlsFeature.KeyExchangeStrength > 0, "KeyExchangeStrength");
+ }
+ catch (Exception ex)
+ {
+ return httpContext.Response.WriteAsync(ex.ToString());
+ }
+ return Task.FromResult(0);
+ }))
+ {
+ string response = await SendRequestAsync(address);
Assert.Equal(string.Empty, response);
}
}
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 1216b48683..fd830d6041 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
@@ -10,4 +10,9 @@
+
+
+ 214124cd-d05b-4309-9af9-9caa44b2b74a
+
+
diff --git a/src/Servers/HttpSys/test/FunctionalTests/Utilities.cs b/src/Servers/HttpSys/test/FunctionalTests/Utilities.cs
index bd38eafd81..24869a8b80 100644
--- a/src/Servers/HttpSys/test/FunctionalTests/Utilities.cs
+++ b/src/Servers/HttpSys/test/FunctionalTests/Utilities.cs
@@ -23,7 +23,10 @@ namespace Microsoft.AspNetCore.Server.HttpSys
// ports during dynamic port allocation.
private const int BasePort = 5001;
private const int MaxPort = 8000;
+ private const int BaseHttpsPort = 44300;
+ private const int MaxHttpsPort = 44399;
private static int NextPort = BasePort;
+ private static int NextHttpsPort = BaseHttpsPort;
private static object PortLock = new object();
internal static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(15);
internal static readonly int WriteRetryLimit = 1000;
@@ -148,17 +151,37 @@ namespace Microsoft.AspNetCore.Server.HttpSys
throw new Exception("Failed to locate a free port.");
}
- internal static IServer CreateHttpsServer(RequestDelegate app)
+ internal static IServer CreateDynamicHttpsServer(out string baseAddress, RequestDelegate app)
{
- return CreateServer("https", "localhost", 9090, string.Empty, app);
+ return CreateDynamicHttpsServer("/", out var root, out baseAddress, options => { }, app);
}
- internal static IServer CreateServer(string scheme, string host, int port, string path, RequestDelegate app)
+ internal static IServer CreateDynamicHttpsServer(string basePath, out string root, out string baseAddress, Action configureOptions, RequestDelegate app)
{
- var server = CreatePump();
- server.Features.Get().Addresses.Add(UrlPrefix.Create(scheme, host, port, path).ToString());
- server.StartAsync(new DummyApplication(app), CancellationToken.None).Wait();
- return server;
+ lock (PortLock)
+ {
+ while (NextHttpsPort < MaxHttpsPort)
+ {
+ var port = NextHttpsPort++;
+ var prefix = UrlPrefix.Create("https", "localhost", port, basePath);
+ root = prefix.Scheme + "://" + prefix.Host + ":" + prefix.Port;
+ baseAddress = prefix.ToString();
+
+ var server = CreatePump();
+ server.Features.Get().Addresses.Add(baseAddress);
+ configureOptions(server.Listener.Options);
+ try
+ {
+ server.StartAsync(new DummyApplication(app), CancellationToken.None).Wait();
+ return server;
+ }
+ catch (HttpSysException)
+ {
+ }
+ }
+ NextHttpsPort = BaseHttpsPort;
+ }
+ throw new Exception("Failed to locate a free port.");
}
internal static Task WithTimeout(this Task task) => task.TimeoutAfter(DefaultTimeout);
diff --git a/src/Shared/HttpSys/NativeInterop/HttpApiTypes.cs b/src/Shared/HttpSys/NativeInterop/HttpApiTypes.cs
index f05d089b9f..324426c60d 100644
--- a/src/Shared/HttpSys/NativeInterop/HttpApiTypes.cs
+++ b/src/Shared/HttpSys/NativeInterop/HttpApiTypes.cs
@@ -1,9 +1,11 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
+using System.Security.Authentication;
+
namespace Microsoft.AspNetCore.HttpSys.Internal
{
internal static unsafe class HttpApiTypes
@@ -426,12 +428,24 @@ namespace Microsoft.AspNetCore.HttpSys.Internal
internal char* pMutualAuthData;
}
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct HTTP_SSL_PROTOCOL_INFO
+ {
+ internal SslProtocols Protocol;
+ internal CipherAlgorithmType CipherType;
+ internal uint CipherStrength;
+ internal HashAlgorithmType HashType;
+ internal uint HashStrength;
+ internal ExchangeAlgorithmType KeyExchangeType;
+ internal uint KeyExchangeStrength;
+ }
+
[StructLayout(LayoutKind.Sequential)]
internal struct HTTP_REQUEST_INFO
{
internal HTTP_REQUEST_INFO_TYPE InfoType;
internal uint InfoLength;
- internal HTTP_REQUEST_AUTH_INFO* pInfo;
+ internal void* pInfo;
}
[Flags]
diff --git a/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs b/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs
index ef15d80394..b19bb1b4a6 100644
--- a/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs
+++ b/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs
@@ -184,10 +184,13 @@ namespace Microsoft.AspNetCore.HttpSys.Internal
{
var info = &requestInfo[i];
if (info != null
- && info->InfoType == HttpApiTypes.HTTP_REQUEST_INFO_TYPE.HttpRequestInfoTypeAuth
- && info->pInfo->AuthStatus == HttpApiTypes.HTTP_AUTH_STATUS.HttpAuthStatusSuccess)
+ && info->InfoType == HttpApiTypes.HTTP_REQUEST_INFO_TYPE.HttpRequestInfoTypeAuth)
{
- return true;
+ var authInfo = (HttpApiTypes.HTTP_REQUEST_AUTH_INFO*)info->pInfo;
+ if (authInfo->AuthStatus == HttpApiTypes.HTTP_AUTH_STATUS.HttpAuthStatusSuccess)
+ {
+ return true;
+ }
}
}
return false;
@@ -202,22 +205,44 @@ namespace Microsoft.AspNetCore.HttpSys.Internal
{
var info = &requestInfo[i];
if (info != null
- && info->InfoType == HttpApiTypes.HTTP_REQUEST_INFO_TYPE.HttpRequestInfoTypeAuth
- && info->pInfo->AuthStatus == HttpApiTypes.HTTP_AUTH_STATUS.HttpAuthStatusSuccess)
+ && info->InfoType == HttpApiTypes.HTTP_REQUEST_INFO_TYPE.HttpRequestInfoTypeAuth)
{
- // Duplicates AccessToken
- var identity = new WindowsIdentity(info->pInfo->AccessToken, GetAuthTypeFromRequest(info->pInfo->AuthType));
+ var authInfo = (HttpApiTypes.HTTP_REQUEST_AUTH_INFO*)info->pInfo;
+ if (authInfo->AuthStatus == HttpApiTypes.HTTP_AUTH_STATUS.HttpAuthStatusSuccess)
+ {
+ // Duplicates AccessToken
+ var identity = new WindowsIdentity(authInfo->AccessToken, GetAuthTypeFromRequest(authInfo->AuthType));
- // Close the original
- UnsafeNclNativeMethods.SafeNetHandles.CloseHandle(info->pInfo->AccessToken);
+ // Close the original
+ UnsafeNclNativeMethods.SafeNetHandles.CloseHandle(authInfo->AccessToken);
- return new WindowsPrincipal(identity);
+ return new WindowsPrincipal(identity);
+ }
}
}
return new WindowsPrincipal(WindowsIdentity.GetAnonymous()); // Anonymous / !IsAuthenticated
}
+ internal HttpApiTypes.HTTP_SSL_PROTOCOL_INFO GetTlsHandshake()
+ {
+ var requestInfo = NativeRequestV2->pRequestInfo;
+ var infoCount = NativeRequestV2->RequestInfoCount;
+
+ for (int i = 0; i < infoCount; i++)
+ {
+ var info = &requestInfo[i];
+ if (info != null
+ && info->InfoType == HttpApiTypes.HTTP_REQUEST_INFO_TYPE.HttpRequestInfoTypeSslProtocol)
+ {
+ var authInfo = (HttpApiTypes.HTTP_SSL_PROTOCOL_INFO*)info->pInfo;
+ return *authInfo;
+ }
+ }
+
+ return default;
+ }
+
private static string GetAuthTypeFromRequest(HttpApiTypes.HTTP_REQUEST_AUTH_TYPE input)
{
switch (input)