diff --git a/src/Servers/Connections.Abstractions/src/Features/ITlsHandshakeFeature.cs b/src/Servers/Connections.Abstractions/src/Features/ITlsHandshakeFeature.cs
index ba9af785b67f..15a99e0efc32 100644
--- a/src/Servers/Connections.Abstractions/src/Features/ITlsHandshakeFeature.cs
+++ b/src/Servers/Connections.Abstractions/src/Features/ITlsHandshakeFeature.cs
@@ -24,6 +24,12 @@ public interface ITlsHandshakeFeature
/// Gets the .
///
TlsCipherSuite? NegotiatedCipherSuite => null;
+
+ ///
+ /// Gets the host name from the "server_name" (SNI) extension of the client hello if present.
+ /// See RFC 6066.
+ ///
+ string? HostName => null;
#endif
///
diff --git a/src/Servers/Connections.Abstractions/src/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/Servers/Connections.Abstractions/src/PublicAPI/net8.0/PublicAPI.Unshipped.txt
index 9e361db01313..c689504701db 100644
--- a/src/Servers/Connections.Abstractions/src/PublicAPI/net8.0/PublicAPI.Unshipped.txt
+++ b/src/Servers/Connections.Abstractions/src/PublicAPI/net8.0/PublicAPI.Unshipped.txt
@@ -6,6 +6,7 @@ Microsoft.AspNetCore.Connections.Features.IConnectionMetricsTagsFeature
Microsoft.AspNetCore.Connections.Features.IConnectionMetricsTagsFeature.Tags.get -> System.Collections.Generic.ICollection>!
Microsoft.AspNetCore.Connections.Features.IConnectionNamedPipeFeature
Microsoft.AspNetCore.Connections.Features.IConnectionNamedPipeFeature.NamedPipe.get -> System.IO.Pipes.NamedPipeServerStream!
+Microsoft.AspNetCore.Connections.Features.ITlsHandshakeFeature.HostName.get -> string?
Microsoft.AspNetCore.Connections.Features.ITlsHandshakeFeature.NegotiatedCipherSuite.get -> System.Net.Security.TlsCipherSuite?
Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector
Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector.CanBind(System.Net.EndPoint! endpoint) -> bool
diff --git a/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs
index 1582c1217524..258d05ad5c87 100644
--- a/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs
+++ b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs
@@ -81,6 +81,10 @@ internal static unsafe partial uint HttpCreateRequestQueue(HTTPAPI_VERSION versi
internal static unsafe partial uint HttpDelegateRequestEx(SafeHandle pReqQueueHandle, SafeHandle pDelegateQueueHandle, ulong requestId,
ulong delegateUrlGroupId, ulong propertyInfoSetSize, HTTP_DELEGATE_REQUEST_PROPERTY_INFO* pRequestPropertyBuffer);
+ [LibraryImport(HTTPAPI, SetLastError = true)]
+ internal static unsafe partial uint HttpQueryRequestProperty(SafeHandle requestQueueHandle, ulong requestId, HTTP_REQUEST_PROPERTY propertyId,
+ void* qualifier, ulong qualifierSize, void* output, ulong outputSize, ulong* bytesReturned, IntPtr overlapped);
+
internal delegate uint HttpSetRequestPropertyInvoker(SafeHandle requestQueueHandle, ulong requestId, HTTP_REQUEST_PROPERTY propertyId, void* input, uint inputSize, IntPtr overlapped);
private static HTTPAPI_VERSION version;
diff --git a/src/Servers/HttpSys/src/RequestProcessing/Request.cs b/src/Servers/HttpSys/src/RequestProcessing/Request.cs
index 7c55263ffb2d..229b9bd8032f 100644
--- a/src/Servers/HttpSys/src/RequestProcessing/Request.cs
+++ b/src/Servers/HttpSys/src/RequestProcessing/Request.cs
@@ -327,6 +327,8 @@ private AspNetCore.HttpSys.Internal.SocketAddress LocalEndPoint
internal WindowsPrincipal User { get; }
+ public string? SniHostName { get; private set; }
+
public SslProtocols Protocol { get; private set; }
public CipherAlgorithmType CipherAlgorithm { get; private set; }
@@ -428,6 +430,9 @@ private void GetTlsHandshakeResults()
HashStrength = (int)handshake.HashStrength;
KeyExchangeAlgorithm = handshake.KeyExchangeType;
KeyExchangeStrength = (int)handshake.KeyExchangeStrength;
+
+ var sni = RequestContext.GetClientSni();
+ SniHostName = sni.Hostname;
}
public X509Certificate2? ClientCertificate
diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs
index a16281148ca6..95148c3070ed 100644
--- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs
+++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs
@@ -587,6 +587,8 @@ bool IHttpBodyControlFeature.AllowSynchronousIO
int ITlsHandshakeFeature.KeyExchangeStrength => Request.KeyExchangeStrength;
+ string? ITlsHandshakeFeature.HostName => Request.SniHostName;
+
IReadOnlyDictionary> IHttpSysRequestInfoFeature.RequestInfo => Request.RequestInfo;
ReadOnlySpan IHttpSysRequestTimingFeature.Timestamps => Request.RequestTimestamps;
diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs
index 6493488a5b1f..5120ce969ffa 100644
--- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs
+++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs
@@ -2,11 +2,13 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
+using System.Runtime.InteropServices;
using System.Security.Authentication.ExtendedProtection;
using System.Security.Principal;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpSys.Internal;
using Microsoft.Extensions.Logging;
+using static Microsoft.AspNetCore.HttpSys.Internal.UnsafeNclNativeMethods;
namespace Microsoft.AspNetCore.Server.HttpSys;
@@ -224,6 +226,31 @@ internal void ForceCancelRequest()
}
}
+ internal unsafe HttpApiTypes.HTTP_REQUEST_PROPERTY_SNI GetClientSni()
+ {
+ var buffer = new byte[HttpApiTypes.SniPropertySizeInBytes];
+ fixed (byte* pBuffer = buffer)
+ {
+ var statusCode = HttpApi.HttpQueryRequestProperty(
+ Server.RequestQueue.Handle,
+ RequestId,
+ HttpApiTypes.HTTP_REQUEST_PROPERTY.HttpRequestPropertySni,
+ qualifier: null,
+ qualifierSize: 0,
+ (void*)pBuffer,
+ (ulong)buffer.Length,
+ bytesReturned: null,
+ IntPtr.Zero);
+
+ if (statusCode == ErrorCodes.ERROR_SUCCESS)
+ {
+ return Marshal.PtrToStructure((IntPtr)pBuffer);
+ }
+ }
+
+ return default;
+ }
+
// You must still call ForceCancelRequest after this.
internal unsafe void SetResetCode(int errorCode)
{
diff --git a/src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs b/src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs
index fad737d8af9b..7b7586491e5a 100644
--- a/src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs
+++ b/src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
+using System.Data.Common;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
@@ -185,6 +186,12 @@ public async Task Https_SetsITlsHandshakeFeature()
var keyExchangeStrength = result.GetProperty("keyExchangeStrength").GetInt32();
Assert.True(keyExchangeStrength >= 0, "KeyExchangeStrength: " + keyExchangeStrength);
+
+ if (Environment.OSVersion.Version > new Version(10, 0, 19043, 0))
+ {
+ var hostName = result.GetProperty("hostName").ToString();
+ Assert.Equal("localhost", hostName);
+ }
}
}
diff --git a/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs b/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs
index d74a7266a3fc..53c24c80677b 100644
--- a/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs
@@ -42,7 +42,6 @@ public X509Certificate2? ClientCertificate
}
}
- // Used for event source, not part of any of the feature interfaces.
public string? HostName { get; set; }
public ReadOnlyMemory ApplicationProtocol => _sslStream.NegotiatedApplicationProtocol.Protocol;
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs
index cd589224aa11..7d7c61e5a477 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs
@@ -126,15 +126,25 @@ void ConfigureListenOptions(ListenOptions listenOptions)
[Fact]
public async Task HandshakeDetailsAreAvailable()
{
+ string expectedHostname = null;
void ConfigureListenOptions(ListenOptions listenOptions)
{
- listenOptions.UseHttps(new HttpsConnectionAdapterOptions { ServerCertificate = _x509Certificate2 });
+ listenOptions.UseHttps(
+ new HttpsConnectionAdapterOptions
+ {
+ ServerCertificateSelector = (connection, name) =>
+ {
+ expectedHostname = name;
+ return _x509Certificate2;
+ }
+ });
};
await using (var server = new TestServer(context =>
{
var tlsFeature = context.Features.Get();
Assert.NotNull(tlsFeature);
+ Assert.Equal(expectedHostname, tlsFeature.HostName);
Assert.True(tlsFeature.Protocol > SslProtocols.None, "Protocol");
Assert.True(tlsFeature.NegotiatedCipherSuite >= TlsCipherSuite.TLS_NULL_WITH_NULL_NULL, "NegotiatedCipherSuite");
Assert.True(tlsFeature.CipherAlgorithm > CipherAlgorithmType.Null, "Cipher");
diff --git a/src/Shared/HttpSys/NativeInterop/HttpApiTypes.cs b/src/Shared/HttpSys/NativeInterop/HttpApiTypes.cs
index 1ee230c27011..4e9f90351f3f 100644
--- a/src/Shared/HttpSys/NativeInterop/HttpApiTypes.cs
+++ b/src/Shared/HttpSys/NativeInterop/HttpApiTypes.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
+using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.Authentication;
using Microsoft.AspNetCore.Http;
@@ -108,6 +109,28 @@ internal struct HTTP_REQUEST_PROPERTY_STREAM_ERROR
internal uint ErrorCode;
}
+ internal const int HTTP_REQUEST_PROPERTY_SNI_HOST_MAX_LENGTH = 255;
+ internal const int SniPropertySizeInBytes = (sizeof(ushort) * (HTTP_REQUEST_PROPERTY_SNI_HOST_MAX_LENGTH + 1)) + sizeof(ulong);
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode, Size = SniPropertySizeInBytes)]
+ internal struct HTTP_REQUEST_PROPERTY_SNI
+ {
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = HTTP_REQUEST_PROPERTY_SNI_HOST_MAX_LENGTH + 1)]
+ internal string Hostname;
+
+ internal HTTP_REQUEST_PROPERTY_SNI_FLAGS Flags;
+ }
+
+ [Flags]
+ internal enum HTTP_REQUEST_PROPERTY_SNI_FLAGS : uint
+ {
+ // Indicates that SNI was used for successful endpoint lookup during handshake.
+ // If client sent the SNI but Http.sys still decided to use IP endpoint binding then this flag will not be set.
+ HTTP_REQUEST_PROPERTY_SNI_FLAG_SNI_USED = 0x00000001,
+ // Indicates that client did not send the SNI.
+ HTTP_REQUEST_PROPERTY_SNI_FLAG_NO_SNI = 0x00000002,
+ }
+
internal const int MaxTimeout = 6;
[StructLayout(LayoutKind.Sequential)]