Skip to content

Expose SNI hostname in ITlsHandshakeFeature for Kestrel/HttpSys #48572

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ public interface ITlsHandshakeFeature
/// Gets the <see cref="TlsCipherSuite"/>.
/// </summary>
TlsCipherSuite? NegotiatedCipherSuite => null;

/// <summary>
/// Gets the host name from the "server_name" (SNI) extension of the client hello if present.
/// See <see href="https://www.rfc-editor.org/rfc/rfc6066#section-3">RFC 6066</see>.
/// </summary>
string? HostName => null;
#endif

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Microsoft.AspNetCore.Connections.Features.IConnectionMetricsTagsFeature
Microsoft.AspNetCore.Connections.Features.IConnectionMetricsTagsFeature.Tags.get -> System.Collections.Generic.ICollection<System.Collections.Generic.KeyValuePair<string!, object?>>!
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
Expand Down
4 changes: 4 additions & 0 deletions src/Servers/HttpSys/src/NativeInterop/HttpApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions src/Servers/HttpSys/src/RequestProcessing/Request.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,8 @@ bool IHttpBodyControlFeature.AllowSynchronousIO

int ITlsHandshakeFeature.KeyExchangeStrength => Request.KeyExchangeStrength;

string? ITlsHandshakeFeature.HostName => Request.SniHostName;

IReadOnlyDictionary<int, ReadOnlyMemory<byte>> IHttpSysRequestInfoFeature.RequestInfo => Request.RequestInfo;

ReadOnlySpan<long> IHttpSysRequestTimingFeature.Timestamps => Request.RequestTimestamps;
Expand Down
27 changes: 27 additions & 0 deletions src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<HttpApiTypes.HTTP_REQUEST_PROPERTY_SNI>((IntPtr)pBuffer);
}
}

return default;
}

// You must still call ForceCancelRequest after this.
internal unsafe void SetResetCode(int errorCode)
{
Expand Down
7 changes: 7 additions & 0 deletions src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<byte> ApplicationProtocol => _sslStream.NegotiatedApplicationProtocol.Protocol;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ITlsHandshakeFeature>();
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");
Expand Down
23 changes: 23 additions & 0 deletions src/Shared/HttpSys/NativeInterop/HttpApiTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)]
Expand Down