Skip to content
Closed
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
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