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
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ internal static void PopulateMultiplexedTransportFeaturesWorker(FeatureCollectio
// The QUIC transport will check if TlsConnectionCallbackOptions is missing.
if (listenOptions.HttpsOptions != null)
{
var sslServerAuthenticationOptions = HttpsConnectionMiddleware.CreateHttp3Options(listenOptions.HttpsOptions, logger);
var sslServerAuthenticationOptions = HttpsConnectionMiddleware.CreateHttp3Options(listenOptions.HttpsOptions.Value, logger);
features.Set(new TlsConnectionCallbackOptions
{
ApplicationProtocols = sslServerAuthenticationOptions.ApplicationProtocols ?? new List<SslApplicationProtocol> { SslApplicationProtocol.Http3 },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,6 @@ public HttpsConnectionAdapterOptions()
/// </summary>
public SslProtocols SslProtocols { get; set; }

/// <summary>
/// The protocols enabled on this endpoint.
/// </summary>
/// <remarks>Defaults to HTTP/1.x only.</remarks>
internal HttpProtocols HttpProtocols { get; set; }

/// <summary>
/// Specifies whether the certificate revocation list is checked during authentication.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/Servers/Kestrel/Core/src/ListenOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ internal string Scheme
}

internal bool IsTls { get; set; }
internal HttpsConnectionAdapterOptions? HttpsOptions { get; set; }
internal Lazy<HttpsConnectionAdapterOptions>? HttpsOptions { get; set; }
internal TlsHandshakeCallbackOptions? HttpsCallbackOptions { get; set; }

/// <summary>
Expand Down
47 changes: 32 additions & 15 deletions src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,17 +166,20 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, Action<Ht
// We consider calls to `UseHttps` to be a clear expression of user intent to pull in HTTPS configuration support
listenOptions.KestrelServerOptions.EnableHttpsConfiguration();

var options = new HttpsConnectionAdapterOptions();
listenOptions.KestrelServerOptions.ApplyHttpsDefaults(options);
configureOptions(options);
listenOptions.KestrelServerOptions.ApplyDefaultCertificate(options);

if (!options.HasServerCertificateOrSelector)
return listenOptions.UseHttps(new Lazy<HttpsConnectionAdapterOptions>(() =>
{
throw new InvalidOperationException(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound);
}
var options = new HttpsConnectionAdapterOptions();
listenOptions.KestrelServerOptions.ApplyHttpsDefaults(options);
configureOptions(options);
listenOptions.KestrelServerOptions.ApplyDefaultCertificate(options);

return listenOptions.UseHttps(options);
if (!options.HasServerCertificateOrSelector)
{
throw new InvalidOperationException(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound);
}

return options;
}));
}

/// <summary>
Expand All @@ -188,17 +191,30 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, Action<Ht
/// <returns>The <see cref="ListenOptions"/>.</returns>
public static ListenOptions UseHttps(this ListenOptions listenOptions, HttpsConnectionAdapterOptions httpsOptions)
{
var loggerFactory = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService<ILoggerFactory>();
var metrics = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService<KestrelMetrics>();
return listenOptions.UseHttps(new Lazy<HttpsConnectionAdapterOptions>(httpsOptions));
}

/// <summary>
/// Configure Kestrel to use HTTPS. This does not use default certificates or other defaults specified via config or
/// <see cref="KestrelServerOptions.ConfigureHttpsDefaults(Action{HttpsConnectionAdapterOptions})"/>.
/// </summary>
/// <param name="listenOptions">The <see cref="ListenOptions"/> to configure.</param>
/// <param name="lazyHttpsOptions">Options to configure HTTPS.</param>
/// <returns>The <see cref="ListenOptions"/>.</returns>
private static ListenOptions UseHttps(this ListenOptions listenOptions, Lazy<HttpsConnectionAdapterOptions> lazyHttpsOptions)
{
listenOptions.IsTls = true;
listenOptions.HttpsOptions = httpsOptions;
listenOptions.HttpsOptions = lazyHttpsOptions;

var loggerFactory = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService<ILoggerFactory>();
var metrics = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService<KestrelMetrics>();

// NB: This lambda will only be invoked if either HTTP/1.* or HTTP/2 is being used
listenOptions.Use(next =>
{
// Set the list of protocols from listen options
httpsOptions.HttpProtocols = listenOptions.Protocols;
var middleware = new HttpsConnectionMiddleware(next, httpsOptions, loggerFactory, metrics);
// Evaluate the HttpsConnectionAdapterOptions, now that the configuration, if any, has been loaded
var httpsOptions = lazyHttpsOptions.Value;
var middleware = new HttpsConnectionMiddleware(next, httpsOptions, listenOptions.Protocols, loggerFactory, metrics);
return middleware.OnConnectionAsync;
});

Expand Down Expand Up @@ -259,6 +275,7 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, TlsHandsh
listenOptions.IsTls = true;
listenOptions.HttpsCallbackOptions = callbackOptions;

// NB: This lambda will only be invoked if either HTTP/1.* or HTTP/2 is being used
listenOptions.Use(next =>
{
// Set the list of protocols from listen options.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ internal sealed class HttpsConnectionMiddleware
private readonly ILogger<HttpsConnectionMiddleware> _logger;
private readonly Func<Stream, SslStream> _sslStreamFactory;

// Internal for testing
internal readonly HttpProtocols _httpProtocols;

// The following fields are only set by HttpsConnectionAdapterOptions ctor.
private readonly HttpsConnectionAdapterOptions? _options;
private readonly KestrelMetrics _metrics;
Expand All @@ -43,17 +46,16 @@ internal sealed class HttpsConnectionMiddleware
// The following fields are only set by TlsHandshakeCallbackOptions ctor.
private readonly Func<TlsHandshakeCallbackContext, ValueTask<SslServerAuthenticationOptions>>? _tlsCallbackOptions;
private readonly object? _tlsCallbackOptionsState;
private readonly HttpProtocols _httpProtocols;

// Pool for cancellation tokens that cancel the handshake
private readonly CancellationTokenSourcePool _ctsPool = new();

public HttpsConnectionMiddleware(ConnectionDelegate next, HttpsConnectionAdapterOptions options, KestrelMetrics metrics)
: this(next, options, loggerFactory: NullLoggerFactory.Instance, metrics: metrics)
public HttpsConnectionMiddleware(ConnectionDelegate next, HttpsConnectionAdapterOptions options, HttpProtocols httpProtocols, KestrelMetrics metrics)
: this(next, options, httpProtocols, loggerFactory: NullLoggerFactory.Instance, metrics: metrics)
{
}

public HttpsConnectionMiddleware(ConnectionDelegate next, HttpsConnectionAdapterOptions options, ILoggerFactory loggerFactory, KestrelMetrics metrics)
public HttpsConnectionMiddleware(ConnectionDelegate next, HttpsConnectionAdapterOptions options, HttpProtocols httpProtocols, ILoggerFactory loggerFactory, KestrelMetrics metrics)
{
ArgumentNullException.ThrowIfNull(options);

Expand All @@ -74,7 +76,7 @@ public HttpsConnectionMiddleware(ConnectionDelegate next, HttpsConnectionAdapter
//_sslStreamFactory = s => new SslStream(s);

_options = options;
_options.HttpProtocols = ValidateAndNormalizeHttpProtocols(_options.HttpProtocols, _logger);
_httpProtocols = ValidateAndNormalizeHttpProtocols(httpProtocols, _logger);

// capture the certificate now so it can't be switched after validation
_serverCertificate = options.ServerCertificate;
Expand Down Expand Up @@ -331,7 +333,7 @@ private Task DoOptionsBasedHandshakeAsync(ConnectionContext context, SslStream s
CertificateRevocationCheckMode = _options.CheckCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck,
};

ConfigureAlpn(sslOptions, _options.HttpProtocols);
ConfigureAlpn(sslOptions, _httpProtocols);

_options.OnAuthenticate?.Invoke(context, sslOptions);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,105 @@ public void ConfigureEndpoint_RecoverFromBadPassword()
void CheckListenOptions(X509Certificate2 expectedCert)
{
var listenOptions = Assert.Single(serverOptions.ConfigurationBackedListenOptions);
Assert.Equal(expectedCert.SerialNumber, listenOptions.HttpsOptions.ServerCertificate.SerialNumber);
Assert.Equal(expectedCert.SerialNumber, listenOptions.HttpsOptions!.Value.ServerCertificate.SerialNumber);
}
}

[Fact]
public void LoadDevelopmentCertificate_ConfigureFirst()
{
try
{
var serverOptions = CreateServerOptions();
var certificate = new X509Certificate2(TestResources.GetCertPath("aspnetdevcert.pfx"), "testPassword", X509KeyStorageFlags.Exportable);
var bytes = certificate.Export(X509ContentType.Pkcs12, "1234");
var path = GetCertificatePath();
Directory.CreateDirectory(Path.GetDirectoryName(path));
File.WriteAllBytes(path, bytes);

var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Certificates:Development:Password", "1234"),
}).Build();

serverOptions.Configure(config);

Assert.Null(serverOptions.ConfigurationLoader.DefaultCertificate);

serverOptions.ConfigurationLoader.Load();

Assert.NotNull(serverOptions.ConfigurationLoader.DefaultCertificate);
Assert.Equal(serverOptions.ConfigurationLoader.DefaultCertificate.SerialNumber, certificate.SerialNumber);

var ran1 = false;
serverOptions.ListenAnyIP(4545, listenOptions =>
{
ran1 = true;
listenOptions.UseHttps();
});
Assert.True(ran1);

var listenOptions = serverOptions.CodeBackedListenOptions.Single();
Assert.False(listenOptions.HttpsOptions.IsValueCreated);
listenOptions.Build();
Assert.True(listenOptions.HttpsOptions.IsValueCreated);
Assert.Equal(listenOptions.HttpsOptions.Value.ServerCertificate?.SerialNumber, certificate.SerialNumber);
}
finally
{
if (File.Exists(GetCertificatePath()))
{
File.Delete(GetCertificatePath());
}
}
}

[Fact]
public void LoadDevelopmentCertificate_UseHttpsFirst()
{
try
{
var serverOptions = CreateServerOptions();
var certificate = new X509Certificate2(TestResources.GetCertPath("aspnetdevcert.pfx"), "testPassword", X509KeyStorageFlags.Exportable);
var bytes = certificate.Export(X509ContentType.Pkcs12, "1234");
var path = GetCertificatePath();
Directory.CreateDirectory(Path.GetDirectoryName(path));
File.WriteAllBytes(path, bytes);

var ran1 = false;
serverOptions.ListenAnyIP(4545, listenOptions =>
{
ran1 = true;
listenOptions.UseHttps();
});
Assert.True(ran1);

var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Certificates:Development:Password", "1234"),
}).Build();

serverOptions.Configure(config);

Assert.Null(serverOptions.ConfigurationLoader.DefaultCertificate);

serverOptions.ConfigurationLoader.Load();

Assert.NotNull(serverOptions.ConfigurationLoader.DefaultCertificate);
Assert.Equal(serverOptions.ConfigurationLoader.DefaultCertificate.SerialNumber, certificate.SerialNumber);

var listenOptions = serverOptions.CodeBackedListenOptions.Single();
Assert.False(listenOptions.HttpsOptions.IsValueCreated);
listenOptions.Build();
Assert.True(listenOptions.HttpsOptions.IsValueCreated);
Assert.Equal(listenOptions.HttpsOptions.Value.ServerCertificate?.SerialNumber, certificate.SerialNumber);
}
finally
{
if (File.Exists(GetCertificatePath()))
{
File.Delete(GetCertificatePath());
}
}
}

Expand Down Expand Up @@ -862,6 +960,8 @@ public void EndpointConfigureSection_CanSetSslProtocol()
});
});

_ = serverOptions.CodeBackedListenOptions.Single().HttpsOptions.Value; // Force evaluation

Assert.True(ranDefault);
Assert.True(ran1);
Assert.True(ran2);
Expand Down Expand Up @@ -997,6 +1097,8 @@ public void EndpointConfigureSection_CanSetClientCertificateMode()
});
});

_ = serverOptions.CodeBackedListenOptions.Single().HttpsOptions.Value; // Force evaluation

Assert.True(ranDefault);
Assert.True(ran1);
Assert.True(ran2);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ void ConfigureListenOptions(ListenOptions listenOptions)
[Fact]
public void ThrowsWhenNoServerCertificateIsProvided()
{
Assert.Throws<ArgumentException>(() => CreateMiddleware(new HttpsConnectionAdapterOptions()));
Assert.Throws<ArgumentException>(() => CreateMiddleware(new HttpsConnectionAdapterOptions(), ListenOptions.DefaultHttpProtocols));
}

[Fact]
Expand Down Expand Up @@ -1318,7 +1318,8 @@ public void ValidatesEnhancedKeyUsageOnCertificate(string testCertName)
CreateMiddleware(new HttpsConnectionAdapterOptions
{
ServerCertificate = cert,
});
},
ListenOptions.DefaultHttpProtocols);
}

[Theory]
Expand All @@ -1337,7 +1338,8 @@ public void ThrowsForCertificatesMissingServerEku(string testCertName)
CreateMiddleware(new HttpsConnectionAdapterOptions
{
ServerCertificate = cert,
}));
},
ListenOptions.DefaultHttpProtocols));

Assert.Equal(CoreStrings.FormatInvalidServerCertificateEku(cert.Thumbprint), ex.Message);
}
Expand All @@ -1357,6 +1359,7 @@ public void LogsForCertificateMissingSubjectAlternativeName(string testCertName)
{
ServerCertificate = cert,
},
ListenOptions.DefaultHttpProtocols,
testLogger);

Assert.Single(testLogger.Messages.Where(log => log.EventId == 9));
Expand Down Expand Up @@ -1404,11 +1407,10 @@ public void Http1AndHttp2DowngradeToHttp1ForHttpsOnIncompatibleWindowsVersions()
var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions
{
ServerCertificate = _x509Certificate2,
HttpProtocols = HttpProtocols.Http1AndHttp2
};
CreateMiddleware(httpConnectionAdapterOptions);
var middleware = CreateMiddleware(httpConnectionAdapterOptions, HttpProtocols.Http1AndHttp2);

Assert.Equal(HttpProtocols.Http1, httpConnectionAdapterOptions.HttpProtocols);
Assert.Equal(HttpProtocols.Http1, middleware._httpProtocols);
}

[ConditionalFact]
Expand All @@ -1419,11 +1421,10 @@ public void Http1AndHttp2DoesNotDowngradeOnCompatibleWindowsVersions()
var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions
{
ServerCertificate = _x509Certificate2,
HttpProtocols = HttpProtocols.Http1AndHttp2
};
CreateMiddleware(httpConnectionAdapterOptions);
var middleware = CreateMiddleware(httpConnectionAdapterOptions, HttpProtocols.Http1AndHttp2);

Assert.Equal(HttpProtocols.Http1AndHttp2, httpConnectionAdapterOptions.HttpProtocols);
Assert.Equal(HttpProtocols.Http1AndHttp2, middleware._httpProtocols);
}

[ConditionalFact]
Expand All @@ -1434,10 +1435,9 @@ public void Http2ThrowsOnIncompatibleWindowsVersions()
var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions
{
ServerCertificate = _x509Certificate2,
HttpProtocols = HttpProtocols.Http2
};

Assert.Throws<NotSupportedException>(() => CreateMiddleware(httpConnectionAdapterOptions));
Assert.Throws<NotSupportedException>(() => CreateMiddleware(httpConnectionAdapterOptions, HttpProtocols.Http2));
}

[ConditionalFact]
Expand All @@ -1448,30 +1448,30 @@ public void Http2DoesNotThrowOnCompatibleWindowsVersions()
var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions
{
ServerCertificate = _x509Certificate2,
HttpProtocols = HttpProtocols.Http2
};

// Does not throw
CreateMiddleware(httpConnectionAdapterOptions);
CreateMiddleware(httpConnectionAdapterOptions, HttpProtocols.Http2);
}

private static HttpsConnectionMiddleware CreateMiddleware(X509Certificate2 serverCertificate)
{
return CreateMiddleware(new HttpsConnectionAdapterOptions
{
ServerCertificate = serverCertificate,
});
},
ListenOptions.DefaultHttpProtocols);
}

private static HttpsConnectionMiddleware CreateMiddleware(HttpsConnectionAdapterOptions options, TestApplicationErrorLogger testLogger = null)
private static HttpsConnectionMiddleware CreateMiddleware(HttpsConnectionAdapterOptions options, HttpProtocols httpProtocols, TestApplicationErrorLogger testLogger = null)
{
var loggerFactory = testLogger is null ? (ILoggerFactory)NullLoggerFactory.Instance : new LoggerFactory(new[] { new KestrelTestLoggerProvider(testLogger) });
return new HttpsConnectionMiddleware(context => Task.CompletedTask, options, loggerFactory, new KestrelMetrics(new TestMeterFactory()));
return new HttpsConnectionMiddleware(context => Task.CompletedTask, options, httpProtocols, loggerFactory, new KestrelMetrics(new TestMeterFactory()));
}

private static HttpsConnectionMiddleware CreateMiddleware(HttpsConnectionAdapterOptions options)
private static HttpsConnectionMiddleware CreateMiddleware(HttpsConnectionAdapterOptions options, HttpProtocols httpProtocols)
{
return new HttpsConnectionMiddleware(context => Task.CompletedTask, options, new KestrelMetrics(new TestMeterFactory()));
return new HttpsConnectionMiddleware(context => Task.CompletedTask, options, httpProtocols, new KestrelMetrics(new TestMeterFactory()));
}

private static async Task App(HttpContext httpContext)
Expand Down
Loading