diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs index 3742211a9a8d..ed61268897ef 100644 --- a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Program.cs @@ -1,60 +1,48 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; +using System.Diagnostics; using System.Reflection; using System.Runtime.InteropServices; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.HttpSys; using Microsoft.Extensions.Hosting; -using TlsFeatureObserve; using TlsFeaturesObserve.HttpSys; HttpSysConfigurator.ConfigureCacheTlsClientHello(); -CreateHostBuilder(args).Build().Run(); - -static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHost(webBuilder => - { - webBuilder.UseStartup() - .UseHttpSys(options => - { - // If you want to use https locally: https://stackoverflow.com/a/51841893 - options.UrlPrefixes.Add("https://*:6000"); // HTTPS - - options.Authentication.Schemes = AuthenticationSchemes.None; - options.Authentication.AllowAnonymous = true; - - options.TlsClientHelloBytesCallback = ProcessTlsClientHello; - }); - }); - -static void ProcessTlsClientHello(IFeatureCollection features, ReadOnlySpan tlsClientHelloBytes) -{ - var httpConnectionFeature = features.Get(); - - var myTlsFeature = new MyTlsFeature( - connectionId: httpConnectionFeature.ConnectionId, - tlsClientHelloLength: tlsClientHelloBytes.Length); - features.Set(myTlsFeature); -} +var builder = WebApplication.CreateBuilder(args); -public interface IMyTlsFeature +builder.WebHost.UseHttpSys(options => { - string ConnectionId { get; } - int TlsClientHelloLength { get; } -} + options.UrlPrefixes.Add("https://*:6000"); + options.Authentication.Schemes = AuthenticationSchemes.None; + options.Authentication.AllowAnonymous = true; +}); -public class MyTlsFeature : IMyTlsFeature +var app = builder.Build(); + +app.Use(async (context, next) => { - public string ConnectionId { get; } - public int TlsClientHelloLength { get; } - - public MyTlsFeature(string connectionId, int tlsClientHelloLength) - { - ConnectionId = connectionId; - TlsClientHelloLength = tlsClientHelloLength; - } -} + var connectionFeature = context.Features.GetRequiredFeature(); + var httpSysPropFeature = context.Features.GetRequiredFeature(); + + // first time invocation to find out required size + var success = httpSysPropFeature.TryGetTlsClientHello(Array.Empty(), out var bytesReturned); + Debug.Assert(!success); + Debug.Assert(bytesReturned > 0); + + // rent with enough memory span and invoke + var bytes = ArrayPool.Shared.Rent(bytesReturned); + success = httpSysPropFeature.TryGetTlsClientHello(bytes, out _); + Debug.Assert(success); + + await context.Response.WriteAsync($"[Response] connectionId={connectionFeature.ConnectionId}; tlsClientHello.length={bytesReturned}; tlsclienthello start={string.Join(' ', bytes.AsSpan(0, 30).ToArray())}"); + await next(context); +}); + +app.Run(); diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs b/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs deleted file mode 100644 index 8ba6d27aef98..000000000000 --- a/src/Servers/HttpSys/samples/TlsFeaturesObserve/Startup.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Connections.Features; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Server.HttpSys; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace TlsFeatureObserve; - -public class Startup -{ - public void Configure(IApplicationBuilder app) - { - app.Run(async (HttpContext context) => - { - context.Response.ContentType = "text/plain"; - - var tlsFeature = context.Features.Get(); - await context.Response.WriteAsync("TlsClientHello data: " + $"connectionId={tlsFeature?.ConnectionId}; length={tlsFeature?.TlsClientHelloLength}"); - }); - } -} diff --git a/src/Servers/HttpSys/samples/TlsFeaturesObserve/TlsFeaturesObserve.csproj b/src/Servers/HttpSys/samples/TlsFeaturesObserve/TlsFeaturesObserve.csproj index f65f8a98a72a..3ba4390b4e73 100644 --- a/src/Servers/HttpSys/samples/TlsFeaturesObserve/TlsFeaturesObserve.csproj +++ b/src/Servers/HttpSys/samples/TlsFeaturesObserve/TlsFeaturesObserve.csproj @@ -7,6 +7,7 @@ + diff --git a/src/Servers/HttpSys/src/HttpSysListener.cs b/src/Servers/HttpSys/src/HttpSysListener.cs index 02f595c98fe6..1963aceea284 100644 --- a/src/Servers/HttpSys/src/HttpSysListener.cs +++ b/src/Servers/HttpSys/src/HttpSysListener.cs @@ -5,7 +5,6 @@ using System.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpSys.Internal; -using Microsoft.AspNetCore.Server.HttpSys.RequestProcessing; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; using Windows.Win32; @@ -42,7 +41,6 @@ internal sealed partial class HttpSysListener : IDisposable private readonly UrlGroup _urlGroup; private readonly RequestQueue _requestQueue; private readonly DisconnectListener _disconnectListener; - private readonly TlsListener? _tlsListener; private readonly object _internalLock; @@ -76,12 +74,7 @@ public HttpSysListener(HttpSysOptions options, ILoggerFactory loggerFactory) _requestQueue = new RequestQueue(options.RequestQueueName, options.RequestQueueMode, options.RequestQueueSecurityDescriptor, Logger); _urlGroup = new UrlGroup(_serverSession, _requestQueue, Logger); - _disconnectListener = new DisconnectListener(_requestQueue, Logger); - if (options.TlsClientHelloBytesCallback is not null) - { - _tlsListener = new TlsListener(Logger, options.TlsClientHelloBytesCallback); - } } catch (Exception exception) { @@ -89,7 +82,6 @@ public HttpSysListener(HttpSysOptions options, ILoggerFactory loggerFactory) _requestQueue?.Dispose(); _urlGroup?.Dispose(); _serverSession?.Dispose(); - _tlsListener?.Dispose(); Log.HttpSysListenerCtorError(Logger, exception); throw; } @@ -106,7 +98,6 @@ internal enum State internal UrlGroup UrlGroup => _urlGroup; internal RequestQueue RequestQueue => _requestQueue; - internal TlsListener? TlsListener => _tlsListener; internal DisconnectListener DisconnectListener => _disconnectListener; public HttpSysOptions Options { get; } @@ -260,7 +251,6 @@ private void DisposeInternal() Debug.Assert(!_serverSession.Id.IsInvalid, "ServerSessionHandle is invalid in CloseV2Config"); _serverSession.Dispose(); - _tlsListener?.Dispose(); } /// diff --git a/src/Servers/HttpSys/src/HttpSysOptions.cs b/src/Servers/HttpSys/src/HttpSysOptions.cs index bb9cdc6954e4..3d47e5cea32d 100644 --- a/src/Servers/HttpSys/src/HttpSysOptions.cs +++ b/src/Servers/HttpSys/src/HttpSysOptions.cs @@ -250,17 +250,6 @@ public Http503VerbosityLevel Http503Verbosity /// public bool UseLatin1RequestHeaders { get; set; } - /// - /// A callback to be invoked to get the TLS client hello bytes. - /// Null by default. - /// - /// - /// Works only if HTTP_SERVICE_CONFIG_SSL_FLAG_ENABLE_CACHE_CLIENT_HELLO flag is set on http.sys service configuration. - /// See - /// and - /// - public Action>? TlsClientHelloBytesCallback { get; set; } - // Not called when attaching to an existing queue. internal void Apply(UrlGroup urlGroup, RequestQueue? requestQueue) { diff --git a/src/Servers/HttpSys/src/IHttpSysRequestPropertyFeature.cs b/src/Servers/HttpSys/src/IHttpSysRequestPropertyFeature.cs new file mode 100644 index 000000000000..392e11cb73e6 --- /dev/null +++ b/src/Servers/HttpSys/src/IHttpSysRequestPropertyFeature.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Server.HttpSys; + +/// +/// Provides API to read HTTP_REQUEST_PROPERTY value from the HTTP.SYS request. +/// +/// +public interface IHttpSysRequestPropertyFeature +{ + /// + /// Reads the TLS client hello from HTTP.SYS + /// + /// Where the raw bytes of the TLS Client Hello message are written. + /// + /// Returns the number of bytes written to . + /// Or can return the size of the buffer needed if wasn't large enough. + /// + /// + /// Works only if HTTP_SERVICE_CONFIG_SSL_FLAG_ENABLE_CACHE_CLIENT_HELLO flag is set on http.sys service configuration. + /// See + /// and + ///

+ /// If you don't want to guess the required size before first invocation, + /// you should first call with set to empty size, so that you can retrieve the required buffer size from , + /// then allocate that amount of memory and retry the query. + ///
+ /// + /// True, if fetching TLS client hello was successful, false if size is not large enough. + /// If unsuccessful for other reason throws an exception. + /// + /// Any HttpSys error except for ERROR_INSUFFICIENT_BUFFER or ERROR_MORE_DATA. + /// If HttpSys does not support querying the TLS Client Hello. + bool TryGetTlsClientHello(Span tlsClientHelloBytesDestination, out int bytesReturned); +} diff --git a/src/Servers/HttpSys/src/PublicAPI.Unshipped.txt b/src/Servers/HttpSys/src/PublicAPI.Unshipped.txt index 393f7b26a8f3..20bc39698c15 100644 --- a/src/Servers/HttpSys/src/PublicAPI.Unshipped.txt +++ b/src/Servers/HttpSys/src/PublicAPI.Unshipped.txt @@ -1,5 +1,5 @@ #nullable enable -Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions.TlsClientHelloBytesCallback.get -> System.Action>? -Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions.TlsClientHelloBytesCallback.set -> void Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions.RequestQueueSecurityDescriptor.get -> System.Security.AccessControl.GenericSecurityDescriptor? Microsoft.AspNetCore.Server.HttpSys.HttpSysOptions.RequestQueueSecurityDescriptor.set -> void +Microsoft.AspNetCore.Server.HttpSys.IHttpSysRequestPropertyFeature +Microsoft.AspNetCore.Server.HttpSys.IHttpSysRequestPropertyFeature.TryGetTlsClientHello(System.Span tlsClientHelloBytesDestination, out int bytesReturned) -> bool diff --git a/src/Servers/HttpSys/src/RequestProcessing/Request.cs b/src/Servers/HttpSys/src/RequestProcessing/Request.cs index 478a8a587db6..8e4babf7ca21 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/Request.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/Request.cs @@ -9,7 +9,6 @@ using System.Security.Cryptography.X509Certificates; using System.Security.Principal; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.AspNetCore.Shared; using Microsoft.Extensions.Logging; @@ -370,9 +369,6 @@ private void GetTlsHandshakeResults() SniHostName = sni.Hostname.ToString(); } - internal bool GetAndInvokeTlsClientHelloCallback(IFeatureCollection features, Action> tlsClientHelloBytesCallback) - => RequestContext.GetAndInvokeTlsClientHelloMessageBytesCallback(features, tlsClientHelloBytesCallback); - public X509Certificate2? ClientCertificate { get diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs index 2751a1025a89..1c80f92febc2 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs @@ -36,6 +36,7 @@ internal partial class RequestContext : IHttpResponseTrailersFeature, IHttpResetFeature, IHttpSysRequestDelegationFeature, + IHttpSysRequestPropertyFeature, IConnectionLifetimeNotificationFeature { private IFeatureCollection? _features; @@ -753,4 +754,9 @@ void IConnectionLifetimeNotificationFeature.RequestClose() Response.Headers[HeaderNames.Connection] = "close"; } } + + public bool TryGetTlsClientHello(Span tlsClientHelloBytesDestination, out int bytesReturned) + { + return TryGetTlsClientHelloMessageBytes(tlsClientHelloBytesDestination, out bytesReturned); + } } diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs index 9e34d23f8584..eba7d33ff3b8 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs @@ -1,11 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Buffers; using System.Runtime.InteropServices; using System.Security.Principal; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; @@ -223,88 +221,56 @@ internal void ForceCancelRequest() /// /// Attempts to get the client hello message bytes from the http.sys. - /// If not successful, will return false. + /// If successful writes the bytes into , and shows how many bytes were written in . + /// If not successful because is not large enough, returns false and shows a size of required in . + /// If not successful for other reason - throws exception with message/errorCode. /// - internal unsafe bool GetAndInvokeTlsClientHelloMessageBytesCallback(IFeatureCollection features, Action> tlsClientHelloBytesCallback) + internal unsafe bool TryGetTlsClientHelloMessageBytes( + Span destination, + out int bytesReturned) { + bytesReturned = default; if (!HttpApi.SupportsClientHello) { // not supported, so we just return and don't invoke the callback - return false; + throw new InvalidOperationException("Windows HTTP Server API does not support HTTP_FEATURE_ID.HttpFeatureCacheTlsClientHello or HttpQueryRequestProperty. See HTTP_FEATURE_ID for details."); } - uint bytesReturnedValue = 0; - uint* bytesReturned = &bytesReturnedValue; uint statusCode; - var requestId = PinsReleased ? Request.RequestId : RequestId; - // we will try with some "random" buffer size - var buffer = ArrayPool.Shared.Rent(512); - try - { - fixed (byte* pBuffer = buffer) - { - statusCode = HttpApi.HttpGetRequestProperty( - requestQueueHandle: Server.RequestQueue.Handle, - requestId, - propertyId: (HTTP_REQUEST_PROPERTY)11 /* HTTP_REQUEST_PROPERTY.HttpRequestPropertyTlsClientHello */, - qualifier: null, - qualifierSize: 0, - output: pBuffer, - outputSize: (uint)buffer.Length, - bytesReturned: (IntPtr)bytesReturned, - overlapped: IntPtr.Zero); - - if (statusCode is ErrorCodes.ERROR_SUCCESS) - { - tlsClientHelloBytesCallback(features, buffer.AsSpan(0, (int)bytesReturnedValue)); - return true; - } - } - } - finally - { - ArrayPool.Shared.Return(buffer, clearArray: true); - } + uint bytesReturnedValue = 0; + uint* bytesReturnedPointer = &bytesReturnedValue; - // if buffer supplied is too small, `bytesReturned` will have proper size - // so retry should succeed with the properly allocated buffer - if (statusCode is ErrorCodes.ERROR_MORE_DATA or ErrorCodes.ERROR_INSUFFICIENT_BUFFER) + fixed (byte* pBuffer = destination) { - try - { - var correctSize = (int)bytesReturnedValue; - buffer = ArrayPool.Shared.Rent(correctSize); + statusCode = HttpApi.HttpGetRequestProperty( + requestQueueHandle: Server.RequestQueue.Handle, + requestId, + propertyId: (HTTP_REQUEST_PROPERTY)11 /* HTTP_REQUEST_PROPERTY.HttpRequestPropertyTlsClientHello */, + qualifier: null, + qualifierSize: 0, + output: pBuffer, + outputSize: (uint)destination.Length, + bytesReturned: (IntPtr)bytesReturnedPointer, + overlapped: IntPtr.Zero); - fixed (byte* pBuffer = buffer) - { - statusCode = HttpApi.HttpGetRequestProperty( - requestQueueHandle: Server.RequestQueue.Handle, - requestId, - propertyId: (HTTP_REQUEST_PROPERTY)11 /* HTTP_REQUEST_PROPERTY.HttpRequestPropertyTlsClientHello */, - qualifier: null, - qualifierSize: 0, - output: pBuffer, - outputSize: (uint)buffer.Length, - bytesReturned: (IntPtr)bytesReturned, - overlapped: IntPtr.Zero); - - if (statusCode is ErrorCodes.ERROR_SUCCESS) - { - tlsClientHelloBytesCallback(features, buffer.AsSpan(0, correctSize)); - return true; - } - } + bytesReturned = checked((int)bytesReturnedValue); + + if (statusCode is ErrorCodes.ERROR_SUCCESS) + { + return true; } - finally + + // if buffer supplied is too small, `bytesReturned` has proper size + if (statusCode is ErrorCodes.ERROR_MORE_DATA or ErrorCodes.ERROR_INSUFFICIENT_BUFFER) { - ArrayPool.Shared.Return(buffer, clearArray: true); + return false; } } Log.TlsClientHelloRetrieveError(Logger, requestId, statusCode); - return false; + throw new HttpSysException((int)statusCode); } internal unsafe HTTP_REQUEST_PROPERTY_SNI GetClientSni() diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs index 399f1292d60d..2a1d06a06d26 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContextOfT.cs @@ -48,12 +48,6 @@ public override async Task ExecuteAsync() context = application.CreateContext(Features); try { - if (Server.Options.TlsClientHelloBytesCallback is not null && Server.TlsListener is not null - && Request.IsHttps) - { - Server.TlsListener.InvokeTlsClientHelloCallback(Request.RawConnectionId, Features, Request.GetAndInvokeTlsClientHelloCallback); - } - await application.ProcessRequestAsync(context); await CompleteAsync(); } diff --git a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.Log.cs b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.Log.cs deleted file mode 100644 index 20ffe5c74b6f..000000000000 --- a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.Log.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Extensions.Logging; - -namespace Microsoft.AspNetCore.Server.HttpSys.RequestProcessing; - -internal sealed partial class TlsListener : IDisposable -{ - private static partial class Log - { - [LoggerMessage(LoggerEventIds.TlsListenerError, LogLevel.Error, "Error during closed connection cleanup.", EventName = "TlsListenerCleanupClosedConnectionError")] - public static partial void CleanupClosedConnectionError(ILogger logger, Exception exception); - } -} diff --git a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs b/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs deleted file mode 100644 index 8e7edb9bb47d..000000000000 --- a/src/Servers/HttpSys/src/RequestProcessing/TlsListener.cs +++ /dev/null @@ -1,143 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Concurrent; -using System.Collections.ObjectModel; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.Extensions.Logging; - -namespace Microsoft.AspNetCore.Server.HttpSys.RequestProcessing; - -internal sealed partial class TlsListener : IDisposable -{ - private readonly ConcurrentDictionary _connectionTimestamps = new(); - private readonly Action> _tlsClientHelloBytesCallback; - private readonly ILogger _logger; - - private readonly PeriodicTimer _cleanupTimer; - private readonly Task _cleanupTask; - private readonly TimeProvider _timeProvider; - - private readonly TimeSpan ConnectionIdleTime = TimeSpan.FromMinutes(5); - private readonly TimeSpan CleanupDelay = TimeSpan.FromSeconds(10); - internal readonly int CacheSizeLimit = 1_000_000; - - // Internal for testing purposes - internal ReadOnlyDictionary ConnectionTimeStamps => _connectionTimestamps.AsReadOnly(); - - internal TlsListener(ILogger logger, Action> tlsClientHelloBytesCallback, TimeProvider? timeProvider = null) - { - if (AppContext.GetData("Microsoft.AspNetCore.Server.HttpSys.TlsListener.CacheSizeLimit") is int limit) - { - CacheSizeLimit = limit; - } - - if (AppContext.GetData("Microsoft.AspNetCore.Server.HttpSys.TlsListener.ConnectionIdleTime") is int idleTime) - { - ConnectionIdleTime = TimeSpan.FromSeconds(idleTime); - } - - if (AppContext.GetData("Microsoft.AspNetCore.Server.HttpSys.TlsListener.CleanupDelay") is int cleanupDelay) - { - CleanupDelay = TimeSpan.FromSeconds(cleanupDelay); - } - - _logger = logger; - _tlsClientHelloBytesCallback = tlsClientHelloBytesCallback; - - _timeProvider = timeProvider ?? TimeProvider.System; - _cleanupTimer = new PeriodicTimer(CleanupDelay, _timeProvider); - _cleanupTask = CleanupLoopAsync(); - } - - // Method looks weird because we want it to be testable by not directly requiring a Request object - internal void InvokeTlsClientHelloCallback(ulong connectionId, IFeatureCollection features, - Func>, bool> invokeTlsClientHelloCallback) - { - if (!_connectionTimestamps.TryAdd(connectionId, _timeProvider.GetUtcNow())) - { - // update TTL - _connectionTimestamps[connectionId] = _timeProvider.GetUtcNow(); - return; - } - - _ = invokeTlsClientHelloCallback(features, _tlsClientHelloBytesCallback); - } - - internal async Task CleanupLoopAsync() - { - while (await _cleanupTimer.WaitForNextTickAsync()) - { - try - { - var now = _timeProvider.GetUtcNow(); - - // Remove idle connections - foreach (var kvp in _connectionTimestamps) - { - if (now - kvp.Value >= ConnectionIdleTime) - { - _connectionTimestamps.TryRemove(kvp.Key, out _); - } - } - - // Evict oldest items if above CacheSizeLimit - var currentCount = _connectionTimestamps.Count; - if (currentCount > CacheSizeLimit) - { - var excessCount = currentCount - CacheSizeLimit; - - // Find the oldest items in a single pass - var oldestTimestamps = new SortedSet>(TimeComparer.Instance); - - foreach (var kvp in _connectionTimestamps) - { - if (oldestTimestamps.Count < excessCount) - { - oldestTimestamps.Add(new KeyValuePair(kvp.Key, kvp.Value)); - } - else if (kvp.Value < oldestTimestamps.Max.Value) - { - oldestTimestamps.Remove(oldestTimestamps.Max); - oldestTimestamps.Add(new KeyValuePair(kvp.Key, kvp.Value)); - } - } - - // Remove the oldest keys - foreach (var item in oldestTimestamps) - { - _connectionTimestamps.TryRemove(item.Key, out _); - } - } - } - catch (Exception ex) - { - Log.CleanupClosedConnectionError(_logger, ex); - } - } - } - - public void Dispose() - { - _cleanupTimer.Dispose(); - _cleanupTask.Wait(); - } - - private sealed class TimeComparer : IComparer> - { - public static TimeComparer Instance { get; } = new TimeComparer(); - - public int Compare(KeyValuePair x, KeyValuePair y) - { - // Compare timestamps first - int timestampComparison = x.Value.CompareTo(y.Value); - if (timestampComparison != 0) - { - return timestampComparison; - } - - // Use the key as a tiebreaker to ensure uniqueness - return x.Key.CompareTo(y.Key); - } - } -} diff --git a/src/Servers/HttpSys/src/StandardFeatureCollection.cs b/src/Servers/HttpSys/src/StandardFeatureCollection.cs index dda57166921e..1c7d078d8253 100644 --- a/src/Servers/HttpSys/src/StandardFeatureCollection.cs +++ b/src/Servers/HttpSys/src/StandardFeatureCollection.cs @@ -27,6 +27,7 @@ internal sealed class StandardFeatureCollection : IFeatureCollection { typeof(IHttpBodyControlFeature), _identityFunc }, { typeof(IHttpSysRequestInfoFeature), _identityFunc }, { typeof(IHttpSysRequestTimingFeature), _identityFunc }, + { typeof(IHttpSysRequestPropertyFeature), _identityFunc }, { typeof(IHttpResponseTrailersFeature), ctx => ctx.GetResponseTrailersFeature() }, { typeof(IHttpResetFeature), ctx => ctx.GetResetFeature() }, { typeof(IConnectionLifetimeNotificationFeature), ctx => ctx.GetConnectionLifetimeNotificationFeature() }, diff --git a/src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs b/src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs index 0d3610cc4cfb..9b2b4134c681 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs @@ -239,6 +239,28 @@ public async Task Https_ITlsHandshakeFeature_MatchesIHttpSysExtensionInfoFeature } } + [ConditionalFact] + [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_20H2)] + public async Task Https_SetsIHttpSysRequestPropertyFeature() + { + using (Utilities.CreateDynamicHttpsServer(out var address, async httpContext => + { + try + { + var requestPropertyFeature = httpContext.Features.Get(); + Assert.NotNull(requestPropertyFeature); + } + catch (Exception ex) + { + await httpContext.Response.WriteAsync(ex.ToString()); + } + }, LoggerFactory)) + { + string response = await SendRequestAsync(address); + Assert.Equal(string.Empty, response); + } + } + [ConditionalFact] [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_20H2)] public async Task Https_SetsIHttpSysRequestTimingFeature() diff --git a/src/Servers/HttpSys/test/FunctionalTests/TlsListenerTests.cs b/src/Servers/HttpSys/test/FunctionalTests/TlsListenerTests.cs deleted file mode 100644 index d0ff2731a017..000000000000 --- a/src/Servers/HttpSys/test/FunctionalTests/TlsListenerTests.cs +++ /dev/null @@ -1,140 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Server.HttpSys.RequestProcessing; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Time.Testing; -using Moq; - -namespace Microsoft.AspNetCore.Server.HttpSys.FunctionalTests; - -public class TlsListenerTests -{ - [Fact] - public void AddsAndUpdatesConnectionTimestamps() - { - // Arrange - var logger = Mock.Of(); - var timeProvider = new FakeTimeProvider(); - var callbackInvoked = false; - var tlsListener = new TlsListener(logger, (_, __) => { callbackInvoked = true; }, timeProvider); - - var features = Mock.Of(); - - // Act - tlsListener.InvokeTlsClientHelloCallback(connectionId: 1UL, features, - invokeTlsClientHelloCallback: (f, cb) => { cb(f, ReadOnlySpan.Empty); return true; }); - - var originalTime = timeProvider.GetUtcNow(); - - // Assert - Assert.True(callbackInvoked); - Assert.Equal(originalTime, Assert.Single(tlsListener.ConnectionTimeStamps).Value); - - timeProvider.Advance(TimeSpan.FromSeconds(1)); - callbackInvoked = false; - // Update the timestamp - tlsListener.InvokeTlsClientHelloCallback(connectionId: 1UL, features, - invokeTlsClientHelloCallback: (f, cb) => { cb(f, ReadOnlySpan.Empty); return true; }); - - // Callback should not be invoked again and the timestamp should be updated - Assert.False(callbackInvoked); - Assert.Equal(timeProvider.GetUtcNow(), Assert.Single(tlsListener.ConnectionTimeStamps).Value); - Assert.NotEqual(originalTime, timeProvider.GetUtcNow()); - } - - [Fact] - public async Task RemovesIdleConnections() - { - // Arrange - var logger = Mock.Of(); - var timeProvider = new FakeTimeProvider(); - using var tlsListener = new TlsListener(logger, (_, __) => { }, timeProvider); - - var features = Mock.Of(); - - bool InvokeCallback(IFeatureCollection f, Action> cb) - { - cb(f, ReadOnlySpan.Empty); - return true; - } - - // Act - tlsListener.InvokeTlsClientHelloCallback(connectionId: 1UL, features, InvokeCallback); - - // 1 less minute than the idle time cleanup - timeProvider.Advance(TimeSpan.FromMinutes(4)); - Assert.Single(tlsListener.ConnectionTimeStamps); - - tlsListener.InvokeTlsClientHelloCallback(connectionId: 2UL, features, InvokeCallback); - Assert.Equal(2, tlsListener.ConnectionTimeStamps.Count); - - // With the previous 4 minutes, this should be 5 minutes and remove the first connection - timeProvider.Advance(TimeSpan.FromMinutes(1)); - - var timeout = TimeSpan.FromSeconds(5); - while (timeout > TimeSpan.Zero) - { - // Wait for the cleanup loop to run - if (tlsListener.ConnectionTimeStamps.Count == 1) - { - break; - } - timeout -= TimeSpan.FromMilliseconds(100); - await Task.Delay(100); - } - - // Assert - Assert.Single(tlsListener.ConnectionTimeStamps); - Assert.Contains(2UL, tlsListener.ConnectionTimeStamps.Keys); - } - - [Fact] - public async Task EvictsOldestConnectionsWhenExceedingCacheSizeLimit() - { - // Arrange - var logger = Mock.Of(); - var timeProvider = new FakeTimeProvider(); - var tlsListener = new TlsListener(logger, (_, __) => { }, timeProvider); - var features = Mock.Of(); - - ulong i = 0; - for (; i < (ulong)tlsListener.CacheSizeLimit; i++) - { - tlsListener.InvokeTlsClientHelloCallback(i, features, (f, cb) => { cb(f, ReadOnlySpan.Empty); return true; }); - } - - timeProvider.Advance(TimeSpan.FromSeconds(5)); - - for (; i < (ulong)tlsListener.CacheSizeLimit + 3; i++) - { - tlsListener.InvokeTlsClientHelloCallback(i, features, (f, cb) => { cb(f, ReadOnlySpan.Empty); return true; }); - } - - // 'touch' first connection to update its timestamp - tlsListener.InvokeTlsClientHelloCallback(0, features, (f, cb) => { cb(f, ReadOnlySpan.Empty); return true; }); - - // Make sure the cleanup loop has run to evict items since we're above the cache size limit - timeProvider.Advance(TimeSpan.FromMinutes(1)); - - var timeout = TimeSpan.FromSeconds(5); - while (timeout > TimeSpan.Zero) - { - // Wait for the cleanup loop to run - if (tlsListener.ConnectionTimeStamps.Count == tlsListener.CacheSizeLimit) - { - break; - } - timeout -= TimeSpan.FromMilliseconds(100); - await Task.Delay(100); - } - - Assert.Equal(tlsListener.CacheSizeLimit, tlsListener.ConnectionTimeStamps.Count); - Assert.Contains(0UL, tlsListener.ConnectionTimeStamps.Keys); - // 3 newest connections should be present - Assert.Contains(i - 1, tlsListener.ConnectionTimeStamps.Keys); - Assert.Contains(i - 2, tlsListener.ConnectionTimeStamps.Keys); - Assert.Contains(i - 3, tlsListener.ConnectionTimeStamps.Keys); - } -}