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 @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ 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();

// If there's a configuration, load it so that the results will be available to ApplyDefaultCertificate
listenOptions.KestrelServerOptions.ConfigurationLoader?.LoadInternal();

var options = new HttpsConnectionAdapterOptions();
listenOptions.KestrelServerOptions.ApplyHttpsDefaults(options);
configureOptions(options);
Expand Down Expand Up @@ -196,9 +199,7 @@ public static ListenOptions UseHttps(this ListenOptions listenOptions, HttpsConn

listenOptions.Use(next =>
{
// Set the list of protocols from listen options
httpsOptions.HttpProtocols = listenOptions.Protocols;
var middleware = new HttpsConnectionMiddleware(next, httpsOptions, loggerFactory, metrics);
var middleware = new HttpsConnectionMiddleware(next, httpsOptions, listenOptions.Protocols, loggerFactory, metrics);
return middleware.OnConnectionAsync;
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,19 @@ 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;

// Internal for testing
internal 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 @@ -402,7 +402,156 @@ 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!.ServerCertificate.SerialNumber);
}
}

[Fact]
public void LoadDevelopmentCertificate_LoadBeforeUseHttps()
{
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();
listenOptions.Build();
Assert.Equal(listenOptions.HttpsOptions.ServerCertificate?.SerialNumber, certificate.SerialNumber);
}
finally
{
if (File.Exists(GetCertificatePath()))
{
File.Delete(GetCertificatePath());
}
}
}

[Fact]
public void LoadDevelopmentCertificate_UseHttpsBeforeLoad()
{
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);

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

// Use Https triggers a load, so the default cert is already set
Assert.NotNull(serverOptions.ConfigurationLoader.DefaultCertificate);
Assert.Equal(serverOptions.ConfigurationLoader.DefaultCertificate.SerialNumber, certificate.SerialNumber);

// This Load is a no-op (tested elsewhere)
serverOptions.ConfigurationLoader.Load();

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

[Fact]
public void LoadDevelopmentCertificate_UseHttpsBeforeConfigure()
{
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 defaultCertificate = TestResources.GetTestCertificate();
Assert.NotEqual(certificate.SerialNumber, defaultCertificate.SerialNumber);
serverOptions.TestOverrideDefaultCertificate = defaultCertificate;

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();
listenOptions.Build();
// In a perfect world, it would match certificate.SerialNumber, but there's no way for an eager UseHttps
// to do that before Configure is called.
Assert.Equal(listenOptions.HttpsOptions.ServerCertificate?.SerialNumber, defaultCertificate.SerialNumber);
}
finally
{
if (File.Exists(GetCertificatePath()))
{
File.Delete(GetCertificatePath());
}
}
}

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