Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 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
2 changes: 1 addition & 1 deletion src/DefaultBuilder/src/WebHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ internal static void ConfigureWebDefaults(IWebHostBuilder builder)

internal static void ConfigureWebDefaultsCore(IWebHostBuilder builder, Action<IServiceCollection>? configureRouting = null)
{
builder.UseKestrel((builderContext, options) =>
builder.UseKestrelSlim().ConfigureKestrel((builderContext, options) =>
{
options.Configure(builderContext.Configuration.GetSection("Kestrel"), reloadOnChange: true);
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ Microsoft.AspNetCore.Connections.NamedPipeEndPoint.PipeName.get -> string!
Microsoft.AspNetCore.Connections.NamedPipeEndPoint.ServerName.get -> string!
override Microsoft.AspNetCore.Connections.NamedPipeEndPoint.Equals(object? obj) -> bool
override Microsoft.AspNetCore.Connections.NamedPipeEndPoint.GetHashCode() -> int
override Microsoft.AspNetCore.Connections.NamedPipeEndPoint.ToString() -> string!
override Microsoft.AspNetCore.Connections.NamedPipeEndPoint.ToString() -> string!
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ Microsoft.AspNetCore.Connections.NamedPipeEndPoint.PipeName.get -> string!
Microsoft.AspNetCore.Connections.NamedPipeEndPoint.ServerName.get -> string!
override Microsoft.AspNetCore.Connections.NamedPipeEndPoint.Equals(object? obj) -> bool
override Microsoft.AspNetCore.Connections.NamedPipeEndPoint.GetHashCode() -> int
override Microsoft.AspNetCore.Connections.NamedPipeEndPoint.ToString() -> string!
override Microsoft.AspNetCore.Connections.NamedPipeEndPoint.ToString() -> string!
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ Microsoft.AspNetCore.Connections.NamedPipeEndPoint.PipeName.get -> string!
Microsoft.AspNetCore.Connections.NamedPipeEndPoint.ServerName.get -> string!
override Microsoft.AspNetCore.Connections.NamedPipeEndPoint.Equals(object? obj) -> bool
override Microsoft.AspNetCore.Connections.NamedPipeEndPoint.GetHashCode() -> int
override Microsoft.AspNetCore.Connections.NamedPipeEndPoint.ToString() -> string!
override Microsoft.AspNetCore.Connections.NamedPipeEndPoint.ToString() -> string!
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ public HttpsConnectionAdapterOptions()
/// </summary>
public Func<ConnectionContext?, string?, X509Certificate2?>? ServerCertificateSelector { get; set; }

/// <summary>
/// A convenience property for checking whether a server certificate or selector has been set.
/// </summary>
internal bool HasServerCertificateOrSelector => ServerCertificate is not null || ServerCertificateSelector is not null;

/// <summary>
/// Specifies the client certificate requirements for a HTTPS connection. Defaults to <see cref="ClientCertificateMode.NoCertificate"/>.
/// </summary>
Expand Down
25 changes: 25 additions & 0 deletions src/Servers/Kestrel/Core/src/ITlsConfigurationLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Https;

namespace Microsoft.AspNetCore.Server.Kestrel;

internal interface ITlsConfigurationLoader
{
void ApplyHttpsDefaults(
KestrelServerOptions serverOptions,
EndpointConfig endpoint,
HttpsConnectionAdapterOptions httpsOptions,
CertificateConfig? defaultCertificateConfig,
ConfigurationReader configurationReader);

CertificateAndConfig? LoadDefaultCertificate(ConfigurationReader configurationReader);

void UseHttps(ListenOptions listenOptions, EndpointConfig endpoint, HttpsConnectionAdapterOptions httpsOptions);
}

internal record CertificateAndConfig(X509Certificate2 Certificate, CertificateConfig CertificateConfig);
11 changes: 11 additions & 0 deletions src/Servers/Kestrel/Core/src/IUseHttpsHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// 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.Server.Kestrel.Core;

namespace Microsoft.AspNetCore.Hosting;

internal interface IUseHttpsHelper
{
ListenOptions UseHttps(ListenOptions listenOptions);
}
26 changes: 16 additions & 10 deletions src/Servers/Kestrel/Core/src/Internal/AddressBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
internal sealed class AddressBinder
{
// note this doesn't copy the ListenOptions[], only call this with an array that isn't mutated elsewhere
public static async Task BindAsync(ListenOptions[] listenOptions, AddressBindContext context, CancellationToken cancellationToken)
public static async Task BindAsync(ListenOptions[] listenOptions, AddressBindContext context, IUseHttpsHelper useHttpsHelper, CancellationToken cancellationToken)
{
var strategy = CreateStrategy(
listenOptions,
context.Addresses.ToArray(),
context.ServerAddressesFeature.PreferHostingUrls);
context.ServerAddressesFeature.PreferHostingUrls,
useHttpsHelper);

// reset options. The actual used options and addresses will be populated
// by the address binding feature
Expand All @@ -32,7 +33,7 @@ public static async Task BindAsync(ListenOptions[] listenOptions, AddressBindCon
await strategy.BindAsync(context, cancellationToken).ConfigureAwait(false);
}

private static IStrategy CreateStrategy(ListenOptions[] listenOptions, string[] addresses, bool preferAddresses)
private static IStrategy CreateStrategy(ListenOptions[] listenOptions, string[] addresses, bool preferAddresses, IUseHttpsHelper useHttpsHelper)
{
var hasListenOptions = listenOptions.Length > 0;
var hasAddresses = addresses.Length > 0;
Expand All @@ -41,10 +42,10 @@ private static IStrategy CreateStrategy(ListenOptions[] listenOptions, string[]
{
if (hasListenOptions)
{
return new OverrideWithAddressesStrategy(addresses);
return new OverrideWithAddressesStrategy(addresses, useHttpsHelper);
}

return new AddressesStrategy(addresses);
return new AddressesStrategy(addresses, useHttpsHelper);
}
else if (hasListenOptions)
{
Expand All @@ -58,7 +59,7 @@ private static IStrategy CreateStrategy(ListenOptions[] listenOptions, string[]
else if (hasAddresses)
{
// If no endpoints are configured directly using KestrelServerOptions, use those configured via the IServerAddressesFeature.
return new AddressesStrategy(addresses);
return new AddressesStrategy(addresses, useHttpsHelper);
}
else
{
Expand All @@ -71,6 +72,9 @@ private static IStrategy CreateStrategy(ListenOptions[] listenOptions, string[]
/// Returns an <see cref="IPEndPoint"/> for the given host an port.
/// If the host parameter isn't "localhost" or an IP address, use IPAddress.Any.
/// </summary>
/// <remarks>
/// Internal for testing.
/// </remarks>
internal static bool TryCreateIPEndPoint(BindingAddress address, [NotNullWhen(true)] out IPEndPoint? endpoint)
{
if (!IPAddress.TryParse(address.Host, out var ip))
Expand Down Expand Up @@ -162,8 +166,8 @@ public async Task BindAsync(AddressBindContext context, CancellationToken cancel

private sealed class OverrideWithAddressesStrategy : AddressesStrategy
{
public OverrideWithAddressesStrategy(IReadOnlyCollection<string> addresses)
: base(addresses)
public OverrideWithAddressesStrategy(IReadOnlyCollection<string> addresses, IUseHttpsHelper useHttpsHelper)
: base(addresses, useHttpsHelper)
{
}

Expand Down Expand Up @@ -216,10 +220,12 @@ public virtual async Task BindAsync(AddressBindContext context, CancellationToke
private class AddressesStrategy : IStrategy
{
protected readonly IReadOnlyCollection<string> _addresses;
private readonly IUseHttpsHelper _useHttpsHelper;

public AddressesStrategy(IReadOnlyCollection<string> addresses)
public AddressesStrategy(IReadOnlyCollection<string> addresses, IUseHttpsHelper useHttpsHelper)
{
_addresses = addresses;
_useHttpsHelper = useHttpsHelper;
}

public virtual async Task BindAsync(AddressBindContext context, CancellationToken cancellationToken)
Expand All @@ -231,7 +237,7 @@ public virtual async Task BindAsync(AddressBindContext context, CancellationToke

if (https && !options.IsTls)
{
options.UseHttps();
_useHttpsHelper.UseHttps(options);
}

await options.BindAsync(context, cancellationToken).ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
@@ -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.

#nullable enable

using System.Net;
using Microsoft.AspNetCore.Connections;

namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;

internal interface IMultiplexedTransportManager
{
bool HasFactories { get; }

Task<EndPoint> BindAsync(EndPoint endPoint, MultiplexedConnectionDelegate multiplexedConnectionDelegate, ListenOptions listenOptions, CancellationToken cancellationToken);

Task StopAsync(CancellationToken cancellationToken);
Task StopEndpointsAsync(List<EndpointConfig> endpointsToStop, CancellationToken cancellationToken);
}
Copy link
Member

@halter73 halter73 Mar 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of adding IMultiplexedTransportManager and ITransportManager, could we register IMultiplexedHttpsFeatureFactory? I know it's internal, but it seems like the smaller change unless there's some other benefit to splitting things up.

internal interface IMultiplexedHttpsFeatureFactory
{
    void PopulateHttpsFeatures(IFeatureCollection features, ListenOptions listenOptions);
}

We could have the default implementation throw an InvalidOperationException similar to InvalidUseHttpsHelper.

Edit: It might make sense just to combine these:

internal interface IUseHttpsHelper
{
    ListenOptions UseHttps(ListenOptions listenOptions);
    void PopulateHttpsFeatures(ListenOptions listenOptions, IFeatureCollection features);
}

Or we could add an ListenOptions.Features and have UseHttps(ListenOptions listenOptions) populate it and keep the one method.

I'm a big fan of limiting the number of services even if they're internal.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I'm not sure I understand this suggestion. (I had assumed I could go study IMultiplexedHttpsFeatureFactory, but I think you just invented it?) I think you might be suggesting that we add the pay-for-play functionality to a (the?) feature collection, rather than the DI container?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented in #47454.

Original file line number Diff line number Diff line change
@@ -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.

#nullable enable

using System.Net;
using Microsoft.AspNetCore.Connections;

namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;

internal interface ITransportManager
{
bool HasFactories { get; }

Task<EndPoint> BindAsync(EndPoint endPoint, ConnectionDelegate connectionDelegate, EndpointConfig? endpointConfig, CancellationToken cancellationToken);

Task StopAsync(CancellationToken cancellationToken);
Task StopEndpointsAsync(List<EndpointConfig> endpointsToStop, CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable enable

using System.IO.Pipelines;
using System.Linq;
using System.Net;
using System.Net.Security;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;

namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;

internal sealed class MultiplexedTransportManager : TransportManagerBase, IMultiplexedTransportManager
{
private readonly List<IMultiplexedConnectionListenerFactory> _factories;

public MultiplexedTransportManager(
ServiceContext serviceContext,
IEnumerable<IMultiplexedConnectionListenerFactory> factories)
: base(serviceContext)
{
_factories = factories.Reverse().ToList();
}

public override bool HasFactories => _factories.Count > 0;

public async Task<EndPoint> BindAsync(EndPoint endPoint, MultiplexedConnectionDelegate multiplexedConnectionDelegate, ListenOptions listenOptions, CancellationToken cancellationToken)
{
if (!HasFactories)
{
throw new InvalidOperationException($"Cannot bind with {nameof(MultiplexedConnectionDelegate)} no {nameof(IMultiplexedConnectionListenerFactory)} is registered.");
}

var features = new FeatureCollection();

// HttpsOptions or HttpsCallbackOptions should always be set in production, but it's not set for InMemory tests.
// The QUIC transport will check if TlsConnectionCallbackOptions is missing.
if (listenOptions.HttpsOptions != null)
{
var sslServerAuthenticationOptions = HttpsConnectionMiddleware.CreateHttp3Options(listenOptions.HttpsOptions);
features.Set(new TlsConnectionCallbackOptions
{
ApplicationProtocols = sslServerAuthenticationOptions.ApplicationProtocols ?? new List<SslApplicationProtocol> { SslApplicationProtocol.Http3 },
OnConnection = (context, cancellationToken) => ValueTask.FromResult(sslServerAuthenticationOptions),
OnConnectionState = null,
});
}
else if (listenOptions.HttpsCallbackOptions != null)
{
features.Set(new TlsConnectionCallbackOptions
{
ApplicationProtocols = new List<SslApplicationProtocol> { SslApplicationProtocol.Http3 },
OnConnection = (context, cancellationToken) =>
{
return listenOptions.HttpsCallbackOptions.OnConnection(new TlsHandshakeCallbackContext
{
ClientHelloInfo = context.ClientHelloInfo,
CancellationToken = cancellationToken,
State = context.State,
Connection = new ConnectionContextAdapter(context.Connection),
});
},
OnConnectionState = listenOptions.HttpsCallbackOptions.OnConnectionState,
});
}

foreach (var factory in _factories)
{
var selector = factory as IConnectionListenerFactorySelector;
if (CanBindFactory(endPoint, selector))
{
var transport = await factory.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.GetType().Name}: {endPoint}");
}

/// <summary>
/// TlsHandshakeCallbackContext.Connection is ConnectionContext but QUIC connection only implements BaseConnectionContext.
/// </summary>
private sealed class ConnectionContextAdapter : ConnectionContext
{
private readonly BaseConnectionContext _inner;

public ConnectionContextAdapter(BaseConnectionContext inner) => _inner = inner;

public override IDuplexPipe Transport
{
get => throw new NotSupportedException("Not supported by HTTP/3 connections.");
set => throw new NotSupportedException("Not supported by HTTP/3 connections.");
}
public override string ConnectionId
{
get => _inner.ConnectionId;
set => _inner.ConnectionId = value;
}
public override IFeatureCollection Features => _inner.Features;
public override IDictionary<object, object?> Items
{
get => _inner.Items;
set => _inner.Items = value;
}
public override EndPoint? LocalEndPoint
{
get => _inner.LocalEndPoint;
set => _inner.LocalEndPoint = value;
}
public override EndPoint? RemoteEndPoint
{
get => _inner.RemoteEndPoint;
set => _inner.RemoteEndPoint = value;
}
public override CancellationToken ConnectionClosed
{
get => _inner.ConnectionClosed;
set => _inner.ConnectionClosed = value;
}
public override ValueTask DisposeAsync() => _inner.DisposeAsync();
}

private sealed class GenericMultiplexedConnectionListener : IConnectionListener<MultiplexedConnectionContext>
{
private readonly IMultiplexedConnectionListener _multiplexedConnectionListener;

public GenericMultiplexedConnectionListener(IMultiplexedConnectionListener multiplexedConnectionListener)
{
_multiplexedConnectionListener = multiplexedConnectionListener;
}

public EndPoint EndPoint => _multiplexedConnectionListener.EndPoint;

public ValueTask<MultiplexedConnectionContext?> AcceptAsync(CancellationToken cancellationToken = default)
=> _multiplexedConnectionListener.AcceptAsync(features: null, cancellationToken);

public ValueTask UnbindAsync(CancellationToken cancellationToken = default)
=> _multiplexedConnectionListener.UnbindAsync(cancellationToken);

public ValueTask DisposeAsync()
=> _multiplexedConnectionListener.DisposeAsync();
}
}
Loading