From c6e228d1768c96f8a416f1e4886af3ef18eb1b17 Mon Sep 17 00:00:00 2001 From: Chris R Date: Thu, 1 Jun 2017 08:51:12 -0700 Subject: [PATCH] #1875 Add Configuration support and tests. --- KestrelHttpServer.sln | 10 ++ .../ConfigureDefaultKestrelServerOptions.cs | 116 +++++++++++++ .../KestrelStrings.resx | 135 ++++++++++++++++ ...Microsoft.AspNetCore.Server.Kestrel.csproj | 5 +- .../Properties/KestrelStrings.Designer.cs | 100 ++++++++++++ .../WebHostBuilderKestrelExtensions.cs | 5 + .../ConfigurationTests.cs | 153 ++++++++++++++++++ 7 files changed, 523 insertions(+), 1 deletion(-) create mode 100644 src/Microsoft.AspNetCore.Server.Kestrel/Internal/ConfigureDefaultKestrelServerOptions.cs create mode 100644 src/Microsoft.AspNetCore.Server.Kestrel/KestrelStrings.resx create mode 100644 src/Microsoft.AspNetCore.Server.Kestrel/Properties/KestrelStrings.Designer.cs create mode 100644 test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/ConfigurationTests.cs diff --git a/KestrelHttpServer.sln b/KestrelHttpServer.sln index b5b6dc71f3..d907c16aee 100644 --- a/KestrelHttpServer.sln +++ b/KestrelHttpServer.sln @@ -71,6 +71,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.Kestrel.Tests", "test\Microsoft.AspNetCore.Server.Kestrel.Tests\Microsoft.AspNetCore.Server.Kestrel.Tests.csproj", "{4F1C30F8-CCAA-48D7-9DF6-2A84021F5BCC}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{FD3692F8-F5C7-49A5-8D69-29F8A87A9DB7}" + ProjectSection(SolutionItems) = preProject + build\common.props = build\common.props + build\dependencies.props = build\dependencies.props + build\Key.snk = build\Key.snk + build\repo.props = build\repo.props + build\repo.targets = build\repo.targets + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -270,5 +279,6 @@ Global {2E9CB89D-EC8F-4DD9-A72B-08D5BABF752D} = {2D5D5227-4DBD-499A-96B1-76A36B03B750} {D95A7EC3-48AC-4D03-B2E2-0DA3E13BD3A4} = {D3273454-EA07-41D2-BF0B-FCC3675C2483} {4F1C30F8-CCAA-48D7-9DF6-2A84021F5BCC} = {D3273454-EA07-41D2-BF0B-FCC3675C2483} + {FD3692F8-F5C7-49A5-8D69-29F8A87A9DB7} = {7972A5D6-3385-4127-9277-428506DD44FF} EndGlobalSection EndGlobal diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/ConfigureDefaultKestrelServerOptions.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/ConfigureDefaultKestrelServerOptions.cs new file mode 100644 index 0000000000..02e1f0d561 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/ConfigureDefaultKestrelServerOptions.cs @@ -0,0 +1,116 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Certificates.Configuration; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Infrastructure; + +namespace Microsoft.AspNetCore.Server.Kestrel.Internal +{ + public class ConfigureDefaultKestrelServerOptions : ConfigureDefaultOptions + { + private const string DefaultCertificateSubjectName = "CN=localhost"; + private const string DevelopmentSSLCertificateName = "localhost"; + + private readonly IServiceProvider _services; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IConfiguration _configurationRoot; + private readonly ILoggerFactory _loggerFactory; + + public ConfigureDefaultKestrelServerOptions( + IServiceProvider services, + IHostingEnvironment hostingEnvironment, + IConfiguration configurationRoot, + ILoggerFactory loggerFactory) + { + _services = services; + _hostingEnvironment = hostingEnvironment; + _configurationRoot = configurationRoot; + _loggerFactory = loggerFactory; + } + + public override void Configure(string name, KestrelServerOptions options) + { + // Don't assume KestrelServerOptionsSetup has already set the services. Needed for UseHttps. + options.ApplicationServices = _services; + BindConfiguration(options); + } + + private void BindConfiguration(KestrelServerOptions options) + { + var certificateLoader = new CertificateLoader(_configurationRoot.GetSection("Certificates"), _loggerFactory, _hostingEnvironment.EnvironmentName); + + foreach (var endPoint in _configurationRoot.GetSection("Microsoft:AspNetCore:Server:Kestrel:EndPoints").GetChildren()) + { + BindEndPoint(options, endPoint, certificateLoader); + } + } + + private void BindEndPoint( + KestrelServerOptions options, + IConfigurationSection endPoint, + CertificateLoader certificateLoader) + { + var configAddress = endPoint.GetValue("Address"); + var configPort = endPoint.GetValue("Port"); + + if (!IPAddress.TryParse(configAddress, out var address)) + { + throw new InvalidOperationException(KestrelStrings.FormatInvalidIp(configAddress)); + } + + if (!int.TryParse(configPort, out var port)) + { + throw new InvalidOperationException(KestrelStrings.FormatInvalidPort(configPort)); + } + + options.Listen(address, port, listenOptions => + { + var certificateConfig = endPoint.GetSection("Certificate"); + X509Certificate2 certificate = null; + if (certificateConfig.Exists()) + { + try + { + try + { + certificate = certificateLoader.Load(certificateConfig).FirstOrDefault(); + } + catch (KeyNotFoundException) when (certificateConfig.Value.Equals(DevelopmentSSLCertificateName, StringComparison.Ordinal) && _hostingEnvironment.IsDevelopment()) + { + var storeLoader = new CertificateStoreLoader(); + certificate = storeLoader.Load(DefaultCertificateSubjectName, "My", StoreLocation.CurrentUser, validOnly: false) ?? + storeLoader.Load(DefaultCertificateSubjectName, "My", StoreLocation.LocalMachine, validOnly: false); + + if (certificate == null) + { + var logger = _loggerFactory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel.KestrelServerConfigureOptions"); + logger.LogError(KestrelStrings.NoCertFoundLog); + } + } + + if (certificate == null) + { + throw new InvalidOperationException(KestrelStrings.FormatNoCertForEndpoint(endPoint.Key)); + } + } + catch (Exception ex) + { + throw new InvalidOperationException(KestrelStrings.UnableToConfigureHttps, ex); + } + + listenOptions.UseHttps(certificate); + } + }); + } + } +} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/KestrelStrings.resx b/src/Microsoft.AspNetCore.Server.Kestrel/KestrelStrings.resx new file mode 100644 index 0000000000..bf5c93410c --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Kestrel/KestrelStrings.resx @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Invalid IP address in configuration: {configAddress}. + + + Invalid port in configuration: {configPort}. + + + No certificate found for endpoint {endPoint}. + + + Unable to configure HTTPS endpoint. For information on configuring HTTPS see https://go.microsoft.com/fwlink/?linkid=848054. + + + No HTTPS certificate was found for development. For information on configuring HTTPS see https://go.microsoft.com/fwlink/?linkid=848054. + + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Microsoft.AspNetCore.Server.Kestrel.csproj b/src/Microsoft.AspNetCore.Server.Kestrel/Microsoft.AspNetCore.Server.Kestrel.csproj index 616830814d..fbaa34e0b7 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Microsoft.AspNetCore.Server.Kestrel.csproj +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Microsoft.AspNetCore.Server.Kestrel.csproj @@ -4,7 +4,7 @@ ASP.NET Core Kestrel cross-platform web server. - netstandard2.0 + netstandard2.0;netcoreapp2.0 true aspnetcore;kestrel CS1591;$(NoWarn) @@ -12,11 +12,14 @@ + + + diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Properties/KestrelStrings.Designer.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Properties/KestrelStrings.Designer.cs new file mode 100644 index 0000000000..ee0b31857d --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Properties/KestrelStrings.Designer.cs @@ -0,0 +1,100 @@ +// +namespace Microsoft.AspNetCore.Server.Kestrel +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class KestrelStrings + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNetCore.Server.Kestrel.KestrelStrings", typeof(KestrelStrings).GetTypeInfo().Assembly); + + /// + /// Invalid IP address in configuration: {configAddress}. + /// + internal static string InvalidIp + { + get => GetString("InvalidIp"); + } + + /// + /// Invalid IP address in configuration: {configAddress}. + /// + internal static string FormatInvalidIp(object configAddress) + => string.Format(CultureInfo.CurrentCulture, GetString("InvalidIp", "configAddress"), configAddress); + + /// + /// Invalid port in configuration: {configPort}. + /// + internal static string InvalidPort + { + get => GetString("InvalidPort"); + } + + /// + /// Invalid port in configuration: {configPort}. + /// + internal static string FormatInvalidPort(object configPort) + => string.Format(CultureInfo.CurrentCulture, GetString("InvalidPort", "configPort"), configPort); + + /// + /// No certificate found for endpoint {endPoint}. + /// + internal static string NoCertForEndpoint + { + get => GetString("NoCertForEndpoint"); + } + + /// + /// No certificate found for endpoint {endPoint}. + /// + internal static string FormatNoCertForEndpoint(object endPoint) + => string.Format(CultureInfo.CurrentCulture, GetString("NoCertForEndpoint", "endPoint"), endPoint); + + /// + /// Unable to configure HTTPS endpoint. For information on configuring HTTPS see https://go.microsoft.com/fwlink/?linkid=848054. + /// + internal static string UnableToConfigureHttps + { + get => GetString("UnableToConfigureHttps"); + } + + /// + /// Unable to configure HTTPS endpoint. For information on configuring HTTPS see https://go.microsoft.com/fwlink/?linkid=848054. + /// + internal static string FormatUnableToConfigureHttps() + => GetString("UnableToConfigureHttps"); + + /// + /// No HTTPS certificate was found for development. For information on configuring HTTPS see https://go.microsoft.com/fwlink/?linkid=848054. + /// + internal static string NoCertFoundLog + { + get => GetString("NoCertFoundLog"); + } + + /// + /// No HTTPS certificate was found for development. For information on configuring HTTPS see https://go.microsoft.com/fwlink/?linkid=848054. + /// + internal static string FormatNoCertFoundLog() + => GetString("NoCertFoundLog"); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/WebHostBuilderKestrelExtensions.cs b/src/Microsoft.AspNetCore.Server.Kestrel/WebHostBuilderKestrelExtensions.cs index 3d3dad73cb..0823e62db1 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/WebHostBuilderKestrelExtensions.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/WebHostBuilderKestrelExtensions.cs @@ -3,10 +3,12 @@ using System; using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Server.Kestrel.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Infrastructure; namespace Microsoft.AspNetCore.Hosting { @@ -28,6 +30,9 @@ namespace Microsoft.AspNetCore.Hosting return hostBuilder.ConfigureServices(services => { services.AddTransient, KestrelServerOptionsSetup>(); + // https://github.com/aspnet/DependencyInjection/issues/500 + services.AddTransient, ConfigureDefaults>(); + services.AddTransient, ConfigureDefaultKestrelServerOptions>(); services.AddSingleton(); }); } diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/ConfigurationTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/ConfigurationTests.cs new file mode 100644 index 0000000000..c7581dfd15 --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/ConfigurationTests.cs @@ -0,0 +1,153 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options.Infrastructure; +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +{ + public class ConfigurationTests + { + private void ConfigureEchoAddress(IApplicationBuilder app) + { + app.Run(context => + { + return context.Response.WriteAsync(context.Request.GetDisplayUrl()); + }); + } + + [Fact] + public void BindsKestrelToInvalidIp_FailsToStart() + { + var hostBuilder = new WebHostBuilder() + .UseKestrel() + .UseConfiguration(new ConfigurationBuilder().AddInMemoryCollection(new Dictionary() + { + { "Microsoft:AspNetCore:Server:Kestrel:Endpoints:0:Address", "ABCDEFGH" }, + { "Microsoft:AspNetCore:Server:Kestrel:Endpoints:0:Port", "0" } + }).Build()) + .ConfigureServices(services => + { + // Microsoft.AspNetCore.dll does this + services.AddTransient(typeof(IConfigureOptions<>), typeof(ConfigureDefaults<>)); + }) + .Configure(ConfigureEchoAddress); + + Assert.Throws(() => hostBuilder.Build()); + } + + [Theory] + [InlineData("127.0.0.1", "127.0.0.1")] + [InlineData("::1", "[::1]")] + public async Task BindsKestrelHttpEndPointFromConfiguration(string endPointAddress, string requestAddress) + { + var hostBuilder = new WebHostBuilder() + .UseKestrel() + .UseConfiguration(new ConfigurationBuilder().AddInMemoryCollection(new Dictionary() + { + { "Microsoft:AspNetCore:Server:Kestrel:Endpoints:0:Address", $"{endPointAddress}" }, + { "Microsoft:AspNetCore:Server:Kestrel:Endpoints:0:Port", "0" } + }).Build()) + .ConfigureServices(services => + { + // Microsoft.AspNetCore.dll does this + services.AddTransient(typeof(IConfigureOptions<>), typeof(ConfigureDefaults<>)); + }) + .Configure(ConfigureEchoAddress); + + using (var webHost = hostBuilder.Start()) + { + var port = GetWebHostPort(webHost); + + Assert.NotEqual(5000, port); // Default port + + Assert.NotEqual(0, port); + + using (var client = new HttpClient()) + { + var response = await client.GetStringAsync($"http://{requestAddress}:{port}"); + Assert.Equal($"http://{requestAddress}:{port}/", response); + } + } + } + + [Fact] + public async Task BindsKestrelHttpsEndPointFromConfiguration_ReferencedCertificateFile() + { + var hostBuilder = new WebHostBuilder() + .UseKestrel() + .UseConfiguration(new ConfigurationBuilder().AddInMemoryCollection(new Dictionary() + { + { "Microsoft:AspNetCore:Server:Kestrel:Endpoints:0:Address", "127.0.0.1" }, + { "Microsoft:AspNetCore:Server:Kestrel:Endpoints:0:Port", "0" }, + { "Microsoft:AspNetCore:Server:Kestrel:Endpoints:0:Certificate", "TestCert" }, + { "Certificates:TestCert:Source", "File" }, + { "Certificates:TestCert:Path", "testCert.pfx" }, + { "Certificates:TestCert:Password", "testPassword" } + }).Build()) + .ConfigureServices(services => + { + // Microsoft.AspNetCore.dll does this + services.AddTransient(typeof(IConfigureOptions<>), typeof(ConfigureDefaults<>)); + }) + .Configure(ConfigureEchoAddress); + + using (var webHost = hostBuilder.Start()) + { + var port = GetWebHostPort(webHost); + + var response = await HttpClientSlim.GetStringAsync($"https://127.0.0.1:{port}", validateCertificate: false); + Assert.Equal($"https://127.0.0.1:{port}/", response); + } + } + + [Fact] + public async Task BindsKestrelHttpsEndPointFromConfiguration_InlineCertificateFile() + { + var hostBuilder = new WebHostBuilder() + .UseKestrel() + .UseConfiguration(new ConfigurationBuilder().AddInMemoryCollection(new Dictionary() + { + { "Microsoft:AspNetCore:Server:Kestrel:Endpoints:0:Address", "127.0.0.1" }, + { "Microsoft:AspNetCore:Server:Kestrel:Endpoints:0:Port", "0" }, + { "Microsoft:AspNetCore:Server:Kestrel:Endpoints:0:Certificate:Source", "File" }, + { "Microsoft:AspNetCore:Server:Kestrel:Endpoints:0:Certificate:Path", "testCert.pfx" }, + { "Microsoft:AspNetCore:Server:Kestrel:Endpoints:0:Certificate:Password", "testPassword" } + }).Build()) + .ConfigureServices(services => + { + // Microsoft.AspNetCore.dll does this + services.AddTransient(typeof(IConfigureOptions<>), typeof(ConfigureDefaults<>)); + }) + .Configure(ConfigureEchoAddress); + + using (var webHost = hostBuilder.Start()) + { + var port = GetWebHostPort(webHost); + + var response = await HttpClientSlim.GetStringAsync($"https://127.0.0.1:{port}", validateCertificate: false); + Assert.Equal($"https://127.0.0.1:{port}/", response); + } + } + + private static int GetWebHostPort(IWebHost webHost) + => webHost.ServerFeatures.Get().Addresses + .Select(serverAddress => new Uri(serverAddress).Port) + .Single(); + } +}