Skip to content
Merged
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 @@ -72,6 +72,12 @@ 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
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public async Task<EndPoint> BindAsync(EndPoint endPoint, MultiplexedConnectionDe
// The QUIC transport will check if TlsConnectionCallbackOptions is missing.
if (listenOptions.HttpsOptions != null)
{
var sslServerAuthenticationOptions = HttpsConnectionMiddleware.CreateHttp3Options(listenOptions.HttpsOptions.Value);
var sslServerAuthenticationOptions = HttpsConnectionMiddleware.CreateHttp3Options(listenOptions.HttpsOptions);
features.Set(new TlsConnectionCallbackOptions
{
ApplicationProtocols = sslServerAuthenticationOptions.ApplicationProtocols ?? new List<SslApplicationProtocol> { SslApplicationProtocol.Http3 },
Expand Down
3 changes: 1 addition & 2 deletions src/Servers/Kestrel/Core/src/ListenOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,7 @@ internal string Scheme
}

internal bool IsTls { get; set; }
/// <remarks>Should not be inspected until the configuration has been loaded.</remarks>
internal Lazy<HttpsConnectionAdapterOptions>? HttpsOptions { get; set; }
internal HttpsConnectionAdapterOptions? HttpsOptions { get; set; }
internal TlsHandshakeCallbackOptions? HttpsCallbackOptions { get; set; }

/// <summary>
Expand Down
60 changes: 28 additions & 32 deletions src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,24 +163,33 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, Action<Ht
{
ArgumentNullException.ThrowIfNull(configureOptions);

return listenOptions.UseHttps(new Lazy<HttpsConnectionAdapterOptions>(() =>
var options = new HttpsConnectionAdapterOptions();
listenOptions.KestrelServerOptions.ApplyHttpsDefaults(options);
configureOptions(options);
listenOptions.KestrelServerOptions.ApplyDefaultCert(options);

if (options.ServerCertificate == null && options.ServerCertificateSelector == null)
{
// We defer configuration of the https options until build time so that the IConfiguration will be available.
// This is particularly important in docker containers, where the docker tools use IConfiguration to tell
// us where the development certificates have been mounted.
throw new InvalidOperationException(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound);
}

var options = new HttpsConnectionAdapterOptions();
listenOptions.KestrelServerOptions.ApplyHttpsDefaults(options);
configureOptions(options);
listenOptions.KestrelServerOptions.ApplyDefaultCert(options);
return listenOptions.UseHttps(options);
}

if (options.ServerCertificate == null && options.ServerCertificateSelector == null)
{
throw new InvalidOperationException(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound);
}
// Use Https if a default cert is available
internal static bool TryUseHttps(this ListenOptions listenOptions)
{
var options = new HttpsConnectionAdapterOptions();
listenOptions.KestrelServerOptions.ApplyHttpsDefaults(options);
listenOptions.KestrelServerOptions.ApplyDefaultCert(options);

return options;
}));
if (options.ServerCertificate == null && options.ServerCertificateSelector == null)
{
return false;
}

listenOptions.UseHttps(options);
return true;
}

/// <summary>
Expand All @@ -192,28 +201,16 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, Action<Ht
/// <returns>The <see cref="ListenOptions"/>.</returns>
public static ListenOptions UseHttps(this ListenOptions listenOptions, HttpsConnectionAdapterOptions httpsOptions)
{
return listenOptions.UseHttps(new Lazy<HttpsConnectionAdapterOptions>(httpsOptions));
}
var loggerFactory = listenOptions.KestrelServerOptions?.ApplicationServices.GetRequiredService<ILoggerFactory>() ?? NullLoggerFactory.Instance;

/// <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 = lazyHttpsOptions;
listenOptions.HttpsOptions = httpsOptions;

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

Expand Down Expand Up @@ -273,7 +270,6 @@ 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,9 +33,6 @@ 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 SslStreamCertificateContext? _serverCertificateContext;
Expand All @@ -45,16 +42,17 @@ 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, HttpProtocols httpProtocols)
: this(next, options, httpProtocols, loggerFactory: NullLoggerFactory.Instance)
public HttpsConnectionMiddleware(ConnectionDelegate next, HttpsConnectionAdapterOptions options)
: this(next, options, loggerFactory: NullLoggerFactory.Instance)
{
}

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

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

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

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

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

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

Expand Down
102 changes: 0 additions & 102 deletions src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -252,104 +252,6 @@ public void ConfigureEndpointDevelopmentCertificateGetsLoadedWhenPresent()
}
}

[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.DefaultCertificate);

serverOptions.ConfigurationLoader.Load();

Assert.NotNull(serverOptions.DefaultCertificate);
Assert.Equal(serverOptions.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.DefaultCertificate);

serverOptions.ConfigurationLoader.Load();

Assert.NotNull(serverOptions.DefaultCertificate);
Assert.Equal(serverOptions.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());
}
}
}

[Fact]
public void ConfigureEndpoint_ThrowsWhen_The_PasswordIsMissing()
{
Expand Down Expand Up @@ -828,8 +730,6 @@ public void EndpointConfigureSection_CanSetSslProtocol()
});
});

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

Assert.True(ranDefault);
Assert.True(ran1);
Assert.True(ran2);
Expand Down Expand Up @@ -965,8 +865,6 @@ 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 @@ -228,8 +228,7 @@ void ConfigureListenOptions(ListenOptions listenOptions)
public void ThrowsWhenNoServerCertificateIsProvided()
{
Assert.Throws<ArgumentException>(() => new HttpsConnectionMiddleware(context => Task.CompletedTask,
new HttpsConnectionAdapterOptions(),
ListenOptions.DefaultHttpProtocols)
new HttpsConnectionAdapterOptions())
);
}

Expand Down Expand Up @@ -1269,8 +1268,7 @@ public void AcceptsCertificateWithoutExtensions(string testCertName)
new HttpsConnectionMiddleware(context => Task.CompletedTask, new HttpsConnectionAdapterOptions
{
ServerCertificate = cert,
},
ListenOptions.DefaultHttpProtocols);
});
}

[Theory]
Expand All @@ -1288,8 +1286,7 @@ public void ValidatesEnhancedKeyUsageOnCertificate(string testCertName)
new HttpsConnectionMiddleware(context => Task.CompletedTask, new HttpsConnectionAdapterOptions
{
ServerCertificate = cert,
},
ListenOptions.DefaultHttpProtocols);
});
}

[Theory]
Expand All @@ -1308,7 +1305,7 @@ public void ThrowsForCertificatesMissingServerEku(string testCertName)
new HttpsConnectionMiddleware(context => Task.CompletedTask, new HttpsConnectionAdapterOptions
{
ServerCertificate = cert,
}, ListenOptions.DefaultHttpProtocols));
}));

Assert.Equal(CoreStrings.FormatInvalidServerCertificateEku(cert.Thumbprint), ex.Message);
}
Expand Down Expand Up @@ -1355,10 +1352,11 @@ public void Http1AndHttp2DowngradeToHttp1ForHttpsOnIncompatibleWindowsVersions()
var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions
{
ServerCertificate = _x509Certificate2,
HttpProtocols = HttpProtocols.Http1AndHttp2
};
var middleware = new HttpsConnectionMiddleware(context => Task.CompletedTask, httpConnectionAdapterOptions, HttpProtocols.Http1AndHttp2);
new HttpsConnectionMiddleware(context => Task.CompletedTask, httpConnectionAdapterOptions);

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

[ConditionalFact]
Expand All @@ -1369,10 +1367,11 @@ public void Http1AndHttp2DoesNotDowngradeOnCompatibleWindowsVersions()
var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions
{
ServerCertificate = _x509Certificate2,
HttpProtocols = HttpProtocols.Http1AndHttp2
};
var middleware = new HttpsConnectionMiddleware(context => Task.CompletedTask, httpConnectionAdapterOptions, HttpProtocols.Http1AndHttp2);
new HttpsConnectionMiddleware(context => Task.CompletedTask, httpConnectionAdapterOptions);

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

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

Assert.Throws<NotSupportedException>(() => new HttpsConnectionMiddleware(context => Task.CompletedTask, httpConnectionAdapterOptions, HttpProtocols.Http2));
Assert.Throws<NotSupportedException>(() => new HttpsConnectionMiddleware(context => Task.CompletedTask, httpConnectionAdapterOptions));
}

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

// Does not throw
new HttpsConnectionMiddleware(context => Task.CompletedTask, httpConnectionAdapterOptions, HttpProtocols.Http2);
new HttpsConnectionMiddleware(context => Task.CompletedTask, httpConnectionAdapterOptions);
}

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