diff --git a/src/Kestrel.Core/Internal/ConfigurationReader.cs b/src/Kestrel.Core/Internal/ConfigurationReader.cs index b36a9af94e..be954d904b 100644 --- a/src/Kestrel.Core/Internal/ConfigurationReader.cs +++ b/src/Kestrel.Core/Internal/ConfigurationReader.cs @@ -9,9 +9,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal { internal class ConfigurationReader { + private const string ProtocolsKey = "Protocols"; + private const string CertificatesKey = "Certificates"; + private const string CertificateKey = "Certificate"; + private const string EndpointDefaultsKey = "EndpointDefaults"; + private const string EndpointsKey = "Endpoints"; + private const string UrlKey = "Url"; + private IConfiguration _configuration; private IDictionary _certificates; private IList _endpoints; + private EndpointDefaults _endpointDefaults; public ConfigurationReader(IConfiguration configuration) { @@ -31,6 +39,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal } } + public EndpointDefaults EndpointDefaults + { + get + { + if (_endpointDefaults == null) + { + ReadEndpointDefaults(); + } + + return _endpointDefaults; + } + } + public IEnumerable Endpoints { get @@ -48,29 +69,42 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal { _certificates = new Dictionary(0); - var certificatesConfig = _configuration.GetSection("Certificates").GetChildren(); + var certificatesConfig = _configuration.GetSection(CertificatesKey).GetChildren(); foreach (var certificateConfig in certificatesConfig) { _certificates.Add(certificateConfig.Key, new CertificateConfig(certificateConfig)); } } + // "EndpointDefaults": { + // "Protocols": "Http1AndHttp2", + // } + private void ReadEndpointDefaults() + { + var configSection = _configuration.GetSection(EndpointDefaultsKey); + _endpointDefaults = new EndpointDefaults() + { + Protocols = ParseProtocols(configSection[ProtocolsKey]) + }; + } + private void ReadEndpoints() { _endpoints = new List(); - var endpointsConfig = _configuration.GetSection("Endpoints").GetChildren(); + var endpointsConfig = _configuration.GetSection(EndpointsKey).GetChildren(); foreach (var endpointConfig in endpointsConfig) { // "EndpointName": { -        // "Url": "https://*:5463", -        // "Certificate": { -          // "Path": "testCert.pfx", -          // "Password": "testPassword" -       // } + // "Url": "https://*:5463", + // "Protocols": "Http1AndHttp2", + // "Certificate": { + // "Path": "testCert.pfx", + // "Password": "testPassword" + // } // } - - var url = endpointConfig["Url"]; + + var url = endpointConfig[UrlKey]; if (string.IsNullOrEmpty(url)) { throw new InvalidOperationException(CoreStrings.FormatEndpointMissingUrl(endpointConfig.Key)); @@ -80,16 +114,37 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal { Name = endpointConfig.Key, Url = url, + Protocols = ParseProtocols(endpointConfig[ProtocolsKey]), ConfigSection = endpointConfig, - Certificate = new CertificateConfig(endpointConfig.GetSection("Certificate")), + Certificate = new CertificateConfig(endpointConfig.GetSection(CertificateKey)), }; _endpoints.Add(endpoint); } } + + private static HttpProtocols? ParseProtocols(string protocols) + { + if (Enum.TryParse(protocols, ignoreCase: true, out var result)) + { + return result; + } + + return null; + } + } + + // "EndpointDefaults": { + // "Protocols": "Http1AndHttp2", + // } + internal class EndpointDefaults + { + public HttpProtocols? Protocols { get; set; } + public IConfigurationSection ConfigSection { get; set; } } // "EndpointName": { // "Url": "https://*:5463", + // "Protocols": "Http1AndHttp2", // "Certificate": { // "Path": "testCert.pfx", // "Password": "testPassword" @@ -99,6 +154,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal { public string Name { get; set; } public string Url { get; set; } + public HttpProtocols? Protocols { get; set; } public IConfigurationSection ConfigSection { get; set; } public CertificateConfig Certificate { get; set; } } diff --git a/src/Kestrel.Core/KestrelConfigurationLoader.cs b/src/Kestrel.Core/KestrelConfigurationLoader.cs index 60204443f6..e568aec6c7 100644 --- a/src/Kestrel.Core/KestrelConfigurationLoader.cs +++ b/src/Kestrel.Core/KestrelConfigurationLoader.cs @@ -23,14 +23,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel { public class KestrelConfigurationLoader { + private bool _loaded = false; + internal KestrelConfigurationLoader(KestrelServerOptions options, IConfiguration configuration) { Options = options ?? throw new ArgumentNullException(nameof(options)); Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + ConfigurationReader = new ConfigurationReader(Configuration); } public KestrelServerOptions Options { get; } public IConfiguration Configuration { get; } + internal ConfigurationReader ConfigurationReader { get; } private IDictionary> EndpointConfigurations { get; } = new Dictionary>(0, StringComparer.OrdinalIgnoreCase); // Actions that will be delayed until Load so that they aren't applied if the configuration loader is replaced. @@ -197,24 +201,39 @@ namespace Microsoft.AspNetCore.Server.Kestrel return this; } + // Called from ApplyEndpointDefaults so it applies to even explicit Listen endpoints. + // Does not require a call to Load. + internal void ApplyConfigurationDefaults(ListenOptions listenOptions) + { + var defaults = ConfigurationReader.EndpointDefaults; + + if (defaults.Protocols.HasValue) + { + listenOptions.Protocols = defaults.Protocols.Value; + } + } + public void Load() { - if (Options.ConfigurationLoader == null) + if (_loaded) { // The loader has already been run. return; } - Options.ConfigurationLoader = null; + _loaded = true; - var configReader = new ConfigurationReader(Configuration); + LoadDefaultCert(ConfigurationReader); - LoadDefaultCert(configReader); - - foreach (var endpoint in configReader.Endpoints) + foreach (var endpoint in ConfigurationReader.Endpoints) { var listenOptions = AddressBinder.ParseAddress(endpoint.Url, out var https); Options.ApplyEndpointDefaults(listenOptions); + if (endpoint.Protocols.HasValue) + { + listenOptions.Protocols = endpoint.Protocols.Value; + } + // Compare to UseHttps(httpsOptions => { }) var httpsOptions = new HttpsConnectionAdapterOptions(); if (https) diff --git a/src/Kestrel.Core/KestrelServerOptions.cs b/src/Kestrel.Core/KestrelServerOptions.cs index 187728a716..0471b0d1c9 100644 --- a/src/Kestrel.Core/KestrelServerOptions.cs +++ b/src/Kestrel.Core/KestrelServerOptions.cs @@ -103,6 +103,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core internal void ApplyEndpointDefaults(ListenOptions listenOptions) { listenOptions.KestrelServerOptions = this; + ConfigurationLoader?.ApplyConfigurationDefaults(listenOptions); EndpointDefaults(listenOptions); } diff --git a/test/Kestrel.Tests/KestrelConfigurationBuilderTests.cs b/test/Kestrel.Tests/KestrelConfigurationBuilderTests.cs index 33caeb1d34..587ca4c8d3 100644 --- a/test/Kestrel.Tests/KestrelConfigurationBuilderTests.cs +++ b/test/Kestrel.Tests/KestrelConfigurationBuilderTests.cs @@ -81,13 +81,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tests Assert.Single(serverOptions.ListenOptions); Assert.Equal(5001, serverOptions.ListenOptions[0].IPEndPoint.Port); - Assert.Null(serverOptions.ConfigurationLoader); + Assert.NotNull(serverOptions.ConfigurationLoader); builder.Load(); Assert.Single(serverOptions.ListenOptions); Assert.Equal(5001, serverOptions.ListenOptions[0].IPEndPoint.Port); - Assert.Null(serverOptions.ConfigurationLoader); + Assert.NotNull(serverOptions.ConfigurationLoader); } [Fact] @@ -131,6 +131,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tests serverOptions.ConfigureEndpointDefaults(opt => { opt.NoDelay = false; + opt.Protocols = HttpProtocols.Http2; }); serverOptions.ConfigureHttpsDefaults(opt => @@ -153,11 +154,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tests Assert.NotNull(opt.HttpsOptions.ServerCertificate); Assert.Equal(ClientCertificateMode.RequireCertificate, opt.HttpsOptions.ClientCertificateMode); Assert.False(opt.ListenOptions.NoDelay); + Assert.Equal(HttpProtocols.Http2, opt.ListenOptions.Protocols); }) .LocalhostEndpoint(5002, opt => { ran2 = true; Assert.False(opt.NoDelay); + Assert.Equal(HttpProtocols.Http2, opt.Protocols); }) .Load(); @@ -316,6 +319,119 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tests } } + [Theory] + [InlineData("http1", HttpProtocols.Http1)] + [InlineData("http2", HttpProtocols.Http2)] + [InlineData("http1AndHttp2", HttpProtocols.Http1AndHttp2)] + public void DefaultConfigSectionCanSetProtocols(string input, HttpProtocols expected) + { + var serverOptions = CreateServerOptions(); + var ranDefault = false; + serverOptions.ConfigureEndpointDefaults(opt => + { + Assert.Equal(expected, opt.Protocols); + ranDefault = true; + }); + + serverOptions.ConfigureHttpsDefaults(opt => + { + opt.ServerCertificate = new X509Certificate2(TestResources.TestCertificatePath, "testPassword"); + opt.ClientCertificateMode = ClientCertificateMode.RequireCertificate; + }); + + var ran1 = false; + var ran2 = false; + var ran3 = false; + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("EndpointDefaults:Protocols", input), + new KeyValuePair("Endpoints:End1:Url", "https://*:5001"), + }).Build(); + serverOptions.Configure(config) + .Endpoint("End1", opt => + { + Assert.True(opt.IsHttps); + Assert.NotNull(opt.HttpsOptions.ServerCertificate); + Assert.Equal(ClientCertificateMode.RequireCertificate, opt.HttpsOptions.ClientCertificateMode); + Assert.Equal(expected, opt.ListenOptions.Protocols); + ran1 = true; + }) + .LocalhostEndpoint(5002, opt => + { + Assert.Equal(expected, opt.Protocols); + ran2 = true; + }) + .Load(); + serverOptions.ListenAnyIP(0, opt => + { + Assert.Equal(expected, opt.Protocols); + ran3 = true; + }); + + Assert.True(ranDefault); + Assert.True(ran1); + Assert.True(ran2); + Assert.True(ran3); + } + + [Theory] + [InlineData("http1", HttpProtocols.Http1)] + [InlineData("http2", HttpProtocols.Http2)] + [InlineData("http1AndHttp2", HttpProtocols.Http1AndHttp2)] + public void EndpointConfigSectionCanSetProtocols(string input, HttpProtocols expected) + { + var serverOptions = CreateServerOptions(); + var ranDefault = false; + serverOptions.ConfigureEndpointDefaults(opt => + { + // Kestrel default. + Assert.Equal(HttpProtocols.Http1AndHttp2, opt.Protocols); + ranDefault = true; + }); + + serverOptions.ConfigureHttpsDefaults(opt => + { + opt.ServerCertificate = new X509Certificate2(TestResources.TestCertificatePath, "testPassword"); + opt.ClientCertificateMode = ClientCertificateMode.RequireCertificate; + }); + + var ran1 = false; + var ran2 = false; + var ran3 = false; + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("Endpoints:End1:Protocols", input), + new KeyValuePair("Endpoints:End1:Url", "https://*:5001"), + }).Build(); + serverOptions.Configure(config) + .Endpoint("End1", opt => + { + Assert.True(opt.IsHttps); + Assert.NotNull(opt.HttpsOptions.ServerCertificate); + Assert.Equal(ClientCertificateMode.RequireCertificate, opt.HttpsOptions.ClientCertificateMode); + Assert.Equal(expected, opt.ListenOptions.Protocols); + ran1 = true; + }) + .LocalhostEndpoint(5002, opt => + { + // Kestrel default. + Assert.Equal(HttpProtocols.Http1AndHttp2, opt.Protocols); + ran2 = true; + }) + .Load(); + serverOptions.ListenAnyIP(0, opt => + { + // Kestrel default. + Assert.Equal(HttpProtocols.Http1AndHttp2, opt.Protocols); + ran3 = true; + }); + + Assert.True(ranDefault); + Assert.True(ran1); + Assert.True(ran2); + Assert.True(ran3); + } + private static string GetCertificatePath() { var appData = Environment.GetEnvironmentVariable("APPDATA"); diff --git a/test/Kestrel.Transport.BindTests/AddressRegistrationTests.cs b/test/Kestrel.Transport.BindTests/AddressRegistrationTests.cs index a67d4bcaed..219db7c252 100644 --- a/test/Kestrel.Transport.BindTests/AddressRegistrationTests.cs +++ b/test/Kestrel.Transport.BindTests/AddressRegistrationTests.cs @@ -20,6 +20,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Xunit; @@ -770,6 +771,36 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } + [Theory] + [InlineData("http1", HttpProtocols.Http1)] + [InlineData("http2", HttpProtocols.Http2)] + [InlineData("http1AndHttp2", HttpProtocols.Http1AndHttp2)] + public void EndpointDefaultsConfig_CanSetProtocolForUrlsConfig(string input, HttpProtocols expected) + { + KestrelServerOptions capturedOptions = null; + var hostBuilder = TransportSelector.GetWebHostBuilder() + .UseKestrel(options => + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("EndpointDefaults:Protocols", input), + }).Build(); + options.Configure(config); + + capturedOptions = options; + }) + .ConfigureServices(AddTestLogging) + .UseUrls("http://127.0.0.1:0") + .Configure(ConfigureEchoAddress); + + using (var host = hostBuilder.Build()) + { + host.Start(); + Assert.Single(capturedOptions.ListenOptions); + Assert.Equal(expected, capturedOptions.ListenOptions[0].Protocols); + } + } + private void ThrowsWhenBindingLocalhostToAddressInUse(AddressFamily addressFamily) { TestApplicationErrorLogger.IgnoredExceptions.Add(typeof(IOException));