diff --git a/MetaPackages.sln b/MetaPackages.sln index 5fbde25dd9..de2568595f 100644 --- a/MetaPackages.sln +++ b/MetaPackages.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26419.0 +VisualStudioVersion = 15.0.26424.2 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{ED834E68-51C3-4ADE-ACC8-6BA6D4207C09}" EndProject @@ -24,9 +24,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{192F EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleApp", "samples\SampleApp\SampleApp.csproj", "{AF5BB04E-92F7-4737-8B98-F86F6244FAB2}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AppSettings", "samples\AppSettings\AppSettings.csproj", "{5009D7C8-6061-49CF-9A30-23B309BBEFB0}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{9E49B5B9-9E72-42FB-B684-90CA1B1BCF9C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.FunctionalTests", "test\Microsoft.AspNetCore.Tests\Microsoft.AspNetCore.FunctionalTests.csproj", "{C72A756A-D29D-44C7-83D4-821DBE82DBCA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.FunctionalTests", "test\Microsoft.AspNetCore.FunctionalTests\Microsoft.AspNetCore.FunctionalTests.csproj", "{C72A756A-D29D-44C7-83D4-821DBE82DBCA}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestSites", "TestSites", "{EC22261D-0DE1-47DE-8F7C-072675D6F5B4}" EndProject @@ -38,6 +40,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StartRequestDelegateUrlApp" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CreateDefaultBuilderApp", "test\TestSites\CreateDefaultBuilderApp\CreateDefaultBuilderApp.csproj", "{79CF58CE-B020-45D8-BDB5-2D8036BEAD14}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestArtifacts", "TestArtifacts", "{9BBA7A0A-109A-4AC8-B6EF-A52EA7CF1D90}" + ProjectSection(SolutionItems) = preProject + test\TestArtifacts\testCert.pfx = test\TestArtifacts\testCert.pfx + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -56,6 +63,10 @@ Global {AF5BB04E-92F7-4737-8B98-F86F6244FAB2}.Debug|Any CPU.Build.0 = Debug|Any CPU {AF5BB04E-92F7-4737-8B98-F86F6244FAB2}.Release|Any CPU.ActiveCfg = Release|Any CPU {AF5BB04E-92F7-4737-8B98-F86F6244FAB2}.Release|Any CPU.Build.0 = Release|Any CPU + {5009D7C8-6061-49CF-9A30-23B309BBEFB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5009D7C8-6061-49CF-9A30-23B309BBEFB0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5009D7C8-6061-49CF-9A30-23B309BBEFB0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5009D7C8-6061-49CF-9A30-23B309BBEFB0}.Release|Any CPU.Build.0 = Release|Any CPU {C72A756A-D29D-44C7-83D4-821DBE82DBCA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C72A756A-D29D-44C7-83D4-821DBE82DBCA}.Debug|Any CPU.Build.0 = Debug|Any CPU {C72A756A-D29D-44C7-83D4-821DBE82DBCA}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -85,11 +96,13 @@ Global {CC8F551E-213A-45E8-AECA-507C4DB4F164} = {ED834E68-51C3-4ADE-ACC8-6BA6D4207C09} {F92CB7A1-C38E-408C-A7EC-A5C040D041E1} = {97D53BEB-A511-4FBE-B784-AB407D9A219F} {AF5BB04E-92F7-4737-8B98-F86F6244FAB2} = {192F583C-C4CA-43E5-B31C-D21B7806E274} + {5009D7C8-6061-49CF-9A30-23B309BBEFB0} = {192F583C-C4CA-43E5-B31C-D21B7806E274} {C72A756A-D29D-44C7-83D4-821DBE82DBCA} = {9E49B5B9-9E72-42FB-B684-90CA1B1BCF9C} {EC22261D-0DE1-47DE-8F7C-072675D6F5B4} = {9E49B5B9-9E72-42FB-B684-90CA1B1BCF9C} {AB42054B-1801-4FEE-B5C3-8529C6D7BFDA} = {EC22261D-0DE1-47DE-8F7C-072675D6F5B4} {3A85FA52-F601-422E-A42E-9F187DB28492} = {EC22261D-0DE1-47DE-8F7C-072675D6F5B4} {401C741B-6C7C-4E08-9F09-C3D43D22C0DE} = {EC22261D-0DE1-47DE-8F7C-072675D6F5B4} {79CF58CE-B020-45D8-BDB5-2D8036BEAD14} = {EC22261D-0DE1-47DE-8F7C-072675D6F5B4} + {9BBA7A0A-109A-4AC8-B6EF-A52EA7CF1D90} = {9E49B5B9-9E72-42FB-B684-90CA1B1BCF9C} EndGlobalSection EndGlobal diff --git a/samples/AppSettings/AppSettings.csproj b/samples/AppSettings/AppSettings.csproj new file mode 100644 index 0000000000..bf6be606f3 --- /dev/null +++ b/samples/AppSettings/AppSettings.csproj @@ -0,0 +1,25 @@ + + + + + + netcoreapp2.0 + aspnetcore-MetaPackagesAppSettings-20170421155031 + + + + + + + + + + + + + + diff --git a/samples/AppSettings/Program.cs b/samples/AppSettings/Program.cs new file mode 100644 index 0000000000..5db5710d52 --- /dev/null +++ b/samples/AppSettings/Program.cs @@ -0,0 +1,21 @@ +// 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 Microsoft.AspNetCore; +using Microsoft.AspNetCore.Http; + +namespace AppSettings +{ + public class Program + { + public static void Main(string[] args) + { + using (WebHost.Start(context => context.Response.WriteAsync("Hello, World!"))) + { + Console.WriteLine("Running application: Press any key to shutdown..."); + Console.ReadKey(); + } + } + } +} diff --git a/samples/AppSettings/Properties/launchSettings.json b/samples/AppSettings/Properties/launchSettings.json new file mode 100644 index 0000000000..fe806c5dc4 --- /dev/null +++ b/samples/AppSettings/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:53434/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "AppSettings": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:53435" + } + } +} diff --git a/samples/AppSettings/appsettings.json b/samples/AppSettings/appsettings.json new file mode 100644 index 0000000000..da7253554d --- /dev/null +++ b/samples/AppSettings/appsettings.json @@ -0,0 +1,63 @@ +{ + "Kestrel": { + "EndPoints": { + "Http": { + "Address": "127.0.0.1", + "Port": 8081 + }, + "HttpV6": { + "Address": "::1", + "Port": 8081 + }, + // Add testCert.pfx to the current user's certificate store to enable this scenario. + //"HttpsInlineCertStore": { + // "Address": "127.0.0.1", + // "Port": 8082, + // "Certificate": { + // "Source": "Store", + // "Subject": "cn=localhost", + // "StoreName": "My", + // "StoreLocation": "CurrentUser", + // "AllowInvalid": "True" + // } + //}, + "HttpsInlineCertFile": { + "Address": "127.0.0.1", + "Port": 8083, + "Certificate": { + "Source": "File", + "Path": "testCert.pfx", + // TODO: remove when dotnet user-secrets is working again + "Password": "testPassword", + } + }, + // Add testCert.pfx to the current user's certificate store to enable this scenario. + //"HttpsCertStore": { + // "Address": "127.0.0.1", + // "Port": 8084, + // "Certificate": "TestCertInStore" + //}, + "HttpsCertFile": { + "Address": "127.0.0.1", + "Port": 8085, + "Certificate": "TestCert" + } + } + }, + "Certificates": { + "TestCert": { + "Source": "File", + "Path": "testCert.pfx", + // TODO: remove when dotnet user-secrets is working again + "Password": "testPassword" + }, + // Add testCert.pfx to the current user's certificate store to enable this scenario. + //"TestCertInStore": { + // "Source": "Store", + // "Subject": "cn=localhost", + // "StoreName": "My", + // "StoreLocation": "CurrentUser", + // "AllowInvalid": "True" + //} + } +} diff --git a/samples/AppSettings/testCert.pfx b/samples/AppSettings/testCert.pfx new file mode 100644 index 0000000000..7118908c2d Binary files /dev/null and b/samples/AppSettings/testCert.pfx differ diff --git a/samples/SampleApp/SampleApp.csproj b/samples/SampleApp/SampleApp.csproj index 6f2a348a9f..37ac635939 100644 --- a/samples/SampleApp/SampleApp.csproj +++ b/samples/SampleApp/SampleApp.csproj @@ -1,6 +1,6 @@  - + netcoreapp2.0 diff --git a/src/Microsoft.AspNetCore/CertificateLoader.cs b/src/Microsoft.AspNetCore/CertificateLoader.cs new file mode 100644 index 0000000000..95339040a1 --- /dev/null +++ b/src/Microsoft.AspNetCore/CertificateLoader.cs @@ -0,0 +1,173 @@ +// 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.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.Configuration; + +namespace Microsoft.AspNetCore +{ + /// + /// A helper class to load certificates from files and certificate stores based on data. + /// + public static class CertificateLoader + { + /// + /// Loads one or more certificates from a single source. + /// + /// An with information about a certificate source. + /// The certificate password, in case it's being loaded from a file. + /// The loaded certificates. + public static X509Certificate2 Load(IConfiguration certificateConfiguration, string password) + { + var sourceKind = certificateConfiguration.GetValue("Source"); + + CertificateSource certificateSource; + switch (sourceKind.ToLowerInvariant()) + { + case "file": + certificateSource = new CertificateFileSource(password); + break; + case "store": + certificateSource = new CertificateStoreSource(); + break; + default: + throw new InvalidOperationException($"Invalid certificate source kind: {sourceKind}"); + } + + certificateConfiguration.Bind(certificateSource); + return certificateSource.Load(); + } + + /// + /// Loads all certificates specified in an . + /// + /// The root . + /// + /// A dictionary mapping certificate names to loaded certificates. + /// + public static Dictionary LoadAll(IConfiguration configurationRoot) + { + return configurationRoot.GetSection("Certificates").GetChildren() + .ToDictionary( + certificateSource => certificateSource.Key, + certificateSource => Load(certificateSource, certificateSource["Password"])); + } + + private abstract class CertificateSource + { + public string Source { get; set; } + + public abstract X509Certificate2 Load(); + } + + private class CertificateFileSource : CertificateSource + { + private readonly string _password; + + public CertificateFileSource(string password) + { + _password = password; + } + + public string Path { get; set; } + + public override X509Certificate2 Load() + { + var certificate = TryLoad(X509KeyStorageFlags.DefaultKeySet, out var error) + ?? TryLoad(X509KeyStorageFlags.UserKeySet, out error) + #if NETCOREAPP2_0 + ?? TryLoad(X509KeyStorageFlags.EphemeralKeySet, out error) + #endif + ; + + if (error != null) + { + throw error; + } + + return certificate; + } + + private X509Certificate2 TryLoad(X509KeyStorageFlags flags, out Exception exception) + { + try + { + var loadedCertificate = new X509Certificate2(Path, _password, flags); + exception = null; + return loadedCertificate; + } + catch (Exception e) + { + exception = e; + return null; + } + } + } + + private class CertificateStoreSource : CertificateSource + { + public string Subject { get; set; } + public string StoreName { get; set; } + public string StoreLocation { get; set; } + public bool AllowInvalid { get; set; } + + public override X509Certificate2 Load() + { + if (!Enum.TryParse(StoreLocation, ignoreCase: true, result: out StoreLocation storeLocation)) + { + throw new InvalidOperationException($"Invalid store location: {StoreLocation}"); + } + + using (var store = new X509Store(StoreName, storeLocation)) + { + X509Certificate2Collection storeCertificates = null; + X509Certificate2Collection foundCertificates = null; + X509Certificate2 foundCertificate = null; + + try + { + store.Open(OpenFlags.ReadOnly); + storeCertificates = store.Certificates; + foundCertificates = storeCertificates.Find(X509FindType.FindBySubjectDistinguishedName, Subject, validOnly: !AllowInvalid); + foundCertificate = foundCertificates + .OfType() + .OrderByDescending(certificate => certificate.NotAfter) + .FirstOrDefault(); + + if (foundCertificate == null) + { + throw new InvalidOperationException($"No certificate found for {Subject} in store {StoreName} in {StoreLocation}"); + } + + return foundCertificate; + } + finally + { + if (foundCertificate != null) + { + storeCertificates.Remove(foundCertificate); + foundCertificates.Remove(foundCertificate); + } + + DisposeCertificates(storeCertificates); + DisposeCertificates(foundCertificates); + } + } + } + + private void DisposeCertificates(X509Certificate2Collection certificates) + { + if (certificates != null) + { + foreach (var certificate in certificates) + { + certificate.Dispose(); + } + } + } + } + } +} diff --git a/src/Microsoft.AspNetCore/KestrelServerOptionsSetup.cs b/src/Microsoft.AspNetCore/KestrelServerOptionsSetup.cs new file mode 100644 index 0000000000..888ece3903 --- /dev/null +++ b/src/Microsoft.AspNetCore/KestrelServerOptionsSetup.cs @@ -0,0 +1,103 @@ +// 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.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Https; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore +{ + /// + /// Binds Kestrel configuration. + /// + public class KestrelServerOptionsSetup : IConfigureOptions + { + private readonly IConfiguration _configurationRoot; + + /// + /// Creates a new instance of . + /// + /// The root . + public KestrelServerOptionsSetup(IConfiguration configurationRoot) + { + _configurationRoot = configurationRoot; + } + + /// + /// Configures a instance. + /// + /// The to configure. + public void Configure(KestrelServerOptions options) + { + BindConfiguration(options); + } + + private void BindConfiguration(KestrelServerOptions options) + { + var certificates = CertificateLoader.LoadAll(_configurationRoot); + var endPoints = _configurationRoot.GetSection("Kestrel:EndPoints"); + + foreach (var endPoint in endPoints.GetChildren()) + { + BindEndPoint(options, endPoint, certificates); + } + } + + private void BindEndPoint( + KestrelServerOptions options, + IConfigurationSection endPoint, + Dictionary certificates) + { + var addressValue = endPoint.GetValue("Address"); + var portValue = endPoint.GetValue("Port"); + + IPAddress address; + if (!IPAddress.TryParse(addressValue, out address)) + { + throw new InvalidOperationException($"Invalid IP address: {addressValue}"); + } + + int port; + if (!int.TryParse(portValue, out port)) + { + throw new InvalidOperationException($"Invalid port: {portValue}"); + } + + options.Listen(address, port, listenOptions => + { + var certificateName = endPoint.GetValue("Certificate"); + + X509Certificate2 endPointCertificate = null; + if (certificateName != null) + { + if (!certificates.TryGetValue(certificateName, out endPointCertificate)) + { + throw new InvalidOperationException($"No certificate named {certificateName} found in configuration"); + } + } + else + { + var certificate = endPoint.GetSection("Certificate"); + + if (certificate.GetChildren().Any()) + { + endPointCertificate = CertificateLoader.Load(certificate, certificate["Password"]); + } + } + + if (endPointCertificate != null) + { + listenOptions.UseHttps(endPointCertificate); + } + }); + } + } +} diff --git a/src/Microsoft.AspNetCore/Microsoft.AspNetCore.csproj b/src/Microsoft.AspNetCore/Microsoft.AspNetCore.csproj index cae574de41..5350bf93a3 100644 --- a/src/Microsoft.AspNetCore/Microsoft.AspNetCore.csproj +++ b/src/Microsoft.AspNetCore/Microsoft.AspNetCore.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Microsoft.AspNetCore/WebHost.cs b/src/Microsoft.AspNetCore/WebHost.cs index 5bd9d11ba9..5cc626fd5e 100644 --- a/src/Microsoft.AspNetCore/WebHost.cs +++ b/src/Microsoft.AspNetCore/WebHost.cs @@ -8,9 +8,11 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore { @@ -182,6 +184,7 @@ namespace Microsoft.AspNetCore .ConfigureServices(services => { services.AddSingleton(); + services.AddTransient, KestrelServerOptionsSetup>(); }); return builder; diff --git a/test/Microsoft.AspNetCore.Tests/Microsoft.AspNetCore.FunctionalTests.csproj b/test/Microsoft.AspNetCore.FunctionalTests/Microsoft.AspNetCore.FunctionalTests.csproj similarity index 75% rename from test/Microsoft.AspNetCore.Tests/Microsoft.AspNetCore.FunctionalTests.csproj rename to test/Microsoft.AspNetCore.FunctionalTests/Microsoft.AspNetCore.FunctionalTests.csproj index d29ae617ca..a4676dcc7f 100644 --- a/test/Microsoft.AspNetCore.Tests/Microsoft.AspNetCore.FunctionalTests.csproj +++ b/test/Microsoft.AspNetCore.FunctionalTests/Microsoft.AspNetCore.FunctionalTests.csproj @@ -6,6 +6,10 @@ netcoreapp2.0 + + + + @@ -17,4 +21,8 @@ + + + + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Tests/WebHostFunctionalTests.cs b/test/Microsoft.AspNetCore.FunctionalTests/WebHostFunctionalTests.cs similarity index 58% rename from test/Microsoft.AspNetCore.Tests/WebHostFunctionalTests.cs rename to test/Microsoft.AspNetCore.FunctionalTests/WebHostFunctionalTests.cs index f925eb91f4..99485cab7d 100644 --- a/test/Microsoft.AspNetCore.Tests/WebHostFunctionalTests.cs +++ b/test/Microsoft.AspNetCore.FunctionalTests/WebHostFunctionalTests.cs @@ -4,9 +4,12 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net.Http; -using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.IntegrationTesting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; @@ -74,6 +77,129 @@ namespace Microsoft.AspNetCore.Tests }, setTestEnvVars: true); } + [Theory] + [InlineData("127.0.0.1", "127.0.0.1")] + [InlineData("::1", "[::1]")] + public async Task BindsKestrelHttpEndPointFromConfiguration(string endPointAddress, string requestAddress) + { + try + { + File.WriteAllText("appsettings.json", @" +{ + ""Kestrel"": { + ""EndPoints"": { + ""EndPoint"": { + ""Address"": """ + endPointAddress + @""", + ""Port"": 0 + } + } + } +} +"); + using (var webHost = WebHost.Start(context => context.Response.WriteAsync("Hello, World!"))) + { + var port = GetWebHostPort(webHost); + + Assert.NotEqual(0, port); + + using (var client = new HttpClient()) + { + var response = await client.GetAsync($"http://{requestAddress}:{port}"); + response.EnsureSuccessStatusCode(); + } + } + } + finally + { + File.Delete("appsettings.json"); + } + } + + [Fact] + public async Task BindsKestrelHttpsEndPointFromConfiguration_ReferencedCertificateFile() + { + try + { + File.WriteAllText("appsettings.json", @" +{ + ""Kestrel"": { + ""EndPoints"": { + ""EndPoint"": { + ""Address"": ""127.0.0.1"", + ""Port"": 0, + ""Certificate"": ""TestCert"" + } + } + }, + ""Certificates"": { + ""TestCert"": { + ""Source"": ""File"", + ""Path"": ""testCert.pfx"", + ""Password"": ""testPassword"" + } + } +} +"); + using (var webHost = WebHost.Start(context => context.Response.WriteAsync("Hello, World!"))) + { + var port = GetWebHostPort(webHost); + + Assert.NotEqual(0, port); + + using (var client = new HttpClient(new HttpClientHandler { ServerCertificateCustomValidationCallback = (msg, cert, chain, errors) => true })) + { + var response = await client.GetAsync($"https://127.0.0.1:{port}"); + response.EnsureSuccessStatusCode(); + } + } + } + finally + { + File.Delete("appsettings.json"); + } + } + + [Fact] + public async Task BindsKestrelHttpsEndPointFromConfiguration_InlineCertificateFile() + { + try + { + File.WriteAllText("appsettings.json", @" +{ + ""Kestrel"": { + ""EndPoints"": { + ""EndPoint"": { + ""Address"": ""127.0.0.1"", + ""Port"": 0, + ""Certificate"": { + ""Source"": ""File"", + ""Path"": ""testCert.pfx"", + ""Password"": ""testPassword"" + } + } + } + } +} +"); + using (var webHost = WebHost.Start(context => context.Response.WriteAsync("Hello, World!"))) + { + var port = GetWebHostPort(webHost); + + Assert.NotEqual(0, port); + + using (var client = new HttpClient(new HttpClientHandler { ServerCertificateCustomValidationCallback = (msg, cert, chain, errors) => true })) + { + var response = await client.GetAsync($"https://127.0.0.1:{port}"); + response.EnsureSuccessStatusCode(); + } + } + } + finally + { + File.Delete("appsettings.json"); + } + } + private async Task ExecuteStartOrStartWithTest(Func> getResponse, string applicationName) { await ExecuteTestApp(applicationName, async (deploymentResult, logger) => @@ -135,5 +261,10 @@ namespace Microsoft.AspNetCore.Tests throw new Exception($"Solution root could not be found using {applicationBasePath}"); } + + private static int GetWebHostPort(IWebHost webHost) + => webHost.ServerFeatures.Get().Addresses + .Select(serverAddress => new Uri(serverAddress).Port) + .FirstOrDefault(); } } diff --git a/test/TestArtifacts/testCert.pfx b/test/TestArtifacts/testCert.pfx new file mode 100644 index 0000000000..7118908c2d Binary files /dev/null and b/test/TestArtifacts/testCert.pfx differ