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)]