Map ListenOptions.Protocols from IConfiguration #2903

This commit is contained in:
Chris Ross (ASP.NET) 2018-09-09 22:03:05 -07:00
parent de5ccb5c78
commit f38f60f8ce
5 changed files with 241 additions and 18 deletions

View File

@ -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<string, CertificateConfig> _certificates;
private IList<EndpointConfig> _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<EndpointConfig> Endpoints
{
get
@ -48,29 +69,42 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
{
_certificates = new Dictionary<string, CertificateConfig>(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<EndpointConfig>();
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<HttpProtocols>(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; }
}

View File

@ -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<string, Action<EndpointConfiguration>> EndpointConfigurations { get; }
= new Dictionary<string, Action<EndpointConfiguration>>(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)

View File

@ -103,6 +103,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
internal void ApplyEndpointDefaults(ListenOptions listenOptions)
{
listenOptions.KestrelServerOptions = this;
ConfigurationLoader?.ApplyConfigurationDefaults(listenOptions);
EndpointDefaults(listenOptions);
}

View File

@ -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<string, string>("EndpointDefaults:Protocols", input),
new KeyValuePair<string, string>("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<string, string>("Endpoints:End1:Protocols", input),
new KeyValuePair<string, string>("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");

View File

@ -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<string, string>("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));