From 06636008237a70932d47a46f80dfe7d95ffb41f9 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 20 Oct 2022 13:16:32 +0800 Subject: [PATCH 1/6] Support multiple transports in Kestrel --- .../src/IConnectionListenerFactory.cs | 9 ++ .../IMultiplexedConnectionListenerFactory.cs | 9 ++ .../PublicAPI/net7.0/PublicAPI.Unshipped.txt | 2 + .../Infrastructure/TransportManager.cs | 44 ++++--- .../Core/src/Internal/KestrelServerImpl.cs | 26 ++--- .../Kestrel/Core/test/KestrelServerTests.cs | 109 ++++++++++++++++++ 6 files changed, 172 insertions(+), 27 deletions(-) diff --git a/src/Servers/Connections.Abstractions/src/IConnectionListenerFactory.cs b/src/Servers/Connections.Abstractions/src/IConnectionListenerFactory.cs index c1570ba4ab11..337f848f17a6 100644 --- a/src/Servers/Connections.Abstractions/src/IConnectionListenerFactory.cs +++ b/src/Servers/Connections.Abstractions/src/IConnectionListenerFactory.cs @@ -19,4 +19,13 @@ public interface IConnectionListenerFactory /// The token to monitor for cancellation requests. /// A that completes when the listener has been bound, yielding a representing the new listener. ValueTask BindAsync(EndPoint endpoint, CancellationToken cancellationToken = default); + +#if NET7_0_OR_GREATER + /// + /// Returns a value that indicates whether the listener factory supports binding to the specified . + /// + /// The to bind to. + /// A value that indicates whether the listener factory supports binding to the specified . + public bool CanBind(EndPoint endpoint) => true; +#endif } diff --git a/src/Servers/Connections.Abstractions/src/IMultiplexedConnectionListenerFactory.cs b/src/Servers/Connections.Abstractions/src/IMultiplexedConnectionListenerFactory.cs index f4fba698b122..06af55f6853f 100644 --- a/src/Servers/Connections.Abstractions/src/IMultiplexedConnectionListenerFactory.cs +++ b/src/Servers/Connections.Abstractions/src/IMultiplexedConnectionListenerFactory.cs @@ -21,4 +21,13 @@ public interface IMultiplexedConnectionListenerFactory /// The token to monitor for cancellation requests. /// A that completes when the listener has been bound, yielding a representing the new listener. ValueTask BindAsync(EndPoint endpoint, IFeatureCollection? features = null, CancellationToken cancellationToken = default); + +#if NET7_0_OR_GREATER + /// + /// Returns a value that indicates whether the listener factory supports binding to the specified . + /// + /// The to bind to. + /// A value that indicates whether the listener factory supports binding to the specified . + public bool CanBind(EndPoint endpoint) => true; +#endif } diff --git a/src/Servers/Connections.Abstractions/src/PublicAPI/net7.0/PublicAPI.Unshipped.txt b/src/Servers/Connections.Abstractions/src/PublicAPI/net7.0/PublicAPI.Unshipped.txt index 4dd8553d83c1..f91ba371573b 100644 --- a/src/Servers/Connections.Abstractions/src/PublicAPI/net7.0/PublicAPI.Unshipped.txt +++ b/src/Servers/Connections.Abstractions/src/PublicAPI/net7.0/PublicAPI.Unshipped.txt @@ -1,6 +1,8 @@ #nullable enable Microsoft.AspNetCore.Connections.Features.IStreamClosedFeature Microsoft.AspNetCore.Connections.Features.IStreamClosedFeature.OnClosed(System.Action! callback, object? state) -> void +Microsoft.AspNetCore.Connections.IConnectionListenerFactory.CanBind(System.Net.EndPoint! endpoint) -> bool +Microsoft.AspNetCore.Connections.IMultiplexedConnectionListenerFactory.CanBind(System.Net.EndPoint! endpoint) -> bool Microsoft.AspNetCore.Connections.TlsConnectionCallbackContext Microsoft.AspNetCore.Connections.TlsConnectionCallbackContext.ClientHelloInfo.get -> System.Net.Security.SslClientHelloInfo Microsoft.AspNetCore.Connections.TlsConnectionCallbackContext.ClientHelloInfo.set -> void diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs index b1871b6d5ca8..df0db56cf866 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs @@ -18,17 +18,17 @@ internal sealed class TransportManager { private readonly List _transports = new List(); - private readonly IConnectionListenerFactory? _transportFactory; - private readonly IMultiplexedConnectionListenerFactory? _multiplexedTransportFactory; + private readonly List _transportFactories; + private readonly List _multiplexedTransportFactories; private readonly ServiceContext _serviceContext; public TransportManager( - IConnectionListenerFactory? transportFactory, - IMultiplexedConnectionListenerFactory? multiplexedTransportFactory, + List transportFactories, + List multiplexedTransportFactories, ServiceContext serviceContext) { - _transportFactory = transportFactory; - _multiplexedTransportFactory = multiplexedTransportFactory; + _transportFactories = transportFactories; + _multiplexedTransportFactories = multiplexedTransportFactories; _serviceContext = serviceContext; } @@ -37,19 +37,27 @@ public TransportManager( public async Task BindAsync(EndPoint endPoint, ConnectionDelegate connectionDelegate, EndpointConfig? endpointConfig, CancellationToken cancellationToken) { - if (_transportFactory is null) + if (_transportFactories.Count == 0) { throw new InvalidOperationException($"Cannot bind with {nameof(ConnectionDelegate)} no {nameof(IConnectionListenerFactory)} is registered."); } - var transport = await _transportFactory.BindAsync(endPoint, cancellationToken).ConfigureAwait(false); - StartAcceptLoop(new GenericConnectionListener(transport), c => connectionDelegate(c), endpointConfig); - return transport.EndPoint; + foreach (var transportFactory in _transportFactories) + { + if (transportFactory.CanBind(endPoint)) + { + var transport = await transportFactory.BindAsync(endPoint, cancellationToken).ConfigureAwait(false); + StartAcceptLoop(new GenericConnectionListener(transport), c => connectionDelegate(c), endpointConfig); + return transport.EndPoint; + } + } + + throw new InvalidOperationException($"No registered {nameof(IConnectionListenerFactory)} supports endpoint: {endPoint}"); } public async Task BindAsync(EndPoint endPoint, MultiplexedConnectionDelegate multiplexedConnectionDelegate, ListenOptions listenOptions, CancellationToken cancellationToken) { - if (_multiplexedTransportFactory is null) + if (_multiplexedTransportFactories.Count == 0) { throw new InvalidOperationException($"Cannot bind with {nameof(MultiplexedConnectionDelegate)} no {nameof(IMultiplexedConnectionListenerFactory)} is registered."); } @@ -87,9 +95,17 @@ public async Task BindAsync(EndPoint endPoint, MultiplexedConnectionDe }); } - var transport = await _multiplexedTransportFactory.BindAsync(endPoint, features, cancellationToken).ConfigureAwait(false); - StartAcceptLoop(new GenericMultiplexedConnectionListener(transport), c => multiplexedConnectionDelegate(c), listenOptions.EndpointConfig); - return transport.EndPoint; + foreach (var multiplexedTransportFactory in _multiplexedTransportFactories) + { + if (multiplexedTransportFactory.CanBind(endPoint)) + { + var transport = await multiplexedTransportFactory.BindAsync(endPoint, features, cancellationToken).ConfigureAwait(false); + StartAcceptLoop(new GenericMultiplexedConnectionListener(transport), c => multiplexedConnectionDelegate(c), listenOptions.EndpointConfig); + return transport.EndPoint; + } + } + + throw new InvalidOperationException($"No registered {nameof(IMultiplexedConnectionListenerFactory)} supports endpoint: {endPoint}"); } /// diff --git a/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs b/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs index 1063849f601a..ea9d14ee13d7 100644 --- a/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs +++ b/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs @@ -22,8 +22,8 @@ internal sealed class KestrelServerImpl : IServer { private readonly ServerAddressesFeature _serverAddresses; private readonly TransportManager _transportManager; - private readonly IConnectionListenerFactory? _transportFactory; - private readonly IMultiplexedConnectionListenerFactory? _multiplexedTransportFactory; + private readonly List _transportFactories; + private readonly List _multiplexedTransportFactories; private readonly SemaphoreSlim _bindSemaphore = new SemaphoreSlim(initialCount: 1); private bool _hasStarted; @@ -37,7 +37,7 @@ public KestrelServerImpl( IOptions options, IEnumerable transportFactories, ILoggerFactory loggerFactory) - : this(transportFactories, null, CreateServiceContext(options, loggerFactory, null)) + : this(transportFactories, Array.Empty(), CreateServiceContext(options, loggerFactory, null)) { } @@ -62,22 +62,22 @@ public KestrelServerImpl( // For testing internal KestrelServerImpl(IConnectionListenerFactory transportFactory, ServiceContext serviceContext) - : this(new[] { transportFactory }, null, serviceContext) + : this(new[] { transportFactory }, Array.Empty(), serviceContext) { } // For testing internal KestrelServerImpl( IEnumerable transportFactories, - IEnumerable? multiplexedFactories, + IEnumerable multiplexedFactories, ServiceContext serviceContext) { ArgumentNullException.ThrowIfNull(transportFactories); - _transportFactory = transportFactories.LastOrDefault(); - _multiplexedTransportFactory = multiplexedFactories?.LastOrDefault(); + _transportFactories = transportFactories.Reverse().ToList(); + _multiplexedTransportFactories = multiplexedFactories.Reverse().ToList(); - if (_transportFactory == null && _multiplexedTransportFactory == null) + if (_transportFactories.Count == 0 && _multiplexedTransportFactories.Count == 0) { throw new InvalidOperationException(CoreStrings.TransportNotFound); } @@ -88,7 +88,7 @@ internal KestrelServerImpl( _serverAddresses = new ServerAddressesFeature(); Features.Set(_serverAddresses); - _transportManager = new TransportManager(_transportFactory, _multiplexedTransportFactory, ServiceContext); + _transportManager = new TransportManager(_transportFactories, _multiplexedTransportFactories, ServiceContext); HttpCharacters.Initialize(); } @@ -177,14 +177,14 @@ async Task OnBind(ListenOptions options, CancellationToken onBindCancellationTok } // Quic isn't registered if it's not supported, throw if we can't fall back to 1 or 2 - if (hasHttp3 && _multiplexedTransportFactory is null && !(hasHttp1 || hasHttp2)) + if (hasHttp3 && _multiplexedTransportFactories.Count == 0 && !(hasHttp1 || hasHttp2)) { throw new InvalidOperationException("This platform doesn't support QUIC or HTTP/3."); } // Disable adding alt-svc header if endpoint has configured not to or there is no // multiplexed transport factory, which happens if QUIC isn't supported. - var addAltSvcHeader = !options.DisableAltSvcHeader && _multiplexedTransportFactory != null; + var addAltSvcHeader = !options.DisableAltSvcHeader && _multiplexedTransportFactories.Count > 0; var configuredEndpoint = options.EndPoint; @@ -193,7 +193,7 @@ async Task OnBind(ListenOptions options, CancellationToken onBindCancellationTok || options.Protocols == HttpProtocols.None) // TODO a test fails because it doesn't throw an exception in the right place // when there is no HttpProtocols in KestrelServer, can we remove/change the test? { - if (_transportFactory is null) + if (_transportFactories.Count == 0) { throw new InvalidOperationException($"Cannot start HTTP/1.x or HTTP/2 server if no {nameof(IConnectionListenerFactory)} is registered."); } @@ -207,7 +207,7 @@ async Task OnBind(ListenOptions options, CancellationToken onBindCancellationTok options.EndPoint = await _transportManager.BindAsync(configuredEndpoint, connectionDelegate, options.EndpointConfig, onBindCancellationToken).ConfigureAwait(false); } - if (hasHttp3 && _multiplexedTransportFactory is not null) + if (hasHttp3 && _multiplexedTransportFactories.Count > 0) { // Check if a previous transport has changed the endpoint. If it has then the endpoint is dynamic and we can't guarantee it will work for other transports. // For more details, see https://github.com/dotnet/aspnetcore/issues/42982 diff --git a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs index 8e7c282d2e41..2eed8ba7c462 100644 --- a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs +++ b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs @@ -243,6 +243,90 @@ public void StartWithMultipleTransportFactoriesDoesNotThrow() StartDummyApplication(server); } + [Fact] + public async Task StartWithNoValidTransportFactoryThrows() + { + var serverOptions = CreateServerOptions(); + serverOptions.Listen(new IPEndPoint(IPAddress.Loopback, 0)); + + var server = new KestrelServerImpl( + Options.Create(serverOptions), + new List { new NonBindableTransportFactory() }, + new LoggerFactory(new[] { new KestrelTestLoggerProvider() })); + + var exception = await Assert.ThrowsAsync( + async () => await server.StartAsync(new DummyApplication(context => Task.CompletedTask), CancellationToken.None)); + + Assert.Equal("No registered IConnectionListenerFactory supports endpoint: 127.0.0.1:0", exception.Message); + } + + [Fact] + public async Task StartWithMultipleTransportFactories_UseSupported() + { + var endpoint = new IPEndPoint(IPAddress.Loopback, 0); + var serverOptions = CreateServerOptions(); + serverOptions.Listen(endpoint); + + var transportFactory = new MockTransportFactory(); + + var server = new KestrelServerImpl( + Options.Create(serverOptions), + new List { transportFactory, new NonBindableTransportFactory() }, + new LoggerFactory(new[] { new KestrelTestLoggerProvider() })); + + await server.StartAsync(new DummyApplication(context => Task.CompletedTask), CancellationToken.None); + + Assert.Collection(transportFactory.BoundEndPoints, + ep => Assert.Equal(endpoint, ep.OriginalEndPoint)); + } + + [Fact] + public async Task StartWithNoValidTransportFactoryThrows_Http3() + { + var serverOptions = CreateServerOptions(); + serverOptions.Listen(new IPEndPoint(IPAddress.Loopback, 0), c => + { + c.Protocols = HttpProtocols.Http3; + c.UseHttps(); + }); + + var server = new KestrelServerImpl( + Options.Create(serverOptions), + new List(), + new List { new NonBindableMultiplexedTransportFactory() }, + new LoggerFactory(new[] { new KestrelTestLoggerProvider() })); + + var exception = await Assert.ThrowsAsync( + async () => await server.StartAsync(new DummyApplication(context => Task.CompletedTask), CancellationToken.None)); + + Assert.Equal("No registered IMultiplexedConnectionListenerFactory supports endpoint: 127.0.0.1:0", exception.Message); + } + + [Fact] + public async Task StartWithMultipleTransportFactories_Http3_UseSupported() + { + var endpoint = new IPEndPoint(IPAddress.Loopback, 0); + var serverOptions = CreateServerOptions(); + serverOptions.Listen(endpoint, c => + { + c.Protocols = HttpProtocols.Http3; + c.UseHttps(); + }); + + var transportFactory = new MockMultiplexedTransportFactory(); + + var server = new KestrelServerImpl( + Options.Create(serverOptions), + new List(), + new List { transportFactory, new NonBindableMultiplexedTransportFactory() }, + new LoggerFactory(new[] { new KestrelTestLoggerProvider() })); + + await server.StartAsync(new DummyApplication(context => Task.CompletedTask), CancellationToken.None); + + Assert.Collection(transportFactory.BoundEndPoints, + ep => Assert.Equal(endpoint, ep.OriginalEndPoint)); + } + [Fact] public async Task ListenWithCustomEndpoint_DoesNotThrow() { @@ -391,6 +475,7 @@ public async Task StopAsyncCallsCompleteWhenFirstCallCompletes() var mockTransport = new Mock(); var mockTransportFactory = new Mock(); + mockTransportFactory.Setup(m => m.CanBind(It.IsAny())).Returns(true); mockTransportFactory .Setup(transportFactory => transportFactory.BindAsync(It.IsAny(), It.IsAny())) .Returns((e, token) => @@ -448,6 +533,7 @@ public async Task StopAsyncCallsCompleteWithThrownException() var mockTransport = new Mock(); var mockTransportFactory = new Mock(); + mockTransportFactory.Setup(m => m.CanBind(It.IsAny())).Returns(true); mockTransportFactory .Setup(transportFactory => transportFactory.BindAsync(It.IsAny(), It.IsAny())) .Returns((e, token) => @@ -507,6 +593,7 @@ public async Task StopAsyncDispatchesSubsequentStopAsyncContinuations() var mockTransport = new Mock(); var mockTransportFactory = new Mock(); + mockTransportFactory.Setup(m => m.CanBind(It.IsAny())).Returns(true); mockTransportFactory .Setup(transportFactory => transportFactory.BindAsync(It.IsAny(), It.IsAny())) .Returns((e, token) => @@ -636,6 +723,7 @@ public async Task ReloadsOnConfigurationChangeWhenOptedIn() var mockTransports = new List>(); var mockTransportFactory = new Mock(); + mockTransportFactory.Setup(m => m.CanBind(It.IsAny())).Returns(true); mockTransportFactory .Setup(transportFactory => transportFactory.BindAsync(It.IsAny(), It.IsAny())) .Returns((e, token) => @@ -773,6 +861,7 @@ public async Task DoesNotReloadOnConfigurationChangeByDefault() var mockTransports = new List>(); var mockTransportFactory = new Mock(); + mockTransportFactory.Setup(m => m.CanBind(It.IsAny())).Returns(true); mockTransportFactory .Setup(transportFactory => transportFactory.BindAsync(It.IsAny(), It.IsAny())) .Returns((e, token) => @@ -850,6 +939,26 @@ public ValueTask BindAsync(EndPoint endpoint, CancellationT } } + private class NonBindableTransportFactory : IConnectionListenerFactory + { + public ValueTask BindAsync(EndPoint endpoint, CancellationToken cancellationToken = default) + { + throw new InvalidOperationException(); + } + + bool IConnectionListenerFactory.CanBind(EndPoint endpoint) => false; + } + + private class NonBindableMultiplexedTransportFactory : IMultiplexedConnectionListenerFactory + { + public ValueTask BindAsync(EndPoint endpoint, IFeatureCollection features = null, CancellationToken cancellationToken = default) + { + throw new InvalidOperationException(); + } + + bool IMultiplexedConnectionListenerFactory.CanBind(EndPoint endpoint) => false; + } + private class MockMultiplexedTransportFactory : IMultiplexedConnectionListenerFactory { public List BoundEndPoints { get; } = new List(); From 7ae48dd40c94379208ffb4ff32ff82ca84936c4f Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 20 Oct 2022 17:17:38 +0800 Subject: [PATCH 2/6] Fix build --- src/Servers/Kestrel/Core/test/KestrelServerTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs index 2eed8ba7c462..5594255a995a 100644 --- a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs +++ b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs @@ -287,7 +287,7 @@ public async Task StartWithNoValidTransportFactoryThrows_Http3() serverOptions.Listen(new IPEndPoint(IPAddress.Loopback, 0), c => { c.Protocols = HttpProtocols.Http3; - c.UseHttps(); + c.UseHttps(TestResources.GetTestCertificate()); }); var server = new KestrelServerImpl( @@ -310,7 +310,7 @@ public async Task StartWithMultipleTransportFactories_Http3_UseSupported() serverOptions.Listen(endpoint, c => { c.Protocols = HttpProtocols.Http3; - c.UseHttps(); + c.UseHttps(TestResources.GetTestCertificate()); }); var transportFactory = new MockMultiplexedTransportFactory(); From 0e2b68036b9f64b18aeea359c29398913fa3ab6a Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Fri, 28 Oct 2022 14:17:32 +0800 Subject: [PATCH 3/6] React to code review changes --- .../src/IConnectionListenerFactory.cs | 9 --------- .../src/IConnectionListenerFactorySelector.cs | 19 +++++++++++++++++++ .../IMultiplexedConnectionListenerFactory.cs | 9 --------- .../PublicAPI/net462/PublicAPI.Unshipped.txt | 2 ++ .../PublicAPI/net7.0/PublicAPI.Unshipped.txt | 4 ++-- .../netstandard2.0/PublicAPI.Unshipped.txt | 2 ++ .../netstandard2.1/PublicAPI.Unshipped.txt | 2 ++ .../Infrastructure/TransportManager.cs | 12 ++++++++++-- .../Kestrel/Core/test/KestrelServerTests.cs | 19 ++++++++++--------- 9 files changed, 47 insertions(+), 31 deletions(-) create mode 100644 src/Servers/Connections.Abstractions/src/IConnectionListenerFactorySelector.cs diff --git a/src/Servers/Connections.Abstractions/src/IConnectionListenerFactory.cs b/src/Servers/Connections.Abstractions/src/IConnectionListenerFactory.cs index 337f848f17a6..c1570ba4ab11 100644 --- a/src/Servers/Connections.Abstractions/src/IConnectionListenerFactory.cs +++ b/src/Servers/Connections.Abstractions/src/IConnectionListenerFactory.cs @@ -19,13 +19,4 @@ public interface IConnectionListenerFactory /// The token to monitor for cancellation requests. /// A that completes when the listener has been bound, yielding a representing the new listener. ValueTask BindAsync(EndPoint endpoint, CancellationToken cancellationToken = default); - -#if NET7_0_OR_GREATER - /// - /// Returns a value that indicates whether the listener factory supports binding to the specified . - /// - /// The to bind to. - /// A value that indicates whether the listener factory supports binding to the specified . - public bool CanBind(EndPoint endpoint) => true; -#endif } diff --git a/src/Servers/Connections.Abstractions/src/IConnectionListenerFactorySelector.cs b/src/Servers/Connections.Abstractions/src/IConnectionListenerFactorySelector.cs new file mode 100644 index 000000000000..e372f8fddf23 --- /dev/null +++ b/src/Servers/Connections.Abstractions/src/IConnectionListenerFactorySelector.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; + +namespace Microsoft.AspNetCore.Connections; + +/// +/// Defines an interface that determines the mechanisms for binding to various types of s. +/// +public interface IConnectionListenerFactorySelector +{ + /// + /// Returns a value that indicates whether the listener factory supports binding to the specified . + /// + /// The to bind to. + /// A value that indicates whether the listener factory supports binding to the specified . + bool CanBind(EndPoint endpoint); +} diff --git a/src/Servers/Connections.Abstractions/src/IMultiplexedConnectionListenerFactory.cs b/src/Servers/Connections.Abstractions/src/IMultiplexedConnectionListenerFactory.cs index 06af55f6853f..f4fba698b122 100644 --- a/src/Servers/Connections.Abstractions/src/IMultiplexedConnectionListenerFactory.cs +++ b/src/Servers/Connections.Abstractions/src/IMultiplexedConnectionListenerFactory.cs @@ -21,13 +21,4 @@ public interface IMultiplexedConnectionListenerFactory /// The token to monitor for cancellation requests. /// A that completes when the listener has been bound, yielding a representing the new listener. ValueTask BindAsync(EndPoint endpoint, IFeatureCollection? features = null, CancellationToken cancellationToken = default); - -#if NET7_0_OR_GREATER - /// - /// Returns a value that indicates whether the listener factory supports binding to the specified . - /// - /// The to bind to. - /// A value that indicates whether the listener factory supports binding to the specified . - public bool CanBind(EndPoint endpoint) => true; -#endif } diff --git a/src/Servers/Connections.Abstractions/src/PublicAPI/net462/PublicAPI.Unshipped.txt b/src/Servers/Connections.Abstractions/src/PublicAPI/net462/PublicAPI.Unshipped.txt index 553973838a81..88184eb7b651 100644 --- a/src/Servers/Connections.Abstractions/src/PublicAPI/net462/PublicAPI.Unshipped.txt +++ b/src/Servers/Connections.Abstractions/src/PublicAPI/net462/PublicAPI.Unshipped.txt @@ -1,3 +1,5 @@ #nullable enable Microsoft.AspNetCore.Connections.Features.IStreamClosedFeature Microsoft.AspNetCore.Connections.Features.IStreamClosedFeature.OnClosed(System.Action! callback, object? state) -> void +Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector +Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector.CanBind(System.Net.EndPoint! endpoint) -> bool diff --git a/src/Servers/Connections.Abstractions/src/PublicAPI/net7.0/PublicAPI.Unshipped.txt b/src/Servers/Connections.Abstractions/src/PublicAPI/net7.0/PublicAPI.Unshipped.txt index f91ba371573b..aa5fad13eb1f 100644 --- a/src/Servers/Connections.Abstractions/src/PublicAPI/net7.0/PublicAPI.Unshipped.txt +++ b/src/Servers/Connections.Abstractions/src/PublicAPI/net7.0/PublicAPI.Unshipped.txt @@ -1,8 +1,8 @@ #nullable enable Microsoft.AspNetCore.Connections.Features.IStreamClosedFeature Microsoft.AspNetCore.Connections.Features.IStreamClosedFeature.OnClosed(System.Action! callback, object? state) -> void -Microsoft.AspNetCore.Connections.IConnectionListenerFactory.CanBind(System.Net.EndPoint! endpoint) -> bool -Microsoft.AspNetCore.Connections.IMultiplexedConnectionListenerFactory.CanBind(System.Net.EndPoint! endpoint) -> bool +Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector +Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector.CanBind(System.Net.EndPoint! endpoint) -> bool Microsoft.AspNetCore.Connections.TlsConnectionCallbackContext Microsoft.AspNetCore.Connections.TlsConnectionCallbackContext.ClientHelloInfo.get -> System.Net.Security.SslClientHelloInfo Microsoft.AspNetCore.Connections.TlsConnectionCallbackContext.ClientHelloInfo.set -> void diff --git a/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 553973838a81..88184eb7b651 100644 --- a/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,3 +1,5 @@ #nullable enable Microsoft.AspNetCore.Connections.Features.IStreamClosedFeature Microsoft.AspNetCore.Connections.Features.IStreamClosedFeature.OnClosed(System.Action! callback, object? state) -> void +Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector +Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector.CanBind(System.Net.EndPoint! endpoint) -> bool diff --git a/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt b/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt index 553973838a81..88184eb7b651 100644 --- a/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt +++ b/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt @@ -1,3 +1,5 @@ #nullable enable Microsoft.AspNetCore.Connections.Features.IStreamClosedFeature Microsoft.AspNetCore.Connections.Features.IStreamClosedFeature.OnClosed(System.Action! callback, object? state) -> void +Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector +Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector.CanBind(System.Net.EndPoint! endpoint) -> bool diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs index df0db56cf866..fdb504ccd09a 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs @@ -44,7 +44,11 @@ public async Task BindAsync(EndPoint endPoint, ConnectionDelegate conn foreach (var transportFactory in _transportFactories) { - if (transportFactory.CanBind(endPoint)) + var selector = transportFactory as IConnectionListenerFactorySelector; + + // By default, the last registered factory binds to the endpoint. + // A factory can implement IConnectionListenerFactorySelector to decide whether it can bind to the endpoint. + if (selector?.CanBind(endPoint) ?? true) { var transport = await transportFactory.BindAsync(endPoint, cancellationToken).ConfigureAwait(false); StartAcceptLoop(new GenericConnectionListener(transport), c => connectionDelegate(c), endpointConfig); @@ -97,7 +101,11 @@ public async Task BindAsync(EndPoint endPoint, MultiplexedConnectionDe foreach (var multiplexedTransportFactory in _multiplexedTransportFactories) { - if (multiplexedTransportFactory.CanBind(endPoint)) + var selector = multiplexedTransportFactory as IConnectionListenerFactorySelector; + + // By default, the last registered factory binds to the endpoint. + // A factory can implement IConnectionListenerFactorySelector to decide whether it can bind to the endpoint. + if (selector?.CanBind(endPoint) ?? true) { var transport = await multiplexedTransportFactory.BindAsync(endPoint, features, cancellationToken).ConfigureAwait(false); StartAcceptLoop(new GenericMultiplexedConnectionListener(transport), c => multiplexedConnectionDelegate(c), listenOptions.EndpointConfig); diff --git a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs index 5594255a995a..fe33cdb6aeca 100644 --- a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs +++ b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs @@ -475,7 +475,6 @@ public async Task StopAsyncCallsCompleteWhenFirstCallCompletes() var mockTransport = new Mock(); var mockTransportFactory = new Mock(); - mockTransportFactory.Setup(m => m.CanBind(It.IsAny())).Returns(true); mockTransportFactory .Setup(transportFactory => transportFactory.BindAsync(It.IsAny(), It.IsAny())) .Returns((e, token) => @@ -533,7 +532,6 @@ public async Task StopAsyncCallsCompleteWithThrownException() var mockTransport = new Mock(); var mockTransportFactory = new Mock(); - mockTransportFactory.Setup(m => m.CanBind(It.IsAny())).Returns(true); mockTransportFactory .Setup(transportFactory => transportFactory.BindAsync(It.IsAny(), It.IsAny())) .Returns((e, token) => @@ -593,7 +591,6 @@ public async Task StopAsyncDispatchesSubsequentStopAsyncContinuations() var mockTransport = new Mock(); var mockTransportFactory = new Mock(); - mockTransportFactory.Setup(m => m.CanBind(It.IsAny())).Returns(true); mockTransportFactory .Setup(transportFactory => transportFactory.BindAsync(It.IsAny(), It.IsAny())) .Returns((e, token) => @@ -609,6 +606,8 @@ public async Task StopAsyncDispatchesSubsequentStopAsyncContinuations() return new ValueTask(mockTransport.Object); }); + mockTransportFactory.As() + .Setup(m => m.CanBind(It.IsAny())).Returns(true); var mockLoggerFactory = new Mock(); var mockLogger = new Mock(); @@ -723,7 +722,6 @@ public async Task ReloadsOnConfigurationChangeWhenOptedIn() var mockTransports = new List>(); var mockTransportFactory = new Mock(); - mockTransportFactory.Setup(m => m.CanBind(It.IsAny())).Returns(true); mockTransportFactory .Setup(transportFactory => transportFactory.BindAsync(It.IsAny(), It.IsAny())) .Returns((e, token) => @@ -740,6 +738,8 @@ public async Task ReloadsOnConfigurationChangeWhenOptedIn() return new ValueTask(mockTransport.Object); }); + mockTransportFactory.As() + .Setup(m => m.CanBind(It.IsAny())).Returns(true); // Don't use "using". Dispose() could hang if test fails. var server = new KestrelServer(Options.Create(options), mockTransportFactory.Object, mockLoggerFactory.Object); @@ -861,7 +861,6 @@ public async Task DoesNotReloadOnConfigurationChangeByDefault() var mockTransports = new List>(); var mockTransportFactory = new Mock(); - mockTransportFactory.Setup(m => m.CanBind(It.IsAny())).Returns(true); mockTransportFactory .Setup(transportFactory => transportFactory.BindAsync(It.IsAny(), It.IsAny())) .Returns((e, token) => @@ -878,6 +877,8 @@ public async Task DoesNotReloadOnConfigurationChangeByDefault() return new ValueTask(mockTransport.Object); }); + mockTransportFactory.As() + .Setup(m => m.CanBind(It.IsAny())).Returns(true); // Don't use "using". Dispose() could hang if test fails. var server = new KestrelServer(Options.Create(options), mockTransportFactory.Object, mockLoggerFactory.Object); @@ -939,24 +940,24 @@ public ValueTask BindAsync(EndPoint endpoint, CancellationT } } - private class NonBindableTransportFactory : IConnectionListenerFactory + private class NonBindableTransportFactory : IConnectionListenerFactory, IConnectionListenerFactorySelector { public ValueTask BindAsync(EndPoint endpoint, CancellationToken cancellationToken = default) { throw new InvalidOperationException(); } - bool IConnectionListenerFactory.CanBind(EndPoint endpoint) => false; + bool IConnectionListenerFactorySelector.CanBind(EndPoint endpoint) => false; } - private class NonBindableMultiplexedTransportFactory : IMultiplexedConnectionListenerFactory + private class NonBindableMultiplexedTransportFactory : IMultiplexedConnectionListenerFactory, IConnectionListenerFactorySelector { public ValueTask BindAsync(EndPoint endpoint, IFeatureCollection features = null, CancellationToken cancellationToken = default) { throw new InvalidOperationException(); } - bool IMultiplexedConnectionListenerFactory.CanBind(EndPoint endpoint) => false; + bool IConnectionListenerFactorySelector.CanBind(EndPoint endpoint) => false; } private class MockMultiplexedTransportFactory : IMultiplexedConnectionListenerFactory From bc1dcf640ca42bb9fe4447273938c72a0ae5cf78 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Fri, 28 Oct 2022 15:07:55 +0800 Subject: [PATCH 4/6] Clean up --- .../src/IConnectionListenerFactorySelector.cs | 6 +++++- src/Servers/Kestrel/Core/test/KestrelServerTests.cs | 10 ++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Servers/Connections.Abstractions/src/IConnectionListenerFactorySelector.cs b/src/Servers/Connections.Abstractions/src/IConnectionListenerFactorySelector.cs index e372f8fddf23..77fefb15faa6 100644 --- a/src/Servers/Connections.Abstractions/src/IConnectionListenerFactorySelector.cs +++ b/src/Servers/Connections.Abstractions/src/IConnectionListenerFactorySelector.cs @@ -6,8 +6,12 @@ namespace Microsoft.AspNetCore.Connections; /// -/// Defines an interface that determines the mechanisms for binding to various types of s. +/// Defines an interface that determines whether the listener factory supports binding to the specified . /// +/// +/// This interface should be implemented by and +/// implementations that want to control want endpoint instances they can bind to. +/// public interface IConnectionListenerFactorySelector { /// diff --git a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs index fe33cdb6aeca..a6e9ca954624 100644 --- a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs +++ b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs @@ -606,8 +606,6 @@ public async Task StopAsyncDispatchesSubsequentStopAsyncContinuations() return new ValueTask(mockTransport.Object); }); - mockTransportFactory.As() - .Setup(m => m.CanBind(It.IsAny())).Returns(true); var mockLoggerFactory = new Mock(); var mockLogger = new Mock(); @@ -738,8 +736,6 @@ public async Task ReloadsOnConfigurationChangeWhenOptedIn() return new ValueTask(mockTransport.Object); }); - mockTransportFactory.As() - .Setup(m => m.CanBind(It.IsAny())).Returns(true); // Don't use "using". Dispose() could hang if test fails. var server = new KestrelServer(Options.Create(options), mockTransportFactory.Object, mockLoggerFactory.Object); @@ -877,8 +873,6 @@ public async Task DoesNotReloadOnConfigurationChangeByDefault() return new ValueTask(mockTransport.Object); }); - mockTransportFactory.As() - .Setup(m => m.CanBind(It.IsAny())).Returns(true); // Don't use "using". Dispose() could hang if test fails. var server = new KestrelServer(Options.Create(options), mockTransportFactory.Object, mockLoggerFactory.Object); @@ -947,7 +941,7 @@ public ValueTask BindAsync(EndPoint endpoint, CancellationT throw new InvalidOperationException(); } - bool IConnectionListenerFactorySelector.CanBind(EndPoint endpoint) => false; + public bool CanBind(EndPoint endpoint) => false; } private class NonBindableMultiplexedTransportFactory : IMultiplexedConnectionListenerFactory, IConnectionListenerFactorySelector @@ -957,7 +951,7 @@ public ValueTask BindAsync(EndPoint endpoint, IF throw new InvalidOperationException(); } - bool IConnectionListenerFactorySelector.CanBind(EndPoint endpoint) => false; + public bool CanBind(EndPoint endpoint) => false; } private class MockMultiplexedTransportFactory : IMultiplexedConnectionListenerFactory From 16152c2a62e904db9d6da2a0ff302d645b74bea4 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Fri, 28 Oct 2022 15:56:15 +0800 Subject: [PATCH 5/6] Update --- .../src/IConnectionListenerFactorySelector.cs | 2 +- .../Core/src/Internal/Infrastructure/TransportManager.cs | 4 ++-- src/Servers/Kestrel/Core/test/KestrelServerTests.cs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Servers/Connections.Abstractions/src/IConnectionListenerFactorySelector.cs b/src/Servers/Connections.Abstractions/src/IConnectionListenerFactorySelector.cs index 77fefb15faa6..fc46c377d976 100644 --- a/src/Servers/Connections.Abstractions/src/IConnectionListenerFactorySelector.cs +++ b/src/Servers/Connections.Abstractions/src/IConnectionListenerFactorySelector.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Connections; /// /// /// This interface should be implemented by and -/// implementations that want to control want endpoint instances they can bind to. +/// types that want to control want endpoint instances they can bind to. /// public interface IConnectionListenerFactorySelector { diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs index fdb504ccd09a..20192677a28b 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs @@ -56,7 +56,7 @@ public async Task BindAsync(EndPoint endPoint, ConnectionDelegate conn } } - throw new InvalidOperationException($"No registered {nameof(IConnectionListenerFactory)} supports endpoint: {endPoint}"); + throw new InvalidOperationException($"No registered {nameof(IConnectionListenerFactory)} supports endpoint {endPoint.GetType().Name}: {endPoint}"); } public async Task BindAsync(EndPoint endPoint, MultiplexedConnectionDelegate multiplexedConnectionDelegate, ListenOptions listenOptions, CancellationToken cancellationToken) @@ -113,7 +113,7 @@ public async Task BindAsync(EndPoint endPoint, MultiplexedConnectionDe } } - throw new InvalidOperationException($"No registered {nameof(IMultiplexedConnectionListenerFactory)} supports endpoint: {endPoint}"); + throw new InvalidOperationException($"No registered {nameof(IMultiplexedConnectionListenerFactory)} supports endpoint {endPoint.GetType().Name}: {endPoint}"); } /// diff --git a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs index a6e9ca954624..37ecd99169b8 100644 --- a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs +++ b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs @@ -257,7 +257,7 @@ public async Task StartWithNoValidTransportFactoryThrows() var exception = await Assert.ThrowsAsync( async () => await server.StartAsync(new DummyApplication(context => Task.CompletedTask), CancellationToken.None)); - Assert.Equal("No registered IConnectionListenerFactory supports endpoint: 127.0.0.1:0", exception.Message); + Assert.Equal("No registered IConnectionListenerFactory supports endpoint IPEndPoint: 127.0.0.1:0", exception.Message); } [Fact] @@ -299,7 +299,7 @@ public async Task StartWithNoValidTransportFactoryThrows_Http3() var exception = await Assert.ThrowsAsync( async () => await server.StartAsync(new DummyApplication(context => Task.CompletedTask), CancellationToken.None)); - Assert.Equal("No registered IMultiplexedConnectionListenerFactory supports endpoint: 127.0.0.1:0", exception.Message); + Assert.Equal("No registered IMultiplexedConnectionListenerFactory supports endpoint IPEndPoint: 127.0.0.1:0", exception.Message); } [Fact] From f4e5a61a0e9e3b2c70afb3c2b21d7268ba407b77 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sun, 30 Oct 2022 07:56:24 +0800 Subject: [PATCH 6/6] Refactor --- .../Internal/Infrastructure/TransportManager.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs index 20192677a28b..441883b9dcd5 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs @@ -45,10 +45,7 @@ public async Task BindAsync(EndPoint endPoint, ConnectionDelegate conn foreach (var transportFactory in _transportFactories) { var selector = transportFactory as IConnectionListenerFactorySelector; - - // By default, the last registered factory binds to the endpoint. - // A factory can implement IConnectionListenerFactorySelector to decide whether it can bind to the endpoint. - if (selector?.CanBind(endPoint) ?? true) + if (CanBindFactory(endPoint, selector)) { var transport = await transportFactory.BindAsync(endPoint, cancellationToken).ConfigureAwait(false); StartAcceptLoop(new GenericConnectionListener(transport), c => connectionDelegate(c), endpointConfig); @@ -102,10 +99,7 @@ public async Task BindAsync(EndPoint endPoint, MultiplexedConnectionDe foreach (var multiplexedTransportFactory in _multiplexedTransportFactories) { var selector = multiplexedTransportFactory as IConnectionListenerFactorySelector; - - // By default, the last registered factory binds to the endpoint. - // A factory can implement IConnectionListenerFactorySelector to decide whether it can bind to the endpoint. - if (selector?.CanBind(endPoint) ?? true) + if (CanBindFactory(endPoint, selector)) { var transport = await multiplexedTransportFactory.BindAsync(endPoint, features, cancellationToken).ConfigureAwait(false); StartAcceptLoop(new GenericMultiplexedConnectionListener(transport), c => multiplexedConnectionDelegate(c), listenOptions.EndpointConfig); @@ -116,6 +110,13 @@ public async Task BindAsync(EndPoint endPoint, MultiplexedConnectionDe throw new InvalidOperationException($"No registered {nameof(IMultiplexedConnectionListenerFactory)} supports endpoint {endPoint.GetType().Name}: {endPoint}"); } + private static bool CanBindFactory(EndPoint endPoint, IConnectionListenerFactorySelector? selector) + { + // By default, the last registered factory binds to the endpoint. + // A factory can implement IConnectionListenerFactorySelector to decide whether it can bind to the endpoint. + return selector?.CanBind(endPoint) ?? true; + } + /// /// TlsHandshakeCallbackContext.Connection is ConnectionContext but QUIC connection only implements BaseConnectionContext. ///